Sequence libraries, macros and default sequences
Read the uvm_do macro family as the calls it makes, organise sequences into API, worker and virtual layers, and recognise a sequence library or a default sequence when inherited code starts stimulus by itself.
- Lesson
- U5.4
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 6 questions
A new test starts three items of its own from run_phase; the driver finishes eight. The base test it extends had set a default sequence for the sequencer's main_phase. Its five items start with that phase, which runs alongside run_phase, so the two sequences shared the driver.
Measured: own api_seq items interleave with the default auto_seq items under FIFO arbitration. A scoreboard expecting three results sees five extra.
Inherited code also hides stimulus behind macros and libraries; this lesson reads each one back into the calls of U4.1 and U5.3.
What a uvm_do line does
uvm_do(req) for an item and uvm_do(c) for a child sequence. Arrows are calls in the measured order.- Create
uvm_createbuilds the object through the factory and sets its sequencer; the variable name becomes the object name.More context
The same first step runs for an item and for a sequence. Which path follows depends only on the object the macro was given.
Follow the calls, values and waits
- call uvm_do(req): Worker W → uvm_create
- An item: grant first, then randomize
start_itemwaits for the grant and runs W'spre_do(1). Only then is the item randomized, so constraints set inpre_dostill count.finish_itemruns W'smid_doand hands the item over.More context
Measured order: parent.pre_do(1), item.randomize, parent.mid_do, parent.post_do. In the example, W's own item reaches the driver at 0 ns.
Follow the calls, values and waits
- data req: uvm_create → Item path
- data req: Item path → Driver
- Waiting: Worker W — in finish_item until item_done
- The item is done: post_do
The driver calls
item_done. W'spost_doruns and the macro returns.More context
In the example the driver takes 10 ns:
10 ns W post_do req.post_dois the first of W's code to see the finished item.Follow the calls, values and waits
- call item_done(): Driver → Item path
- Released: Worker W
- A sequence: randomize first, then start
For
uvm_do(c)the child is created and randomized before anything else, then started withcall_pre_post = 0. Itspre_bodyandpost_bodynever run, the trap of U5.3's hook.More context
Measured order: child.randomize, child.pre_start, parent.pre_do(0), parent.mid_do, child.body. W's
pre_do(0)andmid_dowrap the child after itspre_start, as in U5.3.Follow the calls, values and waits
- call uvm_do(c): Worker W → uvm_create
- data child C: uvm_create → Sequence path
- call c.start(sqr, this, -1, 0): Sequence path → Child C
- Waiting: Worker W — in c.start
- The child ends: post_do, post_start
When C's body returns, W's
post_doruns, then C'spost_start, andstartreturns to W.More context
The example's child
hishows the same order:20 ns W post_do hi, then20 ns hi post_start.Follow the calls, values and waits
- return c.start returns: Child C → Sequence path
- Released: Worker W
Read the diagram as text
- Worker W: Runs the macro in body()
- uvm_create: Factory create, sequencer set
- Item path: start_item, randomize, finish_item
- Driver: Gets the item after mid_do
- Sequence path: randomize, then start(..., 0)
- Child C: pre_start, body, post_start
- Worker W → uvm_create: uvm_do
- uvm_create → Item path: is_item()
- Item path → Driver: item
- uvm_create → Sequence path: a sequence
- Sequence path → Child C: c.start(sqr, this, -1, 0)
The macro decides the path at run time from the object it is given.
| Macro | Calls | Use it when |
|---|---|---|
uvm_do(x, SEQR, PRIORITY, CONSTRAINTS) | uvm_create, then uvm_rand_send | The common case; every argument after the first is optional |
uvm_create(x, SEQR) | Factory create and set_item_context | You fill fields or change rand_mode before sending |
uvm_send(x, PRIORITY) | start_item, finish_item; a sequence is started | The object is already filled in: no randomization |
uvm_rand_send(x, PRIORITY, CONSTRAINTS) | As uvm_send, with randomize() with CONSTRAINTS | An object created earlier, randomized now |
uvm_do_with, uvm_do_pri, uvm_do_on, uvm_do_on_with | uvm_do with some arguments filled in | Older code; uvm_do_on(x, sqr) is uvm_do(x, sqr), which targets another sequencer, as a virtual sequence does |
macros/uvm_sequence_defines.svh and its deprecated/ wrappers, all compiled in with no define. A failed randomization is only a warning, RNDFLD.
Writing the calls out, as U4.1 did, keeps the blocking points visible; the macros save lines and hide them. Read one as its expansion and you can predict every hook; the first quiz question asks you to.
The example and the quiz use uvm_do_with(req, {...}), the older spelling you will meet most often in inherited code. In the 2020 form it is uvm_do(req, , , {...}): the constraints are the fourth argument.
Three layers of sequences
- The test starts one thing
The test's
run_phasestarts the virtual sequence and waits for it. Choice, order and parallelism live there (U10.1).More context
A virtual sequence sends no items, so it can run on a null sequencer. It holds handles to the agents' sequencers instead.
Follow the calls, values and waits
- call vseq.start(null): Test → Virtual sequence
- Waiting: Test — in vseq.start
- Workers per interface
The virtual sequence starts a worker on each agent's sequencer. A worker composes small actions into a job.
More context
The two starts are independent: each worker targets a different sequencer, so neither waits for the other's grants.
Follow the calls, values and waits
- call start wrap_job on a_sqr: Virtual sequence → Worker: wrap_job
- call start B's worker on b_sqr: Virtual sequence → Worker on agent B
- Waiting: Virtual sequence — until its workers return
- API sequences do one thing
Each API sequence sends one protocol action: here one value from a range. The example's worker runs
write_hifirst.More context
write_hiconstrains its one item to 240 through 254. The worker does not know how the item reaches the pins.Follow the calls, values and waits
- call start write_hi: Worker: wrap_job → API: write_hi, write_wrap
- data item: x from 240 to 254: API: write_hi, write_wrap → Agent A sequencer
- Then the wrap case
The worker's second action is
write_wrap, which sends 255, whose result wraps to 0.More context
In the example the worker's children run back to back:
hifrom 10 to 20 ns,wrapfrom 20 to 30 ns.Follow the calls, values and waits
- call start write_wrap: Worker: wrap_job → API: write_hi, write_wrap
- data item: x = 255: API: write_hi, write_wrap → Agent A sequencer
- Returns climb back up
When its last API sequence returns, the worker returns to the virtual sequence; when every worker is done, the virtual sequence returns to the test.
More context
A test that raises its objection around
vseq.starttherefore runs exactly as long as the plan in the upper layer.Follow the calls, values and waits
- return wrap_job returns: Worker: wrap_job → Virtual sequence
- return vseq returns: Virtual sequence → Test
- Released: Virtual sequence
- Released: Test
Read the diagram as text
- Test: run_phase starts one sequence
- Virtual sequence: Coordinates; sends no items
- Worker: wrap_job: Program a value, then the wrap case
- Worker on agent B: Another interface's job
- API: write_hi, write_wrap: One item each, in the agent package
- Agent A sequencer: Arbitrates the items
- Test → Virtual sequence: vseq.start(null)
- Virtual sequence → Worker: wrap_job: uvm_do(w, a_sqr)
- Virtual sequence → Worker on agent B: uvm_do(b, b_sqr)
- Worker: wrap_job → API: write_hi, write_wrap: uvm_do(hi)
- API: write_hi, write_wrap → Agent A sequencer: one item
Change a test by changing the upper layers; the API layer ships, unchanged, with the agent.
An agent often wraps each API sequence in a task such as write(value, sqr, parent), so its callers never see start_item. The example's three API sequences send a value below 16, one from 240 to 254, and 255, whose result wraps to 0. Workers and virtual sequences can then change without touching the agent.
| A new test needs | Change | Leave alone |
|---|---|---|
| A different mix or order of existing jobs, or two interfaces at once | The virtual sequence | Workers and API sequences |
| A new job, such as one corrupted write in the middle of the wrap job | A worker, or a new worker extending it | The API sequences and the agent |
| A new protocol action the agent cannot yet perform | A new API sequence in the agent package | Existing tests, which keep working |
The lower the layer, the more benches share it, so changes move upward whenever they can.
Sequence libraries
uvm_sequence_library is a sequence that runs other sequences chosen from a registered list. Register types with uvm_add_to_seq_lib or add_typewide_sequence, and call init_sequence_library() in the constructor. Each pick is created through the factory, named <type>:<n>, and started with uvm_rand_send: a child with call_pre_post = 0.
selection_mode | Each pick | Measured or source |
|---|---|---|
UVM_SEQ_LIB_RAND | Any registered sequence; repeats are possible | Source |
UVM_SEQ_LIB_RANDC | Cycles through all before repeating | Example: write_hi:1 write_lo:2 write_wrap:3, then a second cycle |
UVM_SEQ_LIB_ITEM | A bare randomized item, no sequence | Source |
UVM_SEQ_LIB_USER | Your select_sequence override | Source |
min_random_count and max_random_count, both 10 by default, bound how many picks it makes.
Register each type once. uvm_add_to_seq_lib(write_lo, incr_lib) inside each API class adds it to the library type, and the instance method add_sequence adds it to one library object. The example calls the static add_typewide_sequence in the constructor, which is harmless here because it builds one library, but repeats the registration for every further instance.
The library reads its mode and counts with typed lookups, uvm_config_db #(uvm_sequence_lib_mode) and #(int unsigned), scoped to where it runs. A +uvm_set_config_int plusarg stores uvm_bitstream_t, which those lookups do not see; copy a command-line value into the typed setting yourself.
Default sequences, phase by phase
A default sequence is one the sequencer starts by itself when a runtime phase begins. The scope is the sequencer's path plus the phase, the field is default_sequence, and the value is a type or an instance:
uvm_config_db #(uvm_object_wrapper)::set(this, "sqr.main_phase", "default_sequence",
incr_lib::get_type());
// to switch an inherited one off: the same scope and field, with null
uvm_config_db #(uvm_object_wrapper)::set(this, "sqr.main_phase", "default_sequence", null);Phase starts
The sequencer looks up the setting, creates and randomizes the sequence and starts it in its own process.
Phase must stay open
Nothing keeps the phase alive except objections. A plain sequence raises none. Measured with nothing else in the test raising an objection, the phase ended at 0 ns and a five-item default sequence was killed before it completed an item. With
set_automatic_phase_objection(1)in its constructor, all 5 ran, to 50 ns. A library raises its own objection.Phase ends
A default sequence still running when its phase ends is killed, with everything U5.3 says a kill leaves behind.
The example sets incr_lib as the default sequence of sqr.main_phase, in RANDC mode with six picks; the hand-driven part runs in pre_main_phase, so the two never share the driver.
Both features work, and the Cookbook recommends against both. It calls the library "not the preferred method" for starting sequences and automatic starting "almost always the wrong thing to do". A random pick from a list gives up the test's control of what runs and in what order. Stimulus that starts because a phase began is hard to coordinate with the rest of the test, as the hook shows. Prefer a virtual sequence started from the test's run_phase.
Try it
The worker's log reads the macros back into calls. For its own item, pre_do(is_item=1) comes at the grant and mid_do just before the driver takes it. For hi and wrap, each child's pre_start comes first, then the worker's pre_do(is_item=0) and mid_do; no child prints pre_body, because the macro starts children with call_pre_post = 0.
The library then runs by itself in main_phase, two RANDC cycles through the three API sequences. Change the mode to UVM_SEQ_LIB_RAND and a sequence may repeat within three picks. Move the worker into main_phase and its items interleave with the library's, as in the hook.
Transfer exercise: a base test you inherit sets a default sequence for sqr.reset_phase. Write the one line that turns it off in your derived test, and say what the driver would see if you forgot it.
0 ns W pre_do(is_item=1) 0 ns W mid_do req 10 ns W post_do req 10 ns hi pre_start 10 ns W pre_do(is_item=0) 10 ns W mid_do hi 20 ns W post_do hi 20 ns hi post_start 20 ns wrap pre_start 20 ns W pre_do(is_item=0) 20 ns W mid_do wrap 30 ns W post_do wrap 30 ns wrap post_start 30 ns test worker sent: W hi wrap UVM_INFO uvm/seq/uvm_sequence_library.svh(713) @ 30: uvm_test_top.sqr@@incr_lib [SEQLIB/START] Starting sequence library incr_lib in main phase: 6 iterations in mode UVM_SEQ_LIB_RANDC 90 ns test library ran: write_hi:1 write_lo:2 write_wrap:3 write_wrap:4 write_lo:5 write_hi:6 UVM_ERROR : 0Check yourself
Remember
uvm_doisuvm_createplusuvm_rand_send: an item is randomized after its grant; a sequence is randomized, then started withcall_pre_post = 0.uvm_sendsends a filled-in object without randomizing;uvm_do(x, sqr)targets another sequencer (older code:uvm_do_on).- API sequences do one action and ship with the agent; workers compose them; a virtual sequence started from the test coordinates the workers.
- A default sequence is set with scope
"sqr.<phase>_phase"and field"default_sequence", needs an objection to run, is killed when its phase ends, and is switched off with a null at the same scope. - Libraries and default sequences work, but the test loses control of what runs and when; prefer a virtual sequence from
run_phase.