Skip to content

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 buffer

Each 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 / CPLDRequester context / Tag
Whose resourcethe Requester's receive bufferthe Requester's correlation table
Advertised tothe Completernobody — internal
Spent bythe Completerthe Requester
Freed whenthe Requester drains the Completion bufferthe last Completion arrives
Runs out becausethe Completer is answering faster than you drainyou 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:

FormCarries dataCost
Cpl — Completion without Datano1 CPLH
CplD — Completion with Datayes1 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: n CPLD, 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 back

7. 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 portCPLH minimumCPLD minimum
Endpointinfiniteinfinite
Root Complex not supporting p2p between all Root Portsinfiniteinfinite
Root Complex supporting p2p among all Root Ports1 FC unitlargest MPS ÷ FC unit size
Switch1 FC unitlargest 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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     0

Read 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import 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;
 
endmodule

Classification: 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:

Completioncplh_costneeds_cpld / cpld_cost
Memory Read Completion, 64 bytes11 / 4
Memory Read Completion, 17 bytes11 / 2
Memory Read Completion, 16 bytes11 / 1
I/O or Configuration Read Completion11 / 1
I/O or Configuration Write Completion10 / 0
AtomicOp Completion11 / 1
not a Completion00 / 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;
 
endmodule

Classification: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

Classification: synthesizable.

Architecture. A ring buffer with an explicit wrap and a separate occupancy counter one bit wider than the pointersChapter 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endclass

Classification: 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 assertedChapter 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

CompletionPayloadExpected CPLH / CPLD
I/O or Configuration Write Completion1 / 0
I/O or Configuration Read Completionbounded1 / 1
AtomicOp Completion≤ 4 DW1 / 1
Memory Read Completion11 / 1
Memory Read Completion15 / 161 / 1 / 1
Memory Read Completion171 / 2 — the floor-division killer
Memory Read Completion641 / 4
Memory Read Completion4095 / 40961 / 256 / 256

Issue

  • CPLH available, CPLD insufficient. Verify neither pool moves (P9) — required.
  • CPLD available, CPLH insufficient. The mirror (P10).
  • A Cpl with 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 CplD with zero length, and a Cpl with a non-zero length. Verify cost_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

#MutationCaught bySilicon symptom
1Cpl consumes CPLDP2, P3CPLD drains on configuration traffic; read bandwidth collapses later
2CplD consumes no CPLHP1, P7receiver's Completion header buffer overflows
3CPLD cost floored instead of ceilingedP4, P4b at 17 bytesreceiver overflow on unaligned Completion payloads
4send permitted with zero CPLHP7Completions overrun the header buffer
5CPLH consumed before discovering CPLD shortP9slow CPLH leak; Completions stop permanently
6NP credit decremented instead of CPLHP14NPH drains on Completions; reads stall for no reason
7stale payload_bytes drives the costA2, P4intermittent mis-charge, load-dependent
8second split Completion reuses the first's costP12pools drain slower than reality; receiver overflow
9cost arithmetic overflows the narrow widthP4 at 4096largest Completions cost almost nothing
10queue entry freed before send_fireP18Completion lost locally, credit spent
11infinite advertisement stored as capacity zeroP13Completions never sent on an Endpoint path
12n CPLD charged for an AtomicOp CompletionP5over-charging; throughput loss only
13Request send decrements the Completion poolsP16pools drain twice as fast as sends explain
14$clog2(DEPTH) used directly for the pointerelaboration fails at DEPTH = 1build 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.

  1. Is a descriptor in the Completion queue? If not, the target never produced one — a Chapter 12.3 problem, not a credit one.
  2. Is cost_valid asserted? A malformed descriptor is blocked deliberately (P6), and cpl_error says so.
  3. blocked_on_cplh or blocked_on_cpld? Now you know which pool, which matters because they have different causes.
  4. 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.
  5. 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).
  6. blocked_on_replay or blocked_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.