Memories and a backdoor without VPI
Reach the design's storage through the frontdoor and through a hand-written backdoor, predict which path triggers a register's side effects, and manage a memory that the model does not mirror.
- Lesson
- U14.1
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 6 questions
A test wants to know how many events have happened, so it reads COUNT. The count is gone: COUNT clears itself when the bus reads it, and a later check expects the old total. The tempting fix, a backdoor read, also clears it, because the register model imitates the bus. Only peek leaves the design alone.
Measured in the example below. Two more events arrive between the read and the backdoor read. Only peek returns the value and leaves COUNT unchanged.
Two routes into the same storage
The slice from U13 has grown. Besides CTRL it has GO, a write-1-to-clear IRQ, a read-to-clear COUNT, a table behind an index register and a 16-word RAM. Every register access now takes a path: UVM_FRONTDOOR sends a bus transfer through the map, as in U13.2; UVM_BACKDOOR goes through the register's backdoor object.
- 60 ns: peek
peekcalls the backdoor'sread_func, which returnstb.dut.count= 3. No transfer and no side effect; the mirror records 3.More context
The log line "60 ns peek -> 3 mirror=3 dut=3". All of it happens in zero time: this backdoor has no timed waits.
Follow the calls, values and waits
- call peek(st, v): lesson_test → regs.count
- call peek(st, v): regs.count → incr_backdoor
- return count = 3 (variable read): tb.dut.count → incr_backdoor
- return count = 3 (variable read): incr_backdoor → regs.count
- return count = 3 (variable read): regs.count → lesson_test
- 70 ns: frontdoor read
The map sends an APB read of 0x0C to the driver, which puts it on the pins.
More context
Unlike peek, this route takes bus time: the test stays inside
read()while the transfer runs.Follow the calls, values and waits
- call read(st, v), frontdoor: lesson_test → regs.count
- call read(st, v), frontdoor: regs.count → map + apb_adapter
- data apb_item: read 0x0C: map + apb_adapter → APB driver
- data apb_item: read 0x0C: APB driver → tb.dut.count
- Waiting: lesson_test — inside read() until 90 ns
- 85 ns: the design answers
The design returns 3 and clears COUNT at 85 ns.
More context
The log line "85 ns APB read 00c -> 3". The read itself is the side effect: the design clears COUNT on that edge.
Follow the calls, values and waits
- return prdata = 3: tb.dut.count → APB driver
- 90 ns: the read returns
The 3 travels back through the driver and the map. The access policy is
RC, so the mirror predicts 0.More context
The log line "90 ns read -> 3 mirror=0 dut=0": the test gets 3, while mirror and design both hold 0.
Follow the calls, values and waits
- return prdata = 3: APB driver → map + apb_adapter
- return v = 3, mirror predicts 0: map + apb_adapter → regs.count
- return v = 3, mirror predicts 0: regs.count → lesson_test
- Released: lesson_test
- 130 ns: backdoor read
The model reads 2 through the backdoor, sees the
RCpolicy and writes 0 back through the same backdoor, imitating the bus. The mirror keeps the 2 it read.More context
The log line "130 ns backdoor read -> 2 mirror=2 dut=0". The read and the write-back both finish at 130 ns, without a bus cycle.
Follow the calls, values and waits
- call read(st, v, UVM_BACKDOOR): lesson_test → regs.count
- call read(st, v, UVM_BACKDOOR): regs.count → incr_backdoor
- return count = 2 (variable read): tb.dut.count → incr_backdoor
- data count = 0 (RC write-back): incr_backdoor → tb.dut.count
- return count = 2 (variable read): incr_backdoor → regs.count
- return count = 2 (variable read): regs.count → lesson_test
- Both routes update one mirror
Each route ends by predicting the same register's mirror, but not always to the value now stored. After the frontdoor read the mirror is 0, like the design; after the backdoor read it is the 2 that was read, while the design holds 0.
Read the diagram as text
- lesson_test: Calls read, peek, poke or write on regs.count
- regs.count: Register model: mirror and access policy RC
- map + apb_adapter: Address 0x0C; builds an apb_item
- APB driver: Setup and access phase, 30 ns
- incr_backdoor: read_func and write use hierarchical references
- tb.dut.count: The design's storage; a bus read clears it
- lesson_test → regs.count: register call
- regs.count → map + apb_adapter: UVM_FRONTDOOR
- map + apb_adapter → APB driver: apb_item
- APB driver → tb.dut.count: APB transfer
- regs.count → incr_backdoor: UVM_BACKDOOR, peek, poke
- incr_backdoor → tb.dut.count: reads or assigns the variable
The route decides whether the design sees an access, and which value the mirror records. The register object is the same either way.
The path is the third argument of read and write. Leave it out and the block's default door applies, which is the frontdoor here. A frontdoor access costs bus time and exercises the decoder, the protocol and every side effect. This bench's backdoor uses no bus cycles and no timed waits, so its accesses finish in zero time. Use it to load a memory image, check a value the bus cannot reach, or inspect state between transfers. A backdoor's read and write methods are tasks (only read_func is a function), so another implementation may wait.
Which access leaves side effects
| Call | Path | What the design sees | Mirror afterwards |
|---|---|---|---|
read(st, v) | Frontdoor | A bus read: COUNT clears | Predicted from the policy: 0 |
read(st, v, UVM_BACKDOOR) | Backdoor | The model deposits the cleared value | The value read (2, while the design holds 0) |
peek(st, v) | Backdoor | Nothing | The value read |
write(st, 1, UVM_BACKDOOR) on IRQ | Backdoor | The model computes 1 & ~1 = 0 and deposits it | 0 |
poke(st, 1) on IRQ | Backdoor | The raw 1 is deposited: done sets | 1 |
Measured for COUNT (RC) and IRQ.done (W1C). read and write imitate bus side effects on either path; peek and poke never do.
Read the last two rows together. Writing 1 to a write-1-to-clear bit through the backdoor clears it, as a bus write would. Poking 1 sets it, which raises an interrupt that no hardware event caused. Use poke to build a state the bus cannot create, and a backdoor write for bus behaviour without bus time.
The imitation comes from the register model, not from your backdoor class. For a backdoor write, uvm_reg reads the current value, applies each field's access policy, then deposits the result. For a backdoor read of an RC field it deposits the cleared value. Your backdoor only moves raw bits.
A backdoor in twenty lines
incr_backdoor extends uvm_reg_backdoor and overrides two methods: read_func fills in the value and write assigns it. Each receives a uvm_reg_item. Its get_element() is the register or memory being accessed, and its get_offset() is the word offset for a memory.
The frontdoor splits a memory burst into one transfer per word, but a backdoor burst arrives as a single item: get_value_size() words starting at the offset. uvm_mem checks only that the first word exists, so the backdoor must check the end. Handling only get_value(0) would load one word and still report success.
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())
"count": tb.dut.count = v[7:0];
"ram": // a burst is one item: every word, 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);
// ... one line per register
default: rw.set_status(UVM_NOT_OK);
endcase
endtask
regs.set_backdoor(bkdr); // in build_phase: every register and memory inherits itA hierarchical reference such as tb.dut.count is resolved when the bench is compiled, so this backdoor works on any simulator. The cost is coupling: the class names the design's hierarchy, and a renamed signal breaks the build. Keep one backdoor class per block, next to the model.
One condition comes from the design. Verilator rejects a variable that the RTL updates with nonblocking assignments and the backdoor assigns with blocking ones. So the slice updates its registers with blocking assignments and captures read data in the setup phase.
The HDL-path backdoor, and what it needs from the simulator
Without a backdoor object, uvm_reg uses HDL paths: add_hdl_path_slice("count_q", 0, 8) on the register and add_hdl_path("tb.dut") on the block. Access then goes through uvm_hdl_read and uvm_hdl_deposit, which look the signal up by name at run time. Generated models usually carry these paths, so no class per block is needed.
A lookup by name needs the simulator's programming interface (VPI) and read and write visibility of those signals, often enabled by a compile option. uvm_reg_mem_hdl_paths_seq checks that every declared path resolves.
A backdoor can also watch the design. Override is_auto_updated and wait_for_change, and the model updates a volatile field's mirror whenever the signal changes, without any read.
Memories have no mirror
A uvm_mem describes a range of words: new("ram", 16, 32, "RW") is 16 words of 32 bits, placed at 0x100 with add_mem. Unlike a register it keeps no desired or mirrored value. A memory may hold megabytes, and a mirror would cost the test as much again. So read and write take a word offset, and the model remembers nothing afterwards.
| Call in the example | Bus transfers | Result |
|---|---|---|
ram.write(st, 2, 'h1234) | APB write at 0x108 | peek of word 2 returns 1234 |
ram.poke(st, 5, 'hcafe) | None | A frontdoor read of word 5 returns cafe |
ram.burst_write(st, 8, words) | Four APB writes, 0x120-0x12c | The adapter has no burst, so the map sends one transfer per word |
ram.burst_write(st, 12, words, UVM_BACKDOOR) | None | One backdoor item with four words; a burst at 14 is refused with UVM_NOT_OK |
Word k sits at 0x100 + 4k because create_map's byte_addressing argument defaults to 1. With 0, words would sit at 0x100, 0x101 and so on.
Without a mirror, a memory check is a comparison you write. Keep the expected words yourself or in a reference model, then peek or read them back. peek checks a large region cheaply; read also proves the bus path.
Sharing a memory without collisions
Two DMA engines need a 4-word buffer each. If each test picks its own offsets, the second buffer can land on part of the first, and the fault surfaces much later as corrupt data. The memory allocation manager, uvm_mem_mam, hands out regions that never overlap. Every uvm_mem already has one, ram.mam, covering all its words.
request_region(16)
Returns a
uvm_mem_regionof 4 words at a random free offset. The example got words 4-7 for A and 0-3 for B.reserve_region(offset, bytes)
Claims a specific range. One that overlaps an allocated region is refused with an error and returns null.
region.write(st, offset, v)
Writes relative to the region's start. Call
release_region()when the engine is done with it.
The allocator only protects code that asks it. A sequence that writes ram at a fixed offset bypasses it, so route every buffer through the same mam.
Try it
Each ledger line shows the mirror beside the design's value after one call; the APB lines are the only bus transfers. The backdoor burst carries four words in one item without a bus write, and the backdoor refuses a burst that would run past word 15. The one UVM_ERROR is the allocator refusing the overlapping region, which is what the last part demonstrates.
Try it: replace the frontdoor read of COUNT with mirror(st, UVM_CHECK). It also reads over the bus, so COUNT still clears. Then poke IRQ.done with 0 and read IRQ through the frontdoor.
60 ns 3 events mirror=0 dut=3 60 ns peek -> 3 mirror=3 dut=3 85 ns APB read 00c -> 3 90 ns read -> 3 mirror=0 dut=0 130 ns backdoor read -> 2 mirror=2 dut=0 130 ns backdoor write(1) mirror=0 dut=0 130 ns poke(1) mirror=1 dut=1 155 ns APB write 108 <- 1234 160 ns ram[2] peek -> 1234 185 ns APB read 114 -> cafe 190 ns ram[5] after poke, read -> cafe 215 ns APB write 120 <- 11 305 ns APB write 12c <- 44 310 ns burst_write done: 5 bus writes in total 310 ns backdoor burst at 12 -> 11 22 33 44, still 5 bus writes 310 ns backdoor burst at 14 -> UVM_NOT_OK 310 ns mam: a = words 4-7, b = words 0-3 310 ns mam: a region overlapping a is refused 340 ns a.write(offset 4) -> UVM_IS_OK; ram[8] 11 -> ee UVM_ERROR : 1Read 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
- The path argument picks the route:
UVM_FRONTDOORis a bus transfer,UVM_BACKDOORgoes through the backdoor object, here in zero time. readandwriteimitate bus side effects on either path;peekandpokemove raw bits and never do.- A hand-written
uvm_reg_backdoorwith hierarchical references needs no VPI;add_hdl_pathanduvm_hdl_*do. - A
uvm_memhas no mirror: access it by offset and check it against values you keep, and make the adapter accept burst kinds. - Take buffers from
mem.mamso concurrent users never share words, and keep offsets inside the region yourself.