Part 4 · Assertions (SVA) · Intermediate
Immediate vs Concurrent Assertions
Procedural instant checks vs clocked temporal properties — syntax, deferred variants, and decision rules for where each belongs.
Two different machines
SystemVerilog has two assertion kinds that share a keyword and almost nothing else. An immediate assertion is a procedural statement: it executes when the surrounding code reaches it, evaluates its expression instantly, combinationally, with current values , and is done. A concurrent assertion is a declarative, clocked process: it samples signals on a clock edge, can describe behavior spread across many cycles , and runs for the whole simulation independent of any procedural flow. Choosing the wrong one is a classic design-review finding: an immediate assertion cannot say "ack within 3 cycles", and a concurrent assertion is overkill for "this cast succeeded".
// IMMEDIATE — procedural, evaluates the instant it executes
task drive_item(bus_item it);
// classic check-the-return-value idiom
a_cast: assert ($cast(burst_it, it))
else $error("item is not a burst_item");
endtask
always_comb begin
// combinational sanity check, re-evaluates on any input change
a_onehot: assert ($onehot0(grant)) else $error("grant not one-hot-0");
end
// CONCURRENT — declarative, clocked, temporal
// "every req gets an ack within 1 to 3 cycles"
ap_req_ack: assert property (@(posedge clk) disable iff (!rst_n)
req |-> ##[1:3] ack)
else $error("ack missed the 3-cycle window");Note where each lives: the immediate assertions sit inside procedural code (task, always_comb), while the concurrent assertion is a module-level declaration — it needs no surrounding process, because the clock event in the property is its process.
Immediate assertions: instant evaluation and its glitch problem
Because an immediate assertion evaluates the moment procedural code reaches it, it sees intermediate delta-cycle values . In an always_comb block, inputs may settle in several delta steps within one time slot; the assertion can fire on a transient combination that no clocked logic would ever observe. That is a false failure — annoying, and in regressions, corrosive to trust.
GLITCH PROBLEM — one time slot, several delta cycles
time slot T (no simulated time passes between deltas):
delta 0: a=1, b=1 → always_comb runs → assert (!(a && b)) FAILS ✗ (transient!)
delta 1: b←0 settles → always_comb runs → assert passes ✓ (final value)
An IMMEDIATE assert fires at delta 0 — a value no flip-flop would capture.
A DEFERRED assert (assert final) waits until the slot settles → no false fire.Deferred immediate assertions
SystemVerilog 2009 added deferred variants to fix exactly this: assert #0 (observed deferred) postpones evaluation to the Observed region of the current time slot, and assert final postpones it to the Postponed region — after everything has settled. If a glitch caused a failure that later deltas cured, the deferred report is flushed and never printed.
always_comb begin
// plain immediate — may fire on delta-cycle glitches
a_glitchy: assert (!(ready && error));
// observed deferred — evaluated in Observed region, glitch-immune
a_observed: assert #0 (!(ready && error))
else $error("ready and error both high");
// final deferred — evaluated in Postponed region, fully settled values
a_final: assert final (!(ready && error));
endConcurrent assertions: clocked and temporal
A concurrent assertion samples its signals on the clocking event — in the preponed region , meaning values from just before the edge (covered in depth in the sampling sub-lesson). Because evaluation is pinned to clock ticks, it can express relationships across cycles: delays, windows, repetition. This is the form used for protocol rules, and the form interviews mean by default when they say "write an assertion".
CONCURRENT ASSERTION — req |-> ##[1:3] ack, cycle by cycle
clk : T0 T1 T2 T3 T4 T5
req : 0 1 0 0 0 0
ack : 0 0 0 1 0 0
T1: req sampled high → attempt becomes active
│
├─ T2: ack? no (cycle 1 of window)
├─ T3: ack? no (cycle 2 of window)
└─ T4: ack sampled high → MATCH ✓ (cycle 3, last allowed)
Same trace, ack one cycle later (at T5) → window exhausted at T4 → FAIL ✗
An immediate assertion CANNOT express this — no notion of "within 3 cycles".// Concurrent assertions live at module scope (or in interfaces/checkers)
module fifo_props (input logic clk, rst_n, push, pop, full, empty);
// structural protocol rules — always-on, every cycle, whole sim
ap_no_push_full: assert property (@(posedge clk) disable iff (!rst_n)
!(push && full))
else $error("push while full");
ap_no_pop_empty: assert property (@(posedge clk) disable iff (!rst_n)
!(pop && empty))
else $error("pop while empty");
endmoduleDecision rules: which one where
Choose by question, not by habit
Checking a function/task result or a value at one instant inside procedural code → immediate (deferred variant if combinational).
Checking a rule that involves time — "within N cycles", "until", "held while" → concurrent, no exceptions.
Testbench-internal sanity ($cast success, config validity, randomize() return) → immediate.
RTL/interface protocol rules (handshakes, FIFO invariants, FSM legality) → concurrent, usually in an interface or bound checker module.
Combinational invariants in RTL (one-hot mux selects) → deferred immediate inside always_comb, or a clocked concurrent assertion if a clock is natural.
DECISION TABLE
┌─────────────────────────┬──────────────────────┬──────────────────────────┐
│ │ IMMEDIATE │ CONCURRENT │
├─────────────────────────┼──────────────────────┼──────────────────────────┤
│ where it lives │ procedural code │ module/interface scope │
│ when it evaluates │ when code reaches it │ every clock tick │
│ values used │ current (delta-live) │ preponed (pre-edge) │
│ time span │ one instant │ many cycles │
│ typical home │ testbench classes │ RTL / interfaces / bind │
│ glitch sensitivity │ yes (use #0/final) │ no (clock-sampled) │
└─────────────────────────┴──────────────────────┴──────────────────────────┘Interview angle
This is the standard SVA opener. A complete 30-second answer: immediate assertions are procedural and evaluate instantly with current values; concurrent assertions are clocked, sample in the preponed region, and express multi-cycle temporal behavior. Strong candidates volunteer two extras: the deferred variants (assert #0, assert final) that solve combinational glitching, and the placement rule — TB result checks immediate, protocol rules concurrent.
Trap question: "Can an immediate assertion check that ack follows req in 2 cycles?" — No; it has no clock and no temporal extent. You would have to hand-code a procedural watcher, which is exactly what concurrent assertions replace.
Trap question: "Why did my always_comb assertion fire when the waveform looks fine?" — delta-cycle glitch; the waveform viewer shows settled values, the immediate assert saw an intermediate delta. Answer: deferred assertion.
Follow-up: "Where do you put concurrent assertions for a DUT you can't modify?" — interface, or a checker module attached with bind (covered in Advanced SVA).
Key takeaways
Immediate = procedural + instant + current values; concurrent = declarative + clocked + temporal.
Deferred immediate assertions (#0 / final) exist to suppress delta-cycle glitch false-fires in combinational code.
Temporal rules (windows, until, held-while) are impossible in immediate assertions — reach for concurrent.
TB sanity checks → immediate; protocol rules → concurrent in interfaces or bound checkers.
Common pitfalls
Plain immediate assert inside always_comb — fires on delta glitches the hardware never sees.
Forgetting that assert(randomize()) is an immediate assertion — with assertions disabled by tool switches, the randomize() call itself may be optimized away on some tools.
Writing a procedural loop with #10 delays to check a handshake window — fragile hand-rolled replica of ##[1:3].
Putting concurrent assertions inside class-based testbench code — they are illegal in classes; they belong in modules, interfaces, or checkers.