01Why UVM Exists
The smallest UVM testbench is thirty lines. All it does is print one line:
program automatic test;
import uvm_pkg::*;
`include "uvm_macros.svh" // without this line, `uvm_info / `uvm_component_utils do not exist
class hello_world extends uvm_test;
`uvm_component_utils(hello_world) // register with the factory
function new(string name = "hello_world", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this); // hand up: I am not finished, do not end the simulation
#50;
`uvm_info("TEST", "Hello World!", UVM_MEDIUM)
phase.drop_objection(this); // hand down: you may finish now
endtask
endclass
initial begin
run_test("hello_world"); // start UVM and name the test to run
end
endprogram
In the same folder, the testbench that tests one adder runs to four hundred lines across eight classes.
Those four hundred lines are not there for the addition — the addition is a single line, expected = in1 + in2;.
What they buy you is these four things:
1. Separation of concerns
Generating stimulus, driving pins, watching waveforms, judging right from wrong and counting coverage are split into five independent classes. Change one and the other four stay untouched.
2. Randomisation
Stimulus comes from rand, not hand-written. A different seed gives you a completely new set of vectors with no code change.
3. Automatic checking
The scoreboard computes the expected value itself and compares it against the DUT, so you are not staring at waveforms. When it disagrees it raises a UVM_ERROR.
4. Something you can measure
Coverage tells you whether you have tested enough. You can write "run until coverage reaches 50%" instead of "a hundred runs is probably enough".
In one sentence: UVM does not write your tests. It breaks the test environment into parts you can replace one at a time, and fixes the conventions between them: how they talk (TLM), when they start (phases), how they are built (factory) and how they receive settings (config_db). The next five chapters are about those four conventions.
02Interface: the boundary between class and module
This is where beginners get confused most easily. A UVM testbench actually contains two entirely separate worlds, with different lifetimes and different memory models.
| The module world (static) | The class world (dynamic) | |
|---|---|---|
| Members | module top, adder dut, adder_intf intf, clock and reset generators |
test, env_c, agent_c, driver_c, trans_c … |
| When they exist | Fixed at compile time, present for the whole simulation, fixed in number | Created at run time with create(). You can make a thousand of them, or throw one away mid-run |
| Can they touch signals? | Yes, wired directly | No — a class has no such thing as a pin |
| Can they be randomised? | No | Yes, rand + constraint |
For the class world to drive the DUT there is exactly one channel: the interface.
interface itself lives in the module world (instantiated once inside top),
and what the class side holds is a reference to it, of type virtual interface.
interface adder_intf (input logic clk, input logic rst_n);
logic [6:0] in1;
logic [6:0] in2;
logic [7:0] out;
modport dut (input in1, input in2, output out); // DUT: takes inputs, produces an output
modport drv (output in1, output in2, input out, // driver: may only write in1/in2
input clk, input rst_n);
modport mon (input in1, input in2, input out, // monitor: observation only, all inputs
input clk, input rst_n);
endinterface
modport is a table of directional permissions. The same wires, seen three different ways:
modport dut:in1/in2are inputs andoutis an output — from the DUT's point of view.modport drv:in1/in2becomes an output — the driver is the one doing the writing.modport mon: everything is an input — deliberately so. A monitor may only look, and the compiler blocks any code that tries to write a signal, so a monitor cannot accidentally contaminate the test.
Where beginners get stuck most often: the type the driver declares is not virtual adder_intf,
but virtual adder_intf.drv (with the modport). That type must match the one used in the
uvm_config_db#(...) call exactly,
or get() returns false and you get a UVM_FATAL. Chapter 8 covers this.
03The Architecture at a Glance
UVM components form a tree. The root is uvm_test_top (run_test("test") builds it for you),
and each level below creates its children inside its own build_phase.
driver and monitor each hold a virtual interface so they can reach across.
What each level is responsible for
| Level | Responsibility | Replaceable? |
|---|---|---|
test | Decides what this run does: how many transactions, the coverage goal, whether to override anything | One test per scenario; the most frequently swapped |
env | Assembles agent, scoreboard and coverage, and wires them together | Reused whole; only touched when the DUT changes |
agent | A complete agent for one protocol interface: generate, drive, observe | One agent per protocol (an APB agent, an AXI agent, and so on) |
sequencer | Arbitration and queueing between sequence and driver | Almost never written by hand; use uvm_sequencer#(T) |
driver | Translates a transaction into signal timing (this is where the protocol details live) | Must change with the protocol |
monitor | Turns signal timing back into a transaction and broadcasts it | Must change with the protocol |
scoreboard | Computes the expected value, compares it against the DUT's actual output, tallies PASS/FAIL | Must change when the DUT's function changes (the reference model) |
coverage | Samples and tallies which scenarios have been exercised | Changes when the verification goal changes |
Active vs. passive:uvm_agent has a built-in is_active field. When active it builds driver + sequencer + monitor; when passive it builds only the monitor (pure observation, no stimulus).
You do this with a check in the agent's build and connect phases:
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (is_active == UVM_ACTIVE) begin // only an active agent needs to drive stimulus
m_drv = driver_c::type_id::create("m_drv", this);
m_sqr = uvm_sequencer#(trans_c)::type_id::create("m_sqr", this);
end
m_mon = monitor_c::type_id::create("m_mon", this); // the monitor is always built
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (is_active == UVM_ACTIVE)
m_drv.seq_item_port.connect(m_sqr.seq_item_export); // do not connect what was never built, or you get a null handle
endfunction
04Datapath: following one transaction
This is the heart of these notes. What follows tracks one trans_c,
from being randomised into existence, driven onto the pins, computed by the DUT, observed on the way back, to being compared — eight stations in all.
Station by station: where the data is and what type it holds
| Station | What happens | What the data is at this moment |
|---|---|---|
| ① sequence | create() creates a trans_c, randomize() fills the in1/in2 |
trans_c object; only the input fields are meaningful, out is still meaningless |
| ② sequencer | finish_item() hands the object to the sequencer; if the driver is still busy it blocks here |
A handle to the same object is passed along (nothing is copied) |
| ③ driver | seq_item_port.get_next_item(m_trans) receives the handle |
The same object; the driver can now read its fields |
| ④ crosses the boundary | @(posedge clk), then vif.in1 <= m_trans.in1; |
object -> signal. From here on the data is a logic level |
| ⑤ DUT | always_ff writes, on the next posedge, in1+in2 into out |
Pure signal, with a one-cycle delay |
| ⑥ monitor | Remember this cycle's in1/in2 and read out only on the next cycle, so the two halves form one transaction |
signal -> object.create() creates a brand new trans_c |
| ⑦ broadcast | m_mon_port.write(m_trans) sends it to every subscriber at once |
A handle to the same object goes to N receivers |
| ⑧ checks | the scoreboard computes expected = in1+in2 and compares; coverage samples |
Once read, nobody holds the object and it goes to the garbage collector |
The driver: one half of the conversion
virtual task run_phase(uvm_phase phase);
trans_c m_trans;
adder_vif.in1 <= '0; // hold the signals at zero during reset
adder_vif.in2 <= '0;
wait (adder_vif.rst_n === 1'b1); // start work only once reset is released
forever begin
seq_item_port.get_next_item(m_trans); // <- blocking: waits here when there is nothing
@(posedge adder_vif.clk); // line up with the clock
adder_vif.in1 <= m_trans.in1; // non-blocking assignment, to avoid a race
adder_vif.in2 <= m_trans.in2;
`uvm_info("DRV", $sformatf("Drive in1=%0d, in2=%0d",
m_trans.in1, m_trans.in2), UVM_LOW)
seq_item_port.item_done(); // <- tell the sequencer this one is finished
end
endtask
get_next_item() and item_done() must come in pairs.
Leave out item_done() and the sequencer believes the driver never finished, so the second transaction is never issued and the simulation looks hung.
The monitor: the other half of the conversion, and where timing is easiest to get wrong
virtual task run_phase(uvm_phase phase);
trans_c m_trans;
logic [6:0] in1_q, in2_q; // hold this cycle's inputs
int idx = 0;
wait (adder_vif.rst_n === 1'b1);
@(posedge adder_vif.clk);
#1; // * let the NBA region settle before reading, dodging the sampling race
forever begin
in1_q = adder_vif.in1; // cycle N: remember the inputs
in2_q = adder_vif.in2;
@(posedge adder_vif.clk);
#1;
m_trans = trans_c::type_id::create($sformatf("mon_trans_%0d", idx++), this);
m_trans.in1 = in1_q; // cycle N's inputs
m_trans.in2 = in2_q;
m_trans.out = adder_vif.out; // cycle N+1's output <- this is the matching half
m_mon_port.write(m_trans); // broadcast
end
endtask
Why the #1?
The DUT uses always_ff with the non-blocking assignment <= to update out, and that update happens in the same time step's
NBA region. If the monitor wakes at @(posedge clk)
and reads immediately, it may catch the value from before the update, and the execution order is not guaranteed to repeat (a race).
Adding a #1 (or switching to a clocking block) guarantees a settled value.
Why hold in1_q/in2_q?
Because the DUT has one cycle of latency: in1/in2 is on cycle N and the matching out is on cycle N+1.
Sample both on the same cycle and you pair this transaction's input with the previous transaction's output, turning the whole scoreboard red.
05The Data Types
The two families: uvm_object vs. uvm_component
Every UVM class grows from uvm_object, but it forks early into two branches that behave nothing alike:
uvm_object familyData | uvm_component familyStructure | |
|---|---|---|
| Typified by | uvm_sequence_item (a transaction)uvm_sequence |
uvm_driver, uvm_monitor, uvm_scoreboard, uvm_subscriber, uvm_agent, uvm_env, uvm_test, uvm_sequencer |
| Does it have a place? | No. Like a letter: passed around, copied, thrown away | Yes. It has a fixed path in the component tree, such as uvm_test_top.m_env.m_agt.m_drv |
| Lifetime | Short. One per transaction, recycled after use | Long. Once build_phase has built it, it exists for the whole simulation |
| Constructor | function new(string name = "");One argument only |
function new(string name, uvm_component parent);One extra: parent |
| Registration macro | `uvm_object_utils(T)or `uvm_object_utils_begin/end |
`uvm_component_utils(T) |
| Does it have phases? | No | Yes: build / connect / run / report / final ... |
| Can it be randomised? | Yes (rand + constraint) | Never used that way |
What using the wrong macro looks like: put `uvm_component_utils on a transaction,
or `uvm_object_utils on a component, and compilation blows up on a constructor signature mismatch
(the macro expands to that family's create() implementation). When you see a pile of odd
new() errors, check that these two macros are paired correctly first.
The transaction: a container for data
class trans_c extends uvm_sequence_item;
rand logic [6:0] in1; // rand: randomize() will fill this in
rand logic [6:0] in2;
logic [7:0] out; // no rand: this comes from the DUT, we do not generate it
function new(string name = "");
super.new(name);
endfunction
`uvm_object_utils_begin(trans_c) // only the begin/end form accepts field macros
`uvm_field_int(in1, UVM_DEFAULT)
`uvm_field_int(in2, UVM_DEFAULT)
`uvm_field_int(out, UVM_DEFAULT)
`uvm_object_utils_end
endclass
Three design decisions worth noting:
outhas norand: the output is a result we observe, not stimulus we decide. Addingrandwould let randomize scribble over it, overwriting whatever the monitor captured.- the width
[6:0] + [6:0] → [7:0]: two 7-bit values reach 127+127 = 254, which needs 8 bits. The extra bit is deliberate — it prevents overflow so the scoreboard'sexpected = in1 + in2agrees with the DUT. Giveoutonly 7 bits and both sides truncate: the test passes, but you are no longer testing the carry. logicrather thanbit:logicis four-valued (0/1/X/Z), whilebitis two-valued (0/1). Onlylogiclets you see the bug where a wire is never driven at all (X); withbit, an undriven wire reads as 0 and the bug hides.
`uvm_field_int gives you what
Once the fields are registered, UVM generates these methods for you:
| Method | Purpose |
|---|---|
print() | Prints the whole transaction as a table (very useful when debugging) |
copy() / clone() | Deep-copies a transaction (essential before queueing one for comparison) |
compare() | Compares two, field by field |
sprint() / convert2string() | Turns it into a string for the log |
pack() / unpack() | Flattens it into a bit stream |
Parameterised types: what goes inside the angle brackets
| Declaration | What the parameter means |
|---|---|
uvm_sequence #(trans_c) | Which item this sequence produces |
uvm_sequencer #(trans_c) | Which item this sequencer carries |
uvm_driver #(trans_c) | Which item this driver receives (it sets the type of seq_item_port type) |
uvm_analysis_port #(trans_c) | Which item this port broadcasts |
uvm_analysis_imp #(trans_c, scoreboard_c) | Two parameters: which item it receives, plus who implements write() |
uvm_subscriber #(trans_c) | Has a built-in analysis_export, so you only override write() |
uvm_tlm_analysis_fifo #(trans_c) | A buffering receiver; read from it with get() |
virtual adder_intf.drv | A reference to the interface, with the modport |
new() vs type_id::create()
Both create objects, but they mean very different things:
obj = new("name")
Calls the constructor directly. What comes out is always the class you wrote; the factory never sees it.
Use it only for things that need no override, such as TLM ports, exports and FIFOs:
m_mon_port = new("m_mon_port", this);
obj = T::type_id::create("name", this)
Creates through the factory. The factory checks for an override first and, if there is one, substitutes a different class.
Every transaction, sequence and component should use this. It is the prerequisite for changing behaviour without changing the structure (see chapter 9).
The difference in arguments:create() for a component takes this as the parent
(create("m_drv", this)); for an object it usually does not
(create("m_trans_0")) — an object has no place in the tree.
SystemVerilog: a few things you must keep straight
| Declaration | Meaning | When it is used in UVM |
|---|---|---|
= (blocking) | Takes effect immediately | Arithmetic inside a class; storing a monitor's captured values into a transaction |
<= (non-blocking) | Queued to the NBA region before taking effect | Always use this when a driver drives a signal, so it does not fight the DUT's always_ff |
== | Logical comparison; returns X when it meets X or Z | Avoid where you can |
=== | Four-valued identity; X equals X, and the result is always 0 or 1 | Always use this for scoreboard comparisons and for waiting on reset |
wait (expr) | Proceeds once the condition holds (and does not wait if it already does) | wait (vif.rst_n === 1'b1) |
@(posedge clk) | Always waits for the next rising edge | Lining up with the clock |
function | Cannot consume time | build/connect/report/write() are all functions |
task | May consume time | run_phase, main_phase, and a sequence's body() |
06Phases: what runs first, what runs after
UVM lays out a fixed order for you. You never call these yourself; you just override the part you want to take a hand in, and UVM calls it at the right moment.
`uvm_info marker to each phase and the log prints in exactly this order.Two rules worth memorising
Rule 1: a function phase cannot delay
build_phase, connect_phase, report_phase... are all
function void. Put a #10 or @(posedge clk)
inside one and you get Illegal time-consuming statement.
Anything that waits belongs in run_phase / main_phase (a task).
Rule 2: build runs top-down, connect runs bottom-up
build_phase is top-down: build the test, the test builds the env, the env builds the agent...
because a parent must exist before its child.
connect_phase goes the other way, bottom-up:
every child must already exist before anything can be connected.
Objections: deciding when the simulation ends
UVM has no idea how long your test should run. An objection is simply a counter: while anyone still has a hand raised against finishing, the run phase continues; the moment it reaches zero, UVM packs up.
virtual task main_phase(uvm_phase phase);
sequence_c m_seq;
int iter = 0;
phase.raise_objection(this); // <- hand up: do not finish yet
// keep sending sequences until coverage hits the target or the iteration cap
while (m_env.m_cov.covgrp.get_coverage() < cov_target && iter < max_iter) begin
m_seq = sequence_c::type_id::create($sformatf("seq_%0d", iter), this);
m_seq.start(m_env.m_agt.m_sqr); // <- attach the sequence to the sequencer and run it
iter++;
end
phase.phase_done.set_drain_time(this, 20ns); // 20 ns of slack for whatever is still in the pipeline
phase.drop_objection(this); // <- hand down: we can pack up
endtask
Forget raise_objection looks like this: the simulation ends at 0 ns and the log is nearly empty — just UVM's header and the report summary. It looks like a broken build, but it is simply that nobody objected, so UVM packed up immediately.
set_drain_time is for: after the last stimulus reaches the DUT, the DUT still needs a few cycles to produce a result. Stop the instant the objection drops and the last few out never arrive: the monitor captures too few and the scoreboard compares too few. Leave a drain time so the pipeline empties.
run_phase and main_phase — what is the difference?
run_phase is the master switch; UVM also divides it into twelve sub-phases:
pre_reset → reset → post_reset → pre_configure → configure → post_configure
→ pre_main → main → post_main → pre_shutdown → shutdown → post_shutdown,
which run run_phase in parallel with it. The usual convention: things that run continuously, like the driver and monitor, go in run_phase, while a test's flow with a definite beginning and end goes in main_phase.
07TLM: how components connect
UVM components never call each other's methods directly. They connect through TLM ports. The benefit is that the monitor need not know who is listening — add a coverage collector today and the monitor does not change by a single line.
uvm_analysis_port #(trans_c),
and the direction of connection is always port.connect(export/imp).connections all go in connect_phase
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
m_agt.m_mon.m_mon_port.connect(m_scr.m_scr_imp); // → scoreboard
m_agt.m_mon.m_mon_port.connect(m_cov.analysis_export); // → coverage
endfunction
uvm_analysis_port is one-to-many:
there are as many subscribers as you made connections. One call to write() runs every subscriber's
write() in turn. And because write() is a function,
it consumes no time, so the broadcast completes instantaneously.
What separates architecture A (imp) from architecture C (FIFO)
| A: analysis_imp | C: analysis_fifo | |
|---|---|---|
| Where the data comes from | One path only, the monitor; a single transaction carries both in and out | Two paths: the driver sends the inputs, the monitor sends the outputs |
| When the comparison happens | write() — compared the instant it is called | run_phase, once get() has data from both sides |
| Where it lives | virtual function void write(trans_c t) | virtual task run_phase(...) (it must be able to block) |
| Extra work required | None | the driver needs an extra uvm_analysis_port, and before item_done() it must call write() |
| When to use it | A simple DUT, where the monitor can capture in and out together | A deeply pipelined DUT, out-of-order results, or in and out separated by many cycles |
uvm_tlm_analysis_fifo #(trans_c) drv_fifo; // holds the inputs sent by the driver
uvm_tlm_analysis_fifo #(trans_c) mon_fifo; // holds the outputs sent by the monitor
virtual task run_phase(uvm_phase phase);
trans_c drv_tr, mon_tr;
logic [7:0] expected;
forever begin
drv_fifo.get(drv_tr); // blocking: waits here when there is nothing
mon_fifo.get(mon_tr); // one from each side, paired automatically
expected = reference_model(drv_tr); // expected = drv_tr.in1 + drv_tr.in2
if (mon_tr.out === expected) pass_cnt++;
else `uvm_error("SCR", $sformatf("FAIL: expected %0d, got %0d", expected, mon_tr.out))
end
endtask
Note that get() is a task, not a function.
It blocks until the FIFO has something, so it can only live inside run_phase.
That is why the FIFO scoreboard has no write() — it has been replaced by run_phase.
08config_db: handing things in from outside
The driver needs a virtual interface, but the interface lives in module top.
The driver is created by the factory, so there is no way to pass the interface in through its constructor.
uvm_config_db solves exactly this, with a global dictionary: one side calls set, the other calls get.
initial begin
uvm_config_db#(virtual adder_intf.drv)::set(null,
"uvm_test_top.m_env.m_agt.m_drv", "adder_vif", intf);
uvm_config_db#(virtual adder_intf.mon)::set(null,
"uvm_test_top.m_env.m_agt.m_mon", "adder_vif", intf);
run_test("test"); // set must always come before run_test
end
virtual adder_intf.drv adder_vif; // the type must match the set side exactly
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual adder_intf.drv)::get(this, "", "adder_vif", adder_vif))
`uvm_fatal("DRV", "Unable to get interface adder_vif (drv)")
endfunction
The four arguments, one by one
| Position | Set side | Get side | Notes |
|---|---|---|---|
| 1 | null | this |
The starting context. null = start from uvm_root (a module is not in the component tree) |
| 2 | "uvm_test_top.m_env.m_agt.m_drv" | "" |
The path relative to the context. The get side already passes this, so an empty path means "me" |
| 3 | "adder_vif" | "adder_vif" |
The field name (a string key); it must match exactly on both sides |
| 4 | intf | adder_vif |
On set it is the value to store; on get it is the variable to store into (passed by reference) |
Three ways to make get() fail, all with the same symptom: UVM_FATAL:
- Mismatched types — set uses
virtual adder_intfwhile get usesvirtual adder_intf.drv. As far asuvm_config_dbis concerned these are two entirely different dictionaries, and what goes into A never comes out of B. This is the most common one and the hardest to spot. - A wrong path — for instance
m_agttyped asm_agent, or forgetting the leadinguvm_test_top.(run_test()always names the test instance this). - A wrong key —
"adder_vif"typed as"adder_intf".
Why set twice? Because the driver and the monitor need different modports
(.drv is writable, .mon is read-only), and different types need separate entries.
This one is easy to miss.
The path accepts wildcards."*" matches any level, so
set(null, "*", "adder_vif", intf) lets every component find it —
quick, but dangerous: it hands the monitor a writable handle too.
09Factory and Override
The factory is a registry that exchanges a string for an object. It exists for exactly one reason: so you can replace one class throughout the system without touching any existing code.
Three steps
- Register —
`uvm_object_utils(T)/`uvm_component_utils(T). A class that is not registered does not exist as far as the factory is concerned. - Create with
create()— rather thannew(). That is the interception point. - Set an override — before any
create()happens.
// (1) derive a new transaction and add one constraint
class trans_in1_3_c extends trans_c;
`uvm_object_utils(trans_in1_3_c) // * must be registered, or the override does nothing
function new(string name = "");
super.new(name);
endfunction
constraint in1_eq_3 { in1 == 3; } // pin in1 to 3
endclass
// (2) set the override at the very top of the test's build_phase
virtual function void build_phase(uvm_phase phase);
set_type_override_by_type(
trans_c::get_type(), // what was going to be created
trans_in1_3_c::get_type() // what actually gets created
);
super.build_phase(phase); // <- the override must come before this line
m_env = env_c::type_id::create("m_env", this);
uvm_factory::get().print(); // (3) print the type-override table to confirm
endfunction
Once that is set, the line sequence_c inside trans_c::type_id::create(...)
has not changed by a single character, yet what comes out is a trans_in1_3_c.
Every in1 in the log becomes 3, the scoreboard still passes throughout
(nothing is broken), but the coverage on cp1 collapses to a single bin.
Two reasons an override does nothing:
- the new class has no
`uvm_object_utils— the factory has never heard of the type. - somewhere the code uses
new()rather thancreate()— that site quietly creates the old type, and you get the baffling behaviour where some transactions are swapped and others are not.
Two scopes of override:
set_type_override_by_type(A, B)— system-wide: every A becomes a B.set_inst_override_by_type(A, B, "path")— swaps only the one instance under the given path. You might swap the driver of one particular agent and leave every other alone.
10Functional coverage: have you tested enough?
The scoreboard answers "is it wrong". Coverage answers "did we ever test it". Both passing does not mean verification is done — you may simply never have sent the input that would fail.
class cov_coll_c extends uvm_subscriber #(trans_c);
`uvm_component_utils(cov_coll_c)
trans_c m_trans; // * the covergroup samples from this member variable
covergroup covgrp;
option.per_instance = 1; // tally each instance separately
cp1: coverpoint m_trans.in1 {
bins in1_min = {0}; // a single value
bins in1_max = {127};
bins in1_low[] = {[1:63]}; // [] = expand automatically into 63 bins
bins in1_high[] = {[64:126]};
}
cp2: coverpoint m_trans.in2 { /* as above */ }
cp3: coverpoint m_trans.out {
bins low = {[0:127]};
bins high = {[128:254]}; // did we ever test a case that carries
}
cp1xcp2: cross cp1, cp2; // cross: have the combinations been walked
endgroup
function new(string name = "", uvm_component parent);
super.new(name, parent);
covgrp = new(); // * the covergroup must be constructed in new()
endfunction
virtual function void write(trans_c t); // called once for every transaction the monitor broadcasts
m_trans = t; // update the member variable first
covgrp.sample(); // then sample
endfunction
endclass
The order matters.covgrp.sample() captures the m_trans
value this member variable holds at that instant. Call sample() before m_trans = t;,
and every sample is one transaction behind.
covgrp = new() belongs in the constructor. Putting it in build_phase
also works, but forget it and you have a null handle that explodes on the first sample().
Using coverage as the stopping condition
The better approach is not "run N times" but "run until coverage reaches the target":
while (m_env.m_cov.covgrp.get_coverage() < cov_target && iter < max_iter) begin
m_seq = sequence_c::type_id::create($sformatf("seq_%0d", iter), this);
m_seq.start(m_env.m_agt.m_sqr);
iter++;
end
if (iter >= max_iter)
`uvm_warning("TEST", "Hit max_iter before reaching coverage target")
| API | Returns |
|---|---|
covgrp.get_coverage() | The coverage of this type, overall (all instances combined) |
covgrp.get_inst_coverage(covered, total) | The coverage of this instance, with output arguments giving how many bins hit out of how many |
covgrp.cp1.get_coverage() | The coverage of a single coverpoint (to find which one is holding you back) |
max_iter safety net is essential:
If the constraints pin the input down too hard (fixing in1 to a single value, say),
coverage will never reach the target, and without max_iter you have an infinite loop.
Seeing that Hit max_iter warning usually means the constraints and the coverage goal contradict each other.
11What changes with a different protocol
Swap the DUT from an adder to an APB slave and the whole UVM architecture — test / env / agent / sequencer / TLM connections / phases / factory — does not change by one line. Only these four things change:
| What changes | adder | APB slave |
|---|---|---|
| Interface signals | in1 / in2 / out |
psel penable pwrite paddr pwdata prdata pready pslverr |
| Transaction fields | rand in1, in2;out |
rand pwrite, paddr, pwdata;prdata, pslverr, plus an address constraint |
| Driver timing | Drive it on one clock edge and you are done | Two phases: setup then access, and it must also wait for pready |
| Monitor sampling condition | Every cycle is one transaction | Only psel && penable && pready is the one cycle that counts as a transaction |
// setup phase: PSEL=1, PENABLE=0, present address, direction and write data
@(posedge apb_vif.clk);
apb_vif.psel <= 1'b1;
apb_vif.penable <= 1'b0;
apb_vif.pwrite <= m_trans.pwrite;
apb_vif.paddr <= m_trans.paddr;
apb_vif.pwdata <= m_trans.pwdata;
// access phase: PENABLE only rises on the next cycle
@(posedge apb_vif.clk);
apb_vif.penable <= 1'b1;
// wait states: keep waiting while PREADY=0, and do not change PADDR/PWRITE/PWDATA meanwhile
while (!apb_vif.pready)
@(posedge apb_vif.clk);
if (apb_vif.psel && apb_vif.penable && apb_vif.pready) begin
s_pwrite = apb_vif.pwrite; // (a) latch the stimulus fields first
s_paddr = apb_vif.paddr;
s_pwdata = apb_vif.pwdata;
@(posedge apb_vif.clk);
#1; // (b) let the prdata/pslverr the DUT wrote with NBA settle
m_trans = trans_c::type_id::create($sformatf("mon_trans_%0d", idx++), this);
m_trans.pwrite = s_pwrite;
m_trans.paddr = s_paddr;
m_trans.pwdata = s_pwdata;
m_trans.prdata = apb_vif.prdata; // (c) then add the DUT's response
m_trans.pslverr = apb_vif.pslverr;
end
Assertions and cover properties: a layer outside UVM
Protocol rules can live in the driver, but SVA can also pin them to the signals directly,
written in module top (outside the class world). Anyone who breaks one is caught immediately —
including your own driver, when you get it wrong.
// assertion: PENABLE may only rise after the previous cycle was already a setup phase
property apb_penable_after_psel;
@(posedge clk) disable iff (!rst_n)
$rose(intf.penable) |->
(intf.psel && $past(intf.psel && !intf.penable));
endproperty
apb_penable_after_psel_a : assert property (apb_penable_after_psel)
else `uvm_error("APB_ASSERT", "PENABLE asserted without previous APB setup phase")
// cover property: confirm the test really walked the setup -> access path
property apb_setup_to_access;
@(posedge clk) disable iff (!rst_n)
(intf.psel && !intf.penable) ##1 (intf.psel && intf.penable);
endproperty
apb_setup_to_access_cp : cover property (apb_setup_to_access);
assert and cover are two sides of one coin:
assert property says this must never happen, while
cover property says this confirms it did happen at least once.
Write only the assert and an assertion that never fires passes forever —
it looks safe, but you never tested it at all.disable iff (!rst_n)
exists to stop an invalid state during reset being reported as a violation.
12Traps for Beginners
Every entry below is something you will actually hit. Start here when you are stuck.
01You edited a .sv file but only ran make run, and nothing changed
- Symptom
- You definitely changed the code, yet the log is identical to last time.
- Cause
make runonly executes the existingsimv; it does not recompile.- Fix
make(= compile + run), ormake compileand thenmake run. Onlyseed/verbosity/TEST_NAMEare runtime arguments; changing those needs no rebuild.
02The simulation ends at 0 ns with an almost empty log
- Symptom
- No DRV / MON / SCR messages at all; it jumps straight to the report summary.
- Cause
- Forget
phase.raise_objection(this). Nobody objected, so UVM packed up at once. - Fix
- Raise at the start of a time-consuming phase and drop at the end; the two must be paired.
03Illegal time-consuming statement
- Symptom
- You get this error at compile or run time.
- Cause
- You put a
build_phase/connect_phase/write()insidefunction, which is a#10or@(posedge clk). - Fix
- Anything that waits belongs in
run_phase/main_phase(task).
04UVM_FATAL: Unable to get interface
- Symptom
- A fatal on the first run; the components were never built.
- Cause
- One of config_db's three essentials does not match: the type (
virtual adder_intf≠virtual adder_intf.drv),path, key string. - Fix
- Put the set line and the get line side by side and compare them character by character. Nine times out of ten it is the missing modport suffix.
05The scoreboard is all red, but the DUT is clearly correct
- Symptom
- Every transaction fails, and fails in a very regular way (as if off by one cycle).
- Cause
- The monitor's sampling timing is wrong. The DUT has one cycle of latency, so capturing
in1/in2andouton the same cycle pairs this transaction's input with the previous transaction's output. - Fix
- Latch the inputs first and read the output only after
@(posedge clk)(see the monitor example in chapter 4).
06Results differ from run to run, or you occasionally catch a stale value
- Symptom
- The same seed gives a different result on a different simulator version.
- Cause
- A sampling race. The DUT updates in the NBA region with
<=, while the monitor reads the moment it wakes at@(posedge clk), and the order between them is not guaranteed. - Fix
- Add a
#1before sampling, or switch to a clocking block and let itsinputskew define the sampling point explicitly.
07The driver writes signals with =
- Symptom
- The waveform looks right, but the result is intermittent.
- Cause
- A blocking assignment races the DUT's
always_fffor update order within the same time step. - Fix
- Always drive DUT inputs with the non-blocking
<=. Use=.
08Comparisons use == rather than ===
- Symptom
- When a signal is X, the comparison is neither PASS nor FAIL, and the if behaves strangely.
- Cause
==returns X when it meets X or Z (neither 0 nor 1).- Fix
- Always compare four-valued signals with
===, which is guaranteed to return 0 or 1 and compares X correctly.
09Each loop iteration produces two transactions
- Symptom
- The PASS count is double what you expect.
- Cause
- The sequence's
body()uses both`uvm_do(m_trans)and a hand-writtenstart_item/finish_item.`uvm_doalready includes create + start_item + randomize + finish_item. - Fix
- Pick one. Write it out by hand when you want a custom constraint or a log line; use
`uvm_do.
10The simulation runs for a long time and then Hit max_iter
- Symptom
- It exhausts the iteration cap and coverage is still low.
- Cause
- The constraints have boxed in the input space (
in1 < 10orin1 == 3, for instance), so the high bins can never be hit. - Fix
- This is in fact a correct and valuable signal: it tells you the constraints and the coverage goal contradict each other.
Use
cp1.get_coverage()to find which coverpoint is stuck.
11A factory override is set but has no effect
- Symptom
uvm_factory::get().print()shows the override table, yet the behaviour is unchanged.- Cause
- (a) the new class forgot
`uvm_object_utils; (b) somewhere usesnew()rather thantype_id::create(); (c) the override was set aftercreate(), too late to take effect. - Fix
- Put the override at the very top of
build_phase— the first line — beforesuper.build_phase().
12+UVM_TESTNAME=... not found
- Symptom
- UVM_FATAL: Requested test from command line not found.
- Cause
- The name given on the command line does not match the class name registered with
`uvm_component_utils(...)(a typo, or the Makefile'sTEST_NAMEwas never updated). - Fix
- The factory looks up by string, so the name must match exactly, case included.
13A message never prints
- Symptom
- You definitely wrote the
`uvm_info, but it is nowhere in the log. - Cause
- The message level is below the current verbosity threshold. A message written at
UVM_HIGHdoes not appear under the defaultUVM_MEDIUM. - Fix
make run verbosity=UVM_HIGH(a runtime argument, no rebuild needed). Put important statistics atUVM_NONE, which always prints.
14Missing `include "uvm_macros.svh"
- Symptom
- A whole run of
`uvm_info/`uvm_component_utilsreported as undefined. - Cause
import uvm_pkg::*;only brings in class library; the macros come from`include.- Fix
- Write both lines together at the top of the file, wrapped in an
`ifndef UVM_MACROS_SVHguard so it is not included twice.
15The second transaction never goes out; it looks hung
- Symptom
- You see the first DRV message and then nothing.
- Cause
- The driver's
get_next_item()has no matchingitem_done(). The sequencer believes the previous item is unfinished and will not hand over the next. - Fix
- They come in pairs, with no
returnorbreakskipping past in between.
13Quick Reference for Running It
Make target
| Command | What it does | Enough after editing a .sv? |
|---|---|---|
make clean | Deletes simv, logs, fsdb and the rest of the build products | — |
make compile | Compiles only, producing simv | Yes |
make run | Runs the existing simv | ✘ No |
make | compile + run | Yes, and the safest |
make random | Compiles, then runs with a random seed | ✔ |
Runtime arguments: these three need no rebuild
| Argument | Example | Effect |
|---|---|---|
seed | make run seed=2 |
A different set of random stimulus. The same seed reproduces the same result — always fix the seed when debugging |
verbosity | make run verbosity=UVM_HIGH |
Adjusts how much detail the log carries |
TEST_NAME | make run TEST_NAME=test |
Chooses which test class runs (equivalent to +UVM_TESTNAME=) |
The five verbosity levels
| Level | Value | When to use it |
|---|---|---|
UVM_NONE | 0 | Must print. Final statistics, the PASS/FAIL summary |
UVM_LOW | 100 | Milestones: phase markers, iteration progress |
UVM_MEDIUM | 200 | The default threshold. The DRV/MON/SCR message for each transaction |
UVM_HIGH | 300 | Debugging detail, such as every coverage sample |
UVM_DEBUG | 500 | The most talkative |
The rule: a message prints when its level is at or below the current threshold. So under the default UVM_MEDIUM,
NONE/LOW/MEDIUM appears and HIGH/DEBUG is filtered out.
Tracing a log ID back to who printed it
| ID | Source component | Seeing it means |
|---|---|---|
[SEQ] | sequence_c | a random stimulus was generated |
[DRV] | driver_c | the stimulus has reached the pins |
[MON] | monitor_c | a complete transaction was observed back from the pins |
[SCR] | scoreboard_c | a comparison came out PASS or FAIL |
[COV] | cov_coll_c | one sample was taken (visible only at UVM_HIGH) |
[TEST] | test | iteration progress, coverage summary |
[PHASE] | test | a phase marker (one you added yourself) |
A suggested debugging order: follow the SEQ → DRV → MON → SCR chain.
Wherever it breaks is where the problem is:
no SEQ -> the sequence never started, or no objection was raised;
SEQ but no DRV -> the driver is stuck in get_next_item or never received the vif;
DRV but no MON -> the monitor's timing is wrong, or it is stuck at wait();
MON but no SCR -> the TLM connection was never made.
14Recap
One sentence per chapter, to be used as an index — jump back from here when you cannot recall a section:
| Ch. | In one sentence |
|---|---|
| 01 | What UVM gives you is four things: reuse, randomisation, automatic checking and measurability |
| 02 | A class cannot touch a signal; interface + virtual interface is the only channel, and modport decides who may write |
| 03 | Components form a tree, and each level creates its children in its own build_phase |
| 04 | Eight stations: object -> signal -> object, with the conversions happening in the driver and the monitor |
| 05 | uvm_object is data and uvm_component is structure; their constructors and registration macros differ |
| 06 | Only run / main are tasks; no other phase may consume time, and objections decide when to pack up |
| 07 | Components never call each other; everything goes through port.connect(export); switch to a FIFO when the output is delayed |
| 08 | config_db's type, path and key must all match exactly |
| 09 | Register -> create() -> override; miss any one and nothing is swapped |
| 10 | The scoreboard says whether it is wrong, coverage says whether it was ever tested |
| 11 | A different protocol touches only transaction / driver / monitor / interface; the architecture is untouched |
| 12 | Fifteen common mistakes; check this chapter first when you are stuck |
| 13 | Editing .sv always needs a rebuild; follow SEQ→DRV→MON→SCR to find where it breaks |
If you only remember five things
- The data flow is always sequence -> sequencer -> driver -> interface -> DUT -> interface -> monitor -> analysis port -> scoreboard / coverage. When you are stuck, first ask which station it broke at.
- Two families: transactions are
uvm_object(new(name)+`uvm_object_utils), components areuvm_component(new(name, parent)+`uvm_component_utils). - Always use
type_id::create(), nevernew(). It is the prerequisite for a factory override. - Only
run_phase/main_phasecan consume time; every other phase is a function. - Objections decide when the simulation ends; forget to raise one and you finish at 0 ns.