Skip to content

UCIe · Module 19

UCIe Buffering

Why a UCIe link has seven kinds of buffer with seven different lifetimes, and how to size and control each — a derived occupancy that cannot drift, pointer wrap that must not assume a power of two, payload and metadata that must share one accept event, headroom computed from the backpressure round trip, hysteresis that stops ready from chattering, ping-pong banks whose ownership the consumer releases, and a replay depth that is a reliability window.

Chapter 19.3 left the Adapter holding staging entries, replay history, reassembly state and protocol-facing queues. This chapter stops calling all of those "a FIFO" and asks what each one actually is.

1. The One-Sentence Model

A buffer stores obligations, not bytes. Every valid entry means somebody upstream transferred ownership of work that must eventually leave correctly — so overflow is an accepted obligation lost, an early pop is an obligation forgotten, a duplicate pop is a semantic duplication, and a wrong watermark is an admission promise the design cannot keep.

2. What This Chapter Owns

QuestionWhere it is answered
Receive-buffer sizing, reservation accounting, watermark policy, burst absorption13.2 — Buffer Management
Backpressure waves, deriving a watermark from the round trip, deadlock13.3 — Backpressure
Credit-based flow control at the protocol level13.1
What each Adapter buffer is for — staging, replay, reassembly, window19.3 §6
The link's block partition and state-ownership table19.1 §7
The credit machine in RTL19.5 — Flow-Control Logic

13.2 and 13.3 own the policy: how much to reserve, when to throttle, and why backpressure propagates. This chapter owns the structures — and the distinction is sharp:

Seven kinds of buffer with seven lifetimes (§4, §6), which is the fact that makes "use a FIFO" the wrong answer four times out of seven.

The RTL that gets them wrong. A drifting occupancy (§9), a wrap that assumes a power of two (§13), payload and metadata with separate pointers (§16), a skid that drops the item it exists to catch (§19), and a ping-pong that swaps on the producer's word rather than the consumer's (§36).

Sizing rules that differ per kind (§21–§25, §39–§43): latency for a staging FIFO, a reliability window for replay, and a completion guarantee for reassembly.

And what each buffer does at a recovery (§51), which is a different answer for each of the seven.

3. Sourcing

4. Seven Kinds of Buffer

KindPurposeTypical depthLifetime of an entry
Skid / elasticbreak a combinational ready path1–2one stall
Ingress FIFOabsorb a producer burstlatency-driven (§21)until consumed
Stagingabsorb pipeline backpressurepipeline depthuntil replay owns it (19.3 §13)
Replay / historyretain a recoverable copya reliability window (§39)until resolution
Reassemblycollect an object's fragmentsconcurrent-objects-driven (§41)one object
Output FIFOabsorb a consumer stalllatency-drivenuntil consumed
Ping-pongoverlap producer and consumer phases2 banksone bank's worth of work
CDC FIFOcross clock domainssynchroniser-latency-drivenuntil read

Four of the eight are not sized by throughput at all. A skid is sized by the ready path; replay by the resolution latency; reassembly by concurrent incomplete objects; ping-pong by the phase structure. Calling them all "a FIFO" and sizing them all by burst absorption gets four of them wrong.

5. Where They Sit

A UCIe link's buffering drawn as nine structures. A protocol engine feeds a one entry skid buffer that breaks the combinational ready path, which feeds a per class ingress FIFO. The ingress FIFO feeds an Adapter staging buffer, which hands objects to a replay and history buffer that retains a recoverable copy until resolution. From there objects reach the physical layer. On the receive side, beats enter a reassembly buffer indexed by fragment position rather than arrival order, then an output FIFO that absorbs consumer stalls, and finally the protocol engine. A clock domain crossing FIFO is drawn as a separate branch between the Adapter and the physical layer, and a ping pong pair of banks is drawn as an alternative structure where producer and consumer work in phases. The point of the drawing is that staging and replay overlap deliberately, and that reassembly and ping pong are not ordered queues at all.Protocol engineproducer and consumerSkid (1 entry)breaks the ready pathIngress FIFOper classStagingpipeline depthReplay / historya reliability window(§39)CDC FIFOGray pointers (§48)Reassemblyindexed, not orderedOutput FIFOabsorbs consumerstallsPing-pong banksowned, not pointed at(§34)12
Eight buffers on one link, with the two that are not queues drawn distinctly. Staging and replay overlap deliberately during the ownership handoff; reassembly is indexed by fragment rather than ordered; and the ping-pong banks are owned rather than pointed at.

Read the two structures that are not in the main chain. Reassembly is indexed rather than ordered, and ping-pong is owned rather than pointed at — so neither has a read pointer, and neither can be sized or verified like a queue.

6. The Buffer Ownership Table

The chapter's backbone.

BufferProducerConsumerAccept eventRelease eventOn a recovery (§51)
Skidupstream stagedownstream stageupstream valid with skid emptydownstream takes ithold
Ingress FIFOprotocol enginethe Adapterpush on handshakepop on handshakehold
StagingAdapter admissionthe replay blockadmissionreplay confirms ownershiphold
Replaystaging handoffresolutionhistory commitresolution (19.3 §44)preserve and re-baseline
Reassemblyreceive paththe delivery gatefirst fragmentobject complete, or abandonedarchitecture-defined
Output FIFOdelivery gateprotocol enginesemantic deliveryconsumer takes ithold
Ping-pong bankthe producer enginethe consumer engineproducer claims a FREE bankthe consumer releases ithold
CDC FIFOwrite domainread domainwrite-side pushread-side popdrain or hold, by domain

Three readings.

Column 5 has seven different answers. Staging releases at replay's confirmation, replay at resolution, reassembly at completion, ping-pong at the consumer's release. A design that frees them all on "the next stage took it" gets four of them wrong.

And column 6 has at least three. §52 is the design that gives them all one answer.

Note the ping-pong row's release event especially. The producer does not decide when its bank is free; the consumer does — and §36 is the design that gets that backwards.

7. A Parameterised FIFO

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Production-style: one owner per counter, handshake-qualified
// events, payload RAM not reset, wrap-safe pointers.
module ucie_fifo #(
  parameter int DEPTH = 16,
  parameter int WIDTH = 64
) (
  input  logic             clk,
  input  logic             rst_n,
  input  logic             push,
  input  logic [WIDTH-1:0] wdata,
  output logic             full,
  input  logic             pop,
  output logic [WIDTH-1:0] rdata,
  output logic             empty,
  output logic [$clog2(DEPTH+1)-1:0] occupancy
);
  localparam int PTR_W = $clog2(DEPTH);
  localparam int OCC_W = $clog2(DEPTH+1);       // DEPTH+1 values: 0..DEPTH
 
  // Elaboration checks (Section 14).
  initial begin
    assert (DEPTH >= 2) else $fatal(1, "DEPTH must be at least 2");
    assert (WIDTH >= 1) else $fatal(1, "WIDTH must be positive");
  end
 
  // Payload storage — inferred RAM, deliberately NOT reset.
  logic [WIDTH-1:0] mem [DEPTH];
 
  // Pointers carry an extra MSB so full and empty are distinguishable (Section 11).
  logic [PTR_W:0] wr_ptr_q, rd_ptr_q;
 
  wire [PTR_W-1:0] wr_idx = wr_ptr_q[PTR_W-1:0];
  wire [PTR_W-1:0] rd_idx = rd_ptr_q[PTR_W-1:0];
 
  wire push_fire = push && !full;
  wire pop_fire  = pop  && !empty;
 
  // Occupancy is DERIVED, not maintained — so it cannot drift (Section 8).
  assign occupancy = OCC_W'(wr_ptr_q - rd_ptr_q);
  assign full      = (occupancy == OCC_W'(DEPTH));
  assign empty     = (occupancy == '0);
  assign rdata     = mem[rd_idx];
 
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      wr_ptr_q <= '0;
      rd_ptr_q <= '0;
    end else begin
      if (push_fire) wr_ptr_q <= next_ptr(wr_ptr_q);   // Section 13
      if (pop_fire)  rd_ptr_q <= next_ptr(rd_ptr_q);
    end
 
  always_ff @(posedge clk)
    if (push_fire) mem[wr_idx] <= wdata;
 
endmodule

Architecture. A queue whose occupancy is derived from the pointers rather than maintained separately — which is the design decision that makes §9's drift impossible rather than merely detectable.

State. DEPTH × WIDTH payload bits plus two pointers of PTR_W+1 bits. The payload is inferred RAM and deliberately not reset: rd_ptr_q equals wr_ptr_q after reset, so empty is true, so no pop can occur, so the contents are unobservable before they are written (17.4 §14). A reset loop over the payload prevents RAM inference and turns a few block RAMs into thousands of flops.

Cycle behaviour. push_fire and pop_fire are handshake-qualified. On a simultaneous cycle both pointers advance and the difference is unchanged automatically (§8).

Contract. Upstream relies on full; downstream on empty and on rdata being stable while it is not popped. Both are exact rather than conservative, because this is a single-clock FIFO — the CDC version is not (§48).

Failure. §9 if a separate occupancy register is added; §12 if the extra pointer bit is dropped; §13 if next_ptr assumes a power of two.

DV. §10's properties; cover empty, mid, full and the simultaneous cycle.

8. Simultaneous Push and Pop

The case every buffer in a link hits constantly at load, and the reason the derived occupancy is preferable.

ApproachSimultaneous cycleRisk
derivedwr_ptr − rd_ptrboth pointers advance; the difference is unchangednone — there is nothing to drift
maintained counter with unique casefour arms, 2'b11 holdscorrect, and one more thing to review
maintained counter with two ifsthe second assignment wins§9 — silent monotonic drift
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// If a design does maintain a counter — for instance because occupancy must be
// registered for timing — ONE owner and all four arms, explicitly.
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) occ_q <= '0;
  else
    unique case ({push_fire, pop_fire})
      2'b10:   occ_q <= occ_q + 1'b1;
      2'b01:   occ_q <= occ_q - 1'b1;
      default: ;                                 // 2'b00 and 2'b11 both hold
    endcase

Prefer the derived form. It is not merely safer — it removes a class of bug rather than detecting it, which is the strongest kind of fix available.

9. Wrong FIFO — Independent Assignments to the Count

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — two independent statements for one register.
always_ff @(posedge clk) begin
  if (push_fire) occ_q <= occ_q + 1'b1;
  if (pop_fire)  occ_q <= occ_q - 1'b1;        // ← the second wins on a tie
end
CyclePushPopShould beActually
nhold at 109
n+1hold at 108
n+2119

Four properties.

The drift is monotonic downward, so the buffer eventually reports space it does not have. Then a push overwrites a live entry — an obligation lost (§1).

It is invisible at low load. A simultaneous push and pop requires the producer and consumer both active in one cycle, which happens only when the buffer is being kept partly full — the design's intended operating point.

Pointers and occupancy diverge, because the pointers are independently maintained and correct. So a comparison between them catches it and a check on either alone does not (§10).

And the fix costs nothing. One unique case is the same logic with the tie resolved deliberately, and the derived form (§7) removes the register entirely.

10. SVA — Occupancy and Safety

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Occupancy matches the pointers — this is what catches Section 9.
property p_occupancy_matches_pointers;
  @(posedge clk) disable iff (!rst_n)
    (occupancy == OCC_W'(wr_ptr_q - rd_ptr_q));
endproperty
a_occupancy_matches_pointers: assert property (p_occupancy_matches_pointers);
 
// Conservation against a testbench count.
property p_conservation;
  @(posedge clk) disable iff (!rst_n)
    (occupancy == (tb_pushes - tb_pops));
endproperty
a_conservation: assert property (p_conservation);
 
property p_no_overflow;
  @(posedge clk) disable iff (!rst_n) (push && full) |-> !push_fire;
endproperty
a_no_overflow: assert property (p_no_overflow);
 
property p_no_underflow;
  @(posedge clk) disable iff (!rst_n) (pop && empty) |-> !pop_fire;
endproperty
a_no_underflow: assert property (p_no_underflow);
 
// The head is stable while it is not taken.
property p_head_stable;
  @(posedge clk) disable iff (!rst_n)
    (!empty && !pop_fire) |=> $stable(rdata);
endproperty
a_head_stable: assert property (p_head_stable);
 
// A simultaneous push and pop leaves occupancy unchanged.
property p_simultaneous_holds;
  @(posedge clk) disable iff (!rst_n)
    (push_fire && pop_fire) |=> (occupancy == $past(occupancy));
endproperty
a_simultaneous_holds: assert property (p_simultaneous_holds);

Architecture. Six properties, and the first two are the ones that matter.

Why pointer-matching catches §9 and bounds do not. The drift stays inside [0, DEPTH] for a long time before breaching anything. The pointer comparison diverges on the very first simultaneous cycle, which is the difference between finding it in the first minute of a soak and finding it in the field.

And conservation catches what pointer-matching cannot — a pointer that advanced without a corresponding memory write, which keeps the two design registers consistent with each other and wrong together.

DV. All six always-on. The sixth requires the simultaneous cycle to occur, which is a coverage bin (§55).

11. Full and Empty Are Not Distinguishable From Indices Alone

When wr_idx == rd_idx, the buffer is either full or empty and the indices cannot say which. Three standard resolutions:

ResolutionCostUsable depth
an extra pointer bit (§7)one flip-flop per pointerall DEPTH
a maintained occupancy countera counter, and §9's riskall DEPTH
leave one slot always emptyone entry of storageDEPTH − 1

The extra bit is usually right, because one flip-flop is cheaper than one entry of a wide payload — and unlike the counter it cannot drift.

12. Wrong Pointer Logic

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — indices only, and full inferred from equality.
logic [PTR_W-1:0] wr_ptr_q, rd_ptr_q;          // no extra bit
 
assign empty = (wr_ptr_q == rd_ptr_q);
assign full  = (wr_ptr_q + 1'b1 == rd_ptr_q);  // "one slot spare" — but see below

This particular form is almost the one-slot-spare policy and it has two problems.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. It silently reduces usable depth to DEPTH-1, which the sizing arithmetic
   in Sections 21-22 did NOT account for.
2. If DEPTH is not a power of two, `wr_ptr_q + 1` does not wrap at DEPTH —
   it wraps at 2**PTR_W, which is larger. The comparison then never
   matches at the intended point (Section 13).

And the more damaging variant is worse:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WORSE — full and empty conflated.
assign full  = (wr_ptr_q == rd_ptr_q);         // ← also true when EMPTY

Three properties.

The first form under-delivers silently. A buffer sized at 16 from §22's arithmetic provides 15, so the headroom calculation in §27 is short by one and the design overflows at exactly the burst it was sized for.

The second form is a data-loss bug. Full read as empty permits a push into a full buffer, overwriting a live entry — an obligation lost.

And both pass every lightly-loaded test, because neither condition is reached until the buffer actually fills.

13. Non-Power-of-Two Depth

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Explicit wrap, so DEPTH need not be a power of two.
function automatic logic [PTR_W:0] next_ptr(input logic [PTR_W:0] p);
  // The MSB is the wrap bit; the low bits are the index.
  if (p[PTR_W-1:0] == PTR_W'(DEPTH-1))
    next_ptr = {~p[PTR_W], PTR_W'(0)};          // wrap index, toggle wrap bit
  else
    next_ptr = {p[PTR_W], p[PTR_W-1:0] + 1'b1};
endfunction

Architecture. An explicit comparison against DEPTH-1 rather than relying on binary overflow. Binary overflow wraps at 2**PTR_W, which equals DEPTH only when DEPTH is a power of two.

Why non-power-of-two depths are common in a link. A replay depth derived from a round trip (§39), a reassembly count derived from concurrent objects (§41), and a per-class partition derived from a share of a pool (§46) all produce arbitrary numbers, and rounding each up to a power of two can waste a substantial fraction of a wide payload RAM.

Failure. Using wr_ptr_q + 1'b1 with a natural wrap and a DEPTH of, say, 12: the pointer runs 0..15, indexes 12..15 address memory that does not exist, and the occupancy subtraction is wrong for four values out of sixteen.

And the alternative is legitimate. A design may mandate power-of-two depths — provided it says so and enforces it (§14), rather than assuming it.

14. Elaboration Checks

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. A bad parameter should fail the build, not produce a silently
// wrong design (18.4's argument, at every buffer).
initial begin
  assert (DEPTH >= 2)
    else $fatal(1, "DEPTH must be at least 2 (a 1-deep queue is a skid — Section 18)");
 
  assert (WIDTH >= 1)
    else $fatal(1, "WIDTH must be positive");
 
  // If THIS instance uses the natural-overflow wrap, say so and enforce it.
  if (USE_POW2_WRAP)
    assert ((DEPTH & (DEPTH-1)) == 0)
      else $fatal(1, "DEPTH must be a power of two for the overflow wrap (Section 13)");
 
  // The watermark must leave room for the in-flight items (Section 29).
  assert (HIGH_WM <= DEPTH - MAX_INFLIGHT_AFTER_STOP)
    else $fatal(1, "HIGH_WM leaves insufficient headroom (Section 29)");
 
  // Hysteresis must actually have a gap (Section 31).
  assert (LOW_WM < HIGH_WM)
    else $fatal(1, "LOW_WM must be below HIGH_WM (Section 32)");
end

Architecture. Five checks that turn silent misconfiguration into a build failure.

The fourth is the valuable one. HIGH_WM set too close to DEPTH produces §28's overflow, and it is a parameter someone chose in a spreadsheet. Checking it at elaboration costs nothing and catches the mistake before any simulation runs.

And the fifth catches §32's oscillation — thresholds set equal, which is the natural thing to write when hysteresis was not considered.

DV. These are elaboration checks, not runtime assertions. They cost nothing at simulation time and nothing in silicon.

15. Payload and Metadata Share One Accept Event

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Data and its metadata are ONE entry, written on ONE event.
typedef struct packed {
  logic [OBJ_ID_W-1:0]    object_id;
  logic [CLASS_W-1:0]     tclass;
  logic [LEN_W-1:0]       length;
  logic [CFG_EPOCH_W-1:0] cfg_epoch;
  logic                   last;
  logic [DATA_W-1:0]      data;
} buf_entry_t;
 
buf_entry_t mem [DEPTH];
 
always_ff @(posedge clk)
  if (push_fire) mem[wr_idx] <= push_entry;      // ONE write, ONE pointer

Architecture. One RAM, one entry type, one pointer. The metadata is inside the entry, not beside it, so there is no second pointer that can disagree.

Why a design splits them anyway. Payload is wide and metadata is narrow, so separate RAMs of different widths can be more area-efficient — and that is a legitimate reason. What is not legitimate is separate pointers (§16).

The correct split, if the RAMs must be separate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Two RAMs, ONE pointer, ONE accept event.
logic [DATA_W-1:0] data_mem [DEPTH];
logic [META_W-1:0] meta_mem [DEPTH];
 
always_ff @(posedge clk)
  if (push_fire) begin
    data_mem[wr_idx] <= push_data;               // same index
    meta_mem[wr_idx] <= push_meta;               // same index, same event
  end

Failure. §16.

DV. §17's alignment property.

16. Wrong RTL — Split RAMs With Split Pointers

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — two RAMs with two pointers advanced on two conditions.
always_ff @(posedge clk) begin
  if (data_push) begin data_mem[data_wr_q] <= push_data; data_wr_q <= data_wr_q + 1'b1; end
  if (meta_push) begin meta_mem[meta_wr_q] <= push_meta; meta_wr_q <= meta_wr_q + 1'b1; end
end

If data_push and meta_push ever differ — for one cycle, once — every subsequent entry is mismatched.

EntryPayloadMetadata attachedCorrect?
0D0M0
1D1M1
(one cycle where meta_push is low)
2D2M3
3D3M4✗ — and forever after

Four properties.

The payload is perfect. Every byte is exactly what was written. It is paired with another entry's identity, class, length and epoch — so the object is routed, sized, retried and interpreted as something else entirely.

It is systematic and permanent. One divergence offsets every subsequent entry, so the failure rate goes from 0% to 100% at one instant and stays there until reset.

And the divergence needs only one asymmetric condition. A guard on meta_push that a refactor added, a stall that gates one and not the other, an error path that skips a metadata write — any of them, once.

The fix is structural: one accept event drives both writes at the same index (§15). The two RAMs may differ in width; they may not differ in when they are written.

17. SVA — Payload and Metadata Stay Aligned

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. One write event, one index, for both RAMs.
property p_single_accept_event;
  @(posedge clk) disable iff (!rst_n)
    (data_write_en == meta_write_en);
endproperty
a_single_accept_event: assert property (p_single_accept_event);
 
property p_same_write_index;
  @(posedge clk) disable iff (!rst_n)
    data_write_en |-> (data_wr_idx == meta_wr_idx);
endproperty
a_same_write_index: assert property (p_same_write_index);
 
// End-to-end: the metadata popped with an entry is the metadata pushed with it.
property p_entry_association(int unsigned rid);
  @(posedge clk) disable iff (!rst_n)
    (pop_fire && (tb_ref_id == rid))
      |-> (rd_meta == tb_meta_of(rid));
endproperty
a_entry_association: assert property (p_entry_association(REF_UT));

Architecture. Three properties: one event, one index, and the end-to-end association.

Why the third is needed given the first two. They check the write side; the third checks that what comes out was written together — which also catches a read side that indexes the two RAMs differently.

DV. The third needs a testbench that tracks which metadata accompanied which payload. §16's failure is caught on the very first mismatched pop.

18. The Skid Buffer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE one-entry skid. Its purpose is NOT storage — it is to make
// up_ready independent of dn_ready.
typedef struct packed {
  logic             valid;
  buf_entry_t       entry;
} skid_t;
 
skid_t skid_q;
 
// The whole point: this depends on the skid's own state and nothing downstream.
assign up_ready = !skid_q.valid;
 
assign dn_valid = skid_q.valid || up_valid;
assign dn_entry = skid_q.valid ? skid_q.entry : up_entry;
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    skid_q.valid <= 1'b0;
  end else begin
    unique case ({skid_q.valid, dn_ready})
      2'b11: skid_q.valid <= 1'b0;                    // held, taken: empties
      2'b10: ;                                        // held, stalled: hold
      2'b00: if (up_valid) begin                      // empty, stalled: CAPTURE
               skid_q.valid <= 1'b1;
               skid_q.entry <= up_entry;
             end
      2'b01: ;                                        // empty, ready: pass through
      default: ;
    endcase
  end

Architecture. One entry, and one entry is exactly sufficient: the upstream saw up_ready last cycle and can therefore have sent exactly one item when readiness falls. A two-entry skid stores something that cannot exist.

State. One valid bit and one entry.

Cycle behaviour. Four cases, all enumerated. The 2'b00 arm is the buffer's entire reason for existing — downstream stalled, skid empty, and an item is being offered that must not be lost.

Contract. Upstream relies on up_ready having no downstream term; downstream on dn_entry being stable while it stalls (§20).

Failure. §19. Also making up_ready = !skid_q.valid && dn_ready, which reintroduces the very path the skid exists to break (19.2 §38).

DV. §20; cover all four cases, especially captured.

19. Wrong Skid — the Capture Case Omitted

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the skid captures only when it is already offering downstream.
always_ff @(posedge clk)
  if (dn_valid && !dn_ready) begin
    skid_q.valid <= 1'b1;
    skid_q.entry <= dn_entry;                 // ← may be the SKID's own entry
  end else if (dn_ready) begin
    skid_q.valid <= 1'b0;
  end
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. The skid is empty. up_ready is high, so upstream sends an item.
2. In the SAME cycle, dn_ready falls.
3. dn_valid is true (from the bypass path) and dn_ready is false, so the
   code captures dn_entry — which IS the upstream item, so this case works.
4. But now consider: the skid is empty, dn_ready is ALREADY low, and
   upstream sends. dn_valid is true via bypass, dn_ready is low, so it
   captures — and then upstream sends AGAIN next cycle because up_ready
   was computed from a stale or wrong condition.
5. -> the second item overwrites the first. An obligation is lost.

Three properties, and the second is why this is worth its own section.

The bug is in the interaction between the capture condition and up_ready, not in either alone. A skid whose up_ready is correct and whose capture is correct can still lose an item if the two disagree about when the skid is full.

It loses exactly one item, once, at a stall boundary. Downstream sees a gap; nothing reports an error; and the protocol engine above is left with a semantic operation whose object never arrived.

And the conservation property is the only detector (§20). Stability and overflow checks both pass — the item was never written, so nothing was overwritten and nothing was unstable.

20. SVA — the Skid Conserves

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Nothing accepted upstream is lost — the only detector for Section 19.
property p_skid_conservation;
  @(posedge clk) disable iff (!rst_n)
    (tb_items_in == tb_items_out + (skid_q.valid ? 1 : 0));
endproperty
a_skid_conservation: assert property (p_skid_conservation);
 
// Upstream readiness has no downstream term.
property p_up_ready_independent;
  @(posedge clk) disable iff (!rst_n)
    !skid_q.valid |-> up_ready;
endproperty
a_up_ready_independent: assert property (p_up_ready_independent);
 
// The offered entry is stable while downstream stalls.
property p_dn_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
    (dn_valid && !dn_ready) |=> (dn_valid && $stable(dn_entry));
endproperty
a_dn_stable_under_stall: assert property (p_dn_stable_under_stall);
 
// The skid never accepts while it already holds an item.
property p_skid_no_overwrite;
  @(posedge clk) disable iff (!rst_n)
    (up_valid && up_ready) |-> !skid_q.valid;
endproperty
a_skid_no_overwrite: assert property (p_skid_no_overwrite);

Architecture. Four properties, and conservation is the one that earns its cost.

Why the fourth is not the same as the first. The fourth catches an overwrite; the first catches an item never written at all. §19 is the second kind, and only a count detects it.

DV. All four; drive downstream ready toggling every cycle with continuous upstream traffic.

21. Sizing Begins With Latency, Not With Bandwidth

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
required_entries  ≈  arrival_rate × blocking_latency  +  headroom
 
     [items/cycle] × [cycles]  =  [items]        <- dimensionally correct

Three inputs, and each must be measured rather than guessed:

InputWhat it isWhere it comes from
arrival rateitems per cycle the producer can offerthe producer's own capability
blocking latencyhow long the consumer can be unable to take onethe downstream's worst case
headroomitems already in flight when backpressure asserts§27's round trip

The third is the one designs omit, and §28 is what happens. It is not a safety margin — it is a computed quantity.

22. A Worked Sizing

Illustrative numbers throughout.

A producer offering one item per cycle, a consumer that can stall for 12 cycles, and a backpressure round trip of 3 cycles.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
STEP 1 — absorb the stall
  arrival_rate     = 1 item/cycle
  blocking_latency = 12 cycles
  stall_absorption = 1 × 12 = 12 items
 
STEP 2 — headroom for items already in flight when we say stop (§27)
  backpressure_round_trip = 3 cycles
  headroom = 1 item/cycle × 3 cycles = 3 items
 
STEP 3 — total
  DEPTH >= 12 + 3 = 15 items
 
STEP 4 — and the high watermark follows (§29)
  HIGH_WM <= DEPTH - headroom = 15 - 3 = 12

Three readings.

The depth and the watermark are computed together, from the same two inputs. A design that sizes the depth and then picks a watermark by intuition has done half the arithmetic.

And now ask the question §23 asks: does this design actually need zero backpressure? If occasional backpressure is acceptable — and it usually is — a depth of 8 with a watermark of 5 may be entirely adequate, at half the storage.

If DEPTH is rounded to 16 for a power-of-two wrap (§13), the extra entry is free headroom. If it is left at 15, the wrap must be explicit.

23. Depth Against Throughput

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ILLUSTRATIVE, same producer and consumer as Section 22:
 
  DEPTH  4  ->  producer stalls often          ~62% of offered rate
  DEPTH  8  ->  producer stalls occasionally   ~88%
  DEPTH 15  ->  producer never stalls         ~100%
  DEPTH 32  ->  producer never stalls         ~100%    <- no change
  DEPTH 64  ->  producer never stalls         ~100%    <- no change

Once the latency is fully hidden, additional depth buys nothing — and costs:

Cost of extra depthWhy
area and leakagewide payload RAM
latencya full buffer is a queue an item waits in (15.2)
verification state spacemore occupancy values, more corner cases
debug difficultya deeper buffer hides the moment congestion began

"Deeper is safer" is false. Depth beyond the latency requirement adds queueing delay and hides the symptom that would have told you the real problem — which is §24.

24. Wrong Architecture — a Large FIFO for a Structural Bottleneck

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG as an architectural response. The downstream is slower than the
// upstream on AVERAGE, and the answer chosen was more storage.
localparam int DEPTH = 4096;      // "that should be enough"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ILLUSTRATIVE:
  sustained arrival rate = 1.0 items/cycle
  sustained service rate = 0.8 items/cycle
  net accumulation       = 0.2 items/cycle
 
  time to fill DEPTH = 4096 / 0.2 = 20,480 cycles
 
  -> the buffer fills, backpressure asserts, and throughput settles at 0.8.
     The 4096 entries bought 20,480 cycles of delay before that happened.

Three properties.

The final throughput is 0.8 either way. A depth of 16 reaches it in 80 cycles; a depth of 4096 reaches it in 20,480. Storage bought latency, not throughput.

And it makes the system worse in one specific respect: every item now waits behind up to 4095 others, so latency at steady state is enormous — bufferbloat (13.4 §28).

The diagnostic damage is the real cost. A small buffer asserts backpressure quickly and the stall counter immediately names the downstream as the constraint. A huge one hides that for 20,000 cycles, so the measurement window may end before the truth appears.

25. The Sustainable-Rate Condition

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
For a bounded queue:      long-term arrival rate  <=  long-term service rate
 
If that is violated:      NO finite buffer bounds the queue.
                          Depth changes WHEN it fills, never WHETHER.
ConditionBuffer's role
arrival < service, with burstsabsorb the bursts — this is what buffers are for
arrival ≈ service, with jitterabsorb the jitter
arrival > service, sustainednothing — the buffer is a delay line to the inevitable

A buffer converts a burst into a delay. It cannot convert a rate deficit into anything. §24's design tried, and the only thing it changed was how long the truth took to appear.

26. Three Thresholds

ThresholdMeaningSet by
FULLno further allocation is possibleDEPTH
HIGH watermarkbegin throttling upstream now§29's headroom equation
LOW watermarkit is safe to resume§31's hysteresis gap

All three are needed, and the reasons are different: FULL is a hard limit, HIGH is a timing decision, and LOW is a stability decision.

27. Why the High Watermark Is Early

Backpressure has a round-trip latency. By the time the producer sees "stop", it may already have launched several more items — and those items must have somewhere to go.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
headroom  >=  backpressure_round_trip  ×  maximum_arrival_rate
 
     [cycles] × [items/cycle]  =  [items]

The round trip has at least three components, and a design that counts only one under-reserves:

ComponentIllustrative
the watermark comparison registering1 cycle
the backpressure signal reaching the producer1 cycle
the producer's own pipeline draining what it already issued1–N cycles

The third is the one that varies and the one that is forgotten. 13.3 §7 makes the general N-stage argument; here it is a number that goes into an equation.

28. Wrong Watermark — Assert at Full

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — backpressure begins only when there is no room left.
assign upstream_stall = full;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. Occupancy reaches DEPTH. `full` asserts.
2. The signal takes 3 cycles to reach the producer and stop it (Section 27).
3. During those 3 cycles the producer sends 3 more items.
4. -> three items arrive at a full buffer.
What the design does with themConsequence
overwritethree obligations lost — §1's failure
dropthree accepted objects vanished
rely on the producer's own skidworks only if the skid is 3 deep — and a skid is 1 (§18)

Four properties.

It is a real architectural bug and not merely a tuning error. No amount of depth fixes it: a deeper buffer just reaches full later and then overflows by the same three items.

The overflow count equals the round trip times the rate, so it is deterministic and reproducible — which makes it easy to fix once diagnosed and impossible to see until the buffer actually fills.

And the design passes every test that never fills the buffer, which is every test that does not saturate.

The fix is §29's equation, and it is checked at elaboration (§14) rather than discovered in silicon.

29. The High-Watermark Equation, Worked

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
HIGH_WM  <=  DEPTH  -  headroom
         =  DEPTH  -  (backpressure_round_trip × max_arrival_rate)

Worked with §22's illustrative numbers:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DEPTH                   = 15 items
backpressure round trip = 3 cycles
max arrival rate        = 1 item/cycle
 
headroom = 3 × 1 = 3 items
HIGH_WM <= 15 - 3 = 12

Now with a deeper producer pipeline — say the producer has 4 stages that must drain:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
backpressure round trip = 1 (register) + 1 (wire) + 4 (drain) = 6 cycles
headroom = 6 × 1 = 6 items
HIGH_WM <= 15 - 6 = 9
 
-> the SAME buffer now throttles at 9 instead of 12, purely because the
   producer got deeper. The depth did not change; the usable depth did.

Three readings.

The producer's pipeline depth is an input to the consumer's watermark. That is a cross-block dependency, and it is exactly the kind that gets lost between two teams.

The usable depth fell from 12 to 9 — a 25% reduction — with no change to the buffer. A design that fixes HIGH_WM and then deepens the producer has silently re-created §28.

And that is why it belongs in an elaboration check (§14): the check names both parameters, so changing one without the other fails the build.

30. SVA — Headroom Is Not Exceeded

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY, with the assumption written down.
//   A1: the producer stops within the stated round trip after the watermark
assume property (@(posedge clk) disable iff (!rst_n)
  high_wm_asserted |-> ##[1:BP_ROUND_TRIP] !producer_offering);
 
property p_never_overflows_after_watermark;
  @(posedge clk) disable iff (!rst_n)
    high_wm_asserted |-> ##[0:BP_ROUND_TRIP] (occupancy <= DEPTH);
endproperty
a_never_overflows_after_watermark:
  assert property (p_never_overflows_after_watermark);
 
// The headroom itself: from the watermark, at most `headroom` more arrive.
property p_headroom_sufficient;
  @(posedge clk) disable iff (!rst_n)
    high_wm_asserted |-> (arrivals_since_watermark <= HEADROOM);
endproperty
a_headroom_sufficient: assert property (p_headroom_sufficient);
 
// And the plain safety property, always.
property p_no_overflow_ever;
  @(posedge clk) disable iff (!rst_n) push_fire |-> !full;
endproperty
a_no_overflow_ever: assert property (p_no_overflow_ever);

Architecture. Three properties plus an assumption, and the assumption is the interesting part.

Why A1 must be explicit. Without it the property is unprovable — a producer that never stops overflows any buffer. Stating it turns "this overflows" into "this overflows because the producer did not stop within the round trip we sized for", which is a diagnosis rather than a symptom.

And it converts §29's cross-block dependency into a checkable claim. If the producer's actual stop latency exceeds BP_ROUND_TRIP, the assumption fails in simulation and names the mismatch.

DV. Run with the producer at maximum rate and confirm the assumption holds; then deepen the producer and confirm it fails, which validates that the parameter is load-bearing.

31. Hysteresis

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Two thresholds and one state bit — Section 32 is one threshold.
typedef enum logic { WM_ACCEPTING, WM_THROTTLED } wm_state_e;
 
wm_state_e wm_state_q;
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n)
    wm_state_q <= WM_ACCEPTING;
  else
    unique case (wm_state_q)
      WM_ACCEPTING: if (occupancy >= HIGH_WM) wm_state_q <= WM_THROTTLED;
      WM_THROTTLED: if (occupancy <= LOW_WM)  wm_state_q <= WM_ACCEPTING;
      default:                                wm_state_q <= WM_ACCEPTING;
    endcase
 
assign upstream_stall = (wm_state_q == WM_THROTTLED);

Architecture. A two-state machine rather than a comparison. The state is what gives the design memory of which side of the gap it is on, and a pure comparison has none.

State. One bit.

Cycle behaviour. Throttle on crossing HIGH_WM upward; release on crossing LOW_WM downward. The gap between them is the hysteresis, and it must be large enough that normal drain activity does not cross it repeatedly.

Contract. The producer sees a signal that changes at most twice per congestion episode rather than every cycle. That is a timing property as much as a performance one — a signal toggling every cycle across a die boundary is a real physical problem.

Failure. §32. Also setting LOW_WM to zero, which only releases when the buffer is completely empty — turning every congestion episode into a full drain and destroying throughput.

DV. §33's properties; cover both transitions and cover sustained occupancy inside the gap.

32. Wrong Design — One Threshold

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — a single comparison, no memory.
assign upstream_stall = (occupancy >= THRESHOLD);

With THRESHOLD = 8 and a producer and consumer both running at one item per cycle:

CycleOccupancyupstream_stallEffect
n70producer sends
n+181producer stops
n+270producer sends
n+381producer stops
7, 8, 7, 81, 0, 1, 0toggling every cycle

Four properties.

Throughput falls even though the buffer never overflows. Each stop-start cycle inserts a bubble, so the achieved rate is roughly half the offered rate while both endpoints are capable of full rate.

The control signal toggles every cycle. Across a die boundary that is a signal-integrity and power problem, and it may not even meet timing if the path is long.

And the diagnostic picture is confusing. Occupancy oscillates around the threshold, so the buffer looks "about right"; throughput is halved with no stall reason dominating — because the stalls are single cycles spread evenly.

The fix is a gap, and its size is a design parameter: large enough that the drain rate cannot cross it in one cycle, small enough that the throttled period is not wasteful.

33. SVA — Watermark Behaviour

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Throttling begins at or before the high watermark.
property p_throttle_at_high_wm;
  @(posedge clk) disable iff (!rst_n)
    (occupancy >= HIGH_WM) |-> upstream_stall;
endproperty
a_throttle_at_high_wm: assert property (p_throttle_at_high_wm);
 
// Release happens only at or below the low watermark.
property p_release_at_low_wm;
  @(posedge clk) disable iff (!rst_n)
    $fell(upstream_stall) |-> ($past(occupancy) <= LOW_WM);
endproperty
a_release_at_low_wm: assert property (p_release_at_low_wm);
 
// Hysteresis: the state does not change twice within a short window.
property p_no_chatter;
  @(posedge clk) disable iff (!rst_n)
    $changed(wm_state_q) |=> $stable(wm_state_q)[*MIN_DWELL];
endproperty
a_no_chatter: assert property (p_no_chatter);
 
// Ordering, checked once at elaboration and again at runtime.
property p_watermark_ordering;
  @(posedge clk) disable iff (!rst_n)
    ((LOW_WM < HIGH_WM) && (HIGH_WM <= DEPTH - HEADROOM));
endproperty
a_watermark_ordering: assert property (p_watermark_ordering);

Architecture. Four properties: the throttle point, the release point, dwell time, and ordering.

Why the third is worth writing. It is the observable form of hysteresis — a design can have two thresholds set one apart and still chatter. MIN_DWELL states how long the design claims each state persists, and if that claim is false the property says so.

DV. Drive the producer and consumer at equal rates near the threshold — which is exactly §32's condition and is the only way to see chatter.

34. Ping-Pong Buffering

Not a queue. Two banks, and at any moment each has an owner.

Ping-pongFIFO
Natural unita block or a phasean item
Latency granularitya whole bankone item
Overlapexcellent — produce and consume in parallelstreaming
Variable burst sizeawkward — banks are fixednatural
Ownershipexplicit, per bankimplicit, per pointer
Suitstile processing, phase-structured workcontinuous streams

Neither is better. A ping-pong suits work that arrives in blocks and is consumed in blocks; a FIFO suits work that arrives and is consumed item by item. Using a ping-pong for a stream wastes half the storage waiting; using a FIFO for a block forces the consumer to track block boundaries itself.

35. Bank Ownership

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Four states per bank, with EXACTLY ONE owner at a time.
typedef enum logic [1:0] {
  BANK_FREE      = 2'd0,   // nobody owns it
  BANK_PRODUCING = 2'd1,   // the producer owns it
  BANK_FULL      = 2'd2,   // filled; ownership is being handed over
  BANK_CONSUMING = 2'd3    // the consumer owns it — DO NOT WRITE
} bank_state_e;
 
bank_state_e bank_state_q [2];
logic        prod_bank_q, cons_bank_q;
 
// The producer may only claim a FREE bank.
assign prod_can_claim = (bank_state_q[prod_bank_q] == BANK_FREE);
// The consumer may only claim a FULL bank.
assign cons_can_claim = (bank_state_q[cons_bank_q] == BANK_FULL);
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    bank_state_q[0] <= BANK_FREE;
    bank_state_q[1] <= BANK_FREE;
    prod_bank_q     <= 1'b0;
    cons_bank_q     <= 1'b0;
  end else begin
    for (int b = 0; b < 2; b++) begin
      unique case (bank_state_q[b])
        BANK_FREE:      if (prod_claim && (prod_bank_q == b[0]))
                                                  bank_state_q[b] <= BANK_PRODUCING;
        BANK_PRODUCING: if (prod_done  && (prod_bank_q == b[0]))
                                                  bank_state_q[b] <= BANK_FULL;
        BANK_FULL:      if (cons_claim && (cons_bank_q == b[0]))
                                                  bank_state_q[b] <= BANK_CONSUMING;
        // ONLY the consumer releases. Section 36 is the producer releasing.
        BANK_CONSUMING: if (cons_release && (cons_bank_q == b[0]))
                                                  bank_state_q[b] <= BANK_FREE;
        default: ;
      endcase
    end
 
    if (prod_done)    prod_bank_q <= ~prod_bank_q;
    if (cons_release) cons_bank_q <= ~cons_bank_q;
  end

Architecture. Four states per bank, and only two of them permit a write or a read. The states are not decoration: FREE and FULL are the only two in which ownership transfers, and the two owners can never both be in an accessing state for one bank.

State. Two two-bit registers plus two selectors. Tiny, and it is the only thing standing between the design and §36.

Cycle behaviour. prod_can_claim gates the producer — it cannot begin filling a bank that is FULL or CONSUMING, which is enforcement rather than convention. cons_release is the only transition out of CONSUMING.

Contract. The consumer reads a bank for the duration of a block and relies on it not changing. That reliance has no signal at the consumer's interface, which is why §37 asserts it.

Failure. §36. Also allowing CONSUMING → PRODUCING directly, which skips FREE and lets the producer start while the consumer is finishing.

DV. §37; cover every state per bank and the full ping-pong cycle, which needs at least three blocks to observe.

36. Wrong Ping-Pong — Swapping When the Producer Finishes

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the producer swaps banks when it finishes filling one.
always_ff @(posedge clk)
  if (prod_done) begin
    prod_bank_q <= ~prod_bank_q;              // move to the other bank
    bank_free_q[~prod_bank_q] <= 1'b1;        // ← and declare it free
  end
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. The producer fills bank A and finishes. It swaps to bank B.
2. The consumer is still reading bank B from the previous round.
3. The producer begins writing bank B.
4. -> the consumer's data changes underneath it, mid-block.

Four properties.

No CRC catches it. The data crossed every link perfectly and was written to memory perfectly. This is a purely local ownership bug, invisible to every transport check (18.1 §18).

The corruption is partial. Only the part of the block the consumer had not yet read is affected, so the result is nearly right — which for numerical work can pass a tolerance check and be blamed on precision.

And it is timing-dependent in the direction that hides it. If the consumer is faster than the producer it always finishes first and nothing goes wrong. The bug appears only when the consumer is slower, which is the load case the ping-pong exists for.

The rule is one sentence: the producer does not decide when its old bank is free — the consumer does. §35's BANK_CONSUMING → BANK_FREE transition is gated on cons_release and on nothing else.

37. SVA — Bank Ownership Is Exclusive

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. The producer never writes a bank the consumer owns.
property p_no_write_to_consuming_bank;
  @(posedge clk) disable iff (!rst_n)
    prod_write_fire |-> (bank_state_q[prod_bank_q] == BANK_PRODUCING);
endproperty
a_no_write_to_consuming_bank:
  assert property (p_no_write_to_consuming_bank);
 
// The consumer never reads a bank it does not own.
property p_no_read_unowned_bank;
  @(posedge clk) disable iff (!rst_n)
    cons_read_fire |-> (bank_state_q[cons_bank_q] == BANK_CONSUMING);
endproperty
a_no_read_unowned_bank: assert property (p_no_read_unowned_bank);
 
// A bank becomes FREE only on a consumer release (Section 36).
property p_free_only_on_consumer_release;
  @(posedge clk) disable iff (!rst_n)
    ((bank_state_q[B] == BANK_FREE) && ($past(bank_state_q[B]) == BANK_CONSUMING))
      |-> $past(cons_release && (cons_bank_q == B));
endproperty
a_free_only_on_consumer_release:
  assert property (p_free_only_on_consumer_release);
 
// A bank's contents are stable while the consumer owns it — the EFFECT.
property p_bank_stable_while_consuming;
  @(posedge clk) disable iff (!rst_n)
    (bank_state_q[B] == BANK_CONSUMING) |=> $stable(bank_mem[B]);
endproperty
a_bank_stable_while_consuming:
  assert property (p_bank_stable_while_consuming);

Architecture. Four properties: no write to an owned bank, no read of an unowned one, the release rule, and content stability.

Why the fourth matters beyond the first three. They check the protocol; the fourth checks the effect. A design can satisfy the ownership protocol and still corrupt a bank through a debug port, a second writer, or an aliased address — and only a stability check on the contents catches that.

DV. Inject a producer write to a CONSUMING bank and confirm the first fires. That is §36, and it is the only detector.

38. Replay Depth Is a Reliability Window

Every other buffer in this chapter is sized by latency or by burst. The replay buffer is not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
replay_depth  >=  maximum number of objects that can be unresolved at once
 
              =  issue_rate  ×  worst-case resolution latency
InputWhat it is
issue rateobjects sent per cycle
worst-case resolution latencysend → far end → its processing → the resolution returning → local processing

And the worst case is not the average. It includes:

ContributionPresent when
the round tripalways
the far end's own queueingunder load
a retransmission (19.3 §16)on an error
a link recovery (14.2)on a recoverable fault
a degraded link (14.4)after a degraded recovery

39. Wrong Replay Sizing — Based on Average Traffic

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ILLUSTRATIVE:
  measured average unresolved objects  = 4
  chosen depth                          = 8      ("2× margin, plenty")
 
  worst-case resolution latency, including one retransmission
    and a degraded link                 = 20 objects' worth
 
  -> the ring fills at 8, admission stops (19.3 §9), and throughput
     collapses to whatever the resolution path can sustain.

Four properties.

It is not a correctness failure, and that is what makes it survive. 19.3 §9's admission term means the Adapter simply refuses work — no object is lost and no obligation is broken. The design is correct and slow.

It works in every easy test. With a clean link and no retries the average is the worst case, so 8 is ample.

And it collapses exactly when reliability is being exercised. A retransmission extends the resolution latency for that object, and every object behind it stays unresolved longer too — so one retry lengthens the window for many entries at once.

The diagnostic signature is distinctive: ADP_REPLAY_FULL dominating the stall histogram (19.3 §52) while the link itself is not saturated.

40. Reassembly Sizing

Reassembly is sized by concurrent incomplete objects, not by bandwidth.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reassembly_slots  >=  maximum number of objects that can be simultaneously
                      incomplete at the receiver
 
reassembly_bytes_per_slot  >=  maximum object size

Two inputs, and the first is the subtle one. It depends on how the sender interleaves — a sender that completes each object before starting the next needs one slot; one that interleaves N objects needs N.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Slots are RESERVED at the first fragment, not discovered later.
logic [NUM_SLOTS-1:0]  slot_busy_q;
logic [MAX_FRAGS-1:0]  frag_received_q [NUM_SLOTS];
logic [DATA_W-1:0]     reasm_mem       [NUM_SLOTS][MAX_FRAGS];
 
// A first fragment is accepted only if a slot can hold the WHOLE object.
assign first_frag_admit = (slot_busy_q != '1)                       // a slot exists
                       && (frag.frag_count <= MAX_FRAGS);           // it fits

Architecture. Slot reservation at the first fragment. Accepting a first fragment is accepting an obligation to assemble the whole object — and §41 is the design that accepts before knowing it can.

Failure. §41.

DV. §42; cover the maximum number of concurrent incomplete objects.

41. Wrong First-Fragment Admission

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — fragments are accepted as they arrive, and space is sought per fragment.
always_ff @(posedge clk)
  if (frag_valid && any_space_available)
    store_fragment(frag);                     // ← no slot reserved for the OBJECT
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. The first fragment of object A is accepted. Space existed.
2. Fragments of objects B, C, D arrive and are accepted. Space still existed.
3. Object A's second fragment arrives. There is now no space.
4. -> the receiver holds a partial object it can never complete, and it is
     occupying a slot that will never be released.

Four properties.

The receiver now owns an obligation it cannot discharge, which is 19.3 §10's failure at the receive side. Accepting the first fragment was the promise.

And the partial object holds its slot forever unless there is an abandonment policy — so the leak is permanent and the receiver's effective capacity shrinks with every occurrence.

The sender cannot help. It has no visibility into the receiver's slot occupancy; from its point of view the fragments were accepted.

The fix is to reserve the whole object's requirement at the first fragment (§40) — refusing the first fragment when the object cannot be completed, which pushes the problem back to a retransmission rather than into a permanent leak.

42. SVA — an Accepted Partial Object Can Complete

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Accepting a first fragment implies capacity for the whole object.
property p_first_frag_implies_capacity;
  @(posedge clk) disable iff (!rst_n)
    first_frag_accept |-> ((slot_busy_q != '1) && (frag.frag_count <= MAX_FRAGS));
endproperty
a_first_frag_implies_capacity:
  assert property (p_first_frag_implies_capacity);
 
// A slot holds exactly one object while busy.
property p_slot_single_object;
  @(posedge clk) disable iff (!rst_n)
    slot_busy_q[S] |-> $stable(slot_object_id_q[S]);
endproperty
a_slot_single_object: assert property (p_slot_single_object);
 
// Every accepted partial object eventually completes or is explicitly abandoned.
//   A1: the sender eventually delivers or the link fails explicitly
property p_partial_resolves;
  @(posedge clk) disable iff (!rst_n)
    first_frag_accept |-> ##[1:REASM_BOUND] (object_complete(S) || object_abandoned(S));
endproperty
a_partial_resolves: assert property (p_partial_resolves);
 
// An abandoned slot is released — no permanent leak (Section 41).
property p_abandoned_slot_released;
  @(posedge clk) disable iff (!rst_n)
    object_abandoned(S) |=> !slot_busy_q[S];
endproperty
a_abandoned_slot_released: assert property (p_abandoned_slot_released);

Architecture. Four properties: the capacity implication, slot exclusivity, bounded resolution, and leak prevention.

Why the fourth is needed given the third. The third permits abandonment; the fourth requires abandonment to actually free the slot. A design that abandons without releasing has replaced a hang with a leak.

DV. Drive the maximum concurrent incomplete objects and then one more; confirm the extra first fragment is refused.

43. Multi-Class Partitioning

StructureUtilisationIsolationFailure mode
fully shared poolhighestnoneone class consumes everything (§44)
fixed per-classlowest — stranded spacecompletea class with no traffic wastes its share
reserved minimum + shared poolhighboundedfragmentation, and a reserve that must be non-zero

The hybrid is usually right, and its parameter is the reserve. A reserve of zero is a shared pool with extra code; a reserve equal to the share is a fixed partition with extra code. The value in between is the design decision.

44. Wrong Shared Pool

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — every class allocates from one pool with no reserve.
assign admit = (free_entries != '0);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. A streaming class fills every entry.
2. A progress-critical completion arrives — one that RELEASES resources.
3. It cannot be admitted.
4. The streaming traffic cannot drain, because draining requires the
   resource that completion would have released.
5. -> deadlock, on a design where every block is individually correct.

Three properties.

It is 18.2 §32's deadlock at buffer scope, and the mechanism is identical: a class that releases resources queues behind one that acquires them.

No safety assertion fires. Nothing overflows, nothing is corrupted, no occupancy is wrong. The buffer is simply full of the wrong thing.

And it is load-dependent. At moderate load the pool never fills, so it appears only at saturation — the operating point a performance campaign creates.

45. Reserved-Entry Policy

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. A shared pool with a reserve that bulk traffic cannot touch.
// Generic — no standard is claimed to require it (Section 3).
localparam int PROGRESS_RESERVE = 4;
 
logic [OCC_W-1:0] free_entries;
 
// Bulk may allocate only above the reserve.
assign bulk_admit     = (free_entries > OCC_W'(PROGRESS_RESERVE));
// Progress-critical traffic may use anything that is free.
assign progress_admit = (free_entries != '0);
 
// The reserve is a real parameter — checked at elaboration (Section 14).
initial begin
  assert (PROGRESS_RESERVE > 0)
    else $fatal(1, "reserve of zero is a shared pool — Section 44 is reachable");
  assert (PROGRESS_RESERVE < DEPTH)
    else $fatal(1, "reserve must leave capacity for bulk traffic");
end

Architecture. One pool, two admission rules. The asymmetry is deliberate: progress traffic may borrow from the bulk region when it is free, and bulk may never borrow from the reserve.

State. None beyond the pool's own occupancy.

Cycle behaviour. Both terms are combinational from free_entries. The reserve costs nothing when the pool is not full, because progress traffic simply uses the shared region.

Contract. §46's liveness property depends on PROGRESS_RESERVE being non-zero. A zero reserve removes the guarantee while leaving every line that implements it — hence the elaboration check (18.2 §34).

Failure. §44. Also reserving so much that bulk throughput collapses — the over-correction, which is a performance failure rather than a deadlock and is the better direction to err in.

DV. §46; saturate with bulk and confirm progress traffic still enters.

46. SVA — the Reserve Is Real

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Bulk traffic never consumes the reserved capacity.
property p_bulk_respects_reserve;
  @(posedge clk) disable iff (!rst_n)
    bulk_push_fire |-> ($past(free_entries) > PROGRESS_RESERVE);
endproperty
a_bulk_respects_reserve: assert property (p_bulk_respects_reserve);
 
// Free entries never fall below the reserve while only bulk is pushing.
property p_reserve_preserved;
  @(posedge clk) disable iff (!rst_n)
    (bulk_push_fire && !progress_push_fire) |=> (free_entries >= PROGRESS_RESERVE);
endproperty
a_reserve_preserved: assert property (p_reserve_preserved);
 
// LIVENESS: progress traffic is admitted within a bound.
//   A1: the consumer eventually drains an entry
//   A2: progress traffic keeps offering until admitted
property p_progress_admitted;
  @(posedge clk) disable iff (!rst_n)
    progress_offering |-> ##[1:PROGRESS_BOUND] progress_push_fire;
endproperty
a_progress_admitted: assert property (p_progress_admitted);

Architecture. Two safety properties and one bounded liveness property.

Why the third is what "isolation" actually means. The first two prove the mechanism exists; only the liveness bound proves a progress-critical item gets in regardless of how much bulk traffic there is — which is the whole point (§44).

DV. Prove the third with bulk at maximum rate. Then set the reserve to zero and confirm it fails, demonstrating the parameter is load-bearing.

47. The CDC FIFO

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Standard structure; the point here is what makes it different
// from Section 7's single-clock FIFO. 18.4 Section 18 covers the general case.
logic [PTR_W:0] wr_ptr_bin_q,  wr_ptr_gray_q;    // write domain
logic [PTR_W:0] rd_ptr_bin_q,  rd_ptr_gray_q;    // read domain
logic [PTR_W:0] wr_gray_sync_q [2];              // synchronised INTO read domain
logic [PTR_W:0] rd_gray_sync_q [2];              // synchronised INTO write domain
 
buf_entry_t cdc_mem [DEPTH];                     // NOT reset
 
function automatic logic [PTR_W:0] bin2gray(input logic [PTR_W:0] b);
  return b ^ (b >> 1);
endfunction
 
always_ff @(posedge wr_clk or negedge wr_rst_n)
  if (!wr_rst_n) begin
    wr_ptr_bin_q  <= '0;
    wr_ptr_gray_q <= '0;
  end else if (wr_en && !wr_full) begin
    wr_ptr_bin_q  <= wr_ptr_bin_q + 1'b1;
    wr_ptr_gray_q <= bin2gray(wr_ptr_bin_q + 1'b1);
  end
 
// Full is computed in the WRITE domain, from a STALE read pointer.
assign wr_full = (wr_ptr_gray_q == {~rd_gray_sync_q[1][PTR_W:PTR_W-1],
                                     rd_gray_sync_q[1][PTR_W-2:0]});
// Empty is computed in the READ domain, from a STALE write pointer.
assign rd_empty = (rd_ptr_gray_q == wr_gray_sync_q[1]);

Architecture. Gray-coded pointers through two-flop synchronisers. Gray coding is the essential part: exactly one bit changes per increment, so a pointer sampled mid-transition resolves to the old value or the new one and never to a combination that was never written.

What makes it different from §7's FIFO, and this is the section's point:

Single-clock (§7)CDC
full / emptyexactconservative — computed from a stale pointer
Occupancyexact, derivednot exactly knowable in either domain
Depth sizinglatency (§21)latency plus synchroniser delay
A simultaneous push and poptrivially handledthe two domains do not share a cycle

Cycle behaviour. Each domain compares its own pointer against a synchronised, therefore stale copy of the other's. Full may assert when the FIFO is not quite full and empty when it is not quite empty — both are safe directions and both are unavoidable.

Contract. The producer relies on wr_full; the consumer on rd_empty. Both are conservative, so the depth must include the synchroniser latency — a shallow CDC FIFO across a slow crossing spends most of its time apparently full.

Failure. §48.

DV. Formal or constrained-random with unrelated clock frequencies and phases; assert no overflow, no underflow, and no read of an unwritten slot.

48. Wrong CDC — a Binary Pointer Synchronised Directly

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — a multi-bit binary pointer crossed through synchronisers.
always_ff @(posedge rd_clk) begin
  wr_ptr_sync_q[0] <= wr_ptr_bin_q;        // ← several bits change at once
  wr_ptr_sync_q[1] <= wr_ptr_sync_q[0];
end

When a binary pointer increments across a carry, several bits change in the same source cycle — and the destination may sample some of them before and some after.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ILLUSTRATIVE: the pointer goes 0111 -> 1000. Four bits change.
 
The destination can sample any combination:
  0111  (old)   ✓ safe
  1000  (new)   ✓ safe
  1111          ✗ a value that was NEVER written
  0000          ✗ likewise
  1011, 0100, ...  ✗ likewise

Four properties.

A sampled value of 1111 makes the FIFO look nearly full when it is nearly empty, or the reverse — so empty may deassert when there is nothing to read, and the consumer reads a slot that was never written.

And 0000 makes it look empty when it is full, so the producer overwrites live entries.

The failure is rare and load-dependent, occurring only when a carry-heavy increment coincides with the destination's sampling edge. It scales with the number of bits that change, so it is worst at the wraps.

Gray coding removes the possibility entirely, which is why it is the standard answer: with one bit changing per increment there is no intermediate combination to sample.

49. Reset and the CDC FIFO

Two domains means two resets, and their relationship matters.

HazardConsequence
one domain reset while the other is notthe pointers disagree: one is zero and the other is not, so full or empty is computed against a stale non-zero value
resets released at different timesthe same, transiently
the payload RAM reset in one domainirrelevant — it is not reset (§7)

50. What Each Buffer Does at a Recovery

Seven kinds, and at least four different answers.

BufferOn a UCIe recoveryWhy
Skidholdit contains an accepted item
Ingress FIFOholdaccepted obligations
Stagingholdaccepted obligations (19.3 §51)
Replaypreserve and re-baselineclearing loses objects and duplicates others
Reassemblyarchitecture-defined — complete, abandon explicitly, or retrya partial object's fate is a protocol question
Output FIFOholdalready-delivered objects
Ping-pong bankhold — ownership is unaffecteda link event does not change a local bank's owner
CDC FIFO to the PHYmay drain — its contents are physical-layer-boundthe PHY's state is being rebuilt anyway

Two readings.

Six of eight say hold. A recovery is a transport event and holds no semantic meaning (14.2 §4) — so the default answer is "nothing", and the exceptions need justification.

And the two exceptions are for opposite reasons. Replay is preserved because its contents are precious; the PHY-facing CDC FIFO may drain because its contents are being regenerated by the retrain anyway. A design that gives both the same answer is wrong about one of them.

51. Wrong Design — a Global Buffer Flush

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — one signal clears every buffer on the link.
assign flush_all = link_recovery;
 
// ...and every buffer uses it:
always_ff @(posedge clk)
  if (flush_all) begin
    ingress_occ_q  <= '0;
    staging_occ_q  <= '0;
    replay_valid_q <= '0;          // ← the worst one
    output_occ_q   <= '0;
    skid_q.valid   <= 1'b0;
  end

Walk §50's table with this line in place.

BufferShould beActuallyConsequence
skidholdclearedone accepted item lost
ingressholdclearedevery queued obligation lost
stagingholdclearedevery accepted object lost
replaypreserveclearedobjects unretransmittable; duplicates on the other side (19.3 §50)
outputholdclearedobjects already delivered semantically are discarded

Four properties.

The output FIFO row is the one people miss. Those objects were already delivered — the gate passed, the duplicate check passed, the protocol engine was told they arrived. Clearing them discards work the far end will never send again, because from its point of view the delivery succeeded.

And the link comes back and reports success. 19.1 §9's observation, at buffer scope.

It is the same anti-pattern as a global reset, a global fabric pause and a global backpressurea local event given a global scope, and this curriculum has now seen it at five layers.

The correct form is per-buffer policy, driven by what each buffer's contents mean — which is exactly what §6's ownership table records.

52. SVA — Buffers Survive a Recovery

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. The buffers that hold accepted obligations are untouched.
property p_ingress_survives_recovery;
  @(posedge clk) disable iff (!rst_n)
    recovery_entered |=> $stable(ingress_occupancy);
endproperty
a_ingress_survives_recovery: assert property (p_ingress_survives_recovery);
 
property p_staging_survives_recovery;
  @(posedge clk) disable iff (!rst_n)
    recovery_entered |=> $stable(staging_occupancy);
endproperty
a_staging_survives_recovery: assert property (p_staging_survives_recovery);
 
property p_output_survives_recovery;
  @(posedge clk) disable iff (!rst_n)
    recovery_entered |=> $stable(output_occupancy);
endproperty
a_output_survives_recovery: assert property (p_output_survives_recovery);
 
property p_skid_survives_recovery;
  @(posedge clk) disable iff (!rst_n)
    recovery_entered |=> $stable(skid_q.valid);
endproperty
a_skid_survives_recovery: assert property (p_skid_survives_recovery);
 
// And the negative form: no global flush signal reaches these buffers.
property p_no_global_flush;
  @(posedge clk) disable iff (!rst_n)
    recovery_entered |-> !(ingress_flush || staging_flush || output_flush);
endproperty
a_no_global_flush: assert property (p_no_global_flush);

Architecture. Four survival properties and one negative property.

Why the negative one is worth writing separately. It forbids a specific wiring — a recovery signal reaching a flush input — which is exactly §51's line, and it fails at integration rather than waiting for a symptom.

DV. Inject a recovery with every buffer non-empty (§54).

53. Instrumentation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Diagnostic only, in the POR reset domain (19.1 Section 7).
logic [63:0] occ_sum_q      [NUM_BUFFERS];   // sum of occupancy -> mean
logic [63:0] occ_max_q      [NUM_BUFFERS];   // high-water mark
logic [63:0] full_cycles_q  [NUM_BUFFERS];
logic [63:0] wm_assert_q    [NUM_BUFFERS];   // times the high watermark fired
logic [63:0] wm_dwell_q     [NUM_BUFFERS];   // cycles spent throttled
logic [63:0] overflow_q     [NUM_BUFFERS];   // MUST stay at zero
logic [63:0] reserve_used_q;                 // progress traffic in the reserve
logic [63:0] cdc_full_q     [NUM_CROSSINGS];
logic [63:0] reasm_abandon_q;                // Section 42
CounterAnswersWithout it
occ_max_qwas this buffer ever close to full?a buffer sized 4× too large looks identical to one sized right
wm_assert_q vs wm_dwell_qmany short throttles, or few long ones?§32's chatter is invisible in an average
overflow_qhas an obligation ever been lost?§28 is silent
reserve_used_qis the reserve ever actually needed? (§45)it looks like dead code
cdc_full_qis a crossing undersized for its clock ratio?a CDC depth problem looks like a producer problem
reasm_abandon_qhow often is a partial object given up?§41's leak vs a healthy abandon path

Two properties.

occ_max_q is the counter that right-sizes a design. A buffer whose high-water mark never exceeds 3 of 32 entries is 10× too large — and that is only visible with a maximum, never with a mean.

And wm_assert_q against wm_dwell_q is the pair that detects chatter. A thousand assertions with a mean dwell of one cycle is §32; ten assertions with a mean dwell of a hundred is healthy.

54. The Buffer Scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Verification-only. THREE models, because a buffer can be wrong in three
// independent ways: what it holds, what it promised, and what it lost.
class buffer_scoreboard;
 
  // ---- Layer 1: CONTENT model — what SHOULD be in the buffer, in order.
  typedef struct {
    int  ref_id;
    int  meta;                   // the metadata that accompanied it (Section 17)
  } entry_model_t;
  entry_model_t expected [int][$];    // [buffer id] -> queue of entries
 
  // ---- Layer 2: OCCUPANCY model — pushes, pops, and the derived count.
  typedef struct {
    int pushes, pops;
    int design_occupancy;
    int max_observed;
  } occ_model_t;
  occ_model_t occ [int];
 
  // ---- Layer 3: PROMISE model — the admission promises the buffer made.
  typedef struct {
    int  depth, high_wm, low_wm, headroom;
    int  arrivals_since_watermark;
    bit  overflowed;                  // MUST stay false
    bit  reserve_violated;            // Section 46
  } promise_model_t;
  promise_model_t promise [int];
 
  // ---- Catches Sections 9 and 12 — occupancy drift or a bad full/empty.
  function void check_occupancy(int b);
    if (occ[b].design_occupancy != (occ[b].pushes - occ[b].pops))
      $error("BUFFER %0d occupancy %0d != pushes %0d - pops %0d (Sections 9, 12)",
             b, occ[b].design_occupancy, occ[b].pushes, occ[b].pops);
  endfunction
 
  // ---- Catches Sections 16 and 19 — a lost item or a mismatched pair.
  function void check_pop(int b, int got_id, int got_meta);
    entry_model_t e = expected[b].pop_front();
    if (got_id != e.ref_id)
      $error("BUFFER %0d popped id %0d, expected %0d (Section 19 — an item was lost)",
             b, got_id, e.ref_id);
    if (got_meta != e.meta)
      $error("BUFFER %0d popped id %0d with meta %0d, expected %0d (Section 16)",
             b, got_id, got_meta, e.meta);
  endfunction
 
  // ---- Catches Section 28 — the headroom was insufficient.
  function void check_headroom(int b);
    if (promise[b].arrivals_since_watermark > promise[b].headroom)
      $error("BUFFER %0d: %0d arrivals after the watermark, headroom is %0d (Section 28)",
             b, promise[b].arrivals_since_watermark, promise[b].headroom);
  endfunction
 
  // ---- Catches Section 51 — a buffer cleared by a recovery.
  function void check_recovery(int b, int occ_before, int occ_after);
    if ((buffer_policy[b] == POLICY_HOLD) && (occ_after != occ_before))
      $error("BUFFER %0d occupancy changed across a recovery: %0d -> %0d (Section 51)",
             b, occ_before, occ_after);
  endfunction
 
  // ---- Right-sizing evidence, not a failure.
  function void report_sizing(int b);
    if (occ[b].max_observed < (promise[b].depth / 4))
      $display("NOTE: buffer %0d high-water mark %0d of depth %0d — likely oversized",
               b, occ[b].max_observed, promise[b].depth);
  endfunction
 
endclass

Architecture. Three models: what should be inside, how much is inside, and what the buffer promised.

Layer 1 stores the metadata alongside the identity, which is what makes §16's split-pointer mismatch detectable — a model that tracks only order sees the right sequence of payloads and passes.

Layer 3 is the unusual one. It models the promise: the depth, the thresholds, the headroom, and how many items arrived after the watermark asserted. §28's overflow is a promise violation rather than a content error, and no content model detects it.

And report_sizing is deliberately a note rather than an error. An oversized buffer is not a bug — it is evidence for the next design review, and printing it is how §23's argument reaches whoever chose the parameter.

55. Coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_ucie_buffering @(posedge clk);
  option.per_instance = 1;
 
  // --- Occupancy (Sections 7-14).
  cp_occ : coverpoint occupancy {
    bins empty  = {0};
    bins low    = {[1:LOW_WM]};
    bins mid    = {[LOW_WM+1:HIGH_WM-1]};
    bins high   = {[HIGH_WM:DEPTH-1]};
    bins full   = {DEPTH};
  }
  cp_simul_push_pop : coverpoint push_and_pop_same_cycle;      // Section 9
  cp_wrap : coverpoint pointer_wrapped {
    bins none = {0}; bins write = {1}; bins read = {2}; bins both = {3};
  }
  cp_depth_pow2 : coverpoint depth_is_power_of_two;            // Section 13
 
  // --- Metadata alignment (Sections 15-17).
  cp_meta_case : coverpoint metadata_write_case {
    bins together = {0}; bins data_only = {1}; bins meta_only = {2};  // 1,2 = §16
  }
 
  // --- Skid (Sections 18-20).
  cp_skid_case : coverpoint skid_case {
    bins pass_through = {0}; bins captured = {1}; bins held = {2}; bins drained = {3};
  }
  cp_skid_stall_boundary : coverpoint item_offered_as_ready_falls;  // Section 19
 
  // --- Watermarks (Sections 26-33).
  cp_wm_state : coverpoint wm_state_q { bins accepting = {0}; bins throttled = {1}; }
  cp_wm_dwell : coverpoint throttle_dwell {
    bins one_cycle = {1};                    // Section 32's chatter
    bins short = {[2:8]}; bins long = {[9:$]};
  }
  cp_arrivals_after_wm : coverpoint arrivals_since_watermark {
    bins none = {0}; bins some = {[1:HEADROOM-1]};
    bins at_headroom = {HEADROOM};           // Section 28's boundary
    bins over = {[HEADROOM+1:$]};            // MUST stay at zero
  }
 
  // --- Ping-pong (Sections 34-37).
  cp_bank_state : coverpoint bank_state_q_ut { bins each[] = {[0:3]}; }
  cp_pp_cycles : coverpoint completed_pingpong_cycles {
    bins none = {0}; bins one = {1}; bins several = {[2:$]};   // needs 3 blocks
  }
  cp_pp_consumer_slower : coverpoint consumer_slower_than_producer;  // Section 36
 
  // --- Replay and reassembly sizing (Sections 38-42).
  cp_replay_occ : coverpoint replay_occupancy {
    bins empty = {0}; bins mid = {[1:REPLAY_DEPTH-1]}; bins full = {REPLAY_DEPTH};
  }
  cp_replay_full_cause : coverpoint replay_full_cause {
    bins none = {0}; bins throughput = {1}; bins retry = {2}; bins recovery = {3};
  }
  cp_reasm_slots : coverpoint reasm_slots_busy {
    bins none = {0}; bins some = {[1:NUM_SLOTS-1]}; bins all = {NUM_SLOTS};
  }
  cp_first_frag_refused : coverpoint first_fragment_refused;   // Section 41
 
  // --- Partitioning (Sections 43-46).
  cp_reserve : coverpoint free_entries {
    bins above_reserve = {[PROGRESS_RESERVE+1:$]};
    bins at_reserve    = {PROGRESS_RESERVE};      // bulk must be refused here
    bins below_reserve = {[0:PROGRESS_RESERVE-1]};// only progress got in
  }
  cp_reserve_used : coverpoint progress_used_reserve;
 
  // --- CDC (Sections 47-49).
  cp_clock_ratio : coverpoint clock_ratio_class {
    bins same = {0}; bins rd_slower = {1}; bins rd_faster = {2}; bins unrelated = {3};
  }
  cp_cdc_occ : coverpoint cdc_occupancy_class {
    bins empty = {0}; bins mid = {1}; bins full = {2};
  }
 
  // --- Recovery (Sections 50-52).
  cp_recovery : coverpoint recovery_with_buffers {
    bins none = {0};
    bins some_nonempty = {1};
    bins all_nonempty = {2};                 // THE case — Section 58
  }
 
  // --- Crosses that carry the information.
  x_wm_arrivals : cross cp_wm_state, cp_arrivals_after_wm;   // Section 28
  x_pp_speed    : cross cp_bank_state, cp_pp_consumer_slower;// Section 36
  x_clock_cdc   : cross cp_clock_ratio, cp_cdc_occ;          // Section 48
  x_recovery_occ: cross cp_recovery, cp_occ;                 // Section 52
endcovergroup

Nine bins worth calling out:

cp_arrivals_after_wm.over must stay at zero. It is a detector, and any hit is §28.

cp_wm_dwell.one_cycle. §32's chatter, which needs producer and consumer at matched rates near the threshold.

cp_skid_case.captured and cp_skid_stall_boundary. §19's precondition — an item offered on the exact cycle readiness falls.

cp_meta_case.data_only and .meta_only. §16's divergence, which must never occur and is only observable if the environment can produce it.

cp_pp_consumer_slower crossed with the bank states. §36's condition — a ping-pong with a faster consumer never exhibits the bug.

cp_first_frag_refused. §41's fix working: the first fragment of an object that cannot be assembled is refused.

cp_reserve.at_reserve. §45's boundary — bulk refused with exactly the reserve remaining.

cp_clock_ratio beyond same. §48 is unreachable at a 1:1 ratio.

And cp_recovery.all_nonempty. §58's trace, and every property in §52 depends on it.

56. Flagship Trace 1 — Watermark, Headroom and Hysteresis

Illustrative. DEPTH = 15, HIGH_WM = 12, LOW_WM = 6, headroom 3, round trip 3 cycles.

CycOccupancywm_state_qProducerConsumerNote
04ACCEPTINGsendingdrainingsteady
2010ACCEPTINGsendingstallsconsumer blocked
2212THROTTLEDsendingstalledwatermark fires
2313THROTTLEDstill sendingstalledin-flight item 1
2414THROTTLEDstill sendingstalledin-flight item 2
2515THROTTLEDstopsstalledin-flight item 3 — exactly the headroom
2615THROTTLEDstoppedstalledfull, and nothing lost
4015THROTTLEDstoppedresumes
4510THROTTLEDstoppeddrainingnot released yet — above LOW_WM
496ACCEPTINGresumesdrainingreleased at LOW_WM
525ACCEPTINGsendingdrainingsteady again

Six readings.

Cycles 23 to 25 are the headroom being consumed. Three items arrive after the watermark, exactly as §29's equation predicted. §28's design asserts at cycle 25 instead, and those three items overflow.

Cycle 26: the buffer is full and nothing was lost. Full is not an error — it is the design operating exactly at its sized limit.

Cycles 45 to 48: the buffer drains from 10 to 6 while still throttled. That is the hysteresis gap doing its work. §32's single-threshold design releases at cycle 45 and re-throttles at 46.

Cycle 49 releases at LOW_WM, not at empty. A LOW_WM of zero would have kept the producer stopped until cycle 55, wasting six cycles of drain time.

The watermark asserted once and dwelled 27 cycles. wm_assert_q = 1, wm_dwell_q = 27 (§53) — the healthy signature.

And occ_max_q reaches 15 of 15, which is evidence the depth is right. A maximum of 5 would have been evidence it is 3× too large (§54).

57. Flagship Trace 2 — Ping-Pong Ownership

Illustrative. The consumer is slower than the producer — the condition §36 needs.

CycBank 0Bank 1ProducerConsumerNote
0FREEFREE
1PRODUCINGFREEclaims 0
20FULLFREEdone with 0
21FULLPRODUCINGclaims 1producer moves on
22CONSUMINGPRODUCINGfilling 1claims 0
40CONSUMINGFULLdone with 1reading 0producer wants a bank
41CONSUMINGFULLblockedreading 0correct — bank 0 is owned
55CONSUMINGFULLblockedstill reading 0consumer is slow
70FREEFULLreleases 0the consumer decides
71PRODUCINGFULLclaims 0
72PRODUCINGCONSUMINGfilling 0claims 1steady ping-pong

Five readings.

Cycles 41 to 70: the producer is blocked. It has finished bank 1 and cannot start bank 0 because the consumer owns it. That is correct behaviour and it is what a ping-pong costs when the consumer is slower.

Cycle 70: the consumer releases bank 0. §36's design has the producer free it at cycle 40 and start writing at 41 — corrupting the 30 cycles of reading the consumer had left.

The corruption in §36's version would be partial: cycles 41 to 70 of the consumer's read return new data. A tolerance check may pass.

And the bug is unreachable with a fast consumer. If the consumer had finished bank 0 by cycle 39, the producer's incorrect free at cycle 40 would have been harmless — which is why cp_pp_consumer_slower is a required coverage bin.

Each bank passes through all four states per cycle of the ping-pong, which is why observing the full state coverage needs at least three blocks.

58. Flagship Trace 3 — Recovery With Every Buffer Occupied

CycSkidIngressStagingReplayOutputLinkMust be true
100held5364operational
104held5364error
105held5364recoveryall hold
110held5364quiescingadmission stopped
140held5364retraining
150held5364re-baseliningreplay preserved, not cleared
160held5364recovered
165held5363operationaloutput drains to the engine
1804351operationalreplays resolving
2200000operationalall work completed

Six readings.

Cycle 105: every buffer holds. Skid, ingress, staging, replay and output — five buffers, eighteen items, all preserved. §51's design clears all eighteen here.

The output FIFO's four entries are the ones most easily overlooked. They were already delivered semantically (19.3 §30) — the far end will never send them again, so clearing them loses work permanently and silently.

Cycle 150 re-baselines the replay ring rather than clearing it (19.3 §50).

Cycle 165: the output FIFO drains during the recovery. Its consumer is the local protocol engine, which never stopped — so held does not mean frozen.

Cycle 220: everything completes. Eighteen items, no losses, no duplicates.

And admission was stopped from cycle 110 (19.3 §51), which is why nothing was added — holding and stopping are two different mechanisms and both are needed.

59. Debug Taxonomy

SignatureMost likely causeFirst instrument
Works at low load, corrupts at high§9 — occupancy drift, or §12 — full read as emptyoccupancy against pointer difference
Throughput about half the offered rate, no overflow§32 — one threshold, ready chatteringwm_assert_q against wm_dwell_q
Overflow exactly N items past full§28 — backpressure asserted at fullarrivals_since_watermark against headroom
Overflow appeared after the producer was deepened§29 — the headroom equation not re-derivedBP_ROUND_TRIP against the producer's actual depth
Every entry after some instant has the wrong metadata§16 — split RAMs with split pointersthe pop-side association check
One item lost at a stall boundary§19 — the skid's capture caseskid conservation count
Results nearly right, worse when the consumer is slow§36 — ping-pong swapped on the producer's wordbank ownership model
The Adapter stalls with the link not saturated§39 — replay depth sized on average trafficADP_REPLAY_FULL share; replay high-water mark
Receive capacity shrinks over time§41 — partial objects leaking slotsreasm_abandon_q; slots busy over time
Deadlock only at saturation§44 — a shared pool with no reservereserve_used_q; is the reserve non-zero?
Rare corruption only when the clocks differ§48 — a binary pointer crossed directlyare the CDC pointers Gray-coded?
Everything lost after a recovery§51 — a global flushwhat asserted at the recovery cycle
A buffer never exceeds a quarter of its depthover-sizing — not a bugocc_max_q against DEPTH (§54)

Row 4 is the one that catches teams out. Overflow appeared after the producer was deepened is §29's cross-block dependency, and the elaboration check in §14 is what turns it from a silicon bug into a build failure.

60. Debug Checklist

  1. Which buffer, and which of the seven kinds is it? (§4)
  2. What are its producer, consumer, accept event and release event? (§6)
  3. What is its occupancy, and does it match the pointer difference? (§10)
  4. Does the occupancy match a testbench push-minus-pop count? (§10)
  5. Is occupancy derived or maintained? (§7, §8)
  6. Is DEPTH a power of two, and does the wrap assume it is? (§13)
  7. Do the elaboration checks pass with the current parameters? (§14)
  8. Are payload and metadata written on one event at one index? (§15, §17)
  9. For a skid: did an item arrive on the exact cycle readiness fell? (§19)
  10. What is HIGH_WM, and what is the headroom it leaves? (§29)
  11. What is the backpressure round trip, including the producer's drain? (§27)
  12. How many items arrived after the watermark asserted? (§30)
  13. What is the gap between HIGH_WM and LOW_WM? (§31)
  14. How many watermark assertions, and what is the mean dwell? (§53)
  15. For a ping-pong: which bank does each side own, and who released last? (§35)
  16. Was a bank freed by the producer rather than the consumer? (§36)
  17. What is the replay high-water mark, and what caused the last full? (§38, §39)
  18. How many reassembly slots are busy, and for how long? (§40, §41)
  19. Have any partial objects been abandoned, and were their slots released? (§42)
  20. Is there a progress reserve, and is it non-zero? (§45)
  21. Has bulk traffic ever entered the reserve? (§46)
  22. For a CDC FIFO: are the pointers Gray-coded, and what is the clock ratio? (§47, §48)
  23. What happened to each buffer at the last recovery, against §50's table?
  24. Did any global flush signal assert? (§52)
  25. What is each buffer's high-water mark against its depth? (§53, §54)

61. Common Misconceptions

"A buffer stores bytes." It stores obligations. Every valid entry means somebody transferred ownership of work that must leave correctly — so overflow is not a performance event, it is an accepted obligation lost (§1).

"They are all FIFOs." Seven kinds with seven lifetimes and at least four different sizing rules. A skid is sized by the ready path, replay by a reliability window, reassembly by concurrent incomplete objects, and ping-pong by the phase structure — and only two of the seven are sized by burst absorption (§4).

"Deeper is safer." Beyond the latency requirement, depth adds queueing delay, area, verification state space, and — worst — it hides the moment congestion began, delaying the measurement that would have named the real constraint (§23, §24).

"A big enough FIFO fixes a slow consumer." No finite buffer bounds a queue whose arrival rate exceeds its service rate. Depth changes when it fills, never whether — a buffer converts a burst into a delay and cannot convert a rate deficit into anything (§25).

"Assert backpressure when full." By the time the producer sees the signal it has already launched round-trip-times-rate more items, and they arrive at a full buffer. The headroom is a computed quantity, not a safety margin (§27, §28).

"One threshold is enough." Occupancy oscillates around it, ready toggles every cycle, throughput halves with no overflow and no dominant stall reason — and across a die boundary a signal toggling every cycle is a physical problem as well as a performance one (§32).

"The producer knows when its old bank is free." It does not. The consumer releases the bank, and a producer that frees it on finishing overwrites data the consumer is still reading — partially, timing-dependently, and only when the consumer is slower, which is the case the ping-pong exists for (§36).

"Replay depth is a throughput parameter." It is a reliability window sized by the worst-case resolution latency, which includes a retransmission, a recovery and a degraded link — so a depth chosen from average traffic collapses exactly when reliability is being exercised (§38, §39).

"Accept fragments as they arrive." Accepting a first fragment is promising to assemble the whole object. Without reserving the object's requirement, the receiver ends up holding a partial object it can never complete, occupying a slot forever (§41).

"A shared pool maximises utilisation." It maximises utilisation and removes isolation. A streaming class fills every entry, a progress-critical completion cannot enter, and the streaming traffic cannot drain because draining needs what that completion would have released — a deadlock in which every block is correct (§44).

"Synchronising a pointer is enough for CDC." A binary pointer changes several bits at a carry, and the destination can sample a combination that was never written — making the FIFO look full when it is empty or empty when it is full. Gray coding removes the possibility rather than reducing it (§48).

"Flush everything on a recovery." Six of eight buffers should hold, replay should be preserved and re-baselined, and only the PHY-facing crossing may reasonably drain. A global flush additionally discards output-FIFO entries that were already delivered semantically — work the far end will never send again (§50, §51).

62. Understanding Check

63. Summary and What Comes Next

A buffer stores obligations, not bytes — so overflow is an accepted obligation lost, an early pop is one forgotten, and a wrong watermark is a promise the design cannot keep.

Seven kinds, seven lifetimes, four sizing rules. Only two of the seven are sized by burst absorption; the others are sized by a ready path, a reliability window, concurrent incomplete objects, and a phase structure.

Derive the occupancy. Two wide pointers and a subtraction remove the simultaneous-push-and-pop bug rather than detecting it, and one extra bit distinguishes full from empty more cheaply than a wasted entry.

Write payload and metadata on one event at one index. Two RAMs are fine; two pointers offset every subsequent entry the first time they disagree, permanently.

The high watermark is computed, not chosen — depth minus round trip times rate — and the producer's pipeline depth is an input to it, which is why the relationship belongs in an elaboration check.

Hysteresis is not optional. One threshold halves throughput with no overflow and toggles a control signal every cycle across a die boundary.

The consumer releases the bank. A producer that frees its old bank on finishing corrupts a slower consumer's data, partially and only under the load the structure exists for.

Replay depth is a worst-case reliability window including a retry, a recovery and a degraded link — and sizing it from average traffic is correct, slow, and invisible until reliability is exercised.

And a recovery flushes almost nothing. Six of eight buffers hold, replay is re-baselined, and a global flush additionally discards output entries that were already delivered and will never be sent again.

The buffers now have depths, thresholds and lifetimes. What decides whether an object may enter one of them at all is the credit machine — the accounting that tells this side how much room the far side has, how it learns about changes, and what happens when a return is delayed, lost, or crosses a clock boundary. The next chapter builds it in RTL.

Browse the full path on the UCIe tutorials index.