The DMA ships in a chip with two instances, at 0x0000 and 0x1000. The integration team asks for the block's register tests at chip level by Friday, and someone starts copying dma_ral_reset_test and adding 0x1000 to every address. A senior engineer stops them: the block's model, the block's sequences and the block's tests should run unchanged. Only the chip model is new, and it is about thirty lines.
This lesson composes block models into a chip model, relocates a block at run time, reuses block register sequences at chip level, keeps a model with a hundred thousand registers fast to build, and keeps generated models honest in CI. It also shows a chip-level bug that every register test passed: a write to one DMA that the model credited to the other.
A house number is fixed by the house; the street is fixed by the town plan. Move the house to another street and its number does not change, only its full address does. A block model is the house: CTRL is at offset 0x00 of the DMA whatever chip it is in. The chip model is the town plan: it places each block's map at a base address inside its own map (a submap). Addresses are resolved when the root model is locked, and again whenever a base moves, so nothing written against the block ever sees the chip's numbers.
Three consequences make reuse work. A register sequence takes a model handle (any uvm_reg_block), so pointing it at soc.dma1 runs it on that instance. The block's map has no sequencer of its own at chip level; accesses go through the root map's sequencer and adapter. And one predictor on the chip bus decodes every block, because get_reg_by_offset on the root map knows the full addresses.
The chip block creates each block model, configures it with the chip as parent, builds it, and adds the block's map to its own. It is verified in a scratch test that reuses dma_reg_block from the bench unchanged:
// snippet: scratch chip model
class soc_reg_block extends uvm_reg_block;
`uvm_object_utils(soc_reg_block)
rand dma_reg_block dma0, dma1;
uvm_reg_map soc_map;
function new(string name = "soc_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction
virtual function void build();
soc_map = create_map("soc_map", 'h0, 4, UVM_LITTLE_ENDIAN);
dma0 = dma_reg_block::type_id::create("dma0");
dma0.configure(this); // parent first: names become soc.dma0.*
dma0.build();
dma1 = dma_reg_block::type_id::create("dma1");
dma1.configure(this);
dma1.build();
soc_map.add_submap(dma0.map, 'h0000);
soc_map.add_submap(dma1.map, 'h1000);
lock_model(); // the root lock resolves every address
endfunction
endclassdma_reg_block::build() calls lock_model() itself, which is convenient for a block bench and questionable for a reusable block. It works here: a sub-block's lock does not build the chip's address maps (only the root's lock does), and the chip's lock_model() skips children that are already locked. Generated blocks usually leave locking to the top, and that is the safer habit. Configure before build matters for another reason: a block built before it has a parent behaves as a root model of its own.
At chip level, configure also takes an HDL path segment: dma0.configure(this, "u_dma0") with the chip block configured as configure(null, "tb_top.u_soc") makes every backdoor path in the instance resolve to tb_top.u_soc.u_dma0... without editing the block model. This course's runs use no HDL-path DPI, so that part is not shown running here.
// snippet: scratch env connect_phase
soc.soc_map.set_sequencer(apb.sqr, adapter); // the root map owns the bus
predictor.map = soc.soc_map; // decodes full chip addresses
predictor.adapter = adapter;
apb.ap.connect(predictor.bus_in);// snippet: scratch test run_phase
dma1.map.set_base_addr('h2000); // on a submap: the parent's set_submap_offset
seq.model = soc.dma0; // the block's reset sequence, unchanged
seq.start(null);set_base_addr on a submap calls the parent's set_submap_offset, and when the model is locked the root map rebuilds its address tables, so get_address() and the predictor's decoding follow at once. Block-level register sequences of your own get reuse for free if they follow the same convention: take a uvm_reg_block handle (or the typed block), never a hard-coded address, and never assume the block is the root.
scratch run, Verilator
UVM_INFO scratch.sv(755) @ 0: uvm_test_top [SOC] dma0.len @8 dma1.len @1008 default_map of dma1: soc.dma1.map
UVM_INFO scratch.sv(760) @ 0: uvm_test_top [SOC] after dma1.map.set_base_addr(2000): dma1.len @2008; soc_map offset 2008 -> soc.dma1.len
UVM_INFO reg/sequences/uvm_reg_hw_reset_seq.svh(153) @ 0: reporter@@seq [uvm_reg_hw_reset_seq] Verifying reset value of register soc.dma0.ctrl in map "soc.dma0.map"...
UVM_INFO reg/sequences/uvm_reg_hw_reset_seq.svh(153) @ 75000: reporter@@seq [uvm_reg_hw_reset_seq] Verifying reset value of register soc.dma0.len in map "soc.dma0.map"...
UVM_INFO reg/sequences/uvm_reg_hw_reset_seq.svh(153) @ 115000: reporter@@seq [uvm_reg_hw_reset_seq] Verifying reset value of register soc.dma0.key in map "soc.dma0.map"...
UVM_INFO scratch.sv(764) @ 155000: uvm_test_top [SOC] hw reset sequence passed on soc.dma0 through the SoC map
UVM_ERROR : 0dma1.len at 0x1008, then 0x2008: the block offset 0x08 never changed; the base did, and the root map's decoding (offset 2008 -> soc.dma1.len) followed immediately.
The reset sequence reports map `soc.dma0.map` but its accesses went out through the root map's sequencer. It checked the same three registers as at block level, for the same reason (STATUS and COUNT are volatile).
The scratch bench has one DMA on the bus, so only dma0 is exercised by traffic; dma1 is there to show addressing, which is where the next section's bug lives.
The scratch test then wrote dma1's KEY through the chip model and printed both mirrors:
scratch run, Verilator
UVM_INFO scratch.sv(770) @ 215000: uvm_test_top [SOC] wrote dma1.key (@200c) = d1d1d1d1: dma1.key mirror=0, dma0.key mirror=d1d1d1d1The model asked for address 0x200C. The bench's apb_item has an 8-bit address, so reg2bus (it.addr = rw.addr) silently truncated it to 0x0C, and the APB bus carried a write to offset 0x0C. The predictor, decoding the monitored 0x0C with the chip map, found dma0.key and updated it. dma1's mirror never moved. Every block-level test passes on such a bench, and so does a chip-level reset test, because no address above 0xFF is ever compared. The fix belongs in the adapter, which is the only place that knows both widths:
// snippet: an adapter that refuses addresses its bus cannot carry
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_item it = apb_item::type_id::create("it");
if (rw.addr >> $bits(it.addr))
`uvm_error("ADDR_WIDTH", $sformatf("address %0h does not fit the %0d-bit bus", rw.addr, $bits(it.addr)))
it.addr = rw.addr;
it.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE});
it.data = rw.data;
return it;
endfunctionThe same family includes an interconnect that decodes fewer address bits than the chip map uses, so two blocks alias in hardware: the model says two registers, the silicon has one. uvm_reg_mem_shared_access_seq and an address-walk test (write a unique value to every register through the chip map, then read every one back) catch both before firmware does.
A chip model can hold tens of thousands of registers and hundreds of thousands of fields, each an object created through the factory at build time with its own configuration, policy string and value storage. The levers that matter, in order:
Lever | What it changes | Caveat |
|---|---|---|
| Both default to 64. | A compile-time |
Build only what the test uses | A config knob tells the chip block which sub-blocks to build; unused subsystems cost nothing | The address map must not depend on absent blocks, and nothing can be added after |
Coverage off by default | No covergroup objects, no sampling on every access | Turn it on per block for the tests that close register coverage |
One model per test, built once | Build in the env's | Never build a model per sequence or per agent |
Avoid per-access string work | Lookups such as | Resolve handles once, keep them in the sequence |
Measure before tuning: build the chip model alone in a tiny test and time it with the simulator's profiler. The fix for a 20-minute build is almost never in the test code.
At chip scale nobody writes register models by hand. One source (SystemRDL or IP-XACT) generates the RTL register file, the UVM model, C headers for firmware and the documentation. The model is only trustworthy if it stays generated from the same revision as the RTL:
Regenerate in CI, then diff. The job runs the generator from the source and fails if the checked-in model differs. Hand edits to generated files are caught the day they happen.
Pin the generator version. A generator upgrade that changes emitted code is a reviewed change, not a surprise in a nightly run.
Run the built-in sequences on every regenerated block. Reset and bit-bash on the block bench are the cheapest proof that the RTL and the model came from the same source.
Keep customisations outside the generated files. Callbacks, custom frontdoors and coverage extensions live in your own classes, attached by factory override or uvm_reg_field_cb::add, so regeneration never erases them.
soc.dma1.map.set_sequencer(...). Is that needed?No. A submap uses its root map's sequencer and adapter; accesses through soc.dma1.map go out on the chip bus through soc_map. Setting a sequencer on the submap is at best redundant. Configure the bus once, on the root map.
UVM_REG_DATA_WIDTH defined as 32 to save memory. One IP has a 64-bit timestamp register. What happens?lock_model() on the root reports a fatal: the model requires UVM_REG_DATA_WIDTH of at least the widest register or field, and it is checked once, when the root is locked. Either keep 64 bits for the whole compile, or model the timestamp as two 32-bit registers if the hardware allows two accesses.
The scratch bench ran the reset sequence on dma0 only, at base 0, where truncation changes nothing. Even run on dma1, it reads reset values, and both instances reset to the same values, so a read of the wrong DMA returns the expected number. Only a test that writes unique values per instance and reads them back, through the chip map, exposes the aliasing.
Next: you have now seen the register layer from the inside, at block and at chip level. RAL Interview Set turns it into the answers senior interviewers listen for.