"""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.")
