761 lines
33 KiB
Python
761 lines
33 KiB
Python
"""W5500 SPI master — sync domain (24 MHz).
|
||
|
||
SPI Mode 0 (CPOL=0, CPHA=0): CLK idles LOW, data captured on rising edge.
|
||
SCK = 12 MHz: the sync domain is 24 MHz and the bit engine toggles SCK via a
|
||
clock-enable (sync ÷ 2).
|
||
|
||
W5500 frame format
|
||
------------------
|
||
Byte 0–1 Address (16-bit big-endian)
|
||
Byte 2 Control: [7:3]=BSB [2]=R/W [1:0]=OM
|
||
Byte 3+ Data
|
||
|
||
BSB values used here:
|
||
0b00000 Common registers
|
||
0b00001 Socket 0 registers
|
||
0b00010 Socket 0 TX buffer
|
||
0b00011 Socket 0 RX buffer
|
||
|
||
After NCRA reset the driver issues the W5500 init sequence (MR reset, SHAR,
|
||
S0_MR MACRAW, S0_CR OPEN, S0_IMR).
|
||
|
||
The module provides:
|
||
- A streaming TX interface (tx_data/tx_valid/tx_ready + sof/eof framing)
|
||
- A streaming RX interface (rx_data/rx_valid/rx_ready + sof/eof)
|
||
- init_req / init_done for the NCRA-triggered init sequence
|
||
- MAC source address shadow input (par[0..5]) for SHAR programming
|
||
"""
|
||
|
||
from amaranth import *
|
||
|
||
__all__ = ["W5500SPIMaster"]
|
||
|
||
# W5500 register addresses. The 16-bit address is the OFFSET WITHIN A BLOCK;
|
||
# the block is selected by the BSB field of the control byte (see _CTRL_*),
|
||
# NOT by the address. So socket-0 registers use small offsets with BSB=1.
|
||
_W5500_MR = 0x0000 # Mode register (common block)
|
||
_W5500_SHAR = 0x0009 # Source MAC, 6 bytes (common block)
|
||
_W5500_S0_MR = 0x0000 # Socket 0 Mode (socket-0 block)
|
||
_W5500_S0_CR = 0x0001 # Socket 0 Command
|
||
_W5500_S0_IR = 0x0002 # Socket 0 Interrupt
|
||
_W5500_S0_RXBUF_SIZE = 0x001E # Socket 0 RX buffer size
|
||
_W5500_S0_TXBUF_SIZE = 0x001F # Socket 0 TX buffer size
|
||
_W5500_S0_TX_FSR = 0x0020 # Socket 0 TX Free Size (2 bytes)
|
||
_W5500_S0_TX_WR = 0x0024 # Socket 0 TX Write Pointer
|
||
_W5500_S0_RX_RSR = 0x0026 # Socket 0 RX Received Size (2 bytes)
|
||
_W5500_S0_RX_RD = 0x0028 # Socket 0 RX Read Pointer
|
||
_W5500_S0_IMR = 0x002C # Socket 0 Interrupt Mask
|
||
|
||
# Control byte = (BSB << 3) | (RWB << 2) | OM.
|
||
# RWB: 1=write 0=read. OM=00 → Variable Data Mode (CS frames the length).
|
||
# BSB: 0=common, 1=socket0 reg, 2=socket0 TX buffer, 3=socket0 RX buffer.
|
||
_CTRL_WR_COMMON = (0 << 3) | (1 << 2) # 0x04
|
||
_CTRL_WR_S0REG = (1 << 3) | (1 << 2) # 0x0C
|
||
_CTRL_RD_S0REG = (1 << 3) | (0 << 2) # 0x08
|
||
_CTRL_WR_S0TX = (2 << 3) | (1 << 2) # 0x14
|
||
_CTRL_RD_S0RX = (3 << 3) | (0 << 2) # 0x18
|
||
|
||
|
||
class W5500SPIMaster(Elaboratable):
|
||
"""W5500 SPI master in the sync clock domain.
|
||
|
||
Physical SPI pins
|
||
-----------------
|
||
spi_clk / spi_mosi / spi_miso / spi_cs_n : to W5500
|
||
w5500_int_n : W5500 INT_N input (active low)
|
||
w5500_rst_n : W5500 hardware reset (active low)
|
||
|
||
Init interface (from BBARegisterFile / BBATop)
|
||
----------------------------------------------
|
||
init_req : pulse to trigger the W5500 init sequence
|
||
init_done : pulse when init sequence completes
|
||
par : 6-byte MAC address (sampled at init_req)
|
||
|
||
TX streaming interface (from TXFrameDrain, sync domain)
|
||
-------------------------------------------------------
|
||
tx_data / tx_valid / tx_ready : byte stream
|
||
tx_sof / tx_eof : frame delimiters on the same cycle as tx_valid
|
||
|
||
RX streaming interface (to RXFrameAssembler, sync domain)
|
||
----------------------------------------------------------
|
||
rx_data / rx_valid / rx_ready : byte stream
|
||
rx_sof / rx_eof : frame delimiters
|
||
"""
|
||
|
||
def __init__(self, clk_div=1, reset_cycles=24000):
|
||
# MR-reset settle wait (in sync cycles). ~1 ms; the testbench
|
||
# overrides with a small value for fast simulation.
|
||
self._reset_cycles = reset_cycles
|
||
|
||
# SPI SCK = sync_clock / (2 * clk_div). clk_div=1 → full rate (SCK =
|
||
# sync/2): at the 24 MHz slow domain that is 12 MHz SCK (~12 Mbit/s),
|
||
# which comfortably exceeds real-world GC BBA TCP throughput. The W5500
|
||
# tolerates up to 80 MHz SCK, so the divider exists only as a safety
|
||
# knob for board-level signal-integrity issues, not a functional need.
|
||
self._clk_div = clk_div
|
||
|
||
# Physical SPI
|
||
self.spi_clk = Signal()
|
||
self.spi_mosi = Signal()
|
||
self.spi_miso = Signal()
|
||
self.spi_cs_n = Signal(init=1)
|
||
self.w5500_int_n = Signal(init=1)
|
||
self.w5500_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()
|
||
|
||
# ── SPI clock enable ─────────────────────────────────────────────
|
||
# clk_en high every `clk_div` sync cycles. The bit engine toggles SCK
|
||
# on each enabled cycle, so SCK = sync / (2 * clk_div).
|
||
clk_en = Signal()
|
||
if self._clk_div <= 1:
|
||
m.d.comb += clk_en.eq(1) # full rate: SCK = sync/2
|
||
else:
|
||
div_ctr = Signal(range(self._clk_div))
|
||
with m.If(div_ctr == self._clk_div - 1):
|
||
m.d.sync += div_ctr.eq(0)
|
||
with m.Else():
|
||
m.d.sync += div_ctr.eq(div_ctr + 1)
|
||
m.d.comb += clk_en.eq(div_ctr == self._clk_div - 1)
|
||
|
||
# ── SPI pin registers (Mode 0: SCK idles LOW) ────────────────────
|
||
sck_r = Signal()
|
||
cs_r = Signal(init=1)
|
||
shift_out = Signal(8)
|
||
shift_in = Signal(8)
|
||
m.d.comb += self.spi_clk .eq(sck_r)
|
||
m.d.comb += self.spi_cs_n.eq(cs_r)
|
||
m.d.comb += self.spi_mosi.eq(shift_out[7]) # MSB first; valid pre-rising
|
||
|
||
# ── Byte-transfer engine (Mode 0) ────────────────────────────────
|
||
# On byte_start, shift out byte_tx MSB-first (8 SCK cycles) and capture
|
||
# MISO into byte_rx; pulse byte_done. CS is owned by the xfer engine.
|
||
byte_start = Signal()
|
||
byte_tx = Signal(8)
|
||
byte_rx = Signal(8)
|
||
byte_done = Signal()
|
||
bit_ctr = Signal(4)
|
||
|
||
m.d.sync += byte_done.eq(0)
|
||
with m.FSM(domain="sync", name="byte_fsm"):
|
||
with m.State("IDLE"):
|
||
m.d.sync += sck_r.eq(0)
|
||
with m.If(byte_start):
|
||
m.d.sync += shift_out.eq(byte_tx)
|
||
m.d.sync += bit_ctr.eq(0)
|
||
m.next = "RUN"
|
||
with m.State("RUN"):
|
||
with m.If(clk_en):
|
||
with m.If(~sck_r):
|
||
# rising edge: slave samples MOSI, master samples MISO
|
||
m.d.sync += sck_r.eq(1)
|
||
m.d.sync += shift_in.eq(Cat(self.spi_miso, shift_in[:-1]))
|
||
with m.Else():
|
||
# falling edge: advance / finish
|
||
m.d.sync += sck_r.eq(0)
|
||
with m.If(bit_ctr == 7):
|
||
m.d.sync += byte_rx.eq(shift_in)
|
||
m.d.sync += byte_done.eq(1)
|
||
m.next = "IDLE"
|
||
with m.Else():
|
||
m.d.sync += shift_out.eq(Cat(0, shift_out[:-1]))
|
||
m.d.sync += bit_ctr.eq(bit_ctr + 1)
|
||
|
||
# ── Generic register transaction engine (Variable Data Mode) ─────
|
||
# One CS-low frame: 3 header bytes (addr_hi, addr_lo, ctrl) then
|
||
# xfer_len payload bytes. Writes source payload from wbuf; reads
|
||
# capture MISO into rbuf.
|
||
WBUF = 8
|
||
xfer_start = Signal()
|
||
xfer_addr = Signal(16)
|
||
xfer_ctrl = Signal(8)
|
||
xfer_len = Signal(range(WBUF + 1))
|
||
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)])
|
||
xfer_idx = Signal(range(WBUF + 3))
|
||
|
||
# Stream-write mode: after the 3-byte header, payload bytes are pulled
|
||
# from (s_data, s_valid, s_last) instead of wbuf, until s_last. Used to
|
||
# forward a frame straight into the W5500 TX buffer. s_consume pulses
|
||
# as each streamed byte is accepted; s_count tracks the byte count.
|
||
xfer_stream = Signal()
|
||
s_data = Signal(8)
|
||
s_valid = Signal()
|
||
s_last = Signal()
|
||
s_consume = Signal()
|
||
s_count = Signal(16)
|
||
s_last_r = Signal() # latched s_last for the in-flight byte
|
||
|
||
# Stream-read mode: after the header, read `xfer_rcount` payload bytes
|
||
# (sending 0x00 dummies) and push each out via (r_data, r_valid,
|
||
# r_first, r_last) with r_ready back-pressure. Used to pull a frame
|
||
# out of the W5500 RX buffer into RXFrameAssembler.
|
||
xfer_sread = Signal()
|
||
xfer_rcount = Signal(16)
|
||
r_data = Signal(8)
|
||
r_valid = Signal()
|
||
r_first = Signal()
|
||
r_last = Signal()
|
||
r_ready = Signal()
|
||
r_idx = Signal(16)
|
||
|
||
x_byte = Signal(8)
|
||
with m.If(xfer_idx == 0):
|
||
m.d.comb += x_byte.eq(xfer_addr[8:16])
|
||
with m.Elif(xfer_idx == 1):
|
||
m.d.comb += x_byte.eq(xfer_addr[0:8])
|
||
with m.Elif(xfer_idx == 2):
|
||
m.d.comb += x_byte.eq(xfer_ctrl)
|
||
with m.Else():
|
||
m.d.comb += x_byte.eq(wbuf[xfer_idx - 3])
|
||
|
||
m.d.comb += byte_start.eq(0)
|
||
m.d.comb += byte_tx.eq(0)
|
||
m.d.comb += s_consume.eq(0)
|
||
m.d.comb += r_valid.eq(0)
|
||
m.d.comb += r_data.eq(0)
|
||
m.d.comb += r_first.eq(0)
|
||
m.d.comb += r_last.eq(0)
|
||
|
||
m.d.sync += xfer_done.eq(0)
|
||
with m.FSM(domain="sync", name="xfer_fsm"):
|
||
with m.State("IDLE"):
|
||
with m.If(xfer_start):
|
||
m.d.sync += cs_r.eq(0) # assert CS for the frame
|
||
m.d.sync += xfer_idx.eq(0)
|
||
m.d.sync += s_count.eq(0)
|
||
m.d.sync += r_idx.eq(0)
|
||
m.next = "LOAD"
|
||
with m.State("LOAD"):
|
||
m.d.comb += byte_tx.eq(x_byte)
|
||
m.d.comb += byte_start.eq(1)
|
||
m.next = "WAIT"
|
||
with m.State("WAIT"):
|
||
with m.If(byte_done):
|
||
with m.If(xfer_idx >= 3):
|
||
m.d.sync += rbuf[xfer_idx - 3].eq(byte_rx)
|
||
with m.If((xfer_idx == 2) & xfer_stream):
|
||
m.next = "SLOAD" # stream the payload (write)
|
||
with m.Elif((xfer_idx == 2) & xfer_sread):
|
||
m.next = "RLOAD" # stream the payload (read)
|
||
with m.Elif(~xfer_stream & ~xfer_sread
|
||
& (xfer_idx == (xfer_len + 2))):
|
||
m.next = "FINISH" # 3 header + len − 1
|
||
with m.Else():
|
||
m.d.sync += xfer_idx.eq(xfer_idx + 1)
|
||
m.next = "LOAD"
|
||
|
||
# ── Streamed-payload sub-loop (TX buffer write) ──────────────
|
||
with m.State("SLOAD"):
|
||
with m.If(s_valid):
|
||
m.d.comb += byte_tx.eq(s_data)
|
||
m.d.comb += byte_start.eq(1)
|
||
m.d.sync += s_last_r.eq(s_last)
|
||
m.next = "SWAIT"
|
||
with m.State("SWAIT"):
|
||
with m.If(byte_done):
|
||
m.d.comb += s_consume.eq(1) # accept this frame byte
|
||
m.d.sync += s_count.eq(s_count + 1)
|
||
with m.If(s_last_r):
|
||
m.next = "FINISH"
|
||
with m.Else():
|
||
m.next = "SLOAD"
|
||
|
||
# ── Streamed-payload sub-loop (RX buffer read) ───────────────
|
||
with m.State("RLOAD"):
|
||
with m.If(r_idx == xfer_rcount):
|
||
m.next = "FINISH"
|
||
with m.Else():
|
||
m.d.comb += byte_tx.eq(0) # dummy MOSI during read
|
||
m.d.comb += byte_start.eq(1)
|
||
m.next = "RWAIT"
|
||
with m.State("RWAIT"):
|
||
with m.If(byte_done):
|
||
m.next = "RPUSH"
|
||
with m.State("RPUSH"):
|
||
m.d.comb += r_data .eq(byte_rx)
|
||
m.d.comb += r_valid.eq(1)
|
||
m.d.comb += r_first.eq(r_idx == 0)
|
||
m.d.comb += r_last .eq(r_idx == (xfer_rcount - 1))
|
||
with m.If(r_ready):
|
||
m.d.sync += r_idx.eq(r_idx + 1)
|
||
m.next = "RLOAD"
|
||
|
||
with m.State("FINISH"):
|
||
m.d.sync += cs_r.eq(1) # deassert CS
|
||
m.d.sync += xfer_done.eq(1)
|
||
m.next = "IDLE"
|
||
|
||
# Saved MAC for SHAR programming; current W5500 TX write pointer.
|
||
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 received size
|
||
rx_rd = Signal(16) # RX read pointer
|
||
pkt_len = Signal(16) # MACRAW packet length (incl. 2-byte header)
|
||
|
||
# Frame stream from TXFrameDrain feeds the xfer engine's stream port.
|
||
# tx_ready pulses (= s_consume) as each frame byte is taken into the
|
||
# TX-buffer write transaction.
|
||
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 buffer read stream → RXFrameAssembler.
|
||
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),
|
||
]
|
||
|
||
# Helper: a setup state that programs one register-write transaction
|
||
# then waits for it to complete and jumps to `nxt`.
|
||
def write_reg(name, addr, ctrl, payload, nxt):
|
||
with m.State(name):
|
||
m.d.sync += xfer_addr.eq(addr)
|
||
m.d.sync += xfer_ctrl.eq(ctrl)
|
||
m.d.sync += xfer_len.eq(len(payload))
|
||
m.d.sync += xfer_stream.eq(0)
|
||
m.d.sync += xfer_sread.eq(0)
|
||
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 ─────────────────────────────────────────────
|
||
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.w5500_int_n):
|
||
m.next = "RX_CHECK"
|
||
with m.Elif(self.tx_valid & self.tx_sof):
|
||
m.next = "TX_START"
|
||
|
||
# Step 1: MR = 0x80 (software reset), then settle ~1 ms.
|
||
write_reg("MR_RST", _W5500_MR, _CTRL_WR_COMMON, [0x80], "MR_WAIT")
|
||
with m.State("MR_WAIT"):
|
||
with m.If(wait_ctr == self._reset_cycles):
|
||
m.d.sync += wait_ctr.eq(0)
|
||
m.next = "SHAR"
|
||
with m.Else():
|
||
m.d.sync += wait_ctr.eq(wait_ctr + 1)
|
||
|
||
# Step 2: SHAR = source MAC (6 bytes from PAR0–5).
|
||
with m.State("SHAR"):
|
||
m.d.sync += xfer_addr.eq(_W5500_SHAR)
|
||
m.d.sync += xfer_ctrl.eq(_CTRL_WR_COMMON)
|
||
m.d.sync += 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 = "S0_MR"
|
||
|
||
# Step 3–5: S0_MR=MACRAW, S0_CR=OPEN, S0_IMR=RECV|SEND_OK.
|
||
write_reg("S0_MR", _W5500_S0_MR, _CTRL_WR_S0REG, [0x04], "S0_CR")
|
||
write_reg("S0_CR", _W5500_S0_CR, _CTRL_WR_S0REG, [0x01], "S0_IMR")
|
||
write_reg("S0_IMR", _W5500_S0_IMR, _CTRL_WR_S0REG, [0x05], "INIT_DONE")
|
||
|
||
with m.State("INIT_DONE"):
|
||
m.d.sync += self.init_done.eq(1)
|
||
m.next = "IDLE"
|
||
|
||
# ── TX path (MACRAW) ─────────────────────────────────────────
|
||
# 1) read S0_TX_WR, 2) stream the frame into the TX buffer at that
|
||
# offset, 3) advance S0_TX_WR by the byte count, 4) issue SEND.
|
||
with m.State("TX_START"):
|
||
m.d.sync += xfer_addr.eq(_W5500_S0_TX_WR)
|
||
m.d.sync += xfer_ctrl.eq(_CTRL_RD_S0REG)
|
||
m.d.sync += xfer_len.eq(2)
|
||
m.d.sync += xfer_stream.eq(0)
|
||
m.d.sync += wbuf[0].eq(0) # read → send 0x00 dummies
|
||
m.d.sync += wbuf[1].eq(0)
|
||
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"):
|
||
m.d.sync += xfer_addr.eq(tx_wr)
|
||
m.d.sync += xfer_ctrl.eq(_CTRL_WR_S0TX) # socket-0 TX buffer
|
||
m.d.sync += xfer_stream.eq(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)
|
||
m.d.sync += tx_wr.eq(tx_wr + s_count) # advanced pointer
|
||
m.next = "TX_UPDPTR"
|
||
|
||
with m.State("TX_UPDPTR"):
|
||
m.d.sync += xfer_addr.eq(_W5500_S0_TX_WR)
|
||
m.d.sync += xfer_ctrl.eq(_CTRL_WR_S0REG)
|
||
m.d.sync += xfer_len.eq(2)
|
||
m.d.sync += xfer_stream.eq(0)
|
||
m.d.sync += wbuf[0].eq(tx_wr[8:16]) # hi (already advanced)
|
||
m.d.sync += wbuf[1].eq(tx_wr[0:8]) # lo
|
||
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 (0x20)
|
||
write_reg("TX_SEND", _W5500_S0_CR, _CTRL_WR_S0REG, [0x20], "IDLE")
|
||
|
||
# ── RX path (MACRAW) ─────────────────────────────────────────
|
||
# Triggered by W5500 INT (w5500_int_n low): read RX_RSR, read
|
||
# RX_RD, read the 2-byte MACRAW length, stream the frame out,
|
||
# advance RX_RD, issue RECV.
|
||
with m.State("RX_CHECK"): # read S0_RX_RSR
|
||
m.d.sync += xfer_addr.eq(_W5500_S0_RX_RSR)
|
||
m.d.sync += xfer_ctrl.eq(_CTRL_RD_S0REG)
|
||
m.d.sync += xfer_len.eq(2)
|
||
m.d.sync += xfer_stream.eq(0)
|
||
m.d.sync += xfer_sread.eq(0)
|
||
m.d.sync += wbuf[0].eq(0)
|
||
m.d.sync += wbuf[1].eq(0)
|
||
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
|
||
m.d.sync += xfer_addr.eq(_W5500_S0_RX_RD)
|
||
m.d.sync += xfer_ctrl.eq(_CTRL_RD_S0REG)
|
||
m.d.sync += 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
|
||
m.d.sync += xfer_addr.eq(rx_rd)
|
||
m.d.sync += xfer_ctrl.eq(_CTRL_RD_S0RX)
|
||
m.d.sync += xfer_len.eq(2)
|
||
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_rd + 2)
|
||
m.d.sync += xfer_ctrl.eq(_CTRL_RD_S0RX)
|
||
m.d.sync += xfer_sread.eq(1)
|
||
m.d.sync += xfer_rcount.eq(pkt_len - 2)
|
||
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)
|
||
m.next = "RX_UPDRD"
|
||
|
||
with m.State("RX_UPDRD"): # S0_RX_RD += pkt_len
|
||
m.d.sync += xfer_addr.eq(_W5500_S0_RX_RD)
|
||
m.d.sync += xfer_ctrl.eq(_CTRL_WR_S0REG)
|
||
m.d.sync += xfer_len.eq(2)
|
||
m.d.sync += xfer_stream.eq(0)
|
||
m.d.sync += xfer_sread.eq(0)
|
||
m.d.sync += wbuf[0].eq((rx_rd + pkt_len)[8:16])
|
||
m.d.sync += 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 (0x40), then clear the RECV interrupt so INT_N
|
||
# deasserts (write 1 to Sn_IR[2]); otherwise the FSM would re-enter
|
||
# RX_CHECK forever on a real W5500.
|
||
write_reg("RX_RECV", _W5500_S0_CR, _CTRL_WR_S0REG, [0x40], "RX_CLR_IR")
|
||
write_reg("RX_CLR_IR", _W5500_S0_IR, _CTRL_WR_S0REG, [0x04], "IDLE")
|
||
|
||
return m
|
||
|
||
|
||
# ── Testbench ─────────────────────────────────────────────────────────────
|
||
|
||
if __name__ == "__main__":
|
||
import sys
|
||
from amaranth.sim import Simulator, Period
|
||
|
||
# Short reset wait so the init sequence runs quickly in simulation.
|
||
dut = W5500SPIMaster(reset_cycles=10)
|
||
errors = []
|
||
|
||
# MAC for SHAR: par[i*8:(i+1)*8] = mac byte i → mac = 11 22 33 44 55 66
|
||
MAC = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]
|
||
PAR = sum(b << (8 * i) for i, b in enumerate(MAC))
|
||
|
||
# Expected W5500 init transactions: [addr_hi, addr_lo, ctrl, *payload].
|
||
# ctrl 0x04 = common-block write (VDM); 0x0C = socket-0-reg write (VDM).
|
||
EXPECTED = [
|
||
[0x00, 0x00, 0x04, 0x80], # MR = 0x80 (reset)
|
||
[0x00, 0x09, 0x04, *MAC], # SHAR = MAC
|
||
[0x00, 0x00, 0x0C, 0x04], # S0_MR = MACRAW
|
||
[0x00, 0x01, 0x0C, 0x01], # S0_CR = OPEN
|
||
[0x00, 0x2C, 0x0C, 0x05], # S0_IMR = RECV|SEND_OK
|
||
]
|
||
|
||
txns = [] # transactions captured by the W5500 slave model
|
||
|
||
# RX frame the W5500 will hand back, and the MACRAW length it reports.
|
||
RX_FRAME = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02]
|
||
RX_PKT_LEN = len(RX_FRAME) + 2 # MACRAW length includes the header
|
||
|
||
def build_response(bsb, addr):
|
||
"""Bytes the W5500 drives on MISO for a read of (bsb, addr)."""
|
||
if bsb == 1 and addr == _W5500_S0_RX_RSR:
|
||
return [(RX_PKT_LEN >> 8) & 0xFF, RX_PKT_LEN & 0xFF]
|
||
if bsb == 1 and addr == _W5500_S0_RX_RD:
|
||
return [0x00, 0x00] # RX read pointer = 0
|
||
if bsb == 3 and addr == 0x0000:
|
||
return [(RX_PKT_LEN >> 8) & 0xFF, RX_PKT_LEN & 0xFF] # length
|
||
if bsb == 3 and addr == 0x0002:
|
||
return list(RX_FRAME) # frame payload
|
||
return [0x00] * 64
|
||
|
||
async def w5500_model(ctx):
|
||
"""W5500 SPI slave model: captures CS-framed transactions (MOSI) and,
|
||
for reads, drives MISO with canned register/buffer data. Mode 0:
|
||
MOSI sampled on rising SCK, MISO shifted out MSB-first.
|
||
"""
|
||
prev_cs, prev_sck = 1, 0
|
||
rx_byte = rx_bits = nbytes = 0
|
||
hdr = [0, 0, 0]
|
||
is_read = False
|
||
resp, ridx = [], 0
|
||
msr = msr_bits = 0
|
||
cur_txn = []
|
||
async for vals in ctx.tick("sync").sample(
|
||
dut.spi_cs_n, dut.spi_clk, dut.spi_mosi):
|
||
cs, sck, mosi = vals[-3:]
|
||
rising = (prev_sck == 0 and sck == 1)
|
||
|
||
if prev_cs == 1 and cs == 0: # CS falling: start frame
|
||
cur_txn = []
|
||
rx_byte = rx_bits = nbytes = 0
|
||
is_read = False
|
||
resp, ridx, msr, msr_bits = [], 0, 0, 0
|
||
|
||
if cs == 0 and rising:
|
||
# MISO bit just sampled by the master → advance shift register
|
||
if is_read and nbytes >= 3:
|
||
msr = (msr << 1) & 0xFF
|
||
msr_bits -= 1
|
||
if msr_bits == 0:
|
||
msr = resp[ridx] if ridx < len(resp) else 0
|
||
ridx += 1
|
||
msr_bits = 8
|
||
# sample MOSI
|
||
rx_byte = ((rx_byte << 1) | mosi) & 0xFF
|
||
rx_bits += 1
|
||
if rx_bits == 8:
|
||
cur_txn.append(rx_byte)
|
||
if nbytes < 3:
|
||
hdr[nbytes] = rx_byte
|
||
if nbytes == 2: # header complete → decode
|
||
ctrl = hdr[2]
|
||
is_read = (ctrl & 0x04) == 0
|
||
bsb = ctrl >> 3
|
||
addr = (hdr[0] << 8) | hdr[1]
|
||
if is_read:
|
||
resp = build_response(bsb, addr)
|
||
msr, ridx, msr_bits = resp[0], 1, 8
|
||
nbytes += 1
|
||
rx_byte = rx_bits = 0
|
||
|
||
if prev_cs == 0 and cs == 1: # CS rising: end frame
|
||
txns.append(list(cur_txn))
|
||
|
||
ctx.set(dut.spi_miso, (msr >> 7) & 1)
|
||
prev_cs, prev_sck = cs, sck
|
||
|
||
rx_collected = []
|
||
|
||
async def rx_collector(ctx):
|
||
async for vals in ctx.tick("sync").sample(
|
||
dut.rx_valid, dut.rx_ready, dut.rx_data):
|
||
valid, ready, data = vals[-3:]
|
||
if valid and ready:
|
||
rx_collected.append(data)
|
||
|
||
async def testbench(ctx):
|
||
ctx.set(dut.par, PAR)
|
||
await ctx.tick("sync").repeat(4)
|
||
|
||
# T1: SPI idle — CLK low (Mode 0), CS high
|
||
if ctx.get(dut.spi_clk) != 0:
|
||
errors.append("T1 CLK idle != 0")
|
||
if ctx.get(dut.spi_cs_n) != 1:
|
||
errors.append("T1 CS idle != 1")
|
||
print(f"T1 idle: CLK={ctx.get(dut.spi_clk)} CS={ctx.get(dut.spi_cs_n)}")
|
||
|
||
# T2: run the init sequence
|
||
ctx.set(dut.init_req, 1)
|
||
await ctx.tick("sync").repeat(1)
|
||
ctx.set(dut.init_req, 0)
|
||
|
||
for _ in range(4000):
|
||
await ctx.tick("sync").repeat(1)
|
||
if ctx.get(dut.init_done):
|
||
break
|
||
if not ctx.get(dut.init_done):
|
||
errors.append("T2 init_done never asserted")
|
||
await ctx.tick("sync").repeat(4)
|
||
print(f"T2 init_done: {ctx.get(dut.init_done)}")
|
||
|
||
# T3: verify the captured init transaction sequence
|
||
print(f"T3 captured {len(txns)} init transactions:")
|
||
for t in txns:
|
||
print(" ", [f"0x{b:02X}" for b in t])
|
||
if txns != EXPECTED:
|
||
errors.append(f"T3 init sequence mismatch:\n got {txns}\n want {EXPECTED}")
|
||
|
||
# ── T4: TX a frame (MACRAW) ──────────────────────────────────────
|
||
txns.clear()
|
||
FRAME = [0xAA, 0xBB, 0xCC, 0xDD]
|
||
# With MISO=0 the read returns S0_TX_WR = 0x0000.
|
||
TX_EXPECTED = [
|
||
[0x00, 0x24, 0x08, 0x00, 0x00], # read S0_TX_WR (dummies)
|
||
[0x00, 0x00, 0x14, *FRAME], # write TX buffer @ 0x0000
|
||
[0x00, 0x24, 0x0C, 0x00, len(FRAME)], # S0_TX_WR += len
|
||
[0x00, 0x01, 0x0C, 0x20], # S0_CR = SEND
|
||
]
|
||
|
||
async def send_frame(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)
|
||
for _ in range(2000):
|
||
if ctx.get(dut.tx_ready):
|
||
break
|
||
await ctx.tick("sync").repeat(1)
|
||
await ctx.tick("sync").repeat(1) # complete the consume
|
||
ctx.set(dut.tx_valid, 0)
|
||
ctx.set(dut.tx_sof, 0)
|
||
ctx.set(dut.tx_eof, 0)
|
||
|
||
await send_frame(FRAME)
|
||
# let the pointer-update + SEND transactions finish
|
||
for _ in range(2000):
|
||
await ctx.tick("sync").repeat(1)
|
||
if len(txns) >= len(TX_EXPECTED):
|
||
break
|
||
await ctx.tick("sync").repeat(4)
|
||
|
||
print(f"T4 captured {len(txns)} TX transactions:")
|
||
for t in txns:
|
||
print(" ", [f"0x{b:02X}" for b in t])
|
||
if txns != TX_EXPECTED:
|
||
errors.append(f"T4 TX sequence mismatch:\n got {txns}\n want {TX_EXPECTED}")
|
||
|
||
# ── T5: RX a frame (MACRAW) ──────────────────────────────────────
|
||
# The model returns RSR=pkt_len, RD=0, MACRAW length=pkt_len, then the
|
||
# frame. Expected transactions (read dummies are 0x00):
|
||
RX_EXPECTED = [
|
||
[0x00, 0x26, 0x08, 0x00, 0x00], # read S0_RX_RSR
|
||
[0x00, 0x28, 0x08, 0x00, 0x00], # read S0_RX_RD
|
||
[0x00, 0x00, 0x18, 0x00, 0x00], # read MACRAW length
|
||
[0x00, 0x02, 0x18, *([0x00] * len(RX_FRAME))], # read frame
|
||
[0x00, 0x28, 0x0C, 0x00, RX_PKT_LEN], # S0_RX_RD += pkt_len
|
||
[0x00, 0x01, 0x0C, 0x40], # S0_CR = RECV
|
||
[0x00, 0x02, 0x0C, 0x04], # S0_IR clear RECV
|
||
]
|
||
txns.clear()
|
||
ctx.set(dut.rx_ready, 1)
|
||
ctx.set(dut.w5500_int_n, 0) # signal a received packet
|
||
for _ in range(4000):
|
||
await ctx.tick("sync").repeat(1)
|
||
if len(txns) >= len(RX_EXPECTED):
|
||
break
|
||
ctx.set(dut.w5500_int_n, 1)
|
||
await ctx.tick("sync").repeat(8)
|
||
|
||
print(f"T5 captured {len(txns)} RX transactions:")
|
||
for t in txns:
|
||
print(" ", [f"0x{b:02X}" for b in t])
|
||
print(f"T5 rx frame: {[f'0x{b:02X}' for b in rx_collected]} "
|
||
f"(want {[f'0x{b:02X}' for b in RX_FRAME]})")
|
||
if txns != RX_EXPECTED:
|
||
errors.append(f"T5 RX sequence mismatch:\n got {txns}\n want {RX_EXPECTED}")
|
||
if rx_collected != RX_FRAME:
|
||
errors.append(f"T5 RX frame mismatch: got {rx_collected}, want {RX_FRAME}")
|
||
|
||
sim = Simulator(dut)
|
||
sim.add_clock(Period(MHz=24), domain="sync")
|
||
sim.add_testbench(testbench)
|
||
sim.add_process(w5500_model)
|
||
sim.add_process(rx_collector)
|
||
|
||
with sim.write_vcd("W5500SPIMaster.vcd"):
|
||
sim.run()
|
||
|
||
if errors:
|
||
print("\nFAILURES:")
|
||
for e in errors:
|
||
print(" ", e)
|
||
sys.exit(1)
|
||
else:
|
||
print("\nAll tests passed.")
|