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
+49 -3
View File
@@ -24,6 +24,7 @@ from exi_bba.w5500_spi_master import W5500SPIMaster
from exi_bba.w5100_parallel_master import W5100ParallelMaster
from exi_bba.status_panel import StatusPanel
from exi_bba.uart_console import UARTConsole
from exi_bba.uart_shell import UARTShell
from amaranth.lib.cdc import FFSynchronizer
@@ -48,7 +49,11 @@ class BBATop(Elaboratable):
"""
def __init__(self, eth="w5100", reset_cycles=24000,
status_panel=False, uart_console=False):
status_panel=False, uart_console=False, uart_shell=False,
udp_socket=3, udp_src_ip="192.168.1.123",
udp_subnet="255.255.255.0", udp_gateway="192.168.1.1",
udp_dst_ip="192.168.1.100",
udp_src_port=40000, udp_dst_port=6464):
# Ethernet back-end: "w5100" (indirect parallel bus, reaches the EXI
# ceiling) or "w5500" (SPI, ~12 Mbit/s). Both expose the identical
# tx/rx/init/par interface, so only the physical pins differ.
@@ -64,6 +69,19 @@ class BBATop(Elaboratable):
# uart_rx ← FT2232H Channel B (net UART_TXD, pin 18) — net names are
# FTDI-perspective, so this is not a naming mismatch, see synth.py.
self._uart_console = uart_console
# Optional interactive UART command shell (bring-up UDP test on the
# W5100's socket 1). Shares the UART pins with the console, so only one
# may be enabled; the UDP path needs the W5100 back-end. See uart_shell.
self._uart_shell = uart_shell
if uart_console and uart_shell:
raise ValueError("uart_console and uart_shell share the UART pins; "
"enable only one")
if uart_shell and eth != "w5100":
raise ValueError("uart_shell UDP test requires eth='w5100'")
self._udp_cfg = dict(udp_socket=udp_socket, src_ip=udp_src_ip,
subnet=udp_subnet, gateway=udp_gateway,
dst_ip=udp_dst_ip, src_port=udp_src_port,
dst_port=udp_dst_port)
# EXI (GC side)
self.exi_clk = Signal(init=1)
@@ -99,7 +117,7 @@ class BBATop(Elaboratable):
self.panel_led = Signal(5) # to onboard LEDs (see StatusPanel)
self.panel_btn = Signal(3) # from onboard button(s)
if uart_console:
if uart_console or uart_shell:
self.uart_tx = Signal(init=1) # FPGA → PC (FT2232H Channel B)
self.uart_rx = Signal(init=1) # PC → FPGA
@@ -161,7 +179,9 @@ class BBATop(Elaboratable):
drain = TXFrameDrain()
eth = (W5500SPIMaster(reset_cycles=self._reset_cycles)
if self._eth == "w5500"
else W5100ParallelMaster(reset_cycles=self._reset_cycles))
else W5100ParallelMaster(reset_cycles=self._reset_cycles,
enable_udp_test=self._uart_shell,
**self._udp_cfg))
m.submodules.cap = cap
m.submodules.reg = reg
@@ -335,6 +355,32 @@ class BBATop(Elaboratable):
console.uart_rx .eq(self.uart_rx),
]
if self._uart_shell:
# Interactive command shell driving the W5100 socket-1 UDP test.
# `eth` is the W5100 master built with enable_udp_test above.
shell = UARTShell()
m.submodules.shell = shell
m.d.comb += [
self.uart_tx .eq(shell.uart_tx),
shell.uart_rx .eq(self.uart_rx),
# send
eth.udp_send_req .eq(shell.udp_send_req),
eth.udp_dst_ip .eq(shell.udp_dst_ip),
eth.udp_pl_data .eq(shell.udp_pl_data),
eth.udp_pl_valid .eq(shell.udp_pl_valid),
eth.udp_pl_last .eq(shell.udp_pl_last),
shell.udp_pl_ready .eq(eth.udp_pl_ready),
shell.udp_test_busy .eq(eth.udp_test_busy),
# receive (reply)
eth.udp_rx_req .eq(shell.udp_rx_req),
shell.udp_rx_busy .eq(eth.udp_rx_busy),
shell.udp_rx_none .eq(eth.udp_rx_none),
shell.udp_rx_data .eq(eth.udp_rx_data),
shell.udp_rx_valid .eq(eth.udp_rx_valid),
shell.udp_rx_eof .eq(eth.udp_rx_eof),
eth.udp_rx_ready .eq(shell.udp_rx_ready),
]
if need_ready:
with m.If(eth.init_done):
m.d.sync += ready.eq(1)