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. User-Defined Frontdoors: Indirect and Paged Registers

Part 9 · Register Model (RAL) · Intermediate

User-Defined Frontdoors: Indirect and Paged Registers

uvm_reg_frontdoor and set_frontdoor for registers without an address of their own: an index-plus-data table, uvm_reg_indirect_data, prediction and locking pitfalls, and serial access.

On this page

    The next block on your chip has 64 filter coefficients but only two APB addresses: write a coefficient number to IDX at 0x00, then read or write the coefficient through DATA at 0x04. The test writers want to say rm.coef[17].write(st, v) like any other register, and they want mirror checks on the coefficients. The map cannot help: a register map turns a register into one address, and these registers have none. Something has to know the two-step recipe.

    The idea: a recipe for one register access

    A user-defined frontdoor replaces the map's built-in "one register, one bus transfer" step with a sequence you write. When a register has a frontdoor, write() and read() hand the whole access to it: the frontdoor receives the abstract access in rw_info (kind, value, status), issues whatever bus transfers it takes, and fills in the status and read data. Think of a letter to someone in a large office: the post knows only the building's address, so the envelope says "care of reception, attention room 17". The frontdoor is the reception desk that knows how to deliver it.

    uvm_reg_frontdoor is a uvm_reg_sequence, so its body() can do anything a sequence can: start bus items on its sequencer (the map fills that in if you leave it null), call other registers' write()/read() with itself as the parent, or start items on a completely different agent. You attach it with rg.set_frontdoor(fd, map), or pass it to map.add_reg(rg, offset, rights, unmapped, fd). An unmapped register has no address of its own and can be reached only through its frontdoor (or a backdoor).

    Note

    A field write on a register with a user frontdoor always becomes a whole-register access: the library will not split it, and warns that individual field access is not available.

    The picture: one indirect write

    Sequence diagram: the test writes tbl_a[2], the register starts its frontdoor, the frontdoor writes IDX with 2 and DATA with the value, the predictor observes both transfers but can only predict the registers at those addresses, and the status returns to the test.
    A write to an unmapped table register through a user frontdoor. The bus sees two ordinary transfers; the explicit predictor can only map them back to registers that have addresses.

    Build it: an IDX/DATA table on a scratch slave

    The DMA has no indirect registers, so a scratch test for this lesson adds a tiny APB slave with IDX at 0x00 and a four-word table behind DATA at 0x04, driven by a second instance of the bench's unchanged apb_agent and apb_reg_adapter:

    ind_slave.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // snippet: the scratch indirect slave (zero wait states, no errors)
    module ind_slave (apb_if bus);
      logic [1:0]  idx;
      logic [31:0] tbl [4];
      assign bus.pready  = 1'b1;
      assign bus.pslverr = 1'b0;
      always_comb bus.prdata = (bus.paddr == 8'h00) ? {30'b0, idx} : tbl[idx];
      always_ff @(posedge bus.clk or negedge bus.rst_n)
        if (!bus.rst_n) begin
          idx <= 0; foreach (tbl[i]) tbl[i] <= 0;
        end else if (bus.psel && bus.penable && bus.pwrite) begin
          if (bus.paddr == 8'h00) idx <= bus.pwdata[1:0];
          else if (bus.paddr == 8'h04) tbl[idx] <= bus.pwdata;
        end
    endmodule

    The frontdoor knows its table index and issues two APB items: select, then access. Note that it reports status from both transfers; a frontdoor that forgets to set rw_info.status returns whatever the item held before, and a failed select would go unnoticed:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // snippet: verified in a scratch run with the DMA bench's APB agent
    class idx_data_frontdoor extends uvm_reg_frontdoor;
      `uvm_object_utils(idx_data_frontdoor)
      int unsigned index;
      function new(string name = "idx_data_frontdoor"); super.new(name); endfunction
      virtual task body();
        apb_item sel = apb_item::type_id::create("sel");
        apb_item acc = apb_item::type_id::create("acc");
        start_item(sel);
        sel.addr = 8'h00; sel.write = 1; sel.data = index;
        finish_item(sel);
        start_item(acc);
        acc.addr = 8'h04; acc.write = (rw_info.kind == UVM_WRITE); acc.data = rw_info.value[0];
        finish_item(acc);
        if (rw_info.kind == UVM_READ) rw_info.value[0] = acc.rdata;
        rw_info.status = (sel.slverr || acc.slverr) ? UVM_NOT_OK : UVM_IS_OK;
      endtask
    endclass

    The scratch block models the table twice, to compare the two ways the library offers. tbl_a[] uses the frontdoor above, added as unmapped registers. tbl_b[] uses the library's ready-made `uvm_reg_indirect_data`: a register at the DATA address that is configured with the IDX register and the table, and that installs its own frontdoors on the table registers when it is added to the map:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // snippet: from the scratch ind_block::build()
    idx = ind_idx_reg::type_id::create("idx");
    idx.configure(this); idx.build();
    foreach (tbl_b[i]) begin                       // the library's way: configure, do not map
      tbl_b[i] = ind_word_reg::type_id::create($sformatf("tbl_b%0d", i));
      tbl_b[i].configure(this); tbl_b[i].build();
      regs_b[i] = tbl_b[i];
    end
    data = new("data", 32, UVM_NO_COVERAGE);       // uvm_reg_indirect_data
    data.configure(idx, regs_b, this);
    map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    map.add_reg(idx,  'h00, "RW");
    map.add_reg(data, 'h04, "RW");                 // adds tbl_b[] as unmapped, with frontdoors
    foreach (tbl_a[i]) begin                       // the hand-written way
      idx_data_frontdoor fd = idx_data_frontdoor::type_id::create($sformatf("fd%0d", i));
      fd.index = i;
      tbl_a[i] = ind_word_reg::type_id::create($sformatf("tbl_a%0d", i));
      tbl_a[i].configure(this); tbl_a[i].build();
      map.add_reg(tbl_a[i], 'h0, "RW", 1, fd);     // unmapped: reached only through fd
    end
    lock_model();

    uvm_reg_indirect_data also marks itself NO_REG_TESTS, since a bit-bash of the DATA address alone is meaningless, and it refuses set(), get(), peek() and poke() on itself: it is a window, not storage.

    Watch it run

    The test writes tbl_a[2] with explicit prediction only, reads it back both ways, mirror-checks it, and finally switches the map to auto prediction for one more write. Each line prints the mirrors of idx, tbl_a[2], tbl_b[2] (the same hardware word, modelled the library's way) and tbl_a[3]:

    Diagram
    scratch run: +UVM_TESTNAME=s_indirect_test (explicit prediction; auto prediction switched on before the last write)
    UVM_INFO sacc_tb.sv(382) @ 96000: uvm_test_top [IND] tbl_a[2].write (UVM_IS_OK) idx=2 tbl_a2=0        tbl_b2=5555aaaa tbl_a3=0
    UVM_INFO sacc_tb.sv(391) @ 156000: uvm_test_top [IND] tbl_a[2].read = 5555aaaa
    UVM_INFO sacc_tb.sv(393) @ 216000: uvm_test_top [IND] tbl_b[2].read = 5555aaaa
    UVM_ERROR reg/uvm_reg.svh(2812) @ 275000: reporter [RegModel] Register "rm.tbl_a2" value read from DUT (0x000000005555aaaa) does not match mirrored value (0x0000000000000000) (valid bit mask = 0x00000000ffffffff)
    UVM_INFO reg/uvm_reg.svh(2833) @ 275000: reporter [RegModel] Field value (rm.tbl_a2[31:0]) mismatch read=32'h5555aaaa mirrored=32'h0
    UVM_INFO sacc_tb.sv(382) @ 276000: uvm_test_top [IND] tbl_a[2].mirror(CHECK)     idx=2 tbl_a2=0        tbl_b2=5555aaaa tbl_a3=0
    UVM_INFO sacc_tb.sv(382) @ 336000: uvm_test_top [IND] auto: tbl_a[3].write       idx=3 tbl_a2=0        tbl_b2=5555aaaa tbl_a3=beef0003
    UVM_ERROR :    1
    • The frontdoor works. The write went out as IDX=2, DATA=5555AAAA, and both reads, through either model, return 5555AAAA from the hardware.

    • `tbl_a2=0`, but `tbl_b2=5555aaaa`. The predictor saw a write to 0x04. That address belongs to data, the uvm_reg_indirect_data, which routes the prediction to the table entry selected by the IDX mirror: tbl_b[2]. Nothing in the model maps any address to tbl_a[2], so its mirror never moved.

    • The mirror check fails on a correct DUT: the read returned 5555AAAA, the mirror expected 0. (A failing check normally repairs the mirror through its read's prediction. Here that read, too, was predicted into tbl_b[2], so tbl_a2 still shows 0.)

    • With auto prediction on, `tbl_a3=beef0003` at once. The map predicts the register the test called, whatever addresses its frontdoor used.

    What breaks: a frontdoor the predictor cannot follow

    The log is the war story. A hand-written frontdoor plus explicit-only prediction leaves the indirect registers' mirrors frozen at their reset values, and the first mirror check on them fails on a working design. Teams then often "fix" the check rather than the model. The real fixes:

    • Use `uvm_reg_indirect_data` for a plain index-plus-data scheme. The stock uvm_reg_predictor recognises it and predicts the selected entry, so explicit prediction keeps working, including for traffic from other masters.

    • Enable auto prediction for the map, as the last line of the log does, if only your register model ever touches the table.

    • Extend the predictor for schemes the library does not know (paged banks, a page register in another block): track the selector in the predictor and predict the right entry from each DATA transfer.

    A second trap is concurrency. The library serializes accesses per register (each register holds its own semaphore while an access is in progress), but every table register is a different register with its own frontdoor object. Two threads writing tbl_a[1] and tbl_a[2] can interleave as IDX=1, IDX=2, DATA, DATA, and both values land in entry 2. uvm_reg_indirect_data's frontdoor locks the IDX and DATA registers for the access; a hand-written one needs a shared lock:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: one semaphore shared by every frontdoor of the same IDX/DATA pair
    class idx_data_frontdoor_safe extends idx_data_frontdoor;
      `uvm_object_utils(idx_data_frontdoor_safe)
      static semaphore pair_lock = new(1);
      function new(string name = "idx_data_frontdoor_safe"); super.new(name); endfunction
      virtual task body();
        pair_lock.get(1);
        super.body();
        pair_lock.put(1);
      endtask
    endclass

    Serial registers: the same recipe on another agent

    A sensor behind I2C or SPI is the same problem in a different shape: each register access is a serial transaction (start, device address, register number, data, stop) on an agent that is not the APB agent. One option is a separate map with its own adapter on the serial agent's sequencer, if one register access is one serial item. When it is not, for example when a read needs a write of the register number and then a repeated-start read, a frontdoor on the APB-side model can drive the serial agent directly:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // snippet: a frontdoor that reaches a register over I2C (i2c_item and its agent are illustrative)
    class i2c_reg_frontdoor extends uvm_reg_frontdoor;
      `uvm_object_utils(i2c_reg_frontdoor)
      bit [6:0] dev_addr;
      bit [7:0] reg_num;
      function new(string name = "i2c_reg_frontdoor"); super.new(name); endfunction
      virtual task body();
        i2c_item it = i2c_item::type_id::create("it");
        start_item(it);                               // runs on fd.sequencer: set it to the I2C sequencer
        it.dev_addr = dev_addr;
        it.reg_num  = reg_num;
        it.read     = (rw_info.kind == UVM_READ);
        it.data     = rw_info.value[0];
        finish_item(it);
        if (it.read) rw_info.value[0] = it.data;
        rw_info.status = it.nack ? UVM_NOT_OK : UVM_IS_OK;
      endtask
    endclass
    // fd.sequencer = env.i2c_agt.sqr;  rm.sensor_cfg.set_frontdoor(fd, rm.map);

    Set fd.sequencer explicitly, because the map would otherwise fill in its own (APB) sequencer. And remember the prediction question from the war story: an APB-side predictor never sees I2C traffic, so either predict from an I2C monitor with its own predictor and map, or rely on auto prediction.

    Interview

    "How would you model an indirect register bank, where software writes an index and then accesses a data register?"

    A weak answer: "Model only IDX and DATA, and let the test do the two writes."

    A strong answer: "I want each entry addressable by name, so each is a uvm_reg with no address of its own. For a plain index-plus-data scheme I use uvm_reg_indirect_data: it installs frontdoors that write the index then access the data, and the stock predictor knows it, so explicit prediction routes each observed DATA transfer to the selected entry. For anything unusual, such as paging, I write a uvm_reg_frontdoor and attach it with set_frontdoor or add_reg(..., unmapped=1, fd). Then I must solve prediction myself, because the predictor cannot map bus addresses back to unmapped registers: auto prediction, or a predictor that tracks the index. And I lock the index/data pair across frontdoors so two threads cannot interleave."

    Follow-ups: Why can't you bit-bash DATA? (Each write lands in whichever entry IDX selects; uvm_reg_indirect_data sets NO_REG_TESTS on itself.) Can you do a field-level write through a user frontdoor? (No; the library falls back to a whole-register access.)

    Check yourself A frontdoor's body() issues both bus items correctly but never sets rw_info.status. What does a test see when the select write gets PSLVERR?

    Whatever status the uvm_reg_item held when the frontdoor started, normally UVM_IS_OK. The failed select goes unreported, the data access lands in the wrong entry, and the test's status check passes. A frontdoor owns the status: set it from every transfer it issued.

    Check yourself With explicit prediction only, which mirrors does one tbl_b[1].write() update in the scratch model, and why is tbl_b better off than tbl_a?

    idx (from the IDX write) and tbl_b[1]: the DATA write is predicted through uvm_reg_indirect_data, which uses the IDX mirror to pick the entry. tbl_a entries are unmapped registers with a hand-written frontdoor, and no address leads the predictor to them.

    Check yourself Why does the frontdoor in this lesson create a new apb_item for each transfer instead of reusing one?

    Each item is a separate request to the sequencer and driver, and the driver writes read data back into the request. Reusing one handle while the driver or a subscriber still holds it would corrupt the first transfer's results. New items per transfer is the same rule monitors follow.

    Next: with access, prediction and checks understood, Registers in Verification puts them to work: the built-in register sequences, register coverage, memories, multiple maps and the RAL interview set.

    Related topics

    • Up: Register Access

    • Nested blocks and submaps

    • The item and driver boundary

    Previous

    160 / 280

    Next: Registers in Verification

    Up next

    Up nextRegisters in Verification

    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