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.

RegisterCPU mapDebug map
chip.u0.ctrl0x0000x00
chip.u0.scale0x0040x04
chip.u1.ctrl0x1000x10
chip.u1.scale0x1040x14

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.

registeroffsetfieldbitsaccessreset
CTRL0x0mode7:0RW0x01
SCALE0x4factor3:0RW0x1

slice.csv, the specification of one slice, with two bus interfaces: cpu and dbg.

What the generator writes for the table above, abridged from chip_bench.svh. The block does not call lock_model: whoever instantiates it does.
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
endclass

Treat 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

Two maps reach the same two slicesA topology diagram, not a call order. Arrows from a sequencer are bus bindings made with set_sequencer; arrows from a root map are submap references, labelled with the offset at which each slice's map is placed. The registers themselves exist once, inside u0 and u1. The moving tokens follow two accesses over those bindings: out from the register through a map to its sequencer, and the result back.
Two maps reach the same two slicesA topology diagram, not a call order. Arrows from a sequencer are bus bindings made with set_sequencer; arrows from a root map are submap references, labelled with the offset at which each slice's map is placed. The registers themselves exist once, inside u0 and u1. The moving tokens follow two accesses over those bindings: out from the register through a map to its sequencer, and the result back. component CPU sequencer +driver set_sequencer oncpu_map component Debug sequencer+ driver set_sequencer ondbg_map map chip.cpu_map Root map, base 0 map chip.dbg_map Root map, base 0 model u0: slice_regs ctrl, scale; maps cpuand dbg model u1: slice_regs ctrl, scale; maps cpuand dbg bus forbus foru0.cpu at 0x000u1.cpu at 0x100u0.dbg at 0x00u1.dbg at 0x10
  1. 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
    1. call u1.ctrl.write(st, 5a): u1: slice_regs → chip.cpu_map
    2. data CPU write 100 <- 5a: chip.cpu_map → CPU sequencer + driver
    3. return write done, mirror = 5a: CPU sequencer + driver → chip.cpu_map
    4. return write done, mirror = 5a: chip.cpu_map → u1: slice_regs
  2. 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
    1. call u1.ctrl.read(..., chip.dbg_map): u1: slice_regs → chip.dbg_map
    2. data DBG read 010: chip.dbg_map → Debug sequencer + driver
    3. Waiting: u1: slice_regs — inside read() on dbg_map
  3. The read returns60 ns (illustrative)

    The value comes back through the debug map. read returns 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
    1. return v = 5a, mirror = 5a: Debug sequencer + driver → chip.dbg_map
    2. return v = 5a, mirror = 5a: chip.dbg_map → u1: slice_regs
    3. Released: u1: slice_regs
  4. 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?

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 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 : 0
Read 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

Predict the outputThe chip model from the example: u0 and u1 at 0x000 and 0x100 in the CPU map, at 0x00 and 0x10 in the debug map; inside a slice CTRL is at 0x0 and SCALE at 0x4. What line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "chip_bench.svh"

// The chip's block: two generated slices, each reachable from both ports.
class chip_regs extends uvm_reg_block;
  `uvm_object_utils(chip_regs)
  rand slice_regs u0, u1;
  uvm_reg_map     cpu_map, dbg_map;
  function new(string name = "chip_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    u0 = slice_regs::type_id::create("u0");  u0.configure(this);  u0.build();
    u1 = slice_regs::type_id::create("u1");  u1.configure(this);  u1.build();
    cpu_map = create_map("cpu_map", 'h0, 4, UVM_LITTLE_ENDIAN);
    dbg_map = create_map("dbg_map", 'h0, 4, UVM_LITTLE_ENDIAN);
    cpu_map.add_submap(u0.cpu, 'h000);
    cpu_map.add_submap(u1.cpu, 'h100);
    dbg_map.add_submap(u0.dbg, 'h00);
    dbg_map.add_submap(u1.dbg, 'h10);
    default_map = cpu_map;
    lock_model();
  endfunction
endclass

class lesson_test extends uvm_test;
  `uvm_component_utils(lesson_test)
  chip_regs                 chip;
  apb_adapter               adapter;
  apb_driver                cpu_drv, dbg_drv;
  uvm_sequencer #(apb_item) cpu_sqr, dbg_sqr;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction

  virtual function void build_phase(uvm_phase phase);
    chip = chip_regs::type_id::create("chip");
    chip.build();
    adapter = apb_adapter::type_id::create("adapter");
    cpu_sqr = uvm_sequencer #(apb_item)::type_id::create("cpu_sqr", this);
    dbg_sqr = uvm_sequencer #(apb_item)::type_id::create("dbg_sqr", this);
    cpu_drv = apb_driver::type_id::create("cpu_drv", this);
    dbg_drv = apb_driver::type_id::create("dbg_drv", this);
    cpu_drv.port = "CPU";
    dbg_drv.port = "DBG";
    if (!uvm_config_db #(virtual apb_if)::get(this, "", "cpu_vif", cpu_drv.vif)) `uvm_fatal("NOVIF", "cpu_vif")
    if (!uvm_config_db #(virtual apb_if)::get(this, "", "dbg_vif", dbg_drv.vif)) `uvm_fatal("NOVIF", "dbg_vif")
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    cpu_drv.seq_item_port.connect(cpu_sqr.seq_item_export);
    dbg_drv.seq_item_port.connect(dbg_sqr.seq_item_export);
    chip.cpu_map.set_sequencer(cpu_sqr, adapter);     // each map has its own bus
    chip.dbg_map.set_sequencer(dbg_sqr, adapter);
    chip.cpu_map.set_auto_predict(1);
    chip.dbg_map.set_auto_predict(1);
  endfunction

  function void ok(uvm_status_e st, string what);
    if (st != UVM_IS_OK) `uvm_error("STATUS", {what, " returned ", st.name()})
  endfunction

  virtual function void end_of_elaboration_phase(uvm_phase phase);
    uvm_reg r = chip.cpu_map.get_reg_by_offset('h104);
    $display("CPU 104 is %s, debug address %0h", r.get_full_name(), r.get_address(chip.dbg_map));
  endfunction
endclass
Read 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
Spot the bugThe chip block places each slice's maps in its two root maps. The design decodes the debug port as described in the example. The test runs uvm_reg_mem_shared_access_seq on the chip. Which placement fails?
Read 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
Conceptu1.ctrl is in both the CPU map and the debug map, with auto prediction on in both. After a CPU-map write of 5a, what does a debug-map mirror(st, UVM_CHECK) compare the read data against?
ConceptThe generated slice model is missing a quirk of the real hardware. What is the maintainable fix?

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) and get_reg_by_offset convert both ways.
  • One register has one mirror, however many maps reach it; each root map has its own sequencer and prediction.
  • Pass the map argument to use a second port, and run uvm_reg_mem_shared_access_seq on an idle design to check that every port reaches the same storage.