Register-driven configuration and coverage
Program a design from a randomized register model in the order the design needs, reconfigure it safely while it runs, protect registers that share a window from concurrent threads, and collect register coverage.
- Lesson
- U14.3
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 4 questions
The design is running, and a test wants a new mode. It randomizes the model and calls regs.update(). The call returns UVM_IS_OK, but the design still runs the old mode, and a pending interrupt has disappeared. The update wrote CTRL while it was locked, and it wrote 1 to the write-1-to-clear IRQ, because that field is volatile.
| Step | GO | Model asks for mode | Design mode | IRQ.done | Writes |
|---|---|---|---|---|---|
| Configured in order | 1 | fe | fe | 0 | 11 |
| An event arrives | 1 | fe | fe | 1 | - |
Randomize, then block update() | 1 | 82 | Still fe | 0: cleared | 10 |
| Reconfigured in order | 1 | 0d | 0d | 0 | 11 |
Measured in the example below (default seed). After the block update the model's desired value is fe again: the request for 82 is gone. The update cost ten writes: it left the mode unchanged, rewrote the table entries and cleared the pending interrupt.
Configuration lives in the model
A register model is a natural home for a design's configuration. Its fields are rand, so regs.randomize() picks a legal configuration, and constraints in the register or block classes keep it legal. The desired values then hold the plan, and update writes every register whose desired value differs from its mirror.
Randomizing fills each rand field's value, and each field's post_randomize copies it into the desired value. regs.randomize() covers every register declared rand in the block; registers that should not change, like GO here, are simply not declared rand. The example randomizes CTRL and the four table entries one by one.
One update for the whole block
void'(regs.randomize());
regs.update(status);
Writes in the order the registers were created, including every volatile writable field, whatever state the design is in.
A sequence that knows the order
write_reg(r.go, st, 0); // stop
update_reg(r.ctrl, st); // configure
foreach (r.tbl[i]) update_reg(r.tbl[i], st);
write_reg(r.go, st, 1); // start
Stops the design first, writes only the configuration registers, then starts it.
Block update visits registers in creation order, the order of their instance IDs, so it is an implementation detail rather than a promise. Some flows shuffle the configuration registers on purpose to show that order does not matter. That is valid only when it really does not: here GO must come last, and IRQ must not be written at all.
Why the block update went wrong
CTRL is written while locked
The design ignores it. The lock callback from U14.2 keeps the mirror at the old mode, and prediction copies that old value into the desired value too. The request for the new mode is gone from the model.
IRQ is written although nobody asked
IRQ.done's desired value is still 0 from reset, while the design holds 1 from the event. A volatile writable field always reports
needs_update(), and for a W1C fieldupdatewrites the inverse of the desired value: 1, which clears the pending event.Every TBL entry is rewritten
Each entry's desired value changed, so each costs two transfers through the window.
The fix is a uvm_reg_sequence that encodes the design's rules. It carries a model handle, typed as uvm_reg_block, and helpers such as write_reg and update_reg that pass the sequence as the parent of each access. Start it with cfg.model = regs; cfg.start(null): register calls find the sequencer through the map, so the configuration sequence needs none.
Reconfiguring mid-flight follows the same sequence: stop, configure, start. If the design had traffic in flight, the stop must also wait for it to drain, using the completion rules from U10.
Two threads, one window
The model can be called from several threads at once. Each register has a semaphore: a second access to the same register waits until the first finishes. That keeps a register's mirror consistent, but it protects nothing shared between registers.
- Both start
Each thread holds only its own register's semaphore, so both frontdoors run. A sends INDEX = 1 first.
More context
Neither semaphore stops the other thread: tbl[1] and tbl[2] are different registers. Without a lock, nothing reserves the sequencer for A's pair.
Follow the calls, values and waits
- data INDEX = 1: Thread A: tbl[1].write(11) → APB sequencer
- data INDEX = 1: APB sequencer → INDEX
- Waiting: Thread B: tbl[2].write(22) — INDEX = 2 waiting for a grant
- B moves the index
B's INDEX = 2 was already waiting, so the sequencer grants it before A's DATA item.
More context
The sequencer grants waiting items in arrival order. B's INDEX request has been queued since both threads started; A asks for its DATA item only after its INDEX write finishes.
Follow the calls, values and waits
- data INDEX = 2: Thread B: tbl[2].write(22) → APB sequencer
- data INDEX = 2: APB sequencer → INDEX
- Waiting: Thread A: tbl[1].write(11) — DATA = 11 queued behind B
- Released: Thread B: tbl[2].write(22)
- A's data lands in entry 2
A's DATA = 11 is written to TBL[INDEX], and INDEX is now 2.
More context
A's frontdoor finishes normally, and auto prediction sets tbl[1]'s mirror to 11. The design's TBL[1] was never written.
Follow the calls, values and waits
- data DATA = 11: Thread A: tbl[1].write(11) → DATA -> TBL[INDEX]
- Released: Thread A: tbl[1].write(11)
- B's data overwrites it
B's 22 also goes to TBL[2]. TBL[1] keeps its old value, while the mirror says 11.
More context
The spot item's no-lock option ends with tbl[1] = 0 and tbl[2] = 22. Only a later read of tbl[1] would show the damage.
Follow the calls, values and waits
- data DATA = 22: Thread B: tbl[2].write(22) → DATA -> TBL[INDEX]
Read the diagram as text
- Thread A: tbl[1].write(11): Holds tbl[1]'s semaphore
- Thread B: tbl[2].write(22): Holds tbl[2]'s semaphore
- APB sequencer: Grants waiting items in arrival order
- INDEX: 1, then 2
- DATA -> TBL[INDEX]: Both values land in entry 2
- Thread A: tbl[1].write(11) → APB sequencer: 1: INDEX = 1
- Thread B: tbl[2].write(22) → APB sequencer: 2: INDEX = 2
- Thread A: tbl[1].write(11) → DATA -> TBL[INDEX]: 3: DATA = 11
- Thread B: tbl[2].write(22) → DATA -> TBL[INDEX]: 4: DATA = 22
- APB sequencer → INDEX: last write wins
Measured in the spot item below: without a lock, TBL[1] stays 0 and TBL[2] ends at 22.
The bench's frontdoor calls lock(m_sequencer) before INDEX and unlock after DATA, so the pair is atomic on the bus. grab or a semaphore shared by all entries' frontdoors works too. The example forks two table writes, and the APB trace shows each pair together.
Register coverage
A register model can carry its own coverage. The test enables it before the model is built with uvm_reg::include_coverage("*", UVM_CVR_FIELD_VALS). Each register's constructor asks build_coverage whether to build its coverage model, and the test turns it on afterwards with set_coverage.
| Hook | Called when | Sees |
|---|---|---|
sample(data, byte_en, is_read, map) | After a successful frontdoor access under auto prediction, or a transfer the predictor processes; before the mirror changes. Not for backdoor accesses or a direct predict | The value of that access, even one the design ignored |
sample_values() | Only when the bench calls it | The mirrored values at that moment |
Override them in the register class, guard them with get_coverage, and sample a covergroup inside. The example counted 3 access samples and 1 value sample on CTRL.
Choose the hook by the question. Which values were written is sample. Which configurations the design actually ran is sample_values, called after each configuration. It reads the mirror, so it skips the ignored write.
Try it
The first four lines follow the ledger in the table above: configuration in order, the tempting block update, and the safe reconfiguration. The 82 that the block update requested appears only in the coverage. Then the APB trace shows two table writes from two threads, each INDEX and DATA pair together. The last line is the coverage the model collected: three written modes (fe, 82, 0d) by sample, and the final mirror (0d) by sample_values, so the 82 the design never ran is counted once.
Try it: delete the lock and unlock lines in tbl_frontdoor inside incr_bench.svh and run again. Then move include_coverage after super.build_phase and watch the counts drop to 0. Then think it through: after the bad block update, why would stopping GO and calling ctrl.update() not restore 82, and where must the requested configuration be kept?
330 ns configured, 11 writes ctrl desired=fe mirror=fe dut=fe | go=1 irq.done=0 650 ns block update asked for mode 82: 10 writes 650 ns after block update ctrl desired=fe mirror=fe dut=fe | go=1 irq.done=0 980 ns reconfigured, 11 writes ctrl desired=d mirror=d dut=d | go=1 irq.done=0 1005 ns APB write 010 <- 1 1035 ns APB write 014 <- 11 1065 ns APB write 010 <- 3 1095 ns APB write 014 <- 33 1100 ns design tbl[1]=11 tbl[3]=33 1100 ns ctrl.mode coverage: 3 access samples, 1 value sample, bins 2 0 1 1 UVM_ERROR : 0Read included file: incr_bench.svh
// incr_bench.svh: the U14 register slice, its APB bus, its register model and the base test.
// main.sv holds only the lesson's test, which extends incr_base_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 register slice (design) ----------------
// 0x00 CTRL mode[7:0] read-write, reset 01; writes are ignored while GO.en = 1
// 0x04 GO en[0] read-write, reset 0
// 0x08 IRQ done[0] an event sets it; writing 1 clears it
// 0x0C COUNT n[7:0] events since the last read; a bus read clears it
// 0x10 INDEX [1:0] selects a TBL entry
// 0x14 DATA [7:0] reads and writes TBL[INDEX]
// 0x100-0x13C RAM 16 words of 32 bits
// Bits a register does not implement read as 0. The storage uses blocking assignments, so the
// backdoor below can also assign it; read data is captured in the setup phase, so a read that
// clears COUNT cannot change its own read data.
module incr_csr (apb_if bus, input logic evt);
logic [7:0] ctrl = 8'h01;
logic go = 0;
logic done = 0;
logic [7:0] count = 0;
logic [1:0] index = 0;
logic [7:0] tbl [4];
logic [31:0] ram [16];
initial begin
foreach (tbl[i]) tbl[i] = 0;
foreach (ram[i]) ram[i] = 0;
end
assign bus.pready = 1;
function automatic logic [31:0] read_value(logic [31:0] a);
if (a >= 32'h100 && a < 32'h140) return ram[a[5:2]];
case (a)
32'h00: return {24'b0, ctrl};
32'h04: return {31'b0, go};
32'h08: return {31'b0, done};
32'h0C: return {24'b0, count};
32'h10: return {30'b0, index};
32'h14: return {24'b0, tbl[index]};
default: return 0;
endcase
endfunction
function automatic void write_value(logic [31:0] a, logic [31:0] d);
if (a >= 32'h100 && a < 32'h140) ram[a[5:2]] = d;
else case (a)
32'h00: if (!go) ctrl = d[7:0];
32'h04: go = d[0];
32'h08: if (d[0]) done = 0;
32'h10: index = d[1:0];
32'h14: tbl[index] = d[7:0];
default: ;
endcase
endfunction
always @(negedge bus.clk)
if (bus.psel && !bus.penable) bus.prdata = read_value(bus.paddr);
always @(posedge bus.clk) begin
if (evt) begin
if (count != 8'hff) count = count + 1;
done = 1;
end
if (bus.psel && bus.penable && bus.pwrite) write_value(bus.paddr, bus.pwdata);
if (bus.psel && bus.penable && !bus.pwrite && bus.paddr == 32'h0C) count = 0;
end
endmodule
// ---------------- The register model ----------------
class ctrl_reg extends uvm_reg;
`uvm_object_utils(ctrl_reg)
rand uvm_reg_field mode;
function new(string name = "ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE);
add_coverage(build_coverage(UVM_CVR_FIELD_VALS)); // only if a test asked for it (U14.3)
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 go_reg extends uvm_reg;
`uvm_object_utils(go_reg)
rand uvm_reg_field en;
function new(string name = "go_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
en = uvm_reg_field::type_id::create("en");
en.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
endfunction
endclass
class irq_reg extends uvm_reg;
`uvm_object_utils(irq_reg)
uvm_reg_field done;
function new(string name = "irq_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
done = uvm_reg_field::type_id::create("done");
done.configure(this, 1, 0, "W1C", 1, 1'b0, 1, 0, 0); // volatile: the design sets it
endfunction
endclass
class count_reg extends uvm_reg;
`uvm_object_utils(count_reg)
uvm_reg_field n;
function new(string name = "count_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
n = uvm_reg_field::type_id::create("n");
n.configure(this, 8, 0, "RC", 1, 8'h00, 1, 0, 0); // volatile: events change it
endfunction
endclass
class tbl_reg extends uvm_reg;
`uvm_object_utils(tbl_reg)
rand uvm_reg_field coef;
function new(string name = "tbl_reg"); super.new(name, 8, UVM_NO_COVERAGE); endfunction
virtual function void build();
coef = uvm_reg_field::type_id::create("coef");
coef.configure(this, 8, 0, "RW", 0, 8'h00, 1, 1, 0);
endfunction
endclass
// A TBL entry has no address of its own. Its frontdoor reaches entry k through the window:
// write INDEX = k, then write or read DATA. lock() keeps the two transfers together.
class tbl_frontdoor extends uvm_reg_frontdoor;
`uvm_object_utils(tbl_frontdoor)
int unsigned k;
function new(string name = "tbl_frontdoor"); super.new(name); endfunction
virtual task body();
apb_item t;
lock(m_sequencer);
t = apb_item::type_id::create("index");
start_item(t);
t.write = 1; t.addr = 'h10; t.data = k;
finish_item(t);
t = apb_item::type_id::create("data");
start_item(t);
t.write = (rw_info.get_kind() == UVM_WRITE); t.addr = 'h14; t.data = rw_info.get_value(0);
finish_item(t);
unlock(m_sequencer);
if (rw_info.get_kind() == UVM_READ) rw_info.set_value(t.data, 0);
rw_info.set_status(UVM_IS_OK);
endtask
endclass
class incr_regs extends uvm_reg_block;
`uvm_object_utils(incr_regs)
rand ctrl_reg ctrl;
go_reg go;
irq_reg irq;
count_reg count;
rand tbl_reg tbl[4];
uvm_mem ram;
function new(string name = "incr_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
virtual function void build();
default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
ctrl = ctrl_reg::type_id::create("ctrl"); ctrl.configure(this); ctrl.build();
go = go_reg::type_id::create("go"); go.configure(this); go.build();
irq = irq_reg::type_id::create("irq"); irq.configure(this); irq.build();
count = count_reg::type_id::create("count"); count.configure(this); count.build();
default_map.add_reg(ctrl, 'h00, "RW");
default_map.add_reg(go, 'h04, "RW");
default_map.add_reg(irq, 'h08, "RW");
default_map.add_reg(count, 'h0C, "RW");
foreach (tbl[i]) begin
tbl_frontdoor fd = new($sformatf("tbl%0d_fd", i));
fd.k = i;
tbl[i] = tbl_reg::type_id::create($sformatf("tbl%0d", i));
tbl[i].configure(this);
tbl[i].build();
default_map.add_reg(tbl[i], 'h0, "RW", 1, fd); // unmapped: reached through its frontdoor
end
// name words bits access
ram = new("ram", 16, 32, "RW", UVM_NO_COVERAGE);
ram.configure(this);
default_map.add_mem(ram, 'h100, "RW");
lock_model();
endfunction
endclass
// ---------------- A backdoor without VPI ----------------
// It reads and writes the design's storage through hierarchical references, with no bus
// cycles and no timed waits. One instance, set on the block, serves every register and the
// memory. A memory burst arrives as one item holding every word.
class incr_backdoor extends uvm_reg_backdoor;
`uvm_object_utils(incr_backdoor)
function new(string name = "incr_backdoor"); super.new(name); endfunction
virtual task write(uvm_reg_item rw);
uvm_object e = rw.get_element();
uvm_reg_data_t v = rw.get_value(0);
rw.set_status(UVM_IS_OK);
case (e.get_name())
"ctrl": tb.dut.ctrl = v[7:0];
"go": tb.dut.go = v[0];
"irq": tb.dut.done = v[0];
"count": tb.dut.count = v[7:0];
"tbl0": tb.dut.tbl[0] = v[7:0];
"tbl1": tb.dut.tbl[1] = v[7:0];
"tbl2": tb.dut.tbl[2] = v[7:0];
"tbl3": tb.dut.tbl[3] = v[7:0];
"ram": // every word of a burst, or none
if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
else for (int i = 0; i < rw.get_value_size(); i++)
tb.dut.ram[rw.get_offset() + i] = rw.get_value(i);
default: rw.set_status(UVM_NOT_OK);
endcase
endtask
virtual function void read_func(uvm_reg_item rw);
uvm_object e = rw.get_element();
rw.set_status(UVM_IS_OK);
case (e.get_name())
"ctrl": rw.set_value(tb.dut.ctrl, 0);
"go": rw.set_value(tb.dut.go, 0);
"irq": rw.set_value(tb.dut.done, 0);
"count": rw.set_value(tb.dut.count, 0);
"tbl0": rw.set_value(tb.dut.tbl[0], 0);
"tbl1": rw.set_value(tb.dut.tbl[1], 0);
"tbl2": rw.set_value(tb.dut.tbl[2], 0);
"tbl3": rw.set_value(tb.dut.tbl[3], 0);
"ram":
if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
else for (int i = 0; i < rw.get_value_size(); i++)
rw.set_value(tb.dut.ram[rw.get_offset() + i], i);
default: rw.set_status(UVM_NOT_OK);
endcase
endfunction
endclass
// ---------------- A quirk no access string describes ----------------
// CTRL ignores bus writes while GO.en = 1. After such a write, put the old value back into the
// prediction. A backdoor write still lands, so it is left alone.
class ctrl_lock_cb extends uvm_reg_cbs;
`uvm_object_utils(ctrl_lock_cb)
uvm_reg_field en;
function new(string name = "ctrl_lock_cb"); super.new(name); endfunction
virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
inout uvm_reg_data_t value, input uvm_predict_e kind,
input uvm_door_e path, input uvm_reg_map map);
if (kind == UVM_PREDICT_WRITE && path != UVM_BACKDOOR && en.get_mirrored_value() == 1)
value = previous;
endfunction
endclass
// ---------------- The base test every U14.1-U14.3 program extends ----------------
class incr_base_test extends uvm_test;
`uvm_component_utils(incr_base_test)
incr_regs regs;
apb_adapter adapter;
apb_driver drv;
uvm_sequencer #(apb_item) sqr;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
virtual function void build_phase(uvm_phase phase);
incr_backdoor bkdr = incr_backdoor::type_id::create("bkdr");
regs = incr_regs::type_id::create("regs");
regs.build();
regs.set_backdoor(bkdr); // inherited by every register and the memory
install_quirks();
// Reading the mirror of a volatile field warns that the design may have changed it since.
// The ledger lines below read those mirrors on purpose, so the warning is switched off.
uvm_root::get().set_report_id_action("UVM/FLD/GET_MIRRORED_VAL/VOL", UVM_NO_ACTION);
adapter = apb_adapter::type_id::create("adapter");
sqr = uvm_sequencer #(apb_item)::type_id::create("sqr", this);
drv = apb_driver::type_id::create("drv", this);
if (!uvm_config_db #(virtual apb_if)::get(this, "", "vif", drv.vif))
`uvm_fatal("NOVIF", "no virtual interface")
endfunction
virtual function void install_quirks();
ctrl_lock_cb cb = ctrl_lock_cb::type_id::create("ctrl_lock");
cb.en = regs.go.en;
uvm_reg_field_cb::add(regs.ctrl.mode, cb);
endfunction
virtual function void connect_phase(uvm_phase phase);
drv.seq_item_port.connect(sqr.seq_item_export);
regs.default_map.set_sequencer(sqr, adapter);
regs.default_map.set_auto_predict(1);
endfunction
// Every register call returns a status; a refused access must not pass silently.
function void ok(uvm_status_e st, string what);
if (st != UVM_IS_OK) `uvm_error("STATUS", {what, " returned ", st.name()})
endfunction
endclass
module tb;
bit clk;
logic evt = 0;
always #5 clk = ~clk;
apb_if bus (clk);
incr_csr dut (bus, evt);
// n hardware events, one every 20 ns: each adds 1 to COUNT and sets IRQ.done
task automatic events(int n);
repeat (n) begin @(negedge clk); evt = 1; @(negedge clk); evt = 0; end
endtask
initial begin
uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "vif", bus);
run_test("lesson_test");
end
endmodule
Check yourself
Remember
- Randomize the model and let a
uvm_reg_sequenceprogram it in the design's order: stop, configure, start. - Block
update()writes in creation order and writes every volatile writable field; use it only when order and side effects do not matter. - A write the design ignores also erases the request from the model once the prediction is corrected.
- Each register's semaphore serializes that register only; registers sharing a window need a lock, grab or shared semaphore.
- Enable coverage before the model is built;
samplesees each frontdoor or observed access,sample_valuessees the mirror when you ask.