Part 5 · Sequences · Intermediate
Starting Sub-Sequences: axi_four_beat_seq and start()
Delegate protocol actions with start(), blocking semantics, p_sequencer typing, and when not to use start().
What start() does
When a higher layer delegates an entire protocol action to a lower sequence, it calls sub_seq.start(p_sequencer). UVM runs the sub-sequence's body() to completion on the target sequencer, sets p_sequencer inside the sub-sequence, and blocks the caller until body() returns. Each start() is a full sequence lifecycle — not a lightweight function call.
[SEQ] start() lifecycle
parent.body()
│
│ beat = axi_single_seq::create("beat")
│ beat.start(p_sequencer) ◄── blocking call
│ │
│ ├── UVM sets beat.p_sequencer = p_sequencer
│ ├── UVM runs beat.body()
│ │ start_item → randomize → finish_item (×N)
│ └── beat.body() returns
│
│ parent continues after sub-seq completes
▼axi_four_beat_seq — canonical pattern
class axi_single_seq extends uvm_sequence #(axi_item);
`uvm_declare_p_sequencer(axi_sequencer)
`uvm_object_utils(axi_single_seq)
task body();
axi_item req = axi_item::type_id::create("req");
start_item(req);
assert(req.randomize());
finish_item(req);
endtask
endclass
class axi_four_beat_seq extends uvm_sequence #(axi_item);
`uvm_declare_p_sequencer(axi_sequencer)
`uvm_object_utils(axi_four_beat_seq)
task body();
axi_single_seq beat;
repeat (4) begin
beat = axi_single_seq::type_id::create($sformatf("beat_%0d", $));
`uvm_info(get_type_name(), "starting sub-seq beat", UVM_HIGH)
beat.start(p_sequencer); // blocks until beat.body() returns
`uvm_info(get_type_name(), "sub-seq beat done", UVM_HIGH)
end
endtask
endclassstart(sub_seq) blocks until sub_seq.body() returns — parent waits naturally.
Sub-sequence must declare same p_sequencer type as parent (or extend common base).
create() per iteration gives unique instance names in debug log.
start() vs send_beat — when each wins
Both patterns produce beats on the bus. The difference is reuse granularity and arbitration visibility:
[SEQ] comparison
axi_four_beat_seq + axi_single_seq.start()
PRO: axi_single_seq reusable in other flows standalone
PRO: clear debug hierarchy — each beat is named sub-sequence
CON: object create overhead per beat
CON: extra sequence depth in log
axi_burst_seq + send_beat() in loop
PRO: lean — no sub-seq object per beat
PRO: shared randomize policy in one helper
CON: beat logic not start()-able standalone without new classWalkthrough — four beats via start()
Legend: [STIM] [SEQ] [DRV] [UVM]
TEST starts axi_four_beat_seq on axi_sqr
│
▼
axi_four_beat_seq.body() iteration 0
│ beat.start(p_sequencer)
▼
axi_single_seq.body()
│ start_item ──► [UVM] sqr FIFO ──► [DRV] get_next_item
│ randomize
│ finish_item ◄── item_done
▼ returns
axi_four_beat_seq iteration 1 … 3
▼ returns
TEST continuesp_sequencer rules for sub-sequences
`uvm_declare_p_sequencer(axi_sequencer) on both parent and child — or child extends parent base.
start(p_sequencer) passes the handle explicitly — child does not inherit parent's p_sequencer automatically.
Virtual sequence: child starts on p_sequencer.axi_sqr, not on virtual sequencer itself.
Starting same sequence instance twice without re-create — second start() may fail or deadlock.
// Virtual sequence delegating to agent sub-seq
class chip_vseq extends uvm_sequence;
`uvm_declare_p_sequencer(chip_virtual_sequencer)
task body();
axi_four_beat_seq axi = axi_four_beat_seq::type_id::create("axi");
axi.start(p_sequencer.axi_sqr); // real sequencer — NOT p_sequencer (virtual)
endtask
endclassKey takeaways
start() delegates whole sub-sequences; blocks until body() completes.
Parent and child share p_sequencer type via declare macro or common base.
Use start() for reusable protocol actions; send_beat for inline atomic beats.
Common pitfalls
beat.start(p_sequencer) where p_sequencer is virtual — no driver below v_sqr.
Child declares apb_sequencer, parent starts on axi_sqr — compile or null field.
Reusing one sub-seq object in a loop without create — UVM state corruption.
fork beat.start() without join — parent body() ends before beats finish.