Immediate assertions are executed based on simulation event semantics and are required to be specified in a procedural block. They're treated the same way as the expression in an if statement during simulation.
The immediate assertion passes if the expression holds true at the time the statement executes, and fails if the expression evaluates to false (X, Z, or 0). These assertions are intended for use in simulation and aren't suitable for formal verification. They can be used in both RTL design and testbench code to flag errors during simulation.
10 min read | Intermediate Level
What You'll Learn
- What makes an assertion "immediate" rather than concurrent, and where it can be used
- The three ways to write an immediate assertion: bare, with pass/fail blocks, and with a name label
- How an immediate assertion inside a design catches a protocol violation the instant it happens
- Why wrapping a
randomize()call in an assertion turns an easily-missed warning into an unmissable failure
Syntax
// Simple assert statement
assert();
// Assert statement with statements to be executed for pass/fail conditions
assert() begin
// If condition is true, execute these statements
end else begin
// If condition is false, execute these statements
end
// Optionally give name for the assertion
[assert_name] : assert();
Immediate Assertion in Design
Here's an example where the design has an immediate assertion checking that a push request to the FIFO doesn't come at a time when the FIFO is already full. If the expression within the assert statement evaluates to true, the first begin end block executes; if false, the else part executes. This is much like the if construct, with the difference that it isn't required for the user to place a display statement to flag the error — the assertion mechanism does that automatically:
module my_des (my_if _if);
always @ (posedge _if.clk) begin
if (_if.push) begin
// Immediate assertion and ensures that
// fifo is not full when push is 1
a_push: assert (!_if.full) begin
$display("[PASS] push when fifo not full");
end else begin
$display("[FAIL] push when fifo full !");
end
end
if (_if.pop) begin
// Immediate assertion to ensure that fifo is not
// empty when pop is 1
a_pop: assert (!_if.empty) begin
$display ("[PASS] pop when fifo not empty");
end else begin
$display ("[FAIL] pop when fifo empty !");
end
end
end
endmodule
Without immediate assertions like this, achieving the same check with concurrent assertions would mean re-expressing the "only when push is asserted" condition from scratch — the immediate assertion instead reuses the surrounding if (_if.push) that already got you to exactly the right point in the procedural flow, at no extra cost.
interface my_if(input bit clk);
logic pop;
logic push;
logic empty;
logic full;
endinterface
module tb;
bit clk;
always #10 clk <= ~clk;
my_if _if (clk);
my_des u0 (.*);
initial begin
for (int i = 0; i < 5; i++) begin
_if.push <= $random;
_if.pop <= $random;
_if.empty <= $random;
_if.full <= $random;
$strobe("[%0t] push=%0b full=%0b pop=%0b empty=%0b",
$time, _if.push, _if.full, _if.pop, _if.empty);
@(posedge clk);
end
#10 $finish;
end
endmodule
Notice how the time and line of assertion failure is displayed as E:
ncsim> run [0] push=0 full=1 pop=1 empty=1 ncsim: *E,ASRTST (./design.sv,13): (time 10 NS) Assertion tb.u0.a_pop has failed [FAIL] pop when fifo empty ! [10] push=1 full=0 pop=1 empty=1 [PASS] push when fifo not full ncsim: *E,ASRTST (./design.sv,13): (time 30 NS) Assertion tb.u0.a_pop has failed [FAIL] pop when fifo empty ! [30] push=1 full=1 pop=1 empty=0 ncsim: *E,ASRTST (./design.sv,5): (time 50 NS) Assertion tb.u0.a_push has failed [FAIL] push when fifo full ! [PASS] pop when fifo not empty [50] push=1 full=0 pop=0 empty=1 [PASS] push when fifo not full [70] push=1 full=1 pop=0 empty=1 ncsim: *E,ASRTST (./design.sv,5): (time 90 NS) Assertion tb.u0.a_push has failed [FAIL] push when fifo full ! Simulation complete via $finish(1) at time 100 NS + 0 ./testbench.sv:25 #10 $finish; ncsim> exit
At the very first posedge (10ns), the values driven at 0ns are what get checked: pop was 1 and empty was 1, so a_pop fails immediately — push was 0 at that same moment, so a_push's if (push) never even evaluates that cycle.
Quick Check
Q: At 50ns, a_push fails while a_pop passes in the same clock cycle. Why can these two outcomes differ within one edge?
Show Answer
They're two completely independent checks against different signal pairs — push against full, and pop against empty. Nothing links their outcomes together; the random stimulus at that specific moment simply happened to violate one condition while satisfying the other.
Immediate Assertion in Testbench
Assume a class called Packet is created and randomized, but this example has a constraint error, and randomization will fail. Without an assertion, the failure is only displayed as a warning message — if the user isn't careful, the test may display incorrect behavior and may even appear to pass:
class Packet;
rand bit [7:0] addr;
constraint c_addr { addr > 5; addr < 3; }
endclass
module tb;
initial begin
Packet m_pkt = new();
m_pkt.randomize();
end
endmodule
Output
ncsim> run
m_pkt.randomize();
|
ncsim: *W,SVRNDF (./testbench.sv,11|18): The randomize method call failed. The unique id of the failed randomize call is 0.
Observed simulation time : 0 FS + 0
ncsim: *W,RNDOCS: These constraints contribute to the set of conflicting constraints:
constraint c_addr { addr > 5; addr < 3; } (./testbench.sv,4)
ncsim: *W,RNDOCS: These variables contribute to the set of conflicting constraints:
rand variables:
addr [./testbench.sv, 2]
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit
Notice that c_addr itself asks for an impossible value — addr can never be both greater than 5 and less than 3 at once — so this call fails every single time, yet the only sign of it is a warning easy to scroll straight past.
Instead, an immediate assertion can be placed directly on the randomization method call, ensuring the return value is always 1, indicating success. If the assertion fails, it forces the user to actually look at the failure, reducing debug effort:
class Packet;
rand bit [7:0] addr;
constraint c_addr { addr > 5; addr < 3; }
endclass
module tb;
initial begin
Packet m_pkt = new();
assert(m_pkt.randomize());
end
endmodule
The simulator assigns a generated name for the assertion if the user hasn't specified one:
ncsim> run
assert(m_pkt.randomize());
|
ncsim: *W,SVRNDF (./testbench.sv,11|25): The randomize method call failed. The unique id of the failed randomize call is 0.
Observed simulation time : 0 FS + 0
ncsim: *W,RNDOCS: These constraints contribute to the set of conflicting constraints:
constraint c_addr { addr > 5; addr < 3; } (./testbench.sv,4)
ncsim: *W,RNDOCS: These variables contribute to the set of conflicting constraints:
rand variables:
addr [./testbench.sv, 2]
ncsim: *E,ASRTST (./testbench.sv,11): (time 0 FS) Assertion tb.unmblk1.__assert_1 has failed
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit
The exact same underlying failure now produces a hard ASRTST assertion failure alongside the warning, instead of just the easily-missed warning on its own. In a similar way, assert can be used with any expression that evaluates to true or false within a procedural block.
Quick Check
Q: If the constraint were fixed to something satisfiable, like addr > 5; addr < 10;, would assert(m_pkt.randomize()); ever fail?
Show Answer
No. With a satisfiable constraint, randomize() succeeds and returns 1 every time, which the assertion accepts — the assertion only exists to catch a failure, not to introduce one on its own.
By default, a failed immediate assertion doesn't stop simulation on its own — both examples above continue running normally after each reported failure. This is because an assertion with no explicit severity system task defaults to $error severity, which reports but doesn't halt. Terminating on failure requires explicitly using $fatal in the assertion's else clause instead.
Where Is This Used?
Wrapping every randomize() call in an assertion (often through a team-defined shorthand macro) is considered mandatory practice in most production testbenches. An unnoticed randomization failure can silently leave a variable at its default or previous value instead of the intended randomized stimulus — potentially masking a real bug or producing a false pass with no obvious sign anything went wrong.
Common Beginner Mistakes
Mistake #1: Assuming immediate assertions work for formal verification
Immediate assertions are simulation-only. Why? They follow simulation event semantics tied to procedural execution order, which formal tools have no equivalent concept of — formal verification needs concurrent assertions instead.
Mistake #2: Trying to use an immediate assertion outside a procedural block
An immediate assertion has to be inside an always, initial, or similar procedural block. Why? It's evaluated exactly like an if statement at the point it's reached, so it needs the same procedural context an if statement would.
Mistake #3: Forgetting to wrap a randomize() call in an assertion
As the Packet example shows, an unwrapped randomize() failure only produces a warning that's easy to overlook. Why? A test can silently continue running with unintended data after a failed randomization, producing confusing or outright wrong results with no obvious explanation.
Mistake #4: Assuming a failed assertion always stops simulation
As the note above explains, the default severity only reports the failure — it doesn't halt anything. Why? If you specifically need simulation to stop the moment a check fails, that requires an explicit $fatal call in the assertion's else clause, not just the bare assertion itself.
Practice Exercise
Challenge: Check Mutual Exclusion
Write an immediate assertion inside an always @(posedge clk) block that checks wr_en and rd_en are never both high at the same time. Name the assertion a_mutex, and print "[FAIL] wr_en and rd_en both high!" on failure.
Hint
The condition you want to hold true is that at most one of the two signals is high — not both. Express that as a single boolean expression inside the assert(...).
Solution
always @ (posedge clk) begin
a_mutex: assert (!(wr_en && rd_en)) begin
// Passed - nothing to do
end else begin
$display ("[FAIL] wr_en and rd_en both high!");
end
end
!(wr_en && rd_en) is true whenever at most one of the two signals is high, and false only when both are high at once — exactly the mutual exclusion condition being checked.
Summary
In this tutorial, you learned:
- An immediate assertion is evaluated like an
ifstatement, at the exact point it's reached in a procedural block - Immediate assertions are simulation-only, and can be used in both design and testbench code
- Placing an immediate assertion inside existing control flow (like
if (push)) reuses that condition instead of re-expressing it separately, unlike a concurrent assertion - Wrapping a
randomize()call in an assertion turns an easily-missed warning into an unmissable failure - A failed assertion doesn't stop simulation by default — that requires explicitly using
$fatalseverity instead