A test preloads a descriptor table with burst_write, starts the engine, and the engine fetches garbage. The status of every call was UVM_IS_OK. Hours later someone turns on the APB monitor's messages and finds four bus reads where the burst should have written. The register model, the map and the driver were all correct. The adapter had a one-word bug that only bursts exercise.
Memories are the part of the register layer people learn last and use most at chip level: descriptor rings, lookup tables, mailboxes, packet buffers. This lesson shows how uvm_mem differs from a register, how bursts, the memory allocation manager (MAM) and virtual registers work on top of it, how to preload a large memory in zero time, and what went wrong with that burst.
A warehouse catalogue lists the shelves: where they are, how many, how wide, who may take things from them. It does not list what is on each shelf; for that you walk to the shelf and look. A register in the model has a mirror, the model's belief about its contents. A uvm_mem has none. It records the size (number of words), the word width, the access policy ("RW" or "RO"), its offset in each map and an optional backdoor. Every read() goes to the hardware; nothing is predicted, compared or updated, and there is no mirror() or update() for a memory.
That choice is deliberate. A model of a 1 MB buffer with a mirror per word would cost memory and time for no gain, because memory contents are usually checked by a scoreboard or by comparing against what was written. The cost is that the model cannot catch a memory that silently drops writes. If you need that, a test writes, reads back and compares, which is exactly what uvm_mem_walk_seq and uvm_mem_access_seq do.
The DMA has no memory and one bus, so these lessons use a small slave built for scratch runs. It reuses the bench's APB agent twice, on two ports. Its SRAM array lives in an interface, the way the DMA's internal registers are exposed through dma_bd_if, so a backdoor class can reach it without HDL-path DPI.
Port | Offset | What | Behaviour |
|---|---|---|---|
cpu | 0x00 | CFG | 8-bit RW |
cpu | 0x04 | THRESH | 16-bit, double-buffered: writes go to a pending copy, reads return the active copy |
cpu | 0x08 | COMMIT | bit 0 WO: copies pending THRESH to active |
cpu | 0x0C | CFG_ALIAS | the same flops as CFG |
cpu | 0x40–0x7C | SRAM | 16 words of 32 bits |
dbg | 0x20 / 0x24 | CFG / THRESH | THRESH is read-only from this port |
dbg | 0x80–0xBC | SRAM | the same 16 words |
uvm_mem has no factory registration in the library and its constructor takes the geometry, so it is built with new(name, size, n_bits, access, has_coverage) and then configure(parent, hdl_path). The order matters: configure attaches it to the block, and add_mem gives it an offset in a map, in bytes.
// snippet: from the scratch block's build()
sram = new("sram", 16, 32, "RW", UVM_NO_COVERAGE); // 16 words of 32 bits
sram.configure(this); // no HDL path: a user backdoor instead
cpu_map = create_map("cpu_map", 'h0, 4, UVM_LITTLE_ENDIAN);
cpu_map.add_mem(sram, 'h40); // words at 0x40, 0x44, ... 0x7C
// ... registers added as usual ...
lock_model();A memory backdoor is the same uvm_reg_backdoor class the bench uses for KEY and LEN, but rw.offset now says which word. Writes are allowed here, because preloading is the point:
// snippet: scratch memory backdoor
class sram_backdoor extends uvm_reg_backdoor;
virtual sram_store_if vif; // the interface that holds the array
function new(string name = "sram_backdoor");
super.new(name);
endfunction
virtual function void read_func(uvm_reg_item rw);
rw.value[0] = vif.mem[rw.offset];
rw.status = UVM_IS_OK;
endfunction
virtual task write(uvm_reg_item rw);
vif.mem[rw.offset] = rw.value[0];
rw.status = UVM_IS_OK;
endtask
endclass
// in the env: rm.sram.set_backdoor(bd);// snippet: scratch test
uvm_reg_data_t wr[] = '{32'hA0, 32'hA1, 32'hA2, 32'hA3}, rd[];
rm.sram.burst_write(st, 0, wr); // words 0..3
rd = new[4];
rm.sram.burst_read(st, 0, rd);
for (int i = 12; i < 16; i++)
rm.sram.poke(st, i, 32'hC0DE_0000 + i); // preload: no bus, no time
rm.sram.read(st, 13, v); // frontdoor read of a poked wordA burst is one call and one uvm_reg_item with an array of values. The map turns it into bus operations. If the adapter cannot express a burst, which is true of APB and of the bench's adapter, it becomes one access per word, each with the operation kind UVM_BURST_WRITE or UVM_BURST_READ. That detail is the war story below.
When several sequences share one memory (a descriptor ring, packet buffers, a mailbox), they need non-overlapping pieces. The memory allocation manager, uvm_mem_mam, hands out regions. reserve_region(offset, n_bytes) claims a fixed range; request_region(n_bytes) finds a free one using the configured policy. Sizes are in bytes and rounded up to whole words; region offsets are in words.
// snippet: scratch test
uvm_mem_mam_cfg mcfg = new;
mcfg.n_bytes = 4; mcfg.start_offset = 0; mcfg.end_offset = 15;
mcfg.mode = uvm_mem_mam::GREEDY; mcfg.locality = uvm_mem_mam::BROAD;
mam = new("mam", mcfg, rm.sram);
ring = mam.reserve_region(0, 4*4); // words 0..3, fixed
buf_r = mam.request_region(2*4); // any 2 free words
buf_r.write(st, 1, 32'hBEEF); // offset 1 inside the regionSome memories hold an array of structures, such as a table of descriptors. A virtual register (uvm_vreg) gives each entry named fields without a mirror: desc.write(2, ...) writes entry 2, and desc.len.write(1, 7) writes one field of entry 1 by reading the word, merging and writing it back. Virtual fields are bit slices like register fields, but with no access policy and no prediction.
// snippet: scratch virtual register over SRAM words 8..11
class desc_vreg extends uvm_vreg;
rand uvm_vreg_field addr, len;
function new(string name = "desc_vreg");
super.new(name, 32);
endfunction
function void build();
addr = new("addr"); addr.configure(this, 16, 0);
len = new("len"); len.configure(this, 16, 16);
endfunction
endclass
// in the block's build(), before lock_model():
desc = new("desc");
desc.configure(this, sram, 4, 'h8, 1); // 4 entries from word 8, one word apart
desc.build();scratch run, Verilator
UVM_INFO mini_tb.sv(328) @ 215000: uvm_test_top [MEM] burst_write status UVM_IS_OK at 215000
UVM_INFO mini_tb.sv(336) @ 335000: uvm_test_top [MEM] burst_read: '{'ha0, 'ha1, 'ha2, 'ha3}, status UVM_IS_OK at 335000
UVM_INFO mini_tb.sv(344) @ 335000: uvm_test_top [MEM] ring ['h0000000000000000:'h0000000000000003], buffer ['h000000000000000c:'h000000000000000d]
UVM_INFO mini_tb.sv(348) @ 366000: uvm_test_top [MEM] buffer word 1 is sram[13] = beef (peek)
UVM_INFO reg/uvm_vreg.svh(1018) @ 395000: reporter [RegModel] Wrote virtual register "desc"[2] via frontdoor with: 'h0000000000030040
UVM_INFO reg/uvm_vreg_field.svh(521) @ 425000: reporter [RegModel] Wrote virtual field "desc.len"[1] via frontdoor with: 'h0000000000070000
UVM_INFO mini_tb.sv(353) @ 455000: uvm_test_top [MEM] desc[2] is sram[10] = 00030040
UVM_INFO mini_tb.sv(355) @ 485000: uvm_test_top [MEM] desc[1] after len.write(1,7) is sram[9] = 00070000
UVM_INFO mini_tb.sv(359) @ 515000: uvm_test_top [MEM] frontdoor read of poked sram[13] = c0de000d at 515000
UVM_INFO reg/sequences/uvm_mem_walk_seq.svh(138) @ 515000: reporter@@walk.single_mem_walk_seq [uvm_mem_walk_seq] Walking memory rm.sram in map "rm.cpu_map"...
UVM_INFO reg/sequences/uvm_mem_walk_seq.svh(138) @ 1925000: reporter@@walk.single_mem_walk_seq [uvm_mem_walk_seq] Walking memory rm.sram in map "rm.dbg_map"...
UVM_ERROR : 0The burst took 120 ns: four APB writes of 30 ns each (this slave has no wait states). The burst is a convenience for the test, not a faster bus.
request_region placed the buffer at words 12–13; the policy chooses randomly among free ranges, so another seed gives another place. Code that hard-codes where a requested region landed breaks on the next seed.
The poke loop at words 12–15 ran in zero time and overwrote the MAM buffer's word 13 (BEEF became C0DE000D). The MAM only manages requests made through it; a backdoor write ignores it. Preload through the regions, or reserve the preloaded range first.
uvm_mem_walk_seq walked the memory once per map: the SRAM is in both the CPU and the debug maps, so it is tested through both ports.
The first run of this scratch test used the bench's apb_reg_adapter unchanged. Every call returned UVM_IS_OK, the burst read returned four zeros, and the APB monitor (at UVM_HIGH) showed what really happened during the burst write:
UVM_INFO dma_tb.sv(151) @ 125000: uvm_test_top.env.cpu.mon [APB_MON] RD addr=40 rdata=00000000
UVM_INFO dma_tb.sv(151) @ 155000: uvm_test_top.env.cpu.mon [APB_MON] RD addr=44 rdata=00000000
UVM_INFO dma_tb.sv(151) @ 185000: uvm_test_top.env.cpu.mon [APB_MON] RD addr=48 rdata=00000000
UVM_INFO dma_tb.sv(151) @ 215000: uvm_test_top.env.cpu.mon [APB_MON] RD addr=4c rdata=00000000The bench adapter's reg2bus used to set it.write = (rw.kind == UVM_WRITE). For single accesses that is right. For a burst, the map passes each per-word operation with kind = UVM_BURST_WRITE, the comparison is false, and the adapter builds a read. Nothing in the register layer checks the direction of the item an adapter returns, and a read of a fresh SRAM returns zeros, so no error appears anywhere. The fix is one line:
// snippet: reg2bus that handles burst operations
it.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE});The bench never called a burst, so its adapter looked correct, and that is exactly why the bug survives in real projects. This run found it, and the course bench now uses the fixed line. In general: an adapter is written for registers, signed off with bit-bash and reset tests, and meets its first burst months later at chip level. Add a burst write and read-back to the adapter's own smoke test.
rm.sram.mirror(...) after a burst write. What happens, and what should the test do instead?There is no mirror() on uvm_mem; memories keep no model copy, so the call does not compile. To check contents, read the words back (frontdoor read/burst_read, or peek through the backdoor) and compare with what was written, or let a scoreboard compare the engine's output.
mam.request_region(64) on the same MAM. Can they overlap? What if one of them also preloads with poke()?Requests through one MAM never overlap: each region is recorded and later requests avoid it. A poke() bypasses the MAM entirely, so a backdoor preload can land on a region another sequence was given, as the scratch run shows. Preload through the region objects or reserve the preload range with reserve_region first.
desc.len.write(1, 7) changed only the upper 16 bits of SRAM word 9. How did the virtual field know the lower 16 bits?A virtual field write is a read-modify-write: the library reads the entry's word through the frontdoor, replaces the field's bits and writes the word back. Virtual registers have no mirror, so the read is real bus traffic. That also means a virtual field write is not atomic against other masters writing the same word.
Next: the scratch SRAM is reachable from two ports, and so are its registers. Multiple Maps and Address Spaces shows how one register lives in two maps without the mirror missing the other bus.