PCIe · Module 24
Scoreboards — Executable Conservation Plus Identity
A scoreboard remembers what the DUT was asked for and consumes what came back. The flagship failure is not subtle: a FIFO of expected results reports 185,564 mismatches on a completely legal PCIe stream.
Chapter 24.2 proved what fits inside one interface. This chapter proves what spans a transaction's whole life — which is everything an assertion cannot reach.
And it opens with the failure that defines the chapter. The most natural scoreboard design — a queue of expected results, compared against actual results in order — is wrong for PCIe, and it is wrong in a way that produces a torrent of failures against a design that is behaving perfectly.
1. Sources, Scope, and What the UVM Track Owns
2. Conservation Plus Identity
A scoreboard has two inputs and one model in the middle:
observed requests -> | independent model | <- observed completions
| what is still owed |It must know six things, and each maps to a field in §10's expected entry:
| It must know | Because |
|---|---|
| what was accepted | an obligation only exists once the request transferred |
| what response is legal | a status-only Completion is legal; wrong bytes are not |
| which bytes remain | retirement is coverage, not arrival (13.3 §1) |
| what may reorder | different transactions may complete in any order (§4) |
| what must retain order | Completions of one transaction must not pass each other |
| what terminally retires it | coverage complete, or an error status |
Notice what is absent: how many Completions to expect. That number is the Completer's choice, and a scoreboard that predicts it is guessing (13.3 §1). Every design decision in this chapter follows from removing that guess.
3. The Transaction Key
The key is (Requester ID, Tag) (21.4 §3), and the reasons are worth separating because they fail differently.
A Tag alone is not unique. Two Functions, or two devices behind a switch, may legally hold Tag 5 simultaneously. §12 measured the collision at allocation, and Chapter 23.5 §12 measured the runtime consequence in hardware at 39.7% misattribution — payload written into another transaction's buffer.
An index is not a key. Keying by table slot means the key changes when the table is reorganized, and a scoreboard entry that moves loses its association.
And a handle is not a key. Keying an associative array by a transaction object handle — tempting in UVM — means a reused object silently rewrites an old entry. That is §13's mutation 6, and it is the most UVM-specific bug in this chapter.
One scoping caution. The pair identifies an outstanding request of one Requester. For a multi-Function DUT the environment's key should carry the Function, which is Chapter 24.6 §12's scaling concern; §10 parameterizes it so the same scoreboard serves both.
4. The FIFO Trap
5. Byte Coverage, Not Packet Count
A read's obligation is a byte extent, and it is discharged by coverage (13.3 §1).
So the accumulator is a count of bytes, and its guards are the interesting part:
| Guard | Detects |
|---|---|
received + this > requested | over-return — writes past the destination |
retire only when received >= requested | early retirement |
| a fragment for a retired identity | the cascade below |
Lower Address inconsistent with the running offset | a fragment placed at the wrong offset |
Byte Count not decreasing across fragments | a Completer or model error (13.3 §2) |
§12 measured the two failure modes on the same 250,000-event stream:
| Injected defect | Result |
|---|---|
| retire on the first fragment | 59,083 unknown-fragment errors |
| duplicate fragment accepted | 4,838 over-returns |
Read the first row as a cascade. The first fragment retires the entry; every later fragment of that transaction now matches nothing and is reported as an unknown identity. A single line of retirement logic produces tens of thousands of errors that all point away from it — and Chapter 23.5 §12 measured the identical cascade in hardware at 80.8% of all fragments.
On Byte Count specifically. It is the remaining count including the current Completion (13.3 §2), so it decreases across a transaction's fragments. A scoreboard that treats it as this-fragment's length either never completes the transaction or completes it early — and §10 checks the progression rather than assuming it.
6. Reset Is a Policy Decision, Not a .delete()
7. End-of-Test Residue
The most valuable check in a scoreboard runs once, at the end.
at end of test: the outstanding map must be empty,
or every remaining entry must be explainedBecause a lost transaction produces no failure while the test runs. No mismatch, no unknown identity — the request simply never completes, and every other check stays green. The residue check is the only thing that sees it.
Three things to report, not one:
| Report | Means |
|---|---|
| entries still outstanding | requests the DUT never answered |
| unexpected actuals seen | Completions with no matching request |
| resource residue | Tags or contexts the DUT never released |
The third is the scoreboard-visible form of Chapter 23.3 §14's leak — the one that wedged an engine after 14 jobs. A residue check at end-of-test finds it in the first error-injection run, hours before a stress test would.
And "the test intended to leave work outstanding" is a legitimate answer — but it must be declared by the test, not inferred by the scoreboard. §10 exposes expect_residue for exactly that.
8. A Watchdog Is Not a Completion Timeout
9. The Scoreboard in Its Environment
Four things to read out of the figure.
Both monitors feed one map, and the map is the only place the streams meet. There is no queue of expected results anywhere — that structure is §4's trap.
The reset epoch enters the map, not the monitors. Monitors publish what they see, stamped; the scoreboard decides what to reject (§6), which keeps the policy in one place.
Coverage hangs off the monitors, not off the scoreboard. It observes the same stream independently — and no arrow runs from coverage back into the map, because a coverage subscriber that influenced expected results would make both wrong together (24.4 §9).
And the residue check is downstream of retirement, running once at end of test — the only check in the figure that detects a transaction nobody ever answered (§7).
10. UVM Code — Items, Map, Scoreboard
// UVM / VERIFICATION-ONLY. Normalized observation objects.
// These are what MONITORS publish -- not wire layouts, and not a VIP
// vendor's classes (24.5 §4 owns that adaptation).
class pcie_txn_key extends uvm_object;
rand bit [15:0] requester_id; // SPEC-DEFINED identity half (21.4 §3)
rand bit [7:0] tag;
rand bit [2:0] function_num; // carried for multi-Function DUTs (§3)
`uvm_object_utils_begin(pcie_txn_key)
`uvm_field_int(requester_id, UVM_ALL_ON)
`uvm_field_int(tag, UVM_ALL_ON)
`uvm_field_int(function_num, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "pcie_txn_key"); super.new(name); endfunction
// The key is a VALUE. Never key an associative array by object handle --
// a reused handle rewrites an old entry (§3, mutation 6).
function string key_str();
return $sformatf("%0d:%04h:%02h", function_num, requester_id, tag);
endfunction
endclass
class pcie_req_item extends uvm_sequence_item;
pcie_txn_key key;
bit is_read; // Posted write vs Non-Posted read (12.1/12.2)
bit [63:0] address;
bit [15:0] byte_count; // what THIS request asked for
bit [31:0] obs_epoch; // reset generation when observed (§6)
time obs_time; // debug only, never a protocol claim
`uvm_object_utils(pcie_req_item)
function new(string name = "pcie_req_item"); super.new(name); endfunction
endclass
class pcie_cpl_item extends uvm_sequence_item;
pcie_txn_key key; // carried BY the completion (10.2 §6)
bit [2:0] status; // 13.2 owns the encodings
bit [11:0] byte_count; // REMAINING, incl. this one (13.3 §2)
bit [6:0] lower_address; // 7 LSBs of this fragment's start (13.3 §2)
bit [15:0] payload_bytes;
bit [31:0] obs_epoch;
time obs_time;
`uvm_object_utils(pcie_cpl_item)
function new(string name = "pcie_cpl_item"); super.new(name); endfunction
endclass// UVM / VERIFICATION-ONLY. One outstanding obligation.
// The fields are exactly what §2's six questions require -- no more.
class pcie_expect_entry extends uvm_object;
pcie_txn_key key;
bit [63:0] address;
bit [15:0] requested;
bit [15:0] received;
bit [11:0] last_byte_count; // to check the DOWNWARD progression (13.3 §2)
bit saw_any;
bit terminal;
string terminal_reason;
bit [31:0] epoch;
time issued_at;
`uvm_object_utils(pcie_expect_entry)
function new(string name = "pcie_expect_entry"); super.new(name); endfunction
function bit covered(); return (received >= requested); endfunction
function int remaining(); return int'(requested) - int'(received); endfunction
endclass// UVM / VERIFICATION-ONLY. THE SCOREBOARD.
// Keyed by identity, accumulates by BYTES, retires on coverage or a
// terminal status, rejects stale epochs, and checks residue at end of test.
// §12: 0 mismatches on a legal 250,000-event stream; a FIFO design reports
// 185,564 on the SAME stream.
class pcie_txn_scoreboard extends uvm_scoreboard;
`uvm_component_utils(pcie_txn_scoreboard)
uvm_analysis_imp_req #(pcie_req_item, pcie_txn_scoreboard) req_imp;
uvm_analysis_imp_cpl #(pcie_cpl_item, pcie_txn_scoreboard) cpl_imp;
protected pcie_expect_entry m_out[string]; // keyed by VALUE (§3)
protected bit [31:0] m_epoch;
protected int m_retired, m_unknown, m_over, m_early, m_stale;
bit expect_residue; // the TEST declares this (§7)
function new(string name, uvm_component parent);
super.new(name, parent);
req_imp = new("req_imp", this);
cpl_imp = new("cpl_imp", this);
endfunction
// ---- reset epoch (§6) ------------------------------------------
virtual function void set_epoch(bit [31:0] e, string policy = "ABORTED_BY_RESET");
// Outstanding entries LEAVE WITH A REASON. Silently clearing the map
// destroys the evidence of anything already lost.
foreach (m_out[k]) begin
`uvm_info("SB_RESET", $sformatf("retiring %s: %s (%0d/%0d bytes)",
k, policy, m_out[k].received, m_out[k].requested), UVM_MEDIUM)
end
m_out.delete();
m_epoch = e;
endfunction
// ---- a request was OBSERVED TO TRANSFER ------------------------
virtual function void write_req(pcie_req_item t);
string k = t.key.key_str();
if (t.obs_epoch != m_epoch) begin m_stale++; return; end
// A Posted write creates NO obligation -- no Completion is coming (12.2).
if (!t.is_read) return;
if (m_out.exists(k)) begin
`uvm_error("SB_DUP_ID", $sformatf("identity %s already outstanding", k))
return;
end
begin
pcie_expect_entry e = pcie_expect_entry::type_id::create("e");
e.key = t.key; e.address = t.address; e.requested = t.byte_count;
e.received = 0; e.saw_any = 0; e.terminal = 0;
e.epoch = t.obs_epoch; e.issued_at = t.obs_time;
m_out[k] = e;
end
endfunction
// ---- a completion was OBSERVED ---------------------------------
virtual function void write_cpl(pcie_cpl_item t);
string k = t.key.key_str();
pcie_expect_entry e;
if (t.obs_epoch != m_epoch) begin m_stale++; return; end
if (!m_out.exists(k)) begin
m_unknown++;
`uvm_error("SB_UNKNOWN", $sformatf("completion for unknown identity %s", k))
return; // NEVER default to some entry
end
e = m_out[k];
if (t.status != 3'b000) begin // 13.2 owns the encodings
e.terminal = 1; e.terminal_reason = "COMPLETION_STATUS";
retire(k, e);
return;
end
// Byte Count DECREASES across fragments (13.3 §2). A non-decreasing
// progression means the model or the Completer is wrong.
if (e.saw_any && (t.byte_count > e.last_byte_count))
`uvm_error("SB_BC", $sformatf("%s: byte count rose %0d -> %0d",
k, e.last_byte_count, t.byte_count))
e.last_byte_count = t.byte_count; e.saw_any = 1;
// Lower Address must agree with where we already are (13.3 §2).
if ((e.address + e.received)[6:0] !== t.lower_address)
`uvm_error("SB_LA", $sformatf("%s: lower address %02h, expected %02h",
k, t.lower_address, (e.address + e.received)[6:0]))
if ((int'(e.received) + int'(t.payload_bytes)) > int'(e.requested)) begin
m_over++;
`uvm_error("SB_OVER", $sformatf("%s: over-return, %0d + %0d > %0d",
k, e.received, t.payload_bytes, e.requested))
e.terminal = 1; e.terminal_reason = "OVER_RETURN";
retire(k, e);
return;
end
e.received += t.payload_bytes;
// COVERAGE, not packet count (§5). Retiring here on the first fragment
// produced 59,083 unknown-fragment errors in §12.
if (e.covered()) begin e.terminal_reason = "COVERED"; retire(k, e); end
endfunction
protected virtual function void retire(string k, pcie_expect_entry e);
if (!e.terminal && !e.covered()) m_early++;
m_retired++;
m_out.delete(k);
endfunction
// ---- END-OF-TEST RESIDUE (§7) -- the lost-transaction check ----
virtual function void check_phase(uvm_phase phase);
super.check_phase(phase);
if (m_out.size() != 0 && !expect_residue)
foreach (m_out[k])
`uvm_error("SB_RESIDUE", $sformatf(
"transaction %s never completed: %0d of %0d bytes, issued at %0t",
k, m_out[k].received, m_out[k].requested, m_out[k].issued_at))
`uvm_info("SB_SUMMARY", $sformatf(
"retired=%0d unknown=%0d over=%0d early=%0d stale=%0d residue=%0d",
m_retired, m_unknown, m_over, m_early, m_stale, m_out.size()), UVM_LOW)
endfunction
endclass// UVM / VERIFICATION-ONLY. The monitor side of the contract.
// It publishes an IMMUTABLE observation on the HANDSHAKE and clones before
// writing -- a reused handle silently rewrites the scoreboard's entry (§3).
class pcie_req_monitor extends uvm_monitor;
`uvm_component_utils(pcie_req_monitor)
uvm_analysis_port #(pcie_req_item) ap;
virtual pcie_if vif;
bit [31:0] epoch;
function new(string name, uvm_component parent);
super.new(name, parent); ap = new("ap", this);
endfunction
task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk);
// THE HANDSHAKE, never `valid` alone (24.1 §6, 24.2 §6).
if (vif.rst_n && vif.rq_valid && vif.rq_ready) begin
pcie_req_item t = pcie_req_item::type_id::create("t");
t.key = pcie_txn_key::type_id::create("k");
t.key.requester_id = vif.rq_requester_id;
t.key.tag = vif.rq_tag;
t.key.function_num = vif.rq_function;
t.is_read = vif.rq_is_read;
t.address = vif.rq_address;
t.byte_count = vif.rq_byte_count;
t.obs_epoch = epoch;
t.obs_time = $time;
ap.write(t); // a FRESH object every time -- never reused
end
end
endtask
endclass// VERIFICATION-ONLY. A watchdog -- NOT a PCIe Completion Timeout (§8).
// It reports that a test budget elapsed, which is a true statement about
// the simulation and makes no claim about a device mechanism.
class pcie_sb_watchdog extends uvm_component;
`uvm_component_utils(pcie_sb_watchdog)
int unsigned budget_cycles = 50_000;
pcie_txn_scoreboard sb;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
task run_phase(uvm_phase phase);
forever begin
#(budget_cycles * 1ns);
// Deliberately worded as a TEST-BUDGET event.
`uvm_warning("SB_WATCHDOG",
"expected completions did not arrive within the test budget; this is a \
verification watchdog, not an observed PCIe Completion Timeout")
end
endtask
endclassClassification: all UVM / verification-only.
Failure — seven. A FIFO of expected results (185,564 false mismatches). A Tag-only or handle key. Retiring on the first fragment (59,083 orphans). Absorbing an over-return. Publishing on valid. Reusing the transaction object. And clearing the map on reset with no recorded reason.
11. Assertions and Local Checks
// P1: a monitor publishes exactly once per observed handshake.
property p_publish_once_per_handshake;
@(posedge clk) disable iff (!rst_n)
(rq_valid && rq_ready) |-> ##0 (ap_write_count == $past(ap_write_count) + 1);
endproperty
// P2: nothing is published on an offer that was not accepted.
property p_no_publish_on_offer;
@(posedge clk) disable iff (!rst_n)
(rq_valid && !rq_ready) |=> $stable(ap_write_count);
endproperty
// P3: an inserted identity was not already outstanding.
property p_no_duplicate_insert;
@(posedge clk) disable iff (!rst_n)
sb_insert |-> !sb_exists_before;
endproperty
// P4: a completion matches an existing entry or is reported unknown --
// it never defaults to an arbitrary entry.
property p_match_or_report;
@(posedge clk) disable iff (!rst_n)
sb_cpl_seen |-> (sb_hit || sb_unknown_reported);
endproperty
// P5: the key is a VALUE comparison, never a handle comparison.
property p_key_is_value;
@(posedge clk) disable iff (!rst_n)
sb_hit |-> ((entry_key.requester_id == cpl_key.requester_id)
&& (entry_key.tag == cpl_key.tag)
&& (entry_key.function_num == cpl_key.function_num));
endproperty
// P6: received bytes are monotonic within an entry.
property p_received_monotonic;
@(posedge clk) disable iff (!rst_n)
entry_live |-> (entry_received >= $past(entry_received));
endproperty
// P7: received never exceeds requested; an over-return is REPORTED.
property p_no_over_return;
@(posedge clk) disable iff (!rst_n)
(sb_hit && ((entry_received + cpl_bytes) > entry_requested))
|=> sb_over_reported;
endproperty
// P8: retirement happens exactly once per identity.
property p_retire_once;
@(posedge clk) disable iff (!rst_n)
sb_retire |=> !sb_exists;
endproperty
// P9: a successful retirement means byte coverage was complete (§5).
property p_retire_requires_coverage;
@(posedge clk) disable iff (!rst_n)
(sb_retire && (reason == "COVERED")) |-> (entry_received >= entry_requested);
endproperty
// P10: a terminal status stops payload accounting.
property p_terminal_stops_accounting;
@(posedge clk) disable iff (!rst_n)
entry_terminal |=> $stable(entry_received);
endproperty
// P11: Byte Count decreases across a transaction's fragments (13.3 §2).
property p_byte_count_decreases;
@(posedge clk) disable iff (!rst_n)
(sb_hit && entry_saw_any) |-> (cpl_byte_count <= entry_last_byte_count);
endproperty
// P12: a Posted write creates no obligation (12.2).
property p_posted_no_obligation;
@(posedge clk) disable iff (!rst_n)
(rq_valid && rq_ready && !rq_is_read) |=> $stable(sb_outstanding_count);
endproperty
// P13: an observation stamped with a stale epoch is DROPPED (§6).
property p_stale_epoch_dropped;
@(posedge clk) disable iff (!rst_n)
(obs_valid && (obs_epoch != sb_epoch)) |=> $stable(sb_outstanding_count);
endproperty
// P14: reset retires outstanding entries WITH A REASON, never silently.
property p_reset_records_reason;
@(posedge clk) disable iff (!rst_n)
(epoch_changed && $past(sb_outstanding_count) > 0) |-> sb_reset_reason_logged;
endproperty
// P15: population equals the map size -- the scoreboard's own conservation.
property p_population_consistent;
@(posedge clk) disable iff (!rst_n)
(sb_outstanding_count == sb_map_size);
endproperty
// P16: at end of test the map is empty unless the test declared residue.
property p_residue_checked;
@(posedge clk) disable iff (!rst_n)
(check_phase_active && !expect_residue) |-> (sb_map_size == 0);
endproperty
// P17: the transaction object published is not mutated afterwards.
property p_observation_immutable;
@(posedge clk) disable iff (!rst_n)
published |=> $stable(published_item_contents);
endproperty
// P18: every analysis write is delivered -- no silent drop under load.
property p_no_analysis_drop;
@(posedge clk) disable iff (!rst_n)
(ap_write_count == sb_receive_count);
endproperty
// P19: the scoreboard never drives the DUT.
property p_scoreboard_non_functional;
@(posedge clk) disable iff (!rst_n)
$stable({rq_valid, rq_ready, cpl_valid}) or !$stable(sb_retired_count);
endproperty
// P20: ordering is checked WITHIN a transaction only -- never across
// transactions (13.3 §2). Asserting cross-transaction order is §4's trap.
property p_no_cross_transaction_ordering;
@(posedge clk) disable iff (!rst_n)
sb_order_violation |-> (violating_key == expected_key);
endproperty
// P21: the watchdog reports a TEST BUDGET, not a protocol timeout (§8).
property p_watchdog_labelled;
@(posedge clk) disable iff (!rst_n)
watchdog_fire |-> (report_id == "SB_WATCHDOG");
endproperty
// P22: EVIDENCE -- reordering between transactions actually occurred.
property c_inter_transaction_reorder;
@(posedge clk) disable iff (!rst_n)
(sb_hit && (cpl_key != oldest_outstanding_key));
endpropertyTwenty-two properties. P20 and P22 are the pair that matters most: P20 forbids the FIFO assumption, and P22 proves the stimulus actually reordered — without P22, a per-identity scoreboard and a FIFO scoreboard are indistinguishable on the run.
12. Measured Behaviour
13. Verification — Mutations
| # | Mutation | Symptom | Caught by |
|---|---|---|---|
| 1 | Expected results held in a FIFO | 185,564 false mismatches on a legal stream (§12) | P20, P22 |
| 2 | Key on Tag alone | collisions across Requester IDs (§12) | P5 |
| 3 | Key the associative array by object handle | a reused object rewrites an old entry | P5, P17 |
| 4 | Key by table index | the key changes when the map reorganizes | P5 |
| 5 | Retire on the first fragment | 59,083 unknown-identity errors (§12) | P9 |
| 6 | Publish the same transaction object twice without cloning | the earlier entry mutates | P17 |
| 7 | Accept a duplicate fragment | 4,838 over-returns (§12) | P7 |
| 8 | Ignore unknown-identity completions | the most diagnostic error is discarded | P4 |
| 9 | Default an unknown completion to the oldest entry | corrupts a valid transaction | P4, P5 |
| 10 | Compare packet counts instead of bytes | breaks on the first Completer that splits differently | P9 |
| 11 | Treat Byte Count as this-fragment's length | never completes, or completes early | P11 |
| 12 | Ignore Lower Address | a fragment placed at the wrong offset passes | P11 |
| 13 | Ignore Completion status | a failed read reports success | P10 |
| 14 | Accumulate payload after a terminal status | a failed read reports a plausible byte count | P10 |
| 15 | Truncate the Requester ID | two requesters alias | P5 |
| 16 | Insert a Posted write as a pending obligation | residue grows on every write | P12 |
| 17 | Publish on valid rather than the handshake | requests that were never sent become obligations | P2 |
| 18 | Clear the map on reset with no record | evidence of pre-reset losses destroyed | P14 |
| 19 | Accept observations stamped with a stale epoch | pre-reset completions match post-reset entries | P13 |
| 20 | Skip the end-of-test residue check | lost transactions produce no failure at all | P16 |
| 21 | Treat residue as always acceptable | the same, one config flag away | P16 |
| 22 | Report the watchdog as a PCIe Completion Timeout | an unobserved mechanism claimed as fact (§8) | P21 |
| 23 | Use a DUT helper to predict the expected byte count | design and checker share the bug (24.1 §5) | review |
| 24 | Import the DUT's package for the key comparison | the same, one import away | review |
| 25 | Serialize stimulus to one outstanding transaction to "fix" §4 | removes the concurrency every defect needs | review |
| 26 | Assert ordering across different transactions | correct reordering reported as a bug | P20 |
| 27 | One scoreboard instance shared across ports without a port key | two ports' transactions collide | P5 |
| 28 | Analysis writes dropped under load | silent under-checking | P18 |
| 29 | Scoreboard raises an objection and never drops it | the test never ends | review |
| 30 | Scoreboard reads DUT internal state for the expected value | a mirror, not an oracle | P19, review |
| 31 | Compare payload at the wrong offset | mismatches on correct data | P11 |
| 32 | Downgrade a mismatch to an informational message | failures scroll past in the log | review |
| 33 | Let the scoreboard write to a DUT signal | the measured design is not the shipping one | P19 |
| 34 | Grow the map forever by never retiring terminal entries | memory exhaustion in long runs | P8, P15 |
Two counterexamples worth stating explicitly.
Mutation 1 is the chapter, and it is worth restating as a trace. Requests A (Tag 3) and B (Tag 7) are issued in that order. The Completer answers B first — entirely legal (13.3 §2). The FIFO scoreboard pops A, compares against B's Completion, and reports "expected Tag 3, got Tag 7". Nothing is wrong except the scoreboard. §12 measured 185,564 such reports in one run, and the natural next step — serializing the stimulus so the scoreboard passes (mutation 25) — removes exactly the concurrency that every hardware defect in Modules 22–23 needed to appear.
Mutation 3 is the UVM-specific one and it is silent. A monitor that reuses one pcie_req_item handle, filling it in and calling ap.write(t) each time, hands the scoreboard the same object every cycle. The map stores a handle; the next observation rewrites the fields of the entry already in the map. Every expected entry becomes the most recent request, and the scoreboard reports mismatches on transactions it has quietly overwritten. §10's monitor creates a fresh object per observation, and P17 asserts immutability after publication.
14. Debugging
Symptom — failures appear only with more than one outstanding transaction. Suspect the scoreboard first, not the DUT (§4). A FIFO of expected results is correct at queue depth one and wrong above it. Check whether the scoreboard has an expected queue or an expected map — that one structural question resolves it before any waveform is opened.
Symptom — unknown-identity errors immediately after reset. Two cases and they are distinguishable. Completions for pre-reset transactions still in flight are correctly reported unknown once the map is cleared — that is the design working. A stale-epoch observation matched against a post-reset entry is the bug (§6). The tell is whether the errors stop once the in-flight traffic drains.
Symptom — the scoreboard fails and a protocol analyzer shows correct traffic.
The monitor or the model is wrong, not the DUT. Check the publication point (handshake versus valid, mutation 17), the key (mutation 2), and whether the transaction object is being reused (mutation 3). An analyzer that agrees with the DUT and disagrees with your scoreboard is evidence about your scoreboard.
Symptom — the DUT and the scoreboard agree, and the system misbehaves. Shared-helper defect (24.1 §5, mutations 23, 24). Agreement between two things containing the same code is not evidence. Rebuild the expected value from the observed request — the byte count the requester asked for — rather than from anything the design computes.
Symptom — residue only at end of test, nothing during it. That is exactly what residue is for (§7). A lost transaction produces no in-run failure. Read which identities remain and how many bytes each received: zero bytes points at the request path, partial bytes points at the completion path, and Chapter 23.3 §14's Tag leak shows as residue that grows run over run.
Symptom — split reads mismatch and single-Completion reads pass. Byte Count or Lower Address handling (§5, mutations 11, 12). A single-Completion read exercises neither the progression check nor the offset check. Reproduce with a two-fragment read — the smallest case that uses both.
Symptom — the same Tag from two Requester IDs collides. The key (§3, mutation 2). This is legal PCIe traffic; the scoreboard is under-keyed. In a multi-Function DUT the Function must be in the key too, which is why §10's key carries it.
Symptom — the scoreboard's memory grows without bound in long runs. Terminal entries are not being deleted (mutation 34), or residue is accumulating because retirement never fires. P15's population check catches it early; without it the first symptom is a simulator running out of memory hours into a regression.
15. Misconceptions
"Expected results go in a queue." Not for PCIe — 185,564 false mismatches on a legal stream (§4).
"Completions arrive in request order." No ordering is implied between different transactions (13.3 §2).
"Serializing the stimulus fixes the mismatches." It removes the concurrency every real defect needs (mutation 25).
"A Tag identifies the transaction." The pair does, plus the Function for a multi-Function DUT (§3).
"An object handle is a fine associative-array key." A reused handle rewrites the entry it keys (§3, mutation 3).
"Count the Completions and compare." The Completer chooses the fragmentation (13.3 §1).
"Byte Count is this fragment's length." It is the remaining count including this one (13.3 §2).
"An over-return can be clamped." Clamping writes past the destination; report it (§5).
"Reset means clear the map." It means retire the entries with a recorded reason (§6).
"The scoreboard's watchdog is a Completion Timeout." It is a test budget (§8).
"If nothing failed during the test, nothing was lost." A lost transaction fails nothing until the residue check (§7).
"Reuse the DUT's helper — it's already tested." Then the checker contains the bug it is meant to find (24.1 §5).
"A Posted write should be tracked like a read." No Completion is coming (12.2).
16. Understanding Check
Q1. Your scoreboard reports thousands of "expected Tag 3, got Tag 7" errors. The analyzer shows legal traffic. What is wrong? The scoreboard assumes an ordering PCIe does not provide (§4). Completions of different transactions may arrive in any order (13.3 §2), so an expected-result FIFO fails on nearly every reordered Completion — §12 measured 185,564 on a 250,000-event legal stream. The fix is a map keyed by identity, with ordering checked within a transaction only.
Q2. Why is the scoreboard's expected byte count taken from the observed request rather than from the DUT? Because an oracle that asks the design what to expect cannot disagree with it (24.1 §5). The requester asked for N bytes on the interface; that observation is independent of every internal computation. If the DUT's own length logic is wrong, the scoreboard still knows the right answer — which is the entire reason it exists.
Q3. A monitor fills in one pcie_req_item and calls ap.write(t) each time. What breaks, and when?
Every scoreboard entry becomes the most recent request (§3, mutation 3). The map stores a handle, and the next observation rewrites the fields of entries already stored. It breaks as soon as two transactions are outstanding, and it presents as mismatches on transactions that were silently overwritten — with no clue pointing at the monitor. §10's monitor creates a fresh object per observation.
Q4. On reset with twelve transactions outstanding, what should the scoreboard do? Retire them with a recorded reason consistent with the DUT's declared reset contract (§6) — not delete them silently, which destroys evidence of anything already lost. And it must reject observations stamped with the pre-reset epoch, or a Completion still in flight will match a post-reset entry. §12's environment model measured 4,692 stale acceptances without the epoch check.
Q5. The test passes, coverage is high, and a transaction was silently dropped. Which check finds it? End-of-test residue (§7). A lost transaction produces no in-run failure — no mismatch, no unknown identity, nothing. It is simply never completed. The residue check is the only thing that sees it, and it also reports the resource residue that is the scoreboard-visible form of Chapter 23.3 §14's Tag leak.
Q6. Your scoreboard reports "Completion Timeout" after 50,000 cycles. What is wrong with that report? It claims a PCIe mechanism the scoreboard never observed (§8). What actually happened is that a test budget elapsed. The honest report names the transaction, the bytes received and the elapsed test time — from which a real Completion Timeout may or may not be inferred, and Chapter 25.7 owns making that inference.
17. What's Next
The scoreboard proves that what came back was correct. It says nothing about whether the campaign ever produced the traffic that matters.
Chapter 24.4 Coverage answers that. §12's stimulus deliberately included inter-transaction reordering, split Completions and multiple Requester IDs — and a campaign without those exercises neither this scoreboard nor 24.2's properties. Coverage is how you know.
24.5 VIP then addresses where the observations come from when the protocol side is a commercial agent, and 24.6 UVM Architecture wires this scoreboard, those subscribers and 24.2's bound checkers into one environment — including the reset epoch §6 assumed was coordinated somewhere.
And 24.7 Error Injection builds the traffic this chapter's error paths need. Every terminal-status branch in §10 is unreachable until something deliberately returns a bad Completion status — which is the next chapter's subject, not this one's.