Files
rebbarb/exi_bba/w5100_parallel_master.py
T
2026-06-13 18:35:38 +02:00

841 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""W5100 parallel-bus master — sync domain.
A drop-in alternative to `W5500SPIMaster` that talks to a WIZnet **W5100** over
its **indirect parallel bus** instead of SPI. The external streaming interface
(init_req/init_done/par, tx_*, rx_*) is identical, so BBATop wiring is unchanged;
only the physical pins differ (a parallel bus instead of 4 SPI wires).
Why parallel
------------
SPI serialises 8 bits per byte, so on this UP5K (whose W5500-operating logic
closes only ~40 MHz) the SPI byte rate caps at ~12 Mbit/s. A parallel bus moves
a whole byte per access, so the same ~24 MHz sync logic clears the 27 Mbit/s EXI
ceiling — the real hard limit — with margin. See CLAUDE.md.
W5100 indirect bus interface (IDM)
----------------------------------
Only two address lines A[1:0] are wired (the upper address lines are tied to 0
on the board, so a power-up *direct*-mode access at A=00 still lands on MR):
A[1:0] register
00 MR (Mode Register — also reachable directly at power-up)
01 IDM_AR0 (indirect address, high byte)
10 IDM_AR1 (indirect address, low byte)
11 IDM_DR (indirect data — accesses mem[IDM_AR]; auto-increments
IDM_AR when MR.AI is set)
So a register/buffer access is: write IDM_AR0/AR1 with the 16-bit address, then
read/write IDM_DR. With MR.AI=1 a multi-byte block is one address-set followed
by a burst of IDM_DR accesses (the chip auto-increments) — used for SHAR and for
streaming frame data.
A bus cycle drives A + (for writes) D with /CS and /RD or /WR asserted for
`strobe_cycles` sync clocks (≥ the W5100's ~80 ns access time at 24 MHz).
Phase status
------------
Phase 1 (this file): bus access engine + transaction engine + init sequence,
verified against a W5100 bus model. TX/RX MACRAW (with socket-buffer ring
wraparound) land in phases 23.
"""
from amaranth import *
__all__ = ["W5100ParallelMaster"]
# ── W5100 register addresses (indirect 16-bit address space) ────────────────
_MR = 0x0000 # Mode register (common)
_SHAR0 = 0x0009 # Source MAC, 6 bytes
_IR = 0x0015 # Interrupt register
_IMR = 0x0016 # Interrupt mask
_RMSR = 0x001A # RX memory size (2 bits/socket)
_TMSR = 0x001B # TX memory size
_S0_MR = 0x0400 # Socket 0 mode
_S0_CR = 0x0401 # Socket 0 command
_S0_IR = 0x0402 # Socket 0 interrupt
_S0_SR = 0x0403 # Socket 0 status
_S0_TX_FSR = 0x0420 # Socket 0 TX free size (2 bytes)
_S0_TX_RD = 0x0422 # Socket 0 TX read pointer
_S0_TX_WR = 0x0424 # Socket 0 TX write pointer
_S0_RX_RSR = 0x0426 # Socket 0 RX received size (2 bytes)
_S0_RX_RD = 0x0428 # Socket 0 RX read pointer
_TX_BASE = 0x4000 # Socket 0 TX buffer base (default 2 KB window)
_RX_BASE = 0x6000 # Socket 0 RX buffer base
_S0_TX_MASK = 0x07FF # 2 KB ring mask
_S0_RX_MASK = 0x07FF
# MR bits / command / mode values
_MR_RST = 0x80
_MR_AI = 0x02 # address auto-increment (indirect mode)
_MR_IND = 0x01 # indirect bus interface mode
_S0_MR_MACRAW = 0x04
_CR_OPEN = 0x01
_CR_SEND = 0x20
_CR_RECV = 0x40
# Indirect-mode address selects (A[1:0])
_A_MR = 0b00
_A_AR0 = 0b01 # IDM_AR high byte
_A_AR1 = 0b10 # IDM_AR low byte
_A_DR = 0b11 # IDM_DR (data)
class W5100ParallelMaster(Elaboratable):
"""W5100 master over the indirect parallel bus, sync clock domain.
Physical bus pins
-----------------
bus_addr : A[1:0] output
bus_data_o : D[7:0] output value (drive when bus_data_oe=1)
bus_data_oe: data-bus output enable (1=FPGA drives D, 0=W5100 drives D)
bus_data_i : D[7:0] input value (sampled during reads)
cs_n / rd_n / wr_n : bus control (active low)
w5100_int_n : W5100 INT_N input (active low)
w5100_rst_n : W5100 hardware reset (active low)
Init / TX / RX interfaces are identical to W5500SPIMaster.
"""
def __init__(self, strobe_cycles=3, reset_cycles=24000):
# /RD//WR strobe width in sync cycles (≥ W5100 access time).
self._strobe = strobe_cycles
# MR-reset settle wait; testbench overrides with a small value.
self._reset_cycles = reset_cycles
# Physical parallel bus
self.bus_addr = Signal(2)
self.bus_data_o = Signal(8)
self.bus_data_oe = Signal()
self.bus_data_i = Signal(8)
self.cs_n = Signal(init=1)
self.rd_n = Signal(init=1)
self.wr_n = Signal(init=1)
self.w5100_int_n = Signal(init=1)
self.w5100_rst_n = Signal(init=1)
# Init control
self.init_req = Signal()
self.init_done = Signal()
self.par = Signal(48) # MAC address (PAR0..5 packed)
# TX stream
self.tx_data = Signal(8)
self.tx_valid = Signal()
self.tx_ready = Signal()
self.tx_sof = Signal()
self.tx_eof = Signal()
# RX stream
self.rx_data = Signal(8)
self.rx_valid = Signal()
self.rx_ready = Signal()
self.rx_sof = Signal()
self.rx_eof = Signal()
def elaborate(self, platform):
m = Module()
STROBE = self._strobe
# ── Bus access engine: one indirect-bus read or write cycle ──────────
bus_go = Signal()
bus_rw = Signal() # 1 = write, 0 = read
bus_a = Signal(2)
bus_wdata = Signal(8)
bus_rdata = Signal(8)
bus_done = Signal()
bus_ctr = Signal(range(STROBE + 2))
rw_r = Signal()
# registered physical outputs
a_o = Signal(2)
d_o = Signal(8)
d_oe = Signal()
cs_r = Signal(init=1)
rd_r = Signal(init=1)
wr_r = Signal(init=1)
m.d.comb += [
self.bus_addr .eq(a_o),
self.bus_data_o .eq(d_o),
self.bus_data_oe.eq(d_oe),
self.cs_n .eq(cs_r),
self.rd_n .eq(rd_r),
self.wr_n .eq(wr_r),
]
m.d.sync += bus_done.eq(0)
with m.FSM(domain="sync", name="bus_fsm"):
with m.State("IDLE"):
m.d.sync += [cs_r.eq(1), rd_r.eq(1), wr_r.eq(1), d_oe.eq(0)]
with m.If(bus_go):
m.d.sync += [a_o.eq(bus_a), rw_r.eq(bus_rw),
cs_r.eq(0), bus_ctr.eq(0)]
with m.If(bus_rw):
m.d.sync += [d_o.eq(bus_wdata), d_oe.eq(1), wr_r.eq(0)]
with m.Else():
m.d.sync += rd_r.eq(0)
m.next = "STROBE"
with m.State("STROBE"):
m.d.sync += bus_ctr.eq(bus_ctr + 1)
with m.If(bus_ctr == STROBE - 1):
with m.If(~rw_r):
m.d.sync += bus_rdata.eq(self.bus_data_i) # sample read
m.d.sync += [rd_r.eq(1), wr_r.eq(1)]
m.next = "FINISH"
with m.State("FINISH"):
m.d.sync += [cs_r.eq(1), d_oe.eq(0), bus_done.eq(1)]
m.next = "IDLE"
# ── Transaction engine: address-set + payload over the bus engine ────
WBUF = 8
xfer_start = Signal()
xfer_direct = Signal() # 1 = single A=00 access (MR), addr ignored
xfer_addr = Signal(16)
xfer_rw = Signal() # payload direction: 1=write, 0=read
xfer_len = Signal(range(WBUF + 1))
xfer_stream = Signal() # stream-write payload from s_*
xfer_sread = Signal() # stream-read payload to r_*
xfer_rcount = Signal(16)
xfer_done = Signal()
wbuf = Array([Signal(8, name=f"wbuf{i}") for i in range(WBUF)])
rbuf = Array([Signal(8, name=f"rbuf{i}") for i in range(WBUF)])
s_count = Signal(16) # bytes streamed-written (advances pointers)
xfer_idx = Signal(range(WBUF + 1))
s_last_r = Signal()
r_idx = Signal(16)
# Streaming payload interfaces.
s_data, s_valid, s_last, s_consume = Signal(8), Signal(), Signal(), Signal()
r_data, r_valid, r_first, r_last, r_ready = (
Signal(8), Signal(), Signal(), Signal(), Signal())
# TX stream source = external tx interface (Phase 2).
m.d.comb += [s_data.eq(self.tx_data), s_valid.eq(self.tx_valid),
s_last.eq(self.tx_eof), self.tx_ready.eq(s_consume)]
# RX stream sink = external rx interface (Phase 3).
m.d.comb += [self.rx_data.eq(r_data), self.rx_valid.eq(r_valid),
self.rx_sof.eq(r_first), self.rx_eof.eq(r_last),
r_ready.eq(self.rx_ready)]
# Socket-buffer ring wraparound. Unlike the W5500, the W5100's IDM
# address does NOT auto-wrap at the socket-buffer boundary — it just
# increments linearly into the next region. So when a streamed access
# reaches `xfer_wend`, the engine re-sets IDM_AR back to `xfer_wbase`.
xfer_wrap = Signal()
xfer_wbase = Signal(16)
xfer_wend = Signal(16)
cur_addr = Signal(16)
m.d.comb += [bus_go.eq(0), bus_rw.eq(0), bus_a.eq(0), bus_wdata.eq(0)]
m.d.comb += [s_consume.eq(0), r_valid.eq(0), r_data.eq(0),
r_first.eq(0), r_last.eq(0)]
m.d.sync += xfer_done.eq(0)
def bus_write(a, data):
m.d.comb += [bus_go.eq(1), bus_rw.eq(1), bus_a.eq(a), bus_wdata.eq(data)]
def bus_read(a):
m.d.comb += [bus_go.eq(1), bus_rw.eq(0), bus_a.eq(a)]
with m.FSM(domain="sync", name="xfer_fsm"):
with m.State("IDLE"):
with m.If(xfer_start):
m.d.sync += [xfer_idx.eq(0), s_count.eq(0), r_idx.eq(0),
cur_addr.eq(xfer_addr)]
with m.If(xfer_direct):
m.next = "DIRECT"
with m.Else():
m.next = "AR_HI"
# Direct MR write (A=00)
with m.State("DIRECT"):
bus_write(_A_MR, wbuf[0])
m.next = "DIRECT_W"
with m.State("DIRECT_W"):
with m.If(bus_done):
m.next = "FINISH"
# Set indirect address IDM_AR (high then low)
with m.State("AR_HI"):
bus_write(_A_AR0, xfer_addr[8:16])
m.next = "AR_HI_W"
with m.State("AR_HI_W"):
with m.If(bus_done):
m.next = "AR_LO"
with m.State("AR_LO"):
bus_write(_A_AR1, xfer_addr[0:8])
m.next = "AR_LO_W"
with m.State("AR_LO_W"):
with m.If(bus_done):
with m.If(xfer_stream):
m.next = "SW_LOAD"
with m.Elif(xfer_sread):
m.next = "SR_LOAD"
with m.Elif(xfer_rw):
m.next = "WB_ISSUE"
with m.Else():
m.next = "RB_ISSUE"
# Fixed-length write from wbuf (IDM_DR burst, auto-increment)
with m.State("WB_ISSUE"):
bus_write(_A_DR, wbuf[xfer_idx])
m.next = "WB_WAIT"
with m.State("WB_WAIT"):
with m.If(bus_done):
m.d.sync += xfer_idx.eq(xfer_idx + 1)
with m.If(xfer_idx + 1 == xfer_len):
m.next = "FINISH"
with m.Else():
m.next = "WB_ISSUE"
# Fixed-length read into rbuf (with ring wrap, for the length header)
with m.State("RB_ISSUE"):
with m.If(xfer_wrap & (cur_addr == xfer_wend)):
m.next = "RB_WRAP_HI"
with m.Else():
bus_read(_A_DR)
m.next = "RB_WAIT"
with m.State("RB_WAIT"):
with m.If(bus_done):
m.d.sync += rbuf[xfer_idx].eq(bus_rdata)
m.d.sync += [xfer_idx.eq(xfer_idx + 1), cur_addr.eq(cur_addr + 1)]
with m.If(xfer_idx + 1 == xfer_len):
m.next = "FINISH"
with m.Else():
m.next = "RB_ISSUE"
with m.State("RB_WRAP_HI"):
bus_write(_A_AR0, xfer_wbase[8:16])
m.next = "RB_WRAP_HI_W"
with m.State("RB_WRAP_HI_W"):
with m.If(bus_done):
m.next = "RB_WRAP_LO"
with m.State("RB_WRAP_LO"):
bus_write(_A_AR1, xfer_wbase[0:8])
m.next = "RB_WRAP_LO_W"
with m.State("RB_WRAP_LO_W"):
with m.If(bus_done):
m.d.sync += cur_addr.eq(xfer_wbase)
m.next = "RB_ISSUE"
# Stream-write payload from s_* until s_last (with ring wrap)
with m.State("SW_LOAD"):
with m.If(xfer_wrap & (cur_addr == xfer_wend)):
m.next = "SW_WRAP_HI"
with m.Elif(s_valid):
bus_write(_A_DR, s_data)
m.d.sync += s_last_r.eq(s_last)
m.next = "SW_WAIT"
with m.State("SW_WAIT"):
with m.If(bus_done):
m.d.comb += s_consume.eq(1)
m.d.sync += [s_count.eq(s_count + 1), cur_addr.eq(cur_addr + 1)]
with m.If(s_last_r):
m.next = "FINISH"
with m.Else():
m.next = "SW_LOAD"
with m.State("SW_WRAP_HI"):
bus_write(_A_AR0, xfer_wbase[8:16])
m.next = "SW_WRAP_HI_W"
with m.State("SW_WRAP_HI_W"):
with m.If(bus_done):
m.next = "SW_WRAP_LO"
with m.State("SW_WRAP_LO"):
bus_write(_A_AR1, xfer_wbase[0:8])
m.next = "SW_WRAP_LO_W"
with m.State("SW_WRAP_LO_W"):
with m.If(bus_done):
m.d.sync += cur_addr.eq(xfer_wbase)
m.next = "SW_LOAD"
# Stream-read payload to r_* for rcount bytes (with ring wrap)
with m.State("SR_LOAD"):
with m.If(r_idx == xfer_rcount):
m.next = "FINISH"
with m.Elif(xfer_wrap & (cur_addr == xfer_wend)):
m.next = "SR_WRAP_HI"
with m.Else():
bus_read(_A_DR)
m.next = "SR_WAIT"
with m.State("SR_WAIT"):
with m.If(bus_done):
m.next = "SR_PUSH"
with m.State("SR_PUSH"):
m.d.comb += [r_data.eq(bus_rdata), r_valid.eq(1),
r_first.eq(r_idx == 0),
r_last.eq(r_idx + 1 == xfer_rcount)]
with m.If(r_ready):
m.d.sync += [r_idx.eq(r_idx + 1), cur_addr.eq(cur_addr + 1)]
m.next = "SR_LOAD"
with m.State("SR_WRAP_HI"):
bus_write(_A_AR0, xfer_wbase[8:16])
m.next = "SR_WRAP_HI_W"
with m.State("SR_WRAP_HI_W"):
with m.If(bus_done):
m.next = "SR_WRAP_LO"
with m.State("SR_WRAP_LO"):
bus_write(_A_AR1, xfer_wbase[0:8])
m.next = "SR_WRAP_LO_W"
with m.State("SR_WRAP_LO_W"):
with m.If(bus_done):
m.d.sync += cur_addr.eq(xfer_wbase)
m.next = "SR_LOAD"
with m.State("FINISH"):
m.d.sync += xfer_done.eq(1)
m.next = "IDLE"
# ── Control regs ─────────────────────────────────────────────────────
mac_shadow = Array([Signal(8, name=f"mac{i}") for i in range(6)])
wait_ctr = Signal(range(self._reset_cycles + 2))
tx_wr = Signal(16)
rx_rsr = Signal(16)
rx_rd = Signal(16)
pkt_len = Signal(16)
def write_reg(name, addr, payload, nxt, direct=False):
"""Emit a 2-state block that writes `payload` (a list) to `addr`."""
with m.State(name):
m.d.sync += [xfer_addr.eq(addr), xfer_rw.eq(1),
xfer_stream.eq(0), xfer_sread.eq(0), xfer_wrap.eq(0),
xfer_direct.eq(1 if direct else 0),
xfer_len.eq(len(payload))]
for i, b in enumerate(payload):
m.d.sync += wbuf[i].eq(b)
m.d.sync += xfer_start.eq(1)
m.next = name + "_W"
with m.State(name + "_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.next = nxt
# ── Main control FSM (Phase 1: init only) ────────────────────────────
with m.FSM(domain="sync", name="main_fsm"):
with m.State("IDLE"):
m.d.sync += self.init_done.eq(0)
with m.If(self.init_req):
for i in range(6):
m.d.sync += mac_shadow[i].eq(self.par[i*8:(i+1)*8])
m.next = "MR_RST"
with m.Elif(~self.w5100_int_n):
m.next = "RX_CHECK"
with m.Elif(self.tx_valid & self.tx_sof):
m.next = "TX_START"
# MR = 0x80 software reset (direct A=00), then settle.
write_reg("MR_RST", _MR, [_MR_RST], "MR_WAIT", direct=True)
with m.State("MR_WAIT"):
with m.If(wait_ctr == self._reset_cycles):
m.d.sync += wait_ctr.eq(0)
m.next = "MR_MODE"
with m.Else():
m.d.sync += wait_ctr.eq(wait_ctr + 1)
# MR = indirect + auto-increment (direct A=00).
write_reg("MR_MODE", _MR, [_MR_IND | _MR_AI], "SHAR", direct=True)
# SHAR = source MAC (6-byte auto-increment burst).
with m.State("SHAR"):
m.d.sync += [xfer_addr.eq(_SHAR0), xfer_rw.eq(1),
xfer_stream.eq(0), xfer_sread.eq(0),
xfer_direct.eq(0), xfer_len.eq(6)]
for i in range(6):
m.d.sync += wbuf[i].eq(mac_shadow[i])
m.d.sync += xfer_start.eq(1)
m.next = "SHAR_W"
with m.State("SHAR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.next = "MEMSZ"
# RMSR/TMSR = 0x55 (2 KB per socket — default; socket 0 used).
write_reg("MEMSZ", _RMSR, [0x55, 0x55], "S0_MODE") # RMSR then TMSR
# Socket 0: MACRAW mode, OPEN, enable interrupt.
write_reg("S0_MODE", _S0_MR, [_S0_MR_MACRAW], "S0_OPEN")
write_reg("S0_OPEN", _S0_CR, [_CR_OPEN], "S0_IMR")
write_reg("S0_IMR", _IMR, [0x01], "INIT_DONE") # enable S0 IRQ
with m.State("INIT_DONE"):
m.d.sync += self.init_done.eq(1)
m.next = "IDLE"
# ── TX MACRAW ────────────────────────────────────────────────────
# read S0_TX_WR → stream frame into the TX buffer at that offset
# (ring-wrapping at the 2 KB boundary) → advance S0_TX_WR → SEND.
with m.State("TX_START"): # read S0_TX_WR (2 bytes)
m.d.sync += [xfer_addr.eq(_S0_TX_WR), xfer_rw.eq(0),
xfer_stream.eq(0), xfer_sread.eq(0), xfer_wrap.eq(0),
xfer_direct.eq(0), xfer_len.eq(2)]
m.d.sync += xfer_start.eq(1)
m.next = "TX_RDPTR_W"
with m.State("TX_RDPTR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.d.sync += tx_wr.eq(Cat(rbuf[1], rbuf[0])) # big-endian
m.next = "TX_DATA"
with m.State("TX_DATA"): # stream frame → TX buffer
m.d.sync += [xfer_addr.eq(_TX_BASE + (tx_wr & _S0_TX_MASK)),
xfer_rw.eq(1), xfer_stream.eq(1), xfer_sread.eq(0),
xfer_direct.eq(0), xfer_wrap.eq(1),
xfer_wbase.eq(_TX_BASE),
xfer_wend.eq(_TX_BASE + _S0_TX_MASK + 1)]
m.d.sync += xfer_start.eq(1)
m.next = "TX_DATA_W"
with m.State("TX_DATA_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.d.sync += [xfer_stream.eq(0), xfer_wrap.eq(0),
tx_wr.eq(tx_wr + s_count)] # advanced pointer
m.next = "TX_UPDPTR"
with m.State("TX_UPDPTR"): # write back S0_TX_WR
m.d.sync += [xfer_addr.eq(_S0_TX_WR), xfer_rw.eq(1),
xfer_stream.eq(0), xfer_sread.eq(0), xfer_wrap.eq(0),
xfer_direct.eq(0), xfer_len.eq(2)]
m.d.sync += [wbuf[0].eq(tx_wr[8:16]), wbuf[1].eq(tx_wr[0:8])]
m.d.sync += xfer_start.eq(1)
m.next = "TX_UPDPTR_W"
with m.State("TX_UPDPTR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.next = "TX_SEND"
# S0_CR = SEND
write_reg("TX_SEND", _S0_CR, [_CR_SEND], "IDLE")
# ── RX MACRAW ────────────────────────────────────────────────────
# On W5100 INT: read RX_RSR; if non-zero read RX_RD, read the 2-byte
# MACRAW length, stream (length2) frame bytes out (ring-wrapping),
# advance RX_RD by the length, issue RECV, clear the RECV interrupt.
with m.State("RX_CHECK"): # read S0_RX_RSR (2 bytes)
m.d.sync += [xfer_addr.eq(_S0_RX_RSR), xfer_rw.eq(0),
xfer_stream.eq(0), xfer_sread.eq(0), xfer_wrap.eq(0),
xfer_direct.eq(0), xfer_len.eq(2)]
m.d.sync += xfer_start.eq(1)
m.next = "RX_RSR_W"
with m.State("RX_RSR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.d.sync += rx_rsr.eq(Cat(rbuf[1], rbuf[0]))
m.next = "RX_RSR_CHK"
with m.State("RX_RSR_CHK"):
with m.If(rx_rsr == 0):
m.next = "IDLE" # nothing received
with m.Else():
m.next = "RX_RDPTR"
with m.State("RX_RDPTR"): # read S0_RX_RD (2 bytes)
m.d.sync += [xfer_addr.eq(_S0_RX_RD), xfer_rw.eq(0),
xfer_stream.eq(0), xfer_sread.eq(0), xfer_wrap.eq(0),
xfer_direct.eq(0), xfer_len.eq(2)]
m.d.sync += xfer_start.eq(1)
m.next = "RX_RDPTR_W"
with m.State("RX_RDPTR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.d.sync += rx_rd.eq(Cat(rbuf[1], rbuf[0]))
m.next = "RX_LEN"
with m.State("RX_LEN"): # read 2-byte MACRAW length (wrap)
m.d.sync += [xfer_addr.eq(_RX_BASE + (rx_rd & _S0_RX_MASK)),
xfer_rw.eq(0), xfer_stream.eq(0), xfer_sread.eq(0),
xfer_direct.eq(0), xfer_len.eq(2), xfer_wrap.eq(1),
xfer_wbase.eq(_RX_BASE),
xfer_wend.eq(_RX_BASE + _S0_RX_MASK + 1)]
m.d.sync += xfer_start.eq(1)
m.next = "RX_LEN_W"
with m.State("RX_LEN_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.d.sync += pkt_len.eq(Cat(rbuf[1], rbuf[0]))
m.next = "RX_FRAME"
with m.State("RX_FRAME"): # stream (pkt_len2) frame bytes
m.d.sync += [xfer_addr.eq(_RX_BASE + ((rx_rd + 2) & _S0_RX_MASK)),
xfer_rw.eq(0), xfer_stream.eq(0), xfer_sread.eq(1),
xfer_direct.eq(0), xfer_rcount.eq(pkt_len - 2),
xfer_wrap.eq(1), xfer_wbase.eq(_RX_BASE),
xfer_wend.eq(_RX_BASE + _S0_RX_MASK + 1)]
m.d.sync += xfer_start.eq(1)
m.next = "RX_FRAME_W"
with m.State("RX_FRAME_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.d.sync += [xfer_sread.eq(0), xfer_wrap.eq(0)]
m.next = "RX_UPDRD"
with m.State("RX_UPDRD"): # S0_RX_RD += pkt_len, write back
m.d.sync += [xfer_addr.eq(_S0_RX_RD), xfer_rw.eq(1),
xfer_stream.eq(0), xfer_sread.eq(0), xfer_wrap.eq(0),
xfer_direct.eq(0), xfer_len.eq(2)]
m.d.sync += [wbuf[0].eq((rx_rd + pkt_len)[8:16]),
wbuf[1].eq((rx_rd + pkt_len)[0:8])]
m.d.sync += xfer_start.eq(1)
m.next = "RX_UPDRD_W"
with m.State("RX_UPDRD_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.next = "RX_RECV"
# S0_CR = RECV, then clear the RECV interrupt bit (S0_IR[2]).
write_reg("RX_RECV", _S0_CR, [_CR_RECV], "RX_CLR_IR")
write_reg("RX_CLR_IR", _S0_IR, [0x04], "IDLE")
return m
# ── Testbench ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
from amaranth.sim import Simulator, Period
dut = W5100ParallelMaster(strobe_cycles=3, reset_cycles=10)
errors = []
MAC = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]
PAR = sum(b << (8 * i) for i, b in enumerate(MAC))
# Expected indirect-address writes captured by the model (addr, value).
# MR is written directly (A=00) → captured as ('MR', value).
EXPECTED = [
("MR", _MR_RST),
("MR", _MR_IND | _MR_AI),
(_SHAR0 + 0, MAC[0]), (_SHAR0 + 1, MAC[1]), (_SHAR0 + 2, MAC[2]),
(_SHAR0 + 3, MAC[3]), (_SHAR0 + 4, MAC[4]), (_SHAR0 + 5, MAC[5]),
(_RMSR + 0, 0x55), (_RMSR + 1, 0x55),
(_S0_MR, _S0_MR_MACRAW),
(_S0_CR, _CR_OPEN),
(_IMR, 0x01),
]
writes = [] # captured (addr-or-'MR', value) — IDM_DR + MR writes
model_mem = {} # W5100 memory image (registers + TX/RX buffers)
async def w5100_model(ctx):
"""W5100 indirect-bus slave model: tracks MR/IDM_AR, records IDM_DR and
MR writes, and drives bus_data_i for reads. Mode-0 timing: a write is
latched on /WR rising while /CS low; reads driven while /RD low."""
idm_ar = 0
mr = 0
prev_cs = prev_rd = prev_wr = 1
async for vals in ctx.tick("sync").sample(
dut.cs_n, dut.rd_n, dut.wr_n,
dut.bus_addr, dut.bus_data_o, dut.bus_data_oe):
cs, rd, wr, a, do, doe = vals[-6:]
ai = (mr >> 1) & 1 # MR.AI
# Drive read data while /RD asserted (combinational, before sample).
if cs == 0 and rd == 0:
if a == _A_MR:
val = mr
elif a == _A_AR0:
val = (idm_ar >> 8) & 0xFF
elif a == _A_AR1:
val = idm_ar & 0xFF
else:
val = model_mem.get(idm_ar, 0)
ctx.set(dut.bus_data_i, val)
# Latch write on /WR rising edge.
if cs == 0 and prev_wr == 0 and wr == 1:
if a == _A_MR:
mr = do
writes.append(("MR", do))
elif a == _A_AR0:
idm_ar = (idm_ar & 0x00FF) | (do << 8)
elif a == _A_AR1:
idm_ar = (idm_ar & 0xFF00) | do
else: # IDM_DR
model_mem[idm_ar] = do
writes.append((idm_ar, do))
# RECV command consumes the RX data: clear RSR (mirrors HW).
if idm_ar == _S0_CR and do == _CR_RECV:
model_mem[_S0_RX_RSR] = 0
model_mem[_S0_RX_RSR + 1] = 0
if ai:
idm_ar = (idm_ar + 1) & 0xFFFF
# Auto-increment after a data read (/RD rising, A=DR).
if cs == 0 and prev_rd == 0 and rd == 1 and a == _A_DR and ai:
idm_ar = (idm_ar + 1) & 0xFFFF
prev_cs, prev_rd, prev_wr = cs, rd, wr
async def testbench(ctx):
ctx.set(dut.par, PAR)
await ctx.tick("sync").repeat(2)
# T1: trigger init, wait for init_done.
ctx.set(dut.init_req, 1)
await ctx.tick("sync").repeat(1)
ctx.set(dut.init_req, 0)
done = False
for _ in range(4000):
await ctx.tick("sync").repeat(1)
if ctx.get(dut.init_done):
done = True
break
if not done:
errors.append("init_done never asserted")
print(f"T1 init captured {len(writes)} writes")
if writes != EXPECTED:
errors.append("init write sequence mismatch")
for i in range(max(len(writes), len(EXPECTED))):
g = writes[i] if i < len(writes) else None
e = EXPECTED[i] if i < len(EXPECTED) else None
mark = "" if g == e else " <-- MISMATCH"
gs = f"({g[0]:#06x},{g[1]:#04x})" if g and isinstance(g[0], int) else str(g)
es = f"({e[0]:#06x},{e[1]:#04x})" if e and isinstance(e[0], int) else str(e)
print(f" [{i:2}] got {gs:20} exp {es:20}{mark}")
else:
print("T1 init sequence matches expected (MR, SHAR, mem sizes, "
"S0 MACRAW/OPEN, IMR)")
# ── helper: stream one TX frame through the external tx interface ─────
async def feed_frame(ctx, frame):
for i, b in enumerate(frame):
ctx.set(dut.tx_data, b)
ctx.set(dut.tx_valid, 1)
ctx.set(dut.tx_sof, 1 if i == 0 else 0)
ctx.set(dut.tx_eof, 1 if i == len(frame) - 1 else 0)
got = False
for _ in range(400):
await ctx.tick("sync").repeat(1)
if ctx.get(dut.tx_ready):
got = True
break
if not got:
errors.append(f"feed_frame: byte {i} never consumed")
return
ctx.set(dut.tx_valid, 0)
ctx.set(dut.tx_sof, 0)
ctx.set(dut.tx_eof, 0)
# let TX_UPDPTR + SEND complete
for _ in range(200):
await ctx.tick("sync").repeat(1)
if model_mem.get(_S0_CR) == _CR_SEND:
break
# ── T2: TX MACRAW frame (TX_WR=0, no wrap) ───────────────────────────
FRAME = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x10, 0x20]
await feed_frame(ctx, FRAME)
buf = [model_mem.get(_TX_BASE + i, None) for i in range(len(FRAME))]
if buf != FRAME:
errors.append(f"T2 TX buffer mismatch: {buf} != {FRAME}")
tx_wr_hi = model_mem.get(_S0_TX_WR, 0)
tx_wr_lo = model_mem.get(_S0_TX_WR + 1, 0)
adv = (tx_wr_hi << 8) | tx_wr_lo
if adv != len(FRAME):
errors.append(f"T2 S0_TX_WR advance: got {adv}, want {len(FRAME)}")
if model_mem.get(_S0_CR) != _CR_SEND:
errors.append("T2 SEND command not issued")
print(f"T2 TX: buffer={['0x%02X' % b for b in buf]} "
f"TX_WR={adv} SEND={model_mem.get(_S0_CR)==_CR_SEND}")
# ── T3: TX MACRAW with ring wraparound (TX_WR near 2 KB boundary) ─────
# Pre-load S0_TX_WR = 0x07FE so a 6-byte frame straddles the boundary:
# offsets 0x7FE,0x7FF then wraps to 0x000,0x001,0x002,0x003.
model_mem[_S0_TX_WR] = 0x07
model_mem[_S0_TX_WR + 1] = 0xFE
model_mem[_S0_CR] = 0x00 # clear so we can detect the new SEND
WFRAME = [0x41, 0x42, 0x43, 0x44, 0x45, 0x46]
await feed_frame(ctx, WFRAME)
# expected physical layout
exp = {
_TX_BASE + 0x7FE: WFRAME[0],
_TX_BASE + 0x7FF: WFRAME[1],
_TX_BASE + 0x000: WFRAME[2],
_TX_BASE + 0x001: WFRAME[3],
_TX_BASE + 0x002: WFRAME[4],
_TX_BASE + 0x003: WFRAME[5],
}
for addr, want in exp.items():
got = model_mem.get(addr)
if got != want:
errors.append(f"T3 wrap byte @0x{addr:04X}: got {got}, want 0x{want:02X}")
adv2 = (model_mem.get(_S0_TX_WR, 0) << 8) | model_mem.get(_S0_TX_WR + 1, 0)
want_wr = (0x07FE + len(WFRAME)) & 0xFFFF
if adv2 != want_wr:
errors.append(f"T3 wrap S0_TX_WR: got 0x{adv2:04X}, want 0x{want_wr:04X}")
ok = all(model_mem.get(a) == v for a, v in exp.items())
print(f"T3 TX wrap: bytes_placed_ok={ok} TX_WR=0x{adv2:04X} (want 0x{want_wr:04X})")
# ── helper: drive an RX event and collect the streamed-out frame ─────
def load_rx(rx_rd_off, frame):
"""Place a MACRAW packet [len_hi,len_lo,frame...] in the RX buffer at
offset rx_rd_off (ring), set RX_RSR/RX_RD, return the 16-bit length."""
plen = len(frame) + 2
payload = [(plen >> 8) & 0xFF, plen & 0xFF] + list(frame)
for i, b in enumerate(payload):
off = (rx_rd_off + i) & _S0_RX_MASK
model_mem[_RX_BASE + off] = b
model_mem[_S0_RX_RSR] = (plen >> 8) & 0xFF
model_mem[_S0_RX_RSR + 1] = plen & 0xFF
model_mem[_S0_RX_RD] = (rx_rd_off >> 8) & 0xFF
model_mem[_S0_RX_RD + 1] = rx_rd_off & 0xFF
return plen
async def do_rx(ctx, rx_rd_off, frame):
plen = load_rx(rx_rd_off, frame)
ctx.set(dut.rx_ready, 1)
collected = []
ctx.set(dut.w5100_int_n, 0) # assert RX interrupt
for _ in range(1500):
await ctx.tick("sync").repeat(1)
if ctx.get(dut.rx_valid) and ctx.get(dut.rx_ready):
collected.append(ctx.get(dut.rx_data))
if model_mem.get(_S0_CR) == _CR_RECV:
break
ctx.set(dut.w5100_int_n, 1) # deassert; let it finish + idle
for _ in range(300):
await ctx.tick("sync").repeat(1)
ctx.set(dut.rx_ready, 0)
return collected, plen
# ── T4: RX MACRAW frame (RX_RD=0, no wrap) ───────────────────────────
model_mem[_S0_CR] = 0x00
RX_FRAME = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03]
got, plen = await do_rx(ctx, 0x0000, RX_FRAME)
if got != RX_FRAME:
errors.append(f"T4 RX frame mismatch: {['0x%02X'%b for b in got]} != "
f"{['0x%02X'%b for b in RX_FRAME]}")
new_rd = (model_mem.get(_S0_RX_RD, 0) << 8) | model_mem.get(_S0_RX_RD + 1, 0)
if new_rd != plen:
errors.append(f"T4 RX_RD advance: got 0x{new_rd:04X}, want 0x{plen:04X}")
print(f"T4 RX: frame={['0x%02X'%b for b in got]} RX_RD=0x{new_rd:04X} "
f"RECV={model_mem.get(_S0_CR)==_CR_RECV}")
# ── T5: RX MACRAW with ring wraparound (RX_RD near 2 KB boundary) ─────
model_mem[_S0_CR] = 0x00
RX_FRAME2 = [0x51, 0x52, 0x53, 0x54, 0x55]
# rx_rd = 0x07FD: [len_hi@7FD][len_lo@7FE][f0@7FF][f1@000][f2@001]...
got2, plen2 = await do_rx(ctx, 0x07FD, RX_FRAME2)
if got2 != RX_FRAME2:
errors.append(f"T5 RX wrap frame mismatch: {['0x%02X'%b for b in got2]} != "
f"{['0x%02X'%b for b in RX_FRAME2]}")
new_rd2 = (model_mem.get(_S0_RX_RD, 0) << 8) | model_mem.get(_S0_RX_RD + 1, 0)
want_rd2 = (0x07FD + plen2) & 0xFFFF
if new_rd2 != want_rd2:
errors.append(f"T5 RX wrap RX_RD: got 0x{new_rd2:04X}, want 0x{want_rd2:04X}")
print(f"T5 RX wrap: frame={['0x%02X'%b for b in got2]} "
f"RX_RD=0x{new_rd2:04X} (want 0x{want_rd2:04X})")
sim = Simulator(dut)
sim.add_clock(Period(MHz=24), domain="sync")
sim.add_testbench(testbench)
sim.add_process(w5100_model)
sim.run()
if errors:
print("\nFAILURES:")
for e in errors:
print(" ", e)
sys.exit(1)
else:
print("\nAll tests passed.")