Immediate Assertions
Section 9.2 built a class-based testbench. Section 9.3 is about checks that do not live in the testbench at all.
The Section 6.4 article on this topic was called “Immediate Assertions and $error Tasks”, and you have been writing the if (condition) $error(...) pattern since. This article introduces the dedicated assert construct, which does the same job with two improvements: cleaner syntax, and, the more important change, the ability to live inside the design RTL rather than only in a testbench.
The basic syntax
assert (result == expected)
else $error("Mismatch: result=%h expected=%h", result, expected);
Functionally this is close to if (!(result == expected)) $error(...). What differs is that there is no if and no negated condition. The intent, “this should be true, and if it is not, do this”, is visible in the syntax rather than expressed through an inverted if that is easy to misread (does if (!(a == b)) check that a equals b, or that it does not?).
Assertions inside the RTL
Section 6.4’s checks lived in the testbench, checking the DUT’s outputs from outside. An assertion can live inside the design, checking an internal invariant every time it is evaluated, regardless of whether any particular testbench thought to check that condition:
module fifo_internal_check #(parameter DEPTH = 16) (
input logic clk, rst_n,
// ... FIFO ports, per Section 7.4 ...
);
// ... FIFO implementation from Section 7.4 ...
// An internal invariant: the write pointer should NEVER exceed
// twice the depth (accounting for the extra wrap bit from Section 7.4)
always_ff @(posedge clk) begin
assert (wr_ptr <= (2 * DEPTH - 1))
else $error("wr_ptr out of legal range: %0d", wr_ptr);
end
endmodule
This is not checking the FIFO’s external behaviour the way a Module 6 testbench would. It is checking an internal implementation invariant, something that should always be true about the module’s own state whatever an external test drives. If a later change to the FIFO breaks the pointer wrap logic, this assertion fires the moment the invariant is violated, in any test that exercises the broken path, including tests never written with pointer wraparound in mind, because the check travels with the design rather than depending on a testbench author remembering to add it.
Severity levels
SystemVerilog attaches the same severity gradation from Section 6.4 to the assertion form:
assert (queue_depth <= MAX_DEPTH)
else $fatal(1, "Queue overflow: cannot continue simulation meaningfully");
assert (latency_cycles <= EXPECTED_MAX_LATENCY)
else $warning("Latency higher than expected: %0d cycles", latency_cycles);
assert (result == golden_value)
else $error("Functional mismatch");
The meanings are as in Section 6.4: $fatal for a condition so broken that continuing would only produce a cascade of meaningless noise, $error for a specific correctness failure that should be logged and counted but does not stop the rest of the test being meaningful, $warning for something worth flagging that is not necessarily a failure.
A check in RTL versus a check in the testbench
A testbench check verifies external, black-box behaviour: does this module produce the right output for this input. An RTL-embedded assertion verifies an internal, white-box invariant: does this module’s state ever violate a rule that should always hold, whatever the external test. Both have their place and neither replaces the other. A design with embedded assertions checking its internals, verified against a testbench checking its externals, catches more classes of bug than either alone: the assertion catches invariant violations an external test might never observe from outside, and the testbench catches functional errors an internal invariant was never meant to look for.
Embedded assertions are one of the most cost-effective verification techniques in practice, because a well-placed one keeps paying off across every future test, including tests by engineers who never saw the assertion and regressions run years later from a different environment. Many style guides require assertions on specific known-dangerous conditions (illegal FSM states, Section 5.6; pointer range violations, Section 7.4) as part of any new module.
Practice Exercise
A one-hot FSM (Section 5.6) should always have exactly one state bit set. Write an immediate assertion checking this invariant, using the $onehot system function.
Solution
always_ff @(posedge clk) begin
assert ($onehot(state))
else $error("state register is not one-hot: %b", state);
end
$onehot() returns true only when exactly one bit of its argument is set. This single assertion continuously enforces the one-hot invariant from Section 5.6, catching a violation, whether from an RTL bug, an illegal-state glitch or a bit flip in a safety-critical context, the moment it occurs in any simulation, without a testbench author having to anticipate the check.
An immediate assertion is functionally close to the if/$error pattern from Section 6.4, with clearer syntax, but the real shift is where it can live: inside the design, continuously checking an internal invariant across every test that ever exercises it. In the next article we look at concurrent assertions, which check not an instant but a relationship that spans several clock cycles.