Skip to content

UCIe · Module 9

Streaming Packet Transport

How a streaming message becomes a bounded transport object and back — message versus transport unit, the packet assembler and flush semantics, byte-valid masks, continuation state, header/payload alignment, packet-boundary backpressure, and the semantic scoreboard framing bugs require.

Chapter 9.1 established what streaming is and why it exists: a protocol UCIe does not understand rides inside flits it does, inheriting the Adapter's error detection and replay because the payload stays opaque. It also named the two problems that opacity leaves to you — the size mismatch, and message boundaries that do not survive.

This chapter is those two problems, taken seriously. It is where the shim stops being a paragraph and becomes hardware.

The reason it deserves a full chapter is a specific and unpleasant property: framing bugs pass every transport-level check. CRC verifies that the bytes arrived as sent. It has no opinion about whether the receiver has drawn the message boundaries in the same places the transmitter did — and when it has not, you get a link reporting perfect health while delivering nonsense.

1. The One-Sentence Model

Packet transport is a boundary-preservation problem. The transmitter must record where each message begins, where it ends, and which bytes belong to it; the receiver must reconstruct exactly those boundaries from a byte stream that carries no inherent notion of them.

Everything below follows from that. The assembler exists because boundaries and transport-unit sizes do not align. The byte-valid mask exists because a partially filled unit has bytes that are present but meaningless. Continuation state exists because a message can outlive the unit that started it. And the scoreboard in §14 checks messages rather than bytes because bytes are the Adapter's problem and messages are yours.

2. What UCIe Actually Provides

The division of labour that matters for this chapter:

Owned by the AdapterOwned by you
flit header insertionmessage framing inside the payload
CRC insertion and detectionmessage boundaries and lengths
replay of corrupted flitswhich bytes of a partial payload are real
delivering payload bytes intactreassembling the original messages

The right-hand column is this chapter.

3. Message Versus Transport Unit

The distinction everything rests on:

A message is a semantic object belonging to your protocol. It has a meaning, a length your protocol chose, and a boundary that matters.

A transport unit is what crosses the link — a payload of a size the Adapter's flit format defines. It has no meaning at all.

Their relationship is many-to-many, and all three cases occur:

CaseWhat it needs
One message fits in one unita length, so the receiver knows where it ends
Several messages share a unitper-message framing, so the receiver can split them
One message spans several unitscontinuation state on both sides

The third is the interesting one, and §8 works it through. The first is where people stop testing.

Message completion and packet completion are different events. A packet may be complete with a message half-carried; a message may be complete with the packet half-empty. Code that conflates them is the source of §6's deadlock and §9's misalignment.

Messages enter a packet assembler, which feeds a packet FIFO, which feeds the UCIe transport. On the receive side the transport feeds a depacketizer, which reconstructs messages and outputs them.Message ingresssemantic messagesPacket assemblerfills, frames, flushesPacket FIFOdecouples rateMessage outputreassembled messagesDepacketizervalidates framingUCIe transportflit, CRC, replay12
Figure 1 — the packetisation path and its mirror. Semantic messages enter the assembler, which fills transport-sized payloads and marks their framing; a small queue decouples assembly from the link so that a momentarily busy transport does not immediately stall the producer. On the far side the depacketizer validates framing and reassembles the original messages. The Adapter in the middle sees only payload bytes — it protects them and it never interprets them, which is exactly why the framing on either end is the shim's responsibility rather than the transport's.

4. The Packet as One Object

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative streaming RTL — not UCIe normative signal naming or packet
// encoding. This is the shim's own framing, carried INSIDE the flit payload.
localparam int PAYLOAD_BYTES = 236;                 // format- and revision-specific
localparam int PAYLOAD_W     = PAYLOAD_BYTES * 8;
localparam int STREAM_ID_W   = 4;
localparam int LEN_W         = $clog2(PAYLOAD_BYTES + 1);
 
typedef struct packed {
  logic [STREAM_ID_W-1:0] stream_id;      // which stream this belongs to (§10)
  logic                   start;          // begins a message
  logic                   end_of_message; // completes a message
  logic [LEN_W-1:0]       byte_count;     // meaningful bytes in this payload
} stream_hdr_t;
 
typedef struct packed {
  stream_hdr_t              hdr;
  logic [PAYLOAD_W-1:0]     payload;
  logic [PAYLOAD_BYTES-1:0] byte_valid;   // per-byte validity (§7)
} stream_packet_t;

Architecture. Header and payload describe the same object and are consumed together, so they must travel together. §9 shows what happens when they do not.

State. None yet — this is the type. The state is in §5.

Cycle behaviour. A packet moves as one unit through every pipeline stage and queue.

Contract. The receiver interprets byte_valid and byte_count before touching payload. Note the redundancy is deliberate — §7 explains why both exist.

Failure. Splitting the object across separate signals invites independent pipelining, which is §9's bug.

Note the start/end_of_message pair rather than a single flag. A message occupying one packet has both set; a spanning message has start on the first, neither on the middles, and end_of_message on the last. Two bits express four states, and the fourth — neither set — is exactly the continuation case a single flag cannot represent.

5. The Assembler

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative streaming RTL — not a UCIe-defined structure.
typedef enum logic [1:0] {
  PK_EMPTY   = 2'd0,   // nothing buffered
  PK_FILLING = 2'd1,   // partially filled, accepting more
  PK_READY   = 2'd2    // a complete packet is waiting to be handed down
} pack_state_t;
 
pack_state_t              pack_state_q;
logic [PAYLOAD_W-1:0]     pack_data_q;
logic [LEN_W-1:0]         pack_bytes_q;    // bytes accumulated so far
logic [PAYLOAD_BYTES-1:0] pack_valid_q;    // which byte lanes are populated
logic                     msg_open_q;      // inside a message, spanning packets
logic                     pack_started_q;  // this packet begins a message

Architecture. Messages arrive at their own sizes and must be emitted at the transport's. Something must hold the partial state between those two rates, and it must remember whether it is mid-message so the framing bits come out right.

State. A three-state FSM, a payload-sized buffer, a byte count, a byte-valid vector, and two framing flags. msg_open_q is per-message state; pack_bytes_q is per-packet state — different lifetimes in one block, which is why they are separate registers rather than derived from each other.

Cycle behaviour. Bytes append and advance the count. A packet becomes PK_READY when it fills or when something flushes it (§6). Handing it down returns the state to PK_EMPTY and clears the per-packet fields — but not msg_open_q, which must survive into the next packet if the message is still open.

Contract. The downstream FIFO receives whole packets. The upstream producer must not offer bytes when there is no space (§11).

Failure. Clearing msg_open_q on packet handoff is the single-flag bug: every packet then looks like a fresh message start, and a spanning message arrives at the receiver as several truncated ones.

DV. Cover all three states and the transitions between them, and specifically cover a packet handoff while a message remains open — the case that distinguishes per-packet from per-message state.

6. Flush, or the Message Never Arrives

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — only a full payload triggers transmission.
if (pack_bytes_q == LEN_W'(PAYLOAD_BYTES))
  send_packet = 1'b1;

Chapter 9.1 §6 named this deadlock; here is the mechanism in the assembler. A message ends. Its final bytes sit in a partially filled buffer. No further bytes arrive, because the protocol is waiting for a response to the message now stuck in that buffer. The link is idle, the buffer is non-empty, and nothing will ever fill it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — a packet is emitted for any of several reasons.
logic pack_full;
logic flush_now;
 
assign pack_full = (pack_bytes_q == LEN_W'(PAYLOAD_BYTES));
 
assign flush_now = pack_full            // no more room
                || end_of_message_seen  // the message is complete
                || flush_policy;        // implementation-defined (see below)

Architecture. Efficiency wants full payloads; correctness and latency want messages to leave. The three terms are three different reasons, and each is necessary.

State. None — combinational over the assembler's registers.

Cycle behaviour. Evaluated every cycle the assembler holds data.

Contract. The receiver must tolerate partially filled payloads, which is what §7's byte-valid exists for.

Failure. Omitting end_of_message_seen is the deadlock above. Omitting pack_full overflows the buffer. Omitting flush_policy leaves a latency-sensitive protocol waiting for traffic that may never come.

On flush_policy. This is implementation-defined — UCIe does not mandate a latency timer, and whether you want one depends entirely on the protocol you are carrying. The usual form is an age-based flush (Chapter 9.1 §6's saturating timer), sometimes with an urgency input from the protocol. A bulk stream may want none at all.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — a non-empty assembler cannot wait indefinitely.
property p_partial_packet_eventually_emitted;
  @(posedge clk) disable iff (!rst_n)
    (pack_bytes_q != '0) |-> ##[1:MAX_FLUSH_WAIT] pack_emitted;
endproperty

Bounded rather than ##[1:$] — an unbounded eventuality is vacuous in simulation, and this property's entire purpose is to fail when the flush is misconfigured.

7. Which Bytes Are Real

A partially filled payload contains bytes the transmitter never wrote. They are not zero, not random in any useful sense, and not distinguishable from data unless something says so.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — per-byte validity, generated as bytes are placed.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    pack_valid_q <= '0;
    pack_bytes_q <= '0;
  end else if (pack_emitted) begin
    pack_valid_q <= '0;                              // per-packet state clears
    pack_bytes_q <= '0;
  end else if (byte_accepted) begin
    pack_valid_q[pack_bytes_q] <= 1'b1;
    pack_bytes_q <= pack_bytes_q + 1'b1;
  end
end

Architecture. The receiver cannot infer which bytes are meaningful, so the transmitter must state it.

State. One bit per byte lane plus a count. Both, deliberately: the mask is what the datapath uses to enable writes, and the count is what the framing header carries. They are two views that must agree, which makes their disagreement assertable.

Cycle behaviour. A bit sets as each byte is placed; both clear on emission.

Contract. The receiver consumes exactly byte_count bytes and ignores the rest.

Failure. If the receiver treats unwritten bytes as data, a short final message is silently extended with garbage — and CRC passes, because those bytes really were transmitted.

DV. The redundancy is the check:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the mask and the count must describe the same extent.
property p_valid_mask_matches_count;
  @(posedge clk) disable iff (!rst_n)
    pack_emitted |-> ($countones(pack_valid_q) == pack_bytes_q);
endproperty
 
// Illustrative — no valid byte beyond the declared length.
property p_no_valid_byte_past_length;
  @(posedge clk) disable iff (!rst_n)
    pack_emitted |-> ((pack_valid_q >> pack_bytes_q) == '0);
endproperty

The second catches a stale mask bit left from a previous packet — which produces one extra garbage byte appended to a message, occasionally, depending on the previous packet's length. That is a genuinely miserable bug to find without the assertion.

8. A Message That Spans Packets

The worked example. Message of 600 bytes, payload capacity of 236 bytes — symbolic sizes, chosen so the arithmetic is visible.

PacketBytes carriedstartend_of_messagebyte_countReceiver state after
0bytes 0–23510236message open, 236 accumulated
1bytes 236–47100236message open, 472 accumulated
2bytes 472–59901128message closed, 600 delivered

Three things to read off it.

Packet 2 is partially filled, at 128 of 236 bytes — and there is nothing wrong with that. The byte-valid mask and count are what make it safe.

The middle packet has neither framing bit set. That is the state a single is_message flag cannot express, and it is why §4 uses two bits.

The receiver's accumulated length is state that must survive between packets — and must be checked against the message's declared length if the protocol carries one, because a lost middle packet would otherwise produce a short message that looks structurally valid.

A 600-byte message is packetised into three packets. The first carries a start marker and 236 bytes, the receiver backpressures, the packetizer holds the second packet until the receiver is ready again, then the second and third packets are sent, and the depacketizer delivers the reassembled 600-byte message to the sink.One message, three packets, one stall — illustrative sizesSourcePacketizerLinkDepacketizerSinkmessage 600 Bpkt 0 startpkt 0not readybackpressurereadypkt 1 middlepkt 1pkt 2 endpkt 2message 600 B
Figure 2 — the same 600-byte message crossing as three packets, with a stall in the middle. Two causal points are worth tracing. The depacketizer holds the message rather than delivering it after packet 0, because a message is only complete when its closing packet arrives — reassembly state must survive between packets. And when the receiver backpressures, the packetizer holds packet 1 unchanged rather than emitting it partially or dropping it; the stall changes timing and must not change framing. Sizes are illustrative.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — receiver-side continuation state, per stream (§10).
logic                      msg_open_q;
logic [MSG_LEN_W-1:0]      assembled_len_q;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    msg_open_q      <= 1'b0;
    assembled_len_q <= '0;
  end else if (packet_valid) begin
    if (rx_hdr.start) begin
      assembled_len_q <= MSG_LEN_W'(rx_hdr.byte_count);
      msg_open_q      <= !rx_hdr.end_of_message;    // single-packet message closes now
    end else begin
      assembled_len_q <= assembled_len_q + MSG_LEN_W'(rx_hdr.byte_count);
      if (rx_hdr.end_of_message) msg_open_q <= 1'b0;
    end
  end
end

Architecture. Reassembly is stateful because the transport is not.

State. An open flag and an accumulating length — per-message lifetime, cleared when the message completes.

Cycle behaviour. Updated once per received packet. Note the start branch assigns the length rather than adding to it, so a lost end_of_message followed by a new start resets cleanly rather than accumulating across two messages.

Contract. Upstream sees a message only when end_of_message closes it.

Failure. Accumulating on start instead of assigning concatenates two messages into one — the exact "receiver joins two messages" failure, with clean CRC throughout.

9. Malformed Framing Is Detectable

Some sequences of framing bits cannot legitimately occur, and checking them turns a silent corruption into a reported error.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — a continuation packet accepted with no message open.
if (packet_valid)
  assembled_len_q <= assembled_len_q + rx_hdr.byte_count;   // no framing check

If a start packet is lost, the next packet arrives with neither framing bit set and no message open. Without a check, its bytes are appended to whatever assembled_len_q happened to hold — attaching a fragment of one message to the tail of another.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — legal framing sequences only.
property p_continuation_requires_open_message;
  @(posedge clk) disable iff (!rst_n)
    (packet_valid && !rx_hdr.start) |-> msg_open_q;
endproperty
 
// Illustrative — a new message cannot start while one is open on that stream.
property p_no_nested_message_start;
  @(posedge clk) disable iff (!rst_n)
    (packet_valid && rx_hdr.start) |-> !msg_open_q;
endproperty

Read the second one carefully, because it is only correct under an assumption: that a single stream carries one message at a time. If your framing deliberately interleaves messages within a stream, this assertion is wrong for your design — and you would need per-message-ID state instead. Stating the assumption is the point; an assertion whose premise you have not checked is a false sense of security.

10. Multiple Streams Break the Single Flag

The stream_id field in §4 has not earned its place yet. Here is where it does.

If more than one stream shares the link — and Chapter 9.1 §5 placed the packing shim above an Adapter that multiplexes protocols — then a single msg_open_q is wrong. Stream A's message can be open while stream B's packets arrive interleaved, and one flag cannot represent both.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — continuation state indexed by stream.
localparam int NUM_STREAMS = 4;
 
logic                 msg_open_q      [NUM_STREAMS];
logic [MSG_LEN_W-1:0] assembled_len_q [NUM_STREAMS];
 
// Every framing decision is now qualified by stream.
assign continuation_legal = rx_hdr.start || msg_open_q[rx_hdr.stream_id];

Architecture. Reassembly state belongs to a message, and messages belong to streams. Sharing one instance across streams corrupts both.

State. Per-stream arrays — per-stream lifetime, distinct from the per-message state they contain.

Cycle behaviour. Indexed by the arriving packet's stream_id.

Contract. Both ends must agree on stream identity and count.

Failure. With a single flag and two active streams, B's start clears A's open state, A's continuation is then rejected or misattributed, and the corruption depends on interleaving — so it appears under load and vanishes in a directed test.

Whether UCIe's streaming mode itself defines stream identity, and what ordering it guarantees between streams, is Chapter 9.3's subject — and, as that chapter says up front, a question to take to the specification rather than to intuition.

11. Boundaries Interact With Backpressure

The bug class that framing and flow control produce jointly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — bytes accepted without checking the assembler has room.
assign msg_byte_ready = 1'b1;

A message is half-packed. The packet FIFO is full, so the assembler cannot emit. More bytes arrive and are accepted, and they overwrite the buffer's contents — silently, because nothing in the datapath is checking.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — accept a byte only when it can be stored.
assign pack_space = (pack_bytes_q < LEN_W'(PAYLOAD_BYTES));
assign msg_byte_ready = pack_space || (pack_emitted && fifo_has_space);

Architecture. Message-boundary state and buffer-resource state are different things that must be satisfied together.

State. The assembler's count plus the FIFO's occupancy.

Cycle behaviour. The second term matters: on the cycle a packet is emitted into a FIFO with space, the assembler frees up and can accept in the same cycle.

Contract. The producer must respect msg_byte_ready.

Failure. Silent overwrite mid-message, producing a message whose middle is wrong and whose framing is perfect.

Framing state and resource state must be satisfied together. A design that checks one and assumes the other corrupts messages exactly when the link is busy — which is when nobody is looking at framing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — nothing is accepted without somewhere to put it.
property p_no_accept_without_space;
  @(posedge clk) disable iff (!rst_n)
    byte_accepted |-> pack_space;
endproperty

12. Header and Payload Must Not Be Pipelined Apart

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — two paths, two depths.
always_ff @(posedge clk) hdr_q     <= hdr_in;                 // 1 stage
always_ff @(posedge clk) pay_q     <= pay_in;
always_ff @(posedge clk) pay_q2    <= pay_q;                  // 2 stages
// Output pairs hdr_q with pay_q2 — header N with payload N-1.

The header describing packet N is emitted alongside the payload of packet N−1. Every packet is then framed by its predecessor's metadata: lengths are wrong, end_of_message lands on the wrong packet, and the receiver joins and splits messages in all the wrong places.

Why this survives review. Each path is individually correct and obviously so. The bug is in the relationship, and reading either always_ff in isolation shows nothing wrong. It also survives simulation whenever consecutive packets happen to have identical headers — which back-to-back full packets in the middle of a long message do.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — one object, one pipeline, cannot misalign by construction.
stream_packet_t pkt_q, pkt_q2;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    pkt_q  <= '0;
    pkt_q2 <= '0;
  end else if (pipe_en) begin
    pkt_q  <= pkt_in;
    pkt_q2 <= pkt_q;
  end
end

Architecture. Fields consumed together should travel together. Bundling makes misalignment structurally impossible rather than a thing to remember.

State. Two pipeline registers holding whole packets.

Cycle behaviour. Both advance under one enable — so a stall stalls everything, and there is no way to advance one field and not another.

Contract. Downstream receives a self-consistent object.

Failure. The separated version above; note it is Chapter 5.5's bundled-struct lesson, arriving at the packet boundary.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the whole object holds still while the consumer is not ready.
property p_packet_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
    (pkt_valid && !pkt_ready) |=> ($stable(pkt_q) && $stable(pkt_valid));
endproperty

One property over the bundled object covers header, payload, and mask together — which is a small argument for bundling all by itself.

13. The Packet Queue

A short queue between assembly and transport, and it is worth being specific about why.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — a small packet FIFO. The mechanics are the standard ones
// (see the CDC and FIFO material in the RTL Design Patterns track); what is
// specific here is that the element is a whole packet object.
localparam int PKT_FIFO_DEPTH = 4;
 
stream_packet_t                        pkt_fifo_q [PKT_FIFO_DEPTH];
logic [$clog2(PKT_FIFO_DEPTH+1)-1:0]   pkt_count_q;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)                        pkt_count_q <= '0;
  else if (pkt_push && !pkt_pop)     pkt_count_q <= pkt_count_q + 1'b1;
  else if (pkt_pop  && !pkt_push)    pkt_count_q <= pkt_count_q - 1'b1;
end

Architecture. Assembly and transmission proceed at different rates. Without a queue, a momentarily busy transport stalls the assembler, which stalls the producer, mid-message — and §11 showed that mid-message stalls are where framing state and resource state collide.

State. A few packet-sized entries and an occupancy counter. Depth is a latency-versus-area decision, not a correctness one: one entry works, and more absorbs more jitter.

Cycle behaviour. Simultaneous push and pop leave the count unchanged — the standard trap, written explicitly.

Contract. Packets leave in the order they entered. That is trivially true of a FIFO and becomes the whole subject of Chapter 9.3 once more than one stream is involved.

Failure. Sizing the queue in bytes rather than packets is the subtle one. A byte-sized queue can hold a partial packet, which reintroduces exactly the boundary-splitting the packet object exists to prevent.

14. Verifying Framing: Check Messages, Not Bytes

The methodological point of the chapter.

A transport-level scoreboard compares transmitted and received bytes. It will pass a design whose framing is completely wrong, because the bytes are correct — that is precisely the failure signature. What catches framing bugs is a semantic scoreboard that models messages.

What it stores:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
expected_messages[stream_id] = FIFO of { length, payload bytes }

How it is driven. On the transmit side, every message the producer offers is pushed — before packetisation, at the semantic boundary. On the receive side, every message the depacketizer completes is popped and compared. Nothing in between is modelled, deliberately: the scoreboard's job is to prove the transformation is an identity, and modelling the intermediate representation would make it agree with the DUT's bugs.

What it detects that byte comparison cannot:

BugByte comparisonSemantic scoreboard
Two messages joinedpasseslength mismatch
One message splitpassesextra message
Extra byte from a stale maskmay passlength mismatch
Fragment attached to the wrong messagepassescontent mismatch
Message attributed to the wrong streampasseswrong queue

The test list that matters, and why each is not covered by the others:

  • Single-packet message — the common case, and the only one many suites test.
  • Message exactly filling a payloadpack_full and end_of_message_seen assert together; a flush written as an if/else chain can take the wrong branch.
  • Message one byte over a payload — produces a continuation packet carrying one byte, which is the minimum-size continuation and where off-by-one lives.
  • Message spanning three or more packets — one middle packet may work by accident; two exercises the accumulate path properly.
  • Several small messages in one payload — multi-message framing.
  • A single message, then idle — §6's deadlock, invisible under continuous traffic.
  • Stall exactly at a message boundary, and stall mid-message — different framing state at the stall point.
  • Interleaved streams, if supported — §10.
  • Injected malformed framing — continuation with no open message; a start while open. These must be rejected and reported, and rejection paths are code that only runs when something is wrong.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative packet-transport coverage — not UCIe-defined.
covergroup cg_packet @(posedge clk iff pkt_emitted);
 
  cp_fill : coverpoint pack_bytes_q {
    bins one_byte = {1};
    bins partial  = {[2 : PAYLOAD_BYTES-1]};
    bins exact    = {PAYLOAD_BYTES};
  }
  cp_framing : coverpoint {tx_hdr.start, tx_hdr.end_of_message} {
    bins single       = {2'b11};   // whole message in one packet
    bins first        = {2'b10};   // begins a spanning message
    bins middle       = {2'b00};   // continuation
    bins last         = {2'b01};   // completes a spanning message
  }
  cp_stalled : coverpoint pkt_stalled_at_emit;
 
  // Was every framing role exercised at every fill level?
  x_framing_by_fill  : cross cp_framing, cp_fill;
  // Was a spanning message ever interrupted by a stall?
  x_framing_by_stall : cross cp_framing, cp_stalled;
 
endgroup

Why cp_framing bins the two bits together. The four combinations are the four framing roles, and middle — neither bit set — is the one a regression can easily never produce, because it requires a message longer than two payloads. If that bin is empty, spanning reassembly has never been tested.

15. Debug: Clean CRC, Wrong Message

The signature this chapter exists for. Work it in order — the first three are free.

  1. Was the source message length what you think? Confirm at the producer, before packetisation.
  2. Do the message counts match? More received than sent means a message was split; fewer means two were joined. That single comparison localises the bug class immediately.
  3. Where did the first divergence occur? The first mismatched message, not the tenth — everything after it is downstream damage.
  4. Were start and end_of_message set on the packets you expect? Dump the framing bits per packet.
  5. Was byte_count correct on the final packet? A stale mask bit adds exactly one byte.
  6. Did the mask and the count agree? §7's assertion, checked manually if it was not written.
  7. Did header and payload stay aligned? §12 — check whether the failure appears only when consecutive packets have different headers.
  8. Did a stall occur mid-message? Correlate the divergence with backpressure.
  9. Did the assembler accept bytes with no space? §11 — silent overwrite.
  10. Is continuation state per stream? §10 — if the failure only appears with two streams active, this is it.
  11. Did a link event occur mid-message? A partially transmitted message and a recovery is Chapter 8.6 §14's ownership question at the shim.

Step 2 is the highest-yield check in the chapter. Message count mismatch tells you whether you are looking at a join or a split before you have read a single line of RTL.

16. Common Misconceptions

"A stream is an unframed byte pipe." Then the receiver could not tell where one message ends and the next begins. Framing inside the payload is what makes it a message stream (§1).

"CRC guarantees framing." CRC proves the bytes arrived as sent. Framing is the receiver's interpretation of them, and it can be wrong on perfectly delivered bytes (§14, §15).

"Unused bytes in a partial payload can be left undefined." They were transmitted and they are indistinguishable from data. The byte-valid mask and count are what make a partial payload safe (§7).

"A packetizer just needs a data buffer." It needs per-packet state and per-message state, with different lifetimes — and clearing the latter on packet handoff breaks every spanning message (§5).

"A stall only affects throughput." A stall mid-message is where framing state and resource state interact, and where the silent-overwrite bug lives (§11).

"Header and payload can be pipelined independently." Each path is individually correct; the relationship is the bug, and it hides whenever consecutive headers happen to match (§12).

"One message always fits in one transport unit." Three cases exist, and the spanning one needs continuation state on both sides (§3, §8).

"Message completion and packet completion are the same event." A packet can complete mid-message and a message can complete mid-packet. Conflating them causes both the deadlock and the misframing (§3, §6).

"A clean transport means depacketization is correct." It means the transport did its job. The shim's job is separate and separately verifiable (§14).

17. Understanding Check

18. Summary and What Comes Next

Packet transport is a boundary-preservation problem. The Adapter carries payload bytes and protects them; it does not know what a message is, so recording and reconstructing boundaries is the shim's work.

Message and transport unit are different objects with a many-to-many relationship, so message completion and packet completion are different events — and conflating them produces both the flush deadlock and the framing corruption.

The mechanisms: two framing bits, not one, because a middle packet has neither set. A byte-valid mask and a count, both, because their redundancy is what makes disagreement assertable and a stale mask bit findable. Per-packet and per-message state with different lifetimes, so a packet handoff does not close an open message. Continuation state per stream, because one flag cannot serve two active streams. One bundled packet object through the pipeline, because separate paths misalign in a way that survives review. And readiness that respects both framing and resource state, because their interaction is where the silent overwrite lives.

The verification lesson is the one to carry: check messages, not bytes. A byte comparison passes on every framing bug in this chapter, because the bytes are correct — the interpretation is not. And in debug, compare message counts first: more received than sent is a split, fewer is a join, and that single number localises the bug class before you read any RTL.

Packetisation preserves boundaries. It does not yet say anything about whether packets — of one stream or several — may pass one another on their way across the link:

  • 9.3 — Streaming Ordering — what an ordering domain is, what per-stream ordering costs in hardware, head-of-line blocking, and why an ordering scoreboard modelled on the wrong domain reports failures that are not real.

Browse the full path on the UCIe tutorials index.