841 lines
37 KiB
Python
841 lines
37 KiB
Python
"""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.")
|