UVM register backdoor access reads and writes DUT registers directly through their HDL signal paths, instead of sending transactions over the bus. Normally, the register model uses the front door, as set up in the register environment: every register read and write is converted into a bus transaction and driven into the design's bus interface, just as any other hardware master in the system would do it.

Because front-door accesses are real bus transactions, they consume bus cycles. This is the recommended way to verify registers, because it closely matches what happens in the real system.

8 min read | Intermediate Level

What You'll Learn

  • What backdoor access is and how it differs from front-door access
  • Define HDL paths at each level of a register model
  • Perform backdoor writes and reads with UVM_BACKDOOR, peek() and poke()
  • Read a waveform that mixes front-door and backdoor accesses

What are backdoor accesses ?

UVM also supports backdoor accesses, which use the simulator's database to access signals inside the DUT directly. A backdoor write deposits a value onto the register's signal, and a backdoor read samples the signal's current value.

Register storage is usually a simple flip-flop, so depositing a new value on it does not usually disturb the rest of the design. However, the control logic that is active during a normal bus write is not exercised. Any logic that depends on that bus access, such as an interrupt raised on a write or a status bit cleared by it, does not react as it would in the real system.

uvm register backdoor access

A backdoor access takes zero simulation time, because the HDL values are accessed directly and no bus transaction is involved. It is not the recommended way to verify register accesses themselves. In certain situations, however, backdoor accesses strengthen a verification effort built on front-door accesses.

AspectFront doorBack door
Path to the registerMap, adapter, bus agent, bus interfaceDirect HDL signal access
Simulation timeBus cycles per accessZero time
Exercises bus and decode logicYesNo
Needs HDL paths in the modelNoYes
Typical useVerifying register access and behaviorFast setup, checking values written by the front door, debug

Where is Backdoor Access Used?

Long SoC tests often start by programming hundreds of configuration registers. Doing this through the back door saves minutes of simulation per test. The front door is still used for the registers under test, and a separate register test checks every register through the bus.

Define backdoor HDL path

The simulator needs the HDL path of the signal it accesses. It is the user's responsibility to specify these paths in the register model, either for every register or only for those that need backdoor access.

To improve reuse and portability, the full path is broken into parts at each level of the hierarchy. For example, the path to a register can be split into four parts:

  • From the testbench top to the subsystem top
  • From the subsystem top to the block top
  • From the block top to the register block
  • From the register block to the register itself

If the subsystem is reused in another design, only the part from the testbench top to the subsystem top needs to be updated.

Syntax


// In a register block: path from the parent block to this block
BLOCK.configure (PARENT, "hdl.path");          // or BLOCK.add_hdl_path ("hdl.path")

// In a register: signal name (and bit range) relative to its block
REG.add_hdl_path_slice ("signal_name", OFFSET, SIZE);

// Backdoor accesses
REG.write (status, value, UVM_BACKDOOR);       // Mimics a front-door write (e.g. RO bits unchanged)
REG.read  (status, value, UVM_BACKDOOR);
REG.poke  (status, value);                     // Deposits the value as-is
REG.peek  (status, value);                     // Samples the value as-is

The full path of a register is formed by joining the paths of every enclosing block, from the top, with the register's slice name, separated by dots.

Example

This example adds HDL paths to the traffic controller design from the UVM Register Model Example, and then performs some backdoor writes and reads. HDL paths are usually added while the register model is built. Because they are added by function calls, they can also be added later, after the model has been constructed.

It is common for verification teams to build an automated flow that parses a list of HDL paths provided by the designers and adds them to an existing register model. The easiest way to understand the arrangement is top-down: first the testbench level, then the DUT level, then any subsystem level, then the register blocks, and finally the registers.

In this simple traffic controller design, the registers sit directly in the DUT, so the paths are short.


// Assume that ral_cfg_ctl and other "uvm_reg" classes are already
// defined as in the frontdoor example

class ral_block_traffic_cfg extends uvm_reg_block;
	rand ral_cfg_ctl    ctrl;       // RW
	rand ral_cfg_timer  timer[2];   // RW
       ral_cfg_stat   stat;       // RO

	`uvm_object_utils(ral_block_traffic_cfg)

	function new(string name = "traffic_cfg");
		super.new(name, build_coverage(UVM_NO_COVERAGE));
	endfunction

  virtual function void build();
    default_map = create_map("", 0, 4, UVM_LITTLE_ENDIAN, 0);
    ctrl = ral_cfg_ctl::type_id::create("ctrl",,get_full_name());
    ctrl.configure(this, null, "");
    ctrl.build();
    
    // HDL path to this instance is "tb.DUT.ctl_reg"
    ctrl.add_hdl_path_slice("ctl_reg", 0, ctrl.get_n_bits());
    default_map.add_reg(this.ctrl, `UVM_REG_ADDR_WIDTH'h0, "RW", 0);
    
    timer[0] = ral_cfg_timer::type_id::create("timer[0]",,get_full_name());
    timer[0].configure(this, null, "");
    timer[0].build();
    
    // HDL path to this instance is "tb.DUT.timer_0"
    timer[0].add_hdl_path_slice("timer_0", 0, timer[0].get_n_bits());
    default_map.add_reg(this.timer[0], `UVM_REG_ADDR_WIDTH'h4, "RW", 0);

    timer[1] = ral_cfg_timer::type_id::create("timer[1]",,get_full_name());
    timer[1].configure(this, null, "");
    timer[1].build();
    
    // HDL path to this instance is "tb.DUT.timer_1"
    timer[1].add_hdl_path_slice("timer_1", 0, timer[1].get_n_bits());
    default_map.add_reg(this.timer[1], `UVM_REG_ADDR_WIDTH'h8, "RW", 0);

    stat = ral_cfg_stat::type_id::create("stat",,get_full_name());
    stat.configure(this, null, "");
    stat.build();
    
    // HDL path from DUT to the status register will now be 
    // "tb.DUT.stat_reg" after the previous hierarchies are used 
    // for path concatenation
    stat.add_hdl_path_slice("stat_reg", 0, stat.get_n_bits());
    default_map.add_reg(this.stat, `UVM_REG_ADDR_WIDTH'hc, "RO", 0);
    // The "DUT" path of this block is given by its parent through configure()
  endfunction 
endclass 

class ral_sys_traffic extends uvm_reg_block;
  rand ral_block_traffic_cfg cfg;

	`uvm_object_utils(ral_sys_traffic)
	function new(string name = "traffic");
		super.new(name);
	endfunction

	function void build();
    default_map = create_map("", 0, 4, UVM_LITTLE_ENDIAN, 0);
    cfg = ral_block_traffic_cfg::type_id::create("cfg",,get_full_name());
    
    // Since registers exist at the DUT level in our design, configure
    // "cfg" class to have an HDL path called "DUT". So complete path to 
    // DUT is now "tb.DUT"
    cfg.configure(this, "DUT");
    cfg.build();
    
    // Path to this top level regblock in our testbench environment is "tb"
    add_hdl_path("tb");
    default_map.add_submap(this.cfg.default_map, `UVM_REG_ADDR_WIDTH'h0);
	endfunction
endclass

The path to each register is assembled from three levels:

LevelCallPath part
Top block ral_sys_trafficadd_hdl_path("tb")tb
Sub-block cfgcfg.configure(this, "DUT")DUT
Register timer[1]add_hdl_path_slice("timer_1", 0, 32)timer_1
Full pathtb.DUT.timer_1

Quick Check

Q: The DUT is now instantiated as u_traffic inside a new top module soc_tb. Which calls must change?

Show Answer

Only the block-level paths: add_hdl_path("soc_tb") in the top block and the "u_traffic" path given to the cfg block. The register slices such as "timer_1" stay the same.

Example of backdoor access in sequence

The test below mixes front-door and backdoor accesses to timer[1], and prints the model's desired and mirrored values after each step.


class reg_backdoor_test extends base_test;
   `uvm_component_utils (reg_backdoor_test)
   function new (string name="reg_backdoor_test", uvm_component parent);
      super.new (name, parent);
   endfunction

   virtual task main_phase(uvm_phase phase);
      ral_sys_traffic   m_ral_model;
      uvm_status_e      status;
      int               rdata;

      phase.raise_objection(this);

      m_env.m_reg_env.set_report_verbosity_level (UVM_HIGH);
      uvm_config_db#(ral_sys_traffic)::get(null, "uvm_test_top", "m_ral_model", m_ral_model);

      // Perform a normal frontdoor access -> write some data first and then read it back
      m_ral_model.cfg.timer[1].write(status, 32'h1234_5678);
      m_ral_model.cfg.timer[1].read(status, rdata);
      `uvm_info(get_type_name(), $sformatf("desired=0x%0h mirrored=0x%0h", m_ral_model.cfg.timer[1].get(), m_ral_model.cfg.timer[1].get_mirrored_value()), UVM_MEDIUM)
      
      // Perform a backdoor access for write and then do a frontdoor read 
      m_ral_model.cfg.timer[1].write(status, 32'ha5a5_a5a5, UVM_BACKDOOR);
      m_ral_model.cfg.timer[1].read(status, rdata);
      `uvm_info(get_type_name(), $sformatf("desired=0x%0h mirrored=0x%0h", m_ral_model.cfg.timer[1].get(), m_ral_model.cfg.timer[1].get_mirrored_value()), UVM_MEDIUM)

      // Perform a frontdoor write and then do a backdoor read
      m_ral_model.cfg.timer[1].write(status, 32'hface_face);
     // Wait for a time unit so that backdoor access reads update value
     #1;  
      m_ral_model.cfg.timer[1].read(status, rdata, UVM_BACKDOOR);
      `uvm_info(get_type_name(), $sformatf("desired=0x%0h mirrored=0x%0h", m_ral_model.cfg.timer[1].get(), m_ral_model.cfg.timer[1].get_mirrored_value()), UVM_MEDIUM)

      phase.drop_objection(this);
   endtask

  virtual task shutdown_phase(uvm_phase phase);
    super.shutdown_phase(phase);
    phase.raise_objection(this);
    #100ns;
    phase.drop_objection(this);
  endtask
endclass
Output
ncsim> run
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(392) @ 0: reporter [UVM/RELNOTES]
UVM_INFO @ 0: reporter [RNTST] Running test reg_backdoor_test...
UVM_WARNING /playground_lib/uvm-1.2/src/base/uvm_resource.svh(1416) @ 0: reporter [UVM/RSRC/NOREGEX] a resource with meta characters in the field name has been created "m_ral_model.cfg"
UVM_INFO env.sv(215) @ 0: uvm_test_top.m_env.m_agent.m_seqr@@m_reset_seq [RESET] Running reset ...
UVM_INFO test.sv(58) @ 110: uvm_test_top [reg_backdoor_test] desired=0x12345678 mirrored=0x12345678
UVM_INFO test.sv(63) @ 150: uvm_test_top [reg_backdoor_test] desired=0xa5a5a5a5 mirrored=0xa5a5a5a5
UVM_INFO test.sv(70) @ 191: uvm_test_top [reg_backdoor_test] desired=0xfaceface mirrored=0xfaceface
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 291: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---

The log and the waveform show three steps:

  • Front-door write, front-door read (until 110): 32'h1234_5678 is written over APB and read back through prdata.
  • Backdoor write, front-door read (110 to 150): 32'ha5a5_a5a5 appears in timer_1 at time 110 with no bus activity. The front-door read then returns it, which proves the backdoor write reached the RTL.
  • Front-door write, backdoor read (150 to 191): 32'hface_face is written over APB, and the backdoor read at 191 returns it in zero time. The #1 delay lets the RTL register update before it is sampled.

In every step, the desired and mirrored values match the value in the DUT, because the model updates its mirror after backdoor accesses as well.

Where is a Backdoor Read Used for Checking?

A common check writes a register through the front door and reads it back through the back door. This catches address decode bugs: if the write landed in the wrong register, the backdoor read of the intended register returns the old value, even if a front-door read would return the expected data from the wrong location.

Common Mistakes

Mistake #1: Incorrect HDL paths

If any part of the path is wrong, the backdoor access reports an error and does not reach the register. Check all paths early with the built-in sequence uvm_reg_mem_hdl_paths_seq, which verifies that every HDL path in the model exists in the design.

Mistake #2: Compiling without signal access enabled

Backdoor accesses rely on the simulator's ability to read and deposit internal signals. Many simulators restrict this by default for performance. Enable read and write access to the register signals with the simulator's access or debug options, or backdoor accesses fail even with correct paths.

Mistake #3: Expecting a backdoor write to trigger bus side effects

Wrong: writing an interrupt-enable register through the back door and expecting the design's write-strobe logic to react
Right: use the front door for any register whose write must trigger hardware behavior

Why? A backdoor write only changes the stored value. Logic that is triggered by the bus write itself, such as write strobes, pulses or counters, never sees an access.

Practice Exercise

Challenge: Paths for a Deeper Hierarchy

In a new testbench, the timer register is at top_tb.u_soc.u_periph.u_timer.timer_1. The register model has a top block soc_regs containing the block periph, which contains the traffic block cfg. Which HDL path should each level add? Then write a check that writes timer[1] through the front door and verifies it through the back door.

Hint

Each block adds only its own part of the path, relative to its parent. The register slice "timer_1" does not change.

Solution

Paths: soc_regs adds "top_tb.u_soc", periph is configured with "u_periph", and cfg with "u_timer". Joined with the slice, this gives top_tb.u_soc.u_periph.u_timer.timer_1.


virtual task body ();
  uvm_status_e   status;
  uvm_reg_data_t rdata;

  m_regs.periph.cfg.timer[1].write (status, 32'h0BAD_C0DE);     // Front door
  #1;                                                          // Let the RTL register update
  m_regs.periph.cfg.timer[1].peek (status, rdata);              // Back door, raw value

  if (rdata != 32'h0BAD_C0DE)
    `uvm_error ("BD_CHK", $sformatf ("timer_1 holds 0x%0h, expected 0x0BADC0DE", rdata))
endtask

peek() is used here so that the check sees exactly what the hardware holds, without any field access policy applied.

Summary

In this tutorial, you learned:

  • Front-door accesses go through the bus and consume time, while backdoor accesses use HDL paths and take zero time
  • Backdoor writes change only the stored value, so bus-triggered side effects in the design do not happen
  • HDL paths are split by hierarchy level with add_hdl_path(), block configure() and add_hdl_path_slice()
  • write() and read() with UVM_BACKDOOR mimic front-door behavior, while poke() and peek() access the raw value
  • The register model updates its mirror after backdoor accesses as well
  • Check HDL paths early with uvm_reg_mem_hdl_paths_seq and make sure the simulator allows signal access
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5