Skip to content

framing_top: Drive TSTRB and TKEEP on the receive stream - #6

Open
dngr2 wants to merge 1 commit into
pulp-platform:mainfrom
dngr2:fix/rx-drive-tstrb-tkeep
Open

framing_top: Drive TSTRB and TKEEP on the receive stream#6
dngr2 wants to merge 1 commit into
pulp-platform:mainfrom
dngr2:fix/rx-drive-tstrb-tkeep

Conversation

@dngr2

@dngr2 dngr2 commented Aug 15, 2026

Copy link
Copy Markdown

Found while investigating #5. Separate bug, receive side, and independent of that one — no shared code.

The problem

The always_comb at rtl/framing_top.sv:81-118 drives the receive stream:

rx_axis_req_o.t.data = accept_frame_q ? rx_axis_tdata_0_q  : 'd0;
rx_axis_req_o.tvalid = accept_frame_q ? rx_axis_tvalid_0_q : 'd0;
rx_axis_req_o.t.last = accept_frame_q ? rx_axis_tlast_0_q  : 'd0;
rx_axis_req_o.t.user = accept_frame_q ? rx_axis_tuser_0_q  : 'd0;

.t.strb and .t.keep are never assigned — not in that block, not anywhere else in the module, and there is no default assignment to the struct. Members left unassigned in an always_comb infer a latch and carry no defined value.

They are not unused: eth_top.sv:115 takes this output as s_framing_rx_req and feeds it to axi_stream_dw_upsizer at line 155, which builds the word-level TKEEP by concatenating the per-byte TKEEP it receives.

The received data is correct. What is missing is the validity signalling — how much of the final word belongs to the frame. A consumer either ignores TKEEP or reads bits that were never driven.

Demonstrated end to end

Two eth_top_synth instances cross-connected over RGMII, one transmitting a broadcast frame into the other. Same simulation, same frame, only this change differs:

Received word Before After
0 aabbffffffffffff keep=0x00 keep=0xff
1 0008112233445566 keep=0x00 keep=0xff
2 000000000000bbaa keep=0x00 keep=0xff
3-6 (pad to 60-byte minimum) keep=0x00 keep=0xff
7 ab83246b... (FCS) keep=0x00 keep=0xff

Padding and FCS are generated by the hardware, so the frame really did traverse the MAC and the RGMII pins.

The change

Both signals track TVALID, gated by accept_frame_q like the neighbouring assignments, because every accepted beat of this byte-wide stream carries one real data byte. '1 rather than a sized replication, so the width follows the stream if FramingDataWidth ever changes.

.t.id and .t.dest are also unassigned. They are zero-width in every configuration here, so there is no hardware to latch and I left them alone rather than introduce zero-width assignments — say the word if you would rather they were driven too.

The testbench

target/sim/src/eth_rx_keep_tb.sv, registered in Bender.yml. Same loopback topology as eth_tb, but driven procedurally rather than with the randomized class-based drivers, so it runs on an open-source simulator as well as Questa and the checks name exact values.

Against the unfixed RTL:

  PASS  a frame is received
  PASS  word 0 matches the transmitted header
  PASS  word 1 matches the transmitted header
  FAIL  TKEEP reports valid bytes -- every received word has TKEEP=0x00
  FAIL  the final word is fully valid -- final word TKEEP is not 0xFF
  PASS  TSTRB tracks TKEEP

The data checks pass both before and after; only the TKEEP checks move. That is the distinction the bug turns on, and it is why the testbench checks both rather than just asserting a frame arrived.

What I have not done

I have no Questa licence, so I ran this with Verilator 5.006 rather than through eth.mk. The testbench uses no classes or randomization and should run unmodified under Questa, but I have not been able to confirm that, and the existing eth_tb is untouched.

Building it required axi_stream, register_interface (including the vendored prim_subreg) and tech_cells_generic. Worth noting separately: Bender.lock pins axi_stream at 472751f550e3918215603e21734fe0ece3c66f79, which is not reachable in the public repository — I fetched all refs before saying so — so I resolved against current master. That is also raised in #5.

The always_comb driving rx_axis_req_o assigned .t.data, .tvalid, .t.last
and .t.user, and never .t.strb or .t.keep. Struct members left unassigned
in an always_comb infer a latch and carry no defined value.

Those bits feed axi_stream_dw_upsizer, which builds the word-level TKEEP
by concatenating the per-byte TKEEP it receives, so a consumer was told
no bytes were valid in any word of any frame. The received data itself is
correct; what is missing is the validity signalling that says how much of
the final word belongs to the frame.

Every accepted beat of this byte-wide stream carries one real data byte,
so both signals simply track TVALID, gated by accept_frame_q like the
neighbouring assignments. '1 is used rather than a sized replication so
the width follows the stream if FramingDataWidth ever changes.

.t.id and .t.dest are also unassigned, but are zero-width in every
configuration in this repository, so they are left alone.

Adds eth_rx_keep_tb: the same two-instance RGMII loopback as eth_tb, with
one instance transmitting into the other, driven procedurally instead of
with the randomized class-based drivers so it also runs on an open-source
simulator. Against the unfixed RTL its two TKEEP checks fail while the
data checks still pass, which is the distinction the bug turns on.
@dngr2
dngr2 force-pushed the fix/rx-drive-tstrb-tkeep branch from 85f1f02 to bf5c062 Compare August 16, 2026 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant