rx_frame_assembler: document the ring and descriptor, drop stale references
844 -> 818 lines, 30% -> 12% comment-only. The descriptor bit layout is kept
in FULL, because it is the one interface in this module an external driver
parses; what went was the disassembly listing that proves it (CLAUDE.md holds
that) and the per-bug narrative around each fix.
Stale references fixed:
* The module claimed to receive frames "from W5500SPIMaster". The W5100
parallel master is the default back-end; the stream interface is
deliberately identical, so the text now names neither.
* "RWP FIFO write-side (sync->exi)" and "rx_irq : 1-cycle pulse ->
PulseSynchronizer input" both described the old split clock domains. The
exi domain was merged into sync; that FIFO is a SyncFIFO and the pulse is
a plain wire.
* The module docstring's write sequence said descriptor "bytes 0, 1 and 2"
are rewritten and "byte 3 (status) stays 0x00", and WRITE_D2's comment
said byte 3 "keeps the placeholder". Both predate WRITE_D3, which exists
precisely because the placeholder NEVER reaches desc_base+3 -- so the
file contradicted its own next state, and in the direction that sounds
reassuring.
Two comment blocks were duplicated outright: the registered-ring-bounds
timing note appeared twice in a row with slightly different wording, and the
NCRA-reset rationale appeared both on the port and again at the logic.
Structure: elaborate() is now _ring_bounds, _free_space, _frame_fsm and
_ncra_reset, with _ncra_reset still elaborated last so it keeps priority over
ADVANCE_RWP. The FSM's docstring lists all eleven states in visit order, and
the valid-&-ready rule that both 2026-10-09 handshake bugs violated is stated
once for every write state instead of twice at two of them.
Also fixed a real (if harmless) bug in the testbench: send_frame's docstring
sat after two statements, making it a no-op expression rather than a
docstring.
VERIFIED EQUIVALENT: generated RTLIL identical, 1930 lines, after stripping
\src attributes. Full module suite passes, bba_top T9 6/6 at EXI_HALF=1, 2
and 8, and this module's own 25 checks all pass including T5's stalled-write
case and T7's small-ring wrap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+276
-302
@@ -1,61 +1,52 @@
|
||||
"""RX frame assembler — sync domain (24 MHz).
|
||||
"""RX frame assembler — takes received ethernet frames into the SPRAM ring.
|
||||
|
||||
Receives raw ethernet frames from W5500SPIMaster and writes them into the SPRAM
|
||||
ring buffer in MX98730EC format.
|
||||
Runs entirely in the `sync` domain (24 MHz). Accepts a byte stream from
|
||||
whichever ethernet master is built (`W5100ParallelMaster` by default, or
|
||||
`W5500SPIMaster`) and lays each frame out in the ring the way the GameCube's
|
||||
own BBA driver expects to find it.
|
||||
|
||||
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).
|
||||
Ring layout
|
||||
-----------
|
||||
SPRAM bytes 0x0100-0x0FFF, as 15 pages of 256 bytes. Pages 0x01-0x0F are
|
||||
usable and page 0x00 is reserved for TX, so the wrap is 0x0F → 0x01. The GC
|
||||
can narrow the ring through its BP/RHBP registers; see `_ring_bounds`.
|
||||
|
||||
Frame descriptor (4 bytes at page start)
|
||||
-----------------------------------------------
|
||||
A PACKED 32-BIT LITTLE-ENDIAN WORD, not a byte-wise layout:
|
||||
A frame occupies consecutive pages from its own page start, and the next frame
|
||||
begins on the next free page -- frames are never split across the wrap, which
|
||||
is why the writer reserves a worst-case 6 pages before starting one.
|
||||
|
||||
bits 0–11 : next_packet_ptr — the PAGE at which the NEXT frame starts
|
||||
bits 12–23 : packet_len — total bytes, INCLUDING these 4
|
||||
bits 24–31 : status — errors are 0x80 and 0x04; 0x00 means good
|
||||
Frame descriptor
|
||||
----------------
|
||||
Four bytes at each page start, forming a PACKED 32-BIT LITTLE-ENDIAN WORD.
|
||||
This is not a byte-wise layout, and it must not be "simplified" into a plain
|
||||
big-endian length:
|
||||
|
||||
so the four bytes at the page start are
|
||||
bits 0-11 next_packet_ptr the PAGE the NEXT frame starts at
|
||||
bits 12-23 packet_len total bytes, INCLUDING these 4
|
||||
bits 24-31 status 0x00 is good; 0x80 and 0x04 are errors
|
||||
|
||||
Byte 0: next_packet_ptr[7:0]
|
||||
Byte 1: packet_len[3:0] << 4 | next_packet_ptr[11:8]
|
||||
Byte 2: packet_len[11:4]
|
||||
Byte 3: status
|
||||
Bytes 4+: raw ethernet frame
|
||||
which puts on the wire:
|
||||
|
||||
⚠️ This is NOT a guess and must not be "simplified" back to a plain
|
||||
big-endian length. It was read out of libogc's own BBA driver
|
||||
(`/opt/devkitpro/libogc/lib/cube/libbba.a`, `gcif.o`, `bba_interrupt`), which
|
||||
is the driver GC software actually runs:
|
||||
byte 0 next_packet_ptr[7:0]
|
||||
byte 1 packet_len[3:0] << 4 | next_packet_ptr[11:8]
|
||||
byte 2 packet_len[11:4]
|
||||
byte 3 status
|
||||
byte 4+ the raw ethernet frame
|
||||
|
||||
lwbrx r9,0,r9 # load the 4 bytes as a LITTLE-ENDIAN word
|
||||
rlwinm r4,r9,20,20,31 # packet_len = (word >> 12) & 0xFFF
|
||||
addi r4,r4,-4 # payload = packet_len - 4
|
||||
cmpwi r4,1540 # ... which must be <= 1540
|
||||
srwi r9,r9,24 # status = word >> 24
|
||||
and. r9,r9,r18 # r18 = 0x...84, so errors are 0x80 | 0x04
|
||||
|
||||
and it writes the low 12 bits straight back to RRP as the next read pointer:
|
||||
|
||||
lwz r9,cur_descr # byte 0 -> register 0x0018 (RRP_LO)
|
||||
rlwinm r9,r31,24,28,31 # bits 11:8 -> register 0x0019 (RRP_HI)
|
||||
|
||||
The length semantics and addressing already matched (descriptor at page<<8,
|
||||
body at +4, length including the descriptor); the PACKING did not, and
|
||||
`next_packet_ptr` was never written at all. Our previous
|
||||
`00 00 len[11:8] len[7:0]` decodes under that driver as packet_len = 16 and
|
||||
next_packet_ptr = 0 for a 342-byte frame — a 12-byte frame and a read pointer
|
||||
into the reserved page.
|
||||
The driver takes the low 12 bits of that word and writes them straight to its
|
||||
RRP register, so `next_packet_ptr` is load-bearing and not informational. The
|
||||
layout was read out of libogc's own compiled driver rather than inferred; the
|
||||
disassembly is recorded in CLAUDE.md.
|
||||
|
||||
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: compute the next page, then rewrite descriptor bytes 0, 1 and 2
|
||||
with the packed next_packet_ptr + packet_len. Byte 3 (status) stays 0x00.
|
||||
4. Advance RWP, push to rx_wptr FIFO, pulse rx_irq.
|
||||
1. Write four placeholder 0x00 bytes where the descriptor will go.
|
||||
2. Write each received byte straight through, one SPRAM write per byte.
|
||||
3. On EOF, compute the next page, then write descriptor bytes 0-3 for real.
|
||||
4. Advance RWP, push it to the register file, and pulse `rx_irq`.
|
||||
|
||||
The descriptor is written last because neither the length nor the next page is
|
||||
known until the frame has ended.
|
||||
"""
|
||||
|
||||
from amaranth import *
|
||||
@@ -72,24 +63,47 @@ _MAX_FRAME_PAGES = 6
|
||||
class RXFrameAssembler(Elaboratable):
|
||||
"""Writes incoming ethernet frames into the SPRAM ring buffer.
|
||||
|
||||
W5500 streaming interface (sync domain)
|
||||
Ethernet byte stream in
|
||||
-----------------------
|
||||
rx_data, rx_valid, rx_ready
|
||||
Standard ready/valid handshake. `rx_ready` is LOW while the
|
||||
descriptor placeholder is being written, so the master must honour it.
|
||||
rx_sof, rx_eof
|
||||
Frame delimiters, asserted in the same cycle as `rx_valid`.
|
||||
|
||||
SPRAM write port out (to `SPRAMArbiter`)
|
||||
----------------------------------------
|
||||
rx_data / rx_valid / rx_ready : byte stream
|
||||
rx_sof / rx_eof : frame delimiters (same cycle as rx_valid)
|
||||
eth_wr_addr, eth_wr_data, eth_wr_valid, eth_wr_ready
|
||||
|
||||
SPRAM write interface (to SPRAMArbiter, sync domain)
|
||||
-----------------------------------------------------
|
||||
eth_wr_addr / eth_wr_data / eth_wr_valid / eth_wr_ready
|
||||
To the register file
|
||||
-------------------
|
||||
rx_wptr_w_data, rx_wptr_w_en, rx_wptr_w_rdy
|
||||
The new write page after each stored frame, through a `SyncFIFO`.
|
||||
rx_irq
|
||||
One-cycle pulse: a frame is in the ring. Becomes IR[1] (RI).
|
||||
rx_overflow
|
||||
One-cycle pulse: a frame was DROPPED because the ring was full.
|
||||
Becomes IR[7] (RBFI).
|
||||
|
||||
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)
|
||||
From the register file
|
||||
---------------------
|
||||
rx_enabled
|
||||
NCRA's SR bit. Frames are ignored entirely while it is low.
|
||||
rrp
|
||||
The GC's read pointer, so free ring space can be computed.
|
||||
bp, rhbp
|
||||
Ring geometry the GC asked for; see `_ring_bounds`.
|
||||
ncra_rst
|
||||
The GC's NCRA reset; see `_ncra_reset`.
|
||||
geom_honoured
|
||||
High while `bp`/`rhbp` are usable and in force, low while they have
|
||||
been rejected and the default layout is standing in. Nothing consumes
|
||||
it in the design -- it exists so the tests can tell the two cases
|
||||
apart, which is otherwise unobservable from outside.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# W5500 stream in
|
||||
# Ethernet stream in
|
||||
self.rx_data = Signal(8)
|
||||
self.rx_valid = Signal()
|
||||
self.rx_ready = Signal()
|
||||
@@ -102,112 +116,75 @@ class RXFrameAssembler(Elaboratable):
|
||||
self.eth_wr_valid = Signal()
|
||||
self.eth_wr_ready = Signal()
|
||||
|
||||
# RWP FIFO write-side (sync→exi)
|
||||
# RWP hand-off to the register file
|
||||
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()
|
||||
self.rx_overflow = Signal()
|
||||
|
||||
# RX gate from NCRA SR bit
|
||||
self.rx_enabled = Signal()
|
||||
|
||||
# ── Ring-overflow protection (RBFI) ───────────────────────────────
|
||||
# The GC's read pointer, so we can tell how much of the ring is still
|
||||
# unread. Without it this module overwrote pages the GC had not read
|
||||
# yet: a traffic burst corrupted whatever frame the driver was midway
|
||||
# through, with no indication that it had happened.
|
||||
self.rrp = Signal(8, init=_RX_PAGE_FIRST)
|
||||
# ── Ring geometry, from the GC's own BP / RHBP registers ──────────
|
||||
# compat TODO item 2. These were hardcoded, so a driver configuring a
|
||||
# SMALLER RX ring was ignored and we kept writing pages it would never
|
||||
# read -- silent corruption with nothing reporting an error. libogc2
|
||||
# writes BP = 0x0001 and RHBP = 0x000F, which happens to match the old
|
||||
# hardcoded layout exactly, which is why it never showed up.
|
||||
self.bp = Signal(12, init=_RX_PAGE_FIRST) # first RX page
|
||||
self.rhbp = Signal(12, init=_RX_PAGE_LAST) # last RX page
|
||||
# Pulses when a frame had to be DROPPED because the ring was full.
|
||||
# Drives IR[7] (RBFI), which the GC's IMR enables (0x86).
|
||||
self.rx_overflow = Signal()
|
||||
# High when the driver's BP/RHBP were usable and are being honoured;
|
||||
# low means they were rejected and the hardcoded layout is in use, so
|
||||
# a mismatch is observable rather than silent.
|
||||
self.geom_honoured = Signal()
|
||||
|
||||
# The GC's NCRA reset. MUST reset `rwp` here as well as the register
|
||||
# file's copy: they are two copies of the same pointer, and resetting
|
||||
# only one leaves them disagreeing about where the ring is. A driver
|
||||
# that then syncs RRP to the register file's value is pointing
|
||||
# somewhere the writer is not, the free-space calculation shrinks with
|
||||
# every frame, and once it falls below _MAX_FRAME_PAGES EVERY frame is
|
||||
# dropped -- silently, because a dropped frame never reaches the ring
|
||||
# to be counted. Hardware symptom: "0 seen, 0 bad desc, 0 lapped".
|
||||
self.ncra_rst = Signal()
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
# ── Ring-buffer state ─────────────────────────────────────────────
|
||||
rwp = Signal(8, init=_RX_PAGE_FIRST) # current RX write page (1–15)
|
||||
# The current RX write page, shared by the free-space calculation, the
|
||||
# frame FSM and the NCRA reset.
|
||||
rwp = Signal(8, init=_RX_PAGE_FIRST)
|
||||
|
||||
# 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)
|
||||
# The page the NEXT frame will start at. Computed before the
|
||||
# descriptor is written, because the descriptor carries it as
|
||||
# next_packet_ptr -- libogc writes those 12 bits straight to RRP.
|
||||
next_page = Signal(12)
|
||||
# All three outputs are one-cycle pulses, so they default low and the
|
||||
# FSM below overrides them for the cycle it needs.
|
||||
m.d.sync += [self.rx_irq.eq(0),
|
||||
self.rx_wptr_w_en.eq(0),
|
||||
self.rx_overflow.eq(0)]
|
||||
|
||||
# Descriptor base (rwp*256) — saved when frame starts
|
||||
desc_base = Signal(16)
|
||||
pg = self._ring_bounds(m)
|
||||
have_room = self._free_space(m, rwp, pg)
|
||||
self._frame_fsm(m, rwp, pg, have_room)
|
||||
self._ncra_reset(m, rwp, pg)
|
||||
return m
|
||||
|
||||
# Placeholder descriptor byte counter (0..3)
|
||||
desc_ctr = Signal(2)
|
||||
def _ring_bounds(self, m):
|
||||
"""Sanitise the GC's requested ring geometry.
|
||||
|
||||
# Number of pages consumed by this frame (rounded up)
|
||||
pages_used = Signal(5)
|
||||
Returns a `(first, last, total)` tuple of signals describing the ring
|
||||
actually in use.
|
||||
|
||||
# Default: no pulses
|
||||
m.d.sync += self.rx_irq.eq(0)
|
||||
m.d.sync += self.rx_wptr_w_en.eq(0)
|
||||
m.d.sync += self.rx_overflow.eq(0)
|
||||
`bp`/`rhbp` are honoured only when they describe a usable ring inside
|
||||
SPRAM; otherwise the default 1-15 layout stands and `geom_honoured`
|
||||
goes low so the disagreement is observable. Page 0 holds the TX
|
||||
buffer, so BP must be at least 1, and a garbage write (BP = 0, or RHBP
|
||||
below BP) would otherwise give a zero or negative ring size and send
|
||||
the writer outside it.
|
||||
|
||||
# ── Sanitised ring bounds ─────────────────────────────────────────
|
||||
# A driver's BP/RHBP are used only when they describe a usable ring
|
||||
# inside SPRAM; otherwise the hardcoded layout stands. Without this a
|
||||
# garbage write (BP = 0, or RHBP below BP) would give a zero or
|
||||
# negative ring size and the assembler would write outside it. Page 0
|
||||
# is reserved for the TX buffer, so BP must be at least 1.
|
||||
# ⚠️ REGISTERED, not combinational. Computing these every cycle put
|
||||
# bp/rhbp -> comparators -> Mux -> subtract -> pg_total -> add -> raw
|
||||
# -> compare -> free -> compare -> have_room all in ONE combinational
|
||||
# path, where it used to be constant-folded. `clk` fmax collapsed
|
||||
# from ~29 MHz to ~17 MHz and ALL EIGHT SEEDS FAILED the 24 MHz
|
||||
# target. bp/rhbp only change when the GC writes BP/RHBP, so one
|
||||
# cycle of staleness costs nothing and splits the path in two.
|
||||
# ⚠️ REGISTERED AND 4 BITS WIDE, both deliberately.
|
||||
#
|
||||
# Computing these combinationally put bp/rhbp -> comparators -> Mux ->
|
||||
# subtract -> pg_total -> add -> raw -> compare -> free -> compare ->
|
||||
# have_room in ONE path, where it used to be constant-folded. `clk`
|
||||
# fmax collapsed from ~29 MHz to ~17 MHz and ALL EIGHT SEEDS FAILED
|
||||
# the 24 MHz target. Registering split it, but 8-bit variable-vs-
|
||||
# variable comparators are carry chains where variable-vs-CONSTANT
|
||||
# folds into a couple of LUTs, so it was still only ~20 MHz.
|
||||
#
|
||||
# Ring pages are 1-15, so 4 bits is the natural width; geom_ok
|
||||
# guarantees bp < rhbp <= 0x0F, which is what makes the narrow slice
|
||||
# safe. bp/rhbp change only when the GC writes BP/RHBP, so a cycle of
|
||||
# staleness costs nothing.
|
||||
⚠️ These are REGISTERED and 4 BITS WIDE, both deliberately, and both
|
||||
for timing rather than function.
|
||||
|
||||
Computing them combinationally put bp/rhbp → comparators → Mux →
|
||||
subtract → total → add → compare → free → compare → have_room in ONE
|
||||
path, where the constants it replaced had been folded away. `clk`
|
||||
fmax collapsed from ~29 MHz to ~17 MHz and ALL EIGHT SEEDS FAILED the
|
||||
24 MHz target. Registering split the path, but 8-bit variable-vs-
|
||||
variable comparators are carry chains where variable-vs-constant folds
|
||||
into a couple of LUTs, so it was still only ~20 MHz.
|
||||
|
||||
Ring pages are 1-15, so 4 bits is the natural width, and the
|
||||
`geom_ok` test guarantees bp < rhbp <= 0x0F -- which is what makes the
|
||||
narrow slice safe. `bp`/`rhbp` only change when the GC writes BP or
|
||||
RHBP, so a cycle of staleness costs nothing.
|
||||
"""
|
||||
geom_ok = Signal()
|
||||
geom_ok_c = Signal()
|
||||
pg_first = Signal(4, init=_RX_PAGE_FIRST)
|
||||
pg_last = Signal(4, init=_RX_PAGE_LAST)
|
||||
pg_total = Signal(5, init=_PAGES_TOTAL)
|
||||
|
||||
m.d.comb += geom_ok_c.eq((self.bp >= _RX_PAGE_FIRST)
|
||||
& (self.rhbp <= _RX_PAGE_LAST)
|
||||
& (self.rhbp > self.bp))
|
||||
@@ -219,19 +196,35 @@ class RXFrameAssembler(Elaboratable):
|
||||
_PAGES_TOTAL)),
|
||||
]
|
||||
m.d.comb += self.geom_honoured.eq(geom_ok)
|
||||
return pg_first, pg_last, pg_total
|
||||
|
||||
def _free_space(self, m, rwp, pg):
|
||||
"""How many pages may still be consumed. Returns `have_room`.
|
||||
|
||||
`rwp == rrp` means EMPTY, so writing the whole ring would wrap back to
|
||||
looking empty and lose the lot. Usable is therefore
|
||||
`(rrp - rwp - 1) mod total`, which is `total - 1` on an empty ring.
|
||||
|
||||
A frame's length is unknown until EOF, so starting one reserves the
|
||||
WORST case -- 1518 bytes plus the descriptor, six pages. Beginning a
|
||||
frame with less room than that risks running into unread data partway
|
||||
through, which is worse than refusing it outright.
|
||||
|
||||
⚠️ `have_room` and the clamped `rrp` are REGISTERED, for timing. The
|
||||
free-space sum is a mod-15 subtract plus a comparator, and feeding
|
||||
that straight into the FSM's branch condition cost real depth in
|
||||
`sync`, the domain with the least margin in this design (`clk` fell
|
||||
27.8-32.2 → 27.0-28.0 MHz). A cycle of staleness is harmless: `rwp`
|
||||
only moves at end-of-frame and `rrp` only when the GC writes it, so
|
||||
both are many cycles old by the time the next SOF arrives.
|
||||
"""
|
||||
pg_first, pg_last, pg_total = pg
|
||||
|
||||
# ── How many pages may we still consume? ──────────────────────────
|
||||
# rwp == rrp means EMPTY, so writing the whole ring would wrap back to
|
||||
# "empty" and lose the lot. Usable = (rrp - rwp - 1) mod pg_total,
|
||||
# which is pg_total-1 when the ring is empty.
|
||||
# rrp_s is REGISTERED too: the clamp is two comparisons against the
|
||||
# (now variable) bounds, and the GC writes RRP about as often as it
|
||||
# writes BP/RHBP. `free` already reaches the FSM through a register
|
||||
# (`have_room`), so this adds no behavioural latency that matters.
|
||||
rrp_s = Signal(4, init=_RX_PAGE_FIRST)
|
||||
m.d.sync += rrp_s.eq(Mux((self.rrp >= pg_first)
|
||||
& (self.rrp <= pg_last),
|
||||
self.rrp[0:4], pg_first))
|
||||
|
||||
raw = Signal(5)
|
||||
free = Signal(5)
|
||||
with m.If(rrp_s >= rwp):
|
||||
@@ -240,21 +233,59 @@ class RXFrameAssembler(Elaboratable):
|
||||
m.d.comb += raw.eq(rrp_s + pg_total - rwp)
|
||||
m.d.comb += free.eq(Mux(raw == 0, pg_total - 1, raw - 1))
|
||||
|
||||
# A frame's length is not known until EOF, so reserve the worst case:
|
||||
# 1518 bytes + a 4-byte descriptor is 6 pages. Starting a frame with
|
||||
# less room than that risks running into unread data partway through,
|
||||
# which is worse than refusing it outright.
|
||||
# REGISTERED, not combinational. The free-space calculation is a
|
||||
# mod-15 subtract plus a comparator, and feeding that into the FSM's
|
||||
# branch condition cost real depth in `sync` -- the domain with the
|
||||
# least margin in this design (clk fell 27.8-32.2 -> 27.0-28.0 MHz
|
||||
# when this was comb). One cycle of staleness is harmless: rwp only
|
||||
# moves at end-of-frame and rrp only when the GC writes it, so both
|
||||
# are many cycles old by the time the next SOF arrives.
|
||||
have_room = Signal()
|
||||
m.d.sync += have_room.eq(free >= _MAX_FRAME_PAGES)
|
||||
return have_room
|
||||
|
||||
def _frame_fsm(self, m, rwp, pg, have_room):
|
||||
"""Store one frame per pass through the ring.
|
||||
|
||||
States, in the order a frame visits them:
|
||||
|
||||
IDLE wait for SOF; branch on whether the ring has
|
||||
room for a worst-case frame
|
||||
DROP_FRAME ring full: consume to EOF and raise rx_overflow
|
||||
WRITE_PLACEHOLDER four 0x00 bytes where the descriptor will go,
|
||||
so the body starts at page+4
|
||||
RECV_AND_WRITE stream each received byte into SPRAM
|
||||
WAIT_LAST_WRITE let the final body write be accepted, and work
|
||||
out how many pages the frame used
|
||||
CALC_NEXT compute the page the NEXT frame will start at
|
||||
WRITE_D0..D3 write the real descriptor, now that length and
|
||||
next page are both known
|
||||
ADVANCE_RWP move the write pointer to that next page
|
||||
PUSH_WPT hand RWP to the register file and pulse rx_irq
|
||||
|
||||
⚠️ EVERY write state advances on `valid & ready`, never on `ready`
|
||||
alone. `eth_wr_valid` is scheduled with `m.d.sync` and so is LOW for
|
||||
the whole first cycle in a state, while `ready` is tested immediately
|
||||
-- and `ready` is usually already high on entry. Testing `ready`
|
||||
alone fires `valid.eq(1)` and `valid.eq(0)` in the same cycle, the
|
||||
later assignment wins, and the write is never issued.
|
||||
|
||||
That bug existed twice, and neither instance was visible to a
|
||||
testbench that held `ready` high:
|
||||
|
||||
* in the placeholder, only THREE bytes went out, so the body began
|
||||
at page+3 and EVERY FRAME WAS SHIFTED ONE BYTE EARLY. On hardware
|
||||
the ring filled and RWP advanced normally while the GC's
|
||||
ethertype test read the wrong offset and silently dropped
|
||||
everything.
|
||||
* in the descriptor, the length write was dropped, leaving byte 2 at
|
||||
its placeholder -- hardware read a descriptor of 00 00 00 01 and
|
||||
rejected the ring. Intermittent, because it depended on whether
|
||||
the arbiter happened to hold `ready` high on the entry cycle.
|
||||
"""
|
||||
pg_first, pg_last, pg_total = pg
|
||||
|
||||
wr_addr = Signal(16) # next SPRAM byte address to write
|
||||
data_ctr = Signal(12) # frame data bytes received so far
|
||||
total_len = Signal(12) # data_ctr + the 4 descriptor bytes
|
||||
next_page = Signal(12) # page the NEXT frame will start at
|
||||
desc_base = Signal(16) # this frame's page start (rwp * 256)
|
||||
desc_ctr = Signal(2) # placeholder bytes written, 0..3
|
||||
pages_used = Signal(5)
|
||||
|
||||
# Combinatorial outputs
|
||||
m.d.comb += total_len.eq(data_ctr + 4)
|
||||
|
||||
with m.FSM(domain="sync", name="rx_fsm"):
|
||||
@@ -264,9 +295,6 @@ class RXFrameAssembler(Elaboratable):
|
||||
m.d.sync += self.eth_wr_valid.eq(0)
|
||||
with m.If(self.rx_valid & self.rx_sof & self.rx_enabled
|
||||
& ~have_room):
|
||||
# Ring full: discard this frame rather than overwrite data
|
||||
# the GC has not read. This is what the real BBA's RBFI
|
||||
# reports, and what this module previously lacked.
|
||||
m.next = "DROP_FRAME"
|
||||
with m.Elif(self.rx_valid & self.rx_sof & self.rx_enabled):
|
||||
frame_base = Signal(16)
|
||||
@@ -278,40 +306,23 @@ class RXFrameAssembler(Elaboratable):
|
||||
m.next = "WRITE_PLACEHOLDER"
|
||||
|
||||
with m.State("DROP_FRAME"):
|
||||
# Consume and discard to EOF. rx_ready MUST stay high here:
|
||||
# stalling the ethernet master instead would back pressure into
|
||||
# the W5100's own buffer and turn a dropped frame into a wedged
|
||||
# receiver.
|
||||
# Discard to EOF rather than overwrite data the GC has not
|
||||
# read. This is what the real BBA reports through RBFI.
|
||||
#
|
||||
# rx_ready MUST stay high: stalling the ethernet master
|
||||
# instead would back-pressure into the W5100's own buffer and
|
||||
# turn a dropped frame into a wedged receiver.
|
||||
m.d.comb += self.rx_ready.eq(1)
|
||||
with m.If(self.rx_valid & self.rx_eof):
|
||||
m.d.sync += self.rx_overflow.eq(1)
|
||||
m.next = "IDLE"
|
||||
|
||||
with m.State("WRITE_PLACEHOLDER"):
|
||||
# Write 4 bytes of 0x00 as placeholder descriptor.
|
||||
#
|
||||
# ⚠️ Advance on `valid & ready`, NOT on `ready` alone.
|
||||
# `eth_wr_valid` is scheduled with m.d.sync and so is LOW for
|
||||
# the whole first cycle in this state, while `ready` is tested
|
||||
# immediately -- and `ready` is usually already high here.
|
||||
# Testing `ready` alone therefore counted a write that was
|
||||
# never issued: only THREE placeholder bytes went out and
|
||||
# wr_addr ended at desc_base+3, so the frame body started at
|
||||
# descriptor byte 3 and EVERY RECEIVED FRAME WAS SHIFTED ONE
|
||||
# BYTE EARLY.
|
||||
#
|
||||
# Hardware symptom (2026-10-09): the ring filled normally and
|
||||
# RWP advanced, but the GC's ethertype test read the wrong
|
||||
# offset and silently dropped every frame -- "no ACK" from the
|
||||
# host with no other sign of trouble. The matching
|
||||
# descriptor bug (same cause, in the length write) showed up as a
|
||||
# descriptor of 00 00 00 01.
|
||||
#
|
||||
# The first byte is written twice as a consequence of
|
||||
# eth_wr_addr trailing wr_addr by a cycle, and desc_base+3 is
|
||||
# left unwritten -- both harmless, because every descriptor
|
||||
# byte is either zero anyway or overwritten by WRITE_LEN_*.
|
||||
# What matters is that wr_addr ends at desc_base+4.
|
||||
# The first byte is written twice, because eth_wr_addr trails
|
||||
# wr_addr by a cycle, and desc_base+3 is never reached. Both
|
||||
# are harmless -- every descriptor byte is overwritten by
|
||||
# WRITE_D0..D3 later. What matters is that wr_addr ends at
|
||||
# desc_base+4, so the frame body starts there.
|
||||
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)
|
||||
@@ -324,7 +335,9 @@ class RXFrameAssembler(Elaboratable):
|
||||
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
|
||||
# One SPRAM write per received byte, with no intermediate
|
||||
# buffering: accept a byte whenever the write port can take
|
||||
# the previous one.
|
||||
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)
|
||||
@@ -338,34 +351,15 @@ class RXFrameAssembler(Elaboratable):
|
||||
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)
|
||||
# pages = ceil(total_len / 256)
|
||||
m.d.sync += pages_used.eq(total_len[8:12] + (total_len[:8] != 0))
|
||||
m.next = "CALC_NEXT"
|
||||
|
||||
# ⚠️ All three descriptor writes must advance on `valid & ready`,
|
||||
# NOT on `ready` alone. `valid` is scheduled with m.d.sync and so is low
|
||||
# for the whole first cycle in the state, while `ready` is tested
|
||||
# immediately -- and coming out of WAIT_LAST_WRITE `ready` is
|
||||
# usually already high. Testing `ready` alone therefore fired both
|
||||
# valid.eq(1) and valid.eq(0) in the same cycle, the later won, the
|
||||
# write was never issued, and the length byte was SILENTLY DROPPED.
|
||||
#
|
||||
# Hardware symptom (2026-10-09): a descriptor of 00 00 00 01 --
|
||||
# byte 2 still at its placeholder 0x00 because the HI write was
|
||||
# dropped -- so the GC read a 1-byte frame length and rejected the
|
||||
# ring. Intermittent by nature: it depends on whether the arbiter
|
||||
# happens to hold `ready` high on the entry cycle, which is why
|
||||
# bring-up's 342-byte DHCP frame read a correct 00 00 01 5A and
|
||||
# this went unnoticed.
|
||||
with m.State("CALC_NEXT"):
|
||||
# The descriptor carries next_packet_ptr, so the next page must
|
||||
# be known BEFORE the descriptor is written -- this state used
|
||||
# to come after it (as ADVANCE_RWP).
|
||||
# next_rwp = ((rwp - 1 + pages_used) % 15) + 1
|
||||
# The descriptor carries next_packet_ptr, so the next page has
|
||||
# to be known BEFORE the descriptor is written.
|
||||
next_rwp_raw = Signal(8)
|
||||
m.d.comb += next_rwp_raw.eq(rwp + pages_used)
|
||||
with m.If(next_rwp_raw > pg_last):
|
||||
@@ -375,7 +369,7 @@ class RXFrameAssembler(Elaboratable):
|
||||
m.next = "WRITE_D0"
|
||||
|
||||
with m.State("WRITE_D0"):
|
||||
# Descriptor byte 0 = next_packet_ptr[7:0]
|
||||
# next_packet_ptr[7:0]
|
||||
m.d.sync += self.eth_wr_addr.eq(desc_base + 0)
|
||||
m.d.sync += self.eth_wr_data.eq(next_page[0:8])
|
||||
m.d.sync += self.eth_wr_valid.eq(1)
|
||||
@@ -384,7 +378,7 @@ class RXFrameAssembler(Elaboratable):
|
||||
m.next = "WRITE_D1"
|
||||
|
||||
with m.State("WRITE_D1"):
|
||||
# Descriptor byte 1 = packet_len[3:0] << 4 | next_ptr[11:8].
|
||||
# packet_len[3:0] << 4 | next_packet_ptr[11:8].
|
||||
# Cat puts its FIRST argument in the low bits.
|
||||
m.d.sync += self.eth_wr_addr.eq(desc_base + 1)
|
||||
m.d.sync += self.eth_wr_data.eq(
|
||||
@@ -395,8 +389,7 @@ class RXFrameAssembler(Elaboratable):
|
||||
m.next = "WRITE_D2"
|
||||
|
||||
with m.State("WRITE_D2"):
|
||||
# Descriptor byte 2 = packet_len[11:4]. Byte 3 (status) keeps
|
||||
# the placeholder 0x00, which libogc reads as "no error".
|
||||
# packet_len[11:4]
|
||||
m.d.sync += self.eth_wr_addr.eq(desc_base + 2)
|
||||
m.d.sync += self.eth_wr_data.eq(total_len[4:12])
|
||||
m.d.sync += self.eth_wr_valid.eq(1)
|
||||
@@ -405,19 +398,16 @@ class RXFrameAssembler(Elaboratable):
|
||||
m.next = "WRITE_D3"
|
||||
|
||||
with m.State("WRITE_D3"):
|
||||
# Descriptor byte 3 = status = 0x00 ("no error": libogc tests
|
||||
# it against 0x84).
|
||||
# status = 0x00, "no error": the driver tests this byte
|
||||
# against 0x84.
|
||||
#
|
||||
# ⚠️ This MUST be written explicitly and cannot rely on the
|
||||
# placeholder. WRITE_PLACEHOLDER issues four writes but the
|
||||
# first lands twice (eth_wr_addr trails wr_addr by a cycle),
|
||||
# so desc_base+3 is NEVER written. That was harmless while
|
||||
# the length write covered byte 3; now byte 3 is the status
|
||||
# and leaving it would hand libogc whatever the previous
|
||||
# frame -- or power-on -- left in that SPRAM cell, and any
|
||||
# frame landing on a stale 0x80 or 0x04 would be discarded as
|
||||
# an error. The simulation model reports it as an unwritten
|
||||
# cell; real SPRAM has no such tell.
|
||||
# ⚠️ This MUST be written explicitly and cannot lean on the
|
||||
# placeholder, because WRITE_PLACEHOLDER never reaches
|
||||
# desc_base+3 (see its note). Leaving it would hand the
|
||||
# driver whatever the previous frame -- or power-on -- left in
|
||||
# that SPRAM cell, and a stale 0x80 or 0x04 discards the frame
|
||||
# as an error. Simulation reports an unwritten cell; real
|
||||
# SPRAM has no such tell.
|
||||
m.d.sync += self.eth_wr_addr.eq(desc_base + 3)
|
||||
m.d.sync += self.eth_wr_data.eq(0x00)
|
||||
m.d.sync += self.eth_wr_valid.eq(1)
|
||||
@@ -436,27 +426,29 @@ class RXFrameAssembler(Elaboratable):
|
||||
m.d.sync += self.rx_irq.eq(1)
|
||||
m.next = "IDLE"
|
||||
|
||||
# ── NCRA reset: the write pointer goes back to the ring start ──────
|
||||
# Placed AFTER the FSM so it takes priority over ADVANCE_RWP in the
|
||||
# same cycle.
|
||||
#
|
||||
# This MUST happen here as well as in the register file: they are two
|
||||
# copies of the same pointer, and resetting only one leaves them
|
||||
# disagreeing about where the ring is. A driver that then syncs RRP to
|
||||
# the register file's value points somewhere the writer is not, the
|
||||
# free-space calculation shrinks with every frame, and once it drops
|
||||
# below _MAX_FRAME_PAGES EVERY frame is dropped -- silently, because a
|
||||
# dropped frame never reaches the ring to be counted. Hardware
|
||||
# symptom: "timeout: no match (0 seen, 0 bad desc, 0 lapped)".
|
||||
#
|
||||
# No in-flight frame needs abandoning: the same NCRA write that pulses
|
||||
# this also clears the SR bit, so rx_enabled is low and the FSM is in
|
||||
# IDLE.
|
||||
def _ncra_reset(self, m, rwp, pg):
|
||||
"""Return the write pointer to the ring start on the GC's NCRA reset.
|
||||
|
||||
Must be elaborated AFTER the FSM so that it takes priority over
|
||||
ADVANCE_RWP in the same cycle.
|
||||
|
||||
This has to happen here as well as in the register file: the two are
|
||||
copies of the same pointer, and resetting only one leaves them
|
||||
disagreeing about where the ring is. A driver that then syncs RRP to
|
||||
the register file's value points somewhere the writer is not, the
|
||||
free-space sum shrinks with every frame, and once it falls below
|
||||
_MAX_FRAME_PAGES EVERY frame is dropped -- silently, since a dropped
|
||||
frame never reaches the ring to be counted. The hardware symptom was
|
||||
"0 seen, 0 bad desc, 0 lapped".
|
||||
|
||||
No in-flight frame needs abandoning: the same NCRA write that pulses
|
||||
this also clears the SR bit, so `rx_enabled` is low and the FSM is
|
||||
already in IDLE.
|
||||
"""
|
||||
pg_first, _pg_last, _pg_total = pg
|
||||
with m.If(self.ncra_rst):
|
||||
m.d.sync += rwp.eq(pg_first)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
# ── Testbench ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -490,28 +482,23 @@ if __name__ == "__main__":
|
||||
errors.append(name)
|
||||
|
||||
async def testbench(ctx):
|
||||
# Final contents of every SPRAM address the DUT wrote.
|
||||
#
|
||||
# The old testbench declared a writes list and never filled it, and
|
||||
# drove the RX stream OPEN-LOOP -- one byte per tick, never checking
|
||||
# rx_ready. Both of the 2026-10-09 handshake bugs were therefore
|
||||
# invisible to it: it only ever asserted on rwp and rx_irq, so a
|
||||
# dropped descriptor byte or a frame body written one byte early
|
||||
# changed nothing it looked at.
|
||||
# Final contents of every SPRAM address the DUT wrote, plus the order
|
||||
# the writes were issued in.
|
||||
mem = {}
|
||||
order = []
|
||||
|
||||
# rx_irq and rx_overflow are 1-CYCLE pulses, and rx_overflow fires on
|
||||
# the very tick that consumes the EOF byte -- i.e. inside send_frame.
|
||||
# Polling for it afterwards misses it entirely, which first showed up
|
||||
# as a dropped frame looking like a stall. Latch both in tick().
|
||||
# Polling for it afterwards misses it entirely, which showed up as a
|
||||
# dropped frame looking like a stall, so tick() latches both.
|
||||
pulse = {"irq": None, "ovf": False}
|
||||
# eth_wr_ready stall pattern. Held constantly HIGH by every test this
|
||||
# file ever had, which is a serious blind spot: on hardware the SPRAM
|
||||
# arbiter stalls the write port constantly, because the GC is reading
|
||||
# the ring over EXI while frames are being written into it. Any
|
||||
|
||||
# eth_wr_ready stall pattern: assert ready 1 cycle in stall[0].
|
||||
# Holding it constantly high is a serious blind spot, because on
|
||||
# hardware the SPRAM arbiter stalls this port constantly -- the GC
|
||||
# reads the ring over EXI while frames are being written into it. Any
|
||||
# address bookkeeping that depends on ready timing is invisible until
|
||||
# ready actually goes low. stall[0] = assert ready 1 cycle in N.
|
||||
# ready actually goes low.
|
||||
stall = [1]
|
||||
cyc = [0]
|
||||
|
||||
@@ -545,24 +532,22 @@ if __name__ == "__main__":
|
||||
tag = ["?"]
|
||||
|
||||
async def send_frame(frame):
|
||||
pulse["irq"] = None
|
||||
pulse["ovf"] = False
|
||||
"""Push a frame RESPECTING BACK-PRESSURE, the way the real
|
||||
ethernet master does. rx_ready is LOW through IDLE and the whole
|
||||
placeholder-descriptor write, so a master that advances anyway
|
||||
simply loses those bytes -- which is what the old testbench did,
|
||||
and why it could not see the frame body land at the wrong
|
||||
offset."""
|
||||
placeholder write, so a master that advances anyway simply loses
|
||||
those bytes -- which is how a frame body landing at the wrong
|
||||
offset went unnoticed."""
|
||||
pulse["irq"] = None
|
||||
pulse["ovf"] = False
|
||||
for i, b in enumerate(frame):
|
||||
ctx.set(dut.rx_data, b)
|
||||
ctx.set(dut.rx_valid, 1)
|
||||
ctx.set(dut.rx_sof, 1 if i == 0 else 0)
|
||||
ctx.set(dut.rx_eof, 1 if i == len(frame) - 1 else 0)
|
||||
# Apply ready, THEN read rx_ready, THEN clock -- so the
|
||||
# value the DUT decides on is the one this loop checked.
|
||||
# Polling before applying had the loop waiting on a byte the
|
||||
# DUT had already accepted: the frame stored correctly while
|
||||
# send_frame reported rx_ready never asserted.
|
||||
# Apply ready, THEN read rx_ready, THEN clock -- so the value
|
||||
# the DUT decides on is the one this loop checked. Polling
|
||||
# before applying left the loop waiting on a byte the DUT had
|
||||
# already accepted.
|
||||
guard = 0
|
||||
while True:
|
||||
apply_ready()
|
||||
@@ -603,7 +588,7 @@ if __name__ == "__main__":
|
||||
return [mem.get(base + i) for i in range(4)]
|
||||
|
||||
def pack_desc(total, next_ptr, status=0):
|
||||
"""The 4 descriptor bytes libogc's driver expects."""
|
||||
"""The 4 descriptor bytes the GC's driver expects."""
|
||||
word = (status << 24) | ((total & 0xFFF) << 12) | (next_ptr & 0xFFF)
|
||||
return [(word >> (8 * i)) & 0xFF for i in range(4)]
|
||||
|
||||
@@ -641,19 +626,15 @@ if __name__ == "__main__":
|
||||
check("T1 descriptor via libogc decode", libogc_decode(descriptor(1)),
|
||||
(len(f1), 2, False))
|
||||
# The body MUST start at base+4. This is the check that catches a
|
||||
# placeholder write being dropped: with only three placeholder bytes
|
||||
# written the body landed at base+3 and every frame was shifted one
|
||||
# byte early, which on hardware made the GC drop every frame.
|
||||
# dropped placeholder write: with only three written, the body landed
|
||||
# at base+3 and the GC dropped every frame.
|
||||
check("T1 body at base+4", body(1, len(f1)), f1)
|
||||
|
||||
await tick(4)
|
||||
|
||||
# ── T2: frame LONGER THAN 255 BYTES, so the descriptor's HIGH length
|
||||
# byte is non-zero. This is the case that catches a dropped
|
||||
# WRITE_LEN_HI: with the high byte left at its placeholder 0x00 the GC
|
||||
# read an impossible length (hardware saw 00 00 00 01) and rejected
|
||||
# the ring. Every earlier test used a short frame, where a dropped
|
||||
# high byte is indistinguishable from a correct zero. ───────────────
|
||||
# ── T2: frame LONGER THAN 255 BYTES, so the descriptor's high length
|
||||
# nibble is non-zero. A short frame cannot distinguish a dropped
|
||||
# high-byte write from a correct zero. ──────────────────────────────
|
||||
f2 = [(i * 7 + 3) & 0xFF for i in range(300)]
|
||||
tag[0] = "T2"
|
||||
await send_frame(f2)
|
||||
@@ -671,11 +652,8 @@ if __name__ == "__main__":
|
||||
# ── T4: ring overflow must DROP, not overwrite ───────────────────
|
||||
# rrp is held still, as if the GC had stopped reading. The writer
|
||||
# must refuse new frames once the ring is nearly full and leave the
|
||||
# unread ones intact. Before this existed the writer simply lapped
|
||||
# the reader, so a traffic burst corrupted whatever frame the driver
|
||||
# was midway through -- with no indication it had happened. That is
|
||||
# what produced a "descriptor" of 00 00 00 01 on hardware: frame data
|
||||
# from the frame that had replaced the one being read.
|
||||
# unread ones intact; lapping the reader corrupts whatever frame the
|
||||
# driver is midway through, with nothing reporting it.
|
||||
ctx.set(dut.rrp, 4) # GC has consumed up to page 4
|
||||
await tick(2)
|
||||
f3 = [(i * 3 + 1) & 0xFF for i in range(300)] # 2 pages each
|
||||
@@ -710,10 +688,9 @@ if __name__ == "__main__":
|
||||
|
||||
# ── T5: the same frame WITH THE WRITE PORT STALLING ──────────────
|
||||
# Models arbiter contention: ready asserted only 1 cycle in 3. The
|
||||
# descriptor and body offset must be identical to the unstalled case.
|
||||
# This is the case hardware runs in and no previous test ever did --
|
||||
# hardware reported a frame whose body was one byte late while
|
||||
# simulation with ready held high was byte-perfect.
|
||||
# descriptor and body offsets must match the unstalled case exactly.
|
||||
# This is the condition hardware runs in, and holding ready high hid a
|
||||
# frame body landing one byte late.
|
||||
ctx.set(dut.rrp, 1)
|
||||
await tick(2)
|
||||
stall[0] = 3
|
||||
@@ -734,11 +711,9 @@ if __name__ == "__main__":
|
||||
await tick(4)
|
||||
|
||||
# ── T6: NCRA reset must return the WRITE pointer to page 1 ───────
|
||||
# The register file resets its own copy of RWP on an NCRA reset; if
|
||||
# this module does not reset its own, the two disagree and the GC ends
|
||||
# up pointing somewhere the writer is not. The free-space sum then
|
||||
# shrinks with every frame until every frame is dropped -- silently,
|
||||
# which on hardware read as "0 seen, 0 bad desc, 0 lapped".
|
||||
# If this module keeps its own RWP while the register file resets its
|
||||
# copy, the two disagree, the free-space sum shrinks with every frame,
|
||||
# and eventually every frame is dropped silently.
|
||||
ctx.set(dut.rrp, 1)
|
||||
await tick(2)
|
||||
ctx.set(dut.ncra_rst, 1)
|
||||
@@ -767,11 +742,10 @@ if __name__ == "__main__":
|
||||
ctx.set(dut.rx_sof, 0)
|
||||
check("T3 no writes while rx disabled", len(mem), before)
|
||||
|
||||
# ── T7: the GC's BP/RHBP are HONOURED (compat TODO item 2) ───────
|
||||
# These were hardcoded to pages 1-15, so a driver configuring a
|
||||
# SMALLER ring was ignored and we kept writing pages it would never
|
||||
# read -- silent corruption. libogc2 writes BP=1/RHBP=15, exactly the
|
||||
# old hardcode, which is why it never showed up.
|
||||
# ── T7: the GC's BP/RHBP are HONOURED ────────────────────────────
|
||||
# A driver configuring a SMALLER ring must not be ignored, or we keep
|
||||
# writing pages it will never read. libogc2 happens to write
|
||||
# BP=1/RHBP=15, so only a different geometry exercises this.
|
||||
ctx.set(dut.rx_enabled, 1)
|
||||
ctx.set(dut.rrp, 1)
|
||||
ctx.set(dut.bp, 1)
|
||||
@@ -814,7 +788,7 @@ if __name__ == "__main__":
|
||||
check(f"T7b reject {why}", "honoured", "rejected")
|
||||
check("T7b garbage geometry rejected", ctx.get(dut.geom_honoured), 0)
|
||||
|
||||
# ... and with it rejected, the hardcoded layout still works.
|
||||
# ... and with it rejected, the default layout still works.
|
||||
ctx.set(dut.rrp, 1)
|
||||
await tick(4)
|
||||
mem.clear()
|
||||
|
||||
Reference in New Issue
Block a user