PCIe · Module 24
Assertions — Executable Statements About Ownership Over Time
A PCIe assertion is not a syntax exercise. It is a claim about who owns an item, what must stay true while they own it, and which event transfers it — and the hardest part is proving the assertion was ever reached.
Chapter 24.1 said what must be verified. This chapter proves the part that can be proven locally, at the interface or state boundary where the rule actually lives.
And it starts somewhere unexpected. Not with $rose, not with |=>, but with a question you must answer before writing a property:
Who owns this item right now, and what event transfers it?
Every strong PCIe assertion in this chapter is an answer to that question. The temporal operators are how you write the answer down.
1. Sources, Scope, and What the Language Tracks Already Own
2. An Assertion Is a Statement About Ownership Over Time
Take a real rule from Chapter 23.4: a packet's header must not change while the packet is being sent.
The naive translation reaches for operators first — something with $stable and |=>. The useful translation answers four questions first:
What became true? the generator accepted a request
Who owns the item? the packet owner, exclusively
What must stay true? every header field
What transfers it? the ACCEPTED end-of-packet beatOnly now is the property mechanical:
(owner_lock && $past(owner_lock)) |-> $stable(pkt.addr)And the four answers also tell you what would be wrong. Releasing on tx_last asserted rather than accepted changes the fourth answer, so the property must be qualified by the transfer — which is why Chapter 23.4 §11's P20 exists as a separate property from P3.
This is why the chapter is organized by contract rather than by operator. Eight shapes, eight property families, and the operators fall out.
3. Eight Shapes of a Protocol Rule
| Shape | Reads as | PCIe example |
|---|---|---|
| state invariant | X is never true | credits never negative (16.1) |
| event implication | if X then Y | a Completion implies a live context (13.3) |
| held-state contract | while owned, X cannot change | header stable under stall (23.4 §4) |
| bounded sequencing | X then Y within N | an entry pulse lasts exactly one cycle |
| conservation | parts sum to the whole | TOTAL == FREE + LIVE (23.6 §4) |
| mutual exclusion | at most one of these | one grant per cycle (22.3 §9) |
| matching | this response belongs to that request | (Requester ID, Tag) (21.4 §3) |
| range legality | this index is in range | BAR select, Tag index (23.2 §5) |
Two practical consequences.
The shape determines the skeleton. A held-state contract is always owned |-> $stable(...). A conservation rule is always combinational and needs no temporal operator at all — and people write it as a sequence anyway, which is harder to read and no stronger.
And the shape determines what a failure means. A conservation failure means ownership was lost somewhere and the specific place is unknown; a matching failure names the transaction. §18's debugging works by moving from the shape that fired to the shape that is upstream of it.
4. assert, assume, cover — and the Pass That Proves Nothing
5. Sampling, $past, and the Same-Cycle Question
The SystemVerilog track owns the timing model. What is PCIe-specific is which coincidences you will actually meet — and Modules 22 and 23 measured all of them.
The single most important one: a Tag freed and allocated in the same cycle (23.3 §7, measured at a 6.3% divergence between the two implementations).
A property written on the sampled previous state answers a different question than one written on the next-state expression:
// Reads the bitmap BEFORE this cycle's free and allocate.
(alloc_fire) |-> $past(free_map)[tag_grant]
// Reads the value the design will actually have.
(alloc_fire) |=> !free_map[$past(tag_grant)]Both are legitimate; they assert different things. The first says the allocator picked something that was free at the sampling point; the second says the pool reflects the allocation afterwards. A design using the next-state form (which 23.6 pattern 9 recommends) will fail the first property while being correct — because the freed Tag was not free at the sampling point and is legitimately allocatable.
The rule this chapter follows: assert the invariant, not the implementation. Conservation (FREE + LIVE == TOTAL, §13) holds under both implementations and catches the two-branch bitmap that loses a write. It is the property to reach for when the same-cycle behaviour is a design choice rather than a protocol rule.
6. Property Set 1 — Stream Ownership
7. Property Set 2 — Snapshot Immutability
8. Property Set 3 — Tag Allocator
9. Property Set 4 — Request and Completion Lifetime
10. Property Set 5 — Credit Ownership
11. Property Set 6 — Packet Lock
12. Property Set 7 — Link State, Bounded Deliberately
13. Property Set 8 — Conservation and Ghost State
14. Binding — Keeping Verification Out of the RTL
15. The Waveform
Payload mutates under stall — and the cycle the assertion actually reports
10 cyclesFour things to read out of the figure.
The defect is at cycle 3 and the report is at cycle 4. A concurrent assertion evaluates on sampled values, so $stable compares this edge against the previous one — the reported cycle is always one after the change, and a debugger who looks only at the reported cycle sees a stable payload and concludes the assertion is wrong.
assert_good never rises. The corrected trace holds the payload across cycles 2–4 and transfers at cycle 5, which is the same bytes delivered one cycle later.
Cycle 5 is the transfer, and only there may the payload change (cycle 7 shows the next item).
And nothing fires in cycles 0–1. The antecedent valid && !ready is not met, which is precisely why §6's cover property exists — on a bench with no stalls this waveform never happens and the property is green and meaningless.
16. Measured Behaviour
17. Verification — Mutations
Each mutation names the property family that catches it. Entries marked review are defects no property can catch, because the property itself is the defect.
| # | Mutation | Symptom | Caught by |
|---|---|---|---|
| 1 | valid asserted only when ready is high | deadlock with a matching producer | P3 |
| 2 | Payload re-driven while stalled | 99,711 firings (§16) | P1 |
| 3 | valid withdrawn before acceptance | consumer sees a packet that never arrives | P2 |
| 4 | Byte count advances on valid | 133,371 firings; throughput overstated | P4 |
| 5 | Second item accepted into a single-entry stage | the first is overwritten | P5 |
| 6 | Header field derived combinationally from the live request | wrong address on a stalled packet | P7–P9 |
| 7 | Descriptor read from host memory mid-job | length changes under the engine | P10 |
| 8 | Configuration read live into the packet path | 15.3% contract violations (22.4) | P11 |
| 9 | Live/effective divergence hidden | software cannot see its write is not in force | P12 |
| 10 | Tag allocated while still live | 49,292 firings | P15 |
| 11 | Free a Tag that is already free | pool exceeds its size | P16 |
| 12 | Two always_ff branches write the free bitmap | one write lost; leak or stall | P17 |
| 13 | Outstanding count kept separately from the bitmap | the two drift silently | P18 |
| 14 | Tag index unbounded | aliasing into another context | P19 |
| 15 | Unknown Completion aliased to context 0 | slot 0's transaction corrupted | P23 |
| 16 | Match on Tag alone | 39.7% misattribution (23.5) | P22 |
| 17 | Retire on the first Completion fragment | 64,690 firings; later fragments orphaned | P25, P26 |
| 18 | Over-return clamped instead of reported | writes past the destination buffer | P24 |
| 19 | Posted write allocates a Completion context | contexts leak on every write | P27 |
| 20 | Credit subtracted without a bound check | 199,995 firings | P29 |
| 21 | Issue on observed availability rather than a reservation | double-spend under load | P30, P31 |
| 22 | Header credit checked, data credit not | receiver data buffer overrun | P32 |
| 23 | FC update applied every valid cycle | credit inflates while the update stalls | P33 |
| 24 | Reservation released twice | pool rises above advertised | P34 |
| 25 | Header-only TLP charged a data credit | NP requests consume data pools | P35 |
| 26 | Credit conservation summed across classes | two classes wrong oppositely pass | P36 |
| 27 | Owner released on eop asserted, not transferred | next request overwrites an unsent beat | P39 |
| 28 | Payload mux follows the arbiter while the header is locked | header from A, payload from B | P40 |
| 29 | Traffic issued while the Link is not operational | packets into a retraining Link | P42 |
| 30 | Local state encoding unconstrained | an illegal encoding propagates | P43 |
| 31 | Entry indication held for many cycles | downstream counts one entry many times | P44 |
| 32 | Ghost counters driven from the DUT's own counters | the design proves it agrees with itself | review |
| 33 | Checker bound with a mismatched parameter | one instance fails, identical ones pass (§14) | review |
| 34 | bind attached to the wrong module type | extra instances fire on legal behaviour | review |
| 35 | Assertion clocked on the wrong domain | false failures unrelated to any bug | review |
| 36 | Property written against internal state instead of the interface | breaks on every refactor (§14) | review |
| 37 | assume placed on a DUT output | the bug is constrained away; proof succeeds anyway (§4) | review |
| 38 | Eventual-Completion asserted as a DUT obligation | formal cannot converge; simulation false-fails (§9) | review |
| 39 | disable iff covering normal operation, not just reset | the interesting cycles are all masked | review |
| 40 | No cover property on any antecedent | 60,205-versus-0 vacuity, undetected (§16) | review |
| 41 | $past used before enough history exists | a startup-only false failure | review |
| 42 | Completion ordering asserted between different transactions | correct reordering reported as a bug (13.3 §2) | review |
| 43 | Checker drives or gates a DUT signal | the measured design is not the shipping one | review |
| 44 | An LTSSM transition rule written from memory | fails on legal behaviour; the checker gets ignored (§12) | review |
Two counterexamples worth stating explicitly.
Mutation 37 is the one that makes a formal sign-off worthless. Adding assume property (tag_free_map != '0) to help convergence tells the tool the Tag pool never empties. Every property then proves, including the ones that would have caught Chapter 23.3 §14's leak — a leak whose entire symptom is the pool emptying. The assumption did not model the environment; it deleted the bug, and the resulting proof is a statement about a design that does not exist.
Mutation 40 is the most common defect in real assertion suites and it produces no failure at all. A property file with forty implications and no cover properties reports forty passes on a testbench that never stalls, never exhausts credits, and never splits a Completion. §16 measured the difference at 60,205 antecedent hits versus 0 — and the assertion report is byte-identical. The fix costs one cover property per family, which is why every family in §6–§13 carries one.
18. Debugging Assertions
Symptom — the assertion fires only when ready is low.
That is the property working, not a false failure (§15). Stability, no-reneging and transfer-accounting properties all have !ready in their antecedent by construction. The bug is real and load-dependent — which Chapter 24.1 §7 measured as the signature of every ownership defect in Modules 22–23.
Symptom — a known bug exists and no assertion fires. Check reachability before checking the property (§4). Read the cover properties: if the antecedent count is zero, the property was never evaluated. §16 measured a property passing with 0 antecedent hits, indistinguishable in the report from one with 60,205.
Symptom — formal cannot prove a property and does not produce a counterexample.
Two causes and they need opposite fixes. An unbounded liveness property (§9's eventual-Completion) has no bounded proof — remove it and move the question to the scoreboard's residue check. Or a missing environment assumption leaves the tool free to hold ready low forever; the fix is an assume on the environment, never on a DUT output (mutation 37).
Symptom — the property fails in the first cycles after reset release.
Usually $past with insufficient history, or a disable iff window that ends before the design has settled. The fix is not to widen disable iff — that masks real cycles (mutation 39). Qualify the property with a "design has been out of reset for N cycles" term, so the masking is explicit and bounded.
Symptom — a same-cycle Tag free and allocate reports a mismatch.
Check which form the property asserts (§5). A property on $past(free_map) fails a correct next-state implementation, because the freed Tag was not free at the sampling point. Assert conservation (P17) instead, which is true of both implementations and false only of the two-branch bitmap that loses a write.
Symptom — one instance of a checker fails and identical instances pass. Parameter or bind mismatch (§14, mutations 33, 34). Compare the elaborated parameter values per instance. A checker bound with a narrower width compares a truncated payload and passes, so the passing instances are the suspicious ones.
Symptom — the assertion fires far downstream of where you believe the bug is. Move the property upstream toward the first violated contract. A malformed-TLP checker at the Link fires because a header field changed at the generator (§7); a scoreboard mismatch fires because a Tag was misattributed at the matcher (§9). The eight shapes of §3 give the ordering: matching and held-state failures are upstream of conservation failures, which are upstream of "the output is wrong."
Symptom — the checker itself changes the design's behaviour. Any checker that drives, gates, or is read by functional logic (mutation 43). In simulation this shows as a bug that disappears when assertions are compiled out, which is the fastest way to identify it.
19. Misconceptions
"An assertion is a syntax exercise." It is a statement about ownership over time; the operators are how you write it down (§2).
"All assertions passed, so the design is verified." They may all be vacuous — 60,205 versus 0 antecedent hits, both reported as passes (§16).
"A cover property is optional documentation." It is the only evidence an implication was ever evaluated (§4).
"assume makes formal converge, so add more." An assumption on a DUT output deletes the bug and proves a design that does not exist (mutation 37).
"Assert that every request eventually completes." That is the environment's obligation, not the DUT's (§9).
"Completions must arrive in request order." No ordering is implied between different transactions (13.3 §2, mutation 42).
"disable iff (!rst_n) is always right." Only for the reset window; widening it masks the interesting cycles (mutation 39).
"The assertion reports the wrong cycle." It reports the sampling edge after the change — that is the timing model, not a defect (§15).
"Write the property against the internal signal; it's more precise." It breaks on every refactor; bind to the interface contract (§14).
"One bind covers everything." It covers every instance of that module type, including ones you did not mean (§14).
"Conservation is too weak to be useful." It holds under every correct implementation of a same-cycle choice and fails only on the real bug (§5, P17).
"Ghost state is cheating." It is how an invariant that spans transactions becomes expressible — provided it is driven from observations, not from the DUT (§13).
"We should assert the full LTSSM." Not from memory. A fabricated transition rule fails on legal behaviour and gets the checker waived (§12).
20. Understanding Check
Q1. Your entire PCIe property file passes on the first run. What is the first thing you check, and why is it not the design? The cover properties. §16 measured an implication passing with 0 antecedent hits, reported identically to one with 60,205. A first-run all-pass on a new property file is far more likely to mean the stimulus never reached the antecedents — no stalls, no credit exhaustion, no split Completions — than that the design is perfect (§4).
Q2. Formal will not converge on "every Memory Read is eventually completed." What is wrong with the property? It asserts a liveness obligation the DUT cannot discharge (§9). The DUT does not make Completions arrive; the environment does. The decomposition is to assume the environment returns Completions and assert the DUT handles them correctly — and to move "did anything never complete?" to the scoreboard's end-of-test residue check (24.3).
Q3. A Tag is freed and allocated in the same cycle, and your property alloc_fire |-> $past(free_map)[tag_grant] fails. Is the design broken?
Probably not. A next-state implementation (23.6 pattern 9) makes the freed Tag allocatable in that same cycle, so it was not free at the sampling point — the property pins an implementation choice rather than a protocol rule (§5). Assert conservation instead (P17): it holds for both implementations and fails only on the two-branch bitmap that actually loses a write.
Q4. Why does this chapter write seven separate header-stability properties instead of one $stable(pkt)?
So a failure names the field that moved (23.4 §11's rationale, §7 here). $stable(pkt) tells you the packet changed; p_hdr_identity_stable tells you the Tag changed, which points directly at the request-acceptance path rather than at the address computation. The cost is six extra lines and the benefit is skipping a debugging session.
Q5. You add assume property (avail_data[0] > 0) to help a proof converge. What have you actually proven?
That the design is correct when data credits never run out — which is the one condition under which the credit properties are trivially satisfiable (§4, mutation 37). The starvation behaviour Chapter 22.3 exists to describe has been assumed away, and the proof is a statement about a system that has infinite receiver buffering.
Q6. Which property in this chapter would you write first for a brand-new PCIe requester, and why? Conservation — P17 and P46. They need no protocol knowledge, no reachability argument, and they detect both failure modes of the ownership law: a resource owned twice and a resource owned by nobody (23.6 §15). They are also the properties least likely to be vacuous, because they are invariants rather than implications — there is no antecedent to miss.
21. What's Next
Assertions prove what is provable locally. Every property in this chapter fits inside one interface, one allocator, one arbiter, or one credit pool — and that is their limit.
Chapter 24.3 Scoreboards takes the questions SVA cannot answer: which transaction lost bytes, whether Completions arriving out of order were the right Completions, and whether anything was left outstanding when the test ended. §13's ghost counters catch that bytes were lost; the scoreboard names the transaction.
24.4 Coverage then answers the question §4 kept raising — how do you know the stimulus reached the antecedents? Cover properties answer it one property at a time; a coverage model answers it for the campaign.
24.5 VIP and 24.6 UVM Architecture then place all of this inside an environment, and 24.7 Error Injection builds the stimulus that makes §10's and §12's antecedents reachable in the first place — the credit exhaustion, the replay, the malformed packet. This chapter deliberately wrote no injection machinery.