A DMA configuration sequence programs KEY and LEN in its pre_body, then streams data in body. Started from a test, it works. A colleague reuses it inside a virtual sequence with a one-line macro, and the regression stays green, but the coverage report shows every job ran with the same key. The configuration never happened. Nothing printed an error.
To see why, you need to know exactly what start() does and in which order. This lesson walks through it, line by line against the library source, and runs it so you can see every hook fire.
A sequence is an object, not a component, so it has no phases of its own. What it has is start(): a task that runs a fixed script around your body(). Think of a theatre performance. The doors open (pre_start), the overture plays (pre_body), the play runs (body), the cast takes a bow (post_body), and the doors close (post_start). The doors always open and close. The overture and the bow are optional scenes: the caller can ask for a performance without them.
That option is the fourth argument of start(), call_pre_post. It defaults to 1, so a sequence you start yourself runs all five hooks. The uvm_do macros start sub-sequences with it set to 0, which is the answer to the opening puzzle.
Argument | Default | What it decides |
|---|---|---|
| (required) | Where the sequence's items go. |
|
| Makes this a child: its name becomes |
|
| -1 means 100 with no parent, or the parent's priority. Below -1 is a |
|
| Whether |
start() on a sequence that is already running is a SEQ_NOT_DONE fatal ("already started"). The bench's dma_vseq shows the parent argument in action: write_reg starts each apb_rw_seq with s.start(apb_sqr, this), so the register writes appear as children of the virtual sequence and inherit its priority.
`pre_start` / `post_start`: always run, whoever starts the sequence and however. Put anything here that must happen on every run, such as a manual objection.
`pre_body` / `post_body`: run only when call_pre_post is 1. Use them only for work that a parent may legitimately want to skip.
`body`: the stimulus. Configuration the sequence depends on belongs here, at the top, not in pre_body.
pre_do, mid_do and post_do are not called on the item or child; they are called on the sequence that sends it, once per item or child sequence. They are where a sequence can count, log or adjust everything it sends without touching each call site:
Hook | For an item, called... | For a child sequence, called... |
|---|---|---|
| inside | after the child's |
| inside | just before the child's |
| inside | right after the child's |
Here is a sequence that logs every hook, with two children, one started normally and one with call_pre_post set to 0:
// snippet: seq_lab_tb.sv, a sequence that reports its own lifecycle
class lab_hooks_seq extends uvm_sequence #(lab_item);
`uvm_object_utils(lab_hooks_seq)
function new(string name = "lab_hooks_seq"); super.new(name); endfunction
task pre_start(); `uvm_info("HOOK", "top.pre_start", UVM_LOW) endtask
task pre_body(); `uvm_info("HOOK", "top.pre_body", UVM_LOW) endtask
task pre_do(bit is_item);
`uvm_info("HOOK", $sformatf("top.pre_do(is_item=%0b)", is_item), UVM_LOW)
endtask
function void mid_do(uvm_sequence_item this_item);
`uvm_info("HOOK", {"top.mid_do(", this_item.get_name(), ")"}, UVM_LOW)
endfunction
function void post_do(uvm_sequence_item this_item);
`uvm_info("HOOK", {"top.post_do(", this_item.get_name(), ")"}, UVM_LOW)
endfunction
task body();
lab_item it = lab_item::type_id::create("it");
lab_child_seq a = lab_child_seq::type_id::create("childA");
lab_child_seq b = lab_child_seq::type_id::create("childB");
`uvm_info("HOOK", $sformatf("top.body state=%s", get_sequence_state().name()), UVM_LOW)
start_item(it);
`uvm_info("HOOK", "top: start_item returned (granted)", UVM_LOW)
if (!it.randomize()) `uvm_fatal("RAND", "randomize failed")
finish_item(it);
`uvm_info("HOOK", "top: finish_item returned", UVM_LOW)
a.start(m_sequencer, this); // call_pre_post defaults to 1
b.start(m_sequencer, this, -1, 0); // call_pre_post = 0
endtask
task post_body(); `uvm_info("HOOK", "top.post_body", UVM_LOW) endtask
task post_start(); `uvm_info("HOOK", "top.post_start", UVM_LOW) endtask
endclasslab_child_seq logs its own five hooks the same way and sends nothing.
get_sequence_state() returns where the sequence is: UVM_CREATED, then UVM_PRE_START, UVM_PRE_BODY, UVM_BODY, UVM_ENDED (body has returned), UVM_POST_BODY, UVM_POST_START and finally UVM_FINISHED. A killed sequence ends in UVM_STOPPED. The values are one-hot, so wait_for_sequence_state(UVM_FINISHED | UVM_STOPPED) waits for either ending, which is the right way for a parent to wait for a child it forked.
The lab run, with the driver's two messages at UVM_MEDIUM (the item is granted at 22 ns, when reset ends and the driver first asks for work):
UVM_INFO seq_lab_tb.sv(173) @ 0: uvm_test_top.env.sqr@@top [HOOK] top.pre_start
UVM_INFO seq_lab_tb.sv(174) @ 0: uvm_test_top.env.sqr@@top [HOOK] top.pre_body
UVM_INFO seq_lab_tb.sv(188) @ 0: uvm_test_top.env.sqr@@top [HOOK] top.body state=UVM_BODY
UVM_INFO seq_lab_tb.sv(176) @ 22000: uvm_test_top.env.sqr@@top [HOOK] top.pre_do(is_item=1)
UVM_INFO seq_lab_tb.sv(190) @ 22000: uvm_test_top.env.sqr@@top [HOOK] top: start_item returned (granted)
UVM_INFO seq_lab_tb.sv(179) @ 22000: uvm_test_top.env.sqr@@top [HOOK] top.mid_do(it)
UVM_INFO seq_lab_tb.sv(48) @ 22000: uvm_test_top.env.drv [DRV] got top WR 30=c71e692e
UVM_INFO seq_lab_tb.sv(52) @ 35000: uvm_test_top.env.drv [DRV] item_done
UVM_INFO seq_lab_tb.sv(182) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_do(it)
UVM_INFO seq_lab_tb.sv(193) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top: finish_item returned
UVM_INFO seq_lab_tb.sv(163) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.pre_start
UVM_INFO seq_lab_tb.sv(164) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.pre_body
UVM_INFO seq_lab_tb.sv(176) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.pre_do(is_item=0)
UVM_INFO seq_lab_tb.sv(179) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.mid_do(childA)
UVM_INFO seq_lab_tb.sv(165) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.body
UVM_INFO seq_lab_tb.sv(182) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_do(childA)
UVM_INFO seq_lab_tb.sv(166) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.post_body
UVM_INFO seq_lab_tb.sv(167) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.post_start
UVM_INFO seq_lab_tb.sv(163) @ 35000: uvm_test_top.env.sqr@@top.childB [HOOK] childB.pre_start
UVM_INFO seq_lab_tb.sv(176) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.pre_do(is_item=0)
UVM_INFO seq_lab_tb.sv(179) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.mid_do(childB)
UVM_INFO seq_lab_tb.sv(165) @ 35000: uvm_test_top.env.sqr@@top.childB [HOOK] childB.body
UVM_INFO seq_lab_tb.sv(182) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_do(childB)
UVM_INFO seq_lab_tb.sv(167) @ 35000: uvm_test_top.env.sqr@@top.childB [HOOK] childB.post_start
UVM_INFO seq_lab_tb.sv(197) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_body
UVM_INFO seq_lab_tb.sv(198) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_startpre_do(is_item=1) fires at 22 ns, together with the grant, not when start_item was called at time 0. It runs inside start_item, after wait_for_grant.
mid_do(it) comes after "start_item returned": your randomize ran between them, so mid_do sees the final values just before the driver does.
childA, started with the default call_pre_post = 1, runs pre_body and post_body even though it is a child. childB skips both. Being a child does not skip them; the argument does.
Back to the opening. The configuration sequence wrote KEY and LEN in pre_body. The virtual sequence started it with uvm_do(cfg_seq), and the macro starts sub-sequences with call_pre_post set to 0. So pre_body never ran, body streamed data with whatever KEY and LEN the previous job left behind, and the DMA did exactly what it was told. The scoreboard predicted from the monitored register writes, saw no new KEY, and agreed with the DUT. Everything matched; nothing was being tested.
The coverage report caught it, because every job had the same key. The fix was to move the register writes to the top of body, and to adopt a rule: pre_body and post_body are for work a parent may skip on purpose, never for work the sequence depends on. The same trap catches objections raised in pre_body, which is why Sequences and objections uses pre_start or an automatic objection instead.
post_do and increments a counter. Does it see the children's items?No. post_do runs on the sequence that sends an item, so a child's items call the child's post_do. The parent's post_do runs once per child sequence it starts (after that child's body). To count every item, count in each child, or count in the driver or a monitor.
In post_start, which always runs, or at the end of body. post_body would be skipped whenever a parent starts the sequence with call_pre_post set to 0, as the uvm_do macros do.
fork child.start(sqr, this); join_none and later needs to wait for it. What do you write?child.wait_for_sequence_state(UVM_FINISHED | UVM_STOPPED);. The states are one-hot bits, so the mask returns when the child ends normally or is killed. Waiting only for UVM_FINISHED would hang forever if the child were killed.
Next: start_item waits for a grant. Inside the sequencer shows who decides the grant when several sequences are waiting, and why priority often does nothing.