Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
    • RAL Overview
    • Register Access
    • Registers in Verification
      • Built-in Register Sequences and NO_* Attributes
      • Register Coverage
      • Memories in the Register Model
      • Multiple Maps and Address Spaces
      • Shadow, Alias and Paged Registers
      • Interrupts with the Register Model
      • The Register Model at SoC Scale
      • RAL Interview Set
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Registers in Verification
  1. Topics
  2. UVM Verification
  3. UVM Register Abstraction Layer (RAL)
  4. Registers in Verification
  5. Register Coverage

Part 9 · Register Model (RAL) · Intermediate

Register Coverage

The three register coverage models, the three switches that must all be on, who calls sample(), and the behavioural register coverage that sign-off needs.

On this page

    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.

    The idea: a key-card log is not an inspection

    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

    UVM_CVR_REG_BITS ('h1)

    Each bit of a register seen as 0 and 1, on reads and on writes

    uvm_reg::sample(data, byte_en, is_read, map), on every observed access

    UVM_CVR_ADDR_MAP ('h2)

    Each register and memory address accessed, by direction

    uvm_reg_block::sample(offset, is_read, map), on every observed access

    UVM_CVR_FIELD_VALS ('h4)

    Field values in bins (often min, max, other)

    sample_values(), only when you call it

    UVM_CVR_ALL (-1)

    Everything above

    Both

    Three switches, and all three must be on

    • 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().

    The picture: who calls sample(), and when

    Sequence diagram: the monitor writes to the predictor, which calls the register's and the block's sample functions and then updates the mirror; the test separately calls sample_values on the block, which calls it on each register.
    With explicit prediction the predictor samples the register and the block for each observed access, before the mirror changes. Field-value coverage is sampled only when the test calls sample_values().

    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.

    Build it: what generators emit, then what the DMA needs

    The generated shape

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    // 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);
    endfunction

    Look 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.

    Register coverage that tracks behaviour

    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:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 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
    endclass

    Why 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:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 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
    endfunction

    Watch it run

    The 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:

    Diagram
    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 :    0
    • Line 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.

    What breaks: the full path that matched nothing

    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:

    Diagram
    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=0

    Nothing 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.

    Note

    Verilator ignores coverpoints (warning COVERIGN), so on this course's simulator every register covergroup reports 0% whether or not the switches are right. That is why the scratch register keeps plain counters. On Riviera-PRO, Questa, VCS or Xcelium the covergroup numbers are real, and the three-switch check above is what makes them trustworthy.

    Interview

    "How do you get register coverage, and is it worth having?"

    A weak answer: "The generator creates it with UVM_CVR_ALL, and you check it reaches 100%."

    A strong answer: "There are three models: bits, address map and field values. Coverage must be allowed with include_coverage before the model is built, because build_coverage runs in each register's constructor, against a name with no parent path. Then it must be turned on with set_coverage, or it stays off. Bits and addresses come from sample(), which the predictor calls for every observed access; field values only when I call sample_values(). Generated coverage proves access, and bit-bash fills most of it. Sign-off register coverage is behavioural: a W1C write while the bit is set, GO with each IRQ_EN value, LEN at GO in its ranges, writes the DUT must ignore while busy. I sample those at the moment they matter."

    Follow-ups: Your register coverage is 0% on every register. List the causes in order. (not allowed by scope, not built, not turned on, the simulator ignoring coverpoints). Why is the generated field-value bin for LEN misleading? (it samples whenever sample_values() is called, not the LEN a job ran with).

    Check yourself Register coverage was enabled with 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.

    Check yourself The env uses explicit prediction. A JTAG sequence outside the register model writes STATUS through a second bus with no predictor. Is that write sampled by register coverage?

    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.

    Check yourself Why must the W1C coverage item know whether the bit was set *before* the write?

    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.

    Related topics

    • Up: Registers in Verification

    • Next: Memories in the Register Model

    • Coverage & Closure

    Previous

    162 / 280

    Next: Memories in the Register Model

    Up next

    Up nextMemories in the Register Model

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Multiple Maps and Address Spaces
    • Shadow, Alias and Paged Registers
    • Interrupts with the Register Model