Skip to content

UCIe · Module 13

Buffer Management

How deep receive storage must be, how occupancy is tracked safely, and why physically free is not the same as allocatable — width-safe occupancy counters, reserved in-flight capacity, watermarks with hysteresis, sizing from burst size and feedback latency, partitioning and head-of-line blocking, entry leaks and double frees, and what a reset may legitimately clear.

Chapter 13.1 treated receive storage as a number: an advertised capacity, a depth, an occupancy. Every credit in it was backed by a slot, and the slots were assumed to exist, be counted correctly, and become free when the consumer drained them.

This chapter opens that up, and the first thing inside is a distinction that most designs get wrong exactly once and then never forget:

A receiver with four physically empty entries may have room for only one more object. The other three are already promised — to objects whose credits have been spent and which are, at this instant, somewhere on the wire. Advertising four is not optimism; it is an overflow scheduled for three round trips' time.

1. The One-Sentence Model

A buffer is not spare space. It is a timing contract — depth that exists to absorb the difference between arrival rate, service rate, and feedback latency.

Which is why depth is an architectural decision rather than an implementation detail, and why "make it bigger" and "make it smaller" are both answers to a question that has an actual number.

2. What This Chapter Owns

To be explicit about boundaries, because three earlier chapters have touched buffers.

Chapter 12.3 §20–21 built a datapath FIFO and the one-writer occupancy discipline. Chapter 5.5 §3 built the two-entry elastic buffer and the reason it needs two entries. Chapter 13.1 built the permission accounting that sits on top. None of that is re-derived here.

What this chapter owns:

  • occupancy counter width, and the off-by-one that makes a full buffer read as empty;
  • reserved in-flight capacity — the difference between free and allocatable, and the reason a receiver must advertise the smaller number;
  • watermarks and hysteresis, and why one threshold oscillates;
  • sizing from burst size and feedback latency, and why average rates are insufficient;
  • partitioning, and the head-of-line blocking it trades against;
  • entry leaks and double frees, and why they present as flow-control faults rather than data corruption;
  • and what a reset may legitimately clear, which depends on who owns the entries.

3. Sourcing

4. The Receive Buffer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE receive-buffer model. Basic FIFO mechanics are not re-taught —
// see Chapter 12.3 §20. Symbolic widths; no UCIe depth is asserted.
localparam int DEPTH  = 8;                    // symbolic
localparam int OCC_W  = $clog2(DEPTH + 1);    // NOTE the +1 — see §5
 
typedef struct packed {
  logic [DATA_W-1:0]   data;
  logic [META_W-1:0]   meta;      // framing metadata, opaque here
  logic [MON_ID_W-1:0] mon_id;    // VERIFICATION ONLY
} rx_entry_t;
 
rx_entry_t        fifo_q  [DEPTH];
logic [DEPTH-1:0] valid_q;        // per-entry validity — see §16
logic [OCC_W-1:0] occ_q;          // occupancy, the single source of truth

Architecture. Storage plus a count. The valid_q vector is present deliberately and it is redundant with occ_q — which sounds like the mistake Chapter 9.5 §4 warns against, and is not, for one reason: it is redundant on purpose, as a check. §6 asserts that the population count of valid_q equals occ_q, and that single property catches every accounting drift in the chapter without a testbench model. A design that keeps only one of them cannot make that check.

State. DEPTH entries with per-entry lifetime (push to pop), plus one occupancy counter.

Cycle behaviour. §6.

Contract. The producer relies on never being asked to push when full. The credit machinery of Chapter 13.1 relies on occupancy being the truth its advertisements are derived from.

Failure. §5 for the width, §6 for the counting, §16 for the leak.

DV. Every occupancy from empty to full; and the valid_q-versus-occ_q equality asserted continuously.

5. The Occupancy Counter's Width

The off-by-one that makes a full buffer read as empty.

An occupancy counter must represent 0 through DEPTH inclusive — that is DEPTH + 1 distinct values. So it needs $clog2(DEPTH + 1) bits, not $clog2(DEPTH).

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG for a power-of-two depth — and correct-looking for every other one,
// which is what makes it survive review.
logic [$clog2(DEPTH)-1:0] occ_q;

Why the bug is depth-dependent, which is the interesting part:

DEPTH$clog2(DEPTH)Values representableCan it hold DEPTH?
530–7yes — 5 fits, by luck
730–7yes — exactly
830–7no — 8 wraps to 0
1240–15yes
1640–15no — 16 wraps to 0

So it fails only at exact powers of two — which are precisely the depths designers choose. And the failure mode is the worst available: a full buffer's occupancy reads zero, so full is deasserted and empty is asserted. The buffer accepts a push it cannot hold, and the consumer is told there is nothing to drain.

The correct form, and the discipline that generalises:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam int OCC_W = $clog2(DEPTH + 1);
logic [OCC_W-1:0] occ_q;

Any counter representing a quantity from 0 to N inclusive needs $clog2(N+1) bits. That covers occupancy, credit counts, reservation counts, outstanding counts, and byte counters — Chapter 12.3 §12 made the same argument for a byte-valid count, and it is the same arithmetic.

And the DV consequence: a coverage bin range of [0 : DEPTH-1] passes on the broken design. The full bin must be DEPTH itself, and the sweep must be inclusive.

6. Push, Pop, and the Redundant Check Worth Keeping

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE occupancy maintenance. One writer, four enumerated cases —
// the discipline is Chapter 12.3 §20's; what is added here is the valid-vector
// cross-check in the assertion below.
wire push = rx_in_valid  && rx_in_ready;
wire pop  = rx_out_valid && rx_out_ready;
 
assign full  = (occ_q == OCC_W'(DEPTH));
assign empty = (occ_q == '0);
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    occ_q   <= '0;
    valid_q <= '0;
  end else begin
    unique case ({push, pop})
      2'b10  : occ_q <= occ_q + OCC_W'(1);
      2'b01  : occ_q <= occ_q - OCC_W'(1);
      2'b11  : occ_q <= occ_q;              // one in, one out — UNCHANGED
      default: occ_q <= occ_q;
    endcase
 
    // Per-entry validity, written from the same events so the two views cannot
    // diverge silently. The SET and CLEAR are separate bits, so they are safe
    // as separate statements — unlike the counter (§7).
    if (push) valid_q[wptr_q] <= 1'b1;
    if (pop)  valid_q[rptr_q] <= 1'b0;
  end
end

Architecture. Two representations of the same fact, updated from the same events. The redundancy is the point.

Cycle behaviour. The 2'b11 arm is unchanged, and it is the steady state of a buffer running at rate rather than a corner case. The valid_q updates are safe as separate if statements because they touch different bits — unless the buffer is empty, in which case pop cannot fire.

Contract. full gates the producer. empty gates the consumer. Both derive from occ_q, so occ_q must be right.

Failure. §7.

DV. The single most valuable property in this chapter, and it needs no model:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the counter and the valid vector must agree. Deliberately
// redundant: it catches every occupancy drift in the cycle it happens.
property p_occ_matches_valid_population;
  @(posedge clk) disable iff (!rst_n)
    occ_q == OCC_W'($countones(valid_q));
endproperty
a_occ_matches_valid_population: assert property (p_occ_matches_valid_population);

On practicality. $countones over a wide vector is a simulation and formal construct, not something to synthesise — and that is fine, because this is a verification property. Its value is that it requires no reference model at all, so it can be written on the first day and left in place for the project's life. It catches the double-assignment drift, a leaked entry, a double free, and a mis-indexed pointer, all from one expression.

7. Wrong RTL — Two Independent Occupancy Updates

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — two assignments to one register with no stated interaction.
always_ff @(posedge clk) begin
  if (push) occ_q <= occ_q + 1'b1;
  if (pop)  occ_q <= occ_q - 1'b1;
end

The mechanism is Chapter 12.3 §21's — the last assignment wins, so a simultaneous push and pop loses the increment and the count drifts downward. What is new here is the receive-buffer consequence, which differs from the datapath one.

In a receive buffer, downward drift means full deasserts while the buffer is genuinely full. Then:

  • the buffer accepts a push over an unread entry, so an object is overwritten — and because this is a receive buffer, the object was successfully transmitted, CRC-checked, and acknowledged. The far side believes it was delivered.
  • the occupancy the credit machinery reads is wrong, so the receiver advertises capacity it does not have (Chapter 13.1 §2's catastrophic direction);
  • and p_occ_matches_valid_population fires in the cycle it happens, which is the entire argument for keeping it.

Note the drift direction matters as much as the drift. Reverse the two statements and the count drifts upward, so full asserts early and the buffer refuses pushes it could accept — a throughput loss that destroys nothing. Same bug, opposite ordering, completely different severity, and neither is visible in a bound check because the count stays a small legal number throughout.

8. Free Is Not Allocatable

The chapter's central idea, and the one that has no counterpart in the earlier buffer material.

At a given instant a receive buffer has:

  • occupied entries — holding objects the consumer has not drained;
  • reserved entries — physically empty, but already promised to objects whose credits have been spent and which are in flight;
  • allocatable entries — genuinely uncommitted.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DEPTH  =  occupied  +  reserved_in_flight  +  allocatable

The receiver may advertise only allocatable. Advertising physically free space — DEPTH - occupied — double-counts the reserved entries, and the objects already in flight will arrive to find their slots given away.

A worked instance, because the arithmetic is where the intuition forms. DEPTH = 8:

QuantityValueMeaning
occupied2consumer has not drained these
reserved in flight3credits spent; objects on the wire
physically free68 - 2 — the tempting number
allocatable38 - 2 - 3 — the safe number

Advertise 6 and the sender may hold 6 credits while 3 objects are already committed — nine commitments against eight slots. The overflow occurs when the in-flight objects and the newly-permitted ones arrive together, which is one round trip later and under load, so it looks like a burst problem rather than an accounting one.

Physically empty is a statement about the past. Allocatable is a statement about the future, and it is the only one a credit may be issued against.

A remote sender transmits objects that are in flight and hold reservations against a receive buffer. The buffer's depth divides into occupied entries awaiting the consumer, reserved entries that are empty but promised to in-flight objects, and allocatable entries. The credit advertiser reads the allocatable count, and the consumer drains occupied entries.Remote senderspends credit, thentransmitsObjects in flighthold reservations,unseen hereOccupied entriesawaiting the consumerReserved entriesempty, alreadypromisedAllocatableentriesthe only advertisablecapacityCredit advertisermust read allocatableConsumerdrains, releasingentriesarrivesreservescount12
Figure 1 — one receive buffer with its depth divided three ways. Objects already in flight hold reservations against entries that are physically empty, so the entries a credit may be issued against are fewer than the entries that contain nothing. The advertiser must read the allocatable count and never the free count; the difference is the reservation, and it is invisible in the buffer itself because it is on the wire.

9. Reservation Accounting

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE reservation maintenance. Two counters, each with ONE writer and
// all four event cases enumerated, because both can move in the same cycle.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    occupied_q <= '0;
    reserved_q <= '0;
  end else begin
    // OCCUPANCY: arrival fills a slot; drain frees one.
    unique case ({obj_arrived, consumer_drained})
      2'b10  : occupied_q <= occupied_q + OCC_W'(1);
      2'b01  : occupied_q <= occupied_q - OCC_W'(1);
      2'b11  : occupied_q <= occupied_q;
      default: occupied_q <= occupied_q;
    endcase
 
    // RESERVATION: issuing a credit creates one; an arrival consumes one,
    // because the object has moved from "promised" to "present".
    unique case ({credit_issued, obj_arrived})
      2'b10  : reserved_q <= reserved_q + OCC_W'(1);
      2'b01  : reserved_q <= reserved_q - OCC_W'(1);
      2'b11  : reserved_q <= reserved_q;      // issued one, consumed one
      default: reserved_q <= reserved_q;
    endcase
  end
end

Architecture. Two counters tracking two different commitments against one physical resource. The key structural insight is in the second block: obj_arrived decrements reserved_q and increments occupied_q in the same cycle. The commitment does not disappear — it changes form, from a promise to an occupancy. That is why the sum is the invariant and neither term is.

State. Two counters with different lifetimes. occupied_q has per-entry lifetime in aggregate. reserved_q has per-credit-in-flight lifetime — from advertisement to arrival, which is a link round trip.

Cycle behaviour. Four cases each, and the interesting arm is the second block's 2'b11: a credit issued in the same cycle another object arrives leaves the reservation count unchanged, and getting that wrong drifts the reservation permanently.

Contract. The credit advertiser reads allocatable and nothing else. The buffer's full logic reads occupied_q.

Failure. §10 for advertising the wrong number. Losing a reserved_q decrement means the count climbs forever and allocatable falls to zero permanently — a receiver that stops advertising while its buffer is empty, which presents as Chapter 13.1 §22's leakage signature but originates here.

DV. Every combination of the two event pairs, including both 2'b11 arms in the same cycle; and the invariant of §10 asserted continuously.

10. Wrong RTL — Advertising Physical Free Space

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the receiver advertises what is empty rather than what is uncommitted.
assign credits_to_advertise = OCC_W'(DEPTH) - occupied_q;

Architecture. It answers the wrong question. DEPTH - occupied is how many entries contain nothing, and the credit machinery needs how many entries can be promised.

Cycle behaviour. Correct whenever nothing is in flight — which is to say, whenever the link is idle. The bug is invisible at low load and appears exactly under sustained traffic, which is the load profile it will actually see.

Failure. §8's worked instance: with 2 occupied and 3 in flight, it advertises 6 against 3 genuinely available. The sender is authorised to create nine commitments against eight slots.

And the timing of the overflow is what makes it hard. The excess credits are advertised now; they are spent over the following cycles; those objects arrive a round trip later, at the same time as the three already in flight. The overflow is displaced from its cause by a full round trip, so the waveform shows a burst arriving at a buffer that was recently nearly empty, and the natural conclusion is that the burst was too large.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — advertise the allocatable count, which already subtracts the
// reservations (§8).
assign credits_to_advertise = allocatable;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the invariant that makes over-advertisement impossible rather
// than unlikely. This is the chapter's most important property.
property p_commitments_within_capacity;
  @(posedge clk) disable iff (!rst_n)
    ({1'b0, occupied_q} + {1'b0, reserved_q}) <= (OCC_W+1)'(DEPTH);
endproperty
a_commitments_within_capacity: assert property (p_commitments_within_capacity);
 
// Illustrative — and a credit is never issued against capacity that is already
// committed. Catches the wrong advertisement at the moment of issue rather
// than a round trip later at the overflow.
property p_no_credit_beyond_allocatable;
  @(posedge clk) disable iff (!rst_n)
    credit_issued |-> (allocatable != '0);
endproperty
a_no_credit_beyond_allocatable: assert property (p_no_credit_beyond_allocatable);

p_no_credit_beyond_allocatable is worth more than the overflow assertion, and the reason is diagnostic distance. An overflow assertion fires a round trip after the mistake, at a buffer that looks momentarily busy. This one fires in the cycle the wrong credit is issued, naming the mechanism.

11. Watermarks, and Why One Threshold Is Not Enough

full is a hard limit and a poor control signal. By the time it asserts, a pipelined producer has already launched objects that have nowhere to go — which is Chapter 13.3's subject. So designs assert backpressure early, at a watermark.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — a single threshold. Occupancy hovering at the boundary makes the
// backpressure signal toggle every cycle.
assign almost_full = (occ_q >= OCC_W'(HIGH_WATERMARK));

The failure is oscillation. With a producer and consumer running at similar rates, occupancy sits near the watermark and crosses it repeatedly. almost_full toggles every cycle or two, and:

  • the upstream stage starts and stops continuously, so its own pipeline never fills — throughput below what either rate would predict;
  • the toggling signal has a wide fan-out and now has a duty cycle that makes its timing worse than a stable one;
  • and in a multi-stage pipeline the oscillation propagates, so several stages toggle in a pattern that is very hard to read in a waveform.

Hysteresis fixes it with one bit of state:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE watermark hysteresis. Assert at HIGH, release at LOW, and hold
// in between — so occupancy must actually move a meaningful distance before
// the control signal changes.
localparam int HIGH_WM = 6;    // symbolic
localparam int LOW_WM  = 3;    // symbolic
 
logic backpressure_q;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    backpressure_q <= 1'b0;
  end else if (occ_q >= OCC_W'(HIGH_WM)) begin
    backpressure_q <= 1'b1;
  end else if (occ_q <= OCC_W'(LOW_WM)) begin
    backpressure_q <= 1'b0;
  end
  // Between LOW and HIGH: HOLD. That gap is the hysteresis.
end

Architecture. A one-bit state machine over occupancy. The gap between the thresholds is the design parameter, and it buys stability at the cost of some average occupancy.

State. One flop, per-buffer lifetime.

Cycle behaviour. Asserts at or above HIGH, releases at or below LOW, holds between. Note this is a latching structure and the else if ordering matters: with HIGH_WM > LOW_WM the two conditions are mutually exclusive, so the order is safe — but §12 asserts that relationship rather than trusting it.

Contract. Upstream relies on backpressure being stable enough to act on. The hard full protection must remain independent of this — a watermark is a performance mechanism and must never be the only thing preventing overflow.

Failure. Setting LOW_WM too close to HIGH_WM reintroduces the oscillation. Setting it too low leaves the buffer under-utilised, because backpressure persists long after the pressure has gone. And swapping them silently inverts the logic into something that latches on and never releases — which §12 catches at elaboration.

DV. Drive occupancy slowly across both thresholds in both directions; hold it exactly at each threshold; and drive it oscillating within the gap, verifying backpressure_q does not move.

12. Watermark Ordering, Checked Rather Than Assumed

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the relationship the hysteresis depends on, checked at
// elaboration so a bad parameter set fails the build rather than the silicon.
initial begin
  assert (LOW_WM < HIGH_WM)
    else $fatal(1, "LOW_WM must be below HIGH_WM or backpressure latches on");
  assert (HIGH_WM <= DEPTH)
    else $fatal(1, "HIGH_WM above DEPTH: backpressure can never assert");
  assert (HIGH_WM > 0)
    else $fatal(1, "HIGH_WM of zero asserts backpressure permanently");
end

Why an elaboration assertion rather than a runtime one. These are relationships between parameters, so they cannot change at runtime and a runtime property would waste simulation checking a constant. Failing at elaboration puts the error where the mistake was made — in the parameter file — rather than in a waveform.

And HIGH_WM <= DEPTH is the one people omit. A watermark above the depth can never be reached, so backpressure never asserts, and the buffer relies entirely on hard full protection — which works, at the cost of every problem §11 exists to prevent. It fails silently as a performance bug.

13. Sizing — the Inputs

There is no default depth. There is a calculation with six inputs, and a design that cannot name all six has not sized its buffer.

InputWhat it isEffect on depth
Peak arrival rateobjects per cycle the producer can deliver at burstlinear
Service rateobjects per cycle the consumer drains sustainablythe deficit is what accumulates
Feedback latencycycles from "buffer is filling" to "producer has stopped"linear, and usually dominant
Burst sizehow many objects can arrive back-to-back before the rate dropslinear
Arbitration and CDC uncertaintyscheduling jitter and clock-crossing delayadditive margin
Replay effectsretransmitted objects re-occupy storage (Ch 9.4)additive under error

The steady-state condition is necessary and grossly insufficient. If the service rate equals or exceeds the average arrival rate, the buffer does not grow on average. It can still overflow, because the average says nothing about the transient, which is §14.

And feedback latency is the term that surprises people, because it is not a property of the buffer at all — it is a property of the control path around it. A deeper buffer with slow feedback overflows where a shallower buffer with fast feedback does not.

14. Burst Absorption, Worked

The arithmetic that shows why average rates are insufficient.

The scenario. A producer can burst 8 objects back-to-back. The consumer drains 1 per cycle. Feedback — the delay from occupancy rising to the producer stopping — is 4 cycles.

CycleArrivalsDrainsOccupancyNote
0101burst begins
1111drain starts
2111steady, briefly
3111
4111producer would stop here if told at cycle 0
5111but the burst continues
6111
7111burst ends

With a matched rate the occupancy never exceeds one — which is the misleading case. Now make the burst genuinely faster than the drain: 2 arrivals per cycle for 8 cycles, draining 1 per cycle:

CycleArrivalsDrainsOccupancyNote
0202
1213net +1 per cycle
2214
3215
4216watermark at 6 asserts now
5217feedback in flight — producer has not stopped
6218DEPTH reached
7219OVERFLOW — 4-cycle feedback, only 2 slots of headroom

Two readings, and they are the section's whole point.

The overflow is not a FIFO bug. The counter was right, the pointers were right, the watermark asserted at exactly the configured threshold. The depth and the watermark were wrong for the feedback latency, and the arithmetic that would have prevented it is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
headroom_needed  ≳  (arrival_rate − service_rate) × feedback_latency

Here: (2 − 1) × 4 = 4 slots of headroom above the watermark, and only 2 existed. Either the watermark drops to 4, or the depth rises to 10.

And the second reading is that this is the local analogue of Chapter 13.1's bandwidth-delay product. There, credits had to cover a round trip. Here, headroom has to cover a feedback latency. Same shape, different loop — and Chapter 13.3 generalises it to a multi-stage pipeline.

15. Partitioning and Head-of-Line Blocking

A depth question becomes a structure question the moment more than one class of traffic shares the buffer.

StructureUtilisationIsolationFailure
Single shared FIFObestnonehead-of-line blocking — a blocked head stalls everything behind it
Per-class queuesworst — a class's idle depth is strandedcompletedepth idle while another class overflows
Shared storage, per-class linked listsgoodgoodmore control state; harder to verify
Reserved minimum plus shared remaindergoodboundedthe reservation must be sized

Head-of-line blocking, precisely. A single FIFO holds objects for classes A and B. The head is an A object whose consumer is stalled. Every B object behind it is blocked, even though B's consumer is idle and ready. Occupancy rises, the watermark asserts, and backpressure reaches both sources — so B is throttled by A's problem.

And the important framing: this is a queue-structure consequence, not a scheduler bug. No arbitration policy fixes it, because the objects are already committed to one ordered structure. The fix is structural — separate the classes — and it costs state.

Why it connects to Chapter 13.1 §13. If the carried protocol distinguishes channels that must make progress from channels that may block — as CXL.cache's pre-allocated response and data channels do (Chapter 11.3 §28) — then a single shared receive FIFO can block the must-progress channel behind a blocked one. The buffer structure has to preserve the protocol's guarantee, and a shared FIFO does not.

16. Overcommit, Leak, and Double Free

Three failures that all present as flow-control faults rather than as data corruption, which is why they are hard to attribute.

Overcommit — logical commitments exceed physical storage. §10's cause, and note it can exist while occupancy is low: the commitments are in flight. The signature is an overflow arriving at a buffer that was recently nearly empty.

Leak — an entry's validity is never cleared after the consumer drains it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the consumer's drain frees the data path but not the bookkeeping.
always_ff @(posedge clk) begin
  if (consumer_takes) begin
    rx_out_data <= fifo_q[rptr_q].data;
    rptr_q      <= rptr_q + 1'b1;
    // valid_q[rptr_q] and occ_q not updated — the entry is gone and still counted
  end
end

The signature is distinctive and it is the best diagnostic in the chapter: occupancy never falls, so allocatable goes to zero and stays there, so credit returns stop, so the sender starves — while the buffer is physically empty. Chapter 13.1 §22's second row is this failure seen from the transmit side, and the way to tell them apart is to look at the receiver: a credit leak in the return path leaves occupancy correct, and a buffer leak leaves occupancy stuck high.

Double free — an entry is released twice, or one release generates two credit returns. Physical storage looks healthy; the accounting diverges upward, so the sender eventually believes in capacity that does not exist and the receiver overflows much later. p_occ_matches_valid_population catches the local half in the cycle it happens; Chapter 13.1 §8's capacity bound catches the credit half.

All three are invisible to a data-integrity check. Every byte that arrives is correct. What is wrong is the count of how many more may arrive, which is why these chapters' scoreboards count resources rather than bytes.

17. What a Reset May Legitimately Clear

A receive buffer's entries are not all owned by the same layer, and a reset that clears all of them is making a decision it may not be entitled to make.

Entry classOwned byMay a link reset discard it?
Transport staging — framed, not yet validatedthe Adapteryes — it will be replayed or is meaningless
Validated, not yet delivered upwardthe Adapter, on behalf of the Protocol Layerno — this is a complete object that was acknowledged
Delivered, awaiting the consumerthe Protocol Layerno — the semantic layer owns it (12.4 §25)
Replay-owned copiesthe sender's Adapternot this buffer's business

The middle two rows are why "flush the FIFO on recovery" is wrong. A validated object in the receive buffer has been CRC-checked and, depending on the acknowledgement scheme, may already have been acknowledged to the far side — which means the far side has retired its replay copy. Discarding it locally loses it permanently, with the transmitter believing it was delivered.

And the reset scope taxonomy applies (Chapter 12.4 §25): a local logic reset may clear pointers and staging; a link reset may clear link-epoch state and un-validated staging; only an explicit, reported semantic abort may discard delivered objects.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — a reset that clears only what it owns, with the scope explicit
// rather than implied by which always_ff block the signal happens to reach.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    occ_q      <= '0;          // full reset: everything goes
    valid_q    <= '0;
    reserved_q <= '0;
  end else if (link_reset_event) begin
    // Link-epoch scope: reservations are void because credits are re-advertised
    // (Ch 13.1 §15). Validated entries are NOT touched.
    reserved_q <= '0;
    // occ_q and valid_q deliberately absent — see the table above.
  end
end

Note what is absent from the link_reset_event branch, and that the comment says so. Chapter 11.5 §16 made the argument that a recovery block's omissions are its most important content and the most likely future edit; the comment exists so the next reader knows the absence is deliberate.

18. Occupancy Diagnostics

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE diagnostics. Not a UCIe mechanism. Diagnostic lifetime: these
// survive link recovery and clear only on a broad deliberate reset.
logic [OCC_W-1:0] occ_high_water_q;      // peak occupancy ever reached
logic [OCC_W-1:0] alloc_low_water_q;     // minimum allocatable ever reached
logic [31:0]      full_cycles_q;         // saturating
logic [31:0]      bp_assert_count_q;     // watermark assertions — oscillation tell
logic [31:0]      oldest_age_q;          // age of the head entry, saturating
logic             overflow_attempt_q;    // sticky: a push while full was seen

Architecture. Six values, each answering one question a post-silicon investigation asks.

occ_high_water_q is the sizing evidence. If a buffer never exceeds half its depth across a full regression, it is over-provisioned; if it reaches DEPTH regularly, the margin is gone. This single number is worth more for sizing than any calculation, because it measures the system rather than the model.

bp_assert_count_q is the oscillation tell. A watermark that asserts thousands of times in a short window is §11's hysteresis failure, and the count makes it visible without a waveform.

oldest_age_q is the head-of-line evidence. A head entry aging while the buffer is not full means the head's consumer is stalled and everything behind it is blocked — §15's signature.

State. Diagnostic lifetime throughout, and occ_high_water_q in particular must survive a recovery: the peak reached before the recovery is precisely what a post-recovery investigation needs.

Failure. Clearing these on the event under investigation destroys the evidence. Saturating without a sticky companion flag makes a saturated counter indistinguishable from a stopped one.

19. The Buffer Scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
JOIN KEY — the verification-only monitor tag, because an object may be
retransmitted and no protocol field says so.
 
PHYSICAL MODEL
  valid_population   — counted from observed pushes and pops, NOT from occ_q
  entry_order[]      — FIFO semantics: the pop sequence must match the push
                       sequence, per class if partitioned
  per entry: pushed_at, popped_at, mon_id
 
COMMITMENT MODEL
  occupied           — arrivals minus drains, independently counted
  reserved_inflight  — credits issued minus objects arrived
  advertised         — the sum of credits the receiver has issued this epoch
 
DIAGNOSTIC
  occ_high_water, alloc_low_water, overflow_attempts, bp_assertions,
  max_head_age

The six checks, and what each catches.

occ_q equals the independently counted valid population. The model's version of §6's assertion, and it catches every drift. Counting from interface observations rather than from the design's counter is the point — a drifting counter must appear as a disagreement, not be adopted as truth.

occupied + reserved_inflight <= DEPTH, every cycle. The commitment invariant of §10, checked outside the design. It catches over-advertisement at the moment of the bad credit rather than at the overflow a round trip later.

Every issued credit is eventually matched by exactly one arrival. Catches a lost reserved_q decrement — the count climbing until allocatable sticks at zero — and catches a duplicate arrival being counted twice.

The pop sequence matches the push sequence, per class. FIFO semantics. Catches a mis-indexed pointer, which $countones cannot see because the population stays right.

Every pushed entry is eventually popped, or explicitly discarded by a scope-legal reset. The leak check of §16, and it fires at end of test — which is why §18's oldest_age_q matters as an earlier signal.

And no push occurred while full without a simultaneous pop. The hard-protection check. Note the "without a simultaneous pop" clause: a full buffer accepting in the same cycle it drains is legal in a design built for it, and an assertion that forbids it is over-restrictive and gets waived.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the hard protection, written to permit the legal same-cycle case.
property p_no_push_when_full;
  @(posedge clk) disable iff (!rst_n)
    (push && full) |-> pop;
endproperty
a_no_push_when_full: assert property (p_no_push_when_full);
 
// Illustrative — and the mirror, likewise permitting the legal bypass case if
// the design implements one. If it does NOT, drop the `|-> push` and forbid it
// outright — but choose deliberately rather than copying this.
property p_no_pop_when_empty;
  @(posedge clk) disable iff (!rst_n)
    (pop && empty) |-> push;
endproperty

20. Coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative buffer coverage. Not UCIe-defined. Every bin reaches a failure
// named in this chapter.
covergroup cg_buffer @(posedge clk iff buf_event);
 
  // Occupancy histogram — note `full` must be DEPTH itself (§5).
  cp_occ       : coverpoint occ_q {
    bins empty = {0}; bins one = {1}; bins low = {[2:LOW_WM]};
    bins mid = {[LOW_WM+1:HIGH_WM-1]}; bins high = {[HIGH_WM:DEPTH-1]};
    bins full = {DEPTH};
  }
  cp_alloc     : coverpoint allocatable { bins zero = {0}; bins one = {1}; bins some = {[2:$]}; }
  cp_reserved  : coverpoint reserved_q { bins none = {0}; bins some = {[1:$-1]}; bins all = {DEPTH}; }
  cp_event     : coverpoint {push, pop} {
    bins idle = {2'b00}; bins push_only = {2'b10}; bins pop_only = {2'b01}; bins both = {2'b11};
  }
  cp_bp        : coverpoint backpressure_q;
  cp_bp_edge   : coverpoint backpressure_edge;      // §11 — oscillation
  cp_class     : coverpoint head_entry_class;       // §15
  cp_head_age  : coverpoint head_age_bucket;        // §15 — HOL evidence
  cp_reset     : coverpoint reset_scope_seen;       // §17
  cp_overflow  : coverpoint overflow_attempt_q;     // must remain ZERO
 
  // Push and pop simultaneously at every occupancy, especially full and empty.
  x_event_occ    : cross cp_event, cp_occ;
  // Occupancy empty while allocatable is zero — the §16 leak signature, and a
  // combination that should be UNREACHABLE in a correct design.
  x_empty_noalloc : cross cp_occ, cp_alloc;
  // Reservations outstanding at every occupancy — §8's arithmetic exercised.
  x_res_occ      : cross cp_reserved, cp_occ;
  // Occupancy oscillating within the hysteresis gap — §11's stability.
  x_bp_mid       : cross cp_bp_edge, cp_occ;
  // A blocked head with the buffer not full — §15's head-of-line case.
  x_hol          : cross cp_head_age, cp_occ;
 
endgroup

Three notes on the bins.

cp_occ's full bin is DEPTH and not DEPTH-1. §5's bug makes a full buffer read as zero, so a bin range stopping at DEPTH-1 never observes the full state at all and passes on the broken design.

x_empty_noalloc should be unreachable. Occupancy empty with zero allocatable means every slot is reserved with nothing occupying it — possible transiently in a deep-in-flight regime, and a persistent hit is §16's leak. Covering it is how the leak becomes visible.

And cp_overflow must stay at zero. Writing a bin whose value is that it never fills converts an assumption into a checked fact.

21. Flagship Trace — Free, Reserved, and Allocatable

DEPTH = 4. Illustrative link round trip of 3 cycles. This trace exists to make §8 concrete, so it tracks all three quantities separately.

CycOccupiedReservedFree (D−occ)AllocatableCredit issued?Event
00044idle; all four advertisable
10143yesone credit issued → one reservation
20242yessecond credit issued
30242nonothing arrived yet — objects in flight
41132nofirst object arrives: reserved → occupied
52022nosecond arrives; no reservations left
62121yesthird credit issued
72220yesfourth issued — fully committed
82220deniedfree = 2, allocatable = 0 — the whole point
91231consumer drains one
101231yesthe drain released one commitment
112121nothird object arrives
123011nofourth arrives; 3 occupied, 1 free

Five things to read off it.

Row 8 is the section. Two entries are physically free and zero are allocatable, because both free slots are promised to objects in flight. A receiver advertising DEPTH − occupied would issue two more credits here, producing six commitments against four slots.

Row 4 shows the commitment changing form, not disappearing. reserved falls and occupied rises in the same cycle. The sum is 2 before and after — which is why §9's second unique case has a 2'b11 arm and why the sum is the invariant.

Rows 1–3 have zero occupancy and non-zero commitment. For three cycles the buffer is completely empty and cannot accept unlimited traffic. A design whose only protection is full has no protection at all in this window.

Row 9's drain releases a commitment that row 10 immediately re-issues. That is the steady state of a buffer running at rate: allocatable hovers near zero and the credit machinery is the only thing preventing overflow.

And the invariant holds at every row. Check row 7: occupied 2 + reserved 2 = 4 = DEPTH, allocatable 0. Check row 12: 3 + 0 = 3 <= 4, allocatable 1. Every row satisfies occupied + reserved <= DEPTH, which is §10's property checked by hand.

22. Debug Taxonomy

SymptomDiagnosisFirst evidence to pull
Buffer full and the sender is still transmittingover-advertisement — §10, or a credit-side bound failure§19's commitment invariant; the credit-issue assertion
Buffer empty and the sender has zero credits§16's leak if occupancy is stuck high; a credit-return leak if occupancy is correctoccupancy versus valid population
Occupancy counter disagrees with the valid vector§7's drift, a leak, a double free, or a mis-indexed pointer§6's $countones assertion — it fires in the cycle
Backpressure toggles every cycle or two§11 — single threshold, or a hysteresis gap that is too narrow§18's bp_assert_count_q
Backpressure never asserts§12 — HIGH_WM above DEPTHthe elaboration assertion
One traffic class stalls while others progress§15 — head-of-line blocking in a shared FIFO§18's oldest_age_q with occupancy below full
Overflow only during bursts§14 — depth and watermark not sized for the feedback latencyoccupancy high-water mark and the feedback delay
Overflow at a buffer that was recently nearly empty§10 displaced by a round trip — not a burst problemthe reservation count at the time of the bad advertisement
Objects lost after a recovery§17 — a reset cleared entries it did not ownwhich reset scope asserted, and what it touched
Occupancy never exceeds half depth in any testover-provisioned, or the stimulus never burstshigh-water mark across the whole regression

The eighth row is the one worth memorising. An overflow at a buffer that was recently nearly empty is almost never a burst that was too large — it is credits issued against reserved capacity a round trip earlier. The cause is displaced from the symptom by the round trip, and looking at the burst is looking at the consequence.

23. Debug Checklist

  1. What is the physical depth, and is OCC_W equal to $clog2(DEPTH+1)? §5 — check this first, it takes ten seconds and explains a whole class of failure.
  2. What is the actual valid-entry population? Counted, not read from the counter.
  3. What does the occupancy counter say? And do the two agree? §6.
  4. How many reservations are outstanding? §8 — and does the design track them at all?
  5. What is the true allocatable count? DEPTH − occupied − reserved, computed at sufficient width.
  6. What number is actually being advertised? §10 — free, or allocatable?
  7. What is the watermark state, and how many times has it changed? §11, §18.
  8. Are LOW_WM < HIGH_WM <= DEPTH? §12.
  9. Did a push and a pop occur in the same cycle? §6's 2'b11 arm.
  10. Did a full buffer accept a push, and was there a simultaneous pop? §19's property permits one and forbids the other.
  11. Is any entry's validity stuck? §16 — occupancy that never falls.
  12. Was any entry freed twice? §16 — accounting climbing while storage looks fine.
  13. Is one class blocking others? §15 — head age with occupancy below full.
  14. Is the buffer simply too shallow for the feedback latency? §14's arithmetic, with the measured latency.
  15. Did a reset clear entries it did not own? §17's table.
  16. Does occupied + reserved <= DEPTH hold at every cycle? §19 — the invariant that makes over-advertisement impossible rather than unlikely.

24. Common Misconceptions

"Free entries equal available credits." Physically empty is a statement about the past; allocatable is a statement about the future. With entries already promised to objects in flight, a buffer can have four free entries and room for one more object — and advertising four schedules an overflow a round trip out (§8, §10).

"Average rate determines the required depth." The average determines whether the buffer grows without bound. The transient determines whether it overflows, and the term that dominates is usually feedback latency, which is a property of the control path rather than of the buffer (§13, §14).

"An occupancy counter needs $clog2(DEPTH) bits." It must represent DEPTH+1 distinct values. At exactly the power-of-two depths designers prefer, the short version makes a full buffer read as zero occupancy — so full deasserts and empty asserts simultaneously (§5).

"Almost-full is the same as full." A watermark is a performance mechanism that stops work early enough for the feedback to arrive. Hard full protection must remain independent of it, and a watermark above the depth silently disables backpressure entirely (§11, §12).

"One threshold is enough." Occupancy hovering at a single threshold makes the backpressure signal toggle every cycle or two, so the upstream pipeline never fills, the signal's timing worsens, and the oscillation propagates through the stages. Hysteresis costs one flop (§11).

"Shared buffering can never hurt utilisation." It maximises utilisation and eliminates isolation. A blocked head stalls every object behind it regardless of class, and where the carried protocol distinguishes must-progress channels from may-block channels, a shared FIFO breaks that guarantee (§15).

"Valid bits and occupancy cannot diverge." They diverge on a lost counter update, a leaked entry, a double free, and a mis-indexed pointer. That is exactly why keeping both and asserting their equality is worth the redundancy — it is the cheapest high-value check in the chapter and needs no reference model (§6).

"Buffer overflow always means the FIFO RTL is broken." In §14's worked case the counter, the pointers, and the watermark were all correct. The depth and the watermark were wrong for the feedback latency, which is an architectural error that no amount of FIFO review finds (§14).

"Resetting the FIFO fixes flow control safely." A validated object in the receive buffer may already have been acknowledged, which means the far side has retired its replay copy. Discarding it locally loses it permanently while the transmitter believes it was delivered (§17).

"A leak and a credit-return failure look the same." They present identically from the transmit side — sender starved, receiver apparently idle — and they are distinguished at the receiver: a buffer leak leaves occupancy stuck high, while a return-path leak leaves occupancy correct (§16).

25. Understanding Check

26. Summary and What Comes Next

A buffer is not spare space. It is a timing contract — depth that exists to absorb the difference between arrival rate, service rate, and feedback latency.

The idea the chapter exists for: physically empty is a statement about the past, and allocatable is a statement about the future. DEPTH = occupied + reserved_in_flight + allocatable, the receiver may advertise only the third, and it can know the second because it issued the credits that created it. Advertising free space instead schedules an overflow one round trip out, displaced far enough from its cause to look like a burst problem.

The mechanisms: $clog2(DEPTH+1) bits, because the short version makes a full buffer read as empty at exactly the power-of-two depths designers pick. One writer per counter with all four event cases, because the simultaneous arm is the steady state rather than a corner. A redundant valid vector asserted against the counter, which is the cheapest high-value check available and needs no model. Two watermarks with hysteresis, because one threshold oscillates and the ordering should fail at elaboration rather than in silicon. Headroom sized from the feedback latency, not from the average rate. And a reset that clears only what it owns, with the omissions commented.

The three failures that all look like flow-control faults and none of which a data check sees: overcommit, which can exist while occupancy is low; a leak, whose signature is occupancy that never falls while the buffer is empty; and a double free, which leaves storage healthy and the accounting drifting upward.

The receiver now knows exactly how much storage is safe to promise, and how early to start refusing work. What remains is the hardest part of the mechanism: that information lives at the receiving end of a pipeline, and the producer that must act on it is several registered stages away — so stopping in time means starting to stop before the buffer is actually in trouble, without coupling the whole system into one combinational path:

Browse the full path on the UCIe tutorials index.