Part 5 · Sequences · Intermediate
Layering Stack: Beat → Burst → Flow → Virtual Sequence
The four-level stimulus stack, what each layer owns, and when to call start() vs inline start_item.
Why layer at all
Protocol stimulus has natural granularity. An AXI burst is not one transaction — it is a sequence of beats with shared address and burst type. A stress scenario is not one burst — it is many bursts with idle gaps and competing masters. A chip test is not one agent — it is coordinated traffic across DMA, PCIe, and AXI ports. Layering maps each granularity to its own sequence class so tests express intent at the top and reuse
mechanics at the bottom. Without layers, every test re-implements beat loops, burst length encoding, and idle insertion — and a single constraint fix requires editing dozens of files.
[SEQ] abstraction ownership — what each layer adds
BEAT layer one start_item/finish_item cycle
owns: per-beat randomization, handshake timing
BURST layer repeat beats with shared burst metadata
owns: burst_len, addr increment, WLAST/RLAST policy
FLOW layer multiple bursts with scenario knobs
owns: num_bursts, inter-burst gaps, error injection policy
VSEQ layer multi-agent coordination
owns: fork/join, sequencer handles, scenario ordering
TEST layer objection lifecycle, config, library selection
owns: when to start, how long to run, factory overridesThe full stack diagram
The diagram below is the canonical layering map for a single AXI master in a multi-agent SoC. Virtual sequences sit above agent-local flow sequences; flow sequences call burst sequences; burst sequences call beat sequences or base helpers.
Legend: [STIM] [SEQ] [DRV] [UVM]
TEST [STIM]
│
│ env.vseqr is virtual — no driver below it
▼
┌──────────────────────────────────────────────────────────────┐
│ chip_stress_vseq [SEQ] │
│ fork join │
│ dma_vseq.start(env.dma_sqr) │
│ pcie_vseq.start(env.pcie_sqr) │
│ axi_flow.start(env.axi_sqr) ◄── flow on real sequencer│
└──────────────────────────────────────────────────────────────┘
│
▼ (per agent)
┌──────────────────────────────────────────────────────────────┐
│ axi_burst_flow_seq [SEQ] │
│ repeat (num_bursts) │
│ axi_burst_seq.start(p_sequencer) ◄── start() delegation │
│ #(gap_cycles) │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ axi_burst_seq [SEQ] │
│ repeat (burst_len + 1) │
│ send_beat(req) ◄── base helper OR axi_single_seq.start() │
└──────────────────────────────────────────────────────────────┘
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ SEQUENCER │───►│ DRIVER │───►│ DUT pins │
│ [UVM] │ │ [DRV] │ │ │
└─────────────┘ └─────────────┘ └─────────────┘Virtual sequencer has child sequencer handles — vseq never calls start_item directly.
Flow and burst sequences run on the agent sequencer — they own arbitration slots.
Beat layer is the only layer that routinely touches start_item/finish_item.
start() vs inline start_item — decision guide
Two mechanisms connect layers: sub_seq.start(p_sequencer) delegates an entire sequence body, while send_beat(req) (or inline start_item) handles a single atomic transaction. Choosing wrong creates either bloated inheritance or hidden arbitration surprises.
[SEQ] when to use which
use start(sub_seq) when:
sub-sequence has its own body() with multiple beats
you want independent arbitration rounds per sub-seq
sub-seq is reused standalone in other flows
use send_beat(req) / inline start_item when:
beat is one atomic handshake
burst loop is simple repeat — no separate sequence class needed
you are inside a base class helper shared by many derived seqsWalkthrough — one burst through the stack
[STIM] timeline — axi_burst_flow_seq drives one burst
T0 TEST raises objection, starts chip_stress_vseq on v_sqr
T1 vseq forks axi_burst_flow_seq.start(axi_sqr)
T2 flow_seq: axi_burst_seq burst = create; burst.start(p_sequencer)
T3 burst_seq body: req = axi_item::create; burst_len = 3
T4 beat 0: start_item(req) → randomize → finish_item(req) [SEQ→DRV]
T5 beat 1: start_item(req) → randomize → finish_item(req)
T6 beat 2: start_item(req) → randomize → finish_item(req)
T7 beat 3: start_item(req) → randomize → finish_item(req) (WLAST)
T8 burst_seq body returns; flow_seq inserts #(20) idle
T9 flow_seq starts next burst or returnsEach start_item is one arbitration slot. A burst with four beats consumes four slots unless the burst sequence holds lock (covered in arbitration lesson).
Layering guidelines
Never skip layers with a 500-line body() — split at natural protocol boundaries.
Each layer adds exactly one level of abstraction; do not mix burst logic into vseq.
Share randomization policy in base sequences, not in every derived body().
Libraries and virtual sequences compose — vseq can start a seq_lib on one agent.
Name layers by protocol action: axi_single_seq, axi_burst_seq, axi_stress_flow_seq.
Key takeaways
Beat → burst → flow → vseq is the standard reuse stack for protocol VIP.
Virtual sequences coordinate; agent sequences drive; beat layer handshakes.
start() for reusable sub-sequences; inline start_item for atomic beats.
Common pitfalls
Virtual sequence calling start_item on v_sqr — no driver below virtual sequencer.
Flow sequence that randomizes beat fields — belongs in burst or beat layer.
Test body() with nested repeat loops — belongs in flow or burst sequence class.