The DMA answers any access outside its five registers with PSLVERR. You need two new kinds of stimulus: register traffic that only ever hits real registers, and an error test that only ever hits the holes. The APB agent is shared with three other teams and you are not allowed to change it. Its sequences, its driver and its monitor all speak apb_item.
This lesson shows how to get both kinds of traffic by extending the item, why every extension needs its own registration line, and why the sequences that send these items should still be typed on the base class.
A derived item is a subclass of the agent's item. It inherits every field and every constraint, and adds more. Constraints in a subclass are combined with the parent's (all must hold), so a subclass can only ever narrow the legal space. To widen or change a rule, the subclass redeclares a constraint block with the same name, which replaces the parent's block of that name.
Think of a printed order form with an extra stamp: "vegetarian menu only". The kitchen (the driver) reads the same form it always has. It does not need to know about the stamp; it just never sees a steak. That is the property that makes derived items so cheap: the sequencer and driver are parameterized on apb_item, and a derived object is an apb_item, so it flows through them unchanged.
// snippet: in a test package that imports dma_pkg
class apb_reg_item extends apb_item;
`uvm_object_utils(apb_reg_item) // its own registration: see "What breaks"
constraint in_map_c { addr inside {CTRL, STATUS, LEN, KEY, COUNT}; }
function new(string name = "apb_reg_item");
super.new(name);
endfunction
endclassin_map_c is added to the inherited word_aligned_c; both hold. The register names come from the dma_pkg localparams, so the item and the scoreboard share one register map.
// snippet
class apb_unmapped_item extends apb_item;
`uvm_object_utils(apb_unmapped_item)
bit expect_slverr; // derived, not random
constraint hole_c { !(addr inside {CTRL, STATUS, LEN, KEY, COUNT}); }
function new(string name = "apb_unmapped_item");
super.new(name);
endfunction
function void post_randomize();
super.post_randomize();
expect_slverr = 1; // every legal value of this item is a hole
endfunction
endclasspost_randomize runs after a successful solve and is the right place for fields that follow from the random ones. Here the expectation is trivial, but the pattern is the same for parity, CRC or a length field computed from a payload.
An item that must produce misaligned addresses cannot add a constraint, because word_aligned_c would still forbid them. It redeclares the block by name instead:
// snippet
class apb_misaligned_item extends apb_item;
`uvm_object_utils(apb_misaligned_item)
constraint word_aligned_c { addr[1:0] != 2'b00; } // same name: replaces the parent's block
function new(string name = "apb_misaligned_item");
super.new(name);
endfunction
endclassUse this sparingly and name the class for what it does. A reader who sees word_aligned_c in the base class should not have to hunt through subclasses to learn that it sometimes means the opposite.
The sequences that send these items stay uvm_sequence #(apb_item). A sequence declared as uvm_sequence #(apb_unmapped_item) would not match apb_sequencer, which is uvm_sequencer #(apb_item): the sequence's typed handle to its sequencer and its response type would both be wrong. Instead, the sequence creates an apb_item through the factory, and the factory decides which class to build. Two ways to choose:
Create the derived type directly in a sequence written for it: apb_item it = apb_unmapped_item::type_id::create("it");. Simple and explicit, and it changes only that sequence.
Override from the test, so existing sequences send the new item without any edit: apb_item::type_id::set_type_override(apb_reg_item::get_type());. To aim the override at one sequence or one agent, create items with a context path and use an instance override; Overriding items and sequences teaches exactly that, so it is not repeated here.
A one-access scratch test added to the bench points an apb_rw_seq at a hole (0x14) and checks slverr after finish_item, as a sequence built on apb_unmapped_item would. The DMA answers with PSLVERR, the driver copies it into the item, and the monitor reports it (Verilator, UVM_HIGH):
UVM_INFO dma_tb.sv(151) @ 75000: uvm_test_top.env.apb.mon [APB_MON] RD addr=14 rdata=00000000 SLVERRconvert2string appends SLVERR when the bit is set, so the error is visible in the log without a waveform. A scoreboard that models the register map would treat it as expected for apb_unmapped_item and as an error for any other item.
A team wrote apb_unmapped_item by copying apb_item, deleting the fields, and adding hole_c. They forgot the uvm_object_utils(apb_unmapped_item) line. Everything compiled. The error test overrode apb_item with apb_unmapped_item and passed on the first run, with no PSLVERR anywhere in the log. That should have been the clue: an error test with no errors.
A class without its own registration macro inherits its parent's type_id, so apb_unmapped_item::get_type() returned the proxy for apb_item. The test had registered an override of apb_item with apb_item. The factory built plain base items, hole_c never existed in any object, and every access hit a real register. It is not a compile error, which is what makes it expensive. The fix is one line, and the habit that prevents it is to make every error test check that its error happened at least once, for example by counting slverr responses in check_phase.
apb_reg_item adds in_map_c. A test randomizes it with with { addr == 8'h14; }. What happens?0x14 is aligned, so word_aligned_c is satisfied, but it is not in the register map, so in_map_c is violated. The constraints of the parent, the child and the in-line block must all hold together, so randomize() returns 0 and the bench's check fires a fatal.
rand int unsigned gap_clks to a derived apb_gap_item and overrides apb_item with it. The items carry random gaps, but the bus shows none. Why?A knob is only intent; the stock apb_driver knows nothing about gap_clks, so it ignores it. A knob only works when the driver implements it: either the agent's owners add it to the base item and driver, or you pair the derived item with a derived driver (installed by a factory override, as the bench does with axis_truncating_driver in the next lesson) that $casts and reads it.
The factory decides the class at the moment type_id::create() runs. If the override is registered in the test's build_phase, every later apb_item::type_id::create() call, inside any sequence, returns the derived class. The sequence's code is unchanged because it only ever asked for an apb_item.
Next: once items are flowing, several pieces of code hold handles to the same object. Field macros and item ownership sets out who may write an item at each step, and why the truncating driver clones before it changes anything.