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.

The driver's eight items, 10 ns each
01234567time (ns)010203040506070item fromowndefaultowndefaultowndefaultthe inherited default sequence

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

One macro, two pathsA worker sequence W runs uvm_do(req) for an item and uvm_do(c) for a child sequence. Arrows are calls in the measured order.
One macro, two pathsA worker sequence W runs `uvm_do(req)` for an item and `uvm_do(c)` for a child sequence. Arrows are calls in the measured order. sequence Worker W Runs the macro inbody() macro uvm_create Factory create,sequencer set macro Item path start_item,randomize, finish_item driver Driver Gets the item aftermid_do macro Sequence path randomize, thenstart(..., 0) sequence Child C pre_start, body,post_start uvm_dois_item()itema sequencec.start(sqr, this, -1,0)
  1. Create

    uvm_create builds 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
    1. call uvm_do(req): Worker W → uvm_create
  2. An item: grant first, then randomize

    start_item waits for the grant and runs W's pre_do(1). Only then is the item randomized, so constraints set in pre_do still count. finish_item runs W's mid_do and 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
    1. data req: uvm_create → Item path
    2. data req: Item path → Driver
    3. Waiting: Worker W — in finish_item until item_done
  3. The item is done: post_do

    The driver calls item_done. W's post_do runs and the macro returns.

    More context

    In the example the driver takes 10 ns: 10 ns W post_do req. post_do is the first of W's code to see the finished item.

    Follow the calls, values and waits
    1. call item_done(): Driver → Item path
    2. Released: Worker W
  4. A sequence: randomize first, then start

    For uvm_do(c) the child is created and randomized before anything else, then started with call_pre_post = 0. Its pre_body and post_body never 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) and mid_do wrap the child after its pre_start, as in U5.3.

    Follow the calls, values and waits
    1. call uvm_do(c): Worker W → uvm_create
    2. data child C: uvm_create → Sequence path
    3. call c.start(sqr, this, -1, 0): Sequence path → Child C
    4. Waiting: Worker W — in c.start
  5. The child ends: post_do, post_start

    When C's body returns, W's post_do runs, then C's post_start, and start returns to W.

    More context

    The example's child hi shows the same order: 20 ns W post_do hi, then 20 ns hi post_start.

    Follow the calls, values and waits
    1. return c.start returns: Child C → Sequence path
    2. 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.

MacroCallsUse it when
uvm_do(x, SEQR, PRIORITY, CONSTRAINTS)uvm_create, then uvm_rand_sendThe common case; every argument after the first is optional
uvm_create(x, SEQR)Factory create and set_item_contextYou fill fields or change rand_mode before sending
uvm_send(x, PRIORITY)start_item, finish_item; a sequence is startedThe object is already filled in: no randomization
uvm_rand_send(x, PRIORITY, CONSTRAINTS)As uvm_send, with randomize() with CONSTRAINTSAn object created earlier, randomized now
uvm_do_with, uvm_do_pri, uvm_do_on, uvm_do_on_withuvm_do with some arguments filled inOlder 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

Who starts whomArrows are start calls. API sequences belong to the agent; workers and the virtual sequence belong to the test library.
Who starts whomArrows are start calls. API sequences belong to the agent; workers and the virtual sequence belong to the test library. test Test run_phase starts onesequence virtual Virtual sequence Coordinates; sends noitems worker Worker: wrap_job Program a value, thenthe wrap case worker Worker on agentB Another interface'sjob api API: write_hi,write_wrap One item each, in theagent package sequencer Agent Asequencer Arbitrates the items vseq.start(null)uvm_do(w, a_sqr)uvm_do(b, b_sqr)uvm_do(hi)one item
  1. The test starts one thing

    The test's run_phase starts 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
    1. call vseq.start(null): Test → Virtual sequence
    2. Waiting: Test — in vseq.start
  2. 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
    1. call start wrap_job on a_sqr: Virtual sequence → Worker: wrap_job
    2. call start B's worker on b_sqr: Virtual sequence → Worker on agent B
    3. Waiting: Virtual sequence — until its workers return
  3. API sequences do one thing

    Each API sequence sends one protocol action: here one value from a range. The example's worker runs write_hi first.

    More context

    write_hi constrains its one item to 240 through 254. The worker does not know how the item reaches the pins.

    Follow the calls, values and waits
    1. call start write_hi: Worker: wrap_job → API: write_hi, write_wrap
    2. data item: x from 240 to 254: API: write_hi, write_wrap → Agent A sequencer
  4. 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: hi from 10 to 20 ns, wrap from 20 to 30 ns.

    Follow the calls, values and waits
    1. call start write_wrap: Worker: wrap_job → API: write_hi, write_wrap
    2. data item: x = 255: API: write_hi, write_wrap → Agent A sequencer
  5. 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.start therefore runs exactly as long as the plan in the upper layer.

    Follow the calls, values and waits
    1. return wrap_job returns: Worker: wrap_job → Virtual sequence
    2. return vseq returns: Virtual sequence → Test
    3. Released: Virtual sequence
    4. 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 needsChangeLeave alone
A different mix or order of existing jobs, or two interfaces at onceThe virtual sequenceWorkers and API sequences
A new job, such as one corrupted write in the middle of the wrap jobA worker, or a new worker extending itThe API sequences and the agent
A new protocol action the agent cannot yet performA new API sequence in the agent packageExisting 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_modeEach pickMeasured or source
UVM_SEQ_LIB_RANDAny registered sequence; repeats are possibleSource
UVM_SEQ_LIB_RANDCCycles through all before repeatingExample: write_hi:1 write_lo:2 write_wrap:3, then a second cycle
UVM_SEQ_LIB_ITEMA bare randomized item, no sequenceSource
UVM_SEQ_LIB_USERYour select_sequence overrideSource

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:

Scope "sqr.main_phase", field "default_sequence". A wrong spelling of that pair leaves the default running; the second quiz question tries one.
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);
  1. Phase starts

    The sequencer looks up the setting, creates and randomizes the sequence and starts it in its own process.

  2. 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.

  3. 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.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 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 : 0

Check yourself

Predict the outputWorker W runs uvm_do(c) on a child sequence C. W records its pre_do, mid_do and post_do; C records its randomization, its four start hooks and its body; C's item records its own randomization. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
string trail = "";
function automatic void note(string s);
  if (trail != "") trail = {trail, " "};
  trail = {trail, s};
endfunction

class item_c extends uvm_sequence_item;
  `uvm_object_utils(item_c)
  rand bit [7:0] x;
  function new(string name = "item_c"); super.new(name); endfunction
  function void post_randomize(); note("item.randomize"); endfunction
endclass

class child_seq extends uvm_sequence #(item_c);
  `uvm_object_utils(child_seq)
  function new(string name = "child_seq"); super.new(name); endfunction
  function void pre_randomize(); note("C.randomize"); endfunction
  task pre_start();  note("C.pre_start");  endtask
  task pre_body();   note("C.pre_body");   endtask
  task post_body();  note("C.post_body");  endtask
  task post_start(); note("C.post_start"); endtask
  task body(); note("C.body"); `uvm_do_with(req, { x == 7; }) endtask
endclass

class worker extends uvm_sequence #(item_c);
  `uvm_object_utils(worker)
  child_seq c;
  function new(string name = "worker"); super.new(name); endfunction
  task pre_do(bit is_item); note($sformatf("W.pre_do(%0d)", is_item)); endtask
  function void mid_do(uvm_sequence_item this_item);  note("W.mid_do");  endfunction
  function void post_do(uvm_sequence_item this_item); note("W.post_do"); endfunction
  task body(); `uvm_do(c) endtask
endclass

class drv_c extends uvm_driver #(item_c);
  `uvm_component_utils(drv_c)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      #10;
      seq_item_port.item_done();
    end
  endtask
endclass

class order_test extends uvm_test;
  `uvm_component_utils(order_test)
  drv_c drv;
  uvm_sequencer #(item_c) sqr;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    drv = drv_c::type_id::create("drv", this);
    sqr = uvm_sequencer #(item_c)::type_id::create("sqr", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
  task run_phase(uvm_phase phase);
    worker w = worker::type_id::create("w");
    phase.raise_objection(this);
    w.start(sqr);
    $display("%s", trail);
    phase.drop_objection(this);
  endtask
endclass

module tb;
  initial run_test("order_test");
endmodule
Spot the bugA base test sets a five-item default sequence, auto_seq, for sqr.main_phase. The derived test runs three items of its own from run_phase and must switch the default off. Each option is the line the derived test adds to build_phase after super.build_phase(phase). Which one leaves the default sequence running?
ConceptA sequencer's main_phase default sequence is a plain five-item sequence that raises no objection, and nothing else in the test raises an objection in any phase, so nothing else holds the run open. How many items does the driver finish?
ConceptA sequence built an item with uvm_create(req) and set every field by hand for a directed test. Which macro sends it without changing those fields?
ConceptAn error test needs one corrupted write in the middle of the wrap job; every other test keeps the normal job. The bench has API sequences in the agent package, worker sequences and a virtual sequence. Where does the change belong?
ConceptThe Cookbook advises against the sequence library and against default sequences started by the sequencer. What is its reason?

Remember

  • uvm_do is uvm_create plus uvm_rand_send: an item is randomized after its grant; a sequence is randomized, then started with call_pre_post = 0.
  • uvm_send sends 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.