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
The plan, as a table an engineer can fill in for a real DUT:
| Transaction | Data Link | Link state | |
|---|---|---|---|
| owns | meaning, identity, completeness | reliable delivery over one Link | reaching and holding a usable state |
| observed at | the request/completion interfaces | the packet interface toward the Link | LTSSM state, or its proxy (§4) |
| can go wrong | wrong requester, wrong bytes, never answered, answered twice | packet lost, replayed forever, accepted twice, integrity error ignored | never trains, retrains under load, wrong width or speed |
| oracle | independent identity-keyed model (§5, §11) | per-Link retention/retry accounting | legal-state and legal-transition checking (24.2 owns the properties) |
| exact rules from | 13.3, 21.4 | 14.4 | Module 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:
| Defect | The stimulus that exposes it | Measured |
|---|---|---|
| header follows live request | stall while a new request is pending | 68.6% (23.4 §12) |
progress on valid | any backpressure | 79.1% (23.4 §12) |
| context freed under a stalled result | stall the result consumer | 75.9% (23.5 §12) |
| Tag leak on error | inject Completion errors | wedged after 14 jobs (23.3 §14) |
| tag/data skew | make the context table a RAM | 87.5% (23.5 §12) |
| shared counter | more than one job in flight | 62.6% (23.3 §14) |
| config changes mid-transaction | write Device Control during a transfer | 15.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:
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 twiceEach 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
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
// 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
endpackageimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleClassification: 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
// ==================================================================
// 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});
endpropertyTwenty-two properties, all about this chapter's own monitors. This is deliberately not a PCIe property library — Chapter 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.
| # | Mutation | Consequence |
|---|---|---|
| 1 | The scoreboard predicts using a DUT function | design and checker agree by construction (§5) |
| 2 | The oracle imports the DUT's package of decode helpers | same, one import away |
| 3 | Key the model by Tag alone where Requester context matters | the model reproduces the DUT's own bug (§6) |
| 4 | Sample monitors on valid rather than the handshake | records requests that were never sent |
| 5 | Sample in the active region rather than the observed region | phantom events from values not yet committed |
| 6 | Reset does not clear the model | stale transactions match post-reset Completions |
| 7 | The model permits a duplicate live identity | the correct design is reported as broken (23.5 §12) |
| 8 | Remove the transaction on the first Completion | the environment generates the 80.8% orphan cascade itself |
| 9 | Ignore unknown-identity Completions | the single most diagnostic error is discarded |
| 10 | Accept a duplicate Completion silently | over-return goes unnoticed |
| 11 | Treat a Data Link ACK as end-to-end delivery | a switch's acceptance mistaken for endpoint receipt (21.2 §3) |
| 12 | Count a replay as a new Transaction Layer request | issued counts inflate; efficiency conclusions invert |
| 13 | Merge the three layers into one pass/fail | a failure is unattributable by construction (§2) |
| 14 | Claim to verify a layer the DUT does not expose | the plan documents coverage that does not exist (§4) |
| 15 | Read the LTSSM state with no observation contract | a value sampled mid-transition, believed |
| 16 | First failure overwritten by later ones | the root cause is lost 96.8% of the time (21.4 §11) |
| 17 | No negative tests at all | every prohibition in §8 is unverified |
| 18 | Only happy-path traffic; no backpressure | every defect in §7's table is invisible |
| 19 | No same-cycle coincidence tests | the races of §11 are never reached |
| 20 | Only one outstanding transaction in the whole environment | ordering, identity and reuse are all untested |
| 21 | Errors injected only at the start of a transaction | the cleanup path with maximum state is never exercised (§7) |
| 22 | Build the full error-injection framework here | consumes 24.7 |
| 23 | Build the production scoreboard here | consumes 24.3 |
| 24 | Write the PCIe property library here | consumes 24.2 |
| 25 | Define the coverage model here | consumes 24.4 |
| 26 | UVM boilerplate substituted for protocol reasoning | a well-structured environment that checks nothing |
| 27 | The timeline indexes out of range at the ring boundary | the debug aid crashes exactly when it is needed |
| 28 | The localizer blames the Transaction Layer when the Link was down | the layers above were unprovable, not false (§3) |
| 29 | Verification logic drives or gates the DUT | the 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.