UVM register coverage and register-based checking use the register model for two jobs beyond programming the DUT. Coverage measures which register values, bits and addresses the tests have exercised. Checking lets a scoreboard use the model's mirrored values to know the DUT's current configuration, so it can compare data correctly when behaviour depends on how the DUT was programmed.

The register model already holds everything a testbench needs for both: the model mirrors the DUT's registers, so any component with a handle to it can read the current configuration at any moment, and can sample it into a covergroup.

9 min read | Intermediate

What You'll Learn

  • Enable, control and sample the coverage models built into a register model
  • Write an external covergroup that samples register values through the model
  • Let a scoreboard adapt to the DUT's configuration using mirrored values
  • Check DUT registers with predict(), mirror() and back-door reads

Three Ways to Collect Register Coverage

Reg coverage paths
  • Automatic: covergroups inside the model are sampled as a side effect of every register access.
  • Controlled: covergroups inside the model that are sampled only when the testbench calls sample_values().
  • External: a covergroup in a testbench component reads register values through the model handle.

The UVM register library does not include any coverage models itself. What a register covergroup should contain depends on a register's fields and layout, so the register model generator adds them, and the library supplies only the controls. A hand-written model has no built-in coverage unless you add it.

Controlling the Built-in Coverage

Coverage models cost memory and simulation speed, especially for models with thousands of registers, so they are off by default. They are chosen with a bit mask, uvm_coverage_model_e:

ValueCovers
UVM_NO_COVERAGENothing
UVM_CVR_REG_BITSWhich bits of registers were read or written
UVM_CVR_ADDR_MAPWhich addresses of a map were read or written
UVM_CVR_FIELD_VALSThe values stored in fields
UVM_CVR_ALLAll of the above

Values can be combined with | or +. Coverage is a two-step choice: which models to build, and which of the built ones to sample.


// 1. Before the register model is built: choose which covergroups to create
uvm_reg::include_coverage("*", UVM_CVR_ADDR_MAP | UVM_CVR_FIELD_VALS);
m_ral = ral_sys_traffic::type_id::create("m_ral");
m_ral.build();

// 2. Later, in any test phase: choose which built models actually sample
void'(m_ral.set_coverage(UVM_CVR_ADDR_MAP));          // sample addresses only
if (m_ral.get_coverage(UVM_CVR_FIELD_VALS))
  `uvm_info("COV", "Field value coverage is on", UVM_LOW)

include_coverage() is a static method that stores its request in the resource database. The model looks it up while it is being built, so it must be called before build(). The first argument is a scope pattern, so a different set of models can be built for different blocks, for example full coverage for the block under test only.

set_coverage() turns sampling on for the listed models and off for all the others, on a block and everything inside it. It can only control models that were built in step 1. has_coverage() tells you what was built, and get_coverage() what is currently being sampled.

The field-value covergroups are the ones sampled on request. Call sample_values() on a block, or on any register, at the moment the state is interesting, such as when an interrupt arrives:


m_ral.sample_values();        // samples the field values of every register in the block

External Covergroups: the Recommended Approach

Automatic coverage of every field is easy to get and rarely useful. A field that has 16 possible values reaches 100% when each was written once, which says nothing about the combinations that matter, such as a mode field together with a clock divider. Meaningful register coverage crosses several registers, and often non-register signals. That is easiest to write in a normal covergroup that reads the model:


class cfg_coverage extends uvm_subscriber #(bus_item);
  `uvm_component_utils(cfg_coverage)
  ral_sys_traffic m_ral;                         // set by the env
  bit              mod_en, profile;
  bit [31:0]       timer0;

  covergroup cg_cfg;
    cp_en      : coverpoint mod_en;
    cp_profile : coverpoint profile;
    cp_timer   : coverpoint timer0 { bins short_t = {[0:'hFF]};
                                     bins long_t  = {['h100:$]}; }
    x_mode     : cross cp_en, cp_profile;                 // module on/off in each profile
    x_timer    : cross cp_profile, cp_timer;              // timer ranges per profile
  endgroup

  function new(string name, uvm_component parent);
    super.new(name, parent);
    cg_cfg = new();
  endfunction

  // Sample the configuration every time the control register is written
  virtual function void write(bus_item t);
    if (t.write && t.addr == m_ral.cfg.ctrl.get_address()) begin
      mod_en  = m_ral.cfg.ctrl.mod_en.get_mirrored_value();
      profile = m_ral.cfg.ctrl.profile.get_mirrored_value();
      timer0  = m_ral.cfg.timer[0].get_mirrored_value();
      cg_cfg.sample();
    end
  endfunction
endclass

This component is a subscriber on the bus monitor, as in Functional Coverage in a UVM Testbench. The covergroup measures what the verification plan cares about: every combination of module mode and profile, and which timer ranges were used in each profile. Because it reads mirrored values, it is only as accurate as the model's prediction, so the mirror must be kept up to date, as described in UVM Register Access Methods.

Quick Check

Q: The covergroup above samples when the control register is written, but timer[0] was written earlier, and the mirror was never updated for it. What does timer0 contain, and why does that matter?

Show Answer

The model's mirrored value, which stays at the reset value if nothing told the model about the write. The coverage then reports timer ranges that were never actually in the DUT. The coverage is only as truthful as the prediction, so use a register predictor or auto-prediction to keep the mirror current.

Using the Model in a Scoreboard

The register model is a ready-made record of how the DUT is configured. Many scoreboards need exactly that. A transmit scoreboard may need to know how many bits of each word the DUT sends, and in which order. Reading it from the model beats duplicating the configuration inside the scoreboard.


class tx_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(tx_scoreboard)
  uvm_analysis_imp #(ser_item, tx_scoreboard) imp;
  ral_ser_block m_ral;                           // handle assigned by the env from its config object
  // Rest of the code (constructor, build_phase creating imp)

  virtual function void write(ser_item t);
    bit [31:0] width = m_ral.fmt.width.get_mirrored_value();        // bits per transfer
    bit        lsb   = m_ral.fmt.lsb_first.get_mirrored_value();
    bit [31:0] mask  = (32'h1 << width) - 1;                        // assumes width < 32
    bit [31:0] exp   = m_ral.txdata.get_mirrored_value() & mask;    // what was loaded to send

    if (lsb) exp = reverse_bits(exp, width);                         // small helper function, not shown
    if (t.data != exp)
      `uvm_error("TXSB", $sformatf("sent 0x%0h, expected 0x%0h (width=%0d, lsb_first=%0b)",
                                   t.data, exp, width, lsb))
  endfunction
endclass

The scoreboard never needs to be told how the DUT was programmed. Whatever the test wrote to the format register, the model already knows. This is how a single scoreboard can check a DUT over all of its configurations.

The handle reaches the scoreboard from a configuration object, set in the environment's connect_phase, the same way the monitor's analysis port is connected.

Checking Registers Against Expected Values

The model can also check the DUT's registers directly. There are two patterns:

  • Predict and mirror. The scoreboard works out what a register should now contain and calls predict() on it. A later mirror(status, UVM_CHECK) reads the real register and reports an error if it differs. The mirror calls can come from the scoreboard or from a checking sequence.
  • Back-door peek. peek() reads the register's HDL signal directly, in zero time. It is the right tool for registers with side effects, such as clear-on-read status, because a front-door read would change the value being checked.

// A receive scoreboard that knows what the DUT should have received
virtual function void write(ser_item t);
  uvm_status_e   status;
  uvm_reg_data_t actual;

  m_ral.rxdata.peek(status, actual);              // back door: no bus access, no side effects
  if (status != UVM_IS_OK)
    `uvm_error("RXSB", "peek of rxdata failed")
  else if (actual != t.data)
    `uvm_error("RXSB", $sformatf("rxdata=0x%0h, expected 0x%0h", actual, t.data))

  void'(m_ral.rxdata.predict(t.data));            // keep the mirror in line for later front-door checks
endfunction

Back-door access needs HDL paths in the model; see UVM Register Backdoor Access. A scoreboard that reads the model must run when the model is up to date, so make sure the transaction that updates the mirror reaches the model before the scoreboard looks at it.

Common Mistakes

Mistake #1: Calling include_coverage() after build()

The request is read while the model is being built. Called afterwards, it changes nothing, and the covergroups never exist.

Mistake #2: Expecting coverage from a hand-written model

The register library has no built-in covergroups; generators add them. A model written by hand needs its own covergroups or an external one.

Mistake #3: Trusting mirrored values that nobody updates

Coverage and scoreboards that read the mirror are wrong if the mirror is not kept current. Use auto-prediction or a predictor before relying on it.

Mistake #4: Front-door reads of volatile registers in a checker

Reading a clear-on-read register to check it changes what the DUT holds. Use peek(), or predict the effect of the read.

Summary

In this article, you learned:

  • Register coverage comes from generator-supplied covergroups, controlled with include_coverage() before the build and set_coverage() afterwards
  • sample_values() samples field-value coverage on demand
  • External covergroups that cross several registers' mirrored values are usually more meaningful than automatic per-field coverage
  • A scoreboard with a model handle can adapt to the DUT's configuration using get_mirrored_value()
  • Registers can be checked with predict() plus mirror(UVM_CHECK), or with a back-door peek()
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5