Skip to content

AMBA APB · Module 15

APB Assertions

Encoding the APB protocol-rules catalogue as SystemVerilog Assertions — property structure (clock, disable iff, antecedent |-> / |=> consequent, $stable), one property per rule for phase, select, stability, ready and error, and binding the checker non-intrusively to the bus with bind. The always-on protocol monitor that fails the instant a rule breaks.

The rules catalogue told you what legal APB is; assertions make a tool check it, continuously, on every edge. The single idea to carry: an APB assertion is a 1:1 encoding of one catalogue rule into a concurrent SVA property — clocked on pclk, disabled during reset, gated by an antecedent and obligated by a consequent — bind-ed non-intrusively to the bus so it fails the instant the rule breaks. You are not re-deriving the rules (they were enumerated in 15.1); you are turning each one into a property that an always-on simulation or formal engine evaluates. Get the property structure right — the clocking event, the disable iff, the antecedent/consequent split, $stable, and the bind — and the catalogue becomes a live protocol monitor. Get it subtly wrong — a vacuous antecedent, a missing reset disable — and the assertion reports green while the rule goes unchecked.

1. Problem statement

The problem is rendering each catalogue rule as a concurrent SystemVerilog Assertion (SVA) property that an engine evaluates on every clock, so a protocol violation fails an assertion the moment it occurs — and doing so non-intrusively, so the checker attaches to any APB bus without editing the DUT.

A rule in prose — "the address is held stable from SETUP through completion" — cannot fail a simulation. It has to become an executable proposition with three things made precise: when it is sampled (which clock edge), when it applies (the gating condition, or antecedent), and what must then hold (the obligation, or consequent). SVA is the language that expresses exactly this, and the encoding has hard requirements:

  • It must be a temporal proposition, not a combinational if. Protocol rules span cycles — "if PSEL is high and the transfer isn't completing this cycle, then next cycle PADDR is unchanged." That "next cycle" is temporal; it needs a clocking event and an implication operator, not a procedural conditional.
  • It must be disabled during reset. While PRESETn is low the bus is not running a protocol; a property that keeps evaluating in reset fires spurious failures (or, worse, gets waived and masks a real one). Every property carries disable iff (!presetn).
  • It must be non-intrusive and reusable. The checker cannot live inside the DUT — it has to attach from outside, to the interface signals, so the same checker works on any APB slave and ships nothing into silicon. That is what bind is for.

So the job is to take a complete catalogue and produce a complete, syntactically correct, idiomatic set of SVA properties — one per rule — bound to the bus as an always-on monitor.

2. Why previous knowledge is insufficient

Chapter 15.1 gave you the rulebook: phase, select, stability, ready, error and reset rules, each atomic and numbered. But a rule list is a document — it does not run. What you are missing is the language and structure that turns a written rule into something an engine checks:

  • A rule statement is not a property. "PENABLE is low in SETUP and high in ACCESS" is English. The property is @(posedge pclk) disable iff(!presetn) (psel && !penable) |=> penable — and writing it forces decisions the prose hides: which edge samples, whether the obligation is same-cycle (|->) or next-cycle (|=>), and what arms the check. The catalogue does not teach SVA; this chapter does.
  • Assertions are not the procedural monitor. Chapter 15.3 (the monitor) reconstructs transactions in procedural code — a class sampling the bus, building transaction objects, comparing in a scoreboard. SVA is declarative: you state the obligation and the engine proves or refutes it every cycle, with no sampling loop to write. The two are complementary — assertions are the dense, always-on rule checks; the monitor is the transaction reconstructor. Confusing them is a classic gap.
  • Reachability is not coverage. This chapter uses cover to confirm an assertion's antecedent actually fires (reachability — did the scenario happen at all). The functional coverage model — bins for every wait-count, every error case, every back-to-back pattern — is Chapter 15.5. cover property here answers "did this property ever arm?"; the coverage model answers "did we exercise the whole space?"

So the model to add is SVA structure: clocking, reset-disable, antecedent/consequent, sequences, $stable/$isunknown, and bind — the machinery that makes the catalogue executable.

3. Mental model

The model: an SVA property is a contract with a trigger and a clause. The antecedent is the trigger ("if this situation arises on the bus"); the consequent is the clause that then becomes legally binding ("then this must hold"). The clocking event says when the contract is read (which edge samples the signals); disable iff(!presetn) says the contract is void during reset. The engine re-reads the contract on every clock and the instant the trigger fires but the clause is broken, the assertion fails — pointing at the exact cycle and signal.

Four refinements make it precise and interview-ready:

  • |-> is same-cycle; |=> is next-cycle. antecedent |-> consequent means the consequent must hold in the same cycle the antecedent matched. antecedent |=> consequent means it must hold one cycle later — exactly |-> ##1 consequent. Stability rules use |=> ("if held this cycle, next cycle the value is unchanged"); a same-cycle combinational rule like "PENABLE high ⇒ PSEL high" uses |->. Choosing wrong shifts the check by a cycle and either fires falsely or misses the violation.
  • $stable / $past / $isunknown are the sampled-value functions. $stable(sig) is true when sig has the same sampled value as the previous clock — the natural encoding of every stability rule. $isunknown(sig) is true if any bit is X/Z — the encoding of "never-X" rules (e.g. PSLVERR must not be unknown at completion). $past(sig, n) reaches back n cycles. These operate on sampled values (preponed to the clock edge), which is why glitches between edges don't trip them.
  • The antecedent must actually fire, or the pass is vacuous. If the antecedent is never true on the trace, the implication is trivially satisfied every cycle — the property reports PASS while never once testing the consequent. A vacuous pass is an unasked question. The defence is cover property on the antecedent: if it never covers, the assertion never armed.
  • bind attaches the checker from outside. You write the properties in a separate module (or checker), then bind it to the APB interface or DUT instance. The checker sees the bus signals as if it were inside, but the DUT source is untouched and the checker ships nothing into the netlist. One checker, bound to every APB slave.
A labelled SVA property: a top band naming clock, disable iff, antecedent and consequent; the middle showing the stability property colour-keyed into those parts with the |=> operator highlighted; and a waveform of pclk, presetn, psel, penable, pready with a blue marker on the edge the antecedent samples true and a green marker one cycle later where the consequent is checked and passes.
Figure 1 — the anatomy of an APB SVA property and where it samples and fires. The top band names the four parts of every concurrent property: the clocking event @(posedge pclk) that fixes the sampling edge, disable iff(!presetn) that voids the property during reset, the antecedent sequence that arms the check, and the |-> / |=> operator joining it to the consequent obligation. The middle shows the stability property (catalogue rule STAB-1) broken into colour-keyed segments — clock blue, disable amber, antecedent blue, the |=> operator highlighted, $stable consequent green. The waveform at the bottom traces pclk, presetn, psel, penable and pready; a blue marker sits on the rising edge where the antecedent samples true (psel high, transfer not yet completing) and a green marker one cycle later where, because |=> defers to the next edge, $stable(paddr) is evaluated and passes. The figure teaches that the antecedent decides when the property is armed and the operator decides whether the obligation is checked this cycle or next — and that an antecedent which never arms makes the whole property pass vacuously.

4. Real SoC implementation

In a real environment the properties live in a dedicated checker module — one property per catalogue rule, each commented with its rule ID — and a bind statement attaches it to the APB interface so it monitors every transfer without touching the DUT. A representative checker:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// apb_protocol_checker.sv — concurrent SVA encoding of the APB rules catalogue.
// Non-intrusive: bound to the APB interface, ships nothing into the netlist.
// One property per catalogue rule; each assert/cover names the rule ID it owns.
module apb_protocol_checker #(parameter int ADDR_W = 32, DATA_W = 32) (
  input logic              pclk,
  input logic              presetn,
  input logic              psel,      // single-slave view; one-hot handled at the fabric
  input logic              penable,
  input logic              pwrite,
  input logic [ADDR_W-1:0] paddr,
  input logic [DATA_W-1:0] pwdata,
  input logic [DATA_W-1:0] prdata,
  input logic [DATA_W/8-1:0] pstrb,
  input logic              pready,
  input logic              pslverr
);
 
  // A transfer is "active and not yet completing" when PSEL is high and we are
  // not on the completing edge (PENABLE & PREADY). Reused by the stability rules.
  let in_held_transfer = psel && !(penable && pready);
 
  // ---- PHASE rules -------------------------------------------------------
  // PHASE-2: PENABLE is low in SETUP. SETUP = first cycle of a transfer:
  //          PSEL rises while PENABLE was low. Same-cycle obligation → |->.
  property p_phase_setup_low;
    @(posedge pclk) disable iff (!presetn)
      ($rose(psel) && !penable) |-> !penable;
  endproperty
  a_phase_setup_low: assert property (p_phase_setup_low);   // PHASE-2 (SETUP)
 
  // PHASE-1 + PHASE-2: SETUP is followed by ACCESS next cycle, i.e. once PSEL
  //          is high with PENABLE low, the NEXT cycle PENABLE must be high
  //          (assuming PSEL stays asserted). Next-cycle obligation → |=>.
  property p_setup_to_access;
    @(posedge pclk) disable iff (!presetn)
      (psel && !penable) |=> (penable);
  endproperty
  a_setup_to_access: assert property (p_setup_to_access);   // PHASE-1/2
 
  // ---- SELECT rules ------------------------------------------------------
  // SEL-1: PENABLE is never high unless PSEL is high (combinational → |->).
  property p_enable_implies_sel;
    @(posedge pclk) disable iff (!presetn)
      penable |-> psel;
  endproperty
  a_enable_implies_sel: assert property (p_enable_implies_sel);  // SEL-1
  // (SEL-2 one-hot PSEL is checked at the fabric level over the PSEL vector:
  //  assert ($onehot0(psel_vec)); — bound where all PSELx are visible.)
 
  // ---- STABILITY rules ---------------------------------------------------
  // STAB-1/2: address, control and write data are held stable across a transfer
  //           that has not yet completed. Next-cycle → |=> with $stable.
  property p_request_stable;
    @(posedge pclk) disable iff (!presetn)
      in_held_transfer |=> ($stable(paddr) && $stable(pwrite) &&
                            $stable(pwdata) && $stable(pstrb));
  endproperty
  a_request_stable: assert property (p_request_stable);     // STAB-1/2
 
  // ---- READY rules -------------------------------------------------------
  // RDY-1: PREADY is only meaningful (sampled) in ACCESS; outside ACCESS it
  //        must not falsely signal completion. Encoded as: completion edge
  //        (PSEL & PENABLE & PREADY) only occurs when PENABLE is high.
  // RDY-2: bounded completion — once in ACCESS, PREADY must rise within N
  //        cycles (N from the slave's worst-case wait spec). Bounded liveness.
  property p_bounded_completion;
    @(posedge pclk) disable iff (!presetn)
      (psel && penable) |-> ##[0:$] pready;   // eventually completes
  endproperty
  a_bounded_completion: assert property (p_bounded_completion);  // RDY-2
 
  // ---- ERROR rules -------------------------------------------------------
  // ERR-1: PSLVERR is only valid at completion (PSEL & PENABLE & PREADY);
  //        and at that completing edge it must be a known 0/1, never X.
  property p_pslverr_only_at_completion;
    @(posedge pclk) disable iff (!presetn)
      pslverr |-> (psel && penable && pready);
  endproperty
  a_pslverr_only_at_completion: assert property (p_pslverr_only_at_completion); // ERR-1
 
  property p_pslverr_known_at_completion;
    @(posedge pclk) disable iff (!presetn)
      (psel && penable && pready) |-> !$isunknown(pslverr);
  endproperty
  a_pslverr_known: assert property (p_pslverr_known_at_completion);  // ERR-1 (never-X)
 
  // ---- COVER: prove the antecedents actually fire (anti-vacuity) ----------
  c_held_transfer: cover property (@(posedge pclk) in_held_transfer);
  c_wait_state:    cover property (@(posedge pclk) (psel && penable && !pready));
  c_error_resp:    cover property (@(posedge pclk) (psel && penable && pready && pslverr));
 
endmodule
 
// ---- BIND: attach the checker to the DUT's APB interface, non-intrusively --
// The checker is instantiated against apb_if without editing the slave RTL.
bind apb_slave apb_protocol_checker #(.ADDR_W(32), .DATA_W(32)) u_apb_chk (
  .pclk    (pclk),    .presetn (presetn),
  .psel    (psel),    .penable (penable), .pwrite (pwrite),
  .paddr   (paddr),   .pwdata  (pwdata),  .prdata (prdata),
  .pstrb   (pstrb),   .pready  (pready),  .pslverr(pslverr)
);

Two facts make this the right shape. First, each property names its catalogue rule ID, so the assertion set is an auditable derivation from 15.1 — a sign-off review walks rule IDs and confirms each has a property (and that each property non-vacuously fires via its cover). Second, bind keeps the checker non-intrusive and reusable: the same apb_protocol_checker binds to every APB slave in the SoC, lives only in the verification compile, and ships nothing into the netlist — which is precisely why assertions and the procedural monitor can both watch the same bus without interfering.

5. Engineering tradeoffs

Writing the SVA is a sequence of structural choices — operator, gating, where each construct fits.

ConstructUse it forDon't use it forWhy
assertChecking DUT behaviour — every catalogue ruleConstraining stimulusAn assert fails the test when the property is violated; it is the rule checker
assumeConstraining inputs, mainly in formalChecking the DUTIn formal, assume tells the engine the manager only drives legal APB — it bounds the input space so the proof is about a legal environment, not a check
coverProving a scenario is reachable (antecedent fired)Measuring functional coveragecover property confirms the property armed non-vacuously; the coverage model measures the space

The second axis is how each rule category maps to property style:

Rule categoryOperator / functionWhy
Phase (SETUP→ACCESS)`=>` (next-cycle)
Phase / Select (combinational, e.g. PENABLEPSEL)`->` (same-cycle)
Stability (PADDR/PWDATA/PWRITE/PSTRB held)`=>+$stable(...)`
Ready (bounded completion)`-> ##[0:$]or##[1:N]`
Error / never-X (PSLVERR known at completion)`->+!$isunknown(...)`

The throughline: assert checks, assume constrains inputs (in formal), cover proves reachability — three different jobs, not three strengths of the same thing. And the operator follows the rule's timing: same-cycle relationships are |->, "next cycle / held" rules are |=>, completion is bounded liveness. Pick the operator from the rule, not from habit.

6. Common RTL mistakes

7. Debugging scenario

The signature SVA failure is not a noisy false fail — it is a silent vacuous pass: an assertion that reports green on every run because its antecedent never once armed, so a real protocol bug it was meant to catch sailed straight through.

  • Observed symptom: a slave passes its full assertion suite, every property reports PASS, sign-off says "assertions clean." In integration the slave intermittently corrupts write data under back-to-back traffic with wait states — the exact failure the stability assertion was written to catch. The assertion that should have screamed never fired.
  • Waveform clue: on the failing trace, PWDATA changes during a wait state (manager holding the transfer with PREADY low). Yet the assertion log shows a_stab: PASS. Pulling the assertion's coverage reveals the tell: the property's antecedent has 0% coverage — it never evaluated non-vacuously, not once, across the entire regression.
  • Root cause: the stability property's antecedent was written too narrowly — it gated on (psel && penable && !pready && pwrite==0), i.e. it only armed on a read wait, while write data only matters on writes. The condition was never true on a write transfer, so the property was vacuously satisfied every cycle and the $stable(pwdata) consequent was never checked. The assertion existed, compiled, and "passed" — but it was an empty contract whose trigger could never fire.
  • Correct RTL (corrected SVA): widen the antecedent to arm on every held transfer beat, and pair it with a cover so a non-firing antecedent is visible:
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// FIX: arm on any held transfer (read or write), and prove the antecedent fires.
property p_request_stable;
  @(posedge pclk) disable iff (!presetn)
    (psel && !(penable && pready)) |=> ($stable(pwdata) && $stable(paddr) &&
                                        $stable(pwrite) && $stable(pstrb));
endproperty
a_request_stable: assert property (p_request_stable);
c_held_transfer:  cover  property (@(posedge pclk) (psel && !(penable && pready)));
  • Verification assertion: beyond fixing this property, add a sign-off gate: every assert must have a paired cover on its antecedent, and the regression report must show every such cover HIT. An assertion whose antecedent cover is 0% is treated as a failure of the suite, not a pass — vacuity is a coverage hole, not a clean result.
  • Debug habit: when a "fully verified" slave fails on a rule its assertion supposedly checks, do not assume the assertion is wrong — check whether it ever fired. Read the non-vacuous evaluation count and the antecedent cover. A PASS with zero non-vacuous evaluations means the property never tested anything; the bug is a too-narrow or mistyped antecedent, and the fix is to make the trigger reachable and prove it with cover.
Two cases: the top red case shows a too-narrow antecedent that never arms, so the assertion passes vacuously (non-vacuous evals = 0) and a PWDATA-drift bug escapes a green regression; the bottom green case shows the corrected antecedent plus a cover that HITs, so the same bug makes $stable fail and the assertion catches it.
Figure 2 — a vacuously-passing assertion that never tested the bug, versus the corrected non-vacuous property. Top (red): the stability property's antecedent is written so narrowly — gated on a read wait while pwdata only matters on writes — that it is never true on the trace; the assertion reports PASS with zero non-vacuous evaluations, and a real PWDATA-drift-during-wait bug escapes a green regression. Middle column: the diagnostic — a passing assertion whose antecedent coverage is 0% never armed. Bottom (green): the corrected property arms on every held transfer beat and is paired with a cover that HITs, so when the same PWDATA drifts during a write wait the consequent ($stable) is evaluated, returns false, and the assertion FAILS — catching the bug in regression. The figure teaches that a green assertion is meaningless until you confirm it fired non-vacuously, and that pairing every assert with a cover on its antecedent turns a silent vacuous pass into a visible reachability failure.

8. Verification perspective

Assertions are not "set and forget" — their own completeness is a verification deliverable, measured by non-vacuity and coverage of antecedents, and the choice between simulation and formal changes what they buy you.

  • Vacuity is the first thing you verify about your assertions. A passing assertion proves nothing unless it armed. So every assert is paired with a cover property on its antecedent, and sign-off requires every such cover to be HIT — a 0% antecedent cover means the property never tested its rule. This is assertion coverage: not "did any assertion fail" but "was every property exercised non-vacuously." It is the single highest-value check on the assertion suite, because a vacuous property is indistinguishable from a passing one in the failure log.
  • Formal and simulation use the same properties differently. In simulation, properties are checked against the stimulus you happen to drive — they fail only on traces you generate, and vacuity is a real risk (an antecedent you never stimulated). In formal, the engine explores all legal input traces: you assume the manager drives legal APB (constraining inputs), and the tool proves each assert holds for every reachable state or returns a counterexample. Formal turns "passed on my tests" into "cannot be violated" — invaluable for the dense, local APB rules (stability, phase, select) that are fully provable, while bounded-liveness ready rules may need a depth bound.
  • bind reuse makes the checker an SoC-wide asset. Because the properties are written against interface signals and attached by bind, one apb_protocol_checker binds to every APB slave in the design — and, in formal, to the same RTL under a different harness. The checker is authored once, audited once against the catalogue, and reused everywhere, which is what keeps the assertion set consistent across dozens of peripherals instead of drifting per-block. It also coexists cleanly with the procedural monitor and feeds the same sign-off as the coverage model: assertions prove the rules, the monitor reconstructs transactions, coverage confirms the space was exercised.

The point: verify the assertions by non-vacuity and antecedent coverage, exploit formal to turn passes into proofs, and use bind to make one audited checker watch every APB bus — because an assertion that never fires is worse than none, since it reads as green.

9. Interview discussion

"Walk me through how you'd write an APB protocol checker in SVA" is a critical verification question, and the answer that signals real experience moves from structure to non-vacuity to bind.

Lead with the structure: every concurrent property has a clocking event (@(posedge pclk)), a disable iff(!presetn) so it is void in reset, an antecedent that arms the check, and a consequent obligated by |-> (same cycle) or |=> (next cycle). Then show you can map the catalogue: a same-cycle relationship like "PENABLEPSEL" is penable |-> psel; a stability rule is (psel && !(penable && pready)) |=> $stable(paddr); "PSLVERR only at completion" is pslverr |-> (psel && penable && pready); the never-X rule uses !$isunknown(...). The depth flourishes are three: assert vs assume vs cover — assert checks the DUT, assume constrains inputs in formal, cover proves reachability, three different jobs not three strengths; vacuity — a green assertion means nothing unless its antecedent fired, so you pair every assert with a cover on its antecedent and treat a 0% antecedent cover as a suite failure; and bind — the checker is a separate module bound to the interface so it is non-intrusive, ships nothing into the netlist, and the one audited checker reuses across every APB slave. Closing with "in formal I assume legal manager stimulus and the tool proves the stability and phase properties for all reachable states — turning 'passed my tests' into 'cannot be violated'" demonstrates you understand assertions as both a simulation monitor and a formal contract.

10. Practice

  1. Encode three rules. Write SVA properties for: "PENABLE is high in ACCESS one cycle after SETUP," "PADDR is stable across a held transfer," and "PSLVERR is only asserted at completion." State the operator (|-> vs |=>) for each and why.
  2. Spot the vacuity. Given (psel && penable && !pready && pwrite==0) |=> $stable(pwdata), explain why it can pass vacuously on every write transfer, and rewrite the antecedent so it arms on every held beat. Add the cover you'd pair with it.
  3. Assert vs assume vs cover. For each of these, say which construct fits and why: (a) the DUT must never assert PSLVERR outside completion; (b) in a formal run, the manager only ever drives legal APB; (c) confirm the wait-state-with-error scenario was reached.
  4. Reset discipline. Show what goes wrong if a stability property omits disable iff(!presetn), and how a waived reset-time failure can later mask a real bug.
  5. Bind it. Write a bind statement attaching an apb_protocol_checker to an apb_slave instance, and explain in one sentence why bind keeps the checker non-intrusive and reusable.

11. Q&A

12. Key takeaways

  • An APB assertion is a 1:1 encoding of one catalogue rule into a concurrent SVA property — clocking event, disable iff(!presetn), antecedent, and |->/|=> consequent — and the full suite is a complete, auditable derivation from the rules catalogue.
  • |-> is same-cycle, |=> is next-cycle (|=>|-> ##1); pick the operator from the rule's timing — combinational relationships use |->, "held / next-cycle" stability and phase rules use |=>, and bounded completion is liveness (##[1:N]).
  • $stable encodes stability rules, $isunknown encodes never-X rules, both on sampled values at the clock edge — which is why glitches between edges don't trip them.
  • assert checks, assume constrains inputs (in formal), cover proves reachability — three different jobs, not three strengths of one construct.
  • A green assertion means nothing unless it fired non-vacuously — pair every assert with a cover on its antecedent and treat a 0% antecedent cover as a suite failure; vacuity is the dominant silent-escape mode.
  • bind makes the checker non-intrusive and reusable — one audited apb_protocol_checker, written against the interface, binds to every APB slave and ships nothing into the netlist, and in formal the same properties turn "passed my tests" into "cannot be violated."