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
+52 -7
View File
@@ -62,6 +62,20 @@ from exi_bba.bba_top import BBATop
# iCE40UP5K's dedicated SB_RGBA_DRV pins 39/40/41 — fixed by the chip
# package on any board, not board-specific, so no resource needed here.
# nextpnr P&R options. The binding constraint is the isolated 54 MHz capture
# domain (the SPI Mode-3 bit engine); the 24 MHz sync domain has wide margin.
#
# TRIED (2026-08 shell build) and REVERTED — prioritising routing toward the
# critical capture paths did NOT help: `--router router2 --tmg-ripup
# --placer-heap-timingweight 30 --placer-heap-critexp 4` left capture at
# 41-49 MHz (best 49.07, still < 54.02) across seeds AND eroded the slow-clock
# margin (24.4-25.9 MHz vs the 28-33 that plain --opt-timing gives). The
# capture shortfall is congestion + inherent path delay at this LC level, not a
# routing-priority tuning problem, so weighting P&R toward it only robs the
# sync domain. Plain --opt-timing is the better baseline.
_PNR_TIMING_OPTS = "--opt-timing"
class IceBreakerPlatform(LatticeICE40Platform):
device = "iCE40UP5K"
package = "SG48"
@@ -185,8 +199,10 @@ class BBATopSynth(BBATop):
o_RGB2=Signal(name="rgb_b"),
)
# ── UART debug console → FT2232H Channel B ─────────────────────
if self._uart_console:
# ── UART debug console/shell → FT2232H Channel B ───────────────
# Both the event-log console and the interactive command shell use
# the same two UART pins (only one may be enabled at a time).
if self._uart_console or self._uart_shell:
uart = platform.request("uart", 0)
m.d.comb += [
uart.tx.o .eq(self.uart_tx),
@@ -205,13 +221,42 @@ class BBATopSynth(BBATop):
# build/top.bin is the result of the last (or best) seed tried.
if __name__ == "__main__":
import os
do_flash = "--flash" in sys.argv
# The interactive UART shell + UDP bring-up test is the default build;
# pass --console to flash the old event-log console instead (they share the
# UART pins, so only one can be built).
use_shell = "--console" not in sys.argv
n_seeds = next((int(sys.argv[i+1]) for i, a in enumerate(sys.argv)
if a == "--seeds"), 1)
# UDP bring-up test config (only used with --shell). Set these env vars to
# match your LAN — src_ip/subnet/gateway are the board's own identity for
# the W5100 IP stack; the unicast destination is typed at runtime.
udp_kw = dict(
udp_socket = int(os.environ.get("UDP_SOCKET", "3")),
udp_src_ip = os.environ.get("UDP_SRC_IP", "192.168.1.123"),
udp_subnet = os.environ.get("UDP_SUBNET", "255.255.255.0"),
udp_gateway = os.environ.get("UDP_GATEWAY", "192.168.1.1"),
udp_dst_ip = os.environ.get("UDP_DST_IP", "192.168.1.100"),
udp_src_port = int(os.environ.get("UDP_SRC_PORT", "40000")),
udp_dst_port = int(os.environ.get("UDP_DST_PORT", "6464")),
)
def make_dut():
if use_shell:
return BBATopSynth(status_panel=True, uart_shell=True, **udp_kw)
return BBATopSynth(status_panel=True, uart_console=True)
print(f"Synthesizing BBATop for {IceBreakerPlatform.device}-"
f"{IceBreakerPlatform.package} "
f"(do_program={do_flash}, seeds=1..{n_seeds})")
f"(do_program={do_flash}, seeds=1..{n_seeds}, "
f"{'shell+UDP' if use_shell else 'console'})")
if use_shell:
print(f" UDP: socket {udp_kw['udp_socket']}, "
f"src {udp_kw['udp_src_ip']}:{udp_kw['udp_src_port']} "
f"gw {udp_kw['udp_gateway']} mask {udp_kw['udp_subnet']} "
f"dst-port {udp_kw['udp_dst_port']}")
best_seed = 1
best_fmax = 0.0
@@ -220,7 +265,7 @@ if __name__ == "__main__":
print(f"\n{'='*60}")
print(f" Seed {seed}/{n_seeds}")
print(f"{'='*60}")
opts = (f"--opt-timing --seed {seed} --timing-allow-fail")
opts = f"{_PNR_TIMING_OPTS} --seed {seed} --timing-allow-fail"
# A Platform instance can only be built ONCE — amaranth's
# TemplatedPlatform.prepare() does `assert not self._prepared`. Reusing
@@ -244,7 +289,7 @@ if __name__ == "__main__":
build_ok = True
try:
platform.build(BBATopSynth(status_panel=True, uart_console=True), do_program=False,
platform.build(make_dut(), do_program=False,
verbose=True, nextpnr_opts=opts, build_dir=build_dir)
except Exception as exc:
# nextpnr exits non-zero even with --timing-allow-fail on some
@@ -334,12 +379,12 @@ if __name__ == "__main__":
"miss bits. Re-run with more seeds (--seeds 16) or reduce logic.")
elif do_flash:
print(f"\nFlashing with seed {best_seed}...")
opts = f"--opt-timing --seed {best_seed} --timing-allow-fail"
opts = f"{_PNR_TIMING_OPTS} --seed {best_seed} --timing-allow-fail"
# Fresh platform again — the sweep above already consumed one per seed.
# Reuse the best seed's own build dir so the flashed bitstream is the
# one that was actually measured.
IceBreakerPlatform().build(
BBATopSynth(status_panel=True, uart_console=True), do_program=True,
make_dut(), do_program=True,
verbose=True, nextpnr_opts=opts, build_dir=f"build/seed{best_seed}")
print("Done.")