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.

diagram
[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

systemverilog
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
endclass
  • start(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:

diagram
[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 class

Walkthrough — four beats via start()

diagram
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 continues

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

systemverilog
// 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
endclass

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