UVM Basics

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
endclass

uvm_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):

  1. 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).
  2. Between start_item() and finish_item() is exactly where you randomize — the item isn't sent anywhere yet.
  3. finish_item(req) sends req to 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
endclass

This 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
endclass

drv.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 a uvm_object (chapter 4) whose body() task produces items of type T, using an inherited req property.
  • 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() and finish_item(), using exactly SV ch12's randomize()/randomize() with {...} syntax.
  • uvm_driver #(REQ, RSP=REQ) is a uvm_component with a seq_item_port built in — the same base class chapter 1's very first example used. get_next_item()/drive()/item_done() inside a forever loop replaces hardcoded stimulus with whatever a connected sequence produces.
  • drv.seq_item_port.connect(sqr.seq_item_export) belongs in connect_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_phase becomes an unending loop: seq.start(sqr) blocks until the sequence finishes, so wrapping it in raise_objection/drop_objection is 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?