Day 3: Sequences, Sequencer, Driver — The Stimulus Handshake
A 30-day SystemVerilog/UVM verification series. Day 1 built the skeleton. Day 2 wired the lifecycle. Today we put the testbench in motion.
By now you have a working UVM environment: driver, monitor, sequencer, agent, scoreboard, env, test. But there is one piece we've been treating as a black box — how does a transaction actually get from a sequence (pure data, no concept of time) to a driver (pure time, no concept of what to send)?
That bridge is the sequencer, and the handshake between sequence and driver is the single most misunderstood part of UVM for beginners. Today we take it apart.
1. Three Classes, Three Jobs
Before the handshake, get the roles straight. These are different classes with different responsibilities, and confusing them is the number one source of bugs:
| Class | Base | Job | Knows about time? |
|---|---|---|---|
| Sequence item | uvm_sequence_item | Holds the data for one transaction | No |
| Sequence | uvm_sequence #(item) | Decides what to send, in what order | Indirectly (via handshake) |
| Sequencer | uvm_sequencer #(item) | Arbitrates between sequences, buffers items | No |
| Driver | uvm_driver #(item) | Translates an item into pin wiggles | Yes — the only one that drives pins |
2. The Handshake — Who Calls Whom
The sequence-driver handshake has two halves, one on each side:
SEQUENCE side (inside body()):
start_item(item); // "sequencer, I want to send this"
item.randomize(); // (after start_item — see section 3)
finish_item(item); // "sequencer, send it now"
SEQUENCER (in the middle):
holds the item until the driver is ready
DRIVER side (inside run_phase):
seq_item_port.get_next_item(req); // "give me the next item"
drive_on_pins(req); // do the actual driving
seq_item_port.item_done(); // "I'm done with this one"
Every transaction passes through all four calls. Miss any one and the testbench either hangs or races.
randomize() before start_item(). It compiles and sometimes even runs, but start_item can trigger a sequence's arbitration and pre/post hooks that expect the item to be unmodified. The UVM LRM says randomize after start_item. Follow it.
3. Why start_item Can Block
start_item is not just a "register interest" call. It can block. Two reasons:
- Sequencer arbitration. If multiple sequences are running on the same sequencer, one may be locked out while another has priority.
start_itemwaits for the arbitration to grant access. - Grab / lock. If another sequence called
sequencer.grab()orlock(), your sequence is blocked until the grab is released.
Once start_item returns, the sequencer has granted the sequence access to send that one item. It randomizes, then finish_item sends the item to the driver and waits for completion. The driver's item_done completes this item handshake; it does not release an explicit sequencer lock or grab, which must be released separately.
4. The Canonical Write Sequence
Here's the write sequence for our FIFO, with what each line actually does:
class fifo_write_seq extends uvm_sequence #(fifo_item);
`uvm_object_utils(fifo_write_seq)
function new(string name = "fifo_write_seq");
super.new(name);
endfunction
task body();
fifo_item tr;
repeat (20) begin
tr = fifo_item::type_id::create("tr");
start_item(tr); // (1) wait for access
if (!tr.randomize() with { is_write == 1; })
`uvm_fatal("SEQ", "randomize failed") // (2) randomize
finish_item(tr); // (3) hand to driver
end
endtask
endclass
Three lines in the loop body do all the work. That's the whole sequence. It's small because the sequencer handles all the coordination.
5. The Driver's Side
task run_phase(uvm_phase phase);
vif.drv_cb.wr_en <= 0;
vif.drv_cb.wr_data <= 0;
forever begin
seq_item_port.get_next_item(req); // (1) wait for next item
vif.drv_cb.wr_en <= req.is_write; // (2) drive on pins
vif.drv_cb.wr_data <= req.data;
@(vif.drv_cb); // (3) one clock of driving
seq_item_port.item_done(); // (4) signal completion
end
endtask
Four lines inside forever. The driver doesn't know how many items are coming, in what order, or what values. It just drives what it's given.
get_next_item but never item_done, the sequencer blocks forever. No error, no timeout warning until the default 9200-second phase timeout fires. If your test hangs with no output, this is the first thing to check.
6. What randomize() Actually Buys You
The with clause is where the sequence controls the stimulus:
if (!tr.randomize() with { is_write == 1; data inside {[8'h00 : 8'hFF]}; })
Constraints inside with are extra constraints on top of the item's own rand fields. The item might also declare its own constraints (constraint c_data { data != 0; }) that always apply. The with clause is per-call. This is how one sequence can send corner-case data, another can send random data, without either modifying the item class.
7. Common Deadlock Patterns
| Symptom | Cause |
|---|---|
| Simulation hangs after first item | Driver never calls item_done() |
| Simulation hangs on second sequence | First sequence called grab() but never ungrab() |
| Sequence body finishes but test doesn't end | Test forgot to drop_objection (see Day 2) |
| Only one sequence's items reach the driver | A sequence holds a lock/grab, or an arbitration policy repeatedly favors another sequence |
| Sequence randomly errors out mid-run | randomize() failed — check constraints are satisfiable |
8. A Real Read Sequence
Reads are trickier because the DUT returns data after the request. Here's how a read sequence works with our FIFO:
class fifo_read_seq extends uvm_sequence #(fifo_item);
`uvm_object_utils(fifo_read_seq)
task body();
fifo_item tr;
repeat (20) begin
tr = fifo_item::type_id::create("tr");
start_item(tr);
if (!tr.randomize() with { is_write == 0; })
`uvm_error("SEQ", "randomize failed")
finish_item(tr);
end
endtask
endclass
The monitor, not the sequence, observes the returned read data. The sequence just requests the read; the scoreboard checks the response via the monitor's analysis port (Day 7 will build that comparison).
9. How a Test Starts a Sequence
task run_phase(uvm_phase phase);
fifo_write_seq wseq;
fifo_read_seq rseq;
phase.raise_objection(this);
wseq = fifo_write_seq::type_id::create("wseq");
rseq = fifo_read_seq::type_id::create("rseq");
wseq.start(env.agent.sqr);
rseq.start(env.agent.sqr);
phase.drop_objection(this);
endtask
start(sequencer) launches the sequence. It runs to completion (the sequence's body() returns), then control returns to the test. Two sequences run sequentially here — the read only starts after the write completes.
fork...join. Both sequences share the same sequencer; arbitration decides which gets access to the driver at any moment.
10. What Day 4 Will Add
We'll take this further with transactions and TLM: how uvm_analysis_port and uvm_analysis_imp actually connect, what happens to a transaction as it travels from monitor to scoreboard (and whether the driver can modify it in flight), and how to wire up a scoreboard that does real comparison instead of just counting writes.
11. Quiz
Ten questions on sequences, sequencers, and drivers. One question at a time, options shuffled on each page load, Submit reveals all answers with explanations.
12. Further Reading
Reference material
- Verification Guide — UVM Sequence — clear walkthrough of the sequence lifecycle with diagrams.
- ChipVerify — Sequence, Sequencer, Driver — the canonical handshake explained with mini examples.
- Verification Academy — UVM Sequence Sessions — deeper dive on virtual sequences and layered stimulus.
- Accellera UVM Standard — Section 7 (Sequences) is the normative source.
Coming Up Next: Day 4 — Transactions and TLM
- TLM ports vs analysis ports — when to use which, and why the analysis port broadcasts while the TLM port is one-to-one.
- Connecting ports to imports — the exact rule for which port goes to which export, with the FIFO example.
- Do transactions get copied? — how analysis ports handle multiple subscribers, and where a shallow copy bites you.
- A real scoreboard — comparing expected vs observed for the FIFO, including ordering and overflow checks.
Series: 30 days of SystemVerilog/UVM verification. Day 1 built the skeleton. Day 2 wired the lifecycle. Day 3 put it in motion.