Suppose the DMA's STATUS register had been modelled as one plain RW field "to save time". A test writes 2 to clear DONE. The model predicts a write of 2 into an RW field, so its mirror now says DONE is 1. The DUT, following its spec, cleared DONE to 0. The next check of STATUS against the mirror would blame the hardware for doing the right thing. Nothing in the test was wrong; one configure() call was.
A register model is only as good as its description of each bit. This lesson builds the DMA's model from its containers down to its fields, and explains every argument that decides how a field behaves.
A UVM register model has four kinds of container. A block (uvm_reg_block) is one unit of hardware with registers, usually one IP; it owns its registers, memories, sub-blocks and maps. A register (uvm_reg) is one addressable word. A field (uvm_reg_field) is a named bit range inside a register, and it is where behaviour lives: width, position, access policy, reset value. A memory (uvm_mem) is an array of words with no fields and no mirror, such as a RAM or a FIFO window.
Think of a filing cabinet. The block is the cabinet, each register a drawer at a known position, each field a folder in the drawer with a label saying who may change it and what it holds when the office opens. A memory is a drawer full of identical unlabelled sheets: you can read and write any sheet, but nobody keeps a copy of each one.
Each register is a class derived from uvm_reg. Its constructor gives the register's width in bits (32) and whether it has coverage. Its build() creates each field with the factory and calls configure() on it. CTRL has the comment that names every argument:
// snippet: from the course DMA bench
// CTRL @0x00: GO is a trigger (write 1 to start, always reads 0), IRQ_EN is RW.
class dma_ctrl_reg extends uvm_reg;
`uvm_object_utils(dma_ctrl_reg)
uvm_reg_field go;
rand uvm_reg_field irq_en;
function new(string name = "dma_ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
go = uvm_reg_field::type_id::create("go");
irq_en = uvm_reg_field::type_id::create("irq_en");
// parent size lsb access volatile reset has_reset is_rand indiv
go.configure (this, 1, 0, "WO", 0, 1'b0, 1, 0, 0);
irq_en.configure(this, 1, 1, "RW", 0, 1'b0, 1, 1, 0);
endfunction
endclass
// STATUS @0x04: BUSY is read-only; DONE and ERR are set by the hardware and
// cleared by writing 1. All three change without a bus write, so all are volatile.
class dma_status_reg extends uvm_reg;
`uvm_object_utils(dma_status_reg)
uvm_reg_field busy, done, err;
function new(string name = "dma_status_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
busy = uvm_reg_field::type_id::create("busy");
done = uvm_reg_field::type_id::create("done");
err = uvm_reg_field::type_id::create("err");
busy.configure(this, 1, 0, "RO", 1, 1'b0, 1, 0, 0);
done.configure(this, 1, 1, "W1C", 1, 1'b0, 1, 0, 0);
err.configure (this, 1, 2, "W1C", 1, 1'b0, 1, 0, 0);
endfunction
endclassArgument | What it means | go | busy | done | len.value |
|---|---|---|---|---|---|
| The register that owns the field. | this | this | this | this |
| Width in bits. | 1 | 1 | 1 | 16 |
| Position of the field's lowest bit in the register. | 0 | 0 | 1 | 0 |
| The policy: how writes and reads change the field (table below). | WO | RO | W1C | RW |
| 1 when the hardware can change the field without a bus write. The library then turns off the mirror compare for it. | 0 | 1 | 1 | 0 |
| The value after reset. | 0 | 0 | 0 | 0 |
| 0 when the field has no defined reset value; reset checks then skip it. | 1 | 1 | 1 | 1 |
| Whether | 0 | 0 | 0 | 1 |
| Whether this field can be written on its own on the bus. It also needs an adapter with byte enables and a field that fills its byte lanes; otherwise the library warns and accesses the whole register. | 0 | 0 | 0 | 0 |
Two choices deserve a sentence. GO is WO with is_rand 0: it is a trigger, writing 1 starts a job and the bit always reads 0, so randomising it would start jobs at random. BUSY, DONE and ERR are volatile because the DMA sets and clears them itself. COUNT is volatile for the same reason. LEN and KEY are not: only software changes them.
The block then creates each register, attaches it to itself with configure(this), builds its fields, and finally creates the map and places every register in it. The map is the next lesson's subject; here, notice that the block owns both the registers and the map, and that it calls lock_model() itself at the end.
// snippet: from the course DMA bench
class dma_reg_block extends uvm_reg_block;
`uvm_object_utils(dma_reg_block)
rand dma_ctrl_reg ctrl;
dma_status_reg status;
rand dma_len_reg len;
rand dma_key_reg key;
dma_count_reg count;
uvm_reg_map map;
function new(string name = "dma_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction
virtual function void build();
ctrl = dma_ctrl_reg::type_id::create("ctrl");
status = dma_status_reg::type_id::create("status");
len = dma_len_reg::type_id::create("len");
key = dma_key_reg::type_id::create("key");
count = dma_count_reg::type_id::create("count");
ctrl.configure(this); ctrl.build();
status.configure(this); status.build();
len.configure(this); len.build();
key.configure(this); key.build();
count.configure(this); count.build();
// One APB map: base 0, 4-byte bus, little endian.
map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
map.add_reg(ctrl, 'h00, "RW");
map.add_reg(status, 'h04, "RW");
map.add_reg(len, 'h08, "RW");
map.add_reg(key, 'h0C, "RW");
map.add_reg(count, 'h10, "RO");
lock_model();
endfunction
endclassPolicy | On a write of | On a read | DMA example |
|---|---|---|---|
RW | field = w | no effect | irq_en, LEN, KEY |
RO | no effect | no effect | busy, COUNT |
WO | field = w | reads are not predicted | go |
W1C | each 1 in w clears that bit | no effect | done, err |
W1S | each 1 in w sets that bit | no effect | (none) |
RC | no effect | field clears to 0 | (none) |
WRC | field = w | field clears to 0 | (none) |
W1T | each 1 in w toggles that bit | no effect | (none) |
The library defines about 25 policies, including W0C, W1SRC, WO1 and NOACCESS, and uvm_reg_field::define_access() adds your own name. A policy only decides how the model predicts; side effects such as "writing CTRL.GO also sets STATUS.BUSY" need a callback, which Register Access covers.
The DMA has no memory, but a later revision with a descriptor RAM would add one like this. uvm_mem is created with new() (it takes the size and word width), configured with only its parent and an optional HDL path, and added to the map at an offset. A memory has no mirror: each access goes to the DUT and nothing remembers the result.
// snippet
// Inside a block's build(): a 256-word descriptor RAM at offset 0x100.
desc_ram = new("desc_ram", 256, 32, "RW", UVM_NO_COVERAGE);
desc_ram.configure(this);
map.add_mem(desc_ram, 'h100, "RW");The quickest proof that the field descriptions match the RTL is the built-in reset check. dma_ral_reset_test runs uvm_reg_hw_reset_seq, which reads every register over the bus and compares each field that has a reset value with the model:
UVM_INFO dma_ral_tb.sv(289) @ 155000: uvm_test_top [RAL] reset values checked (registers whose fields are all volatile are skipped)
UVM_INFO dma_tb.sv(411) @ 155000: uvm_test_top.env.sb [SB] 0 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0Zero errors means every reset value in a configure() call matches the hardware. Change one, for example LEN's reset to 16'h1, and this test is the one that fails. The scoreboard's "0 words" is expected: a register-only test moves no data, so it waives the empty-scoreboard check.
GO's policy is honest about writes but not about the hardware's intent. After rm.ctrl.write(st, 3), the WO prediction stores GO=1 in the mirror, even though the DMA's GO reads 0 a cycle later. For a second job, the natural code is rm.ctrl.go.set(1); rm.ctrl.update(st);. update() writes only when desired differs from mirrored; both say 1, so nothing is written and the second job never starts. The bench sidesteps this by writing CTRL directly for every job, with a comment saying why.
Two other modelling slips are worth knowing. Bits that no field covers, such as LEN's bits 31:16, are left out of the mirror compare, so a DUT that wrongly stores data there passes every check; model reserved bits as RO fields if you want them checked. And a field marked non-volatile that the hardware does change (COUNT, say) turns every mid-job mirror check into a false failure.
busy configured volatile, and what does that turn off?The DMA sets BUSY when a job starts and clears it when the job ends, with no bus write, so the mirror cannot track it. Volatile tells the model so: the library sets the field's compare to off, so mirror(UVM_CHECK) will not report a mismatch on it, and reading its mirrored value draws a warning in the 1800.2 library.
is_rand = 1 and calls rm.randomize(). What happens to COUNT?Nothing. COUNT is RO, and the library forces is_rand to 0 for policies software cannot set, so its value is not randomised. Only LEN, KEY and irq_en (RW, is_rand 1) take new desired values.
No. No field covers bits 31:16, and the mirror compare uses only modelled bits (the valid bit mask in the RegModel error is 0xffff). Frontdoor writes drive 0 into those bits, and the reset and bit-bash sequences skip them. Model the gap as an RO field with reset 0 to get it checked.
Next: the registers exist, but a test still needs addresses on a bus. Maps, Offsets and Endianness places them.