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
+19 -7
View File
@@ -148,14 +148,26 @@ class ExiCapture(Elaboratable):
# clock for DMA reads, so when CS deasserts mid-stream a few unsent
# bytes remain. On CS-fall (frame_start) drain tx_fifo to empty before
# the new transaction's data phase, so stale bytes never reach MISO.
flushing = Signal()
m.d.comb += tx_fifo.r_en.eq(
(spi.tx_load & (txld_cnt >= 2)) | (flushing & tx_fifo.r_rdy)
)
# The flush must NOT read `tx_fifo.r_rdy`. r_rdy is derived from the
# gray-coded FIFO pointers that `r_en` advances, so any r_rdy → r_en
# dependence (either gating the drain with `& r_rdy`, or deasserting a
# `flushing` flag on `~r_rdy`) closes a long capture-domain loop
# (consume_ptr → gray → r_rdy → flush → r_en → consume_ptr) — the
# measured critical path capping capture_clk. Instead, drain for a
# FIXED number of cycles: the FIFO is only `tx_depth` deep, so pulsing
# r_en for `tx_depth + 1` cycles empties it regardless of occupancy
# (the FIFO advances its read pointer only on r_en & r_rdy internally,
# so pulses past empty are harmless). This removes the pointer
# feedback from r_en entirely; the only remaining r_en source is the
# legitimate data-byte pop.
drain = Signal(range(self._tx_depth + 2))
with m.If(spi.frame_start):
m.d.capture += flushing.eq(1)
with m.Elif(~tx_fifo.r_rdy):
m.d.capture += flushing.eq(0)
m.d.capture += drain.eq(self._tx_depth + 1)
with m.Elif(drain != 0):
m.d.capture += drain.eq(drain - 1)
m.d.comb += tx_fifo.r_en.eq(
(spi.tx_load & (txld_cnt >= 2)) | (drain != 0)
)
with m.If(spi.frame_start):
m.d.capture += txld_cnt.eq(0)