"""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 2–3. """ 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 (length−2) 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_len−2) 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.")