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.

systemverilog
// 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.

diagram
$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 false
diagram
Glitch 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.

diagram
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   ← x1 counts as rose
      x       0         0       1        0        1   ← x0 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
systemverilog
// 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”.

diagram
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 transaction

Interview 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.