SystemVerilog $rose, $fell, $stable covered detecting an edge or the absence of one. This article covers three more sampled-value system functions: $past, for reaching back further than just "the previous cycle"; $sampled, for reading a signal's assertion-visible value from procedural code; and $changed, the direct logical complement of $stable.
11 min read | Intermediate Level
What You'll Learn
- How
$pastretrieves a signal's value from a specific number of cycles ago, not just the immediately previous one - How
$past's optional gating argument skips cycles where a condition wasn't true - What
$sampledis for, and why it matters specifically inside procedural code - Why
$changedis just the logical opposite of$stable
$past: Reaching Back Further Than One Cycle
$past(expr) returns the value expr had one clock tick ago — the classic use case is checking a registered signal against the value that fed it:
// Checks that out correctly holds whatever in was one cycle earlier -
// a straightforward 1-cycle pipeline register check
assert property (@(posedge clk) out == $past(in));
Given in sampled as 5, 7, 2, 9 across four consecutive cycles, a correctly-implemented 1-cycle delay register (out <= in; every cycle) produces out values of 5, 7, 2 starting from the second cycle onward — and checking out == $past(in) passes at every one of those cycles, since each out value exactly matches what in was one cycle before it.
Now suppose the design has a bug — an off-by-one delay, lagging by 2 cycles instead of 1. At the cycle where out should show in's value from one cycle back (7), it instead still shows the value from two cycles back (5). The assertion catches this immediately: $past(in) correctly evaluates to 7, but out is 5 — a clear failure, exactly identifying the pipeline timing bug.
Looking Back More Than One Cycle
A second argument specifies how many cycles back to look:
assert property (@(posedge clk) out2 == $past(in, 2)); // 2-cycle pipeline stage
Quick Check
Q: At the very first clock edge of a simulation, what would $past(in) return?
Show Answer
The default sampled value of in — not nothing. Per the LRM, when there's no prior clock tick to look back to yet, $past falls back to the expression's default sampled value (0 for a 2-state variable, X for a 4-state one) instead of being left undefined. It's a well-defined result, just not necessarily a meaningful one — it's being compared against a value nothing ever actually drove.
Skipping Cycles With a Gating Argument
A third, optional argument gates which cycles actually count as "history" — instead of strictly N ticks ago in absolute time, $past looks back to the most recent cycle where the gating condition was true, skipping over any cycles where it wasn't:
// Compares captured against data's value from the most recent cycle
// where enable was actually high - not just literally 1 cycle ago
assert property (@(posedge clk) captured == $past(data, 1, enable));
This is useful whenever a value is only meant to update on specific qualifying cycles — without the gating argument, $past would compare against whatever data happened to be exactly one cycle back, even on a cycle where enable was low and nothing was actually supposed to be captured.
$sampled: Reading a Signal's Assertion-Visible Value
$sampled(expr) returns the same preponed-region value an assertion would use, but callable from ordinary procedural code — useful when you want a $display or other procedural statement to show exactly the value an assertion actually evaluated, rather than whatever the signal's live value happens to be at the exact point that procedural code executes:
if (some_condition)
$display ("Value seen by the assertion: %0d", $sampled(data));
Without $sampled, procedural code reading data directly could, in principle, see a value that's already moved on from what the assertion itself sampled in the preponed region — $sampled guarantees the two match.
$changed: The Opposite of $stable
$changed(expr) is true exactly when expr's value differs from its previous sampled value — the direct logical complement of $stable. Reusing the same signal trace verified in the $rose/$fell/$stable article:
| Time (ns) | a | $stable(a) | $changed(a) |
|---|---|---|---|
| 10 | 0 | PASS | FAIL |
| 30 | 1 | FAIL | PASS |
| 50 | 1 | PASS | FAIL |
| 70 | 1 | PASS | FAIL |
| 90 | 1 | PASS | FAIL |
| 110 | 1 | PASS | FAIL |
| 130 | 1 | PASS | FAIL |
| 150 | 0 | FAIL | PASS |
| 170 | 1 | FAIL | PASS |
| 190 | 1 | PASS | FAIL |
At every one of these samples, $changed(a) is exactly the opposite of $stable(a) — wherever the value held steady, $stable passes and $changed fails, and wherever it moved, the reverse.
Even at 10ns — the very first sample, with no earlier posedge yet — $stable and $changed still land on exactly opposite results. Before any real prior tick exists, both functions compare against a's default sampled value (0 for a 2-state bit) instead of being left undefined, so the same complementary relationship holds there too.
Quick Check
Q: Would assert property (@(posedge clk) !$changed(config_reg)); and assert property (@(posedge clk) $stable(config_reg)); check the same thing?
Show Answer
Yes — from the second sample onward, !$changed(x) and $stable(x) are logically identical, since $changed is defined as the exact complement of $stable. Either form expresses "this value must not change" equally well.
Where Are These Used?
$past is the standard way to verify a pipeline stage's registered output against whatever fed it some number of cycles earlier — exactly the kind of check that catches an off-by-one timing bug in a multi-stage datapath. $changed is a natural fit anywhere the condition being checked reads more naturally as "this changed" than "this failed to stay the same," even though both express the same idea.
Common Beginner Mistakes
Mistake #1: Assuming $past returns nothing at all on the very first checked edge
It actually returns a well-defined value even there — the expression's default sampled value, not an undefined result. Why? Per the LRM, when there's no prior clock tick to look back to yet, $past falls back to that default (0 for a 2-state variable, X for a 4-state one) instead of being left unusable — it just may not be a meaningful comparison yet, since nothing real has actually been registered.
Mistake #2: Assuming $past(x, N) always means "N cycles ago" when a gating argument is present
Once a gating condition is added, $past instead looks back to the Nth most recent cycle where that condition was true — not a fixed number of cycles in absolute time. Why? Mixing these two interpretations up produces a check comparing against the wrong historical value entirely, especially on a signal that doesn't update every single cycle.
Mistake #3: Reading a signal directly in procedural code when $sampled was needed
A signal's live value at the point procedural code executes isn't always guaranteed to match what an assertion sampled for that same cycle. Why? If a display statement or debug check needs to show exactly what an assertion actually evaluated, $sampled is the way to guarantee that match.
Mistake #4: Treating $changed as if it were a different check from !$stable
They're the same check, just phrased in opposite terms. Why? Writing both a $changed-based and a $stable-based assertion for what's logically the same condition is redundant — pick whichever phrasing reads more naturally for the specific property being expressed.
Practice Exercise
Challenge: Check a 3-Stage Pipeline
Write an assertion checking that stage3_out correctly holds the value data_in had exactly 3 cycles earlier.
Hint
$past's second argument specifies how many cycles back to look — this is a direct extension of the basic 1-cycle example in this article.
Solution
assert property (@(posedge clk) stage3_out == $past(data_in, 3));
This follows the exact same pattern as the article's own 1-cycle and 2-cycle examples, just extended to a 3-cycle pipeline depth — $past's second argument scales to any fixed delay a design's datapath actually has.
Summary
In this tutorial, you learned:
$past(expr)returns a signal's value from one cycle ago by default, orNcycles ago with an explicit second argument- An optional gating argument makes
$pastlook back to the most recent cycle where a condition was true, rather than a fixed number of cycles in absolute time $sampled(expr)reads a signal's assertion-visible (preponed-region) value from ordinary procedural code$changed(expr)is the exact logical complement of$stable(expr)— true whenever the value differs from its previous sample- Before a real prior sample exists, all of these functions fall back to the expression's default sampled value rather than being left undefined — a well-defined result, even if not yet a meaningful one