"""RX frame assembler — sync domain (24 MHz). Receives raw ethernet frames from W5500SPIMaster and writes them into the SPRAM ring buffer in MX98730EC format. Ring buffer layout (SPRAM byte addresses) ------------------------------------------ 0x0100–0x0FFF 15 pages × 256 bytes = 3840 bytes Pages 0x01–0x0F; page 0x00 is reserved. Page wrap: after 0x0F → 0x01 (skip 0x00). Frame descriptor (4 bytes at page start) ----------------------------------------------- Byte 0: LRPS (last received packet status) — 0x00 Byte 1: 0x00 Byte 2: total_length[15:8] (big-endian; includes 4 descriptor bytes) Byte 3: total_length[7:0] Bytes 4+: raw ethernet frame Write sequence -------------- 1. Issue 4 SPRAM writes of 0x00 (placeholder descriptor). 2. For each byte received from W5500, issue one SPRAM write. 3. After EOF: rewrite descriptor bytes 2 and 3 with actual length. 4. Advance RWP, push to rx_wptr FIFO, pulse rx_irq. """ from amaranth import * __all__ = ["RXFrameAssembler"] _RX_PAGE_FIRST = 0x01 _RX_PAGE_LAST = 0x0F _PAGES_TOTAL = _RX_PAGE_LAST - _RX_PAGE_FIRST + 1 # 15 class RXFrameAssembler(Elaboratable): """Writes incoming ethernet frames into the SPRAM ring buffer. W5500 streaming interface (sync domain) ---------------------------------------- rx_data / rx_valid / rx_ready : byte stream rx_sof / rx_eof : frame delimiters (same cycle as rx_valid) SPRAM write interface (to SPRAMArbiter, sync domain) ----------------------------------------------------- eth_wr_addr / eth_wr_data / eth_wr_valid / eth_wr_ready CDC outputs (wired by BBATop) ----------------------------- rx_wptr_w_data / rx_wptr_w_en / rx_wptr_w_rdy rx_irq : 1-cycle pulse → PulseSynchronizer input rx_enabled : controlled by NCRA SR bit (from BBARegisterFile) """ def __init__(self): # W5500 stream in self.rx_data = Signal(8) self.rx_valid = Signal() self.rx_ready = Signal() self.rx_sof = Signal() self.rx_eof = Signal() # SPRAM write out self.eth_wr_addr = Signal(16) self.eth_wr_data = Signal(8) self.eth_wr_valid = Signal() self.eth_wr_ready = Signal() # RWP FIFO write-side (sync→exi) self.rx_wptr_w_data = Signal(8) self.rx_wptr_w_en = Signal() self.rx_wptr_w_rdy = Signal() # rx_irq pulse (→ PulseSynchronizer) self.rx_irq = Signal() # RX gate from NCRA SR bit self.rx_enabled = Signal() def elaborate(self, platform): m = Module() # ── Ring-buffer state ───────────────────────────────────────────── rwp = Signal(8, init=_RX_PAGE_FIRST) # current RX write page (1–15) # Write address within current frame wr_addr = Signal(16) # Number of frame data bytes received data_ctr = Signal(12) # Total length = data_ctr + 4 total_len = Signal(12) # Descriptor base (rwp*256) — saved when frame starts desc_base = Signal(16) # Placeholder descriptor byte counter (0..3) desc_ctr = Signal(2) # Number of pages consumed by this frame (rounded up) pages_used = Signal(5) # Default: no pulses m.d.sync += self.rx_irq.eq(0) m.d.sync += self.rx_wptr_w_en.eq(0) # Combinatorial outputs m.d.comb += total_len.eq(data_ctr + 4) with m.FSM(domain="sync", name="rx_fsm"): with m.State("IDLE"): m.d.comb += self.rx_ready.eq(0) m.d.sync += self.eth_wr_valid.eq(0) with m.If(self.rx_valid & self.rx_sof & self.rx_enabled): frame_base = Signal(16) m.d.comb += frame_base.eq(Cat(Const(0, 8), rwp)) m.d.sync += desc_base.eq(frame_base) m.d.sync += wr_addr.eq(frame_base) m.d.sync += data_ctr.eq(0) m.d.sync += desc_ctr.eq(0) m.next = "WRITE_PLACEHOLDER" with m.State("WRITE_PLACEHOLDER"): # Write 4 bytes of 0x00 as placeholder descriptor m.d.sync += self.eth_wr_addr.eq(wr_addr) m.d.sync += self.eth_wr_data.eq(0x00) m.d.sync += self.eth_wr_valid.eq(1) with m.If(self.eth_wr_ready): m.d.sync += wr_addr.eq(wr_addr + 1) with m.If(desc_ctr == 3): m.d.sync += self.eth_wr_valid.eq(0) m.next = "RECV_AND_WRITE" with m.Else(): m.d.sync += desc_ctr.eq(desc_ctr + 1) with m.State("RECV_AND_WRITE"): # Accept bytes from W5500 and write each to SPRAM immediately m.d.comb += self.rx_ready.eq(~self.eth_wr_valid | self.eth_wr_ready) with m.If(self.rx_valid & (~self.eth_wr_valid | self.eth_wr_ready)): m.d.sync += self.eth_wr_addr.eq(wr_addr) m.d.sync += self.eth_wr_data.eq(self.rx_data) m.d.sync += self.eth_wr_valid.eq(1) m.d.sync += wr_addr.eq(wr_addr + 1) m.d.sync += data_ctr.eq(data_ctr + 1) with m.If(self.rx_eof): m.next = "WAIT_LAST_WRITE" with m.Elif(self.eth_wr_valid & self.eth_wr_ready): m.d.sync += self.eth_wr_valid.eq(0) with m.State("WAIT_LAST_WRITE"): # Wait for the last data byte write to be accepted with m.If(~self.eth_wr_valid | self.eth_wr_ready): m.d.sync += self.eth_wr_valid.eq(0) # Compute pages used: ceil((data_ctr + 4) / 256) # = (total_len + 255) >> 8 = total_len[11:8] + (total_len[7:0] != 0) m.d.sync += pages_used.eq(total_len[8:12] + (total_len[:8] != 0)) m.next = "WRITE_LEN_HI" with m.State("WRITE_LEN_HI"): # Overwrite descriptor byte 2 with total_len[15:8] m.d.sync += self.eth_wr_addr.eq(desc_base + 2) m.d.sync += self.eth_wr_data.eq(total_len[8:12]) m.d.sync += self.eth_wr_valid.eq(1) with m.If(self.eth_wr_ready): m.d.sync += self.eth_wr_valid.eq(0) m.next = "WRITE_LEN_LO" with m.State("WRITE_LEN_LO"): # Overwrite descriptor byte 3 with total_len[7:0] m.d.sync += self.eth_wr_addr.eq(desc_base + 3) m.d.sync += self.eth_wr_data.eq(total_len[:8]) m.d.sync += self.eth_wr_valid.eq(1) with m.If(self.eth_wr_ready): m.d.sync += self.eth_wr_valid.eq(0) m.next = "ADVANCE_RWP" with m.State("ADVANCE_RWP"): # next_rwp = ((rwp - 1 + pages_used) % 15) + 1 next_rwp_raw = Signal(8) m.d.comb += next_rwp_raw.eq(rwp + pages_used) with m.If(next_rwp_raw > _RX_PAGE_LAST): m.d.sync += rwp.eq(next_rwp_raw - _PAGES_TOTAL) with m.Else(): m.d.sync += rwp.eq(next_rwp_raw) m.next = "PUSH_WPT" with m.State("PUSH_WPT"): with m.If(self.rx_wptr_w_rdy): m.d.sync += self.rx_wptr_w_data.eq(rwp) m.d.sync += self.rx_wptr_w_en.eq(1) m.d.sync += self.rx_irq.eq(1) m.next = "IDLE" return m # ── Testbench ───────────────────────────────────────────────────────────── if __name__ == "__main__": import sys from amaranth.sim import Simulator, Period dut = RXFrameAssembler() errors = [] # Track all SPRAM writes issued by the DUT spram_writes = [] async def testbench(ctx): # Setup: acknowledge all SPRAM writes immediately ctx.set(dut.eth_wr_ready, 1) ctx.set(dut.rx_wptr_w_rdy, 1) ctx.set(dut.rx_enabled, 1) await ctx.tick("sync").repeat(2) # ── T1: 10-byte frame → pages_used=1, rwp advances 1→2 ────────────── # Send SOF + first byte frame = [0xAA, 0xBB, 0xCC, 0xDD, 0x08, 0x00, 0x45, 0x00, 0x00, 0x01] ctx.set(dut.rx_data, frame[0]) ctx.set(dut.rx_valid, 1) ctx.set(dut.rx_sof, 1) await ctx.tick("sync").repeat(1) ctx.set(dut.rx_sof, 0) for i, b in enumerate(frame[1:], start=1): ctx.set(dut.rx_data, b) ctx.set(dut.rx_eof, 1 if i == len(frame) - 1 else 0) await ctx.tick("sync").repeat(1) ctx.set(dut.rx_valid, 0) ctx.set(dut.rx_eof, 0) # Poll for up to 30 ticks until rx_irq pulses (1-cycle pulse) t1_irq_seen = False t1_wptr_d = 0 for _ in range(30): await ctx.tick("sync").repeat(1) if ctx.get(dut.rx_irq): t1_irq_seen = True t1_wptr_d = ctx.get(dut.rx_wptr_w_data) break print(f"T1 rx_irq_seen={t1_irq_seen} wptr_data=0x{t1_wptr_d:02X}") if not t1_irq_seen: errors.append("T1: rx_irq never pulsed") if t1_wptr_d != 2: errors.append(f"T1: rwp should be 2 (page 1→2), got {t1_wptr_d}") await ctx.tick("sync").repeat(4) # ── T2: Send a second frame; verify rwp advances further ──────────── frame2 = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66] ctx.set(dut.rx_data, frame2[0]) ctx.set(dut.rx_valid, 1) ctx.set(dut.rx_sof, 1) await ctx.tick("sync").repeat(1) ctx.set(dut.rx_sof, 0) for i, b in enumerate(frame2[1:], start=1): ctx.set(dut.rx_data, b) ctx.set(dut.rx_eof, 1 if i == len(frame2) - 1 else 0) await ctx.tick("sync").repeat(1) ctx.set(dut.rx_valid, 0) ctx.set(dut.rx_eof, 0) t2_irq_seen = False t2_wptr_d = 0 for _ in range(30): await ctx.tick("sync").repeat(1) if ctx.get(dut.rx_irq): t2_irq_seen = True t2_wptr_d = ctx.get(dut.rx_wptr_w_data) break print(f"T2 rx_irq_seen={t2_irq_seen} wptr_data=0x{t2_wptr_d:02X}") if not t2_irq_seen: errors.append("T2: rx_irq never pulsed after second frame") if t2_wptr_d != 3: errors.append(f"T2: rwp should be 3 (page 2→3), got {t2_wptr_d}") # ── T3: RX disabled — SOF must be ignored ────────────────────────── ctx.set(dut.rx_enabled, 0) ctx.set(dut.rx_data, 0xDE) ctx.set(dut.rx_valid, 1) ctx.set(dut.rx_sof, 1) await ctx.tick("sync").repeat(4) ctx.set(dut.rx_valid, 0) ctx.set(dut.rx_sof, 0) # No SPRAM write should have been issued wr_valid = ctx.get(dut.eth_wr_valid) if wr_valid: errors.append("T3: SPRAM write issued while rx_enabled=0") print(f"T3 rx disabled: eth_wr_valid={wr_valid} (expected 0)") sim = Simulator(dut) sim.add_clock(Period(MHz=24), domain="sync") sim.add_testbench(testbench) with sim.write_vcd("RXFrameAssembler.vcd"): sim.run() if errors: print("\nFAILURES:") for e in errors: print(" ", e) sys.exit(1) else: print("\nAll tests passed.")