SystemVerilog assertions in a UVM testbench check pin-level protocol rules directly in the interface, while procedural checks in the monitor handle rules that need whole transactions. A UVM testbench has places for both, and a few conventions make them work together: assertions live in the interface (or are attached to it with bind), failures are reported through UVM's reporting so they show up in the same log as everything else, and a configuration bit can switch the checks off when they shouldn't fire.
UVM does not replace assertions. A scoreboard checks what the DUT did with the data. An assertion checks how the signals behaved cycle by cycle, such as "ready must rise within 8 cycles of valid", which no transaction-level checker can see.
8 min read | Intermediate
What You'll Learn
- Decide whether a check belongs in an interface assertion or in the monitor
- Write protocol assertions in an interface, and use
assertorassumecorrectly - Report assertion failures through UVM
- Turn checks on and off from the testbench
Where a Check Belongs
| Check | Place | Why |
|---|---|---|
| Signal timing: "grant follows request within 4 cycles", "no X on addr when valid" | Assertion in the interface | Concurrent assertions describe cycle-by-cycle behaviour concisely |
| Transaction rules: "size is 1, 2, 4 or 8", "data length matches the size field" | Immediate check in the monitor | They need the collected transaction |
| Data correctness against an expected value | Scoreboard | Needs a model, or another stream to compare with |
The User's Guide states one restriction: concurrent assertions are not allowed in classes. Class-based checks in a monitor are procedural code or immediate assertions, and the concurrent ones go in the interface, a module or a checker. Its tip for monitor checks is to use an immediate assertion when the check is a few lines, and a function when it is long, because a complicated check is also complicated to debug.
Assertions in the Interface
The interface is the natural home for protocol assertions, because it already has all the signals and the clock. Assertions placed there are written once, and travel with the interface wherever it is used, including at integration level where the agent may be passive.
interface bus_if (input bit clk, input bit rstn);
logic valid, ready, write;
logic [31:0] addr, data;
// Protocol rules, checked on every clock while out of reset
property p_ready_in_time;
@(posedge clk) disable iff (!rstn)
valid && !ready |-> ##[1:8] ready;
endproperty
property p_stable_until_ready;
@(posedge clk) disable iff (!rstn)
valid && !ready |=> valid && $stable(addr) && $stable(write);
endproperty
a_ready_in_time: assert property (p_ready_in_time)
else uvm_pkg::uvm_report_error("BUS_IF", "ready not seen within 8 cycles of valid");
a_stable_until_ready: assert property (p_stable_until_ready)
else uvm_pkg::uvm_report_error("BUS_IF", "addr or write changed before ready");
c_back_to_back: cover property (@(posedge clk) disable iff (!rstn) valid && ready ##1 valid && ready);
endinterface
The else branch is the action block, run when the assertion fails. Calling uvm_report_error() from it, the package-level reporting function, makes the failure appear in the UVM log with a severity, an ID and a time, and count towards the test's error total. Without that, a failed assertion would only be reported by the simulator, outside UVM's counts and verbosity controls. A failure that is not counted as a UVM error can leave a test looking clean when the simulator logged violations.
assert, assume and cover
The same property can play three roles, chosen by the directive in front of it:
assert: the property describes behaviour of the DUT, and a failure is a DUT bug.assume: the property describes behaviour of the environment, the stimulus the DUT may rely on. It is mostly relevant to formal tools, which treat it as a constraint.cover: counts how often a behaviour happens, which tells you whether the stimulus reached it.
For a bus whose master is your testbench, rules about what the testbench is allowed to do are assume, and rules about what the DUT slave does are assert. The wrong directive misleads: asserting a rule about your own driver reports a testbench bug as a DUT failure.
Attaching Assertions with bind
Not every rule involves pins of an existing interface. To check something inside the DUT, such as a FIFO that must never overflow, put the assertions in a separate checker module and attach it to the DUT's module with bind, without editing the RTL:
module fifo_checker (input clk, input rstn, input push, input full);
a_no_overflow: assert property (@(posedge clk) disable iff (!rstn) !(push && full))
else uvm_pkg::uvm_report_error("FIFO_CHK", "push while full");
endmodule
// In the testbench: attach to every instance of the DUT's fifo module
bind sync_fifo fifo_checker u_chk (.clk(clk), .rstn(rstn), .push(wr_en), .full(full));
The checker sees the DUT's internal signals through the bind, and the design source stays untouched. The same technique is how a testbench taps internal signals for monitoring.
Turning Checks On and Off
Checks must sometimes be disabled: during reset, while a test deliberately violates the protocol, or at integration level where an inner interface is no longer driven by the testbench. The User's Guide suggests a configuration bit for the monitor's procedural checks, set through uvm_config_db:
// In the monitor
bit checks_enable = 1;
// build_phase: void'(uvm_config_db #(bit)::get(this, "", "checks_enable", checks_enable));
virtual task run_phase(uvm_phase phase);
forever begin
collect_transaction();
if (checks_enable) perform_transfer_checks(); // procedural checks only when enabled
ap.write(trans);
end
endtask
// In a test that injects protocol errors on purpose
uvm_config_db #(bit)::set(this, "m_env.m_agent.m_monitor", "checks_enable", 0);
Assertions in an interface or checker are not part of the class hierarchy, so the configuration database does not reach them. SystemVerilog's own assertion control tasks do. $assertoff, $asserton and $assertkill turn assertions off, on, or abort them, for a scope or for individual assertions. The test can call them at the right moment:
// Before deliberately breaking the protocol
$assertoff(0, tb.dut_if); // levels 0 = this scope and everything below it
// ... inject the error ...
$asserton(0, tb.dut_if);
A reset also turns assertions off by design, through disable iff (!rstn) in each property, so there is no need to disable them by hand around reset.
Quick Check
Q: A negative test deliberately sends a malformed packet, the interface assertion fires and the test counts an error. How can the test still pass?
Show Answer
By turning the assertion off around the injected error with $assertoff, or by demoting the expected message with +uvm_set_severity or a report catcher in that test. The first option stops the check running, the second lets it run but keeps it out of the error count.
Common Mistakes
Mistake #1: Concurrent assertions in a class
Concurrent assertions are not allowed in classes. Put them in the interface, a module or a checker, and use immediate assertions or procedural checks inside the monitor.
Mistake #2: Failures that UVM doesn't count
An assertion with no action block only reports through the simulator. Add an action block that calls uvm_report_error() so that the failure counts towards the UVM result.
Mistake #3: assert for rules about the testbench
Use assume for rules that describe the environment's behaviour, and assert only for the DUT. Mixing them up blames the wrong side.
Mistake #4: Forgetting disable iff
Without disable iff (!rstn), properties evaluate during reset, when signals are undefined or unstable, and report false failures at time zero.
Summary
In this article, you learned:
- Cycle-level protocol rules belong in interface assertions; transaction-level rules belong in monitor code; data checks in the scoreboard
- Concurrent assertions can't be in classes, so they live in interfaces, modules or checkers, attached with
bindwhen needed - An action block calling
uvm_report_error()makes assertion failures part of the UVM log and error count assertchecks the DUT,assumedescribes the environment andcovermeasures what was reached- A
checks_enablebit controls monitor checks, and$assertoffand$assertoncontrol assertions