Part 4 · Assertions (SVA) · Intermediate
$rose, $fell, $stable, $changed
Edge detection on sampled values vs @posedge events, the LSB rule, the X-transition table, and level vs edge antecedents.
Edges between samples, not events on wires
Procedural code detects an edge as an event — @(posedge req) fires the instant req transitions, at any point in time. $rose(req) is something entirely different: it compares the value of req sampled at the current clock tick against the value sampled at the previous clock tick, and returns true when that pair is (not-1, 1). No clock tick, no edge — a pulse that rises and falls between two clock edges is invisible to $rose because neither snapshot ever captured it high.
The four functions split into two families. $rose and $fell are edge detectors on the least significant bit of their argument. $stable and $changed are full-vector comparators: $stable(v) is true when every bit of v matches the previous sample, and $changed(v) is its exact negation.
// All four functions, sampled at posedge clk by the property clock
property p_grant_follows_req;
@(posedge clk) disable iff (!rst_n)
$rose(req) |-> ##[1:3] $rose(gnt);
endproperty
assert property (p_grant_follows_req);
// $stable on a full vector: config must not move while the core is busy
assert property (@(posedge clk) disable iff (!rst_n)
busy |-> $stable(cfg_reg));
// $changed: every data movement must be flagged by a valid pulse
assert property (@(posedge clk) disable iff (!rst_n)
$changed(data_bus) |-> valid);Cycle-by-cycle waveforms
Replay these by hand — interviewers love asking you to mark exactly which tick each function returns true. Remember the value row shows the sampled value at each tick (Preponed snapshot), and each function compares tick N against tick N−1.
$rose / $fell on a single-bit signal
tick : 1 2 3 4 5 6 7 8
clk : ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐
req (sampled): 0 0 1 1 1 0 0 1
^ ^ ^
$rose(req) : - 0 1 0 0 0 0 1
$fell(req) : - 0 0 0 0 1 0 0
$stable(req) : - 1 0 1 1 0 1 0
$changed(req): - 0 1 0 0 1 0 1
tick 3: previous sample 0, current 1 → $rose=1
tick 4: previous sample 1, current 1 → $rose=0 (still high ≠ rising)
tick 6: previous sample 1, current 0 → $fell=1
tick 1: no previous sample exists → previous value treated as X,
so $stable is false and $rose(req) with req=0 is falseGlitch invisibility: pulse between ticks is never seen
tick : 1 2 3
clk : ┌──┐ ┌──┐ ┌──┐
req (actual) : ___________┌──┐_________________
└──┘ ← rises AND falls between ticks 2 and 3
req (sampled): 0 0 0
$rose(req) : - 0 0 ← never fires
@(posedge req) in procedural code WOULD trigger here.
$rose(req) under @(posedge clk) cannot — no snapshot saw req=1.The LSB rule and the X-transition table
Pass a multi-bit expression to $rose and it silently watches only bit 0. $rose(data_bus) is true when data_bus[0] went to 1 — almost never what you meant. This compiles cleanly on every simulator, which is exactly why it is a classic code-review and interview trap. For vectors, say what you mean: $changed(data_bus), or $rose(|data_bus) if you want “bus became non-zero”.
The 4-state definition matters too: $rose is true when the LSB changes to 1 from any non-1 value — including from X or Z. Right out of reset, a signal whose snapshot history is X→1 reports a rise. That can fire assertions during bring-up before the design is meaningfully initialized, which is one reason disable iff (!rst_n) belongs on nearly every property.
X-TRANSITION TABLE (LSB of the argument)
previous → current $rose $fell $stable $changed
───────────────────────────────────────────────────────
0 → 1 1 0 0 1
1 → 0 0 1 0 1
x → 1 1 0 0 1 ← x→1 counts as rose
x → 0 0 1 0 1 ← x→0 counts as fell
z → 1 1 0 0 1
0 → x 0 0 0 1 ← to-X: neither edge,
1 → x 0 0 0 1 but $changed fires
x → x 0 0 1 0 ← X to X is "stable"
1 → 1 0 0 1 0
0 → 0 0 0 1 0// WRONG: watches only data_bus[0]
assert property (@(posedge clk) $rose(data_bus) |-> valid);
// RIGHT: any movement on the vector
assert property (@(posedge clk) $changed(data_bus) |-> valid);
// RIGHT: bus transitioned from all-zero to non-zero
assert property (@(posedge clk) $rose(|data_bus) |-> valid);Level vs edge antecedents
Choosing req |-> ... versus $rose(req) |-> ... changes how many evaluation threads you spawn. A level antecedent matches every cycle the level holds — if req stays high for 5 cycles you get 5 overlapping attempts, 5 potential failures for one stuck request, and noisy logs. An edge antecedent matches once per event, which is usually the intent for “when a request arrives, then...” style rules. Use the level form only when the rule genuinely applies continuously, such as “while busy, config is stable”.
Level vs edge antecedent: attempts spawned
tick : 1 2 3 4 5 6 7
req (sampled) : 0 1 1 1 0 0 0
req |-> ##2 gnt attempts start at ticks 2,3,4 (three threads)
$rose(req) |-> ##2 gnt attempt starts at tick 2 only (one thread)
If gnt arrives exactly once at tick 4:
edge form → tick-2 attempt checks gnt at tick 4 → PASS, done
level form → tick-3 attempt checks gnt at tick 5 → FAIL
tick-4 attempt checks gnt at tick 6 → FAIL
two spurious failures from one transactionInterview angle
The two most common probes here: “What is the difference between @(posedge sig) and $rose(sig)?” — answer with the sampling model: one is an asynchronous event, the other a comparison of two clock-tick snapshots, so sub-cycle pulses are invisible to $rose. And “what does $rose of a bus mean?” — the LSB rule. Strong candidates also volunteer the X table (x→1 is a rise) and explain when they would deliberately choose a level antecedent over an edge one. If asked to write “grant within 3 cycles of request” on a whiteboard, start from $rose(req) |-> ##[1:3] gnt and say why you picked the edge form.
Key takeaways
$rose/$fell compare consecutive sampled values of the LSB — they are not event controls.
$stable/$changed compare the entire vector; use them (or a reduction) for multi-bit signals.
x→1 counts as $rose and x→0 as $fell — gate bring-up with disable iff.
Edge antecedents spawn one attempt per event; level antecedents spawn one per cycle held.
Pulses that live entirely between clock ticks are invisible to all sampled value functions.
Common pitfalls
$rose(data_bus) on a vector — silently checks bit 0 only; compiles without warning.
Using a level antecedent for a one-shot rule — N overlapping attempts and duplicate failures.
Expecting $rose to catch a glitch the waveform viewer shows between edges — it cannot.
Forgetting that tick 1 has no previous sample — $stable is false and X-history can fake an edge.
Writing $rose(req) when req can legally start high before reset release — the rise is never seen.