Add integrated UART UDP bring-up shell (socket 3) + capture critical-path fix

An interactive UART command shell drives a UDP send/receive test on the
W5100's socket 3, running ALONGSIDE the live BBA (socket-0 MACRAW) with
EXI keeping bus priority. MACRAW+UDP coexistence is W5100S-datasheet
confirmed (S4.6 + "4 independent SOCKETs"). Now the default flash build
(--console selects the old event-log console).

New / changed gateware:
- uart_shell.py (new): rebbarb> shell over FT2232H channel B. Commands:
  help; udp unicast <ip> [msg]; udp broadcast [msg]. After each send it
  waits (bounded, else "timeout") for a reply and prints "rx <payload>".
  Line buffer + message ROM live in block RAM with a sequential parser
  (LC-efficient); 1-deep RX holding reg keeps pastes intact. 8 sim tests.
- w5100_parallel_master.py: configurable UDP socket (default 3) with UDP
  send AND receive (IP-stack init, runtime dest IP, WIZnet UDP RX header
  + payload). Gated by enable_udp_test so the MACRAW path is unchanged
  when off. Tests U1-U4 + MACRAW T1-T5.
- exi_capture.py: CAPTURE-DOMAIN CRITICAL-PATH FIX. The TX byte-FIFO
  read-enable was gated by its own gray-coded ready
  (r_en = ... | (flushing & r_rdy)), forming a consume_ptr -> gray ->
  r_rdy -> flush -> r_en -> consume_ptr loop that capped capture_clk.
  Replaced the r_rdy-based "drain until empty" flush with a fixed-length
  drain counter (FIFO is only tx_depth deep), removing the pointer
  feedback from r_en. Path 24.3 -> 19.6 ns; flush behavior preserved.
- bba_top.py: wire shell <-> W5100 UDP (send + rx); shell additive.
- synth.py: shell default build; env-var UDP network config; documents a
  reverted PNR-timing-priority experiment.

Timing (--seeds 8, default shell build, 67% LC): capture closes on 4/8
seeds (best seed 4 = 58.36 MHz, +8%), clk passes on all. This is BETTER
than the pre-shell 2/8 baseline because the flush fix improved the
capture domain intrinsically. Flash build/seed4/top.bin.

Bring-up caveats (unchanged): W5100 socket register addresses / UDP
header format are datasheet-derived (confirm on hardware); UDP_SRC_IP /
subnet / gateway must match the LAN for unicast ARP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 14:36:47 +00:00
parent a72e4ab1d4
commit c16afb6eea
6 changed files with 1458 additions and 37 deletions
+523 -9
View File
@@ -45,7 +45,10 @@ __all__ = ["W5100ParallelMaster"]
# ── W5100 register addresses (indirect 16-bit address space) ────────────────
_MR = 0x0000 # Mode register (common)
_GAR0 = 0x0001 # Gateway IP, 4 bytes
_SUBR0 = 0x0005 # Subnet mask, 4 bytes
_SHAR0 = 0x0009 # Source MAC, 6 bytes
_SIPR0 = 0x000F # Source IP, 4 bytes
_IR = 0x0015 # Interrupt register
_IMR = 0x0016 # Interrupt mask
_RMSR = 0x001A # RX memory size (2 bits/socket)
@@ -60,20 +63,50 @@ _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
# Per-socket register/buffer geometry (2 KB per socket, RMSR/TMSR=0x55).
# The UDP-test socket number is configurable; addresses are computed from it in
# __init__ (see _socket_addrs). Socket-n register block base = 0x0400+n*0x100,
# TX buffer base = 0x4000+n*0x800, RX buffer base = 0x6000+n*0x800.
_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
_SN_MASK = 0x07FF # 2 KB ring mask (any socket)
def _socket_addrs(n):
"""Return the register/buffer addresses for socket `n` (0..3)."""
base = 0x0400 + n * 0x0100
return dict(
MR=base + 0x00, CR=base + 0x01, IR=base + 0x02, SR=base + 0x03,
PORT=base + 0x04, DIPR=base + 0x0C, DPORT=base + 0x10,
TX_WR=base + 0x24, RX_RSR=base + 0x26, RX_RD=base + 0x28,
TX_BASE=0x4000 + n * 0x0800, RX_BASE=0x6000 + n * 0x0800,
)
# 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
_S1_MR_UDP = 0x02 # socket UDP mode
_CR_OPEN = 0x01
_CR_SEND = 0x20
_CR_RECV = 0x40
def _ip_bytes(dotted):
"""'192.168.1.100' → [192, 168, 1, 100] (big-endian, network order)."""
parts = [int(x) for x in dotted.split(".")]
if len(parts) != 4 or any(not 0 <= p <= 255 for p in parts):
raise ValueError(f"invalid IPv4 address: {dotted!r}")
return parts
def _port_bytes(port):
"""6464 → [0x19, 0x40] (big-endian)."""
return [(port >> 8) & 0xFF, port & 0xFF]
# Indirect-mode address selects (A[1:0])
_A_MR = 0b00
_A_AR0 = 0b01 # IDM_AR high byte
@@ -97,12 +130,64 @@ class W5100ParallelMaster(Elaboratable):
Init / TX / RX interfaces are identical to W5500SPIMaster.
"""
def __init__(self, strobe_cycles=3, reset_cycles=24000):
def __init__(self, strobe_cycles=3, reset_cycles=24000,
enable_udp_test=False, udp_socket=3,
src_ip="192.168.1.123", subnet="255.255.255.0",
gateway="192.168.1.1", dst_ip="192.168.1.100",
src_port=6464, dst_port=6464,
udp_payload=b"REBBARB-UDP-TEST\r\n"):
# /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
# ── Optional socket-1 UDP bring-up test ───────────────────────────────
# When enabled, init also configures the W5100's IP stack (GAR/SUBR/
# SIPR) and opens socket 1 in UDP mode; pulsing `udp_send_req` then
# emits one UDP datagram to `udp_dst_ip`:dst_port with the payload
# supplied on the `udp_pl_*` stream. MACRAW socket 0 is untouched.
# Everything below is elaborated only when enabled, so a plain BBA
# build (and the existing MACRAW testbench) is bit-for-bit unchanged.
#
# Network config is build-time (the board's own address); the
# destination and payload are RUNTIME so the UART shell can drive them
# from a typed command. `dst_ip` here is only a power-on default for
# the udp_dst_ip input.
if not 1 <= udp_socket <= 3:
raise ValueError("udp_socket must be 1..3 (socket 0 is MACRAW)")
self._enable_udp = enable_udp_test
self._sn = _socket_addrs(udp_socket)
self._src_ip = _ip_bytes(src_ip)
self._subnet = _ip_bytes(subnet)
self._gateway = _ip_bytes(gateway)
self._src_port = _port_bytes(src_port)
self._dst_port = _port_bytes(dst_port)
_dst_default = _ip_bytes(dst_ip)
# Runtime UDP-send control / datapath.
self.udp_send_req = Signal() # pulse: emit one datagram
self.udp_test_busy = Signal() # level: send in progress
self.udp_dst_ip = Signal(32, # destination IP (big-endian:
init=int.from_bytes(bytes(_dst_default), "big")) # [0]=MSB octet)
# Payload byte stream (driven by the shell; consumed during the send).
self.udp_pl_data = Signal(8)
self.udp_pl_valid = Signal()
self.udp_pl_last = Signal()
self.udp_pl_ready = Signal()
# UDP receive (poll-driven): pulse udp_rx_req to check the socket's RX
# buffer for a datagram. Either udp_rx_none pulses (nothing waiting) or
# the payload streams out on udp_rx_* (sof/eof frame the datagram);
# udp_rx_busy is high while a check/read is in progress.
self.udp_rx_req = Signal()
self.udp_rx_busy = Signal()
self.udp_rx_none = Signal()
self.udp_rx_data = Signal(8)
self.udp_rx_valid = Signal()
self.udp_rx_sof = Signal()
self.udp_rx_eof = Signal()
self.udp_rx_ready = Signal()
# Physical parallel bus
self.bus_addr = Signal(2)
self.bus_data_o = Signal(8)
@@ -136,6 +221,7 @@ class W5100ParallelMaster(Elaboratable):
def elaborate(self, platform):
m = Module()
STROBE = self._strobe
sn = self._sn # UDP-test socket register/buffer addresses
# ── Bus access engine: one indirect-bus read or write cycle ──────────
bus_go = Signal()
@@ -209,13 +295,43 @@ class W5100ParallelMaster(Elaboratable):
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)]
# TX stream-write source mux: during a UDP-test send the payload comes
# from the external `udp_pl_*` stream (the shell); otherwise the normal
# MACRAW TX interface feeds it. `udp_streaming` is raised only while the
# UDP payload is being written to the socket-1 TX buffer.
udp_streaming = Signal()
if self._enable_udp:
with m.If(udp_streaming):
m.d.comb += [s_data.eq(self.udp_pl_data),
s_valid.eq(self.udp_pl_valid),
s_last.eq(self.udp_pl_last),
self.udp_pl_ready.eq(s_consume)]
with m.Else():
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)]
else:
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-read sink mux: during a socket-N UDP receive the payload
# streams out on `udp_rx_*` (to the shell); otherwise it goes to the
# MACRAW rx interface (to the frame assembler). `udp_rx_streaming` is
# raised only while the UDP payload is being read out.
udp_rx_streaming = Signal()
if self._enable_udp:
with m.If(udp_rx_streaming):
m.d.comb += [self.udp_rx_data.eq(r_data),
self.udp_rx_valid.eq(r_valid),
self.udp_rx_sof.eq(r_first),
self.udp_rx_eof.eq(r_last),
r_ready.eq(self.udp_rx_ready)]
with m.Else():
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)]
else:
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
@@ -391,6 +507,10 @@ class W5100ParallelMaster(Elaboratable):
rx_rsr = Signal(16)
rx_rd = Signal(16)
pkt_len = Signal(16)
s1_tx_wr = Signal(16) # UDP-socket TX write pointer (send)
sn_rx_rsr = Signal(16) # UDP-socket RX received size
sn_rx_rd = Signal(16) # UDP-socket RX read pointer
sn_pkt_len = Signal(16) # UDP datagram payload length (from header)
def write_reg(name, addr, payload, nxt, direct=False):
"""Emit a 2-state block that writes `payload` (a list) to `addr`."""
@@ -409,6 +529,8 @@ class W5100ParallelMaster(Elaboratable):
m.next = nxt
# ── Main control FSM (Phase 1: init only) ────────────────────────────
if self._enable_udp:
m.d.sync += self.udp_rx_none.eq(0) # pulse: default low
with m.FSM(domain="sync", name="main_fsm"):
with m.State("IDLE"):
m.d.sync += self.init_done.eq(0)
@@ -420,6 +542,11 @@ class W5100ParallelMaster(Elaboratable):
m.next = "RX_CHECK"
with m.Elif(self.tx_valid & self.tx_sof):
m.next = "TX_START"
if self._enable_udp:
with m.Elif(self.udp_send_req):
m.next = "UDP_DIPR"
with m.Elif(self.udp_rx_req):
m.next = "UDP_RX_RSR"
# MR = 0x80 software reset (direct A=00), then settle.
write_reg("MR_RST", _MR, [_MR_RST], "MR_WAIT", direct=True)
@@ -452,7 +579,21 @@ class W5100ParallelMaster(Elaboratable):
# 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
# After S0 IMR, either finish (plain BBA) or configure the IP stack
# and open socket 1 in UDP mode (bring-up test build).
write_reg("S0_IMR", _IMR, [0x01],
"UDP_CFG_GAR" if self._enable_udp else "INIT_DONE")
if self._enable_udp:
# IP stack: gateway, subnet, source IP (SHAR already set above).
write_reg("UDP_CFG_GAR", _GAR0, self._gateway, "UDP_CFG_SUBR")
write_reg("UDP_CFG_SUBR", _SUBR0, self._subnet, "UDP_CFG_SIPR")
write_reg("UDP_CFG_SIPR", _SIPR0, self._src_ip, "UDP_CFG_MR")
# Socket 1: UDP mode, source port, OPEN, fixed dest port.
write_reg("UDP_CFG_MR", sn["MR"], [_S1_MR_UDP], "UDP_CFG_PORT")
write_reg("UDP_CFG_PORT", sn["PORT"], self._src_port, "UDP_CFG_OPEN")
write_reg("UDP_CFG_OPEN", sn["CR"], [_CR_OPEN], "UDP_CFG_DPORT")
write_reg("UDP_CFG_DPORT", sn["DPORT"], self._dst_port, "INIT_DONE")
with m.State("INIT_DONE"):
m.d.sync += self.init_done.eq(1)
@@ -581,6 +722,168 @@ class W5100ParallelMaster(Elaboratable):
write_reg("RX_RECV", _S0_CR, [_CR_RECV], "RX_CLR_IR")
write_reg("RX_CLR_IR", _S0_IR, [0x04], "IDLE")
# ── UDP test send (configurable socket) ──────────────────────────
# Runtime dest IP → Sn_DIPR, read Sn_TX_WR, stream the shell-
# supplied payload into the socket TX buffer (ring-wrapping),
# advance Sn_TX_WR, SEND. DPORT + socket open were done at init.
if self._enable_udp:
with m.State("UDP_DIPR"): # write Sn_DIPR (runtime, 4 B)
m.d.sync += self.udp_test_busy.eq(1)
m.d.sync += [xfer_addr.eq(sn["DIPR"]), xfer_rw.eq(1),
xfer_stream.eq(0), xfer_sread.eq(0),
xfer_wrap.eq(0), xfer_direct.eq(0),
xfer_len.eq(4)]
m.d.sync += [wbuf[0].eq(self.udp_dst_ip[24:32]),
wbuf[1].eq(self.udp_dst_ip[16:24]),
wbuf[2].eq(self.udp_dst_ip[8:16]),
wbuf[3].eq(self.udp_dst_ip[0:8])]
m.d.sync += xfer_start.eq(1)
m.next = "UDP_DIPR_W"
with m.State("UDP_DIPR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.next = "UDP_TXWR"
with m.State("UDP_TXWR"): # read Sn_TX_WR (2 B)
m.d.sync += [xfer_addr.eq(sn["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 = "UDP_TXWR_W"
with m.State("UDP_TXWR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.d.sync += s1_tx_wr.eq(Cat(rbuf[1], rbuf[0]))
m.next = "UDP_DATA"
with m.State("UDP_DATA"): # stream payload → Sn TX buffer
m.d.sync += [xfer_addr.eq(sn["TX_BASE"] + (s1_tx_wr & _SN_MASK)),
xfer_rw.eq(1), xfer_stream.eq(1),
xfer_sread.eq(0), xfer_direct.eq(0),
xfer_wrap.eq(1), xfer_wbase.eq(sn["TX_BASE"]),
xfer_wend.eq(sn["TX_BASE"] + _SN_MASK + 1),
udp_streaming.eq(1)]
m.d.sync += xfer_start.eq(1)
m.next = "UDP_DATA_W"
with m.State("UDP_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),
udp_streaming.eq(0),
s1_tx_wr.eq(s1_tx_wr + s_count)]
m.next = "UDP_UPDPTR"
with m.State("UDP_UPDPTR"): # write back Sn_TX_WR (2 B)
m.d.sync += [xfer_addr.eq(sn["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(s1_tx_wr[8:16]),
wbuf[1].eq(s1_tx_wr[0:8])]
m.d.sync += xfer_start.eq(1)
m.next = "UDP_UPDPTR_W"
with m.State("UDP_UPDPTR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.next = "UDP_SEND"
write_reg("UDP_SEND", sn["CR"], [_CR_SEND], "UDP_DONE")
with m.State("UDP_DONE"):
m.d.sync += self.udp_test_busy.eq(0)
m.next = "IDLE"
# ── UDP test receive (configurable socket) ───────────────────────
# Poll-driven (the shell pulses udp_rx_req). Read Sn_RX_RSR; if 0,
# pulse udp_rx_none. Otherwise read Sn_RX_RD, read the 8-byte WIZnet
# UDP header ([srcIP 4][srcPort 2][len 2]), stream `len` payload
# bytes out on udp_rx_* (ring-wrapping), advance Sn_RX_RD by 8+len,
# RECV, clear the socket IR. MACRAW socket-0 RX is untouched.
if self._enable_udp:
with m.State("UDP_RX_RSR"): # read Sn_RX_RSR (2 B)
m.d.sync += self.udp_rx_busy.eq(1)
m.d.sync += [xfer_addr.eq(sn["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 = "UDP_RX_RSR_W"
with m.State("UDP_RX_RSR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.d.sync += sn_rx_rsr.eq(Cat(rbuf[1], rbuf[0]))
m.next = "UDP_RX_CHK"
with m.State("UDP_RX_CHK"):
with m.If(sn_rx_rsr == 0):
m.d.sync += [self.udp_rx_none.eq(1),
self.udp_rx_busy.eq(0)]
m.next = "IDLE"
with m.Else():
m.next = "UDP_RX_RD"
with m.State("UDP_RX_RD"): # read Sn_RX_RD (2 B)
m.d.sync += [xfer_addr.eq(sn["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 = "UDP_RX_RD_W"
with m.State("UDP_RX_RD_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.d.sync += sn_rx_rd.eq(Cat(rbuf[1], rbuf[0]))
m.next = "UDP_RX_HDR"
with m.State("UDP_RX_HDR"): # read 8-byte UDP header (wrap)
m.d.sync += [xfer_addr.eq(sn["RX_BASE"] + (sn_rx_rd & _SN_MASK)),
xfer_rw.eq(0), xfer_stream.eq(0), xfer_sread.eq(0),
xfer_direct.eq(0), xfer_len.eq(8), xfer_wrap.eq(1),
xfer_wbase.eq(sn["RX_BASE"]),
xfer_wend.eq(sn["RX_BASE"] + _SN_MASK + 1)]
m.d.sync += xfer_start.eq(1)
m.next = "UDP_RX_HDR_W"
with m.State("UDP_RX_HDR_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
# header[6:8] = payload length (big-endian)
m.d.sync += sn_pkt_len.eq(Cat(rbuf[7], rbuf[6]))
m.next = "UDP_RX_FRAME"
with m.State("UDP_RX_FRAME"): # stream `len` payload bytes out
m.d.sync += [xfer_addr.eq(sn["RX_BASE"] + ((sn_rx_rd + 8) & _SN_MASK)),
xfer_rw.eq(0), xfer_stream.eq(0), xfer_sread.eq(1),
xfer_direct.eq(0), xfer_rcount.eq(sn_pkt_len),
xfer_wrap.eq(1), xfer_wbase.eq(sn["RX_BASE"]),
xfer_wend.eq(sn["RX_BASE"] + _SN_MASK + 1),
udp_rx_streaming.eq(1)]
m.d.sync += xfer_start.eq(1)
m.next = "UDP_RX_FRAME_W"
with m.State("UDP_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),
udp_rx_streaming.eq(0)]
m.next = "UDP_RX_UPDRD"
with m.State("UDP_RX_UPDRD"): # Sn_RX_RD += 8 + len, write back
m.d.sync += [xfer_addr.eq(sn["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((sn_rx_rd + 8 + sn_pkt_len)[8:16]),
wbuf[1].eq((sn_rx_rd + 8 + sn_pkt_len)[0:8])]
m.d.sync += xfer_start.eq(1)
m.next = "UDP_RX_UPDRD_W"
with m.State("UDP_RX_UPDRD_W"):
m.d.sync += xfer_start.eq(0)
with m.If(xfer_done):
m.next = "UDP_RX_RECV"
write_reg("UDP_RX_RECV", sn["CR"], [_CR_RECV], "UDP_RX_CLR_IR")
write_reg("UDP_RX_CLR_IR", sn["IR"], [0x04], "UDP_RX_DONE")
with m.State("UDP_RX_DONE"):
m.d.sync += self.udp_rx_busy.eq(0)
m.next = "IDLE"
return m
@@ -831,6 +1134,217 @@ if __name__ == "__main__":
sim.run()
# ── UDP-test path (configurable socket, enable_udp_test=True) ───────────
# Reuses the same address-agnostic bus model (it reads/writes model_mem at
# whatever IDM_AR is set), so it transparently covers the IP-config and
# socket registers. Verifies: init programs GAR/SUBR/SIPR + opens the
# socket in UDP mode; a udp_send_req writes Sn_DIPR to the runtime dest,
# streams the payload into the socket TX buffer, advances Sn_TX_WR, issues
# SEND; and a udp_rx_req reads a datagram (header + payload) back out.
SRC_IP, SUBNET, GATEWAY = "10.0.0.7", "255.255.255.0", "10.0.0.1"
DEF_DST, DST_PORT, SRC_PORT = "192.168.1.100", 6464, 40000
UDP_SOCK = 3
S = _socket_addrs(UDP_SOCK)
dut2 = W5100ParallelMaster(
strobe_cycles=3, reset_cycles=10, enable_udp_test=True,
udp_socket=UDP_SOCK, src_ip=SRC_IP, subnet=SUBNET, gateway=GATEWAY,
dst_ip=DEF_DST, src_port=SRC_PORT, dst_port=DST_PORT)
writes2, model_mem2 = [], {}
async def w5100_model2(ctx):
idm_ar = 0
mr = 0
prev_wr = prev_rd = 1
async for vals in ctx.tick("sync").sample(
dut2.cs_n, dut2.rd_n, dut2.wr_n,
dut2.bus_addr, dut2.bus_data_o):
cs, rd, wr, a, do = vals[-5:]
ai = (mr >> 1) & 1
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_mem2.get(idm_ar, 0)
ctx.set(dut2.bus_data_i, val)
if cs == 0 and prev_wr == 0 and wr == 1:
if a == _A_MR:
mr = do; writes2.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:
model_mem2[idm_ar] = do; writes2.append((idm_ar, do))
if ai: idm_ar = (idm_ar + 1) & 0xFFFF
if cs == 0 and prev_rd == 0 and rd == 1 and a == _A_DR and ai:
idm_ar = (idm_ar + 1) & 0xFFFF
prev_wr, prev_rd = wr, rd
def _u16(addr):
return (model_mem2.get(addr, 0) << 8) | model_mem2.get(addr + 1, 0)
async def testbench2(ctx):
ctx.set(dut2.par, PAR)
ctx.set(dut2.udp_pl_valid, 0)
await ctx.tick("sync").repeat(2)
# U1: init → IP stack + socket-1 UDP open.
ctx.set(dut2.init_req, 1)
await ctx.tick("sync").repeat(1)
ctx.set(dut2.init_req, 0)
done = False
for _ in range(6000):
await ctx.tick("sync").repeat(1)
if ctx.get(dut2.init_done):
done = True; break
if not done:
errors.append("UDP: init_done never asserted")
checks = {
_GAR0: _ip_bytes(GATEWAY), _SUBR0: _ip_bytes(SUBNET),
_SIPR0: _ip_bytes(SRC_IP),
}
for base, octets in checks.items():
got = [model_mem2.get(base + i) for i in range(4)]
if got != octets:
errors.append(f"UDP init {base:#06x}: got {got}, want {octets}")
if model_mem2.get(S["MR"]) != _S1_MR_UDP:
errors.append(f"UDP init Sn_MR: got {model_mem2.get(S['MR'])}, want UDP")
if _u16(S["PORT"]) != SRC_PORT:
errors.append(f"UDP init Sn_PORT: {_u16(S['PORT'])} != {SRC_PORT}")
if _u16(S["DPORT"]) != DST_PORT:
errors.append(f"UDP init Sn_DPORT: {_u16(S['DPORT'])} != {DST_PORT}")
if (S["CR"], _CR_OPEN) not in writes2:
errors.append("UDP init: socket OPEN not issued")
print(f"U1 init (socket {UDP_SOCK}): GAR/SUBR/SIPR set, Sn_MR=UDP, "
f"Sn_PORT={_u16(S['PORT'])} Sn_DPORT={_u16(S['DPORT'])} OPEN issued")
# U2: send one datagram to a runtime dest IP with a payload.
DST = "192.168.1.55"
PAYLOAD = list(b"HELLO-ETH")
ctx.set(dut2.udp_dst_ip, int.from_bytes(bytes(_ip_bytes(DST)), "big"))
idx = 0
ctx.set(dut2.udp_pl_data, PAYLOAD[0])
ctx.set(dut2.udp_pl_last, 1 if len(PAYLOAD) == 1 else 0)
ctx.set(dut2.udp_pl_valid, 1)
ctx.set(dut2.udp_send_req, 1)
await ctx.tick("sync").repeat(1)
ctx.set(dut2.udp_send_req, 0)
sent = False
for _ in range(4000):
await ctx.tick("sync").repeat(1)
if ctx.get(dut2.udp_pl_ready) and ctx.get(dut2.udp_pl_valid):
idx += 1
if idx < len(PAYLOAD):
ctx.set(dut2.udp_pl_data, PAYLOAD[idx])
ctx.set(dut2.udp_pl_last, 1 if idx == len(PAYLOAD) - 1 else 0)
else:
ctx.set(dut2.udp_pl_valid, 0)
if model_mem2.get(S["CR"]) == _CR_SEND:
sent = True; break
ctx.set(dut2.udp_pl_valid, 0)
if not sent:
errors.append("UDP send: SEND command never issued")
for _ in range(50):
await ctx.tick("sync").repeat(1)
if ctx.get(dut2.udp_test_busy) == 0:
break
dip = [model_mem2.get(S["DIPR"] + i) for i in range(4)]
if dip != _ip_bytes(DST):
errors.append(f"UDP send DIPR: got {dip}, want {_ip_bytes(DST)}")
buf = [model_mem2.get(S["TX_BASE"] + i) for i in range(len(PAYLOAD))]
if buf != PAYLOAD:
errors.append(f"UDP send payload: {buf} != {PAYLOAD}")
if _u16(S["TX_WR"]) != len(PAYLOAD):
errors.append(f"UDP send Sn_TX_WR: {_u16(S['TX_WR'])} != {len(PAYLOAD)}")
print(f"U2 send: DIPR={dip} payload={bytes(buf)!r} "
f"Sn_TX_WR={_u16(S['TX_WR'])} SEND={sent}")
# ── U3: receive a UDP datagram from the socket RX buffer ─────────────
# WIZnet UDP RX format: [srcIP(4)][srcPort(2)][len(2)][payload].
model_mem2[S["CR"]] = 0x00
SRC = _ip_bytes("192.168.1.9")
SPORT = 6464
RPAYLOAD = list(b"WORLD!")
rlen = len(RPAYLOAD)
rx_rd0 = 0x0000
hdr = SRC + [SPORT >> 8, SPORT & 0xFF, (rlen >> 8) & 0xFF, rlen & 0xFF]
packet = hdr + RPAYLOAD
for i, b in enumerate(packet):
model_mem2[S["RX_BASE"] + ((rx_rd0 + i) & _SN_MASK)] = b
total = len(packet)
model_mem2[S["RX_RSR"]] = (total >> 8) & 0xFF
model_mem2[S["RX_RSR"] + 1] = total & 0xFF
model_mem2[S["RX_RD"]] = (rx_rd0 >> 8) & 0xFF
model_mem2[S["RX_RD"] + 1] = rx_rd0 & 0xFF
ctx.set(dut2.udp_rx_ready, 1)
rx_got = []
got_none = [False]
ctx.set(dut2.udp_rx_req, 1)
# hold req until the master goes busy, then drop it
for _ in range(200):
await ctx.tick("sync").repeat(1)
if ctx.get(dut2.udp_rx_busy):
ctx.set(dut2.udp_rx_req, 0); break
recvd = False
for _ in range(3000):
await ctx.tick("sync").repeat(1)
if ctx.get(dut2.udp_rx_valid) and ctx.get(dut2.udp_rx_ready):
rx_got.append(ctx.get(dut2.udp_rx_data))
if ctx.get(dut2.udp_rx_none):
got_none[0] = True
if model_mem2.get(S["CR"]) == _CR_RECV:
recvd = True; break
for _ in range(30):
await ctx.tick("sync").repeat(1)
ctx.set(dut2.udp_rx_ready, 0)
if rx_got != RPAYLOAD:
errors.append(f"U3 RX payload: {bytes(rx_got)!r} != {bytes(RPAYLOAD)!r}")
new_rd = _u16(S["RX_RD"])
if new_rd != rx_rd0 + total:
errors.append(f"U3 RX_RD advance: got {new_rd}, want {rx_rd0 + total}")
if model_mem2.get(S["CR"]) != _CR_RECV:
errors.append("U3 RX: RECV command not issued")
if got_none[0]:
errors.append("U3 RX: udp_rx_none pulsed despite data present")
print(f"U3 recv: payload={bytes(rx_got)!r} RX_RD={new_rd} "
f"RECV={model_mem2.get(S['CR'])==_CR_RECV}")
# ── U4: RX poll with an empty buffer → udp_rx_none, no stream ────────
model_mem2[S["RX_RSR"]] = 0
model_mem2[S["RX_RSR"] + 1] = 0
model_mem2[S["CR"]] = 0x00
none_seen = False
streamed = False
ctx.set(dut2.udp_rx_req, 1)
for _ in range(200):
await ctx.tick("sync").repeat(1)
if ctx.get(dut2.udp_rx_busy):
ctx.set(dut2.udp_rx_req, 0); break
for _ in range(200):
await ctx.tick("sync").repeat(1)
if ctx.get(dut2.udp_rx_none):
none_seen = True
if ctx.get(dut2.udp_rx_valid):
streamed = True
if none_seen and not ctx.get(dut2.udp_rx_busy):
break
if not none_seen:
errors.append("U4 RX-empty: udp_rx_none never pulsed")
if streamed:
errors.append("U4 RX-empty: streamed data despite empty buffer")
if model_mem2.get(S["CR"]) == _CR_RECV:
errors.append("U4 RX-empty: RECV issued on empty buffer")
print(f"U4 recv-empty: none={none_seen} streamed={streamed}")
sim2 = Simulator(dut2)
sim2.add_clock(Period(MHz=24), domain="sync")
sim2.add_testbench(testbench2)
sim2.add_process(w5100_model2)
sim2.run()
if errors:
print("\nFAILURES:")
for e in errors: