Several maps, sub-blocks and generated models
Assemble generated register blocks into a chip-level model with an address map per bus interface, predict each register's address in every map, and check that all the ports reach the same storage.
- Lesson
- U14.4
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 4 questions
The chip has two copies of the slice and two ways in: the CPU bus and a debug port. A test writes slice 1's CTRL through the CPU and reads it back through the debug port. The value is wrong, yet every CPU-side test passes. The debug decoder chooses the slice from the wrong address bit, and nothing checked the second door.
| Register | CPU map | Debug map |
|---|---|---|
| chip.u0.ctrl | 0x000 | 0x00 |
| chip.u0.scale | 0x004 | 0x04 |
| chip.u1.ctrl | 0x100 | 0x10 |
| chip.u1.scale | 0x104 | 0x14 |
One register object, two addresses. The model computes each from the slice's own offsets plus the offset at which the chip placed the slice in that map.
From a specification to a model
Real register models are rarely written by hand. The registers are described once, in a spreadsheet, IP-XACT or SystemRDL file, and a generator writes the model, the RTL decoder, C headers and documentation from it. When the specification changes, everything is regenerated, so the model cannot drift from the design team's view.
| register | offset | field | bits | access | reset |
|---|---|---|---|---|---|
| CTRL | 0x0 | mode | 7:0 | RW | 0x01 |
| SCALE | 0x4 | factor | 3:0 | RW | 0x1 |
slice.csv, the specification of one slice, with two bus interfaces: cpu and dbg.
class slice_regs extends uvm_reg_block;
rand slice_ctrl ctrl; // one class per register type, one field per row
rand slice_scale scale;
uvm_reg_map cpu, dbg; // one map per bus interface
virtual function void build();
ctrl = slice_ctrl::type_id::create("ctrl"); ctrl.configure(this); ctrl.build();
...
cpu = create_map("cpu", 'h0, 4, UVM_LITTLE_ENDIAN);
cpu.add_reg(ctrl, 'h0, "RW");
cpu.add_reg(scale, 'h4, "RW");
dbg = create_map("dbg", 'h0, 4, UVM_LITTLE_ENDIAN);
dbg.add_reg(ctrl, 'h0, "RW");
dbg.add_reg(scale, 'h4, "RW");
default_map = cpu;
endfunction
endclassTreat generated files as read-only. Changes go into the specification, and bench-specific behaviour goes into subclasses selected by a factory override, or into callbacks such as U14.2's lock. A hand edit is lost at the next regeneration.
Sub-blocks and address arithmetic
- Write through the CPU map25 ns (illustrative)
chip.u1.ctrl.write(st, 'h5a)uses the default map, cpu_map: address 0x100 + 0x0 = 0x100, on the CPU sequencer. Auto prediction sets the mirror to 5a.More context
The log line "25 ns CPU write 100 <- 5a". The map adds u1's submap offset, 0x100, to CTRL's offset, 0x0.
Follow the calls, values and waits
- call u1.ctrl.write(st, 5a): u1: slice_regs → chip.cpu_map
- data CPU write 100 <- 5a: chip.cpu_map → CPU sequencer + driver
- return write done, mirror = 5a: CPU sequencer + driver → chip.cpu_map
- return write done, mirror = 5a: chip.cpu_map → u1: slice_regs
- Read through the debug map55 ns (illustrative)
read(st, v, UVM_FRONTDOOR, chip.dbg_map)computes 0x10 + 0x0 = 0x10 and uses the debug sequencer. The debug port answers 5a.More context
The log line "55 ns DBG read 010 -> 5a". The same storage answered on the other port.
Follow the calls, values and waits
- call u1.ctrl.read(..., chip.dbg_map): u1: slice_regs → chip.dbg_map
- data DBG read 010: chip.dbg_map → Debug sequencer + driver
- Waiting: u1: slice_regs — inside read() on dbg_map
- The read returns60 ns (illustrative)
The value comes back through the debug map.
readreturns 5a, and the mirror is refreshed to 5a.More context
The log line "60 ns u1.ctrl through the debug port -> 5a, mirror=5a".
Follow the calls, values and waits
- return v = 5a, mirror = 5a: Debug sequencer + driver → chip.dbg_map
- return v = 5a, mirror = 5a: chip.dbg_map → u1: slice_regs
- Released: u1: slice_regs
- One mirror
Both accesses update the same register object. This ordinary read refreshes its mirror with the value returned and compares nothing; the shared-access test below does the checked reads.
Read the diagram as text
- CPU sequencer + driver: set_sequencer on cpu_map
- Debug sequencer + driver: set_sequencer on dbg_map
- chip.cpu_map: Root map, base 0
- chip.dbg_map: Root map, base 0
- u0: slice_regs: ctrl, scale; maps cpu and dbg
- u1: slice_regs: ctrl, scale; maps cpu and dbg
- CPU sequencer + driver → chip.cpu_map: bus for
- Debug sequencer + driver → chip.dbg_map: bus for
- chip.cpu_map → u0: slice_regs: u0.cpu at 0x000
- chip.cpu_map → u1: slice_regs: u1.cpu at 0x100
- chip.dbg_map → u0: slice_regs: u0.dbg at 0x00
- chip.dbg_map → u1: slice_regs: u1.dbg at 0x10
A register has one mirror however many maps reach it. Each map has its own addresses and its own sequencer.
A sub-block is a uvm_reg_block inside another. The chip block creates u0 and u1, calls configure(this) and build() on each, then places each slice's map inside its own with add_submap(u1.cpu, 'h100). Finally the top block alone calls lock_model(), which computes every address.
In byte-addressed maps with the same bus width, like these, a register's address in a map is the sum of the offsets on the path from the root map. In the CPU map, 0x100 for u1 plus 0x4 for SCALE is 0x104. With different address units the library scales a submap's offset to the parent's units. get_address(map) returns the address, and map.get_reg_by_offset(addr) goes the other way. A map can only hold registers of its own block, so a chip map always reaches a sub-block's registers through that sub-block's map.
Choosing the map for each access
The frontdoor operations read, write, mirror and update take a map argument; peek and poke do not, because the backdoor uses no address. Leave the map out and the block's default_map is used, here the CPU map. Pass chip.dbg_map to go through the debug port. Each root map needs its own set_sequencer and its own prediction setting: auto prediction here, or a predictor per bus monitor at chip level, as in U13.3.
Use the second map in tests deliberately. A model with two maps lets you check that both ports reach the same storage. uvm_reg_mem_shared_access_seq does that for every register in at least two maps: it writes a random value through each map and compares the value read back through every map. It needs an idle design whose registers hold still for the whole test.
Checks only the default map
chip.u1.ctrl.write(st, 'h5a);
chip.u1.ctrl.mirror(st, UVM_CHECK);
Both accesses use the CPU port. A debug decoder that ignores address bit 4 goes unnoticed.
Crosses the ports
shared.model = chip;
shared.start(null); // every register, every pair of maps
A write through one map and a read through the other expose a decoder that picks the wrong slice.
Register files, arrays of registers and narrow fields
A uvm_reg_file groups registers inside a block without being a block itself: it has no map of its own, and each register keeps its offset in the block's map. Generators use it for repeated groups such as one set of registers per channel. An array of registers, like the table in U14.1, is simply an array of handles, each added at its own offset.
A field narrower than the bus can be accessed alone only if it was configured individually accessible and the bus supports byte enables. Otherwise the model reads or writes the whole register. Register data is limited by UVM_REG_DATA_WIDTH, 64 bits unless the bench defines it larger at compile time.
Try it
The first two lines are address arithmetic in both directions. Then one write goes out on the CPU port and an ordinary read comes back on the debug port; that read refreshes the same mirror but compares nothing. The last line counts the transfers shared_access made on each port; that test saves the expected value and compares every read.
Try it: in incr_chip, make the debug port choose the slice with dbg.paddr[5] instead of dbg.paddr[4]. The CPU-only checks would still pass; shared_access reports errors. Then think it through: a wrong debug read refreshes the mirror, so why could a later mirror(UVM_CHECK) against that refreshed mirror pass? Which value must you keep before the read?
0 ns u1.scale: CPU address 104, debug address 14 0 ns debug address 10 is chip.u1.ctrl 25 ns CPU write 100 <- 5a 55 ns DBG read 010 -> 5a 60 ns u1.ctrl through the debug port -> 5a, mirror=5a 780 ns after shared_access: CPU 5 writes 8 reads, debug 4 writes 9 reads, errors=0 UVM_ERROR : 0Read included file: chip_bench.svh
// chip_bench.svh: the APB bus, a chip with two register slices on two ports, and the
// register model generated for one slice. main.sv assembles the chip's model and runs the test.
// ---------------- APB bus (from U13; the adapter also accepts memory bursts) ----------------
interface apb_if (input logic clk);
logic psel = 0, penable = 0, pwrite = 0;
logic [31:0] paddr = 0, pwdata = 0;
logic [31:0] prdata;
logic pready;
endinterface
class apb_item extends uvm_sequence_item;
`uvm_object_utils(apb_item)
bit write;
bit [31:0] addr, data;
function new(string name = "apb_item"); super.new(name); endfunction
endclass
class apb_adapter extends uvm_reg_adapter;
`uvm_object_utils(apb_adapter)
function new(string name = "apb_adapter");
super.new(name);
supports_byte_enable = 0;
provides_responses = 0; // the driver writes read data into the request itself
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_item t = apb_item::type_id::create("t");
t.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE}); // a memory burst arrives as BURST_WRITE
t.addr = rw.addr;
t.data = rw.data;
return t;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_item t;
if (!$cast(t, bus_item)) `uvm_fatal("NOTAPB", "bus2reg needs an apb_item")
rw.kind = t.write ? UVM_WRITE : UVM_READ;
rw.addr = t.addr;
rw.data = t.data;
rw.status = UVM_IS_OK; // this small APB has no pslverr
endfunction
endclass
// One APB transfer per item. Counts transfers; prints each one while trace is 1.
class apb_driver extends uvm_driver #(apb_item);
`uvm_component_utils(apb_driver)
virtual apb_if vif;
string port = "APB";
bit trace = 1;
int n_wr, n_rd;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
@(negedge vif.clk); // setup phase
vif.psel <= 1; vif.penable <= 0;
vif.pwrite <= req.write; vif.paddr <= req.addr; vif.pwdata <= req.data;
@(negedge vif.clk); // access phase
vif.penable <= 1;
do @(posedge vif.clk); while (!vif.pready);
if (req.write) n_wr++;
else begin req.data = vif.prdata; n_rd++; end
if (trace && req.write) $display("%4d ns %s write %h <- %0h", $time, port, req.addr[11:0], req.data);
if (trace && !req.write) $display("%4d ns %s read %h -> %0h", $time, port, req.addr[11:0], req.data);
@(negedge vif.clk);
vif.psel <= 0; vif.penable <= 0;
seq_item_port.item_done();
end
endtask
endclass
// ---------------- The chip: two copies of the slice, two ports ----------------
// CPU port: slice s at 0x100 * s. Debug port: slice s at 0x10 * s.
// Inside a slice: CTRL.mode[7:0] at +0 (reset 01), SCALE.factor[3:0] at +4 (reset 1).
// The decoder looks at one address bit per port to choose the slice.
module incr_chip (apb_if cpu, apb_if dbg);
logic [7:0] mode [2] = '{8'h01, 8'h01};
logic [3:0] scale [2] = '{4'h1, 4'h1};
assign cpu.pready = 1;
assign dbg.pready = 1;
assign cpu.prdata = (cpu.paddr[3:0] == 0) ? {24'b0, mode[cpu.paddr[8]]}
: (cpu.paddr[3:0] == 4) ? {28'b0, scale[cpu.paddr[8]]} : 32'b0;
assign dbg.prdata = (dbg.paddr[3:0] == 0) ? {24'b0, mode[dbg.paddr[4]]}
: (dbg.paddr[3:0] == 4) ? {28'b0, scale[dbg.paddr[4]]} : 32'b0;
always @(posedge cpu.clk) begin
if (cpu.psel && cpu.penable && cpu.pwrite) begin
if (cpu.paddr[3:0] == 0) mode[cpu.paddr[8]] <= cpu.pwdata[7:0];
if (cpu.paddr[3:0] == 4) scale[cpu.paddr[8]] <= cpu.pwdata[3:0];
end
if (dbg.psel && dbg.penable && dbg.pwrite) begin
if (dbg.paddr[3:0] == 0) mode[dbg.paddr[4]] <= dbg.pwdata[7:0];
if (dbg.paddr[3:0] == 4) scale[dbg.paddr[4]] <= dbg.pwdata[3:0];
end
end
endmodule
// ---------------- Generated from slice.csv: do not edit ----------------
// One class per register, one per block, one map per bus interface of the block.
class slice_ctrl extends uvm_reg;
`uvm_object_utils(slice_ctrl)
rand uvm_reg_field mode;
function new(string name = "slice_ctrl"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
mode = uvm_reg_field::type_id::create("mode");
mode.configure(this, 8, 0, "RW", 0, 8'h01, 1, 1, 0);
endfunction
endclass
class slice_scale extends uvm_reg;
`uvm_object_utils(slice_scale)
rand uvm_reg_field factor;
function new(string name = "slice_scale"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
factor = uvm_reg_field::type_id::create("factor");
factor.configure(this, 4, 0, "RW", 0, 4'h1, 1, 1, 0);
endfunction
endclass
class slice_regs extends uvm_reg_block;
`uvm_object_utils(slice_regs)
rand slice_ctrl ctrl;
rand slice_scale scale;
uvm_reg_map cpu;
uvm_reg_map dbg;
function new(string name = "slice_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
virtual function void build();
ctrl = slice_ctrl::type_id::create("ctrl"); ctrl.configure(this); ctrl.build();
scale = slice_scale::type_id::create("scale"); scale.configure(this); scale.build();
cpu = create_map("cpu", 'h0, 4, UVM_LITTLE_ENDIAN);
cpu.add_reg(ctrl, 'h0, "RW");
cpu.add_reg(scale, 'h4, "RW");
dbg = create_map("dbg", 'h0, 4, UVM_LITTLE_ENDIAN);
dbg.add_reg(ctrl, 'h0, "RW");
dbg.add_reg(scale, 'h4, "RW");
default_map = cpu;
endfunction
endclass
// ---------------- end of generated code ----------------
module tb;
bit clk;
always #5 clk = ~clk;
apb_if cpu_bus (clk);
apb_if dbg_bus (clk);
incr_chip dut (cpu_bus, dbg_bus);
initial begin
uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "cpu_vif", cpu_bus);
uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "dbg_vif", dbg_bus);
run_test("lesson_test");
end
endmodule
Check yourself
Remember
- Generate register models from one specification and never edit the output; extend through overrides and callbacks.
- A chip block holds sub-blocks and places each sub-block's map with
add_submap(map, offset); only the top block locks the model. - In byte-addressed maps like these, a register's address is the sum of offsets from the root;
get_address(map)andget_reg_by_offsetconvert both ways. - One register has one mirror, however many maps reach it; each root map has its own sequencer and prediction.
- Pass the
mapargument to use a second port, and runuvm_reg_mem_shared_access_seqon an idle design to check that every port reaches the same storage.