fix(tx): queue the frame length at TWD_HI so the drain runs concurrently
TXFrameDrain sat in IDLE until it had a length, and the length only reached tx_ctrl when the NCRA ST bits were written -- after the GC had streamed the whole frame. So the entire frame had to fit in the 16-deep tx_bytes FIFO, and BBARegisterFile silently drops bytes when w_rdy is low (SPI cannot back-pressure). On hardware this truncated frames at exactly 16 bytes: a captured ARP had 16 correct bytes followed by the frame restarting over itself. The length is now handed to the drain one cycle after the TWD_HI (0x35) write, which the GC issues BEFORE streaming data. The drain starts early and blocks on tx_bytes_r_rdy, consuming bytes as they arrive, so tx_bytes becomes a rate-matching buffer instead of whole-frame storage. The ST write remains a fallback trigger for software that skips TWD, guarded by tx_queued so one frame is never queued twice. Adds BBATop T8: streams a 34-byte frame (> the 16-deep FIFO) over EXI and checks the W5100 model received it byte-for-byte. Verified to FAIL without the fix with exactly the hardware symptom -- 16/34 bytes, first bad byte at index 16. The gap that let this ship: TXFrameDrain's own tests drive the FIFO signals directly, and the BBATop test only covered the RX loop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -208,6 +208,11 @@ class BBARegisterFile(Elaboratable):
|
||||
xfer_len = Signal(2) # 0=1B … 3=4B
|
||||
byte_ctr = Signal(2)
|
||||
tx_frame_len = Signal(16)
|
||||
# TX trigger bookkeeping. `twd_armed` fires one cycle AFTER TWD_HI is
|
||||
# written, so tx_frame_len has both halves by then; `tx_queued` records
|
||||
# that the drain already has a length for this frame.
|
||||
twd_armed = Signal()
|
||||
tx_queued = Signal()
|
||||
|
||||
# True until first NCRA reset write: return device ID on addr=0 reads
|
||||
id_phase = Signal(init=1)
|
||||
@@ -328,10 +333,15 @@ class BBARegisterFile(Elaboratable):
|
||||
m.d.exi += r_ncra[0].eq(0) # RESET self-clears
|
||||
m.d.exi += ncra_rst_ps.i.eq(1)
|
||||
m.d.exi += id_phase.eq(0)
|
||||
with m.If(self.rx_data[1:3].any()):
|
||||
# Fallback trigger: TWD_HI normally queues the
|
||||
# frame (see _TWD_HI). Only fire here if it did
|
||||
# not, so software that skips TWD still transmits
|
||||
# and we never double-queue one frame.
|
||||
with m.If(self.rx_data[1:3].any() & ~tx_queued):
|
||||
with m.If(tx_ctrl.w_rdy):
|
||||
m.d.exi += tx_ctrl.w_data.eq(tx_frame_len)
|
||||
m.d.exi += tx_ctrl.w_en.eq(1)
|
||||
m.d.exi += tx_queued.eq(1)
|
||||
with m.Case(_IMR):
|
||||
m.d.exi += r_imr.eq(self.rx_data)
|
||||
with m.Case(_IR):
|
||||
@@ -349,6 +359,15 @@ class BBARegisterFile(Elaboratable):
|
||||
with m.Case(_TWD_HI):
|
||||
m.d.exi += r_twd_hi.eq(self.rx_data)
|
||||
m.d.exi += tx_frame_len[8:16].eq(self.rx_data)
|
||||
# Hand the length to TXFrameDrain NOW, not at the ST
|
||||
# trigger. The drain blocks on tx_bytes_r_rdy, so
|
||||
# starting it here lets it consume bytes AS the GC
|
||||
# streams them and keeps tx_bytes a rate-matching
|
||||
# buffer. Waiting for ST meant the whole frame had
|
||||
# to fit in the 16-deep FIFO, and anything past 16
|
||||
# bytes was silently dropped (hardware 2026-09-30:
|
||||
# frames truncated at exactly 16 bytes on the wire).
|
||||
m.d.exi += twd_armed.eq(1)
|
||||
with m.Case(_TXDATA):
|
||||
with m.If(tx_bytes.w_rdy):
|
||||
m.d.exi += tx_bytes.w_data.eq(self.rx_data)
|
||||
@@ -433,9 +452,19 @@ class BBARegisterFile(Elaboratable):
|
||||
# ── PulseSynchronizer arrivals ────────────────────────────────────
|
||||
with m.If(rx_irq_ps.o):
|
||||
m.d.exi += r_ir[1].eq(1) # RI bit
|
||||
# ── Deferred TX queue (armed by a TWD_HI write) ──────────────────
|
||||
# One cycle after TWD_HI so tx_frame_len holds both bytes. Held until
|
||||
# tx_ctrl has room so a full control FIFO cannot drop the trigger.
|
||||
with m.If(twd_armed & tx_ctrl.w_rdy):
|
||||
m.d.exi += tx_ctrl.w_data.eq(tx_frame_len)
|
||||
m.d.exi += tx_ctrl.w_en.eq(1)
|
||||
m.d.exi += twd_armed.eq(0)
|
||||
m.d.exi += tx_queued.eq(1)
|
||||
|
||||
with m.If(tx_irq_ps.o):
|
||||
m.d.exi += r_ir[2].eq(1) # TI bit
|
||||
m.d.exi += r_ncra[1:3].eq(0) # clear ST bits
|
||||
m.d.exi += tx_queued.eq(0) # ready for the next frame
|
||||
|
||||
return m
|
||||
|
||||
|
||||
Reference in New Issue
Block a user