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:
+52
-7
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user