A passing test proves that nothing went wrong in the scenarios it ran. It says nothing about the scenarios it never ran. Functional coverage measures which features, values and combinations the stimulus actually reached, and in a UVM testbench the monitor's analysis port makes collecting it almost free.
Functional coverage in a UVM testbench is collected by a covergroup that samples the transactions a monitor observes. The usual home for it is a uvm_subscriber connected to the monitor's analysis port, so every transaction the DUT really saw is sampled exactly once, and coverage can be switched off for a test or a whole agent through the configuration database.
This article focuses on the UVM side: where the covergroup lives, how it receives data, how to control it and how to report it. The SystemVerilog constructs themselves are covered in SystemVerilog Covergroup and Coverpoint, SystemVerilog Coverpoint Bins and SystemVerilog Cross Coverage.
8 min read | Intermediate
What You'll Learn
- Place a covergroup in a
uvm_subscriberand feed it from the monitor - Write a covergroup that measures features, not just values
- Switch coverage collection on and off through
uvm_config_db - Report coverage at the end of the test and use it to close holes
Where the Covergroup Lives
There are two common places for class-based coverage in UVM:
- Inside the monitor. The monitor samples its covergroup right after it has collected a transaction. This is the approach used in the UVM User's Guide examples, with a
coverage_enablebit deciding whether sampling happens. - In a separate subscriber. The monitor only collects and broadcasts transactions, and a
uvm_subscriberon its analysis port does the sampling. This keeps the monitor reusable, lets coverage be replaced through the factory, and lets one testbench add project-specific coverage without touching the agent.
Either works. The subscriber is the more flexible choice and is the one used here. Both have one thing in common: they sample what the monitor observed on the interface, not what a sequence intended to send. A transaction that a sequence generated but that never reached the DUT, because of a bug or an early end of test, must not count as covered.
Syntax: A Coverage Subscriber
class extends uvm_subscriber #();
`uvm_component_utils()
tr; // the sample source, declared before the covergroup
covergroup cg;
// coverpoints and crosses on tr's fields
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg = new(); // an embedded covergroup must be built in new()
endfunction
virtual function void write( t);
tr = t;
cg.sample();
endfunction
endclass
uvm_subscriber already provides an analysis_export and requires you to implement write(). Every transaction the monitor broadcasts arrives there.
Example: Coverage for the Register Block
The example extends the register-block testbench from Subscriber [uvm_subscriber]. reg_transaction has wr_en, rd_en, a 4-bit addr and an 8-bit wdata. A coverpoint on addr alone only says that each address was touched. What the verification plan really asks is whether every register was both written and read, and whether the extreme data values were written:
class reg_coverage extends uvm_subscriber #(reg_transaction);
`uvm_component_utils(reg_coverage)
reg_transaction tr;
covergroup cg_reg;
option.per_instance = 1;
cp_addr : coverpoint tr.addr; // 16 automatic bins, one per register
cp_op : coverpoint tr.wr_en {
bins read = {0};
bins write = {1};
}
cp_data : coverpoint tr.wdata iff (tr.wr_en) { // only meaningful for writes
bins zero = {8'h00};
bins max = {8'hFF};
bins mid = {[8'h01:8'hFE]};
}
x_addr_op : cross cp_addr, cp_op; // every register read AND written
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg_reg = new();
endfunction
virtual function void write(reg_transaction t);
tr = t;
cg_reg.sample();
endfunction
virtual function void report_phase(uvm_phase phase);
super.report_phase(phase);
`uvm_info("COV", $sformatf("Register coverage: %0.2f%%", cg_reg.get_inst_coverage()), UVM_LOW)
endfunction
endclass
A few details make this work. tr is declared before the covergroup, because the coverpoints refer to it. The covergroup is created in new(), because SystemVerilog only allows an embedded covergroup to be created there. And the iff on cp_data stops reads, whose wdata is meaningless, from counting as data coverage.
The cross x_addr_op has 32 bins: 16 registers times read or write. It is the coverpoint that answers the plan's question directly, and it is usually the last to reach 100%.
Connecting it
The agent creates the subscriber and connects the monitor's analysis port to it in connect_phase:
m_monitor.mon_analysis_port.connect(m_coverage.analysis_export);
Because the monitor sends each transaction only once it is complete, including read data, every sample is a full and correct transaction.
Switching Coverage On and Off
Coverage collection costs simulation time. Regressions that only check for errors, block-level agents reused passively at SoC level, and debug runs often don't need it. The configuration database is the standard way to control it.
Because the covergroup can only be created in new(), which runs before build_phase can read any configuration, the switch is best placed one level up: the agent decides whether to create the coverage component at all.
class reg_agent extends uvm_agent;
// Rest of the code (registration, driver, monitor, sequencer handles)
reg_coverage m_coverage;
bit has_coverage = 1; // default: collect coverage
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
void'(uvm_config_db #(bit)::get(this, "", "has_coverage", has_coverage));
// ... sequencer, driver, monitor as before ...
if (has_coverage)
m_coverage = reg_coverage::type_id::create("m_coverage", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// ... driver to sequencer as before ...
if (has_coverage)
m_monitor.mon_analysis_port.connect(m_coverage.analysis_export);
endfunction
endclass
// In a test that doesn't need coverage
uvm_config_db #(bit)::set(this, "m_top_env.m_agent", "has_coverage", 0);
The get() is wrapped in void'() so a missing setting simply leaves the default in place. The set() must use the same type, bit, and the same field name, or the agent never sees it. If you keep the covergroup inside the monitor instead, the equivalent is a coverage_enable bit read the same way and checked before each sample().
Quick Check
Q: A test sets has_coverage using uvm_config_db #(int)::set(...). The agent reads it with uvm_config_db #(bit)::get(...). Is coverage switched off?
Show Answer
No. The configuration database matches on type as well as name, so a bit lookup never finds an int setting. The get() fails silently because of the void'(), and the default of 1 stays. Use the same type on both sides.
Reporting and Closing Coverage
get_inst_coverage() returns the coverage of this covergroup instance as a percentage, and printing it in report_phase puts a one-line summary at the end of every log. With option.per_instance = 1, the simulator's coverage report also lists each instance separately, which matters when several agents of the same type each have their own coverage subscriber.
Coverage from one seed is only a starting point. The usual loop is:
- Run the regression with many seeds and merge the coverage databases with the simulator's tools.
- Look at the uncovered bins. In this example a likely hole is
readfor some registers, because random traffic writes far more often than it happens to read back a particular address. - Close the hole with constraints that make the missing case more likely, or a short directed sequence, rather than by deleting the bin.
- Rerun and confirm the bin is now hit by a test that also passed its checks.
The planning side of this loop is described in Coverage Driven Verification, and reading the reports in Coverage Reports.
Coverage only means something when the checks pass. A bin hit by a test that also reported a scoreboard mismatch is not verified. Most teams only merge coverage from passing runs.
Common Mistakes
Mistake #1: Sampling in the sequence
A sequence knows what it meant to send, not what the DUT received. Sample in the monitor or a subscriber on its analysis port, so that only real interface activity counts.
Mistake #2: Creating the covergroup outside new()
An embedded covergroup can only be created in the class constructor. Creating it in build_phase is not allowed by the language, and forgetting to create it at all means sample() is called on a null handle.
Mistake #3: Covering values instead of features
Sixteen address bins at 100% say nothing about whether each register was read back after a write. Use crosses and iff conditions to express what the verification plan actually requires.
Mistake #4: Counting coverage from failing runs
Merging coverage from tests that failed their checks overstates what was verified. Merge from passing runs only.
Summary
In this article, you learned:
- Coverage is usually collected in a
uvm_subscriberon the monitor's analysis port, sampling only observed transactions - An embedded covergroup is created in
new()and sampled inwrite() - Crosses and
iffconditions turn value coverage into feature coverage - A
has_coveragebit inuvm_config_db, read by the agent, decides whether coverage is collected get_inst_coverage()inreport_phasegives a per-run summary; merged results from passing seeds drive coverage closure