The sequence as a transient object — body() task, the start_item/finish_item handshake, how sequences start on a sequencer, rand fields in sequences, sub-sequences, the API sequence pattern, linear and parallel execution, and sequence hierarchy from atomic to test-level.
A sequence is a transient object that encapsulates a unit of stimulus. It is created, its body() task runs, it generates sequence items and sends them to the driver, and then it goes out of scope. Unlike a component, it has no phases and no fixed place in the component hierarchy.
Sequences solve the fundamental problem of test reuse: the same driver can execute any sequence — a single write, a burst, a complete register initialisation, or a randomised traffic generator — without knowing in advance which one it will receive. The sequencer arbitrates between concurrent sequences and presents one item at a time to the driver.
In terms of class inheritance, uvm_sequence extends uvm_sequence_item which extends uvm_object. This means a sequence is itself a uvm_object — it can be randomised, copied, and passed around just like any other object.
Every sequence has the same basic structure. The only required override is body() — everything else has a default implementation.
class apb_write_seq extends uvm_sequence #(apb_seq_item); `uvm_object_utils(apb_write_seq) // ── Optional: rand fields control sequence behaviour ──── rand int unsigned num_transfers = 10; rand logic [11:0] target_addr; constraint num_c { num_transfers inside {[1:100]}; } constraint addr_c { target_addr inside {12'h000, 12'h004, 12'h008}; } function new(string name = "apb_write_seq"); super.new(name); endfunction // ── body() is the ONLY required override ──────────────── task body(); apb_seq_item item; repeat(num_transfers) begin item = apb_seq_item::type_id::create("item"); start_item(item); if (!item.randomize() with { read_not_write == 0; // force write addr == target_addr; // use sequence field }) `uvm_fatal("SEQ", "Randomise failed") finish_item(item); end endtask endclass
Key properties of body():
m_sequencer — a handle to the sequencer it is running onp_sequencer — a type-cast handle to the sequencer (requires `uvm_declare_p_sequencer)Every item sent from a sequence to the driver goes through a two-step handshake. Understanding what each call does is critical for debugging stuck simulations.
| Call | Who calls | What it does | Blocks? |
|---|---|---|---|
start_item(item) | Sequence body() | Requests sequencer arbitration — waits until the driver calls get_next_item() | Yes — until driver is ready |
finish_item(item) | Sequence body() | Delivers item to driver — waits until driver calls item_done() | Yes — until driver finishes |
get_next_item(item) | Driver run_phase | Retrieves the next ready item from the sequencer — grants the start_item() block | Yes — until sequence calls finish_item() |
item_done() | Driver run_phase | Signals the sequencer that the item is consumed — unblocks finish_item() | No — returns immediately |
A sequence is started with its start() method. This associates the sequence with a sequencer, then calls body(). The caller blocks until body() completes.
// ── Basic: start on a sequencer, block until done ──────── seq.start(m_env.m_agent.m_seqr); // ── With parent sequence (for proper response routing) ─── // Always pass 'this' as parent when starting from body() of another seq child_seq.start(m_sequencer, this); // ── start() signature ───────────────────────────────────── // task start( // uvm_sequencer_base sequencer, // target sequencer // uvm_sequence_base parent = null, // parent sequence (for response routing) // int this_priority = -1, // arbitration priority (-1 = inherit) // bit call_pre_post = 1 // whether to call pre/post_body // ); // ── Starting from test run_phase ───────────────────────── task run_phase(uvm_phase phase); apb_write_seq seq; phase.raise_objection(this); seq = apb_write_seq::type_id::create("seq"); if (!seq.randomize() with { num_transfers == 20; }) `uvm_fatal("TEST", "Seq randomise failed") seq.start(m_env.m_agent.m_seqr); // blocks until seq completes phase.drop_objection(this); endtask
A sequence can contain rand fields that control its behaviour — how many items to generate, what address range to target, what data patterns to use. Randomising the sequence before starting it changes the whole scenario the sequence generates, without changing the sequence code.
class mem_fill_seq extends uvm_sequence #(apb_seq_item); `uvm_object_utils(mem_fill_seq) // ── Rand fields control this sequence's behaviour ─────── rand logic [11:0] start_addr; rand logic [11:0] end_addr; rand int unsigned num_writes; rand logic [31:0] fill_value; constraint range_c { end_addr > start_addr; num_writes inside {[1:256]}; } function new(string name = "mem_fill_seq"); super.new(name); endfunction task body(); apb_seq_item item; logic [11:0] addr = start_addr; repeat(num_writes) begin item = apb_seq_item::type_id::create("item"); start_item(item); item.randomize() with { read_not_write == 0; addr == local::addr; write_data == local::fill_value; byte_en == 4'b1111; }; finish_item(item); addr += 4; end endtask endclass // ── In test: randomise before starting ──────────────────── task run_phase(uvm_phase phase); mem_fill_seq seq; phase.raise_objection(this); seq = mem_fill_seq::type_id::create("seq"); seq.randomize() with { start_addr == 12'h000; num_writes == 10; }; seq.start(m_env.m_agent.m_seqr); phase.drop_objection(this); endtask
A sequence can start other sequences inside its body(). This is equivalent to a function calling a subroutine — it allows complex behaviour to be built from simple reusable pieces. Sub-sequences always pass this as the parent sequence for proper response routing.
class gpio_init_seq extends uvm_sequence #(apb_seq_item); `uvm_object_utils(gpio_init_seq) function new(string name="gpio_init_seq"); super.new(name); endfunction task body(); apb_write_seq wr_seq; apb_read_seq rd_seq; // ── Sequential sub-sequences ────────────────────────── // Start write seq — blocks until it completes wr_seq = apb_write_seq::type_id::create("wr_seq"); wr_seq.target_addr = 12'h004; // set DIR register wr_seq.start(m_sequencer, this); // pass 'this' as parent // Start read seq to verify — blocks until it completes rd_seq = apb_read_seq::type_id::create("rd_seq"); rd_seq.target_addr = 12'h004; rd_seq.start(m_sequencer, this); if (rd_seq.read_data != 32'hFFFF0000) `uvm_error("SEQ", "DIR register mismatch!") endtask endclass
this as the parent when calling start() from inside body(). The parent argument tells the sequencer how to route response items back to the correct sequence. Without it, response routing breaks in bidirectional protocols where the driver returns data to the sequence.
Sub-sequences can run in any order. Linear (sequential) execution is the default — each sub-sequence completes before the next starts. Parallel execution uses SystemVerilog fork/join to run multiple sub-sequences simultaneously on the same or different sequencers.
// ── Linear (sequential) — default ──────────────────────── task body(); seq_a.start(m_sequencer, this); // A completes seq_b.start(m_sequencer, this); // then B starts endtask // ── Parallel (fork/join) — both run simultaneously ──────── task body(); fork seq_a.start(m_sequencer, this); seq_b.start(m_sequencer, this); join // wait for BOTH to finish endtask // ── fork/join_any — continue when FIRST finishes ────────── task body(); fork seq_a.start(m_sequencer, this); timeout_seq.start(m_sequencer, this); join_any // whichever finishes first disable fork; // WARNING: use with care — see pitfalls section endtask
fork / join_any / disable fork with sequences unless the cancelled sequences have completed all their item handshakes. Killing a sequence mid-handshake (between start_item and finish_item, or between get_next_item and item_done) permanently locks the sequencer. The locked state cannot be recovered — the simulation must be restarted. If you need a timeout pattern, implement it outside the sequence architecture (e.g., a parallel monitor that sets a flag).
Well-structured sequence libraries follow a three-level hierarchy. Each level builds on the one below it, and any level can be used standalone by a test.
The API sequence pattern creates a task wrapper around the start_item/finish_item handshake, giving worker sequences a clean functional interface instead of raw item manipulation. This is the recommended style for reusable protocol VIP.
// ── API sequence: one write operation ───────────────────── class apb_write_seq extends uvm_sequence #(apb_seq_item); `uvm_object_utils(apb_write_seq) rand logic [11:0] addr; rand logic [31:0] data; rand logic [3:0] strobe; function new(string name="apb_write_seq"); super.new(name); endfunction task body(); apb_seq_item req; req = apb_seq_item::type_id::create("req"); start_item(req); req.randomize() with { read_not_write == 0; addr == local::addr; write_data == local::data; byte_en == local::strobe; }; finish_item(req); endtask // ── Convenience task — hides start() from callers ──────── task write( input logic [11:0] addr, input logic [31:0] data, input uvm_sequencer_base seqr, input uvm_sequence_base parent = null, input logic [3:0] strobe = 4'b1111 ); this.addr = addr; this.data = data; this.strobe = strobe; this.start(seqr, parent); endtask endclass // ── Worker calls the convenience task — clean interface ─── task body(); apb_write_seq wr = apb_write_seq::type_id::create("wr"); wr.write(12'h004, 32'hFFFF0000, m_sequencer, this); // set DIR reg wr.write(12'h008, 32'h0000000F, m_sequencer, this); // set IEN reg endtask
| Pitfall | Symptom | Fix |
|---|---|---|
| Randomising item before start_item() | pre_do() modifications not applied. Constraints may be wrong. | Always: create → start_item → randomise → finish_item. Never randomise before start_item. |
| Missing item_done() in driver | finish_item() never returns. Sequence hangs forever. Simulation timeout. | Every get_next_item() must be paired with exactly one item_done(). |
Forgetting this as parent in sub-sequence start() | Response items lost. Bidirectional protocols silently fail. | Always child_seq.start(m_sequencer, this) when calling from inside body(). |
| fork/join_any then disable fork | Sequencer deadlock. Simulation appears to run but no more items delivered. | Never use disable fork with active sequences. Let all forked sequences complete naturally. |
Using null as sequencer in start() | Null pointer dereference. Fatal runtime error. | Always pass a valid sequencer handle to start(). Retrieve it from the component hierarchy before calling start(). |
| Starting sequence with no sequencer handle | Null handle crash or compilation error. | Test’s run_phase must have a valid path to the sequencer: m_env.m_agent.m_seqr. |
| Running the same sequence instance twice | Race condition — sequence state is not reset between runs. | Create a new sequence instance for each start(). Do not reuse the same handle. |
| Item | Key fact |
|---|---|
| Base class | uvm_sequence #(REQ, RSP = REQ) extends uvm_sequence_item → uvm_object |
| Registration | `uvm_object_utils(ClassName) — sequences are objects, not components |
| Constructor | function new(string name = "classname") — no parent |
| body() type | Task — can consume simulation time and call blocking operations |
| Correct item creation order | create() → start_item() → randomize() → finish_item() |
| start_item() blocks until | Driver calls get_next_item() — sequencer grants access |
| finish_item() blocks until | Driver calls item_done() |
| start() signature | seq.start(sequencer, parent_seq, priority, call_pre_post) |
| Parent in sub-sequences | Always pass this: child.start(m_sequencer, this) |
| m_sequencer | Handle to the sequencer this sequence is running on — available in body() |
| Rand seq fields | Declare fields as rand in the sequence class, randomise the sequence before start() |
| fork/join warning | Never kill a sequence mid-handshake — locks sequencer permanently |
| Level 1 — API seq | One atomic operation: start_item → randomize → finish_item. Returns response. |
| Level 2 — Worker seq | Meaningful DUT operation built from API sequence calls |
| Level 3 — Test seq | Full test scenario: calls worker sequences in order or parallel |