Chapter 7 of 10
Sequences and Sequencers
uvm_sequence's body() task and the start_item/finish_item handshake with a sequencer, randomizing transactions the SV ch12 way, and turning the driver into the uvm_driver#(REQ)/seq_item_port shape chapter 1's very first example already used.
Every driver so far has invented its own stimulus — two hardcoded sel/a/b combinations, written directly into run_phase. That's exactly chapter 1's third gap: testing a different scenario means editing the driver itself. This chapter is where stimulus becomes a separate, swappable layer: a sequence, generating transactions and handing them to a sequencer, which hands them to the driver.
uvm_sequence: one level past uvm_sequence_item
Chapter 4 covered uvm_sequence_item — the base every transaction extends. A sequence is a different class, one level up, that produces items:
class my_sequence extends uvm_sequence #(mux_transaction);
`uvm_object_utils(my_sequence)
function new(string name = "my_sequence");
super.new(name);
endfunction
task body();
req = mux_transaction::type_id::create("req");
start_item(req);
if (!req.randomize())
`uvm_error("SEQ", "randomize failed")
finish_item(req);
req = mux_transaction::type_id::create("req");
start_item(req);
if (!req.randomize() with { sel == 1; })
`uvm_error("SEQ", "randomize failed")
finish_item(req);
endtask
endclassuvm_sequence #(mux_transaction) is itself a uvm_object (chapter 4's material applies directly — `uvm_object_utils, a name-only constructor, no parent). It already declares a req property of the parameterized type, which is why body() can just assign to req without declaring it. The two randomize() calls are exactly SV ch12 (randomization-and-constraints) material — including randomize() with { sel == 1; }, the same inline-constraint syntax from that chapter, now constraining which transaction gets driven next instead of a plain local variable.
start_item()/finish_item(): the sequence side of a two-way handshake
start_item(req)/finish_item(req) aren't just bookkeeping — they're one half of a conversation with whatever's on the other end (the sequencer, and beyond it, the driver):
start_item(req)blocks until the sequencer is ready to accept a new item from this sequence (arbitration, in case more than one sequence is running — not something this track's single-sequence examples need to worry about).- Between
start_item()andfinish_item()is exactly where you randomize — the item isn't sent anywhere yet. finish_item(req)sendsreqto whatever's connected on the other side, and blocks until that side signals it's done with this item.
That last point matters: body()'s second start_item() doesn't run until the driver has finished processing the first item. A sequence's pacing is entirely driven by how fast the other side consumes items.
The driver: uvm_driver#(REQ), not plain uvm_component
The other half of that handshake needs a driver that actually speaks it. Change my_driver's base class:
class my_driver extends uvm_driver #(mux_transaction);
`uvm_component_utils(my_driver)
virtual mux2_if.tb_mp vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual mux2_if.tb_mp)::get(this, "", "vif", vif))
`uvm_fatal("DRV", "no virtual interface set for vif -- check the config_db::set() call in the top module")
endfunction
task run_phase(uvm_phase phase);
forever begin
mux_transaction tr;
seq_item_port.get_next_item(tr);
drive(tr);
seq_item_port.item_done();
end
endtask
task drive(mux_transaction tr);
vif.sel = tr.sel;
vif.a = tr.a;
vif.b = tr.b;
#1;
`uvm_info("DRV", $sformatf("%s -> y=%0b", tr.convert2string(), vif.y), UVM_MEDIUM)
endtask
endclassThis is exactly the shape chapter 1's very first code example used — simple_driver extends uvm_driver #(my_transaction) — now you know why: uvm_driver #(REQ, RSP=REQ) is a uvm_component subclass that comes with a seq_item_port already declared, ready to connect to a sequencer. build_phase and drive() are untouched from chapters 4 and 6 — the factory override from chapter 6 still works exactly the same way here, nothing about it depends on where transactions come from. What changed is run_phase: instead of driving two hardcoded transactions, it's now a forever loop — get_next_item(tr) blocks until a sequence's finish_item() hands one over, drive(tr) does the same work as before, and item_done() is what unblocks that finish_item() call, letting the sequence continue.
Wiring sequencer to driver, and where the objection went
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_driver drv;
uvm_sequencer #(mux_transaction) sqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = my_driver::type_id::create("drv", this);
sqr = uvm_sequencer#(mux_transaction)::type_id::create("sqr", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
task run_phase(uvm_phase phase);
my_sequence seq;
phase.raise_objection(this);
seq = my_sequence::type_id::create("seq");
seq.start(sqr);
phase.drop_objection(this);
endtask
endclassdrv.seq_item_port.connect(sqr.seq_item_export) is ordinary connect_phase work — and chapter 2's "connect_phase runs bottom-up" is exactly why this is safe to write here: both drv and sqr are fully built by the time my_test's own connect_phase runs.
Notice the objection moved: it's no longer inside the driver at all. run_phase's forever loop has no natural end of its own anymore — it just keeps servicing whatever the sequencer hands it, forever. The only thing that knows when there's been "enough" stimulus is whoever decided to run the sequence in the first place, which is now the test. seq.start(sqr) is a blocking call — it doesn't return until body() finishes — so wrapping it in raise_objection/drop_objection is really the same pattern chapter 2 taught, just applied to a task the test calls, instead of to a loop the driver runs.
Different runs, same driver and sequencer
Nothing about my_driver or sqr needs to change to run different stimulus — that was the entire point of chapter 1's third promise. A second sequence class, with a different body(), run with seq2.start(sqr) instead of seq.start(sqr), drives an entirely different set of transactions through the exact same driver and sequencer.
Summary
- A
uvm_sequence #(T)is auvm_object(chapter 4) whosebody()task produces items of typeT, using an inheritedreqproperty. start_item(req)/finish_item(req)is a two-way handshake with the other side (sequencer, then driver):finish_item()blocks until the driver signals it's done, which is what paces a sequence's execution.- Randomize a sequence item between
start_item()andfinish_item(), using exactly SV ch12'srandomize()/randomize() with {...}syntax. uvm_driver #(REQ, RSP=REQ)is auvm_componentwith aseq_item_portbuilt in — the same base class chapter 1's very first example used.get_next_item()/drive()/item_done()inside aforeverloop replaces hardcoded stimulus with whatever a connected sequence produces.drv.seq_item_port.connect(sqr.seq_item_export)belongs inconnect_phase, safe because chapter 2's bottom-up connect ordering guarantees both sides already exist.- The objection moves from the driver to the test once the driver's
run_phasebecomes an unending loop:seq.start(sqr)blocks until the sequence finishes, so wrapping it inraise_objection/drop_objectionis how the test decides when there's been enough stimulus.
Inside body(), why does finish_item(req) block?
Why does my_driver now extend uvm_driver #(mux_transaction) instead of plain uvm_component?
Why did the objection move from the driver's run_phase (chapters 3-6) to the test's run_phase in this chapter?
What does drv.seq_item_port.connect(sqr.seq_item_export); rely on from chapter 2, and why is it safe inside my_test's connect_phase?