Every sequence and property you've seen so far compares signals against their values at whatever cycle they're checked. But some checks genuinely need to remember a value from earlier in the sequence, and compare it against something that happens later — without that, you can only ever compare against a signal's current value, which may have already moved on by the time you actually need it.

12 min read | Intermediate Level

What You'll Learn

  • Why comparing against a signal's current value can silently check the wrong thing, once that signal has already changed
  • How to declare and assign a local variable inside a sequence, using assignment-action syntax
  • Why local variables correctly track each separate attempt independently, since a new attempt starts on every clock edge
  • A canonical real use case: verifying that returned data matches the address that was actually requested

The Problem: A Signal Can Change Before You're Ready to Check It

Consider checking that acknowledged data matches the address that was requested, without any local variable:


// Without a local variable - compares against addr's CURRENT value
assert property (@(posedge clk) req ##2 (ack && data == addr));

Now suppose addr is 0x10 when req fires, but by the time ack arrives two cycles later, the bus has already moved on and addr now reads 0x20 — a completely normal thing to happen. data correctly echoes back 0x10, the address that was actually requested. But this property compares data against addr's current value at the moment of the check — 0x20, not 0x10 — so 0x10 == 0x20 is false, and the assertion reports a failure, even though the design behaved exactly as intended.

Declaring and Assigning a Local Variable

A local variable, declared inside a sequence, lets you capture a value at the moment it matters and carry it forward, immune to whatever happens to the original signal afterward:


sequence s_addr_check;
  logic [7:0] captured_addr;
  (req, captured_addr = addr) ##2 (ack && data == captured_addr);
endsequence

assert property (@(posedge clk) s_addr_check)
  $display ("[%0t] Match: data correctly matches the originally requested address", $time);

logic [7:0] captured_addr; declares the local variable at the top of the sequence. (req, captured_addr = addr) is the key piece — the comma-separated part after req is an assignment action, executed the same cycle req is found true, locking in whatever addr was at that exact moment. Two cycles later, the consequent compares data against captured_addr — the frozen value from request time — not against addr's current value.

Comparing Both Versions on the Same Trace

Using the exact same signal trace for both properties:

CycleEvent
1req=1, addr=0x10
2addr changes to 0x20
3ack=1, data=0x10

Without the local variable: at cycle 3, data == addr compares 0x10 against addr's current value, 0x20 — fails.

With the local variable: captured_addr locked in 0x10 back at cycle 1. At cycle 3, data == captured_addr compares 0x10 against 0x10 — passes, correctly recognizing that the design did exactly what it was supposed to.

Quick Check

Q: If addr had stayed at 0x10 the entire time instead of changing to 0x20, would both versions of the property produce the same result?

Show Answer

Yes. The two versions only disagree when the signal being compared changes between the moment it's captured and the moment it's checked. If addr never changes, comparing against its current value and comparing against a captured value give the identical result.

Local Variables Track Each Attempt Independently

Since a fresh attempt at a sequence starts on every single clock edge, several attempts can be in flight simultaneously — and each one gets its own independent copy of any local variable it declares. An attempt that starts at cycle 5, capturing its own addr value, doesn't interfere with a completely separate attempt that started at cycle 3 and captured a different value at that time. Each thread's local storage is entirely private to that thread.

Quick Check

Q: If a second req fired at cycle 2 (one cycle after the first, with a different address), while the cycle-1 attempt is still waiting to resolve at cycle 3, would the two attempts' captured_addr values interfere with each other?

Show Answer

No. Each attempt has its own private copy of captured_addr, tied to that specific thread — the cycle-1 attempt's captured value and the cycle-2 attempt's captured value are completely independent, even though both are in flight at the same time.

Where Are Local Variables Used?

Verifying that returned data matches a requested address is one of the most common uses, but the same pattern applies anywhere a value needs to be remembered across a multi-cycle transaction — counting how many grants occurred during a request window, or checking that a response's tag matches the tag of the request that triggered it.

Common Beginner Mistakes

Mistake #1: Comparing against a signal's current value instead of capturing it

As the first example shows, this produces a spurious failure the moment the compared signal legitimately changes between the trigger and the check. Why? If a design has any reason to move on to something else before the response arrives — which is completely normal — a check with no local variable can only ever see the signal's latest value, not the one that actually mattered.

Mistake #2: Forgetting the comma-separated assignment-action syntax

A local variable assignment is attached to a specific term using (condition, variable = expression), inside the parentheses — not as a separate statement elsewhere in the sequence. Why? This syntax ties the assignment to the exact cycle the condition is found true, which is what makes the captured value correspond to the right moment in the first place.

Mistake #3: Assuming overlapping attempts share the same local variable

Each independent attempt at a sequence gets its own private copy of any local variable it declares. Why? Assuming a shared, single copy would mean one attempt's captured value could overwrite another's, corrupting checks that are supposed to be completely independent of each other.

Mistake #4: Declaring a local variable outside the sequence or property

A local variable has to be declared inside the sequence or property block it belongs to, not as an ordinary module-level variable. Why? A module-level variable would be shared storage across every attempt, losing exactly the per-attempt independence a local variable is meant to provide.

Practice Exercise

Challenge: Capture and Compare a Transaction Tag

Write a sequence with a local variable captured_tag that captures tag the cycle req is seen, and checks that resp_tag matches it exactly 3 cycles later, when resp_valid is high.

Hint

Follow the same structure as s_addr_check — declare the local variable, attach the assignment action to the req term, and compare against the captured value in the consequent.

Solution

sequence s_tag_check;
  logic [3:0] captured_tag;
  (req, captured_tag = tag) ##3 (resp_valid && resp_tag == captured_tag);
endsequence

assert property (@(posedge clk) s_tag_check);

Just like the address example, this correctly validates resp_tag against the tag that was actually present when req fired — not whatever tag happens to be three cycles later, which could easily have changed if another transaction started in the meantime.

Summary

In this tutorial, you learned:

  • Comparing against a signal's current value only works if that signal never changes between the trigger and the check — a local variable is needed whenever it might
  • A local variable is declared inside a sequence or property, and assigned using (condition, variable = expression) attached to a specific term
  • Each independent attempt at a sequence gets its own private copy of any local variable, even when several attempts are in flight simultaneously
  • Verifying that a response (data, tag, or similar) matches what was actually requested is the canonical use case for this feature
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5
Cron Job Starts