UVM Driver built reg_driver and left one piece dangling: seq_item_port needs something to connect to before get_next_item() can ever return anything. This article builds that something - the sequencer - and closes the connection the driver only pointed at.

8 min read | Intermediate Level

What You'll Learn

  • What a sequencer does, and why the driver-sequencer relationship is a pull, not a push
  • How to parameterize uvm_sequencer for reg_transaction
  • How to connect a sequencer to reg_driver in connect_phase
  • When a custom sequencer class is worth writing, versus instantiating uvm_sequencer directly

What Is a Sequencer?

A sequencer manages the flow of transactions on their way to a driver. It doesn't generate stimulus itself - that's a sequence's job, covered in UVM Sequence - and it doesn't drive any pins either - that's reg_driver's job. A sequencer sits in between, deciding when and in what order transactions reach the driver.

A sequencer is normally one of three components inside an agent, alongside a driver and a monitor. UVM Agent builds the agent itself; for now, reg_sequencer and reg_driver are built as siblings that will eventually live inside one.

How the Pull Model Works

The relationship between a sequencer and a driver is a pull, not a push: the driver asks for a transaction when it's ready, rather than the sequencer forcing one onto the driver on its own schedule. UVM Driver already named the two sides of this connection without building either of them:

  • reg_driver declares seq_item_port, a built-in uvm_seq_item_pull_port
  • A sequencer provides seq_item_export, a built-in uvm_seq_item_pull_imp

Both of these already exist automatically - uvm_driver declares seq_item_port, and uvm_sequencer declares seq_item_export, the same way uvm_subscriber declares analysis_export automatically. The only thing left to do is connect them.

Declaring reg_sequencer

For most testbenches, including this one, there's no need to write a sequencer class at all - uvm_sequencer already contains everything required to communicate with a driver. It only needs to be told what transaction type it's carrying:


uvm_sequencer #(reg_transaction) m_sequencer;

uvm_sequencer is declared as uvm_sequencer #(type REQ = uvm_sequence_item, RSP = REQ). Supplying just one type parameter sets both REQ and RSP to reg_transaction - the same outcome reg_driver already relies on, since reg_transaction carries both the request fields and the response field in a single class.

A sequencer can be parameterized with two different types - uvm_sequencer #(my_data, data_rsp) m_seqr0; - when requests and responses genuinely need to be different classes. reg_transaction doesn't need that, so a single type parameter is enough here.

Connecting the Sequencer to reg_driver

Both sides are built and parameterized to match. What's left is the single line that wires them together, written inside whichever component owns both - here, shown directly so the connection itself is clear before the agent that will eventually own it exists:


virtual function void build_phase(uvm_phase phase);
  super.build_phase(phase);

  m_sequencer = uvm_sequencer#(reg_transaction)::type_id::create("m_sequencer", this);
  m_driver    = reg_driver::type_id::create("m_driver", this);
endfunction

virtual function void connect_phase(uvm_phase phase);
  super.connect_phase(phase);

  // Connect the driver's seq_item_port to the sequencer's seq_item_export
  m_driver.seq_item_port.connect(m_sequencer.seq_item_export);
endfunction

After this connect() call runs, reg_driver's seq_item_port.get_next_item() calls are no longer waiting on nothing - they're waiting on m_sequencer. Nothing about reg_driver needs to change to make this work; it was already written correctly, just waiting for the other end of its port to exist.

Quick Check

Q: Why is this connection made in connect_phase rather than build_phase, where m_sequencer and m_driver are created?

Show Answer

Both components have to exist before they can be connected to each other - you can't call .connect() on a handle that hasn't been assigned yet. build_phase guarantees the entire hierarchy is constructed first; connect_phase runs afterward, specifically so every component it might need to wire together is already guaranteed to exist.

When to Write a Custom Sequencer

Writing a sequencer class of your own is rarely necessary - uvm_sequencer already does everything a sequence needs to talk to a driver. The reason to extend it anyway is the same reason to extend uvm_monitor or uvm_subscriber instead of plain uvm_component: it's recommended practice, and it gives you a place to add functionality beyond what the base class provides.


class reg_sequencer extends uvm_sequencer #(reg_transaction);

  `uvm_component_utils(reg_sequencer)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

endclass

This is functionally identical to declaring a plain uvm_sequencer #(reg_transaction) - the only difference is that reg_sequencer now has its own class name, which becomes useful the moment you want to add a custom method or property a sequence might need to access. UVM Sequence shows exactly that scenario.

A sequence needing to reach back into the sequencer it's running on - through handles UVM calls m_sequencer and p_sequencer - is entirely a sequence-side concern, and depends on writing a sequence's body() task first. That's covered in UVM Sequence. (The name collision is unfortunate: this article's m_sequencer is just a local variable name for the agent's sequencer instance, completely unrelated to UVM's own built-in m_sequencer handle that every sequence carries.)

Common Mistakes

Mistake #1: Forgetting the connect_phase wiring

A driver and sequencer that are each individually correct still do nothing together if m_driver's seq_item_port is never connected to m_sequencer's seq_item_export. Unlike a missing virtual interface, this usually doesn't produce an immediate error - the driver's get_next_item() just blocks forever, since there's nothing on the other end to ever respond.

Mistake #2: Connecting the export to the port instead of the port to the export

Wrong: m_sequencer.seq_item_export.connect(m_driver.seq_item_port);
Right: m_driver.seq_item_port.connect(m_sequencer.seq_item_export);

Why? connect() is meant to be called on the port, with the export passed as its argument, because of how a port resolves its connections during elaboration - it's the port side that keeps track of what it's bound to. Calling it the other way around goes against that direction and won't resolve into a working connection, even though both classes technically inherit a connect() method.

Practice Exercise

Challenge: Add a Transaction Limit

Extend reg_sequencer with a property, max_transactions, that a future sequence could read to know how many transactions it's allowed to send.

Hint

This only requires adding a plain class property to reg_sequencer - nothing about the constructor or factory registration needs to change.

Solution
class reg_sequencer extends uvm_sequencer #(reg_transaction);

  `uvm_component_utils(reg_sequencer)

  int max_transactions = 100;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

endclass
  

This is exactly the kind of addition that makes a custom sequencer worth writing in the first place - max_transactions doesn't exist on plain uvm_sequencer, so without a class of its own to belong to, a sequence would have no type-safe way to read it later.

Summary

In this article, you learned:

  • A sequencer manages the flow of transactions to a driver, using a pull model rather than pushing them
  • uvm_sequencer already provides seq_item_export, the matching half of a driver's seq_item_port
  • Connecting the two happens in connect_phase, after both components are built
  • A custom sequencer class is only necessary when a sequence needs to access something beyond what uvm_sequencer already provides

Going Deeper

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5