Your DMA regression has three tiny tests that each read one register in a loop: STATUS, COUNT, KEY. A lead asks for one test that mixes them differently on every seed. Someone suggests a sequence library; someone else says libraries are "magic nobody can debug". Then a third engineer tries one in UVM_SEQ_LIB_ITEM mode, and the log says it ran in UVM_SEQ_LIB_RAND instead. What does a library actually do, step by step, and why did it change its own mode?
Big stimulus is built from small stimulus. A sub-sequence is a sequence started from inside another sequence's body, on the same or another sequencer. The register programming of a DMA job is three single-register sequences in a row; a long stream is several packets. Composing keeps each small piece testable and reusable, and the parent argument of start() keeps the family together for naming, locks and priority.
A sequence library adds choice. It is itself a sequence that holds a list of registered sequence types and, when started, runs a number of them picked by a selection mode. Think of a jukebox on shuffle: you load it with records (registered sequences), pick a shuffle rule (random, or every record once before any repeat), and say how many songs to play. Each seed gives a different playlist from the same records.
Two details in the picture matter later. The picked sequences are created through the factory, so a type override replaces them inside the library too. And the configuration is read in pre_randomize, which runs when you call randomize() on the library, not when you start it.
The bench's dma_vseq programs KEY, LEN and CTRL through a helper that starts one apb_rw_seq per register. Packaged as a plain APB sequence, the same idea reads like this. Every child is started on the parent's own sequencer with this as the parent:
// snippet
class dma_prog_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(dma_prog_seq)
rand bit [31:0] key;
rand bit [15:0] len;
constraint len_c { len inside {[1:16]}; }
function new(string name = "dma_prog_seq");
super.new(name);
endfunction
task write_reg(bit [7:0] addr, bit [31:0] data);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = 1; s.data = data;
s.start(m_sequencer, this); // same sequencer, this as parent
endtask
task body();
write_reg(KEY, key);
write_reg(LEN, len);
write_reg(CTRL, 32'h3); // IRQ_EN | GO
endtask
endclassBecause the children belong to dma_prog_seq, a lock() taken at the top of its body would let all three writes through and keep every other sequence out, which is how you make register programming atomic (lock vs grab). Without a lock, the three children's items still compete with everything else on the APB sequencer one item at a time; how the sequencer chooses, and what priority does in each mode, is the home topic of Inside the Sequencer.
For the scratch run, three one-access sequences share a base class that reads one register; each derived class picks the address. The library registers them with ` uvm_add_to_seq_lib `, and its constructor must call init_sequence_library()`, which copies the registered types into this instance's list:
// snippet
class dma_reg_rd_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(dma_reg_rd_seq)
bit [7:0] reg_addr = STATUS;
function new(string name = "dma_reg_rd_seq");
super.new(name);
endfunction
task body();
apb_item it = apb_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { addr == reg_addr; write == 0; })
`uvm_fatal("RAND", "apb_item randomize failed")
finish_item(it);
endtask
endclass
// dma_status_rd_seq, dma_count_rd_seq and dma_key_rd_seq extend it and set reg_addr in new()
class dma_rd_lib extends uvm_sequence_library #(apb_item);
`uvm_object_utils(dma_rd_lib)
`uvm_sequence_library_utils(dma_rd_lib)
function new(string name = "dma_rd_lib");
super.new(name);
init_sequence_library(); // without it: SEQLIB/NOSEQS
endfunction
endclass
`uvm_add_to_seq_lib(dma_status_rd_seq, dma_rd_lib)
`uvm_add_to_seq_lib(dma_count_rd_seq, dma_rd_lib)
`uvm_add_to_seq_lib(dma_key_rd_seq, dma_rd_lib)The test sets the mode and the count range, randomizes the library (which picks sequence_count between the minimum and maximum), and starts it on the APB sequencer like any sequence:
// snippet: dma_lib_test::run_phase
dma_rd_lib lib = dma_rd_lib::type_id::create("lib");
phase.raise_objection(this);
lib.selection_mode = UVM_SEQ_LIB_RANDC;
lib.min_random_count = 6;
lib.max_random_count = 6;
if (!lib.randomize()) `uvm_fatal("RAND", "dma_rd_lib randomize failed")
lib.start(env.apb.sqr);
#100ns;
phase.drop_objection(this);Selection mode | What runs |
|---|---|
| A random registered sequence each iteration |
| Random cyclic: every registered sequence once before any repeats |
| No sequences at all: randomized items of the library's item type |
| Whatever your |
The defaults are 10 for min_random_count, max_random_count and sequence_count. The same three settings can come from config_db instead, as a uvm_sequence_library_cfg under the key default_sequence.config in the scope of the sequencer and phase, which is how a library is used as a phase's default sequence. Setting them on the object, as above, is easier to read.
The scratch run of dma_lib_test with +UVM_VERBOSITY=UVM_HIGH (the distribution line is printed at UVM_HIGH):
UVM_INFO uvm_sequence_library.svh(752) @ 0: uvm_test_top.env.apb.sqr@@lib [SEQLIB/START] Starting sequence library dma_rd_lib in unknown phase: 6 iterations in mode UVM_SEQ_LIB_RANDC
UVM_INFO uvm_sequence_library.svh(829) @ 275000: uvm_test_top.env.apb.sqr@@lib [SEQLIB/END] Ending sequence library in phase unknown
UVM_INFO uvm_sequence_library.svh(831) @ 275000: uvm_test_top.env.apb.sqr@@lib [SEQLIB/DSTRB] '{"dma_count_rd_seq":'h2, "dma_key_rd_seq":'h2, "dma_status_rd_seq":'h2}6 iterations: the randomized count, forced to 6 by equal minimum and maximum.
`'h2` for each type: RANDC ran every sequence once before repeating, twice round. With RAND the counts would be uneven.
"in unknown phase": the library looks for a starting phase, which only exists when it runs as a phase's default sequence. Started from a test, there is none; that is harmless.
275000 ps for six reads: one APB read every 40 ns after reset. The library's traffic is ordinary traffic on the APB sequencer: if a poller were running, the two would interleave item by item.
Here is the third engineer's run from the opening: the same test with selection_mode = UVM_SEQ_LIB_ITEM. The library refused, with an error that blames a virtual sequence nobody wrote:
UVM_ERROR uvm_sequence_library.svh(712) @ 0: reporter@@lib [SEQLIB/VIRT_SEQ] selection_mode cannot be UVM_SEQ_LIB_ITEM when running as a virtual sequence. Using UVM_SEQ_LIB_RAND mode
UVM_INFO uvm_sequence_library.svh(752) @ 0: uvm_test_top.env.apb.sqr@@lib [SEQLIB/START] Starting sequence library dma_rd_lib in unknown phase: 6 iterations in mode UVM_SEQ_LIB_RANDLook at the reporter: reporter@@lib, not env.apb.sqr@@lib. The check ran inside randomize() (from pre_randomize), before start() had given the library a sequencer. With no sequencer, the library cannot tell it will run on a real one, so it treats itself as virtual, reports an error and switches to RAND. The fix is one line before the randomize: lib.set_sequencer(env.apb.sqr);. With it, ITEM mode runs, and shows what it really does:
UVM_INFO uvm_sequence_library.svh(752) @ 0: uvm_test_top.env.apb.sqr@@lib [SEQLIB/START] Starting sequence library dma_rd_lib in unknown phase: 6 iterations in mode UVM_SEQ_LIB_ITEM
UVM_INFO dma_tb.sv(151) @ 75000: uvm_test_top.env.apb.mon [APB_MON] WR addr=3c data=baac3d01 SLVERR
UVM_INFO dma_tb.sv(151) @ 115000: uvm_test_top.env.apb.mon [APB_MON] RD addr=d4 rdata=00000000 SLVERR
UVM_INFO uvm_sequence_library.svh(831) @ 275000: uvm_test_top.env.apb.sqr@@lib [SEQLIB/DSTRB] '{"apb_item":'h6}Six raw apb_items, randomized only by the item's own constraints: random addresses, mostly unmapped, so mostly SLVERR, and random writes. None of the three registered sequences ran. ITEM mode is a quick way to spray a bus with legal-by-constraint items, which is useful for a protocol agent's own tests and almost never what a design-level test wants. Two lessons generalize: configuration that is read in pre_randomize needs the sequencer set first, and a library's distribution line is the first thing to read when it does not do what you expected.
axis_pkt_seq, and a test adds axis_pkt_seq::type_id::set_type_override(axis_short_pkt_seq::get_type()). Which sequence does the library run?axis_short_pkt_seq. The library creates each picked type through the factory, so the type override applies inside it exactly as it would anywhere else.
dma_prog_seq starts its three register writes with s.start(m_sequencer), without this. What changes when it runs on its own, and what changes if it is ever wrapped in a lock()?On its own, only the names (apb.sqr@@s instead of apb.sqr@@prog.s) and anything keyed on them, such as config_db lookups. Inside a lock, the children are no longer part of the lock holder's family, so the first write is blocked by its own parent's lock and the sequence deadlocks.
UVM_SEQ_LIB_RANDC mode with three registered sequences runs 7 iterations. Can one sequence run three times?Yes, but only in the third cycle: RANDC runs all three once (3 iterations), then all three again (6), then one more. Two of them run twice and one runs three times.
Next: the bench's own virtual sequence needs none of the virtual-sequencer machinery from earlier in this topic. Virtual Sequences Without a Virtual Sequencer takes dma_vseq apart.