"""Interactive UART command shell (sync domain, 24 MHz). A tiny line-oriented console for bring-up over the FT2232H channel-B UART (115200 8N1). Prints a ``rebbarb> `` prompt, echoes typed characters (with backspace editing), and on Enter parses one command: help list commands udp unicast [msg] send a UDP datagram to :DST_PORT (W5100 ARPs) udp broadcast [msg] send a UDP datagram to 255.255.255.255:DST_PORT `msg` is optional; when omitted a built-in default payload is sent. The shell drives the W5100 master's runtime UDP-send interface (`udp_send_req`, `udp_dst_ip`, and the `udp_pl_*` payload stream) — it does not touch the MACRAW BBA path. Grammar notes (it's a debug shell, not bash): * commands and keywords are case-insensitive; * tokens are separated by exactly one space; * the destination is parsed as a dotted IPv4 literal; * everything after the address token (unicast) / keyword (broadcast) is the raw payload, preserving case and embedded spaces, to end of line. On the PC: udp unicast → nc -ul 6464 (or: sudo tcpdump -n udp port 6464) udp broadcast → nc -ul 6464 (limited broadcast reaches the segment) """ from amaranth import * from amaranth.lib.cdc import FFSynchronizer from amaranth.lib.memory import Memory __all__ = ["UARTShell"] # ── Printable message ROM ────────────────────────────────────────────────── _MSGS = { "BANNER": b"\r\nre-bba-rb ethernet test\r\n", "PROMPT": b"rebbarb> ", "HELP": (b"commands:\r\n" b" help show this\r\n" b" udp unicast [msg] send UDP to \r\n" b" udp broadcast [msg] send UDP broadcast\r\n"), "SENT": b"sent\r\n", "ERR": b"? (try 'help')\r\n", "BADIP": b"bad ip\r\n", "BUSY": b"busy\r\n", "CRLF": b"\r\n", "BKSP": b"\b \b", "RXPFX": b"rx ", "TIMEOUT": b"timeout\r\n", } _ORDER = list(_MSGS) _ROM = b"".join(_MSGS[k] for k in _ORDER) _OFF = {} _acc = 0 for _k in _ORDER: _OFF[_k] = (_acc, _acc + len(_MSGS[_k])) _acc += len(_MSGS[_k]) _DEFAULT_PAYLOAD = b"rebbarb-udp-test" # return codes for the shared print routine _RET_PROMPT = 0 # after printing, (re)issue the prompt then read input _RET_INPUT = 1 # after printing, go straight back to reading input _RET_PARSE = 2 # after printing (the CR echo), parse the line _RET_RXWAIT = 3 # after printing, wait for a UDP reply (with timeout) _RET_RXBODY = 4 # after printing "rx ", stream the reply payload class UARTShell(Elaboratable): def __init__(self, clk_freq=24_000_000, baud_rate=115_200, default_payload=_DEFAULT_PAYLOAD, lbuf_len=64, reply_timeout_cycles=24_000_000, poll_gap_cycles=24_000): self._div = round(clk_freq / baud_rate) self._defpl = list(default_payload) self._lbuf_n = lbuf_len # After a send, wait this many sync cycles for a reply before giving up # (default ~1 s at 24 MHz) so the console never hangs; poll the socket # RX every `poll_gap_cycles` (~1 ms) in between. self._to_cyc = reply_timeout_cycles self._gap_cyc = poll_gap_cycles # UART pins self.uart_tx = Signal(init=1) self.uart_rx = Signal(init=1) # W5100 UDP-send interface (drive the W5100ParallelMaster) self.udp_send_req = Signal() self.udp_dst_ip = Signal(32) self.udp_pl_data = Signal(8) self.udp_pl_valid = Signal() self.udp_pl_last = Signal() self.udp_pl_ready = Signal() self.udp_test_busy = Signal() # W5100 UDP-receive interface (poll for a reply after each send) self.udp_rx_req = Signal() self.udp_rx_busy = Signal() self.udp_rx_none = Signal() self.udp_rx_data = Signal(8) self.udp_rx_valid = Signal() self.udp_rx_eof = Signal() self.udp_rx_ready = Signal() def elaborate(self, platform): m = Module() div = self._div N = self._lbuf_n # Message ROM lives in a block RAM (SB_RAM40_4K) instead of LUTs — the # ~230-byte string table was the single biggest LUT consumer (a wide # addressed mux). PRINT already streams one byte per UART-byte-time, so # the block RAM's 1-cycle read latency is hidden (one prime cycle on # entry). The tiny default payload stays in logic (cheap, and keeps the # payload path combinational — no streaming-latency hazard). m.submodules.rom_mem = rom_mem = Memory( shape=unsigned(8), depth=len(_ROM), init=list(_ROM)) rom_rd = rom_mem.read_port() # synchronous (block RAM) defrom = Array([Const(b, 8) for b in self._defpl]) DEF_LEN = len(self._defpl) # ── UART TX (8N1) ───────────────────────────────────────────────── tx_cnt = Signal(range(div)) tx_bits = Signal(range(11)) tx_shreg = Signal(10, init=0b1111111111) tx_busy = Signal() tx_load = Signal() tx_byte = Signal(8) m.d.comb += [tx_busy.eq(tx_bits != 0), self.uart_tx.eq(tx_shreg[0])] with m.If(tx_load & ~tx_busy): m.d.sync += [tx_shreg.eq(Cat(Const(0, 1), tx_byte, Const(1, 1))), tx_bits.eq(10), tx_cnt.eq(div - 1)] with m.Elif(tx_busy): with m.If(tx_cnt == 0): m.d.sync += [tx_shreg.eq(Cat(tx_shreg[1:], Const(1, 1))), tx_bits.eq(tx_bits - 1), tx_cnt.eq(div - 1)] with m.Else(): m.d.sync += tx_cnt.eq(tx_cnt - 1) # ── UART RX (8N1, mid-bit sampling) ─────────────────────────────── rx_sync = Signal(init=1) rx_prev = Signal(init=1) rx_cnt = Signal(range(div)) rx_bits = Signal(range(9)) rx_shr = Signal(8) rx_byte = Signal(8) rx_valid = Signal() m.submodules += FFSynchronizer(self.uart_rx, rx_sync, init=1) m.d.sync += rx_valid.eq(0) with m.FSM(name="rx"): with m.State("IDLE"): m.d.sync += rx_prev.eq(rx_sync) with m.If(rx_prev & ~rx_sync): m.d.sync += rx_cnt.eq(div // 2 - 1) m.next = "START" with m.State("START"): with m.If(rx_cnt == 0): with m.If(~rx_sync): m.d.sync += [rx_cnt.eq(div - 1), rx_bits.eq(7)] m.next = "DATA" with m.Else(): m.next = "IDLE" with m.Else(): m.d.sync += rx_cnt.eq(rx_cnt - 1) with m.State("DATA"): with m.If(rx_cnt == 0): m.d.sync += [rx_shr.eq(Cat(rx_shr[1:], rx_sync)), rx_cnt.eq(div - 1)] with m.If(rx_bits == 0): m.next = "STOP" with m.Else(): m.d.sync += rx_bits.eq(rx_bits - 1) with m.Else(): m.d.sync += rx_cnt.eq(rx_cnt - 1) with m.State("STOP"): with m.If(rx_cnt == 0): with m.If(rx_sync): m.d.sync += [rx_byte.eq(rx_shr), rx_valid.eq(1)] m.next = "IDLE" with m.Else(): m.d.sync += rx_cnt.eq(rx_cnt - 1) # 1-deep RX holding register: decouples byte capture from the shell's # echo/print activity so a character arriving while the shell is busy # echoing (or printing a short response) isn't dropped. This keeps a # back-to-back paste of a command line intact — the incoming and echo # rates match, so at most one byte is ever in flight during an echo. # (A byte arriving during a long print, e.g. 'help' output, can still # be lost, but you don't type into a response.) rx_pending = Signal() rx_hold = Signal(8) with m.If(rx_valid): m.d.sync += [rx_hold.eq(rx_byte), rx_pending.eq(1)] # ── Line buffer (block RAM) ─────────────────────────────────────── # Stored in an SB_RAM40 rather than flip-flops; the parser reads it # SEQUENTIALLY (one byte per cycle) through a single registered read # port. This is what lets the 64-byte buffer AND its wide read muxes # leave the LC fabric (they were the dominant cost). `la` drives the # read address; `lbuf_rd.data` is valid the cycle after `la` settles. m.submodules.lbuf_mem = lbuf_mem = Memory( shape=unsigned(8), depth=N, init=[]) lbuf_wr = lbuf_mem.write_port() lbuf_rd = lbuf_mem.read_port() llen = Signal(range(N + 1)) la = Signal(range(N)) m.d.comb += lbuf_rd.addr.eq(la) m.d.comb += la.eq(0) # default; read states override per-cycle # write port: addr/data always presented; a store pulses `en` in INPUT. m.d.comb += [lbuf_wr.addr.eq(llen), lbuf_wr.data.eq(rx_hold), lbuf_wr.en.eq(0)] def low(c): return Mux((c >= ord('A')) & (c <= ord('Z')), c | 0x20, c) # Command templates, matched sequentially in the SCAN state. Padded to # a common length so a runtime index is always in range (guarded by the # per-template length so padding bytes are never actually compared). _TH, _TU, _TB, _TA = "help", "udp unicast ", "udp broadcast", "udp bcast" _TMAX = 14 def _tmpl(s): return Array([Const(ord(c), 8) for c in s.ljust(_TMAX, "\x00")]) THarr, TUarr, TBarr, TAarr = _tmpl(_TH), _tmpl(_TU), _tmpl(_TB), _tmpl(_TA) # ── Shared print routine ────────────────────────────────────────── pr_ptr = Signal(range(len(_ROM) + 1)) pr_end = Signal(range(len(_ROM) + 1)) pr_ret = Signal(3) to_ctr = Signal(range(self._to_cyc + 1)) # reply timeout countdown gap = Signal(range(self._gap_cyc + 1)) # inter-poll gap countdown # ROM read port is addressed by the print pointer (1-cycle latency). m.d.comb += rom_rd.addr.eq(pr_ptr) def start_print(msg_key, ret, nxt="PRINT_PRIME"): s, e = _OFF[msg_key] m.d.sync += [pr_ptr.eq(s), pr_end.eq(e), pr_ret.eq(ret)] m.next = nxt # ── Parse / send scratch ────────────────────────────────────────── ip_b = Array([Signal(8, name=f"ip{i}") for i in range(4)]) ip_pos = Signal(range(N + 1)) octet = Signal(9) noct = Signal(3) kwend = Signal(range(N + 1)) use_def = Signal() req_r = Signal() ech = Signal(8) # character currently being echoed # sequential command scan si = Signal(range(_TMAX + 2)) # scan index mh, muni, mbc, mbca = (Signal(init=1), Signal(init=1), Signal(init=1), Signal(init=1)) dh, dbc, dbca = Signal(), Signal(), Signal() # delimiter-ok captures # payload streaming (prefetch: `la` runs one byte ahead of pl_byte) pl_base = Signal(range(N + 1)) pl_len = Signal(range(N + 1)) j = Signal(range(N + 1)) pl_byte = Signal(8) m.d.comb += [self.udp_send_req.eq(req_r), self.udp_dst_ip.eq(Cat(ip_b[3], ip_b[2], ip_b[1], ip_b[0])), self.udp_pl_data.eq(pl_byte)] tx_load_i = Signal() tx_byte_i = Signal(8) m.d.comb += [tx_load.eq(tx_load_i), tx_byte.eq(tx_byte_i)] # ── Shell FSM ───────────────────────────────────────────────────── with m.FSM(name="shell"): with m.State("BOOT"): start_print("BANNER", _RET_PROMPT) # One prime cycle so the block RAM read (rom_rd.data = rom[pr_ptr]) # is valid before the first byte is loaded. with m.State("PRINT_PRIME"): m.next = "PRINT" # Generic ROM printer → dispatch on pr_ret. rom_rd.data holds # rom[pr_ptr] (address is comb-driven; stable across the tx-busy # wait, so it is valid whenever ~tx_busy lets a byte load). with m.State("PRINT"): with m.If(~tx_busy): with m.If(pr_ptr == pr_end): with m.Switch(pr_ret): with m.Case(_RET_PROMPT): m.next = "PROMPT" with m.Case(_RET_INPUT): m.next = "INPUT" with m.Case(_RET_PARSE): m.next = "PARSE" with m.Case(_RET_RXWAIT): m.next = "RX_INIT" with m.Case(_RET_RXBODY): m.next = "RX_BODY" with m.Else(): m.d.comb += [tx_load_i.eq(1), tx_byte_i.eq(rom_rd.data)] m.d.sync += pr_ptr.eq(pr_ptr + 1) with m.State("PROMPT"): m.d.sync += llen.eq(0) start_print("PROMPT", _RET_INPUT) # Read + echo a line into lbuf until CR/LF. with m.State("INPUT"): with m.If(rx_pending): m.d.sync += rx_pending.eq(0) with m.If((rx_hold == 0x0D) | (rx_hold == 0x0A)): start_print("CRLF", _RET_PARSE) with m.Elif((rx_hold == 0x08) | (rx_hold == 0x7F)): with m.If(llen != 0): m.d.sync += llen.eq(llen - 1) start_print("BKSP", _RET_INPUT) with m.Elif((rx_hold >= 0x20) & (rx_hold < 0x7F)): with m.If(llen != N): m.d.comb += lbuf_wr.en.eq(1) # store at addr=llen m.d.sync += [llen.eq(llen + 1), ech.eq(rx_hold)] m.next = "ECHO" # Echo one stored character. with m.State("ECHO"): with m.If(~tx_busy): m.d.comb += [tx_load_i.eq(1), tx_byte_i.eq(ech)] m.next = "INPUT" # Decide which command the line holds — set up the sequential scan. with m.State("PARSE"): with m.If(llen == 0): m.next = "PROMPT" with m.Else(): m.d.sync += [si.eq(0), mh.eq(1), muni.eq(1), mbc.eq(1), mbca.eq(1), dh.eq(0), dbc.eq(0), dbca.eq(0)] m.next = "SCAN_RD" # Scan the first up-to-14 bytes, comparing each against all four # command templates in parallel (one byte/cycle from block RAM). with m.State("SCAN_RD"): m.d.comb += la.eq(si) # address the current byte m.next = "SCAN_USE" with m.State("SCAN_USE"): m.d.comb += la.eq(si) c = lbuf_rd.data lc = low(c) inb = si < llen # byte position is within the line with m.If(si < len(_TH)): m.d.sync += mh.eq(mh & inb & (lc == THarr[si])) with m.If(si == len(_TH)): m.d.sync += dh.eq((si >= llen) | (c == ord(' '))) with m.If(si < len(_TU)): m.d.sync += muni.eq(muni & inb & (lc == TUarr[si])) with m.If(si < len(_TB)): m.d.sync += mbc.eq(mbc & inb & (lc == TBarr[si])) with m.If(si == len(_TB)): m.d.sync += dbc.eq((si >= llen) | (c == ord(' '))) with m.If(si < len(_TA)): m.d.sync += mbca.eq(mbca & inb & (lc == TAarr[si])) with m.If(si == len(_TA)): m.d.sync += dbca.eq((si >= llen) | (c == ord(' '))) with m.If(si == len(_TB)): # scanned enough to decide m.next = "EVAL" with m.Else(): m.d.sync += si.eq(si + 1) m.next = "SCAN_RD" # Classify from the accumulated match/delimiter bits (same priority # and semantics as the old parallel parser). with m.State("EVAL"): with m.If(mh & dh): start_print("HELP", _RET_PROMPT) with m.Elif(muni): # "udp unicast " prefix present m.d.sync += [ip_pos.eq(len(_TU)), octet.eq(0), noct.eq(0), ip_b[0].eq(0), ip_b[1].eq(0), ip_b[2].eq(0), ip_b[3].eq(0)] m.next = "IP_RD" with m.Elif(mbc & dbc): m.d.sync += [kwend.eq(len(_TB)), ip_b[0].eq(0xFF), ip_b[1].eq(0xFF), ip_b[2].eq(0xFF), ip_b[3].eq(0xFF)] m.next = "BC_RD" with m.Elif(mbca & dbca): m.d.sync += [kwend.eq(len(_TA)), ip_b[0].eq(0xFF), ip_b[1].eq(0xFF), ip_b[2].eq(0xFF), ip_b[3].eq(0xFF)] m.next = "BC_RD" with m.Else(): start_print("ERR", _RET_PROMPT) # Sequential dotted-quad IPv4 parse from lbuf[ip_pos ...]. with m.State("IP_RD"): m.d.comb += la.eq(ip_pos) m.next = "IP_USE" with m.State("IP_USE"): m.d.comb += la.eq(ip_pos) c = lbuf_rd.data with m.If((ip_pos == llen) | (c == ord(' '))): # end of address token → commit final octet + payload bounds with m.If((noct == 3) & (octet <= 255)): m.d.sync += ip_b[3].eq(octet) with m.If(ip_pos == llen): m.d.sync += use_def.eq(1) # no payload with m.Else(): # c == ' ' m.d.sync += [pl_base.eq(ip_pos + 1), pl_len.eq(llen - (ip_pos + 1)), use_def.eq((ip_pos + 1) >= llen)] m.next = "SEND_SETUP" with m.Else(): start_print("BADIP", _RET_PROMPT) with m.Elif(c == ord('.')): with m.If((noct < 3) & (octet <= 255)): m.d.sync += [ip_b[noct].eq(octet), noct.eq(noct + 1), octet.eq(0), ip_pos.eq(ip_pos + 1)] m.next = "IP_RD" with m.Else(): start_print("BADIP", _RET_PROMPT) with m.Elif((c >= ord('0')) & (c <= ord('9'))): m.d.sync += [octet.eq(octet * 10 + (c - ord('0'))), ip_pos.eq(ip_pos + 1)] m.next = "IP_RD" with m.Else(): start_print("BADIP", _RET_PROMPT) # Broadcast payload: everything after the keyword's trailing space. with m.State("BC_RD"): m.d.comb += la.eq(kwend) m.next = "BC_USE" with m.State("BC_USE"): m.d.comb += la.eq(kwend) with m.If((llen > kwend) & (lbuf_rd.data == ord(' '))): m.d.sync += [pl_base.eq(kwend + 1), pl_len.eq(llen - (kwend + 1)), use_def.eq((kwend + 1) >= llen)] with m.Else(): m.d.sync += use_def.eq(1) m.next = "SEND_SETUP" with m.State("SEND_SETUP"): with m.If(self.udp_test_busy): start_print("BUSY", _RET_PROMPT) with m.Else(): with m.If(use_def): m.d.sync += [pl_base.eq(0), pl_len.eq(DEF_LEN)] m.d.sync += j.eq(0) m.next = "PL_PRIME" # Prime the prefetch register with payload byte 0 (block RAM read # for typed payload; the default payload is combinational). with m.State("PL_PRIME"): m.d.comb += la.eq(pl_base) # fetch typed byte 0 m.next = "PL_PRIME2" with m.State("PL_PRIME2"): m.d.comb += la.eq(pl_base) m.d.sync += pl_byte.eq(Mux(use_def, defrom[0], lbuf_rd.data)) m.next = "SEND_REQ" # Hold the request until the W5100 acknowledges by going busy. with m.State("SEND_REQ"): m.d.comb += la.eq(pl_base + j + 1) # prefetch next byte m.d.sync += req_r.eq(1) with m.If(self.udp_test_busy): m.d.sync += req_r.eq(0) m.next = "SEND_STREAM" # Feed payload bytes as the W5100 consumes them. `pl_byte` holds # the current byte; `la` prefetches the next so it is ready by the # time the master pulls it (block RAM 1-cycle latency hidden). with m.State("SEND_STREAM"): m.d.comb += la.eq(pl_base + j + 1) m.d.comb += [self.udp_pl_valid.eq(1), self.udp_pl_last.eq(j + 1 == pl_len)] with m.If(self.udp_pl_ready): with m.If(j + 1 == pl_len): m.next = "SEND_WAIT" with m.Else(): m.d.sync += [pl_byte.eq(Mux(use_def, defrom[j + 1], lbuf_rd.data)), j.eq(j + 1)] with m.State("SEND_WAIT"): with m.If(~self.udp_test_busy): # datagram is out; now wait (bounded) for a reply. start_print("SENT", _RET_RXWAIT) # ── Wait for a UDP reply on the socket, with a timeout ──────────── with m.State("RX_INIT"): m.d.sync += to_ctr.eq(self._to_cyc) m.next = "RX_POLL" # Ask the W5100 to check the socket RX buffer. with m.State("RX_POLL"): m.d.comb += self.udp_rx_req.eq(1) with m.If(self.udp_rx_busy): m.next = "RX_WAIT" # One of: a datagram streams (udp_rx_valid) or none (udp_rx_none). with m.State("RX_WAIT"): with m.If(self.udp_rx_valid): start_print("RXPFX", _RET_RXBODY) with m.Elif(self.udp_rx_none): m.d.sync += gap.eq(self._gap_cyc) m.next = "RX_GAP" # Idle a bit between polls; count down the overall timeout. with m.State("RX_GAP"): with m.If(to_ctr == 0): start_print("TIMEOUT", _RET_PROMPT) with m.Elif(gap == 0): m.next = "RX_POLL" with m.Else(): m.d.sync += [gap.eq(gap - 1), to_ctr.eq(to_ctr - 1)] # Stream the reply payload to the UART (throttled by tx_busy). with m.State("RX_BODY"): with m.If(self.udp_rx_valid & ~tx_busy): m.d.comb += [tx_load_i.eq(1), tx_byte_i.eq(self.udp_rx_data), self.udp_rx_ready.eq(1)] with m.If(self.udp_rx_eof): m.next = "RX_BODY_END" with m.State("RX_BODY_END"): start_print("CRLF", _RET_PROMPT) return m # ── Testbench ─────────────────────────────────────────────────────────────── if __name__ == "__main__": import sys from amaranth.sim import Simulator, Period # Bit period is irrelevant to the shell logic, so use a tiny divisor to # keep the simulation fast; the real build uses 24 MHz / 115200 (div≈208). # Small reply timeout/gap so the timeout test finishes quickly. CLK, BAUD = 24_000_000, 3_000_000 DIV = round(CLK / BAUD) # = 8 sim cycles per bit dut = UARTShell(clk_freq=CLK, baud_rate=BAUD, reply_timeout_cycles=4000, poll_gap_cycles=300) errors = [] # ── UART line helpers (drive uart_rx, sample uart_tx) ────────────────── async def send_byte(ctx, val): ctx.set(dut.uart_rx, 0) await ctx.tick().repeat(DIV) for i in range(8): ctx.set(dut.uart_rx, (val >> i) & 1) await ctx.tick().repeat(DIV) ctx.set(dut.uart_rx, 1) await ctx.tick().repeat(DIV) async def send_line(ctx, s): for ch in s: await send_byte(ctx, ord(ch)) await send_byte(ctx, 0x0D) # Enter # Background collector: continuously samples uart_tx into a byte list. tx_chars = bytearray() async def tx_collector(ctx): while True: # wait for start bit if ctx.get(dut.uart_tx) == 0: await ctx.tick().repeat(DIV // 2) if ctx.get(dut.uart_tx) != 0: continue b = 0 for i in range(8): await ctx.tick().repeat(DIV) b |= ctx.get(dut.uart_tx) << i await ctx.tick().repeat(DIV) # stop tx_chars.append(b) else: await ctx.tick() # Background responder: models the W5100 UDP interface for BOTH directions. # On udp_send_req it drains + records the payload (send handshake); on # udp_rx_req it either streams a staged reply datagram or pulses udp_rx_none # (nothing waiting) — mirroring the real master's poll-driven RX. captured = {"payload": None, "dst": None, "count": 0} staged = {"reply": None} # bytes to deliver on the next RX poll async def w5100_udp_model(ctx): while True: await ctx.tick() if ctx.get(dut.udp_send_req): ctx.set(dut.udp_test_busy, 1) dst = ctx.get(dut.udp_dst_ip) await ctx.tick().repeat(6) pl = bytearray(); last = False; guard = 0 while not last: ctx.set(dut.udp_pl_ready, 1) got = ctx.get(dut.udp_pl_valid) if got: pl.append(ctx.get(dut.udp_pl_data)) last = bool(ctx.get(dut.udp_pl_last)) await ctx.tick() if got: # Space consumes ~5 cycles apart, like the W5100's bus # cycle — the shell's block-RAM prefetch needs ≥1 cycle # between pulls to present the next byte. ctx.set(dut.udp_pl_ready, 0) await ctx.tick().repeat(4) guard += 1 if guard > 5000: break ctx.set(dut.udp_pl_ready, 0) await ctx.tick().repeat(6) captured["payload"] = bytes(pl) captured["dst"] = [(dst >> 24) & 0xFF, (dst >> 16) & 0xFF, (dst >> 8) & 0xFF, dst & 0xFF] captured["count"] += 1 ctx.set(dut.udp_test_busy, 0) elif ctx.get(dut.udp_rx_req): ctx.set(dut.udp_rx_busy, 1) await ctx.tick().repeat(4) # emulate RSR/RD/header latency rep = staged["reply"] if rep is None: ctx.set(dut.udp_rx_none, 1) await ctx.tick() ctx.set(dut.udp_rx_none, 0) else: staged["reply"] = None for i, b in enumerate(rep): ctx.set(dut.udp_rx_data, b) ctx.set(dut.udp_rx_valid, 1) ctx.set(dut.udp_rx_eof, 1 if i == len(rep) - 1 else 0) g = 0 while not ctx.get(dut.udp_rx_ready): await ctx.tick(); g += 1 if g > 20000: break await ctx.tick() # consume cycle ctx.set(dut.udp_rx_valid, 0) ctx.set(dut.udp_rx_eof, 0) ctx.set(dut.udp_rx_busy, 0) async def wait_prompt(ctx, timeout=400_000): """Wait until the tail of tx_chars ends with 'rebbarb> '.""" for _ in range(timeout): if tx_chars.endswith(b"rebbarb> "): return True await ctx.tick() return False async def testbench(ctx): ctx.set(dut.uart_rx, 1) # udp_pl_ready / udp_test_busy are owned by w5100_udp_model — do not # drive them here (two testbenches on one signal deadlocks the send). # Boot banner + first prompt. if not await wait_prompt(ctx): errors.append("no initial prompt"); return print(f"boot tx: {bytes(tx_chars)!r}") # T1: 'help' lists commands. tx_chars.clear() await send_line(ctx, "help") await wait_prompt(ctx) if b"udp unicast" not in tx_chars or b"udp broadcast" not in tx_chars: errors.append(f"T1 help missing commands: {bytes(tx_chars)!r}") print(f"T1 help ok ({len(tx_chars)} bytes)") # T2: unicast with payload, and a staged reply → 'rx ' printed. tx_chars.clear(); captured["count"] = 0 staged["reply"] = b"pong-A" await send_line(ctx, "udp unicast 192.168.1.55 hello world") await wait_prompt(ctx) if captured["dst"] != [192, 168, 1, 55]: errors.append(f"T2 dst {captured['dst']} != [192,168,1,55]") if captured["payload"] != b"hello world": errors.append(f"T2 payload {captured['payload']!r} != b'hello world'") if b"sent" not in tx_chars: errors.append(f"T2 no 'sent' ack: {bytes(tx_chars)!r}") if b"rx pong-A" not in tx_chars: errors.append(f"T2 reply not printed: {bytes(tx_chars)!r}") print(f"T2 unicast+reply: dst={captured['dst']} " f"payload={captured['payload']!r} tx={bytes(tx_chars)!r}") # T3: broadcast default payload, NO reply → 'timeout' printed (no hang). tx_chars.clear(); staged["reply"] = None await send_line(ctx, "udp broadcast") await wait_prompt(ctx) if captured["dst"] != [255, 255, 255, 255]: errors.append(f"T3 dst {captured['dst']} != broadcast") if captured["payload"] != bytes(_DEFAULT_PAYLOAD): errors.append(f"T3 payload {captured['payload']!r} != default") if b"timeout" not in tx_chars: errors.append(f"T3 no timeout on no-reply: {bytes(tx_chars)!r}") print(f"T3 broadcast default→timeout: dst={captured['dst']} " f"payload={captured['payload']!r}") # T4: broadcast with payload + reply. tx_chars.clear(); staged["reply"] = b"Hello World" await send_line(ctx, "udp broadcast ping123") await wait_prompt(ctx) if captured["payload"] != b"ping123": errors.append(f"T4 payload {captured['payload']!r} != b'ping123'") if b"rx Hello World" not in tx_chars: errors.append(f"T4 reply not printed: {bytes(tx_chars)!r}") print(f"T4 broadcast+reply: payload={captured['payload']!r} " f"tx={bytes(tx_chars)!r}") # T5: case-insensitive command + uppercase preserved in payload. tx_chars.clear(); staged["reply"] = b"ok" await send_line(ctx, "UDP UNICAST 10.0.0.9 MixedCase") await wait_prompt(ctx) if captured["dst"] != [10, 0, 0, 9]: errors.append(f"T5 dst {captured['dst']} != [10,0,0,9]") if captured["payload"] != b"MixedCase": errors.append(f"T5 payload {captured['payload']!r} != b'MixedCase'") print(f"T5 case: dst={captured['dst']} payload={captured['payload']!r}") # T6: bad IP → 'bad ip', no send. tx_chars.clear(); before = captured["count"] await send_line(ctx, "udp unicast 1.2.3 x") await wait_prompt(ctx) if b"bad ip" not in tx_chars: errors.append(f"T6 no 'bad ip': {bytes(tx_chars)!r}") if captured["count"] != before: errors.append("T6 sent despite bad ip") print(f"T6 bad ip ok (no send)") # T7: unknown command → error. tx_chars.clear() await send_line(ctx, "frobnicate") await wait_prompt(ctx) if b"?" not in tx_chars: errors.append(f"T7 no error marker: {bytes(tx_chars)!r}") print(f"T7 unknown ok") # T8: backspace editing (type 'helX' 'p' → 'help'). The backspace # echo is 3 bytes ("\b \b"), longer than one incoming byte time, so a # realistic source pauses after it (a human always does). Settle # between edits — this is the one spot the 1-deep RX register can't # absorb, and it never occurs with paste (no backspaces) or typing. tx_chars.clear() for ch in "helX": await send_byte(ctx, ord(ch)) await ctx.tick().repeat(DIV * 15) # > 1-byte echo (10 bits) await send_byte(ctx, 0x08) # backspace removes 'X' await ctx.tick().repeat(DIV * 40) # > 3-byte "\b \b" echo (~30 bits) await send_byte(ctx, ord('p')) await ctx.tick().repeat(DIV * 15) await send_byte(ctx, 0x0D) await wait_prompt(ctx) if b"udp unicast" not in tx_chars: errors.append(f"T8 backspace edit failed: {bytes(tx_chars)!r}") print(f"T8 backspace edit ok") sim = Simulator(dut) sim.add_clock(Period(MHz=24)) sim.add_testbench(testbench) sim.add_testbench(tx_collector, background=True) sim.add_testbench(w5100_udp_model, background=True) sim.run() if errors: print("\nFAILURES:") for e in errors: print(" ", e) sys.exit(1) print("\nAll UARTShell tests passed.")