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
      • Why a Register Model: Desired, Mirrored and the DUT
      • Blocks, Registers, Fields and Memories
      • Maps, Offsets and Endianness
      • Nested Blocks and Submaps
      • Build, Lock and Generated Models
      • The Frontdoor Path, End to End
      • The Register Layer: Full Source
    • Register Access
    • Registers in Verification
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹RAL Overview
  1. Topics
  2. UVM Verification
  3. UVM Register Abstraction Layer (RAL)
  4. RAL Overview
  5. The Frontdoor Path, End to End

Part 9 · Register Model (RAL) · Intermediate

The Frontdoor Path, End to End

One STATUS read through the DMA's register layer, on the real log: test, register and map, adapter, sequencer and driver, bus, monitor, predictor and mirror, and the explicit-prediction lag that made the mirror one step behind.

On this page

    dma_ral_job_test waits for the DMA's interrupt, reads STATUS by name, and prints the value and the DONE bit's mirror on one line. The log says STATUS=2: done=0. Two is DONE. The read returned it. And on the same line, at the same simulation time, the model says DONE is 0. Nothing is broken, and by the next line the model agrees. The question is what happened in between.

    This lesson follows one frontdoor access, that STATUS read, through every object it touches, with the real log beside each step. It is the one diagram of the register layer worth drawing from memory.

    The idea: a letter and its receipt

    A frontdoor access goes over the real bus, exactly as software would. The opposite, a backdoor access, reads or writes the RTL directly with no bus traffic and no simulation time; the DMA bench uses one to peek KEY. The frontdoor is slower, but it is the only path that proves the address decoder, the bus protocol and the register logic together.

    Think of posting a letter and getting a receipt. You hand the letter to the post office (the map), which addresses it and puts it in the right kind of envelope (the adapter) for the courier (the sequencer and driver). The courier delivers it and comes back to tell you it arrived. Separately, a clerk who watches every delivery (the monitor) writes it in the register of deliveries (the predictor updates the mirror). You learn the letter arrived before the clerk has written it down.

    The picture: one STATUS read

    Sequence diagram: the test calls status.read; the register and map ask the adapter for an APB read of 0x04 and send it through the sequencer to the driver, which runs the APB transfer; the DUT returns PRDATA 2; the driver completes the item, bus2reg turns it into data 2 and UVM_IS_OK, and the read returns 2 to the test, which prints done 0 because the mirror has not been updated. Then the monitor publishes the same transfer to the predictor, which uses bus2reg and get_reg_by_offset to find status and predicts the mirror, setting done to 1, at the same simulation time.
    A frontdoor read with explicit prediction. The read returns to the test before the monitor's copy of the same transfer reaches the predictor, so code that reads the mirror at once sees the old value.

    Build it: the two pieces you write

    Of all the objects in the diagram, you write two: the adapter and the env that wires everything. The map, the register and the predictor are library classes. The adapter translates between the library's generic uvm_reg_bus_op (kind, address, data, status) and your bus's sequence item. reg2bus builds an apb_item for the map to send; bus2reg turns a completed item, from the driver or from the monitor, back into an operation:

    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
    // Translates between register operations and APB items. The APB driver writes
    // read data and PSLVERR back into the request item, so there is no separate
    // response: provides_responses = 0.
    class apb_reg_adapter extends uvm_reg_adapter;
      `uvm_object_utils(apb_reg_adapter)
    
      function new(string name = "apb_reg_adapter");
        super.new(name);
        supports_byte_enable = 0;
        provides_responses   = 0;
      endfunction
    
      virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
        apb_item it = apb_item::type_id::create("it");
        it.addr  = rw.addr;
        it.write = (rw.kind == UVM_WRITE) || (rw.kind == UVM_BURST_WRITE);
        it.data  = rw.data;
        return it;
      endfunction
    
      virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
        apb_item it;
        if (!$cast(it, bus_item))
          `uvm_fatal("NOT_APB", "bus2reg got an item that is not an apb_item")
        rw.kind   = it.write ? UVM_WRITE : UVM_READ;
        rw.addr   = it.addr;
        rw.data   = it.write ? it.data : it.rdata;
        rw.status = it.slverr ? UVM_NOT_OK : UVM_IS_OK;   // a bus error is a status, not a fatal
      endfunction
    endclass

    provides_responses = 0 tells the map that the driver returns read data inside the request item, so the map calls bus2reg on the request once finish_item returns. A bus error (PSLVERR) becomes UVM_NOT_OK, a status, while an item of the wrong type is a wiring bug and stops the run. The full adapter contract, including byte enables and separate responses, is in Register Access.

    The env builds the model, the adapter and a uvm_reg_predictor #(apb_item), then connects three things: the map to the APB sequencer and the adapter, the predictor to the same map and adapter, and the APB monitor's analysis port to the predictor's bus_in. Auto-prediction is a config knob, off by default:

    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
    // snippet: from the course DMA bench
    // The block env plus the register layer: the model, the adapter on the APB
    // sequencer, and an explicit predictor fed by the APB monitor.
    class dma_ral_env extends dma_env;
      `uvm_component_utils(dma_ral_env)
      dma_reg_block                 regmodel;
      apb_reg_adapter               adapter;
      uvm_reg_predictor #(apb_item) predictor;
      dma_reg_backdoor              bd;
      virtual dma_bd_if             bd_vif;
      bit                           auto_predict;   // config_db knob, default 0
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        void'(uvm_config_db#(bit)::get(this, "", "auto_predict", auto_predict));
        regmodel = dma_reg_block::type_id::create("regmodel");
        regmodel.build();
        adapter   = apb_reg_adapter::type_id::create("adapter");
        predictor = uvm_reg_predictor #(apb_item)::type_id::create("predictor", this);
        if (!uvm_config_db#(virtual dma_bd_if)::get(this, "", "bd_vif", bd_vif))
          `uvm_fatal("NOVIF", "dma_bd_if not set for the register env")
        bd = new("bd");
        bd.vif = bd_vif;
        regmodel.key.set_backdoor(bd);
        regmodel.len.set_backdoor(bd);
        regmodel.count.set_backdoor(bd);
      endfunction
    
      function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        regmodel.map.set_sequencer(apb.sqr, adapter);
        // The explicit predictor updates the mirror from what the APB monitor sees.
        // That happens when the monitor publishes the transfer - in this bench a
        // moment after write()/read() return, in the same time step. Code that
        // reads the mirror the instant an access returns (uvm_reg_bit_bash_seq's
        // rg.get(), mirror()'s expected value) then sees the old value. Tests
        // that need it turn on auto_predict as well.
        regmodel.map.set_auto_predict(auto_predict);
        predictor.map     = regmodel.map;
        predictor.adapter = adapter;
        apb.ap.connect(predictor.bus_in);
      endfunction
    endclass

    With auto-prediction off, the access itself never touches the mirror. The predictor does, for every transfer the monitor sees: it runs bus2reg, looks the address up with get_reg_by_offset, and calls do_predict on that register, which applies each field's policy. Because it works from the bus, it also catches accesses the model did not make, such as a firmware sequence or another bus master. That is explicit prediction, and it is the robust default.

    Watch it run

    dma_ral_job_test at +UVM_VERBOSITY=UVM_HIGH shows each step of the diagram as a line, all at 425000 ps (paths shortened):

    Diagram
    UVM_INFO @ 425000: reporter [RegModel] Read  register via map regmodel.map: regmodel.status=0x2
    UVM_INFO dma_ral_tb.sv(346) @ 425000: uvm_test_top [RAL] STATUS=2: done=0 err=0
    UVM_INFO dma_tb.sv(151) @ 425000: uvm_test_top.env.apb.mon [APB_MON] RD addr=04 rdata=00000002
    UVM_INFO reg/uvm_reg_predictor.svh(253) @ 425000: uvm_test_top.env.predictor [REG_PREDICT] Observed READ transaction to register regmodel.status: value='h2
    • Read register ... status=0x2: the map's report that the frontdoor read completed. read() returns to the test here with v = 2.

    • STATUS=2: done=0: the test's own line. It printed DONE's mirror, which the predictor has not updated yet.

    • APB_MON RD addr=04: the monitor publishes the same transfer, later in the same time step.

    • REG_PREDICT ... value='h2: the predictor maps 0x04 to regmodel.status and predicts 2. The mirror now says DONE is 1.

    Writes go the same way. A little later the test clears DONE by writing 2 to STATUS, and the predictor applies the W1C policy: written 2, predicted 0.

    Diagram
    UVM_INFO @ 505000: reporter [RegModel] Wrote register via map regmodel.map: regmodel.status=0x2
    UVM_INFO dma_tb.sv(151) @ 505000: uvm_test_top.env.apb.mon [APB_MON] WR addr=04 data=00000002
    UVM_INFO reg/uvm_reg_predictor.svh(248) @ 505000: uvm_test_top.env.predictor [REG_PREDICT] Observed WRITE transaction to register regmodel.status: value='h2 : updated value = 'h0

    What breaks: code that reads the mirror too early

    The lag cost a day while this bench was built. dma_ral_bitbash_test first ran with explicit prediction only. uvm_reg_bit_bash_seq writes a pattern, then takes its expected value from rg.get() straight after write() returns, and reads the register back to compare. With the mirror still one step behind, every expected value was the previous pattern, and the sequence failed on every bit it bashed. The design was fine.

    The same lag broke the busy-LEN test the other way round. mirror(UVM_CHECK) takes its expected value from the mirror before it reads. The LEN=9 write had returned, but its prediction had not landed yet, so the check expected the old 4, read 4, and passed: a real mismatch hidden.

    Both tests now set the env's auto_predict knob, which makes the map predict inside write() and read() as soon as bus2reg returns, so the mirror is current when the call returns. The predictor stays connected, and both predict the same value. That double prediction is harmless for RW and W1C fields; for a toggle (W1T) field, predicting one write twice would toggle the mirror back, so a bench with such fields picks one mode. Code of your own that reads the mirror right after an access has two options: use the value the access returned, or wait until the predictor has run (the next clock edge is enough here) before reading the mirror.

    Interview

    "Trace a register write from the test to the mirror."

    A weak answer: "the test calls write, the adapter converts it, the driver drives it, and the mirror is updated." It skips the map, the monitor and the predictor, and so it cannot explain any failure.

    A strong answer: "write() sets the desired value and hands the access to the register's map, which finds the address and calls the adapter's reg2bus to make a bus item. The map starts that item on the sequencer it was given with set_sequencer, the driver runs the transfer, and when the item completes the map calls bus2reg for the status. With explicit prediction, the access does not touch the mirror: the bus monitor publishes the transfer, the predictor runs bus2reg, finds the register with get_reg_by_offset, and calls do_predict, which applies the field policies. That happens after write() returns, so reading the mirror at once gives the old value; auto-prediction updates it inside write() instead, but only sees accesses the model made."

    Follow-ups: Why prefer explicit prediction? (it sees every access on the bus, including other masters and firmware) and What if the adapter sets provides_responses but the driver never sends a response? (the map waits for a response that never comes, and the access hangs).

    Check yourself The predictor's bus_in is never connected to the APB monitor, and auto-prediction is off. A test writes KEY=0xCAFEF00D and then calls rm.key.mirror(st, UVM_CHECK). What happens?

    The write reaches the DUT, but nothing predicts it, so KEY's mirror stays at its reset value 0. The mirror check reads 0xCAFEF00D, expects 0, and reports a RegModel mismatch. The DUT is right; the missing connection is the bug. A mirror that never moves after writes points at the prediction path first.

    Check yourself In the job test, the STATUS read returns 2 but the line printed straight after it says done=0. Name two ways to print the DONE bit correctly.

    Use the returned value, v[1], which is what the bus carried. Or wait until the predictor has run, for example to the next clock edge, before reading get_mirrored_value(). Turning on auto-prediction would also work, but changes the env for one print.

    Check yourself A write gets PSLVERR from the DMA. The adapter reports UVM_NOT_OK. Does the mirror change?

    It depends on the prediction mode and the library. With auto-prediction, the register skips the prediction when the status is not OK. With the stock uvm_reg_predictor, every observed transfer goes to do_predict: in UVM 1.2 the mirror is updated anyway, and in IEEE 1800.2 do_predict skips the update and warns PREDICT_NOK. To keep the mirror unchanged in UVM 1.2, filter in pre_predict or in a custom predictor.

    Next: every arrow in the diagram has more to it. Register Access takes the access API, the adapter and the prediction modes apart, one at a time.

    Related topics

    • Register Access

    • The access API

    • Why a register model

    • Agent architecture

    Previous

    152 / 280

    Next: The Register Layer: Full Source

    Up next

    Up nextThe Register Layer: Full Source

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why a Register Model: Desired, Mirrored and the DUT
    • Blocks, Registers, Fields and Memories
    • Maps, Offsets and Endianness