Skip to content

PCIe · Module 12

Completion Flow — The Return Path Is Its Own Pipeline

A Completer having data is not the same as a Completion having been sent. The return path has its own generation stage, its own queue, its own routing, and its own progress accounting at the Requester — and the read is not finished until that accounting says so.

Chapter 12.1 drew the return journey as three arrows: the Completer produced a Completion, the fabric routed it back, the Requester correlated it. Each of those arrows is a pipeline stage with its own buffering, its own backpressure and its own failure modes, and the read is not finished when the first one fires.

Once a Completer has serviced a non-posted Request, how does response information and returned data flow back through the fabric and resolve the correct Requester state?

1. Three Boundaries, Not One Arrow

Chapter 10.2 introduced Completions as a transaction concept. Chapter 12.1 used them. This chapter is about the machinery between the two, and the first thing to establish is that there is more of it than the transaction view suggests.

BoundaryOwnerWhat "done" means hereWhat can stall it
target operation → response availableCompleter resourcethe data existsthe resource itself
response → Completion descriptorCompleter TLthe packet has been builtdescriptor construction resources
descriptor → transmitted packetCompleter TXthe packet has leftqueue depth, flow control, arbitration
packet → Requesterfabricit arrivedcongestion
arrival → correlatedRequester TLthe read it belongs to is knownnothing — but it can fail
correlated → progress updatedRequester contexthow much is still expectednothing
resolved → deliveredRequester result paththe client took itthe client

Seven boundaries. Chapter 12.2 §3 made the same point for the forward path, and the discipline is identical: "the read completed" is a statement about one of these, and a design that means two of them at once has a bug.

2. The Verified Semantics

3. Data Available Is Not Packet Sent

The most common mental-model error about the return path, and it produces a specific debugging failure (§12).

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
target read completes
   → response data and status become available          ← boundary 1
   → a Completion descriptor is constructed             ← boundary 2
   → the descriptor is queued for transmission          ← boundary 3
   → the packet is transmitted                          ← boundary 4

4. What the Completer Must Retain

To build the response, the Completer needs information from the Request that the Request itself is no longer around to supply.

RetainedWhy
the requester identityit is the return route (Chapter 11.5 §7)
the correlation fieldit is how the Requester will match the response
how much was requested, and how much this response coversto know whether more Completions are owed
response type and status contextwhether data accompanies the response
the transaction's handling contextattributes travel with the transaction (Chapter 11.6 §7)

5. The Requester's Side — Progress, Not Arrival

The Requester's job on receipt has three steps, and only the third one decides anything.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Completion arrives
   → CORRELATE: which read does this belong to?          (identity, not order)
   → ACCOUNT:   how much of the expected extent has now arrived?
   → DECIDE:    is this read finished, or is more owed?

Chapter 12.1 established the first step and this chapter owns the second and third.

6. The Return Path

A target resource makes response data available. The completer transaction layer builds a completion descriptor and queues it for transmission. Two completion packets are sent in turn across the fabric. The requester transaction layer receives each one and performs a correlation lookup against its context table. The first completion updates progress but does not retire the read. The second completes the expected extent, retires the context, and the result is buffered and later delivered to the local consumer when it is ready.Generate, queue, transmit, correlate, account, retire, deliverTarget resourceCompleter TLFabricRequester TLContext tableresponse dataavailabledescriptor built -not yet sentCompletion with Data- chunk 1routed by RequesterIDcorrelate - progressupdatedstill outstanding -do NOT retireCompletion with Data- chunk 2routed by RequesterIDextent complete -retireresult buffered,then delivered
Figure 2 — the return path as a pipeline. Every arrow is a boundary that can stall. Note that the context entry stays active across the two Completion chunks and retires only after the second, and that local delivery is a separate event after that.

The two amber elements are the chapter. The descriptor that exists but has not been sent is §3. The context that must not retire on the first chunk is §5.

7. RTL — Completion Generation Queue

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Turn accepted target-response events into queued Completion
// descriptors. Decoupling the resource from the outbound link: an
// ARCHITECTURAL requirement (section 3). The descriptor fields, the depth,
// and final_for_request: ILLUSTRATIVE internal metadata.
package cpl_flow_pkg;
 
  // NORMALIZED INTERNAL DESCRIPTOR — NOT a wire-format Completion.
  typedef struct packed {
    logic [15:0] requester_id;   // return route, copied from the Request
    logic [15:0] corr;           // correlation field, copied from the Request
    logic [10:0] dw;             // DW carried by this response chunk
    logic        has_data;       // status-only vs. with-data (section 2)
    logic        err;            // ABSTRACTED status. Module 13 owns the
                                 // actual Completion Status encodings.
    logic        final_for_request;  // INTERNAL teaching metadata only
  } cpl_desc_t;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import cpl_flow_pkg::*;
 
module cpl_gen_queue #(
  parameter int DEPTH  = 4,
  parameter int DATA_W = 128
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- Accepted target-response event ----------------------------------
  // Context and data arrive TOGETHER, for Chapter 12.2 section 4's reason:
  // a response whose identity and payload can drift apart is a response that
  // can be delivered to the wrong Requester.
  input  logic                 rsp_valid,
  output logic                 rsp_ready,
  input  cpl_desc_t            rsp_desc,
  input  logic [DATA_W-1:0]    rsp_data,
 
  // ---- Queued descriptor out, to the return TX path --------------------
  output logic                 cpl_valid,
  input  logic                 cpl_ready,
  output cpl_desc_t            cpl_desc,
  output logic [DATA_W-1:0]    cpl_data
);
 
  generate
    if (DEPTH < 1) $error("DEPTH must be at least 1");
  endgenerate
 
  localparam int IDX_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH);
  localparam int CNT_W = $clog2(DEPTH + 1);
 
  typedef struct packed {
    cpl_desc_t         desc;
    logic [DATA_W-1:0] data;
  } entry_t;
 
  entry_t           mem_q [DEPTH];
  logic [IDX_W-1:0] wr_q, rd_q;
  logic [CNT_W-1:0] cnt_q;
 
  wire full  = (cnt_q == CNT_W'(DEPTH));
  wire empty = (cnt_q == '0);
 
  assign cpl_valid = !empty;
 
  wire pop = cpl_valid && cpl_ready;
 
  // EFFECTIVE capacity — the slot a pop vacates is usable this cycle.
  // Refusing a response at full-with-pop would stall the target resource at
  // exactly the moment the queue was draining (Chapter 12.1's fixed bug,
  // and the same shape).
  assign rsp_ready = !full || pop;
 
  wire push = rsp_valid && rsp_ready;
 
  // Outputs from STORED state. There is no bypass, so a response cannot be
  // presented from a live input while an earlier one is still queued.
  assign cpl_desc = mem_q[rd_q].desc;
  assign cpl_data = mem_q[rd_q].data;
 
  function automatic logic [IDX_W-1:0] next_idx (input logic [IDX_W-1:0] i);
    next_idx = (i == IDX_W'(DEPTH - 1)) ? '0 : (i + IDX_W'(1));
  endfunction
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wr_q <= '0; rd_q <= '0; cnt_q <= '0;
    end else begin
      if (push) begin
        mem_q[wr_q].desc <= rsp_desc;
        mem_q[wr_q].data <= rsp_data;
        wr_q <= next_idx(wr_q);
      end
      if (pop) rd_q <= next_idx(rd_q);
 
      case ({push, pop})
        2'b10:   cnt_q <= cnt_q + CNT_W'(1);
        2'b01:   cnt_q <= cnt_q - CNT_W'(1);
        default: cnt_q <= cnt_q;
      endcase
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. A decoupling queue between the target's response rate and the link's availability — §3's structure, made concrete. Response identity and response data are one entry, for Chapter 12.2 §4's reason.

State. Entries, pointers, occupancy.

Contract. The response producer relies on rsp_ready reflecting real capacity and must not drop a response it offered but that was refused — it must hold it. The TX path relies on the descriptor being stable while cpl_ready is low.

Failure — four. Gating rsp_ready on !full alone stalls the target at full-rate steady state. A bypass path from rsp_* to cpl_* lets a response overtake earlier queued ones, which reorders the return stream. Natural pointer rollover aliases entries at non-power-of-two DEPTH. And treating a refused response as consumed loses a Completion entirely — and a lost Completion is a read that never resolves, which is the most expensive failure available on the return path.

Deliberately simplified: one data beat per entry; no per-Traffic-Class queueing; no arbitration against other outbound traffic; no flow-control credit interaction; the split decision is upstream.

Production implication: a real return path arbitrates against other outbound traffic, respects flow control, and decides how to divide a response according to the rules Module 13 owns. The decoupling structure above is unchanged by any of that.

8. RTL — Completion Progress Tracker

The chapter's most important new block. It answers exactly one question: is this read finished?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Per-read progress accounting on the Requester side.
// That a read may require more than one Completion: NORMATIVE consequence of
// MPS bounding Completion payloads (section 2).
// The accounting scheme, the retire condition and the error outputs:
// ILLUSTRATIVE IMPLEMENTATION ACCOUNTING — this does NOT model the PCIe rules
// governing how a Completer may legally divide a response (Module 13).
module cpl_progress_tracker #(
  parameter int CTXS  = 8,
  parameter int LEN_W = 11,
  parameter int IDX_W = (CTXS <= 1) ? 1 : $clog2(CTXS)
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- A read becomes outstanding --------------------------------------
  input  logic               open_valid,
  input  logic [IDX_W-1:0]   open_id,
  input  logic [LEN_W-1:0]   open_expected_dw,
 
  // ---- A correlated Completion chunk arrives ---------------------------
  // Correlation has ALREADY happened upstream. This block accounts; it does
  // not identify (section 5).
  input  logic               chunk_valid,
  input  logic [IDX_W-1:0]   chunk_id,
  input  logic [LEN_W-1:0]   chunk_dw,
 
  // ---- Decisions -------------------------------------------------------
  output logic               chunk_credited,   // counted toward this read
  output logic               read_retire,      // extent complete THIS cycle
  output logic [LEN_W-1:0]   remaining_dw,     // for the credited read
 
  // ---- Reported conditions ---------------------------------------------
  output logic               unknown_ctx_error,
  output logic               overrun_error,
  output logic               reopen_error,
  // An identifier outside 0..CTXS-1 was presented. Distinct from
  // unknown_ctx_error, which means "in range but not active".
  output logic               illegal_id_error
);
 
  logic [LEN_W-1:0] remain_q [CTXS];
  logic [CTXS-1:0]  active_q;
 
  // ---- RANGE SAFETY ----------------------------------------------------
  // IDX_W = $clog2(CTXS) can express values >= CTXS whenever CTXS is not a
  // power of two. At CTXS = 5, IDX_W is 3 and the interface can present
  // 5, 6 or 7 — indices that do not exist. Every array access below is
  // therefore GUARDED, and the guard comes first.
  // Compared one bit wider than the identifier, so CTXS is representable
  // even when it is an exact power of two. A same-width comparison would
  // truncate CTXS to zero at CTXS = 8 and reject every legal identifier.
  localparam int CHK_W = IDX_W + 1;
  wire open_id_legal  = (CHK_W'(open_id)  < CHK_W'(CTXS));
  wire chunk_id_legal = (CHK_W'(chunk_id) < CHK_W'(CTXS));
 
  // Guarded reads. The defaults are chosen so that an illegal identifier
  // looks INACTIVE and EMPTY — it can therefore never satisfy `hit`, never
  // be credited, and never retire anything.
  logic             chunk_active;
  logic [LEN_W-1:0] chunk_remaining;
  logic             open_active;
 
  always_comb begin
    chunk_active    = 1'b0;
    chunk_remaining = '0;
    open_active     = 1'b0;
 
    if (chunk_id_legal) begin
      chunk_active    = active_q[chunk_id];
      chunk_remaining = remain_q[chunk_id];
    end
    if (open_id_legal)
      open_active = active_q[open_id];
  end
 
  // Every downstream decision now reads the GUARDED values, never the
  // arrays directly.
  wire hit = chunk_valid && chunk_id_legal && chunk_active;
 
  // Progress can never exceed what is still expected. An overrun is a
  // reported condition, NOT a saturating subtraction — silently clamping
  // would let a too-large chunk retire a read that is not complete.
  wire overrun = hit && (chunk_dw > chunk_remaining);
  wire credit  = hit && !overrun && (chunk_dw != '0);
 
  assign chunk_credited = credit;
  assign remaining_dw   = chunk_remaining;
 
  // RETIRE only when the chunk completes the expected extent. A partial
  // return NEVER retires — section 5, and the single most important line
  // in this module.
  assign read_retire    = credit && (chunk_dw == chunk_remaining);
 
  // Allocation is likewise gated on legality.
  wire do_open = open_valid && open_id_legal && !open_active;
 
  logic unk_q, ovr_q, reo_q, ill_q;
  assign unknown_ctx_error = unk_q;
  assign overrun_error     = ovr_q;
  assign reopen_error      = reo_q;
  assign illegal_id_error  = ill_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < CTXS; i++) remain_q[i] <= '0;
      active_q <= '0;
      unk_q <= 1'b0; ovr_q <= 1'b0; reo_q <= 1'b0; ill_q <= 1'b0;
    end else begin
      // Both write paths are gated on legality, so no out-of-range index
      // ever reaches an array. This is STRUCTURAL prevention — the module
      // does not rely on any language behaviour for illegal indices.
      if (do_open) begin
        remain_q[open_id] <= open_expected_dw;
        active_q[open_id] <= 1'b1;
      end
 
      if (credit) begin
        remain_q[chunk_id] <= chunk_remaining - chunk_dw;
        if (chunk_dw == chunk_remaining)
          active_q[chunk_id] <= 1'b0;
      end
 
      // Reported, never acted on. None of these writes any entry.
      // Note the ordering of the two "unknown" cases: an out-of-range
      // identifier is illegal_id_error, not unknown_ctx_error, because
      // they call for different investigations (section 8a).
      if (chunk_valid && !chunk_id_legal)                  ill_q <= 1'b1;
      if (open_valid  && !open_id_legal)                   ill_q <= 1'b1;
      if (chunk_valid &&  chunk_id_legal && !chunk_active) unk_q <= 1'b1;
      if (overrun)                                         ovr_q <= 1'b1;
      if (open_valid  &&  open_id_legal  &&  open_active)  reo_q <= 1'b1;
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. One remaining-DW counter and one active bit per read. Deliberately not merged with the correlation lookup — §5's two responsibilities stay in two blocks so that a correlation bug and an accounting bug cannot hide behind each other.

State. remain_q per context, an activity bitmap, four sticky error flags. The bitmap is exported-shaped rather than a per-entry struct so a reset assertion can cover every entry at once.

Range safety, and why it is not optional. IDX_W is $clog2(CTXS), which can express values ≥ CTXS whenever CTXS is not a power of two. At CTXS = 5, IDX_W is 3 and the interface can present 5, 6 or 7 — indices that do not exist.

So every array access is guarded, and the guard comes first. The module reads active_q and remain_q only inside if (*_id_legal), with defaults chosen so an illegal identifier looks inactive and empty — it therefore cannot satisfy hit, cannot be credited, and cannot retire anything. Both write paths are gated on the same legality, so no out-of-range index ever reaches an array. This is structural prevention: the module does not rely on any language behaviour for illegal indices, because that behaviour is not something a design should be trusting.

And the legality comparison is one bit wider than the identifier. A same-width compare would truncate CTXS to zero at CTXS = 8 and reject every legal identifier — a correct-looking fix that breaks the power-of-two case it was not aimed at.

Cycle behaviour. Opening a read sets its expectation. A credited chunk decrements it. read_retire pulses for exactly one cycle, on the chunk that brings the remainder to zero. An entry is reusable the following cycle.

Contract. The caller relies on read_retire meaning the whole expected extent has arrivednot the client has taken it (Chapter 12.1 §12), and not the last packet arrived. Upstream guarantees chunk_id is the result of a correlation lookup, not an arrival counter.

Failure — five, and the first is the reason the module exists. Retiring on any credited chunk rather than on the completing one frees the context while data is in flight (Chapter 12.1 §8). Using a saturating subtraction instead of the overrun check lets an oversized chunk zero the remainder and retire a read that is short. Crediting a chunk for an inactive context corrupts an entry that belongs to a different read. Reopening an already-active context silently overwrites a live read's expectation — which is why reopen_error exists rather than the write simply being allowed. And indexing the arrays before the legality check lets an out-of-range identifier read or write outside the table at any non-power-of-two CTXS.

Deliberately simplified: no Completion Status handling (Module 13); no timeout; no modelling of the PCIe rules that constrain how a response may be divided — this counts DW and nothing more; one chunk per cycle on this interface, stated explicitly in §10.

8a. Two Different Kinds of "Unknown"

An identifier that is out of range and an identifier that is in range but inactive are different faults, and merging them costs a debugging session.

illegal_id_errorunknown_ctx_error
Meansthe identifier is outside 0..CTXS-1in range, but no read is active there
Implies about the senderit produced a value the table cannot representit produced a plausible value at the wrong time
Usual causea width or mapping bug upstream — a Tag slice, an ID translation, or a CTXS that is not a power of twoa context freed too early, or a duplicate/straggling Completion
Where to lookthe correlation lookup and its identifier widththe retire condition, and Chapter 12.1 §8

Reporting them separately is what makes the distinction usable. A single "unknown context" flag would fire for both, and the first instinct — check whether a context was freed early — is the wrong one half the time.

And note that an illegal identifier is a statement about this design's table, not a protocol violation. A Completion carrying a correlation field this Requester never issued is a real and separate condition; an identifier that does not fit the table is a local mapping bug, and calling it a protocol error would send the investigation to the wrong layer entirely.

9. RTL — Return Result Router

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Deliver credited Completion data to the correct local
// destination, decoupled from the local consumer's readiness.
// The separation of protocol resolution from local delivery: ARCHITECTURAL
// (Chapter 12.1 section 12). The interface and depth: ILLUSTRATIVE.
module cpl_result_router #(
  parameter int DEPTH  = 4,
  parameter int DEST_W = 8,
  parameter int DATA_W = 128
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- A credited chunk, with the destination from RETAINED context -----
  // dest comes from the context entry, never from the arriving packet and
  // never from arrival order (Chapter 12.1 section 11).
  input  logic                 in_valid,
  input  logic [DEST_W-1:0]    in_dest,
  input  logic [DATA_W-1:0]    in_data,
  input  logic [DATA_W/8-1:0]  in_byte_valid,
  input  logic                 in_err,
  input  logic                 in_final,      // this chunk retired the read
 
  // ---- To the local consumer -------------------------------------------
  output logic                 out_valid,
  input  logic                 out_ready,
  output logic [DEST_W-1:0]    out_dest,
  output logic [DATA_W-1:0]    out_data,
  output logic [DATA_W/8-1:0]  out_byte_valid,
  output logic                 out_err,
  output logic                 out_final,
 
  output logic                 overflow_error
);
 
  localparam int IDX_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH);
  localparam int CNT_W = $clog2(DEPTH + 1);
 
  typedef struct packed {
    logic [DEST_W-1:0]   dest;
    logic [DATA_W-1:0]   data;
    logic [DATA_W/8-1:0] bv;
    logic                err;
    logic                fin;
  } res_t;
 
  res_t             mem_q [DEPTH];
  logic [IDX_W-1:0] wr_q, rd_q;
  logic [CNT_W-1:0] cnt_q;
  logic             ovf_q;
 
  wire full = (cnt_q == CNT_W'(DEPTH));
  wire pop  = out_valid && out_ready;
 
  // Same effective-capacity rule as section 7. The input here CANNOT be
  // backpressured meaningfully — the data has already arrived from the
  // fabric — so refusing it at full-with-pop would discard a chunk the
  // buffer had room for.
  wire can_accept = !full || pop;
  wire push       = in_valid && can_accept;
 
  assign out_valid      = (cnt_q != '0);
  assign out_dest       = mem_q[rd_q].dest;
  assign out_data       = mem_q[rd_q].data;
  assign out_byte_valid = mem_q[rd_q].bv;
  assign out_err        = mem_q[rd_q].err;
  assign out_final      = mem_q[rd_q].fin;
  assign overflow_error = ovf_q;
 
  function automatic logic [IDX_W-1:0] next_idx (input logic [IDX_W-1:0] i);
    next_idx = (i == IDX_W'(DEPTH - 1)) ? '0 : (i + IDX_W'(1));
  endfunction
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wr_q <= '0; rd_q <= '0; cnt_q <= '0; ovf_q <= 1'b0;
    end else begin
      if (push) begin
        mem_q[wr_q].dest <= in_dest;
        mem_q[wr_q].data <= in_data;
        mem_q[wr_q].bv   <= in_byte_valid;
        mem_q[wr_q].err  <= in_err;
        mem_q[wr_q].fin  <= in_final;
        wr_q <= next_idx(wr_q);
      end
      if (pop) rd_q <= next_idx(rd_q);
 
      case ({push, pop})
        2'b10:   cnt_q <= cnt_q + CNT_W'(1);
        2'b01:   cnt_q <= cnt_q - CNT_W'(1);
        default: cnt_q <= cnt_q;
      endcase
 
      // Genuinely no room: full, nothing leaving, something arriving.
      if (in_valid && full && !pop) ovf_q <= 1'b1;
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. A decoupling buffer carrying the local destination alongside the data, so that delivery never depends on order.

State. Entries, pointers, occupancy, a sticky overflow flag.

Contract. The consumer relies on the result being stable while it stalls. The upstream relies on in_dest having come from the context entry — this module cannot check that and does not claim to.

Failure — three. Deriving out_dest from arrival order rather than carrying it per entry delivers correctly-correlated data to the wrong client. Gating push on !full alone drops a chunk at full-rate steady state. And dropping a chunk without reporting produces a read that retired at the protocol level and delivered nothing.

Deliberately simplified: one queue rather than per-destination queues; abstracted error status; whole-chunk granularity.

10. Same-Cycle Contract on the Return Path

Stated explicitly, because the return path is where same-cycle assumptions quietly accumulate.

InterfaceContract
§7 response inat most one response event per cycle. A Completer whose target can produce two simultaneously needs a wider interface or an input arbiter — not modelled here.
§8 chunk inat most one correlated chunk per cycle. Two Completions landing together require an arbiter upstream.
§8 open + chunkmay occur in the same cycle for different contexts. For the same context, open_valid on an active entry is reopen_error, not a race.
§8 retire + opena retiring context is reusable the following cycle, never the same cycle. This removes the window in which a straggler could match a newly opened read.
§9 push + poplegal and conserving. Full + pop + push is accepted, not overflow.

The last row is Chapter 12.1's corrected bug, and it is repeated here because it is the same shape in a different module: can_accept = !full || pop, never !full alone. Every queue on the return path uses that form, and any that does not will drop data at exactly the moment it is busiest.

11. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over cpl_gen_queue, cpl_progress_tracker and cpl_result_router.
// LOCAL flow contracts plus the normative consequence that a read may need
// more than one Completion. NOT claims about how a Completer may legally
// divide a response (Module 13), and NOT claims about Completion packet
// legality.
 
// GENERATOR — P1: one accepted response event produces exactly one queued
// descriptor. (gen_in_count / gen_out_count are testbench counters.)
property p_one_descriptor_per_response;
  @(posedge clk) disable iff (!rst_n)
  (cpl_valid && cpl_ready) |-> (gen_out_count + 1 <= gen_in_count);
endproperty
a_one_desc : assert property (p_one_descriptor_per_response);
 
// GENERATOR — P2: the queued descriptor and its data are stable while the
// return TX path stalls. Identity and payload do not drift apart.
property p_desc_stable;
  @(posedge clk) disable iff (!rst_n)
  (cpl_valid && !cpl_ready) |=> (cpl_valid && $stable({cpl_desc, cpl_data}));
endproperty
a_desc_stable : assert property (p_desc_stable);
 
// GENERATOR — P3: a refused response is not consumed. The producer must be
// able to re-offer it — a lost response is a read that never resolves.
property p_refused_not_consumed;
  @(posedge clk) disable iff (!rst_n)
  (rsp_valid && !rsp_ready) |-> !push;
endproperty
a_not_consumed : assert property (p_refused_not_consumed);
 
// GENERATOR — P4: full + pop accepts. The target must not stall at the
// full-rate steady state.
property p_full_pop_accepts;
  @(posedge clk) disable iff (!rst_n)
  (full && pop) |-> rsp_ready;
endproperty
a_full_pop : assert property (p_full_pop_accepts);
 
// TRACKER — P5: THE CENTRAL PROPERTY. A partial return NEVER retires a read.
property p_partial_does_not_retire;
  @(posedge clk) disable iff (!rst_n)
  (chunk_credited && (chunk_dw < remaining_dw)) |-> !read_retire;
endproperty
a_no_partial_retire : assert property (p_partial_does_not_retire);
 
// TRACKER — P6: and the converse. The chunk that completes the extent DOES
// retire it. Together P5 and P6 pin the retire condition exactly.
property p_complete_retires;
  @(posedge clk) disable iff (!rst_n)
  (chunk_credited && (chunk_dw == remaining_dw)) |-> read_retire;
endproperty
a_complete_retires : assert property (p_complete_retires);
 
// TRACKER — P7: CONSERVATION. Progress never exceeds the expected extent.
property p_progress_bounded;
  @(posedge clk) disable iff (!rst_n)
  chunk_credited |-> (chunk_dw <= remaining_dw);
endproperty
a_bounded : assert property (p_progress_bounded);
 
// TRACKER — P8: an oversized chunk is REPORTED, not clamped. A saturating
// subtraction would zero the remainder and retire a short read.
// NOTE the guarded signals: the properties read chunk_active/chunk_remaining,
// never the arrays, for the same reason the RTL does.
property p_overrun_reported_not_absorbed;
  @(posedge clk) disable iff (!rst_n)
  (chunk_valid && chunk_id_legal && chunk_active
                && (chunk_dw > chunk_remaining))
    |-> (!chunk_credited && !read_retire) ##1 overrun_error;
endproperty
a_overrun : assert property (p_overrun_reported_not_absorbed);
 
// TRACKER — P9: ISOLATION. A chunk for an in-range but inactive context
// changes nothing. (remain_snapshot / active_snapshot are testbench copies.)
property p_inactive_ctx_harmless;
  @(posedge clk) disable iff (!rst_n)
  (chunk_valid && chunk_id_legal && !chunk_active)
    |=> ((remain_q == $past(remain_snapshot))
      && (active_q == $past(active_snapshot))
      && unknown_ctx_error);
endproperty
a_isolated : assert property (p_inactive_ctx_harmless);
 
// RANGE — P9a: THE STRUCTURAL PROPERTY. An out-of-range identifier changes
// NOTHING in the table. This is the one that fails for a design which
// indexes before it checks, at any non-power-of-two CTXS.
property p_illegal_id_changes_nothing;
  @(posedge clk) disable iff (!rst_n)
  ((chunk_valid && !chunk_id_legal) || (open_valid && !open_id_legal))
    |=> ((remain_q == $past(remain_snapshot))
      && (active_q == $past(active_snapshot))
      && illegal_id_error);
endproperty
a_illegal_inert : assert property (p_illegal_id_changes_nothing);
 
// RANGE — P9b: an illegal open never allocates.
property p_illegal_open_never_allocates;
  @(posedge clk) disable iff (!rst_n)
  (open_valid && !open_id_legal) |=> (active_q == $past(active_snapshot));
endproperty
a_illegal_open : assert property (p_illegal_open_never_allocates);
 
// RANGE — P9c: an illegal chunk is never credited and never retires.
property p_illegal_chunk_never_credited;
  @(posedge clk) disable iff (!rst_n)
  (chunk_valid && !chunk_id_legal) |-> (!chunk_credited && !read_retire);
endproperty
a_illegal_chunk : assert property (p_illegal_chunk_never_credited);
 
// RANGE — P9d: the two "unknown" conditions are DISJOINT (section 8a).
// An identifier is out of range or it is in range; it is never both, and a
// design that raised both would have made the distinction useless.
property p_error_kinds_disjoint;
  @(posedge clk) disable iff (!rst_n)
  chunk_valid |-> !($rose(illegal_id_error) && $rose(unknown_ctx_error));
endproperty
a_disjoint : assert property (p_error_kinds_disjoint);
 
// RANGE — P9e: state changes only ever occur for a LEGAL selected index.
// The generate loop makes it per-entry, so a write to a neighbouring entry
// cannot hide inside an aggregate comparison.
generate for (genvar g = 0; g < CTXS; g++) begin : g_legal_write
  a_write_needs_legal : assert property (@(posedge clk) disable iff (!rst_n)
    (!$stable(active_q[g]) || !$stable(remain_q[g]))
      |-> ($past(do_open && (open_id == IDX_W'(g)))
        || $past(credit  && (chunk_id == IDX_W'(g)))
        || $past(!rst_n)));
end endgenerate
 
// TRACKER — P10: retirement happens at most once per read. read_retire is a
// single-cycle pulse and cannot repeat without a reopen.
property p_retire_once;
  @(posedge clk) disable iff (!rst_n)
  read_retire |=> !active_q[$past(chunk_id)] until_with
                  (do_open && (open_id == $past(chunk_id)));
endproperty
 
// TRACKER — P11: an active context is never silently reopened.
property p_no_silent_reopen;
  @(posedge clk) disable iff (!rst_n)
  (open_valid && open_id_legal && open_active) |=> reopen_error;
endproperty
a_reopen : assert property (p_no_silent_reopen);
 
// ROUTER — P12: the destination is carried per entry. Data credited to one
// read never emerges against another read's destination.
// (exp_dest is a testbench map keyed by a shadow chunk ID.)
property p_dest_preserved;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && out_ready) |-> (out_dest == exp_dest[out_seq]);
endproperty
a_dest : assert property (p_dest_preserved);
 
// ROUTER — P13: the result is stable while the local consumer stalls.
property p_result_stable;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready)
    |=> (out_valid && $stable({out_dest, out_data, out_byte_valid,
                               out_err, out_final}));
endproperty
a_result_stable : assert property (p_result_stable);
 
// ROUTER — P14: full + pop + push is accepted, not overflow.
property p_router_full_pop_push;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && full && pop) |-> (push && !$rose(overflow_error));
endproperty
a_router_full_pop : assert property (p_router_full_pop_push);
 
// ROUTER — P15: a chunk is never silently lost.
property p_no_silent_loss;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && full && !pop) |=> overflow_error;
endproperty
a_no_loss : assert property (p_no_silent_loss);
 
// RESET — P16: reset clears local flow state in all three blocks. This is a
// LOCAL TEACHING-MODEL contract, not a statement about PCIe system reset.
property p_reset_clears_flow;
  @(posedge clk)
  !rst_n |=> ((cnt_q == '0) && (active_q == '0) && !out_valid && !cpl_valid);
endproperty
// And every remaining count, per entry — an aggregate check on active_q alone
// would pass a design that cleared the bitmap and left stale expectations.
generate for (genvar g = 0; g < CTXS; g++) begin : g_reset_remain
  a_reset_remain : assert property (@(posedge clk)
    !rst_n |=> (remain_q[g] == '0));
end endgenerate
a_reset : assert property (p_reset_clears_flow);

Liveness, with assumptions:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A1: the return TX path eventually accepts a queued Completion.
assume property (@(posedge clk) disable iff (!rst_n)
  cpl_valid |-> s_eventually cpl_ready);
// A2: the local consumer eventually accepts a presented result.
assume property (@(posedge clk) disable iff (!rst_n)
  out_valid |-> s_eventually out_ready);
// A3: every outstanding read eventually receives its full extent. PCIe does
// NOT guarantee this — it is precisely why Completion timeout exists.
assume property (@(posedge clk) disable iff (!rst_n)
  open_valid |-> s_eventually read_retire);
 
// L1: under A1-A3, an opened read eventually delivers its final chunk.
property p_read_eventually_delivered;
  @(posedge clk) disable iff (!rst_n)
  open_valid |-> s_eventually (out_valid && out_ready && out_final);
endproperty
a_liveness : assert property (p_read_eventually_delivered);

P5 and P6 are the pair that defines "finished", and neither is sufficient alone. P5 forbids early retirement — the catastrophe. P6 forbids the opposite failure, a read whose final chunk arrives and which never retires, occupying a context forever and slowly starving the design of concurrency. A design that satisfies P5 by never retiring anything is not correct, and only P6 says so.

P8 is deliberately stated as "not credited and reported" rather than as a bound on the counter. A saturating subtraction satisfies a bound — it just produces the wrong answer quietly. The property has to assert the absence of the credit, not the sanity of the result.

P9 uses whole-array snapshots for Chapter 12.1's reason: the bug it catches is a write to some entry when an unknown identifier arrives, and checking only the indexed entry would miss a decode that indexed elsewhere.

P9a–P9e are the range-safety set, and P9e is the one that cannot be faked. P9a–P9c check that an illegal identifier is inert; P9e is stated per entry and says a table entry changes only when that entry was the legal selected index. An aggregate comparison would let a write to a neighbouring entry hide — which is precisely what an unguarded out-of-range index produces in a simulator that wraps rather than errors.

P9d asserts that the two error kinds are disjoint, which is what makes §8a's diagnostic table usable. A design that raised both for the same event would have collapsed the distinction back into one flag with extra steps.

P11 makes reopening an error rather than a race. A design could plausibly treat open_valid on an active context as "just overwrite it" — and that silently discards a live read's expectation, after which its Completions overrun a remainder that belongs to a different transaction. Reporting it is the only behaviour that leaves evidence.

12. Verification

Monitors observe: the target-response interface; the generation queue's output; the correlated-chunk interface with a shadow ID; the tracker's decisions and reported conditions; and the local result interface.

The scoreboard maintains its own expected-remaining map, keyed by context, decremented by its own arithmetic. It must not read remain_q, active_q, or call the tracker's credit logic. It maintains its own destination map keyed by the shadow ID.

And it must check the negative case explicitly: for every read it opened, no read_retire may be observed before the expected total has been delivered. A scoreboard that only checks the final total cannot distinguish a correct design from one that retired early and happened to receive the rest anyway.

Generation

  • One response, one Completion descriptor. The baseline.
  • A burst of responses faster than the TX path drains. Verify none is lost (P3) and rsp_ready backpressures the target.
  • The queue driven to full, then a response offered. Verify rsp_ready low and the response is not consumed.
  • Full with a simultaneous pop. Verify the response is accepted (P4).
  • The TX path stalled with a descriptor queued. Verify stability of descriptor and data together (P2).
  • A status-only response (has_data low) interleaved with data-carrying ones. Verify both pass through intact.
  • DEPTH = 1 and a non-power-of-two DEPTH.

Progress accounting

  • A read answered by one Completion covering the whole extent. Verify retire on that chunk (P6).
  • A read answered by two chunks, the first partial. Verify no retire on the first (P5) and retire on the second.
  • A read answered by many small chunks. Verify the remainder decrements correctly throughout and retires exactly once.
  • A final chunk smaller than the others. The residue case.
  • A chunk larger than the remainder. Verify overrun_error, no credit, no retire (P8), and that the remainder is unchanged.
  • A zero-DW chunk. Verify no credit.
  • A chunk for an inactive context. Verify nothing changes and unknown_ctx_error sets (P9).
  • A duplicate final chunk after retirement. Verify it is treated as unknown, not as fresh progress.
  • open_valid on an already-active context. Verify reopen_error and that the live expectation is not overwritten (P11).

Range safety — non-power-of-two CTXS

  • CTXS = 5, then present chunk_id = 5, 6 and 7. Verify illegal_id_error, no credit, no retire, and no entry changed (P9a, P9c). This is the required stimulus — at CTXS = 4 or 8 these identifiers are unreachable and the bug is invisible.
  • CTXS = 5, open_id = 5, 6, 7. Verify no allocation (P9b).
  • CTXS = 3, 6, 7. Sweep the other non-power-of-two sizes and check every out-of-range identifier at each.
  • CTXS = 8 — the power-of-two case. Verify every identifier 0–7 is accepted as legal. This is the counter-test for a same-width legality compare, which would truncate CTXS to zero and reject all of them.
  • CTXS = 1. Verify identifier 0 is legal and the index width guard holds.
  • An illegal identifier immediately followed by a legal one. Verify the legal one is processed normally and the illegal one left no residue.
  • Verify illegal_id_error and unknown_ctx_error are never both raised for one event (P9d).
  • Open and credit in the same cycle for different contexts. Verify both take effect.
  • Retire and reopen the same index on consecutive cycles. Verify the reuse is clean.

Return ordering and interleaving

  • Two reads outstanding, Completions returning in issue order.
  • Two reads outstanding, returning in the opposite order. Verify each read's data reaches its own destination (P12) — the direct test that arrival order is not identity.
  • Chunks from two multi-chunk reads interleaved. The strongest test in the chapter: verify both progress counters advance independently and neither retires early.
  • Several reads interleaved with different extents and different chunk counts.

Local delivery

  • The consumer stalled for a long period. Verify results buffer, stay stable (P13), and that the tracker's contexts still retire — protocol resolution must not wait on the client (Chapter 12.1 §12).
  • The router at full with a simultaneous pop and push. Verify acceptance (P14).
  • The router genuinely full. Verify overflow_error and no silent loss (P15).
  • Reset with reads outstanding and results buffered. Verify all flow state clears (P16).

Which test kills which bug

Injected faultWhat catches it
context retired on the first chunkP5, and the two-chunk test
read never retires on its final chunkP6, and the scoreboard's completion timeout
oversized chunk clamped instead of reportedP8
chunk credited to the wrong contextP9, and the interleaved-chunk test
data delivered to the wrong local clientP12, with per-read distinctive payloads
response dropped when the generation queue is fullP3
target stalled at full-with-popP4
result chunk dropped at full+pop+pushP14
active context silently reopenedP11
array indexed before the legality checkP9a/P9e, at non-power-of-two CTXS
legality compared at the identifier's own widththe CTXS = 8 counter-test — every legal ID rejected
out-of-range identifier reported as unknown-contextP9d, and §8a's table
descriptor and data drift apart under TX stallP2

Coverage should include: single-chunk and multi-chunk reads; every chunk-count from one to the modelled maximum; final chunks of full and partial size; all four reported error conditions; CTXS at 1, a power of two, and several non-powers of two, with every out-of-range identifier exercised at each; both queue depths' corner cases; every ordering permutation for two outstanding reads; and consumer stalls long enough to exercise the retire-versus-deliver separation.

13. Debugging

This is §3, and the diagnosis is a walk down four boundaries.

  1. Was the response event accepted? rsp_valid && rsp_ready. If rsp_ready was low, the generation queue was full — and that is a downstream problem wearing this symptom.
  2. Is a descriptor queued? If the handshake completed and occupancy did not rise, the queue's push condition is wrong.
  3. Did cpl_valid rise? If not, the queue's output logic is broken.
  4. Did cpl_ready ever rise? If not, the fault is in arbitration or flow control on the outbound path — not in Completion generation at all.

The general error this scenario corrects: the target has the data is boundary 1 and a packet left is boundary 4. Three things can go wrong between them, and starting the investigation at the Requester wastes all of it.

A Completion reaches the Requester but the context never retires

Correlation succeeded — the chunk was credited — so the fault is in accounting.

Three shapes, distinguished by the remainder. If remaining_dw never reaches zero, chunks are being under-counted or one was dropped: sum the DW actually delivered and compare against the expectation. If it reached zero and read_retire never pulsed, the retire condition compares the wrong things (P6). If it wrapped to a large value, the overrun check is missing and a subtraction underflowed.

And check unknown_ctx_error. A chunk that was not credited leaves the remainder untouched, which looks identical to a chunk that was never sent.

A context retires early and later data arrives orphaned

Premature retirement — §5 and P5.

The signature is unmistakable: unknown_ctx_error sets shortly after a read completed successfully. The stragglers arrived for a context that no longer exists.

Two causes. The retire condition is chunk_credited rather than chunk_dw == remaining_dw — retiring on any chunk. Or the expectation was set wrong at open time, so the read appeared complete earlier than it was.

How to tell them apart in one observation: compare the total DW delivered against the expectation. If the total is short, it retired early on a partial chunk. If the total matches, the expectation was wrong — and the bug is at open_valid, not in the tracker.

And the downstream consequence is Chapter 12.1 §8's: a freed context is reusable, so a later read takes the identifier and the stragglers resolve against it. Data from one read is delivered as the answer to another, with both reads reporting success.

illegal_id_error sets and no read is affected

The table is intact — that is what the property guarantees — so nothing downstream is corrupted. The question is where the identifier came from.

It is a local mapping fault, not a protocol one (§8a). Something upstream produced an index the table cannot represent. Three candidates, in order. The correlation lookup's output width does not match IDX_W. A Tag-to-index translation truncated or failed to bound its result (Chapter 11.3 §6). Or CTXS was changed to a non-power-of-two value and something upstream still assumes the full 2^IDX_W range is usable.

The observation: print the offending identifier and compare against CTXS. If it is in [CTXS, 2^IDX_W), the third cause is almost certain — and it is the one that appears the day someone changes a parameter from 8 to 5 to save area.

The right data reaches the wrong local client

Correlation and accounting are both fine — the destination is wrong.

Check where in_dest came from. It must be read from the context entry, populated at open time. If it was derived from arrival order, from a queue position, or from anything in the arriving packet, that is the bug — and it works perfectly with one read outstanding, which is why it survives bring-up.

The confirming test is §12's: two reads outstanding, Completions returning in the opposite order. A design that routes by arrival delivers each read's data to the other's client, and both clients receive plausible data.

14. Common Misconceptions

  • "Target data ready means the Completion has been transmitted." Three boundaries separate them, and the queue between can be full (§3).
  • "One target response always equals one Completion packet." A response larger than what one payload may carry becomes several Completions (§2). The rules governing that division are Module 13's.
  • "Every Completion carries data." Some non-posted operations resolve with a status-only Completion. A Memory Read's normal successful response carries data; not every Completion does (§2).
  • "A returned packet can be matched by arrival order." It is matched by identity — the Requester ID and correlation field the Completer copied from the Request (§5).
  • "A partial return may free the context." It may not. The context lives until the whole expected extent has arrived (§5, P5).
  • "A local consumer stall can be propagated across PCIe as a ready signal." The remote transmitter does not see an application-style ready from the Requester's local client. The receive architecture must buffer (Chapter 12.1 §12).
  • "The Completion queue cannot overflow because PCIe has flow control." Flow control governs admission between components. It says nothing about a local queue's depth relative to a target's burst rate (§3).
  • "Routing and correlation are the same operation." Routing gets the packet to the right component (Chapter 11.5); correlation identifies which transaction it answers. Different mechanisms, different failure modes (§5).
  • "Completion Status and a local error code are the same concept." §7's err is an abstracted internal flag. The actual Completion Status encodings and their meanings are Module 13's.
  • "Request context is no longer needed once the Endpoint accepts the Request." The Completer needs it to build the response, and the Requester needs it until the read resolves (§4, §5).
  • "Retirement and delivery are the same event." Retirement frees the correlation resource; delivery frees the buffer. Merging them lets a stalled client consume protocol resources (Chapter 12.1 §12).

15. Understanding Check

16. What's Next

This chapter took the return path apart. A Completer having data turned out to be three boundaries away from a packet being on the wire; a Completion arriving turned out to be two decisions away from a read being finished; and retiring a context turned out to be a different event from handing the data to whoever asked for it.

Chapter 12.4 — Examples now puts reads, writes and this return path together in worked traces — including the failure signatures that distinguish a routing problem from a decode problem from an ownership problem.

Module 13 owns everything about the Completion packet that this chapter deliberately abstracted: the Cpl and CplD forms in detail, Completion Status, Byte Count and Lower Address, the rules constraining how a response may be divided, and the ordering matrix. Chapter 12.5 takes the performance consequences of return-path depth and latency.

The idea to carry forward: a Completion arriving is not a read finishing — the accounting decides that, and it must be a separate decision from the correlation that found the read in the first place.