PCIe · Module 16
Completion Credits — The Return Path Has Its Own Budget
The NPH a read spent going out buys nothing coming back. Completions are charged to CPLH and CPLD on the opposite direction — and whether that pool can ever run dry depends on what the receiving port is.
Chapter 16.3 left one thing deliberately open. A Memory Read costs 1 NPH to send, and that credit buys the request a place in the Completer's receive buffer.
It buys nothing at all for the data coming back.
The Completion is a separate packet, travelling the other direction, into a different port's receive buffers, charged against a different pool that the Requester advertised. The forward and return paths share a transaction and share no resources whatsoever.
What do CPLH and CPLD represent, why must a Completer check them before it can answer a request it has already accepted, and under what conditions can this pool run dry at all?
1. The Verified Costs
2. Two Directions, Two Independent Contracts
The single most useful diagram in this chapter is a pair of arrows that do not touch.
FORWARD Requester → Completer
Requester spends NPH advertised by the Completer
Memory Read Request lands in the Completer's NP receive buffer
RETURN Completer → Requester
Completer spends CPLH + CPLD advertised by the REQUESTER
CplD lands in the Requester's Completion receive bufferEach Link direction maintains its own full set of six pools. A port is simultaneously a transmitter (spending what its partner advertised) and a receiver (advertising what its partner may spend). Twelve counters exist across one Link, and the six that a port advertises are not the six it spends.
3. Completion Credit Is Not the Requester's Context
Chapter 16.3 §4 distinguished NPH from the Tag pool. The same distinction applies again here, and now there are three resources, not two.
| CPLH / CPLD | Requester context / Tag | |
|---|---|---|
| Whose resource | the Requester's receive buffer | the Requester's correlation table |
| Advertised to | the Completer | nobody — internal |
| Spent by | the Completer | the Requester |
| Freed when | the Requester drains the Completion buffer | the last Completion arrives |
| Runs out because | the Completer is answering faster than you drain | you have too many reads outstanding |
Both belong to the Requester, and they are still different. One is buffer space the far side is permitted to fill; the other is bookkeeping state that waits for the answer.
4. Completion Credit Is Not the Reassembly Buffer
A subtler conflation, and one this chapter must refuse to make.
Chapters 12.1 and 13.3 described how a Requester assembles returned data — matching Completions to a context, tracking byte counts, delivering a result upward.
Flow-control credit does not describe that architecture. CPLD is advertised Link receive capacity: how much Completion payload the port will accept onto the Link before it needs the sender to wait.
What happens to the data after it is received is implementation-defined. It may go straight into a reassembly buffer; it may be forwarded; it may be written to memory immediately. PCIe does not specify the mapping, and this chapter does not invent one.
Do not teach that 1 CPLD credit equals one reassembly-buffer entry.
Why it matters practically. A design that sized its advertised CPLD equal to its reassembly buffer has coupled a protocol-visible advertisement to an internal structure, and any later change to the internal structure silently changes what the far side is permitted to send. They are related by design choice, not by protocol.
The one thing that is true: a port must not advertise capacity it cannot actually absorb. That is a lower bound on the relationship, not an equality, and it is the only claim this chapter makes about it.
5. Cpl and CplD Cost Differently
From §1's table, mapped onto Chapter 13.1's two Completion forms:
| Form | Carries data | Cost |
|---|---|---|
Cpl — Completion without Data | no | 1 CPLH |
CplD — Completion with Data | yes | 1 CPLH + n CPLD |
The header credit is a constant — one — for both, exactly as PH and NPH were (Chapter 16.1 §5). The unit is sized for the largest header, so a Completion header always costs one.
The data credit is the ceiling division again, and §1's table shows two shapes of it:
- Memory Read Completion:
nCPLD, scaling with the payload actually returned. - I/O and Configuration Read Completion, and AtomicOp Completion: exactly 1 CPLD — because the source states the returned data is bounded (never more than four aligned DWs for an AtomicOp Completion), which fits inside a single 16-byte credit.
6. Split Completions Pay Per Packet
Chapter 13.3 established that one Memory Read may be answered by several Completions.
Each of them is a separate TLP and pays separately.
one 256-byte Memory Read Request
costs 1 NPH going out
answered by four 64-byte CplD packets
costs 4 × (1 CPLH + 4 CPLD) = 4 CPLH + 16 CPLD coming back7. Infinite Completion Credit — Verified, and Conditional
This is the chapter's most valuable finding, and it is precisely the kind of claim the normative gate exists to protect.
The intuitive worry is real: Completions are obligatory return traffic. Something is already waiting for them. If Completion capacity could be exhausted, a Requester could be blocked waiting for data that the Completer cannot send — and if the Requester's ability to drain that data itself depended on making progress, the situation would be self-sustaining.
PCIe's answer, from §1's Table 6, is not "Completion credits never run out". It is conditional on what the receiving port is:
| Receiving port | CPLH minimum | CPLD minimum |
|---|---|---|
| Endpoint | infinite | infinite |
| Root Complex not supporting p2p between all Root Ports | infinite | infinite |
| Root Complex supporting p2p among all Root Ports | 1 FC unit | largest MPS ÷ FC unit size |
| Switch | 1 FC unit | largest MPS ÷ FC unit size |
8. A Trace
Internal teaching signals, not PCIe wire signals. A Completer answering a Switch — so the pools are finite. CPLH capacity 2, CPLD capacity 8.
step 1 2 3 4 5 6 7
cpl_valid 1 1 1 1 1 1 0
cpl_kind CplD CplD CplD Cpl CplD - -
cpl_bytes 64 64 64 0 64 - -
cplh_cost 1 1 1 1 1 - -
cpld_cost 4 4 4 0 4 - -
cplh_avail 2 1 0 0 1 1 1
cpld_avail 8 4 0 0 4 0 0
eligible 1 1 0 1 1 0 -
send_fire 1 1 0 1 1 0 -
cplh_return 0 0 0 0 1 0 0
cpld_return 0 0 0 0 4 0 0Read steps 1–2. Two 64-byte CplD packets go out. Each costs 1 CPLH + 4 CPLD; both pools fall together.
Read step 3 — the block. CPLH is 0 and CPLD is 0. Nothing moves, and neither pool changes — the atomic pair again (Chapter 16.1 §9).
Read step 4 — the case worth staring at. A Cpl — an I/O Write Completion, no data. It costs 1 CPLH and zero CPLD. But cplh_avail is 0, so it is still blocked… except the trace shows eligible = 1.
That is deliberate and it is the point: the returns at step 5 are shown arriving in step 5's column, and eligible at step 4 reflects the effective availability including a same-cycle return (Chapter 16.1 §8's declared contract). Read step 4 as "the moment CPLH becomes available, a Cpl can go even though CPLD is still empty" — because a Cpl does not need CPLD at all.
Read step 5. A CplD goes when both pools have recovered.
Read step 6. Blocked again on CPLD alone, with CPLH available. Note cplh_avail stays at 1 — the header credit is not consumed by a packet that could not be sent.
9. RTL — Completion Credit Cost Deriver
// SYNTHESIZABLE. Derive Completion flow-control cost from normalized
// Completion metadata.
// 1 CPLH per Completion, and n CPLD where n = Roundup(Length / FC unit
// size) with the data unit 4 DW: NORMATIVE (section 1). The descriptor
// shape and the error reports: ILLUSTRATIVE.
package cpl_fc_pkg;
// VERIFIED (section 1): the data credit unit is 4 DW = 16 bytes, the same
// unit as every other pool in Module 16.
localparam int FC_DATA_UNIT_BYTES = 16;
// Normalized Completion class, from Chapter 13.1's decode. NOT
// re-derived from Fmt/Type here.
typedef enum logic [2:0] {
CPL_MEM_READ = 3'd0, // CplD : 1 CPLH + n CPLD
CPL_IO_CFG_READ = 3'd1, // CplD : 1 CPLH + 1 CPLD (bounded payload)
CPL_IO_CFG_WRITE= 3'd2, // Cpl : 1 CPLH -- NO data credit
CPL_ATOMIC = 3'd3, // CplD : 1 CPLH + 1 CPLD (<= 4 aligned DW)
CPL_NOT_A_CPL = 3'd4 // not Completion traffic
} cpl_kind_e;
function automatic bit cpl_has_data(input cpl_kind_e k);
return (k == CPL_MEM_READ) || (k == CPL_IO_CFG_READ)
|| (k == CPL_ATOMIC);
endfunction
// CEILING DIVISION, in the only correct form, with the same widening
// discipline as Chapters 16.2 and 16.3. The addition is performed one bit
// wider than the input so it cannot wrap before the division shrinks it.
function automatic logic [11:0]
cpld_credit_cost(input logic [13:0] payload_bytes);
logic [14:0] widened;
widened = {1'b0, payload_bytes} + 15'(FC_DATA_UNIT_BYTES - 1);
return 12'(widened / 15'(FC_DATA_UNIT_BYTES));
endfunction
endpackageimport cpl_fc_pkg::*;
module completion_credit_cost #(
parameter int LEN_W = 14, // payload bytes; 14 bits covers 4096
parameter int COST_W = 12
) (
input cpl_kind_e cpl_kind,
input logic [LEN_W-1:0] payload_bytes,
output logic needs_cplh,
output logic [COST_W-1:0] cplh_cost,
output logic needs_cpld,
output logic [COST_W-1:0] cpld_cost,
output logic cost_valid,
output logic cost_error
);
wire is_cpl = (cpl_kind != CPL_NOT_A_CPL);
wire has_data = cpl_has_data(cpl_kind);
// THE HEADER COST IS A CONSTANT -- one per Completion, whatever the
// header size (Chapter 16.1 section 5). Every Completion pays it,
// including a Cpl that carries nothing.
assign needs_cplh = is_cpl;
assign cplh_cost = is_cpl ? COST_W'(1) : '0;
// KEYED TO THE COMPLETION FORM, NOT TO THE LENGTH FIELD (section 5).
// A Cpl has no payload and must be charged NO data credit; deriving this
// from `payload_bytes != 0` would work by accident and break the moment
// a malformed descriptor arrived with a stale length.
assign needs_cpld = has_data;
always_comb begin
cpld_cost = '0;
if (has_data) begin
unique case (cpl_kind)
// Scales with the data actually returned.
CPL_MEM_READ : cpld_cost = COST_W'(cpld_credit_cost(14'(payload_bytes)));
// Bounded by the source: an AtomicOp Completion returns at most
// four aligned DW, and an I/O or Configuration Read Completion at
// most one -- both inside a single 16-byte credit.
CPL_IO_CFG_READ, CPL_ATOMIC : cpld_cost = COST_W'(1);
default : cpld_cost = '0;
endcase
end
end
// A CplD claiming zero payload, or a Cpl claiming payload, is a
// descriptor fault. Reported, and cost_valid withheld, rather than
// silently costed as zero.
wire zero_len_cpld = has_data && (cpl_kind == CPL_MEM_READ)
&& (payload_bytes == '0);
wire len_on_a_cpl = !has_data && is_cpl && (payload_bytes != '0);
assign cost_error = !is_cpl || zero_len_cpld || len_on_a_cpl;
assign cost_valid = is_cpl && !zero_len_cpld && !len_on_a_cpl;
endmoduleClassification: synthesizable (combinational) plus a compile-time helper.
Architecture. A constant for the header, and a three-way selection for the data — only one arm of which reaches the division, because only the Memory Read Completion has an unbounded payload.
Cost table produced by this module:
| Completion | cplh_cost | needs_cpld / cpld_cost |
|---|---|---|
| Memory Read Completion, 64 bytes | 1 | 1 / 4 |
| Memory Read Completion, 17 bytes | 1 | 1 / 2 |
| Memory Read Completion, 16 bytes | 1 | 1 / 1 |
| I/O or Configuration Read Completion | 1 | 1 / 1 |
| I/O or Configuration Write Completion | 1 | 0 / 0 |
| AtomicOp Completion | 1 | 1 / 1 |
| not a Completion | 0 | 0 / 0, cost_error |
Failure — five. Charging a Cpl for CPLD drains payload capacity against packets with no payload, and surfaces as a read-bandwidth collapse (§5). Deriving needs_cpld from payload_bytes != 0 works until a stale or malformed length arrives. Floor division in the Memory Read arm under-charges by one for every payload that is not a multiple of 16 — the same defect as Chapter 16.2 §12, on the return path. Charging n CPLD for an AtomicOp or I/O Read Completion ignores the bounded-payload rows and over-charges. And letting a cost_error descriptor proceed sends a Completion whose cost was never computed.
Deliberately simplified: one Completion per cycle; Completion Status not modelled — it does not affect cost (Chapter 13.2); no split-boundary computation — Chapter 13.3 owns that, and §11 takes already-split descriptors.
10. RTL — Completion Eligibility Gate
import cpl_fc_pkg::*;
// SYNTHESIZABLE. Gate a Completion on every resource it needs.
// The CPLH/CPLD costs are NORMATIVE (section 1), and the atomic
// multi-pool rule is stated by a vendor source (section 1: "A transaction
// cannot be transmitted unless there is at least 1 header credit and
// enough data credits for the packet payload"). The composition and the
// per-pool reason flags are ILLUSTRATIVE.
module completion_eligibility #(
parameter int CRED_W = 12,
parameter int COST_W = 12,
parameter int LEN_W = 14
) (
input logic clk,
input logic rst_n,
// ---- Head of the Completion transmit queue ----------------------------
input logic cpl_valid,
input cpl_kind_e cpl_kind,
input logic [LEN_W-1:0] payload_bytes,
// ---- Other launch resources (Chapter 16.1 section 10) -----------------
input logic replay_space,
input logic tx_path_ready,
// ---- Credit plumbing for the COMPLETION pools -------------------------
// Advertised by the REQUESTER, spent by this Completer (section 2).
input logic init_valid,
input logic [CRED_W-1:0] init_cplh_capacity,
input logic init_cplh_infinite, // Endpoint: TRUE (s7)
input logic [CRED_W-1:0] init_cpld_capacity,
input logic init_cpld_infinite,
input logic cplh_return_valid,
input logic [CRED_W-1:0] cplh_return_count,
input logic cpld_return_valid,
input logic [CRED_W-1:0] cpld_return_count,
// ---- Outputs ------------------------------------------------------------
output logic cpl_ready,
output logic send_fire,
output logic [CRED_W-1:0] cplh_available,
output logic [CRED_W-1:0] cpld_available,
output logic blocked_on_cplh,
output logic blocked_on_cpld,
output logic blocked_on_replay,
output logic blocked_on_path,
output logic cpl_error
);
logic needs_cplh, needs_cpld, cost_valid, cost_err, credit_eligible, pair_err;
logic [COST_W-1:0] cplh_cost, cpld_cost;
completion_credit_cost #(.LEN_W(LEN_W), .COST_W(COST_W)) u_cost (
.cpl_kind, .payload_bytes,
.needs_cplh, .cplh_cost, .needs_cpld, .cpld_cost,
.cost_valid, .cost_error(cost_err)
);
// ATOMIC ACROSS THE COMPLETION POOLS (Chapter 16.1 section 9). A Cpl
// engages only the header pool: it must be neither charged to nor
// blocked by CPLD (section 5).
credit_pair_gate #(.CRED_W(CRED_W), .COST_W(COST_W)) u_pair (
.clk, .rst_n,
.pkt_valid(cpl_valid && cost_valid),
.needs_header(needs_cplh), .header_cost(cplh_cost),
.needs_data(needs_cpld), .data_cost(cpld_cost),
.send_fire,
.hdr_return_valid(cplh_return_valid), .hdr_return_count(cplh_return_count),
.dat_return_valid(cpld_return_valid), .dat_return_count(cpld_return_count),
.init_valid,
.init_hdr_capacity(init_cplh_capacity),
.init_hdr_infinite(init_cplh_infinite),
.init_dat_capacity(init_cpld_capacity),
.init_dat_infinite(init_cpld_infinite),
.eligible(credit_eligible),
.hdr_available(cplh_available), .dat_available(cpld_available),
.pool_error(pair_err)
);
// A descriptor whose cost could not be computed NEVER launches. Sending a
// Completion with an unknown cost is how a pool goes wrong silently.
wire all_ready = cost_valid && credit_eligible && replay_space && tx_path_ready;
assign cpl_ready = all_ready;
assign send_fire = cpl_valid && all_ready;
assign blocked_on_cplh = cpl_valid && needs_cplh
&& (cplh_available < CRED_W'(cplh_cost));
assign blocked_on_cpld = cpl_valid && needs_cpld
&& (cpld_available < CRED_W'(cpld_cost));
assign blocked_on_replay = cpl_valid && !replay_space;
assign blocked_on_path = cpl_valid && !tx_path_ready;
assign cpl_error = cost_err | pair_err;
endmoduleClassification: synthesizable.
Architecture. Cost derivation, Chapter 16.1's atomic pair, and a conjunction — the same shape as Chapters 16.2 §7 and 16.3 §8, with the Completion pools substituted.
cost_valid gates the launch, which is new here. A malformed Completion descriptor — a CplD with zero length, a Cpl carrying a length — must not be sent with a cost nobody computed. Chapters 16.2 and 16.3 reported cost errors; this one also blocks on them, because a Completion is generated internally and a bad descriptor is a local bug rather than something arriving off the wire.
Failure — four. A combined blocked_on_credit flag cannot distinguish CPLH from CPLD exhaustion, which have different causes and different fixes. Consulting CPLD for a Cpl blocks a Completion on a pool it does not use. Letting a cost_error descriptor through spends unknown credit. And treating an infinite advertisement as capacity zero blocks the pool that, on an Endpoint path, can never be the constraint (§7).
11. RTL — Completion Queue with a Credit-Starved Head
The architectural point Chapter 12.3 implies and this chapter makes explicit: producing a Completion and transmitting it are separate ownership stages.
import cpl_fc_pkg::*;
// SYNTHESIZABLE. A small Completion transmit queue whose head is gated on
// Completion credit.
// The decoupling of "Completion produced" from "Completion transmitted" is
// ILLUSTRATIVE ARCHITECTURE; the credit gating it feeds is normative
// (section 1). DEPTH is a teaching choice, not a protocol requirement.
module completion_tx_queue #(
parameter int DEPTH = 4,
parameter int LEN_W = 14,
// Opaque context carried with the descriptor -- Tag, Requester ID and
// whatever else Chapter 12.3 needs. NOT interpreted here.
parameter int CTX_W = 32,
// GUARDED INDEX WIDTH. $clog2(1) is zero, and a zero-width pointer
// cannot index anything (Chapter 15.2's parameter corner).
parameter int PTR_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH)
) (
input logic clk,
input logic rst_n,
// ---- From the target / completion builder (Chapter 12.3) --------------
input logic enq_valid,
output logic enq_ready,
input cpl_kind_e enq_kind,
input logic [LEN_W-1:0] enq_bytes,
input logic [CTX_W-1:0] enq_ctx,
// ---- Head, to the credit gate ------------------------------------------
output logic head_valid,
input logic head_ready, // = send_fire (section 10)
output cpl_kind_e head_kind,
output logic [LEN_W-1:0] head_bytes,
output logic [CTX_W-1:0] head_ctx,
output logic [PTR_W:0] occupancy, // one bit wider than PTR_W
output logic queue_full
);
generate
if (DEPTH < 1) $error("DEPTH must be at least 1");
if ((DEPTH > 1) && ((1 << PTR_W) < DEPTH))
$error("PTR_W too narrow to index DEPTH");
endgenerate
cpl_kind_e kind_q [DEPTH];
logic [LEN_W-1:0] bytes_q [DEPTH];
logic [CTX_W-1:0] ctx_q [DEPTH];
logic [PTR_W-1:0] rd_q, wr_q;
// OCCUPANCY IS A COUNTER, ONE BIT WIDER THAN THE POINTERS. Full and empty
// are never inferred from pointer equality (Chapter 14.4 section 10).
logic [PTR_W:0] cnt_q;
assign occupancy = cnt_q;
assign queue_full = (cnt_q == (PTR_W+1)'(DEPTH));
assign enq_ready = !queue_full;
assign head_valid = (cnt_q != '0);
assign head_kind = kind_q[rd_q];
assign head_bytes = bytes_q[rd_q];
assign head_ctx = ctx_q[rd_q];
wire push = enq_valid && enq_ready;
wire pop = head_valid && head_ready;
// EXPLICIT WRAP at DEPTH-1. Never rely on a pointer wrapping naturally --
// it only does at a power-of-two DEPTH, and DEPTH is a free parameter.
function automatic logic [PTR_W-1:0] next_idx(input logic [PTR_W-1:0] i);
return (i == PTR_W'(DEPTH-1)) ? '0 : (i + PTR_W'(1));
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd_q <= '0; wr_q <= '0; cnt_q <= '0;
end else begin
if (push) begin
kind_q[wr_q] <= enq_kind;
bytes_q[wr_q] <= enq_bytes;
ctx_q[wr_q] <= enq_ctx;
wr_q <= next_idx(wr_q);
end
if (pop) rd_q <= next_idx(rd_q);
// Push and pop in the same cycle leave occupancy unchanged. Written
// as one expression so the two cannot disagree.
unique case ({push, pop})
2'b10 : cnt_q <= cnt_q + (PTR_W+1)'(1);
2'b01 : cnt_q <= cnt_q - (PTR_W+1)'(1);
default : ;
endcase
end
end
endmoduleClassification: synthesizable.
Architecture. A ring buffer with an explicit wrap and a separate occupancy counter one bit wider than the pointers — Chapter 14.4 §10's discipline, because full and empty are indistinguishable by pointer equality alone.
The head is offered continuously and consumed only on send_fire. A credit-starved head sits there, valid and stable, while the queue behind it fills — which is exactly the observable a debugger needs (§15).
The context is opaque. The queue carries enq_ctx and never interprets it; Chapter 12.3 owns what is in it. Re-deriving a Completion's identity here would couple queued descriptors to live target state (Chapter 11.6 §7).
Failure — five. Inferring full from wr == rd confuses full with empty. Relying on natural pointer wrap breaks at any non-power-of-two DEPTH. $clog2(DEPTH) in a port or pointer width fails to elaborate at DEPTH = 1. Popping on head_valid && credit_eligible rather than on send_fire frees the entry before the packet has actually left. And recomputing the cost from live target signals at pop time rather than from the stored descriptor attaches whatever the target now holds to a Completion built cycles earlier.
Deliberately simplified: strict FIFO — no reordering among Completions (§6, and Chapter 13.4 owns the rules); no payload storage, only the descriptor; DEPTH is a teaching choice.
12. Independent Reference Model
// VERIFICATION-ONLY. Independent reference for Completion credit cost,
// written from section 1's Table 5 rather than from section 9's structure.
typedef struct { int cplh; int cpld; } cpl_cost_t;
function automatic cpl_cost_t ref_cpl_cost(string kind, int payload_bytes);
cpl_cost_t c;
c.cplh = 1; // every Completion, always
case (kind)
// Scales with the returned payload. COUNTED, not divided -- a rounding
// bug cannot be present in both this and section 9.
"MEM_READ_CPL" : begin
c.cpld = 0;
while (payload_bytes > 0) begin
c.cpld = c.cpld + 1;
payload_bytes = payload_bytes - 16; // 4 DW, verified
end
end
"IO_CFG_READ_CPL" : c.cpld = 1; // bounded payload
"ATOMIC_CPL" : c.cpld = 1; // <= 4 aligned DW
"IO_CFG_WRITE_CPL": c.cpld = 0; // Cpl -- NO data credit
default : begin c.cplh = 0; c.cpld = 0; end
endcase
return c;
endfunction
// Independent Completion-pool model. Driven from OBSERVED sends and
// OBSERVED updates -- never from the DUT's available counters.
class cpl_pool_model;
int cplh, cpld;
bit cplh_infinite, cpld_infinite;
function void on_send(cpl_cost_t c);
if (!cplh_infinite) cplh -= c.cplh;
if (!cpld_infinite) cpld -= c.cpld;
endfunction
function void on_return(int h, int d);
if (!cplh_infinite) cplh += h;
if (!cpld_infinite) cpld += d;
endfunction
// An infinite pool is never decremented and never blocks (section 7).
function bit eligible(cpl_cost_t c);
return (cplh_infinite || cplh >= c.cplh)
&& (cpld_infinite || cpld >= c.cpld);
endfunction
endclassClassification: verification-only.
The infinite flags are first-class in the model, not represented as a large number. A reference that modelled infinite as 2**31 would agree with a DUT that did the same — and both would be wrong at the boundary, which is exactly where §7's Endpoint case lives.
13. Assertions
// SVA over completion_credit_cost, completion_eligibility and
// completion_tx_queue. These assert the VERIFIED Completion costs and the
// local issue contract. Nothing here asserts credit-return timing (an
// environment property -- Chapter 16.1 section 15), Completion Status
// (13.2), split boundaries (13.3), spend policy (16.5), or the update
// protocol (16.6).
// ---- ENVIRONMENT ------------------------------------------------------
// A1: cpl_kind is Chapter 13.1's normalized class, not re-decoded here.
assume property (@(posedge clk) disable iff (!rst_n)
cpl_valid |-> (cpl_kind != CPL_NOT_A_CPL));
// A2: the descriptor is stable while offered and not taken.
assume property (@(posedge clk) disable iff (!rst_n)
(cpl_valid && !cpl_ready) |=> ($stable(cpl_kind) && $stable(payload_bytes)));
// ---- COST -------------------------------------------------------------
// P1: EVERY Completion costs exactly one header credit -- including a Cpl.
property p_cplh_is_one;
@(posedge clk) disable iff (!rst_n)
needs_cplh |-> (cplh_cost == COST_W'(1));
endproperty
a_cplh_one : assert property (p_cplh_is_one);
// P2: A Cpl CONSUMES NO DATA CREDIT. The chapter's sharpest cost property,
// and the one whose violation surfaces as a read-bandwidth collapse.
property p_cpl_costs_no_cpld;
@(posedge clk) disable iff (!rst_n)
(cpl_kind == CPL_IO_CFG_WRITE) |-> (!needs_cpld && (cpld_cost == '0));
endproperty
a_cpl_no_cpld : assert property (p_cpl_costs_no_cpld);
// P3: and it is never CHARGED one either -- asserted at the pool, not just
// at the cost, because a design could compute zero and decrement anyway.
property p_cpl_send_leaves_cpld;
@(posedge clk) disable iff (!rst_n)
(send_fire && (cpl_kind == CPL_IO_CFG_WRITE)) |=> $stable(cpld_available);
endproperty
a_cpl_pool : assert property (p_cpl_send_leaves_cpld);
// P4: the Memory Read Completion data cost matches an INDEPENDENT model
// (section 12's counting loop, not section 9's division).
property p_cpld_matches_reference;
@(posedge clk) disable iff (!rst_n)
(cpl_kind == CPL_MEM_READ)
|-> (cpld_cost == COST_W'(ref_cpl_cost("MEM_READ_CPL",
int'(payload_bytes)).cpld));
endproperty
a_cpld_ref : assert property (p_cpld_matches_reference);
// P4b: CEILING, NOT FLOOR -- stated directly so a failure names itself.
property p_cpld_is_ceiling;
@(posedge clk) disable iff (!rst_n)
((cpl_kind == CPL_MEM_READ) && (payload_bytes != '0))
|-> ((COST_W'(cpld_cost) * 16 >= payload_bytes)
&& ((COST_W'(cpld_cost) - 1) * 16 < payload_bytes));
endproperty
a_ceiling : assert property (p_cpld_is_ceiling);
// P5: the bounded-payload Completions cost exactly one CPLD -- never n.
property p_bounded_cpl_one_cpld;
@(posedge clk) disable iff (!rst_n)
(cpl_kind inside {CPL_IO_CFG_READ, CPL_ATOMIC})
|-> (needs_cpld && (cpld_cost == COST_W'(1)));
endproperty
a_bounded : assert property (p_bounded_cpl_one_cpld);
// P6: a descriptor whose cost could not be computed NEVER launches.
property p_no_send_without_cost;
@(posedge clk) disable iff (!rst_n)
send_fire |-> cost_valid;
endproperty
a_cost_gate : assert property (p_no_send_without_cost);
// ---- ISSUE ------------------------------------------------------------
// P7: no Completion issues without sufficient CPLH.
property p_no_issue_without_cplh;
@(posedge clk) disable iff (!rst_n)
(send_fire && needs_cplh) |-> (cplh_available >= CRED_W'(cplh_cost));
endproperty
a_cplh_gate : assert property (p_no_issue_without_cplh);
// P8: no CplD issues without sufficient CPLD.
property p_no_issue_without_cpld;
@(posedge clk) disable iff (!rst_n)
(send_fire && needs_cpld) |-> (cpld_available >= CRED_W'(cpld_cost));
endproperty
a_cpld_gate : assert property (p_no_issue_without_cpld);
// P9: ATOMICITY. A failed CPLD eligibility consumes NO CPLH.
property p_failed_cpld_leaves_cplh;
@(posedge clk) disable iff (!rst_n)
(cpl_valid && needs_cpld && (cpld_available < CRED_W'(cpld_cost)))
|=> $stable(cplh_available);
endproperty
a_atomic : assert property (p_failed_cpld_leaves_cplh);
// P10: and the mirror.
property p_failed_cplh_leaves_cpld;
@(posedge clk) disable iff (!rst_n)
(cpl_valid && needs_cplh && (cplh_available < CRED_W'(cplh_cost)))
|=> $stable(cpld_available);
endproperty
a_atomic_mirror : assert property (p_failed_cplh_leaves_cpld);
// P11: a credit-starved Completion is STABLE -- descriptor unchanged.
property p_starved_stable;
@(posedge clk) disable iff (!rst_n)
(cpl_valid && !cpl_ready)
|=> (cpl_valid && $stable(cpl_kind) && $stable(payload_bytes));
endproperty
a_stable : assert property (p_starved_stable);
// P12: SPLIT COMPLETIONS PAY INDEPENDENTLY. Each packet of a split costs
// its own CPLH -- the second must not reuse the first's accounting.
// (g_cplh_spent and g_cpl_sent are testbench ghost counters.)
property p_split_pays_per_packet;
@(posedge clk) disable iff (!rst_n)
send_fire |=> (g_cplh_spent == $past(g_cplh_spent) + 1);
endproperty
a_split : assert property (p_split_pays_per_packet);
// P13: AN INFINITE POOL NEVER BLOCKS AND NEVER MOVES (section 7). On an
// Endpoint path this is the whole behaviour, and a design storing the
// zero advertisement as capacity fails here immediately.
property p_infinite_never_blocks;
@(posedge clk) disable iff (!rst_n)
(init_cplh_infinite && cpl_valid) |-> !blocked_on_cplh;
endproperty
a_infinite : assert property (p_infinite_never_blocks);
// ---- CROSS-CLASS NON-INTERFERENCE -------------------------------------
// P14: THE DIRECTIONAL PROPERTY. A Completion NEVER consumes NP credit --
// the NPH that carried the Request out is a different pool on a different
// direction (section 2).
property p_completion_spends_no_np;
@(posedge clk) disable iff (!rst_n)
send_fire |=> ($stable(dut_np_engine.nph_available)
&& $stable(dut_np_engine.npd_available));
endproperty
a_not_np : assert property (p_completion_spends_no_np);
// P15: nor Posted credit.
property p_completion_spends_no_posted;
@(posedge clk) disable iff (!rst_n)
send_fire |=> ($stable(dut_posted_engine.ph_available)
&& $stable(dut_posted_engine.pd_available));
endproperty
a_not_posted : assert property (p_completion_spends_no_posted);
// P16: and the reverse -- consuming NPH for a Request does not reduce the
// Completion pools. Credits are not transferred through a transaction.
property p_np_send_leaves_completion_pools;
@(posedge clk) disable iff (!rst_n)
(dut_np_engine.launch && !send_fire)
|=> ($stable(cplh_available) && $stable(cpld_available));
endproperty
a_no_transfer : assert property (p_np_send_leaves_completion_pools);
// P17: a full replay buffer blocks the launch and touches no credit.
property p_replay_is_not_credit;
@(posedge clk) disable iff (!rst_n)
!replay_space |=> ($stable(cplh_available) && $stable(cpld_available));
endproperty
a_not_replay : assert property (p_replay_is_not_credit);
// ---- QUEUE ------------------------------------------------------------
// P18: the queue entry is freed only on an actual send.
property p_pop_only_on_send;
@(posedge clk) disable iff (!rst_n)
(head_valid && !head_ready) |=> ($stable(occupancy) || push);
endproperty
a_pop : assert property (p_pop_only_on_send);
// P19: occupancy is bounded and never underflows.
property p_occupancy_sane;
@(posedge clk) disable iff (!rst_n)
(occupancy <= (PTR_W+1)'(DEPTH)) && !(pop && (occupancy == '0));
endproperty
a_occ : assert property (p_occupancy_sane);P2 and P3 are a deliberate pair. P2 says a Cpl's cost is zero; P3 says the CPLD pool does not move when one is sent. A design can compute the cost correctly and decrement anyway — the two properties catch different halves, and only P3 catches a consume strobe wired to the wrong condition.
P14, P15 and P16 make §2's directional claim checkable, and none can be written inside a single module — no block sees both a Completion send and the NP engine's counters. P16 is the one that catches the "credits travel with the transaction" mental model: sending a Request must leave the Completion pools untouched.
P13 is the Endpoint property. On a path where the receiver advertised infinite Completion credit, the entire blocking behaviour should be absent — and a design storing that zero advertisement as a capacity fails immediately, converting the pool that cannot stall into the one that always does (§7).
No liveness is asserted — Chapter 16.1 §15's argument applies unchanged.
14. Verification and Fault Injection
The scoreboard uses §12's model, never §9's helpers, and tracks the Completion pools from observed sends and observed updates.
Cost boundaries — the required set
| Completion | Payload | Expected CPLH / CPLD |
|---|---|---|
| I/O or Configuration Write Completion | — | 1 / 0 |
| I/O or Configuration Read Completion | bounded | 1 / 1 |
| AtomicOp Completion | ≤ 4 DW | 1 / 1 |
| Memory Read Completion | 1 | 1 / 1 |
| Memory Read Completion | 15 / 16 | 1 / 1 / 1 |
| Memory Read Completion | 17 | 1 / 2 — the floor-division killer |
| Memory Read Completion | 64 | 1 / 4 |
| Memory Read Completion | 4095 / 4096 | 1 / 256 / 256 |
Issue
- CPLH available, CPLD insufficient. Verify neither pool moves (P9) — required.
- CPLD available, CPLH insufficient. The mirror (P10).
- A
Cplwith CPLD at zero. Verify it issues and CPLD is untouched (P2, P3) — required. - Both pools exhausted. Verify the Completion waits, stable (P11).
- A CPLD return while the head is stalled. Verify it becomes eligible that cycle (Chapter 16.1 §8).
- An infinite CPLH/CPLD advertisement. Verify
blocked_on_*never asserts and the counters never move (P13) — the Endpoint case. - Four split Completions for one read. Verify four CPLH are spent, not one (P12) — required.
- A split blocked partway — Completions 1 and 2 sent, 3 stalled on CPLD. Verify the queue holds 3 and 4 and nothing is lost (§6).
- A malformed descriptor — a
CplDwith zero length, and aCplwith a non-zero length. Verifycost_error, and that no send occurs (P6). - Replay buffer full with credit available (P17).
- Queue full, and the enqueue backpressure that follows.
DEPTH= 1, 3, 4, 5 — the parameter corners, including the$clog2(1)width case and two non-powers of two.- Reset mid-issue, and reset with a stalled head.
Mutations
| # | Mutation | Caught by | Silicon symptom |
|---|---|---|---|
| 1 | Cpl consumes CPLD | P2, P3 | CPLD drains on configuration traffic; read bandwidth collapses later |
| 2 | CplD consumes no CPLH | P1, P7 | receiver's Completion header buffer overflows |
| 3 | CPLD cost floored instead of ceilinged | P4, P4b at 17 bytes | receiver overflow on unaligned Completion payloads |
| 4 | send permitted with zero CPLH | P7 | Completions overrun the header buffer |
| 5 | CPLH consumed before discovering CPLD short | P9 | slow CPLH leak; Completions stop permanently |
| 6 | NP credit decremented instead of CPLH | P14 | NPH drains on Completions; reads stall for no reason |
| 7 | stale payload_bytes drives the cost | A2, P4 | intermittent mis-charge, load-dependent |
| 8 | second split Completion reuses the first's cost | P12 | pools drain slower than reality; receiver overflow |
| 9 | cost arithmetic overflows the narrow width | P4 at 4096 | largest Completions cost almost nothing |
| 10 | queue entry freed before send_fire | P18 | Completion lost locally, credit spent |
| 11 | infinite advertisement stored as capacity zero | P13 | Completions never sent on an Endpoint path |
| 12 | n CPLD charged for an AtomicOp Completion | P5 | over-charging; throughput loss only |
| 13 | Request send decrements the Completion pools | P16 | pools drain twice as fast as sends explain |
| 14 | $clog2(DEPTH) used directly for the pointer | elaboration fails at DEPTH = 1 | build break at a legal configuration |
15. Debugging
A Memory Read reaches the Endpoint, the data is ready, and no CplD leaves
Walk the return path; each step eliminates the ones before.
- Is a descriptor in the Completion queue? If not, the target never produced one — a Chapter 12.3 problem, not a credit one.
- Is
cost_validasserted? A malformed descriptor is blocked deliberately (P6), andcpl_errorsays so. blocked_on_cplhorblocked_on_cpld? Now you know which pool, which matters because they have different causes.- Is that pool infinite? If the receiver is an Endpoint it should be (§7) — and if the design shows it as zero capacity, that is mutation 11.
- Is the pool being returned? A pool that only falls means the Requester is not draining its Completion buffer, or the update path is not decoding (Chapter 16.6).
blocked_on_replayorblocked_on_path? Then credit is fine and the problem is local (Chapter 14.4).
Step 4 is the one that reframes the search. If the direct partner is an Endpoint and Completion credit is genuinely the blocker, either the advertisement was mishandled locally or there is a Switch in the path — and it is the Switch's finite advertisement you are hitting.
Cpl works and CplD stalls
CPLH is fine and CPLD is not — the two Completion forms differ in exactly one pool (§5).
Check the derived cpld_cost against §12's model for the payload in question. A mismatch is §9's division; a match means the pool is genuinely short and the question moves to why it is not being returned.
Small reads work and larger reads stall
The CPLD cost scales with the payload and CPLH does not, so this is almost always CPLD.
Compare the advertised CPLD against one maximum-size Completion. If the advertisement is smaller than a single Completion's cost, that Completion can never be sent — a permanent stall by construction, the same shape as Chapter 16.2 §13's step 4 on the forward path.
NP credits decrease when Completions transmit
A pool-classifier bug (§2, P14).
The Completion engine is decrementing the NP pools. The tell is that NPH falls without any Request on the wire, and that it falls in step with outbound Completions.
This is the "credits travel with the transaction" model implemented literally — and P16 catches its mirror, where sending a Request reduces the Completion pools.
16. Common Misconceptions
- "A Completion uses NPH because the original Request was Non-Posted." Different direction, different pool. The Completion is charged CPLH/CPLD (§1, §2).
- "A Memory Read's NPH also reserves space for the returned data." It reserves nothing on the return path. The return path is paid for separately, by the Completer (§2).
- "Credits are transferred through a transaction." They are never transferred. Each direction accounts independently (§2, P16).
- "A CplD needs only data credits." Every Completion costs one header credit (§1, P1).
- "A Cpl consumes a data credit." It carries no data and costs 1 CPLH and nothing else (§5, P2).
- "Completion credit equals the Requester's Tag capacity." Different resources, different lifetimes, both owned by the Requester (§3).
- "1 CPLD credit equals one reassembly-buffer entry." The mapping is implementation-defined; PCIe advertises Link receive capacity (§4).
- "A read consumes Completion credits when it is issued." The Completer consumes them, later, when it sends the answer (§2).
- "CPLH/CPLD are returned by an ACK." An ACK returns replay storage (Chapter 14.2); UpdateFC returns credit (Chapter 16.6).
- "A CplD's credit cost is determined by MRRS." MRRS bounds the request; the Completion's cost follows the payload it actually carries (§1).
- "A split Completion pays once for the whole original read." Each Completion packet pays its own CPLH and CPLD (§6, P12).
- "Available Completion credit means the Requester's software is ready." It means buffer space was advertised. Software readiness is invisible across the Link (§4).
- "Completion credits can never run out." On an Endpoint path they cannot — infinite is advertised. On a Switch or a peer-to-peer-capable Root Complex they can (§7).
- "Every port advertises infinite Completion credit." The rule is conditional on the port type, and getting it backwards is mutation 11 (§7).
17. Understanding Check
18. What's Next
The return path has its own budget, drawn from the opposite direction's advertisement. Every Completion costs 1 CPLH; a CplD also costs Roundup(Length / 16) CPLD, and a Cpl costs no data credit at all. Split Completions pay per packet, so the header cost multiplies with the split while the data cost does not.
Credits are never carried through a transaction. The NPH that took the Request out and the CPLH/CPLD that bring the answer back are four different numbers in two different directions, and no success on one side refunds the other.
And whether the Completion pools can run dry is conditional — infinite at an Endpoint, finite at a Switch — which is the kind of fact that turns a week of debugging into an afternoon.
Module 16's six pools are now complete. Chapter 16.5 — Credit Consumption takes the question all three cost chapters have been deferring: at exactly what hardware event does a transmitter spend a credit, how are several pools committed atomically, and what has to exist in a pipelined transmitter so that two packets cannot spend the same credit.
Chapter 16.6 then closes the loop from the other end — when receive storage becomes reusable, and how a wrapping cumulative counter becomes live credit again, exactly once.
The idea to carry forward: a transaction spans two directions, and each direction bills separately.