Skip to content

PCIe · Module 24

Protocol Verification — Three Contracts, and an Oracle That Owes You Nothing

A PCIe design is not verified by checking packets. Three different contracts must be proven, each observed somewhere different — and a checker that shares code with the design proves only that the design agrees with itself.

Module 23 built an endpoint. This module proves one works.

And the first thing to get right is what "works" means. A PCIe design that emits well-formed TLPs may still be wrong: its Link may never reach L0, its replay may drop a packet, its Completions may reach the wrong requester. Those are three different contracts, broken in three different places, observable at three different points.

1. Sources, Scope, and What Module 24 Still Owns

2. Three Contracts, Not One Packet Check

"Check the packets" is the intuition, and it is one third of the job.

A Transaction Layer contract is about meaning. Did the read return the bytes that were asked for, to the requester that asked, exactly once? Chapter 23.5 §12 measured a device that emitted perfectly legal Completions and applied 39.7% of them to the wrong context — every packet valid, the contract broken.

A Data Link contract is about one hop. Did this packet cross this Link reliably — retained until acknowledged, retried when not, rejected when its integrity check failed? Its scope is one Link (14.4), which is why Chapter 21.2 §3's ACK tells you the switch received the packet and nothing about the endpoint beyond it.

A Link-state contract is about availability. Did the Link reach L0, stay there, and recover legally when it did not? A design whose Link drops into Recovery under load produces symptoms at every other layer — timeouts, throughput collapse, apparent corruption — and none of them names the cause.

The architectural consequence is the whole chapter: because the contracts are different, the observations must be separate and the failures must be attributable. An environment that merges them into one "did the test pass" answer can tell you something is wrong and never which contract broke.

3. The Layer Plan

Three verification contracts stacked. The Transaction Layer contract is end to end: identity, byte coverage, exactly-once retirement and ordering. The Data Link Layer contract is per link: retention until acknowledgement, retry on failure, and integrity rejection on receive. The Physical and link-state contract is availability: reaching L0 and remaining there, with legal recovery. Each layer is observed at a different point and each fails in a different way.What each verification contract coversTransaction contract — end to endidentity (Requester ID, Tag) · byte coverage · exactly-once retirement · ordering · request answered at allidentity (Requester ID, Tag) · byte coverage · exactly-once retirement · ordering · request answered at allData Link contract — ONE linkretained until acknowledged · retried when delivery failed · integrity checked on receive · an ACK proves the link partner received it, nothing furtherretained until acknowledged · retried when delivery failed · integrity checked on receive · an ACK proves the link partner received it, nothing furtherLink-state contract — availabilityreaches L0 and stays · recovery is legal and bounded · a broken link makes the layers above unprovable, not falsereaches L0 and stays · recovery is legal and bounded · a broken link makes the layers above unprovable, not false
Figure 1 — the three contracts and their scopes. The Transaction Layer's contract spans the whole path from requester to completer and back. The Data Link Layer's contract covers exactly one link, which is why an acknowledgement proves delivery to the link partner only. The Physical and link-state contract covers whether the link is usable at all, and its failure makes the two contracts above unprovable rather than false.

The plan, as a table an engineer can fill in for a real DUT:

TransactionData LinkLink state
ownsmeaning, identity, completenessreliable delivery over one Linkreaching and holding a usable state
observed atthe request/completion interfacesthe packet interface toward the LinkLTSSM state, or its proxy (§4)
can go wrongwrong requester, wrong bytes, never answered, answered twicepacket lost, replayed forever, accepted twice, integrity error ignorednever trains, retrains under load, wrong width or speed
oracleindependent identity-keyed model (§5, §11)per-Link retention/retry accountinglegal-state and legal-transition checking (24.2 owns the properties)
exact rules from13.3, 21.414.4Module 18

Read the "can go wrong" row across. A Link that retrains under load, a Data Link that replays excessively, and a Transaction Layer that mismatches Completions all present as "reads are slow and sometimes wrong." Only separate observation distinguishes them (§15).

4. Observation Points, and What Your DUT Actually Exposes

5. Independence Is the Whole Point

6. Identity Is the Key to Everything

Every model in §11 is keyed by the same thing: (Requester ID, Tag) (21.4 §3).

That is not a coincidence; it is the only stable handle a verification environment has. A transaction has no other name. It is not "the third read" — reads complete out of order (13.3 §2). It is not "the one at address X" — two transactions may target the same address. It is the pair, and if the environment loses it, nothing downstream can be attributed.

Three environment-level rules follow.

Insert on the observed transfer, not the observed offer. The same handshake rule as the design (23.1 §3) — a monitor sampling valid records requests that were never sent.

A live identity must be unique in the model. Chapter 23.5 §12's first model was wrong precisely because it allowed duplicates, and reported the correct design as broken.

And removal happens once, on coverage or a terminal outcome. An environment that removes on the first Completion generates unknown-identity errors for the remaining fragments — the same 80.8% cascade the design would produce (23.5 §12), now inside your testbench, where it will be blamed on the DUT.

7. Stimulus Targets Ownership Boundaries

Random packet contents are the weakest useful stimulus. Every defect measured across Modules 22 and 23 needed a concurrency condition, not an unusual field value:

DefectThe stimulus that exposes itMeasured
header follows live requeststall while a new request is pending68.6% (23.4 §12)
progress on validany backpressure79.1% (23.4 §12)
context freed under a stalled resultstall the result consumer75.9% (23.5 §12)
Tag leak on errorinject Completion errorswedged after 14 jobs (23.3 §14)
tag/data skewmake the context table a RAM87.5% (23.5 §12)
shared countermore than one job in flight62.6% (23.3 §14)
config changes mid-transactionwrite Device Control during a transfer15.3% (22.4 §12)

Not one of those rows is about a field value. Every one is about two things happening at once, or about a resource running out.

So a strong test plan is written in terms of coincidences: valid without ready; a response in the same cycle as an allocation or a free; reset with work outstanding; a table or configuration update while a transaction is owned; resource exhaustion; reordered responses; an error on the final fragment rather than the first.

And the last one deserves its own line, because it is the cheapest high-yield test in this list: errors are usually injected at the start of a transaction, where the design has done nothing yet. Injecting on the final fragment exercises the cleanup path with maximum state to release — which is where Chapter 23.3 §8's leak lives.

8. Negative Tests — What Must Not Happen

Most of the contracts in §3 are prohibitions, and a test suite made only of positive cases cannot see them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a packet must not be duplicated
a Tag must not be reused while its transaction is outstanding
a credit must not be spent twice
a Completion must not be applied to an unknown request
a stale descriptor must not execute after an abort
packet metadata must not change while stalled
a transaction must not retire twice

Each of these is a real defect from Modules 22–23, and each is invisible to a checker that only confirms expected outputs appeared. You have to look for the thing that should be absent.

This is the bridge into Chapter 24.2, which turns prohibitions into a property library. This chapter's contribution is the observation that the prohibitions are the interesting half of the plan — and §13's model demonstrates it, since three of its four injected defects (duplicate identity, early reuse, stale-after-reset) are prohibitions rather than missing outputs.

9. The Environment

A PCIe verification environment. Transaction stimulus and configuration and error stimulus both drive the device under test. Three monitors observe the transaction interface, the data link interface and the link state. Each monitor emits normalized layer events into an event timeline. An independent transaction model keyed by requester identity and tag consumes transaction events. Per-layer checkers consume their own events. Both feed a failure localizer, which names the layer whose contract was broken and reports the correlated history.transaction stimulusconfig + errorstimulusDUTtransaction monitordata link monitorlink-state monitorevent timelineindependent modelper-layer checksfailure localizer12
Figure 2 — a layer-aware verification environment, drawn methodology-neutrally. Stimulus drives transactions and, separately, configuration and error events. Three monitors observe at different points and emit normalized events into a shared timeline. An independent transaction model keyed by identity checks the end-to-end contract, while per-layer checks handle their own contracts, and a failure localizer names the layer whose contract broke.

Four things to read out of the figure.

Three monitors, not one. They observe different points because they prove different contracts (§2), and merging them is what makes a failure unattributable.

Every monitor feeds the timeline. That shared, bounded history is what makes §15's triangulation possible — the Transaction Layer saw the request, the Data Link never saw the transfer is only a sentence you can say if both events are in one ordered record.

The independent model hangs off the transaction monitor and nothing else. It shares no code with the DUT (§5) — that isolation is drawn deliberately, because a line from the DUT to the model would be the mutation this chapter most wants to prevent.

And there is no arrow from the localizer back into stimulus. Checking observes; it does not steer. The same non-interference rule as every monitor in Modules 22 and 23 (23.6 §10, P31).

10. Verification Code — Records, Model, Timeline, Monitor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// COMPILE-TIME. Verification types. Normalized metadata, NOT wire layouts.
package pcie_vplan_pkg;
 
  parameter int MAX_OUTSTANDING = 64;
  parameter int HISTORY_DEPTH   = 256;
  parameter int RID_W = 16;
  parameter int TAG_W = 8;
  parameter int LEN_W = 16;
 
  // The only stable handle a transaction has (§6).
  typedef struct packed {
    logic [RID_W-1:0] requester_id;
    logic [TAG_W-1:0] tag;
  } txn_key_t;
 
  typedef enum logic [2:0] {
    LYR_TL = 3'd0, LYR_DL = 3'd1, LYR_LINK = 3'd2, LYR_CFG = 3'd3
  } layer_e;
 
  typedef enum logic [3:0] {
    EV_REQ_ISSUED   = 4'd0,   // TL: a request transferred
    EV_CPL_RECEIVED = 4'd1,   // TL: a completion transferred
    EV_TXN_RETIRED  = 4'd2,   // TL: byte coverage or terminal
    EV_DL_ACCEPTED  = 4'd3,   // DL: the link partner accepted it
    EV_DL_REPLAY    = 4'd4,   // DL: retransmission
    EV_LINK_UP      = 4'd5,   // LINK: reached the operational state
    EV_LINK_RECOVER = 4'd6,   // LINK: left it and is recovering
    EV_CFG_WRITE    = 4'd7,   // a control-plane change
    EV_RESET        = 4'd8
  } event_e;
 
  typedef struct packed {
    logic            valid;
    layer_e          layer;
    event_e          kind;
    txn_key_t        key;         // meaningful for TL events
    logic [LEN_W-1:0] bytes;
    logic [31:0]     cycle;
    logic            is_error;
  } layer_event_t;
 
  function automatic bit key_eq(input txn_key_t a, input txn_key_t b);
    return (a.requester_id == b.requester_id) && (a.tag == b.tag);
  endfunction
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_vplan_pkg::*;
 
// VERIFICATION-ONLY. THE INDEPENDENT MODEL (§5). It knows the CONTRACT --
// identity, byte coverage, exactly-once retirement -- and nothing about
// how the DUT is built. It calls no DUT function, by construction.
// §13: 140,924 events clean, and every injected defect caught.
module outstanding_model (
  input logic clk,
  input logic rst_n,
 
  // observations, all on TRANSFERS (§6)
  input logic             req_seen,
  input txn_key_t         req_key,
  input logic [LEN_W-1:0] req_bytes,
 
  input logic             cpl_seen,
  input txn_key_t         cpl_key,
  input logic [LEN_W-1:0] cpl_bytes,
  input logic             cpl_error,
 
  input logic             retire_seen,
  input txn_key_t         retire_key,
 
  output int  outstanding,
  output logic err_duplicate_identity,   // a live key issued again
  output logic err_unknown_completion,   // no live transaction owns it
  output logic err_over_return,          // more bytes than requested
  output logic err_early_retire,         // retired before coverage
  output logic err_double_retire,
  output logic err_leak_at_end           // set by the end-of-test check
);
  txn_key_t         k    [MAX_OUTSTANDING];
  logic [LEN_W-1:0] want [MAX_OUTSTANDING];
  logic [LEN_W-1:0] got  [MAX_OUTSTANDING];
  logic             live [MAX_OUTSTANDING];
  logic             term [MAX_OUTSTANDING];
  int               n;
 
  assign outstanding = n;
 
  function automatic int find(input txn_key_t q);
    for (int i = 0; i < MAX_OUTSTANDING; i++)
      if (live[i] && key_eq(k[i], q)) return i;
    return -1;                                  // NEVER defaults to 0 (§6)
  endfunction
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < MAX_OUTSTANDING; i++) begin live[i] <= 1'b0; term[i] <= 1'b0; end
      n <= 0;
      err_duplicate_identity <= 1'b0; err_unknown_completion <= 1'b0;
      err_over_return <= 1'b0; err_early_retire <= 1'b0;
      err_double_retire <= 1'b0; err_leak_at_end <= 1'b0;
    end else begin
      // ---- a request was OBSERVED TO TRANSFER ----------------------
      if (req_seen) begin
        if (find(req_key) >= 0) err_duplicate_identity <= 1'b1;
        else for (int i = 0; i < MAX_OUTSTANDING; i++)
          if (!live[i]) begin
            live[i] <= 1'b1; k[i] <= req_key; want[i] <= req_bytes;
            got[i] <= '0; term[i] <= 1'b0; n <= n + 1;
            break;
          end
      end
 
      // ---- a completion was OBSERVED ------------------------------
      if (cpl_seen) begin
        automatic int j = find(cpl_key);
        if (j < 0) err_unknown_completion <= 1'b1;
        else if (cpl_error) term[j] <= 1'b1;
        else begin
          if ((got[j] + cpl_bytes) > want[j]) err_over_return <= 1'b1;
          else got[j] <= got[j] + cpl_bytes;      // COVERAGE, not packet count
        end
      end
 
      // ---- a retirement was OBSERVED ------------------------------
      if (retire_seen) begin
        automatic int j = find(retire_key);
        if (j < 0) err_double_retire <= 1'b1;
        else begin
          if ((got[j] < want[j]) && !term[j]) err_early_retire <= 1'b1;
          live[j] <= 1'b0; n <= n - 1;
        end
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_vplan_pkg::*;
 
// VERIFICATION-ONLY. The event timeline (§9). A bounded ring of normalized
// observations from ALL monitors, in one order. This is what makes §15's
// triangulation a sentence you can say rather than a guess.
module event_timeline (
  input  logic clk,
  input  logic rst_n,
  input  logic         ev_valid,
  input  layer_event_t ev,
  input  logic         snapshot_req,
 
  output layer_event_t history [HISTORY_DEPTH],
  output int           depth,
  output logic         wrapped,
  output logic         err_index_range      // sticky
);
  layer_event_t h [HISTORY_DEPTH];
  int  wr;
  logic wrap_q, e_q;
 
  assign depth           = wrap_q ? HISTORY_DEPTH : wr;
  assign wrapped         = wrap_q;
  assign err_index_range = e_q;
  always_comb for (int i = 0; i < HISTORY_DEPTH; i++) history[i] = h[i];
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wr <= 0; wrap_q <= 1'b0; e_q <= 1'b0;
      for (int i = 0; i < HISTORY_DEPTH; i++) h[i] <= '0;
    end else if (ev_valid) begin
      // Range-safe by construction (Chapter 23.6 pattern 11): the write
      // index is bounded before use, and an out-of-range write is reported
      // rather than aliasing to entry 0.
      if (wr >= HISTORY_DEPTH) e_q <= 1'b1;
      else begin
        h[wr] <= ev;
        if (wr == HISTORY_DEPTH-1) begin wr <= 0; wrap_q <= 1'b1; end
        else wr <= wr + 1;
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_vplan_pkg::*;
 
// VERIFICATION-ONLY. THE LAYER CONTRACT MONITOR (§2, §4).
// It proves the educational point directly: a Transaction Layer request
// that was issued, and a Data Link acceptance, are DIFFERENT events, and
// neither implies the other. Where a layer is unobservable, its input is
// tied off and `dl_observable` says so -- rather than the environment
// pretending (§4, mutation 26).
module layer_contract_monitor #(parameter bit DL_OBSERVABLE = 1'b1) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic     tl_req_fire,        // TL: request transferred
  input  txn_key_t tl_req_key,
  input  logic     dl_accept,          // DL: link partner accepted (if visible)
  input  txn_key_t dl_key,
  input  logic     dl_replay,
  input  logic     tl_rx_fire,         // TL: the answer came back
  input  txn_key_t tl_rx_key,
  input  logic     link_operational,
 
  output logic dl_observable,
  output int   tl_issued,
  output int   dl_accepted,
  output int   dl_replays,
  output int   tl_returned,
  output logic err_return_without_issue,
  output logic err_dl_without_tl,
  output logic err_issue_while_link_down
);
  assign dl_observable = DL_OBSERVABLE;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      tl_issued<=0; dl_accepted<=0; dl_replays<=0; tl_returned<=0;
      err_return_without_issue<=1'b0; err_dl_without_tl<=1'b0;
      err_issue_while_link_down<=1'b0;
    end else begin
      if (tl_req_fire) begin
        tl_issued <= tl_issued + 1;
        // A request accepted while the Link is not operational is a
        // cross-layer contract violation, not a TL bug.
        if (!link_operational) err_issue_while_link_down <= 1'b1;
      end
      if (DL_OBSERVABLE && dl_accept) begin
        dl_accepted <= dl_accepted + 1;
        // A link-layer acceptance with no corresponding TL request means
        // the two observations disagree -- exactly the case §15 triangulates.
        if (tl_issued == 0) err_dl_without_tl <= 1'b1;
      end
      if (DL_OBSERVABLE && dl_replay) dl_replays <= dl_replays + 1;
      if (tl_rx_fire) begin
        tl_returned <= tl_returned + 1;
        if (tl_returned >= tl_issued) err_return_without_issue <= 1'b1;
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_vplan_pkg::*;
 
// SYNTHESIZABLE. Design-for-verification hooks (§4). These are SILICON
// DEBUG observability, not testbench components -- they exist so a layer
// that is otherwise invisible becomes inferable. Chapter 25 owns using
// them in the lab; this chapter owns the argument for having them.
module dfv_observability #(parameter int N_CLASS = 3) (
  input  logic clk,
  input  logic rst_n,
  input  logic             txn_issued,
  input  logic             txn_retired,
  input  logic             replay_event,
  input  logic [3:0]       link_state,
  input  logic             first_error,
  input  logic [7:0]       first_error_cause,
  input  logic [N_CLASS-1:0] credit_blocked,      // pending AND short (22.3 §5)
  input  logic             clear,
 
  output logic [15:0] outstanding_count,
  output logic [31:0] replay_count,
  output logic [3:0]  link_state_now,
  output logic        err_captured,
  output logic [7:0]  err_first_cause,
  output logic [31:0] credit_block_cycles [N_CLASS]
);
  logic [15:0] out_q; logic [31:0] rep_q; logic cap_q; logic [7:0] cause_q;
  logic [31:0] cb_q [N_CLASS];
 
  assign outstanding_count = out_q;
  assign replay_count      = rep_q;
  assign link_state_now    = link_state;
  assign err_captured      = cap_q;
  assign err_first_cause   = cause_q;
  always_comb for (int c = 0; c < N_CLASS; c++) credit_block_cycles[c] = cb_q[c];
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      out_q<='0; rep_q<='0; cap_q<=1'b0; cause_q<='0;
      for (int c = 0; c < N_CLASS; c++) cb_q[c] <= '0;
    end else begin
      // Next-state arithmetic, so a same-cycle issue and retire is exact
      // (Chapter 23.6 pattern 9).
      out_q <= out_q + 16'(txn_issued) - 16'(txn_retired);
      if (replay_event && !(&rep_q)) rep_q <= rep_q + 32'd1;
      // Sticky FIRST failure (Chapter 23.6 pattern 12).
      if (first_error && !cap_q) begin cap_q <= 1'b1; cause_q <= first_error_cause; end
      for (int c = 0; c < N_CLASS; c++)
        if (credit_blocked[c] && !(&cb_q[c])) cb_q[c] <= cb_q[c] + 32'd1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_vplan_pkg::*;
 
// VERIFICATION-ONLY. The failure localizer. Given the model's verdicts and
// the per-layer counters, it names the CONTRACT that broke rather than
// reporting "test failed" -- which is the entire argument of §2.
module failure_localizer (
  input logic tl_model_error,
  input logic tl_unknown_completion,
  input logic tl_early_retire,
  input logic dl_observable,
  input int   dl_replays,
  input int   dl_replay_threshold,
  input logic link_left_operational,
  input logic err_issue_while_link_down,
 
  output logic blame_transaction,
  output logic blame_data_link,
  output logic blame_link_state,
  output logic blame_unattributable      // the honest fourth answer
);
  always_comb begin
    // Link state first: if the Link was not usable, the layers above are
    // UNPROVABLE rather than false (§3).
    blame_link_state   = link_left_operational || err_issue_while_link_down;
    blame_data_link    = !blame_link_state && dl_observable
                       && (dl_replays > dl_replay_threshold);
    blame_transaction  = !blame_link_state && !blame_data_link && tl_model_error;
    // A transaction-layer symptom with an unobservable data link cannot be
    // attributed. Saying so beats guessing (§4).
    blame_unattributable = tl_model_error && !dl_observable
                        && !blame_link_state && !blame_transaction;
  end
endmodule

Classification: four verification-only, one synthesizable, one compile-time.

Failure — six. A scoreboard calling a DUT function (§5). Sampling valid instead of the handshake. A model that permits a duplicate live identity. Removing a transaction on the first Completion. Claiming to observe a layer the DUT does not expose (§4). And reporting "test failed" instead of naming the contract.

11. Same-Cycle Audit and Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ==================================================================
// OBSERVATION INTEGRITY (§6) -- monitors record what happened.
// ==================================================================
 
// P1: a request is recorded only on an observed TRANSFER, never an offer.
property p_record_on_transfer;
  @(posedge clk) disable iff (!rst_n)
    (outstanding != $past(outstanding)) |-> $past(req_seen || retire_seen);
endproperty
 
// P2: exactly one model insertion per observed request transfer.
property p_one_insert_per_request;
  @(posedge clk) disable iff (!rst_n)
    (req_seen && !err_duplicate_identity && !retire_seen)
      |=> (outstanding == $past(outstanding) + 1);
endproperty
 
// P3: a live identity is unique in the model. §6: a model that allows
// duplicates reports the correct design as broken.
property p_no_duplicate_identity;
  @(posedge clk) disable iff (!rst_n)
    (req_seen && (find(req_key) >= 0)) |=> err_duplicate_identity;
endproperty
 
// P4: a monitor's emitted event is stable for the cycle it is valid.
property p_event_stable;
  @(posedge clk) disable iff (!rst_n)
    ev_valid |-> ##0 $stable(ev.kind);
endproperty
 
// P5: every event carries a classified layer -- nothing is "unknown".
property p_event_layer_valid;
  @(posedge clk) disable iff (!rst_n)
    ev_valid |-> (ev.layer inside {LYR_TL, LYR_DL, LYR_LINK, LYR_CFG});
endproperty
 
// P6: event cycle stamps are monotonic, so the timeline is an order.
property p_timestamps_monotonic;
  @(posedge clk) disable iff (!rst_n)
    ev_valid |-> (ev.cycle >= $past(ev.cycle));
endproperty
 
// ==================================================================
// MODEL CORRECTNESS (§5, §6).
// ==================================================================
 
// P7: a completion matches a LIVE transaction or is reported unknown --
// it never defaults to entry 0.
property p_completion_matches_or_reports;
  @(posedge clk) disable iff (!rst_n)
    (cpl_seen && (find(cpl_key) < 0)) |=> err_unknown_completion;
endproperty
 
// P8: a transaction is removed exactly once.
property p_remove_once;
  @(posedge clk) disable iff (!rst_n)
    (retire_seen && (find(retire_key) < 0)) |=> err_double_retire;
endproperty
 
// P9: same-cycle retire and issue of the same key is handled exactly --
// the key is free for the new transaction.
property p_same_cycle_retire_issue;
  @(posedge clk) disable iff (!rst_n)
    (retire_seen && req_seen && key_eq(retire_key, req_key))
      |=> !err_duplicate_identity;
endproperty
 
// P10: retirement before byte coverage, with no terminal outcome, is
// REPORTED -- the environment's version of Chapter 23.5's 80.8% cascade.
property p_early_retire_reported;
  @(posedge clk) disable iff (!rst_n)
    err_early_retire |=> err_early_retire;
endproperty
 
// P11: model population equals the live entry count -- conservation.
property p_population_consistent;
  @(posedge clk) disable iff (!rst_n)
    (outstanding >= 0) && (outstanding <= MAX_OUTSTANDING);
endproperty
 
// ==================================================================
// TIMELINE (§9) -- bounded, and never out of range.
// ==================================================================
 
// P12: the write index is always within the ring.
property p_history_index_safe;
  @(posedge clk) disable iff (!rst_n)
    (wr >= 0) && (wr < HISTORY_DEPTH);
endproperty
 
// P13: an out-of-range write is reported, never aliased.
property p_history_range_reported;
  @(posedge clk) disable iff (!rst_n)
    (ev_valid && (wr >= HISTORY_DEPTH)) |=> err_index_range;
endproperty
 
// P14: depth never exceeds the ring's capacity.
property p_depth_bounded;
  @(posedge clk) disable iff (!rst_n) (depth <= HISTORY_DEPTH);
endproperty
 
// ==================================================================
// LAYER SEPARATION (§2, §4).
// ==================================================================
 
// P15: a Data Link acceptance is NOT treated as end-to-end delivery --
// the two counters are independent (Chapter 21.2 §3).
property p_dl_accept_is_not_tl_delivery;
  @(posedge clk) disable iff (!rst_n)
    (dl_accept && DL_OBSERVABLE) |-> ##0 !$rose(tl_returned);
endproperty
 
// P16: a replay does not count as a new Transaction Layer request.
property p_replay_is_not_new_request;
  @(posedge clk) disable iff (!rst_n)
    dl_replay |=> $stable(tl_issued);
endproperty
 
// P17: an unobservable layer is declared, not assumed.
property p_unobservable_declared;
  @(posedge clk) disable iff (!rst_n)
    (!dl_observable) |-> ((dl_accepted == 0) && (dl_replays == 0));
endproperty
 
// P18: a transaction-layer symptom with an unobservable data link is
// reported UNATTRIBUTABLE rather than blamed on the transaction layer.
property p_honest_attribution;
  @(posedge clk) disable iff (!rst_n)
    (tl_model_error && !dl_observable && !blame_link_state)
      |-> (blame_unattributable || blame_transaction);
endproperty
 
// P19: link-state failure outranks the layers above -- they become
// unprovable, not false (§3).
property p_link_state_outranks;
  @(posedge clk) disable iff (!rst_n)
    blame_link_state |-> (!blame_data_link && !blame_transaction);
endproperty
 
// ==================================================================
// RESET AND NON-INTERFERENCE.
// ==================================================================
 
// P20: reset clears the model completely -- no stale transaction survives.
property p_reset_clears_model;
  @(posedge clk) (!rst_n) |=> (outstanding == 0);
endproperty
 
// P21: an observation in the cycle after reset finds an empty model and
// is reported unknown, rather than matching a stale entry.
property p_no_stale_match_after_reset;
  @(posedge clk) disable iff (!rst_n)
    ($past(!rst_n) && cpl_seen) |=> err_unknown_completion;
endproperty
 
// P22: verification logic never drives the DUT.
property p_verification_non_functional;
  @(posedge clk) disable iff (!rst_n)
    $stable({req_seen, cpl_seen}) or !$stable({outstanding, dl_replays});
endproperty

Twenty-two properties, all about this chapter's own monitors. This is deliberately not a PCIe property libraryChapter 24.2 owns that, and it remains a large chapter. P15–P19 are the ones worth stealing: they assert that the environment keeps its layers apart, which is the failure mode a checker cannot report on itself.

12. Measured Behaviour

13. Verification of the Verification — Mutations

The mutations below are defects in the environment. Each one produces a testbench that passes while the design is broken, or fails while the design is correct — and both are expensive.

#MutationConsequence
1The scoreboard predicts using a DUT functiondesign and checker agree by construction (§5)
2The oracle imports the DUT's package of decode helperssame, one import away
3Key the model by Tag alone where Requester context mattersthe model reproduces the DUT's own bug (§6)
4Sample monitors on valid rather than the handshakerecords requests that were never sent
5Sample in the active region rather than the observed regionphantom events from values not yet committed
6Reset does not clear the modelstale transactions match post-reset Completions
7The model permits a duplicate live identitythe correct design is reported as broken (23.5 §12)
8Remove the transaction on the first Completionthe environment generates the 80.8% orphan cascade itself
9Ignore unknown-identity Completionsthe single most diagnostic error is discarded
10Accept a duplicate Completion silentlyover-return goes unnoticed
11Treat a Data Link ACK as end-to-end deliverya switch's acceptance mistaken for endpoint receipt (21.2 §3)
12Count a replay as a new Transaction Layer requestissued counts inflate; efficiency conclusions invert
13Merge the three layers into one pass/faila failure is unattributable by construction (§2)
14Claim to verify a layer the DUT does not exposethe plan documents coverage that does not exist (§4)
15Read the LTSSM state with no observation contracta value sampled mid-transition, believed
16First failure overwritten by later onesthe root cause is lost 96.8% of the time (21.4 §11)
17No negative tests at allevery prohibition in §8 is unverified
18Only happy-path traffic; no backpressureevery defect in §7's table is invisible
19No same-cycle coincidence teststhe races of §11 are never reached
20Only one outstanding transaction in the whole environmentordering, identity and reuse are all untested
21Errors injected only at the start of a transactionthe cleanup path with maximum state is never exercised (§7)
22Build the full error-injection framework hereconsumes 24.7
23Build the production scoreboard hereconsumes 24.3
24Write the PCIe property library hereconsumes 24.2
25Define the coverage model hereconsumes 24.4
26UVM boilerplate substituted for protocol reasoninga well-structured environment that checks nothing
27The timeline indexes out of range at the ring boundarythe debug aid crashes exactly when it is needed
28The localizer blames the Transaction Layer when the Link was downthe layers above were unprovable, not false (§3)
29Verification logic drives or gates the DUTthe measured system is not the shipping one

Two counterexamples worth stating explicitly.

Mutation 1 is the one that gets an environment signed off. The scoreboard uses dut_bar_decode() because it is right there, it is already tested, and reimplementing it "would just duplicate code." That reasoning is exactly backwards: the duplication is the verification. Chapter 23.2 §12's oracle deliberately used a range scan where the RTL used a mask compare — provably equivalent under the alignment precondition, and therefore a disagreement is always a real bug. Sharing the function makes the 50.1% aliasing defect and the 33.3% misalignment defect both invisible.

Mutation 11 is the cross-layer error that reads as competence. The environment sees a Data Link acknowledgement and marks the transaction delivered. It is not wrong about the ACK — the link partner did receive the packet. It is wrong about what that proves: Chapter 21.2 §3 established that a switch ACKs on its own behalf, so the packet may still be sitting in that switch, or dropped after it. The environment now reports success for transactions that never arrived, and the resulting bug reports go to the endpoint team.

14. Debugging by Triangulation

Symptom — the Transaction monitor saw the request; the Data Link monitor never saw a transfer. The contract that broke is between them — the request was accepted at the transaction interface and never made it onto the Link. Look at flow control (22.3 §3: a healthy Link that cannot legally transmit) and at the TX path's own backpressure. The Transaction Layer is not at fault, and neither is the Link.

Symptom — the Data Link accepted the packet and the Transaction result appears twice. A replay was counted as a new transaction somewhere (mutation 12), either in the DUT or in your environment. Check dl_replays against tl_issued: if issued rose in the same cycle a replay was observed, the environment has the bug; if not, the DUT does.

Symptom — completions mismatch, but only with several Requester IDs active. Identity (§6). Either the DUT matches on Tag alone (23.5 §12's 39.7%) or your model does (mutation 3). Distinguish by keying the model on the pair and re-running: if the errors vanish, the model was the problem; if they persist and now point at specific transactions, the DUT was.

Symptom — the failure disappears when backpressure is removed. Almost certainly a real DUT bug, and the environment is fine (§7). Every defect in §7's table has exactly this signature. Do not "fix" the test by relaxing the stimulus — reduce it to the smallest stalling case instead, which is usually a stall on the final beat.

Symptom — after reset the scoreboard reports outstanding transactions. Two possibilities and they are distinguishable. If the model was not cleared, that is mutation 6. If it was cleared and Completions for pre-reset transactions are still arriving, that is correct behaviour — they should be reported as unknown-identity (P21). The tell is whether the errors stop once the in-flight traffic drains.

Symptom — LTSSM Recovery events correlate with completion timeouts. Read the layer plan in order (§3). A Link that leaves the operational state makes the layers above unprovable, not false: requests in flight may never be delivered, and their timeouts are a consequence. The localizer ranks link-state first for this reason (P19), and investigating the timeouts before the Recovery is time spent on a symptom.

Symptom — the DUT and the scoreboard agree, and an analyzer shows the traffic going somewhere wrong. This is the signature of a shared model bug (§5, mutation 1). Agreement between two things that contain the same code is not evidence. Rebuild the prediction independently — a different algorithm, written from the specification — and compare all three. Chapter 23.2 §16's "answers addresses belonging to another device" is exactly this class, and a scoreboard sharing the decoder cannot see it.

15. Misconceptions

"Verification means checking packets." That is one of three contracts (§2).

"If the packets are legal, the design is correct." Chapter 23.5 §12 measured 39.7% of perfectly legal Completions applied to the wrong context.

"An ACK means the transaction arrived." It means the link partner received the packet (21.2 §3).

"A replay is another request." It is the same request, again, on one Link (14.4).

"The scoreboard should reuse the DUT's decode function to stay in sync." Then it cannot disagree, which is the only thing it was for (§5).

"Duplicating logic in the testbench is waste." The duplication is the verification (§5, mutation 1).

"Random field values are strong stimulus." Every defect in §7's table needed a coincidence, not a value.

"Positive tests are enough." Most of the contract is prohibitions (§8).

"One outstanding transaction is enough to prove the mechanism." It hides identity, ordering and reuse entirely (§7).

"Inject errors at the start of the transaction." The cleanup path with maximum state to release is the one that leaks (§7, 23.3 §14).

"Every layer is observable." Not behind a hard core, and pretending otherwise documents coverage that does not exist (§4).

"If the test failed, the DUT is broken." It may be the model, the sampling region, or an unattributable cross-layer effect — and saying "unattributable" is a legitimate result (§10, P18).

"A UVM environment is a verification plan." Structure is not reasoning (mutation 26).

16. Understanding Check

Q1. A scoreboard uses the DUT's own BAR decode helper to predict which aperture an address belongs to. They always agree. Why is this a weak oracle? Because agreement is guaranteed by construction — the scoreboard contains the design's logic, including its bugs. Chapter 23.2 measured a lower-32-only comparison claiming addresses 4 GiB away in 50.1% of probes; a scoreboard sharing that function reports a clean run. The fix is to model the contract differently on purpose: 23.2's oracle used an explicit range scan where the RTL used a mask compare — equivalent under the alignment precondition, so any disagreement is a real defect (§5).

Q2. An ingress Link ACK is observed, and the TLP never appears at the far endpoint of a switch. Which contract succeeded and which is unproven? The Data Link contract for the first Link succeeded — the switch received the packet and acknowledged it on its own behalf (21.2 §3). The Transaction Layer contract is unproven, and so is the Data Link contract of the second Link. Nothing observed so far says the packet left the switch, which is why §10's monitor counts dl_accepted and tl_returned separately and P15 forbids conflating them.

Q3. A Completion for Tag 5 arrives in the same cycle a Tag 5 transaction retires and a new Tag 5 request is issued. What must the environment order explicitly? Removal, then insertion, then matching (§11, P9). If insertion happens first, the model sees a duplicate live identity and reports the design broken. If matching happens before removal, the Completion is applied to the retiring transaction rather than reported. All three orderings are defensible in isolation and only one is consistent, which is why it is declared in the same-cycle table rather than left to code order.

Q4. Your DUT is your logic on top of a hard PCIe core that exposes no Data Link events. What is the honest verification plan? Verify the Transaction Layer contract directly, verify link state through whatever status the core exports, and state that the Data Link contract is inherited from the core's own verification (§4). Build the event correlator anyway, so Data Link misbehaviour is inferable from an unexplained gap between issued and returned transactions. And set DL_OBSERVABLE = 0 so the localizer reports unattributable instead of blaming the Transaction Layer (P17, P18) — a plan that names its blind spot is stronger than one that does not have one on paper.

Q5. Your test suite passes with a fast responder and fails when you add backpressure. Where do you look first? At the DUT, not the test (§7). Every defect measured across Modules 22 and 23 has this signature — 68.6%, 79.1%, 75.9%, 80.7% — because the naive implementation is correct until something stalls. Reduce to the smallest stalling case, usually a stall on the final beat or on the result consumer, and check the ownership patterns of Chapter 23.6 §13 in that order.

Q6. Why does §10's localizer have a fourth output, blame_unattributable? Because three answers are not always available. With an unobservable Data Link, a Transaction Layer symptom may originate in a layer the environment cannot see (§4). Reporting "transaction layer" would be a guess presented as a finding, and it sends the wrong team on the investigation. P18 asserts the honest attribution, and the practical value is that "unattributable, here is the correlated timeline" is a useful bug report while a wrong attribution is worse than none.

Q7. Which is a better use of one hour: adding 100 random packet-content tests, or adding one test that stalls the result consumer? The stall test, decisively. Random contents explore a space where almost nothing is defect-bearing; §7's table shows every measured defect needing a concurrency condition instead. The stalled-result-consumer test alone reaches Chapter 23.5 §12's 75.9% context-reuse corruption, which no amount of content randomization can find — the window does not exist unless the consumer stalls.

17. What's Next

This chapter framed the problem. Module 24 now executes it.

24.2 Assertions builds the PCIe property library — the TLP, Data Link and LTSSM rules as formal properties. §8's prohibitions are its input, and §11's twenty-two properties are deliberately about this chapter's own monitors so that catalogue stays whole.

24.3 Scoreboards turns §10's forty-line model into a production scoreboard: reassembly, out-of-order matching, reporting and performance. 24.4 Coverage defines what "enough" means — and this chapter defined no covergroups on purpose. 24.5 VIP covers what verification IP checks for you, which is the other half of §4's observability argument. 24.6 UVM Architecture gives §9's methodology-neutral diagram a class structure. And 24.7 Error Injection builds the stimulus §7 argued for — including injecting on the final fragment rather than the first.

One idea carries from Module 23 into all of them. Every defect those chapters measured was an ownership failure, and every oracle that caught one was independent of the design. Module 25 then leaves the testbench entirely and debugs real silicon, where the observation points are fewer and §4's argument matters most.