The sequence lifecycle

Know the order of a sequence's hooks and states, what kill and stop_sequences leave behind, and what happens to responses that nobody reads.

Lesson
U5.3
Time
30 min
Runs on
Verilator with UVM
Check
5 questions

A base sequence fetches its register model handle in pre_body(). Used on its own it works. Started from a virtual sequence as a child, with call_pre_post set to 0, it crashes on a null handle: pre_body never ran. The handle belonged in pre_start, which runs for every start.

TimePrinted byHookWhy it ran
10 nsPpre_start, pre_bodyP is a root sequence; both run
10 nsKpre_startThe child's pre_start always runs
10 nsPpre_do(is_item=0), mid_doP's hooks wrap the child it starts
10 nsKpre_do(is_item=1), mid_doK's own item, after the grant
20 nsKpost_doK's item is done
20 nsPpost_doP's hook closes the child it started
20 nsKpost_startNo post_body: P started K with call_pre_post = 0
20 nsPpost_body, post_startP ends normally

The measured hook order when sequence P starts child K with call_pre_post = 0. K's pre_body and post_body never run.

start() runs a fixed script

P starts child K; K sends one itemArrows are calls and the item's trip to the driver. Follow the measured run from the hook table: P is a root sequence, K is its child with call_pre_post = 0, and the driver takes 10 ns.
P starts child K; K sends one itemArrows are calls and the item's trip to the driver. Follow the measured run from the hook table: P is a root sequence, K is its child with `call_pre_post = 0`, and the driver takes 10 ns. caller Test Calls p.start(sqr) sequence Parent P pre_start, pre_body,then body sequence Child K pre_start only: nopre_body hooks P's item hooks pre_do(0), mid_do,post_do wrap K arbiter Sequencer Grants K's item driver Driver 10 ns per item start(sqr)k.start(sqr, this, -1,0)calls P hooksstart_itemitem
  1. Root sequence set-up10 ns (illustrative)

    The test starts P. Because P has no parent and call_pre_post defaults to 1, pre_start runs, then pre_body, then body.

    More context

    P starts at 10 ns because the test first ran R, which finished at 10 ns. Log lines 10 ns P pre_start and 10 ns P pre_body.

    Follow the calls, values and waits
    1. call p.start(sqr): Test → Parent P
    2. Waiting: Test — in p.start until 20 ns
  2. Child set-up10 ns (illustrative)

    P's body calls k.start(sqr, this, -1, 0). K's pre_start runs; its pre_body is skipped because the fourth argument is 0. Then P's own pre_do(0) and mid_do run around the child.

    More context

    The parent hooks run with is_item = 0: they wrap a sequence, not an item. They run after K's pre_start, as the log shows.

    Follow the calls, values and waits
    1. call k.start(sqr, this, -1, 0): Parent P → Child K
    2. call pre_do(0), mid_do: Child K → P's item hooks
    3. Waiting: Parent P — in k.start
  3. K's item10 ns (illustrative)

    K's body calls start_item. After the grant, K's pre_do(1) runs, then mid_do, and the item goes to the driver.

    More context

    These are K's own item hooks, with is_item = 1, in K's own object. mid_do is the last hook before the driver takes the item.

    Follow the calls, values and waits
    1. data K item 0: Child K → Sequencer
    2. data K item 0: Sequencer → Driver
    3. Waiting: Child K — finish_item waits for item_done
  4. Item done20 ns (illustrative)

    The driver calls item_done. K's post_do runs and finish_item returns. K's body ends: its state becomes UVM_ENDED.

    More context

    The driver takes 10 ns per item. Log lines 20 ns K post_do and 20 ns K finish_item 0 returned.

    Follow the calls, values and waits
    1. call item_done(): Driver → Sequencer
    2. Released: Child K
  5. Close the child, then the parent20 ns (illustrative)

    P's post_do closes the child; K skips post_body and runs post_start. Then P runs post_body and post_start and its start returns to the test.

    More context

    Code that every start needs belongs in pre_start and post_start; pre_body and post_body are conditional.

    Follow the calls, values and waits
    1. call post_do: Child K → P's item hooks
    2. return k.start returns: Child K → Parent P
    3. return p.start returns: Parent P → Test
    4. Released: Parent P
    5. Released: Test
Read the diagram as text
  • Test: Calls p.start(sqr)
  • Parent P: pre_start, pre_body, then body
  • Child K: pre_start only: no pre_body
  • P's item hooks: pre_do(0), mid_do, post_do wrap K
  • Sequencer: Grants K's item
  • Driver: 10 ns per item
  • Test → Parent P: start(sqr)
  • Parent P → Child K: k.start(sqr, this, -1, 0)
  • Child K → P's item hooks: calls P hooks
  • Child K → Sequencer: start_item
  • Sequencer → Driver: item

One start, eight hooks. The parent's three item hooks also wrap every child it starts.

Every item gets its own trip through pre_do, mid_do and post_do. In the example, R's log reads pre_do(is_item=1), mid_do, a 10 ns gap for the driver, post_do, then finish_item 0 returned. post_do is the first code in the sequence that runs after the driver has finished with the item.

The fourth argument of start is call_pre_post, default 1. A parent often passes 0 so that it does the set-up itself; older code that raised objections in pre_body relied on this too. The uvm_do macros pass 0 for every child they start (U5.4). The Cookbook advises avoiding pre_body and post_body altogether, because code that may or may not run is a trap.

All the hooks, their kind and their typical use
HookKindWhenTypical use
pre_start / post_starttaskAlways, around everythingFetch configuration, raise and drop an objection
pre_body / post_bodytaskOnly when call_pre_post is 1Set-up that a parent sequence would rather do itself
pre_do(bit is_item)taskFor an item: after the grant, before randomization by the uvm_do macros. For a child sequence: after the child's pre_start and, if it runs, pre_bodyLast-moment constraints on an item
mid_do(item)functionJust before the item is sentInspect or log the final item
post_do(item)functionAfter the driver's item_doneRead results, count, update credits (U5.2)
do_killfunctionWhen the sequence is killedRelease resources the body held

The hooks of uvm_sequence_base. All are virtual and empty by default.

States you can wait for

Where a sequence's state goesBoxes are values of get_sequence_state(); arrows are the transitions start makes, labelled with what runs next. A new sequence is UVM_CREATED.
Where a sequence's state goesBoxes are values of `get_sequence_state()`; arrows are the transitions `start` makes, labelled with what runs next. A new sequence is `UVM_CREATED`. state UVM_PRE_START pre_start runs state UVM_PRE_BODY only if call_pre_post state UVM_BODY body runs state UVM_ENDED set as body returns state UVM_POST_BODY only if call_pre_post state UVM_POST_START post_start runs state UVM_FINISHED start returns state UVM_STOPPED kill(): start returns pre_bodybodybody returnsparent post_do,post_bodypost_startpost_start returnskill()call_pre_post = 0call_pre_post = 0
  1. A normal run: R0–10 ns (illustrative)

    With call_pre_post = 1 the state visits every box on the top row and back along the bottom to UVM_FINISHED.

    More context

    R is the example's root sequence. Its body starts at 0 ns and its one item finishes at 10 ns; the four post states follow in that same time step. At 45 ns L's log reports R as UVM_FINISHED.

    Follow the calls, values and waits
    1. data R's state: UVM_PRE_START → UVM_PRE_BODY
    2. data R's state: UVM_PRE_BODY → UVM_BODY
    3. data R's state: UVM_BODY → UVM_ENDED
    4. data R's state: UVM_ENDED → UVM_POST_BODY
    5. data R's state: UVM_POST_BODY → UVM_POST_START
    6. data R's state: UVM_POST_START → UVM_FINISHED
  2. A child started with call_pre_post = 0: K10–20 ns (illustrative)

    The two body states are skipped: UVM_PRE_START goes straight to UVM_BODY, and UVM_ENDED straight to UVM_POST_START.

    More context

    K's log has no pre_body and no post_body line. P's post_do runs while K is in UVM_ENDED, before K's post_start.

    Follow the calls, values and waits
    1. data K's state: UVM_PRE_START → UVM_BODY
    2. data K's state: UVM_BODY → UVM_ENDED
    3. data K's state: UVM_ENDED → UVM_POST_START
    4. data K's state: UVM_POST_START → UVM_FINISHED
  3. L reaches its body20 ns (illustrative)

    L is started in a fork. The other branch waits for UVM_BODY and returns when L's body begins.

    More context

    Log line 20 ns L is in state UVM_BODY, printed by wait_for_sequence_state(UVM_BODY) returning.

    Follow the calls, values and waits
    1. data L's state: UVM_PRE_START → UVM_PRE_BODY
    2. data L's state: UVM_PRE_BODY → UVM_BODY
    3. Waiting: UVM_BODY — L's body: five items
  4. A kill45 ns (illustrative)

    The example kills L while it is in UVM_BODY. The state jumps to UVM_STOPPED; post_body and post_start never run and the state never becomes UVM_FINISHED.

    More context

    A kill in any running state ends in UVM_STOPPED; the diagram shows the case the example measures. A test waiting for UVM_FINISHED alone would now wait forever.

    Follow the calls, values and waits
    1. data L's state: UVM_BODY → UVM_STOPPED
    2. Released: UVM_BODY
Read the diagram as text
  • UVM_PRE_START: pre_start runs
  • UVM_PRE_BODY: only if call_pre_post
  • UVM_BODY: body runs
  • UVM_ENDED: set as body returns
  • UVM_POST_BODY: only if call_pre_post
  • UVM_POST_START: post_start runs
  • UVM_FINISHED: start returns
  • UVM_STOPPED: kill(): start returns
  • UVM_PRE_START → UVM_PRE_BODY: pre_body
  • UVM_PRE_BODY → UVM_BODY: body
  • UVM_BODY → UVM_ENDED: body returns
  • UVM_ENDED → UVM_POST_BODY: parent post_do, post_body
  • UVM_POST_BODY → UVM_POST_START: post_start
  • UVM_POST_START → UVM_FINISHED: post_start returns
  • UVM_BODY → UVM_STOPPED: kill()
  • UVM_PRE_START → UVM_BODY: call_pre_post = 0
  • UVM_ENDED → UVM_POST_START: call_pre_post = 0

UVM_ENDED comes before the parent's post_do and the post hooks. UVM_FINISHED and UVM_STOPPED are the two ends.

get_sequence_state() returns the current box. wait_for_sequence_state(mask) blocks until the state is one in the mask, which may be a bitwise or of several. The example starts L in a fork and, in the other branch, waits for UVM_BODY: it prints L is in state UVM_BODY at 20 ns, when L's body began. A test can wait for a background sequence to reach its main loop, or ask whether a sequence finished or was stopped, without a flag of its own.

Wait for both ends. A killed sequence never reaches UVM_FINISHED, so wait_for_sequence_state(UVM_FINISHED) blocks forever; measured here, it was still blocked 100 ns after the kill with the state UVM_STOPPED. wait_for_sequence_state(UVM_FINISHED | UVM_STOPPED) returns at once.

Stopping a sequence

What each stop removes, with an item in the driverL has queued requests and its third item, L2, is in the driver. Arrows are calls and the item's path. The request FIFO holds the item the driver has taken but not finished.
What each stop removes, with an item in the driverL has queued requests and its third item, L2, is in the driver. Arrows are calls and the item's path. The request FIFO holds the item the driver has taken but not finished. caller Test Calls the stop sequence Sequence L Body in UVM_BODY queue Arbitration queue L's waiting requestsand locks queue Request FIFO Holds L2 untilitem_done driver Driver Working on L2 until50 ns seq.kill()sqr.stop_sequences()requestsL2flush
  1. kill() on one sequence45 ns (illustrative)

    kill() asks the sequencer to remove L's requests and locks, runs do_kill, kills L's children and body, and sets UVM_STOPPED. L2 stays in the request FIFO.

    More context

    Log lines 45 ns L do_kill and 45 ns L killed; state UVM_STOPPED. L2 has been in the driver since 40 ns; nothing tells the driver about the kill.

    Follow the calls, values and waits
    1. call kill(): Test → Sequence L
    2. Waiting: Driver — still working on L2
  2. The driver finishes quietly50 ns (illustrative)

    The driver's item_done finds L2 in the FIFO and completes. Its response finds no sequence and is dropped with a warning.

    More context

    The warning reads UVM_WARNING @ 50: ... Dropping response for sequence 4, sequence not found. The run still ends with 0 errors.

    Follow the calls, values and waits
    1. call item_done(): Driver → Request FIFO
    2. Released: Driver
  3. Had the test called stop_sequences()

    stop_sequences() kills every sequence on the sequencer and also flushes the request FIFO under the busy driver.

    More context

    An alternative to the first two steps, not a continuation. The driver is not told; it keeps working on the item it already took.

    Follow the calls, values and waits
    1. call stop_sequences(): Test → Arbitration queue
    2. call flush the FIFO: Arbitration queue → Request FIFO
    3. Waiting: Driver — still holds its item; the FIFO is empty
  4. The next item_done is fatal

    The driver's next item_done finds nothing outstanding and ends the run with fatal SQRBADITMDN, as the second quiz question shows.

    More context

    Measured in the quiz bench: stopping at 35 ns with the fourth item in the driver ends the run at 40 ns with fatal SQRBADITMDN.

    Follow the calls, values and waits
    1. call item_done(): Driver → Request FIFO
    2. Released: Driver
Read the diagram as text
  • Test: Calls the stop
  • Sequence L: Body in UVM_BODY
  • Arbitration queue: L's waiting requests and locks
  • Request FIFO: Holds L2 until item_done
  • Driver: Working on L2 until 50 ns
  • Test → Sequence L: seq.kill()
  • Test → Arbitration queue: sqr.stop_sequences()
  • Sequence L → Arbitration queue: requests
  • Request FIFO → Driver: L2
  • Arbitration queue → Request FIFO: flush

A kill removes one sequence's queued work and leaves the driver's item alone. stop_sequences also empties the FIFO under a busy driver.

Neither call checks whether the driver is in the middle of an item, which is why the Cookbook says a started sequence should not be stopped at all. If you must stop, make sure the driver is idle first, or kill the one sequence and let the driver finish.

After the callseq.kill()sqr.stop_sequences()
Sequence stateUVM_STOPPED, do_kill ranEvery sequence UVM_STOPPED
Queued requests and locksRemoved for this sequence and its childrenRemoved for all
Item the driver already holdsStays in the request FIFO; the driver's item_done completesFlushed; the driver's next item_done is a fatal SQRBADITMDN
Other sequencesUntouchedAll killed

For the driver shown here: get_next_item and item_done. The third row is the one that bites.

Killing is not undoing. The item in the driver still reaches the design, so a register it wrote stays written, and a scoreboard that predicted from the sequence still expects a result. post_start does not run, so an objection raised by hand in pre_start must be dropped in do_kill, or raised through set_automatic_phase_objection(1), which kill drops. kill also removes the sequence's queued requests, locks and grabs from every other sequencer it used, so a coordinator killed while holding a grab on another lane releases it. Anything else it changed, such as a configuration field or a register, stays changed.

The guard that kills an orphaned sequence

start() installs a guard on the process that called it. If that process dies without killing the sequence, the guard fires, the sequence reports warning SEQPRTZMB and calls kill() on itself. If the process that runs the body is terminated while start itself survives, for example by an errant disable fork, start reports warning SEQBDYZMB and kills the sequence. The sequencer has separate reports for queued requests whose process died, SEQREQZMB and SEQLCKZMB. Rely on these only for debugging.

Responses that nobody reads

A response travels back by sequence ID into that sequence's response queue (U4.2), and the queue belongs to the sequence object. Three lifecycle rules follow.

  1. start clears it

    Every start empties the queue, so responses from an earlier run of the same object are gone.

  2. Eight, then silence

    The queue holds 8 responses by default. Measured here: a sequence sends 12 items, the driver answers each, and the sequence reads only after the last; it gets 8, and the 4 overflows vanish without a report. With set_response_queue_error_report_enabled(1) each overflow is a UVM_ERROR.

  3. After a kill, a warning

    A response that arrives after its sequence was killed finds no sequence and is dropped with warning [Sequencer] Dropping response ... sequence exited or has been killed. The example shows it at 50 ns, for L2.

Read responses as they arrive, or raise the depth with set_response_queue_depth(-1), or use use_response_handler(1) so the sequencer calls a function instead of queuing.

Try it

Three sequences on one driver that takes 10 ns per item and returns a response for each. R shows the full hook order for a root sequence. P starts K with call_pre_post = 0, so K has no pre_body or post_body, while P's pre_do(is_item=0), mid_do and post_do wrap the child. L is killed at 45 ns with its third item in the driver; do_kill runs, post_body does not, and the driver finishes the item at 50 ns. That item's response is dropped with the warning, because L is no longer registered.

Change k.start(m_sequencer, this, -1, 0) to k.start(m_sequencer, this) and K's pre_body and post_body reappear. Then replace l.kill() with sqr.stop_sequences() and predict the driver's next item_done before running it.

Transfer exercise: L raised an objection by hand in pre_start and drops it in post_start. Write the do_kill that keeps the test from hanging after the kill, and say which state a waiting test should accept.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: --- R: one item, started from the test 0 ns R pre_start 0 ns R pre_body 0 ns R pre_do(is_item=1) 0 ns R mid_do 10 ns R post_do 10 ns R finish_item 0 returned 10 ns R post_body 10 ns R post_start --- P: starts child K with call_pre_post = 0 10 ns P pre_start 10 ns P pre_body 10 ns K pre_start 10 ns P pre_do(is_item=0) 10 ns P mid_do 10 ns K pre_do(is_item=1) 10 ns K mid_do 20 ns K post_do 20 ns K finish_item 0 returned 20 ns P post_do 20 ns K post_start 20 ns P post_body 20 ns P post_start --- L: five items, killed 25 ns into its body; state UVM_CREATED before start 20 ns L pre_start 20 ns L pre_body 20 ns L is in state UVM_BODY 40 ns L finish_item 1 returned 40 ns L pre_do(is_item=1) 40 ns L mid_do 45 ns L do_kill 45 ns L killed; state UVM_STOPPED 45 ns L start() returned; state UVM_STOPPED; R is UVM_FINISHED UVM_WARNING @ 50: uvm_test_top.sqr [Sequencer] Dropping response for sequence 4, sequence not found. Probable cause: sequence exited or has been killed UVM_ERROR : 0

Check yourself

Predict the outputWhat line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
class item_c extends uvm_sequence_item;
  `uvm_object_utils(item_c)
  int idx;
  function new(string name = "item_c"); super.new(name); endfunction
endclass
class drv_c extends uvm_driver #(item_c);
  `uvm_component_utils(drv_c)
  string grants = "";
  int done;
  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);
      grants = {grants, " ", req.get_parent_sequence().get_name(), $sformatf("%0d", req.idx)};
      #10;
      seq_item_port.item_done();
      done++;
    end
  endtask
endclass
class seq_c extends uvm_sequence #(item_c);
  `uvm_object_utils(seq_c)
  int finished;
  bit post_body_ran;
  function new(string name = "seq_c"); super.new(name); endfunction
  task body();
    for (int i = 0; i < 5; i++) begin
      item_c it = item_c::type_id::create("it");
      it.idx = i;
      start_item(it);
      finish_item(it);
      finished++;
    end
  endtask
  task post_body(); post_body_ran = 1; endtask
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_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);
    seq_c s = seq_c::type_id::create("S");
    phase.raise_objection(this);
    fork
      s.start(sqr);
      begin #25; s.kill(); end
    join
    #20;
    $display("finished=%0d state=%s post_body=%0d driver_done=%0d", s.finished, s.get_sequence_state().name(), s.post_body_ran, drv.done);
    phase.drop_objection(this);
  endtask
endclass
module tb;
  initial run_test("my_test");
endmodule
Spot the bugA background sequence sends ten items through a driver that takes 10 ns each. The test must stop it at 35 ns, with the fourth item in the driver, and end with the sequence in UVM_STOPPED and four items completed. Each option is the test code after the sequence is forked. Which one fails?
ConceptA virtual sequence starts a child with child.start(sqr, this, -1, 0). Which of the child's hooks run?
ConceptA parent sequence overrides pre_do(bit is_item) to tighten constraints on its items. During a run it is called with is_item equal to 0. What happened?
ConceptA test forks a background sequence and, before dropping its objection, calls bg.wait_for_sequence_state(UVM_FINISHED). In one run an error path has already called bg.kill(). What happens at the wait?

Remember

  • start runs pre_start, pre_body, body, post_body, post_start; call_pre_post = 0 skips the two body hooks, so put what every start needs in pre_start.
  • Each item passes pre_do after the grant, mid_do just before it is sent and post_do after item_done; a parent's three hooks also wrap each child it starts, with is_item = 0.
  • The state ends in UVM_FINISHED or, after a kill, UVM_STOPPED; wait for both.
  • kill() stops one sequence (do_kill, no post_body or post_start) and leaves the driver's item to finish; stop_sequences() also flushes the request FIFO, so a driver mid-item then hits a fatal item_done.
  • A response queue holds 8 by default and drops overflows silently; start clears it, and a response after a kill is dropped with a warning.

Practice