The register coverage report at the block's sign-off review reads 100%: every address read and written, every bit of every register seen as 0 and as 1. The bit-bash test alone produces most of that. Two weeks later a firmware engineer finds that clearing ERR while DONE is still set loses the DONE interrupt. No test had ever cleared a status bit that was actually set; the coverage model had counted every write of 1 to a W1C bit, set or not.
Register coverage has two parts that get confused. The library gives you hooks and switches, and generators fill them with covergroups that measure access: which addresses, which bits, which values were seen. What sign-off needs is coverage of behaviour: the register values and sequences that change what the design does. This lesson covers both, including the switch that leaves generated coverage at zero without a single warning.
A hotel's key-card system logs every door that opened. It can prove that every room was entered. It cannot tell you whether anyone checked that the minibar was restocked. Generated register coverage is the key-card log: complete, automatic and shallow. Functional register coverage is the inspection checklist, and you write it from the spec.
The library defines three coverage models, as bits in a uvm_reg_cvr_t value:
Model | Typical generated content | Sampled from |
|---|---|---|
| Each bit of a register seen as 0 and 1, on reads and on writes |
|
| Each register and memory address accessed, by direction |
|
| Field values in bins (often min, max, other) |
|
| Everything above | Both |
Allowed: uvm_reg::include_coverage(scope, models) puts a resource in the database before the model is built. A register's constructor calls build_coverage(models), which returns only the models that resource allows. The default is UVM_NO_COVERAGE.
Built: the constructor passes the result to super.new(name, n_bits, has_coverage) (or add_coverage()), and creates each covergroup only if has_coverage(model) is true. has_coverage() tells you what exists.
On: sampling is gated by get_coverage(model), which is true only after set_coverage(models) has been called on the register or a parent block. In the IEEE 1800.2 library this course runs, the on-state starts at zero: built coverage is off until you call set_coverage().
Two consequences follow. Inside sample(), get_mirrored_value() still returns the value from before this access, which is exactly what you need to tell a clear of a set bit from a clear of a clear bit. And with auto prediction instead, the map samples only accesses made through the register model; with explicit prediction, every bus access is sampled, whoever made it.
Generators (SystemRDL tools such as PeakRDL-uvm, IP-XACT flows, Synopsys ralgen) emit roughly this for each register and block when coverage is requested. The details vary; the three switches never do.
// snippet: the shape a generator emits for one register and its block
class len_reg extends uvm_reg;
`uvm_object_utils(len_reg)
rand uvm_reg_field value;
local uvm_reg_data_t m_data;
local bit m_is_read;
covergroup cg_bits; // UVM_CVR_REG_BITS
option.per_instance = 1;
b0_wr: coverpoint m_data[0] iff (!m_is_read);
b0_rd: coverpoint m_data[0] iff (m_is_read);
// ... one pair per bit
endgroup
covergroup cg_vals; // UVM_CVR_FIELD_VALS
option.per_instance = 1;
len_val: coverpoint value.value { bins zero = {0}; bins max = {16'hFFFF}; bins other = default; }
endgroup
function new(string name = "len_reg");
super.new(name, 32, build_coverage(UVM_CVR_REG_BITS | UVM_CVR_FIELD_VALS));
if (has_coverage(UVM_CVR_REG_BITS)) cg_bits = new();
if (has_coverage(UVM_CVR_FIELD_VALS)) cg_vals = new();
endfunction
protected virtual function void sample(uvm_reg_data_t data, uvm_reg_data_t byte_en,
bit is_read, uvm_reg_map map);
if (get_coverage(UVM_CVR_REG_BITS)) begin
m_data = data; m_is_read = is_read;
cg_bits.sample();
end
endfunction
virtual function void sample_values();
super.sample_values();
if (get_coverage(UVM_CVR_FIELD_VALS)) cg_vals.sample();
endfunction
endclass
// In the block: address-map coverage
covergroup cg_addr with function sample(uvm_reg_addr_t offset, bit is_read);
option.per_instance = 1;
cp_addr: coverpoint offset { bins ctrl = {'h00}; bins status = {'h04}; bins len = {'h08};
bins key = {'h0C}; bins count = {'h10}; }
cp_dir: coverpoint is_read;
cross cp_addr, cp_dir;
endgroup
protected virtual function void sample(uvm_reg_addr_t offset, bit is_read, uvm_reg_map map);
if (get_coverage(UVM_CVR_ADDR_MAP)) cg_addr.sample(offset, is_read);
endfunctionLook at what that measures. len_val bins LEN at whatever moment sample_values() is called, which says nothing about the LEN a job ran with unless you call it at GO. The bit bins are filled by bit-bash in one test. The address cross is filled by the reset and bit-bash tests too. None of it would have caught the lost-interrupt bug in the opening.
Behavioural register coverage samples a register event at the moment it changes what the DUT does. For the DMA, the useful items are: a write of 1 to DONE or ERR while that bit is set (a real clear, versus a no-op); GO written with IRQ_EN 0 and with IRQ_EN 1; LEN at GO in the zero, one, maximum-packet and over-maximum ranges; and a write to LEN or KEY while BUSY, which the DMA ignores. Most of these are already items in the bench's dma_coverage component, built in Coverage & Closure; the W1C one is the coverage-model bug the bench itself once had. Here is the W1C item placed in the register model, so it is sampled by the predictor for every observed access. It was verified in a scratch test that type-overrides the bench's STATUS register:
// snippet: scratch extension of the bench's STATUS register
class status_reg_cov extends dma_status_reg;
`uvm_object_utils(status_reg_cov)
bit last_read_done; // DONE as the last observed read returned it
covergroup clear_cg with function sample(bit done_was_set, bit wrote_one);
option.per_instance = 1;
cp_clear: coverpoint {done_was_set, wrote_one} {
bins clears_set_bit = {2'b11};
bins clears_clear_bit = {2'b01};
}
endgroup
function new(string name = "status_reg_cov");
super.new(name); // the bench passes UVM_NO_COVERAGE
add_coverage(build_coverage(UVM_CVR_FIELD_VALS)); // so ask for it here
if (has_coverage(UVM_CVR_FIELD_VALS)) clear_cg = new();
endfunction
protected virtual function void sample(uvm_reg_data_t data, uvm_reg_data_t byte_en,
bit is_read, uvm_reg_map map);
if (!get_coverage(UVM_CVR_FIELD_VALS)) return;
if (is_read) last_read_done = data[1];
else clear_cg.sample(last_read_done, data[1]);
endfunction
endclassWhy remember the last read instead of asking done.get_mirrored_value()? DONE is volatile, so reading its mirror warns (UVM/FLD/GET_MIRRORED_VAL/VOL), and the mirror only changes when someone reads STATUS anyway. Tracking the observed read says the same thing honestly: software saw DONE set, then wrote 1 to it. The test turns the switches on in the right order:
// snippet: scratch test
function void build_phase(uvm_phase phase);
dma_status_reg::type_id::set_type_override(status_reg_cov::get_type());
uvm_reg::include_coverage("*", UVM_CVR_ALL); // 1. allowed: before the model is built
super.build_phase(phase); // 2. built: the env builds the model
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
void'(renv.regmodel.set_coverage(UVM_CVR_ALL)); // 3. on: sampling enabled
endfunctionThe scratch test writes 6 to STATUS first (the habit of clearing everything after reset, with nothing set), runs a 3-word job, reads STATUS, and writes back what it read. Because Verilator compiles covergroups but ignores their coverpoints, the scratch register also counted the two cases in plain variables; those counts are what the log prints. On a commercial simulator the covergroup reports the same two hits. Scratch run, Verilator:
UVM_INFO scratch.sv(533) @ 0: uvm_test_top [COV] status: has_coverage(FIELD_VALS)=1 get_coverage(FIELD_VALS)=0
UVM_INFO scratch.sv(537) @ 0: uvm_test_top [COV] after set_coverage: get_coverage(FIELD_VALS)=1
UVM_INFO scratch.sv(558) @ 535000: uvm_test_top [COV] status.sample() calls=3 sample_values() calls=1
UVM_INFO scratch.sv(559) @ 535000: uvm_test_top [COV] DONE clears of a set bit=1, of a clear bit=1
UVM_INFO dma_tb.sv(411) @ 535000: uvm_test_top.env.sb [SB] 3 words matched, 0 mismatched
UVM_ERROR : 0Line 1 is the trap: the covergroup was built, and it is off. Without the set_coverage() call the report stays at zero for the whole regression.
sample() calls=3: the predictor sampled the write of 6, the read of STATUS and the clearing write. sample_values() ran once, when the test called it on the block.
Clears of a set bit=1, of a clear bit=1: the post-reset write of 6 is counted where it belongs, as a no-op, and only the write after a read that saw DONE counts as a real clear.
The first version of the scratch test enabled coverage for just the one register, with the scope a reader would expect: uvm_reg::include_coverage("regmodel.status", UVM_CVR_ALL). Scratch run, Verilator:
UVM_INFO scratch.sv(533) @ 0: uvm_test_top [COV] status: has_coverage(FIELD_VALS)=0 get_coverage(FIELD_VALS)=0
UVM_INFO scratch.sv(537) @ 0: uvm_test_top [COV] after set_coverage: get_coverage(FIELD_VALS)=0
UVM_INFO scratch.sv(559) @ 535000: uvm_test_top [COV] DONE clears of a set bit=0, of a clear bit=0Nothing was built. build_coverage() runs inside the register's constructor, and looks up {"uvm_reg::", get_full_name()}. At that moment the register has no parent yet: configure(this) comes after create(). Its full name is just status, so the scope regmodel.status never matches. Use a pattern that matches the bare name ("*status"), or "*" with a model mask, and print has_coverage() once at end of elaboration so a silent zero shows up on the first run instead of at sign-off. The same check catches the third switch: without set_coverage(), the log reads has_coverage=1 get_coverage=0 and every count stays 0.
include_coverage("*", UVM_CVR_ALL) in the test's connect_phase, after the env built the model. The report shows nothing. Why?Coverage is decided when each register is constructed: build_coverage() reads the resource in the constructor. By connect_phase the model already exists, so no covergroup was created. Set include_coverage before the env's build_phase creates the model, for example at the top of the test's build_phase.
No. sample() is called by a predictor (or by auto prediction for accesses made through the model). A bus with no predictor connected to the model is invisible to both the mirror and register coverage. Give that bus its own map and predictor, as in Multiple Maps and Address Spaces.
Writing 1 to a W1C bit that is already 0 does nothing in hardware. Counting it as a clear marks the clear path covered when no clear ever happened, which is exactly how a test that clears STATUS after every job, set or not, closes the item without exercising it. The predictor calls sample() before it updates the mirror, so the register can compare the write with the state software last observed.
Next: registers have mirrors; memories do not. Memories in the Register Model shows what the library does keep for a memory, and how a burst write on this bench turned into four reads.