fix(w5100): revert buffer base to 0x4000/0x6000 — IDM decodes only 15 bits

Reverts the 0x8000/0xC000 change from 162dbf7, which was WRONG and actively
broke transmission. The W5100S datasheet pseudo-code quotes 0x8000 (TX) /
0xC000 (RX) for the memory blocks, but the PARALLEL/indirect (IDM) interface
only decodes a 15-bit offset. Bench-proven with an address-aliasing probe:
writing distinct values to each candidate base and reading them back showed
0xC000 and 0x4000 are the SAME cell (0xC000 & 0x7FFF == 0x4000) while 0x6000
holds its own value.

Consequence of the bad base: every payload byte written to 0x9800+i aliased
down onto 0x0000+i, landing on SHAR (0x09-0x0E) and SIPR (0x0F-0x12) and
overwriting them with the payload. The chip then transmitted frames with a
garbage source MAC/IP, which the peer NIC drops without counting -- the
"SEND_OK but rx_packets=0" symptom, and the bogus SIPR0 readback (0x50 was
literally payload byte 15, 'P').

The NETLCKR unlock from 9c4bc7a is a genuine W5100S requirement and stays.

Also:
- w5100_udptest.py: self-contained configure+send+readback probe (and the
  address-aliasing probe that found this), reports over UART.
- scripts/build-flash.sh: sweep N seeds and flash the best (capture timing is
  seed-dependent, so the sweep is mandatory).
- scripts/uart-console.py: interactive UART console, plus --send/--read
  one-shot modes for scripting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 18:56:38 +00:00
parent 5d9880f9c5
commit 394b677c8f
5 changed files with 434 additions and 12 deletions
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Build the current gateware over N nextpnr seeds and flash the best result.
#
# The capture domain (54 MHz) is routing-dominated and only closes on a minority
# of seeds, so a sweep is mandatory — see CLAUDE.md. synth.py ranks the seeds by
# capture_clk fmax and reports the winner; this flashes that one.
#
# ./scripts/build-flash.sh # 8 seeds (default), build + flash
# ./scripts/build-flash.sh 12 # 12 seeds
# ./scripts/build-flash.sh 8 --no-flash # build only, don't touch the board
#
# The socket-3 UDP bring-up test identity is set from these env vars (override
# to match your LAN before running):
# UDP_SRC_IP UDP_SUBNET UDP_GATEWAY UDP_DST_IP UDP_SRC_PORT UDP_DST_PORT UDP_SOCKET
set -euo pipefail
cd "$(dirname "$0")/.."
SEEDS="${1:-8}"
NOFLASH="${2:-}"
export UDP_SRC_IP="${UDP_SRC_IP:-192.168.2.99}"
export UDP_SUBNET="${UDP_SUBNET:-255.255.255.0}"
export UDP_GATEWAY="${UDP_GATEWAY:-192.168.2.1}"
export UDP_DST_IP="${UDP_DST_IP:-192.168.2.112}"
export UDP_SRC_PORT="${UDP_SRC_PORT:-40000}"
export UDP_DST_PORT="${UDP_DST_PORT:-6464}"
export UDP_SOCKET="${UDP_SOCKET:-3}"
LOG="$(mktemp -t bbabuild.XXXXXX.log)"
echo "==> building $SEEDS seeds (log: $LOG)"
echo " UDP: ${UDP_SRC_IP}:${UDP_SRC_PORT} -> ${UDP_DST_IP}:${UDP_DST_PORT} (socket ${UDP_SOCKET})"
python -m exi_bba.synth --seeds "$SEEDS" 2>&1 | tee "$LOG"
# synth.py prints: "Bitstream for best seed: build/seedN/top.bin"
BIN="$(grep -aoE 'build/seed[0-9]+/top\.bin' "$LOG" | tail -1 || true)"
if [ -z "$BIN" ] || [ ! -f "$BIN" ]; then
echo "!! no best-seed bitstream found — check the log above" >&2
exit 1
fi
grep -aE 'Best seed' "$LOG" | tail -1 || true
if [ "$NOFLASH" = "--no-flash" ]; then
echo "==> built $BIN (not flashing, --no-flash given)"
exit 0
fi
echo "==> flashing $BIN"
iceprog "$BIN" 2>&1 | grep -aE 'VERIFY|cdone|Error|error|Bye' || true
echo "==> done"
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Interactive UART console to the re-bba-rb board (FT2232H channel B).
The BBA default build runs an interactive shell on this port ("rebbarb> "):
help list commands
udp broadcast [msg] send a UDP broadcast on the test socket
udp unicast <ip> [msg] send a UDP datagram to <ip>
The bring-up diagnostics (w5100_selftest / w5100_udptest / w5100_regdump) just
stream a status line on the same port, so this works for those too.
Usage:
sudo ./scripts/uart-console.py # /dev/ttyUSB1 @ 115200
sudo ./scripts/uart-console.py /dev/ttyUSB0 115200
./scripts/uart-console.py --send "udp broadcast hi" # one-shot, no tty
./scripts/uart-console.py --read 5 # just dump 5s of output
Channel A of the FT2232H is the MPSSE/iceprog port; the UART is channel B, which
is normally the SECOND ttyUSB (…USB1). Needs read/write on the device node —
run under sudo, or add yourself to the port's group.
Quit the interactive console with Ctrl-] (or Ctrl-C).
"""
import sys, time, argparse
try:
import serial
except ImportError:
sys.exit("needs pyserial: pip install pyserial")
def main():
ap = argparse.ArgumentParser(add_help=True)
ap.add_argument("port", nargs="?", default="/dev/ttyUSB1")
ap.add_argument("baud", nargs="?", type=int, default=115200)
ap.add_argument("--send", metavar="CMD",
help="send one command, print the reply, exit")
ap.add_argument("--read", metavar="SECONDS", type=float,
help="just read for N seconds and exit (no tty needed)")
args = ap.parse_args()
try:
ser = serial.Serial(args.port, args.baud, timeout=0.1)
except serial.SerialException as e:
sys.exit(f"cannot open {args.port}: {e}\n(try: sudo {' '.join(sys.argv)})")
# ── one-shot modes (safe in scripts / no controlling terminal) ───────────
if args.read is not None:
end = time.time() + args.read
while time.time() < end:
data = ser.read(256)
if data:
sys.stdout.write(data.decode("ascii", "replace"))
sys.stdout.flush()
return
if args.send is not None:
time.sleep(0.2); ser.reset_input_buffer()
ser.write(args.send.encode() + b"\r")
end = time.time() + 2.0
buf = b""
while time.time() < end:
buf += ser.read(256)
sys.stdout.write(buf.decode("ascii", "replace"))
return
# ── interactive console ─────────────────────────────────────────────────
import termios, tty, select
print(f"[uart-console] {args.port} @ {args.baud} 8N1 — Ctrl-] to quit\r")
fd = sys.stdin.fileno()
try:
old = termios.tcgetattr(fd)
except termios.error:
old = None
try:
if old is not None:
tty.setraw(fd)
while True:
r, _, _ = select.select([sys.stdin, ser], [], [], 0.05)
if ser in r:
data = ser.read(4096)
if data:
sys.stdout.write(data.decode("ascii", "replace"))
sys.stdout.flush()
if sys.stdin in r:
ch = sys.stdin.buffer.read(1)
if not ch or ch == b"\x1d": # Ctrl-]
break
if ch == b"\n": # send CR, the shell wants it
ch = b"\r"
ser.write(ch)
except KeyboardInterrupt:
pass
finally:
if old is not None:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
ser.close()
print("\r\n[uart-console] closed")
if __name__ == "__main__":
main()