Built-in register tests and quirky registers
Run UVM's built-in register and memory tests knowing what each assumes and costs, exclude what does not fit, and model registers whose behaviour no access string describes.
- Lesson
- U14.2
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
A block's first regression runs uvm_reg_bit_bash_seq and fails 48 times on CTRL, all on bits 8 to 31. The model and the design agree on the 8-bit mode field. They disagree on the 24 bits CTRL does not implement: the test expects them to read 0, and this design stores whatever is written there.
| Test | What it checks | Assumes | Cost on this slice |
|---|---|---|---|
uvm_reg_hw_reset_seq | Each register reads its reset value | Design just reset; volatile fields skipped | 4 writes, 6 reads |
uvm_reg_bit_bash_seq | Every bit can be set and cleared, alone | Unimplemented bits read 0 | 238 writes, 238 reads |
uvm_mem_walk_seq | Each memory word holds its own value | RW memory | 31 writes, 16 reads |
uvm_reg_access_seq | Frontdoor and backdoor agree | A backdoor or HDL path | 16 writes, 8 reads |
uvm_mem_access_seq | Same, for each memory word | A backdoor or HDL path | 16 writes, 16 reads |
Measured in the example. The table entries are excluded from bit_bash; the other counts include them. bit_bash skips the volatile fields IRQ.done[0] and COUNT.n[7:0] but still bashes the unimplemented bits around them: 2 x (32 + 32 + 31 + 24) = 238.
Running a built-in test
Each built-in test is a uvm_reg_sequence. You give it the block and start it on no sequencer: seq.model = regs; seq.start(null);. It walks the block and its sub-blocks, and its register calls go through each register's map to the map's sequencer. So the map needs set_sequencer and a way to predict, exactly as for your own accesses.
Run them in an order that matches their assumptions. hw_reset comes first, straight after reset, because every later test leaves values behind. uvm_reg_mem_built_in_seq runs the whole set in one call; its tests field selects members, such as UVM_DO_REG_HW_RESET | UVM_DO_MEM_WALK.
These tests are the cheapest check that the model and the decoder agree. They find a wrong offset, a wrong reset value, a field that does not stick, or two registers that alias. They say nothing about what the registers do, so they complement functional tests rather than replace them.
What each test assumes
Reset values
hw_resetcallsmirror(UVM_CHECK)on each register aftermodel.reset(). Fields without a reset, or with checking off, are skipped; a register whose fields are all volatile is skipped entirely, like IRQ and COUNT here.Bit bashing
For each of the register's 32 bits,
bit_bashwrites it flipped and reads back, twice. Unimplemented bits are treated as read-only zeros. Volatile fields and write-only fields are skipped, because their read-back is unpredictable.Memory walk
mem_walkwrites~kto word k, reads word k-1 back, then writes k-1 there. Two adjacent words that alias each other show up as a mismatch.Access through both doors
reg_accesswrites through the frontdoor and checks through the backdoor, then the reverse. Without a backdoor or HDL path it reports an error, so it runs here only because ofincr_backdoor.
Exclude what a test cannot check with a resource attribute. The key is "REG::" plus the register's full name; the attribute name says which test to skip. NO_REG_TESTS skips all of them, and the more specific ones skip one each.
foreach (regs.tbl[i])
uvm_resource_db #(bit)::set({"REG::", regs.tbl[i].get_full_name()},
"NO_REG_BIT_BASH_TEST", 1, this);Excluding is a decision, not a fix. Record why each exclusion exists. Marking a field volatile to quiet bit_bash also turns off its mirror checks everywhere else.
A quirk the access string cannot describe
CTRL ignores bus writes while GO.en is 1. No access string says that, because an access policy describes a field on its own, and this rule depends on another register. Left alone, the model predicts every CTRL write, and the first mirror(UVM_CHECK) after a write while running reports a mismatch that looks like a design bug.
- The transfer17725 ns (illustrative)
The write reaches the design, which keeps mode = 01 because GO.en is 1.
More context
The log line "17725 ns APB write 000 <- 5a". The transfer is legal on the bus; the design's lock rule drops it.
Follow the calls, values and waits
- data APB write 000 <- 5a: regs.ctrl.write(5a) → APB write 000 <- 5a
- data APB write 000 <- 5a: APB write 000 <- 5a → tb.dut.ctrl
- Waiting: regs.ctrl.write(5a) — inside write() until the access is predicted
- The prediction17730 ns (illustrative)
The RW policy predicts 5a, then each callback registered on the field may change the value.
More context
Auto prediction runs when the transfer completes, before
writereturns. Without the callback, 5a would go straight into the mirror.Follow the calls, values and waits
- data predicted value = 5a: regs.ctrl.write(5a) → Field prediction
- call post_predict(01, 5a): Field prediction → ctrl_lock_cb.post_predict
- The correction17730 ns (illustrative)
The callback sees GO.en = 1 and a frontdoor path, so it restores the previous value. The mirror stays 01, and the check passes.
More context
The log line "17730 ns ctrl written while running: mirror=1 dut=1": model and design agree, so the
mirror(UVM_CHECK)that follows reads 1 at 17755 ns and matches.Follow the calls, values and waits
- data value = previous 01: ctrl_lock_cb.post_predict → mode mirror
- Released: regs.ctrl.write(5a)
Read the diagram as text
- regs.ctrl.write(5a): Frontdoor, GO.en = 1
- APB write 000 <- 5a: The design keeps mode = 01
- tb.dut.ctrl: Locked while running
- Field prediction: RW policy: value = 5a
- ctrl_lock_cb.post_predict: GO.en mirror is 1, path is not backdoor: value = previous
- mode mirror: Stays 01, as in the design
- regs.ctrl.write(5a) → APB write 000 <- 5a: frontdoor
- APB write 000 <- 5a → tb.dut.ctrl: ignored
- regs.ctrl.write(5a) → Field prediction: auto prediction
- Field prediction → ctrl_lock_cb.post_predict: post_predict(previous, value)
- ctrl_lock_cb.post_predict → mode mirror: value = previous
The callback changes only the prediction. A backdoor write still lands in the design, so the callback leaves it alone.
post_predict receives the field, its previous mirror, the predicted value by reference, the kind of prediction and the path. Register it with uvm_reg_field_cb::add(regs.ctrl.mode, cb). It runs for the read and write predictions of auto prediction, of an explicit predictor and of backdoor accesses, so test path when the quirk applies only to the bus. A direct predict(v) does not call it.
When the rule is a field on its own, prefer the right access string: write-1-to-clear is "W1C", not "RW" plus a callback. The callback is for rules that cross registers or depend on state the field cannot see.
A register with no address
The four table entries share one window: write INDEX, then read or write DATA. In the model they are ordinary registers added to the map as unmapped with a user frontdoor: add_reg(tbl[i], 0, "RW", 1, fd). Any access to tbl[2] then runs fd.body() instead of the map's built-in frontdoor.
regs.tbl[2].write(st, 'h3c). The register hands its operation to its frontdoor as rw_info.- Start
The register sees a user frontdoor in its map, fills
fd.rw_infowith the operation and startsfdon the map's sequencer.More context
The register has no address in the map, so there is no
uvm_reg_bus_opto send. The frontdoor sequence decides what the bus sees.Follow the calls, values and waits
- call rw_info: write tbl[2] <- 3c: regs.tbl[2].write(3c) → tbl_frontdoor (k = 2)
- Waiting: regs.tbl[2].write(3c) — inside write() until body() returns
- Index17815 ns (illustrative)
body()locks the sequencer and sends INDEX = 2.More context
The log line "17815 ns APB write 010 <- 2". The lock keeps any other sequence's item from landing between this write and the next.
Follow the calls, values and waits
- data apb_item: INDEX <- 2: tbl_frontdoor (k = 2) → APB sequencer
- data apb_item: INDEX <- 2: APB sequencer → INDEX at 0x10
- Waiting: APB sequencer — locked by tbl_frontdoor
- Then data17845 ns (illustrative)
Still under the lock,
body()sends DATA = 3c, which the design writes into TBL[2]. A read sends INDEX, reads DATA and copies the data intorw_info.More context
The log line "17845 ns APB write 014 <- 3c". One model access has cost two bus transfers.
Follow the calls, values and waits
- data apb_item: DATA <- 3c: tbl_frontdoor (k = 2) → APB sequencer
- data apb_item: DATA <- 3c: APB sequencer → DATA at 0x14
- Predict
Back in the register, auto prediction updates
tbl[2]'s mirror as for any frontdoor access.More context
body()unlocks the sequencer before it returns. The later read of tbl[2] prints "17910 ns tbl[2] read -> 3c, mirror=3c".Follow the calls, values and waits
- return body() returned: tbl_frontdoor (k = 2) → regs.tbl[2].write(3c)
- data mirror = 3c: regs.tbl[2].write(3c) → tbl[2] mirror
- Released: APB sequencer
- Released: regs.tbl[2].write(3c)
Read the diagram as text
- regs.tbl[2].write(3c): An unmapped register
- tbl_frontdoor (k = 2): A uvm_reg_frontdoor sequence; reads rw_info
- APB sequencer: lock() held for both items
- INDEX at 0x10: Selects entry 2
- DATA at 0x14: Writes TBL[INDEX]
- tbl[2] mirror: Auto prediction after body() returns: 3c
- regs.tbl[2].write(3c) → tbl_frontdoor (k = 2): fd.start(sequencer)
- tbl_frontdoor (k = 2) → APB sequencer: two apb_items
- APB sequencer → INDEX at 0x10: write 2
- APB sequencer → DATA at 0x14: write 3c
- regs.tbl[2].write(3c) → tbl[2] mirror: predict 3c
Measured: the write costs two APB writes and the read costs a write and a read. U14.3 shows why the lock matters.
The library's own quirky registers: uvm_reg_indirect_data and uvm_reg_fifo
uvm_reg_indirect_data packages exactly this pattern. You give it the index register and the array of table registers. Its configure installs one frontdoor per entry and adds each entry to the map as unmapped, and its own predictions follow the index register's value.
uvm_reg_fifo models a register where writes push and reads pop. It keeps a queue of expected values, set pushes onto it, update writes the pending entries, and reads predict from the head. Backdoor access is not supported, because a FIFO's read and write pointers cannot be deposited.
Other quirks follow the same recipe. Use an access policy when the rule is local, a callback when it depends on other state, and a user frontdoor when the bus path is unusual. Aliased registers, for instance, use a post_predict on one that predicts the other.
Try it
The first five lines are the built-in tests, with the transfers each one cost and the errors so far. Then the two quirks run with the APB trace on: a CTRL write while running, checked by mirror, and one table entry written and read through its window.
Try it: remove install_quirks()'s effect by overriding it with an empty function in lesson_test. The CTRL mirror check then reports a mismatch. Then delete the NO_REG_BIT_BASH_TEST loop and compare the bit_bash count.
hw_reset writes=4 reads=6 errors=0 bit_bash writes=238 reads=238 errors=0 mem_walk writes=31 reads=16 errors=0 reg_access writes=16 reads=8 errors=0 mem_access writes=16 reads=16 errors=0 17725 ns APB write 000 <- 5a 17730 ns ctrl written while running: mirror=1 dut=1 17755 ns APB read 000 -> 1 17815 ns APB write 010 <- 2 17845 ns APB write 014 <- 3c 17905 ns APB read 014 -> 3c 17910 ns tbl[2] read -> 3c, mirror=3c 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
- Built-in tests are register sequences: set
modelandstart(null); the map must have a sequencer and a way to predict. - Each test has assumptions: hw_reset needs a fresh reset, bit_bash expects unimplemented bits to read 0, the access tests need a backdoor.
- Exclude a register with a
REG::resource attribute such asNO_REG_BIT_BASH_TEST, and record why. - Use the access string for a field's own rule and a
post_predictcallback for a rule that depends on other state. - A register without an address gets a
uvm_reg_frontdoor: one model access, several bus transfers, normal prediction.
Practice
- The overflow flag that would not stay set Fix · Easy