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
      • The Access API: write, read, set, get, update, mirror, predict, peek, poke
      • The Adapter Contract: reg2bus, bus2reg and provides_responses
      • Prediction: Auto, Explicit and Passive
      • Field Access Policies and Callbacks
      • Volatile Fields and Mirror Checks
      • Backdoor Access: HDL Paths and User Backdoors
      • User-Defined Frontdoors: Indirect and Paged Registers
    • Registers in Verification
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Register Access
  1. Topics
  2. UVM Verification
  3. UVM Register Abstraction Layer (RAL)
  4. Register Access
  5. Backdoor Access: HDL Paths and User Backdoors

Part 9 · Register Model (RAL) · Intermediate

Backdoor Access: HDL Paths and User Backdoors

HDL paths and slices through the uvm_hdl DPI, the bench's user backdoor through a bound interface, what peek and poke predict, and the backdoor-versus-clock race.

On this page

    A chip-level test needs to load 4,096 coefficient words before the first frame, and the frontdoor takes 40 microseconds of simulated time per word through two bridges. A backdoor load takes zero. So the team writes HDL paths for every register, and it works on the RTL. Then the IP vendor delivers the encrypted netlist, every path lookup fails, and the regression goes red overnight. The register model was right; the way it reached the hardware was not portable.

    The idea: reach the flip-flops without the bus

    A backdoor access reads or writes a register's storage directly: no bus transfer, no adapter, no simulated time. It is the maintenance hatch at the back of a building, used for jobs the front desk would make slow: preloading memories, checking a value without disturbing bus timing, reading a register that has side effects on a frontdoor read, or setting up a state the frontdoor cannot reach quickly.

    There are two ways to build the hatch. HDL paths give each register the string name of its storage in the design, and the library reads and deposits through the uvm_hdl_* DPI routines. A user backdoor is a class derived from uvm_reg_backdoor that you write yourself, reaching the storage any way you like: a bound interface, a C model, a memory's own access task. Both end the same way: the result is predicted into the mirror.

    The picture: two routes to the same flip-flop

    Flowchart: a backdoor call checks for a user backdoor; if one is set it calls the bench's dma_reg_backdoor, which reads the bound dma_bd_if; otherwise it builds the full HDL path and uses the uvm_hdl DPI routines; both reach the DUT storage and the value is then predicted into the mirror.
    How the library chooses a backdoor route. A user backdoor set on the register, or inherited from a block, wins; otherwise the HDL paths are resolved and accessed through DPI. Either way the access ends with a prediction.

    Build it: HDL paths first

    With HDL paths, the block gets a root and each register gets one or more slices: a signal name, the bit offset where it lands in the register, and its width. A register stored in two places (say bits 15:0 in one flop and 31:16 in another) gets two slices. For the DMA it would look like this:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet: HDL paths for the DMA (not used by the bench, see below)
    // In the env, before regmodel.build(): the block's root in the design.
    regmodel.configure(null, "top.dut");        // or regmodel.add_hdl_path("top.dut")
    
    // In dma_reg_block::build(), one slice per stored signal: name, offset, size.
    key.add_hdl_path_slice("key",   0, 32);
    len.add_hdl_path_slice("len",   0, 16);
    count.add_hdl_path_slice("count", 0, 16);
    // STATUS is three flops in one register: three slices.
    status.add_hdl_path_slice("busy", 0, 1);
    status.add_hdl_path_slice("done", 1, 1);
    status.add_hdl_path_slice("err",  2, 1);

    The full path is the concatenation of every level's path: block roots, register files, then the slice name, so top.dut.key. A kind argument (default "RTL") lets one model carry several path sets, for example "GATES" for the netlist, chosen per access. uvm_reg::backdoor_read() and backdoor_write() then call uvm_hdl_read and uvm_hdl_deposit for each slice. Two more DPI routines exist for tests rather than the model: uvm_hdl_force and uvm_hdl_release hold a value against the design's own drivers.

    Three things break HDL paths, and each is a reason the bench does something else:

    • No DPI. The course's Verilator runs compile with UVM_NO_DPI, which turns every uvm_hdl_* routine into a fatal stub ("uvm_hdl DPI routines are compiled off"). Commercial simulators also need signal visibility switched on, and each lookup by string costs time.

    • Encrypted or synthesised RTL. Internal names are hidden or renamed, as in the opening story.

    • Storage that is not a plain register. A memory with ECC stores data plus check bits: a raw deposit of the data alone leaves the check bits wrong, and the first frontdoor read reports an ECC error. Scrambled or compressed storage has the same problem.

    The bench's way: a user backdoor through a bound interface

    The DMA bench binds a small read-only interface into every dma_dut, and the backdoor class reads through it. bind attaches the interface inside the DUT's scope without editing the design, so it sees key, len and count by their local names:

    dma_bd_if.sv
    1
    2
    3
    4
    5
    // snippet: from the course DMA bench
    // A read-only window into the DMA's internal registers, bound into every
    // dma_dut. The register model's backdoor reads through it.
    interface dma_bd_if (input logic [31:0] key, input logic [15:0] len, input logic [15:0] count);
    endinterface
    design.sv
    // snippet: the bind statement in dma_ral_tb.sv
    bind dma_dut dma_bd_if bd (.key(key), .len(len), .count(count));
    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
    // snippet: from the course DMA bench
    // A user-defined backdoor: reads KEY, LEN and COUNT straight from the DUT
    // through the bound dma_bd_if, with no bus traffic and no simulation time.
    // It needs no HDL-path DPI, so it works on every simulator. Writes are refused.
    class dma_reg_backdoor extends uvm_reg_backdoor;
      virtual dma_bd_if vif;
    
      function new(string name = "dma_reg_backdoor");
        super.new(name);
      endfunction
    
      virtual function void read_func(uvm_reg_item rw);
        uvm_reg r;
        if (!$cast(r, rw.element)) begin
          rw.status = UVM_NOT_OK;
          return;
        end
        rw.status = UVM_IS_OK;
        case (r.get_name())
          "key":   rw.value[0] = vif.key;
          "len":   rw.value[0] = vif.len;
          "count": rw.value[0] = vif.count;
          default: rw.status = UVM_NOT_OK;
        endcase
      endfunction
    
      virtual task write(uvm_reg_item rw);
        `uvm_warning("BD_RO", "this backdoor is read-only")
        rw.status = UVM_NOT_OK;
      endtask
    endclass

    Three choices in it are worth copying. It overrides read_func(), the zero-time function the library's read() task calls, so it never consumes time. It sets status on every path, including UVM_NOT_OK for a register it does not know. And it refuses writes with a warning, because the bound interface only has inputs: an honest backdoor says what it cannot do. The env attaches it to the three registers that have storage the interface can see:

    design.sv
    1
    2
    3
    4
    5
    6
    // snippet: from dma_ral_env in the course DMA bench (dma_ral_tb.sv)
    bd = new("bd");
    bd.vif = bd_vif;
    regmodel.key.set_backdoor(bd);
    regmodel.len.set_backdoor(bd);
    regmodel.count.set_backdoor(bd);

    The same pattern solves the other two problems. For encrypted IP, the vendor usually provides a debug port or a bound probe; a user backdoor wraps it. For an ECC memory, the backdoor's write computes the check bits before depositing and its read checks and corrects them, so the model sees data and the storage stays consistent. uvm_reg_backdoor also has is_auto_updated() and wait_for_change(), which let a backdoor watch storage and update the mirror whenever the hardware changes it; that is the one way to keep a volatile field's mirror live.

    peek, poke, and what they predict

    Call

    Reaches the DUT

    Applies field policies?

    Prediction afterwards

    peek(st, v)

    Reads

    No: an RC field is not cleared

    Mirror and desired = value read

    poke(st, v)

    Writes v as is

    No: a W1C bit gets the raw 1

    Mirror and desired = v

    read(st, v, UVM_BACKDOOR)

    Reads, then writes back if a read side effect applies

    Yes

    As a frontdoor read

    write(st, v, UVM_BACKDOOR)

    Reads, computes the policy result, writes it

    Yes

    As a frontdoor write

    Both peek() and poke() predict. In the 2020 library both skip the prediction when the status is UVM_NOT_OK, with a PREDICT_NOK warning; UVM 1.2's do_predict has no such check. Lesson 1's log shows the bench's backdoor refusing a poke() that way. Lesson 5 showed the cost of the prediction: a peek before a check overwrites the evidence.

    Watch it run

    The bench's job test ends by comparing KEY's mirror, built from the bus traffic, with a backdoor peek of the flip-flop:

    Diagram
    +UVM_TESTNAME=dma_ral_job_test
    UVM_INFO dma_ral_tb.sv(346) @ 425000: uvm_test_top [RAL] STATUS=2: done=0 err=0
    UVM_INFO dma_ral_tb.sv(355) @ 545000: uvm_test_top [RAL] KEY mirror=cafef00d, backdoor peek=cafef00d, status=UVM_IS_OK
    UVM_INFO dma_tb.sv(411) @ 645000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
    UVM_ERROR :    0
    UVM_FATAL :    0

    backdoor peek=cafef00d, status=UVM_IS_OK: the user backdoor read the DUT's KEY through dma_bd_if with no APB transfer, at the same simulation time as the line before it. This is the cheapest end-to-end check the register layer offers: if the frontdoor path (adapter, address map, predictor) had any error, the mirror and the peek would disagree. Note the order in the test: it saves the mirror into exp_key before the peek, and checks st too. peek() predicts, so a compare against get_mirrored_value() after the peek would compare the peeked value with itself and could never fail.

    What breaks: a backdoor that read the past

    Backdoor accesses take no time, and that is exactly what makes them race the clock. A scratch test for this lesson writes KEY through the frontdoor and peeks it on the very next line:

    Diagram
    scratch run: +UVM_TESTNAME=s_bd_race_test
    UVM_INFO sacc_tb.sv(434) @ 75000: uvm_test_top [RACE] peek right after write() returns: 00000000 (mirror now 00000000)
    UVM_INFO sacc_tb.sv(437) @ 75000: uvm_test_top [RACE] peek after #0:                    00000000
    UVM_INFO sacc_tb.sv(439) @ 76000: uvm_test_top [RACE] 1ns later, before peeking: mirror cafe0001
    UVM_INFO sacc_tb.sv(441) @ 76000: uvm_test_top [RACE] peek 1ns later:                   cafe0001 (mirror now cafe0001)

    The write had returned, yet the peek read 0. The APB driver returns on the clock edge where the DUT samples the write, and the DUT's key <= pwdata is a nonblocking assignment: it lands in the NBA region of that time step, after the test's next statements have run. #0 does not help, because it only moves to the inactive region, still before NBA. One nanosecond later the flop holds the new value. The peek also predicted the stale 0 into the mirror; the explicit predictor overwrote it moments later, but code that read the mirror in between would have seen 0.

    Writes race the same way in reverse. A poke() that deposits on an active clock edge can be overwritten by the design's own flop update in the same time step, so the deposit seems to vanish. The rules: do backdoor accesses away from the active edge (after the edge, once NBAs have settled, or on the opposite edge), never immediately after a frontdoor access to the same register, and use force and release (not deposit) for a value the design must not change.

    Interview

    "When would you write a user-defined backdoor instead of using HDL paths?"

    A weak answer: "When the HDL path is too long."

    A strong answer: "HDL paths need DPI and signal visibility and plain storage with stable names. I write a uvm_reg_backdoor subclass when any of those fail: encrypted or synthesised IP, a simulator flow without the uvm_hdl DPI, or storage that needs translation, such as an ECC memory whose check bits must be computed on a write. Our DMA bench binds a read-only interface into the DUT and overrides read_func(), so it works on every simulator, and it refuses writes with UVM_NOT_OK. Either way the access is predicted into the mirror, so I keep backdoor reads away from checks and away from the active clock edge."

    Follow-ups: Does `peek()` change the mirror? (Yes; it predicts the value read.) What is the difference between `poke()` and `write(..., UVM_BACKDOOR)`? (poke() deposits the raw value; the backdoor write() applies the field policies first.)

    Check yourself A sequence pokes 1 into a W1C interrupt bit to set up an interrupt test, then calls write(st, 1, UVM_BACKDOOR) to clear it. What does each do to the DUT and the mirror?

    The poke() deposits the raw 1, so the bit is set in the DUT, and the mirror predicts 1. The backdoor write() applies the W1C policy: it reads 1, computes 1 & ~1 = 0 and deposits 0, clearing the bit, and the mirror predicts 0. The poke is the only way to set a W1C bit from software.

    Check yourself Your HDL-path backdoor works on RTL but every peek fails on the gate-level netlist. Name two fixes that keep the same register model.

    Add a second path set with kind = "GATES" (the netlist's names) and select it per access, or attach a user backdoor that reaches the storage another way (a bound probe or the vendor's debug port). Both keep the model; only the route changes.

    Check yourself Why does the bench's backdoor override read_func() and not read()?

    read() is a task that calls do_pre_read(), read_func() and do_post_read(). Overriding the zero-time function keeps the pre- and post-read callbacks working and guarantees that a backdoor read never consumes simulation time.

    Next: the backdoor reached registers without the bus. User-Defined Frontdoors reaches registers the bus cannot address directly: indirect tables, paged banks and serial devices.

    Related topics

    • Up: Register Access

    • Monitor sampling and races

    • DPI-C models

    Previous

    159 / 280

    Next: User-Defined Frontdoors: Indirect and Paged Registers

    Up next

    Up nextUser-Defined Frontdoors: Indirect and Paged Registers

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Access API: write, read, set, get, update, mirror, predict, peek, poke
    • The Adapter Contract: reg2bus, bus2reg and provides_responses
    • Prediction: Auto, Explicit and Passive