The previous three articles built everything a driver needs: reg_block and reg_if to drive signals onto, and reg_transaction to describe what to drive. This article writes the first active piece of this testbench - the part that actually wiggles pins on the DUT.
9 min read | Intermediate Level
What You'll Learn
- What a UVM driver does and how it connects to a sequencer
- How to get a virtual interface handle into a driver
- The get_next_item / item_done handshake, and why both calls are required
- How to translate a reg_transaction into pin wiggles on reg_if
What Is a Driver?
A UVM driver is an active component that knows how to drive signals onto a specific interface. All driver classes extend uvm_driver, either directly or indirectly. It doesn't decide what to drive on its own - transaction objects come from a sequencer, and the driver's only job is to take each one and translate it into the right pin-level activity, cycle by cycle.
reg_transaction is exactly the kind of object a driver consumes here: it carries wr_en/rd_en/addr/wdata as a single piece of data, and the driver's job is to turn those fields into the actual signal toggles reg_if expects.
Building reg_driver
A driver is built in three parts: the class declaration and constructor, getting hold of the interface, and the task that actually drives it.
1. Declare the Class
class reg_driver extends uvm_driver #(reg_transaction);
`uvm_component_utils(reg_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// Code for rest of the steps come here
endclass
uvm_driver is a parameterized class: uvm_driver #(type REQ = uvm_sequence_item, type RSP = REQ). reg_driver only specifies one type parameter, reg_transaction, which becomes both REQ and RSP since RSP defaults to whatever REQ is. That's a natural fit here - reg_transaction already carries both the request fields (wr_en/wdata) and the response field (rdata) in one class, so there's no need for two separate types.
2. Get the Interface Handle
virtual reg_if vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(virtual reg_if)::get(this, "", "vif", vif)) begin
`uvm_fatal(get_type_name(), "Didn't get handle to virtual interface reg_if")
end
endfunction
vif is declared as a virtual reg_if handle rather than the interface itself, since a class can't directly instantiate an interface - it can only hold a reference to one that already exists at the top level. uvm_config_db::get() retrieves that reference, and the if (!...) wrapped around it with uvm_fatal exists because a missing handle here means the driver has nothing to drive - better to stop the test immediately than let it run silently broken.
The full mechanics of uvm_config_db - how the value got set in the first place, scope strings, and overrides - are covered in UVM Config Database. For now, the call shown above is the standard, fixed pattern for retrieving any virtual interface handle.
3. Drive the Interface in run_phase
This is the part that actually does the work, and it's worth tracing through carefully - the timing here is exactly the kind of detail reg_block's Mistake #2 warned about back in the first article.
virtual task run_phase(uvm_phase phase);
reg_transaction tr;
forever begin
// 1. Get next item from the sequencer
seq_item_port.get_next_item(tr);
// 2. Drive request signals, then wait for the DUT to act on them
@(posedge vif.clk);
vif.wr_en <= tr.wr_en;
vif.rd_en <= tr.rd_en;
vif.addr <= tr.addr;
vif.wdata <= tr.wdata;
@(posedge vif.clk);
// Clear the request now that the DUT has sampled it once, so it is not repeated
vif.wr_en <= 0;
vif.rd_en <= 0;
// 3. For a read, rdata isn't valid until one cycle later
if (tr.rd_en) begin
@(posedge vif.clk);
tr.rdata = vif.rdata;
end
// 4. Tell the sequencer this item is done
seq_item_port.item_done();
end
endtask
Notice there are two clock waits between driving the signals and the DUT acting on them, not one. The first @(posedge vif.clk) drives wr_en/rd_en/addr/wdata onto the interface; those values only become stable shortly after that edge. The second @(posedge vif.clk) is the edge reg_block actually samples them on and performs the write, or schedules rdata to update. For a read, rdata still isn't valid yet at that point - it only settles after that same edge, so a third wait is needed before it's safe to capture. Sampling rdata one edge too early here would silently grab a stale value, exactly the race the Mistake box in the DUT article was warning about. Right after that second edge the driver also clears wr_en and rd_en; left high, the DUT would sample the same request again on every following edge and repeat the write or read.
Quick Check
Q: What would go wrong if the driver sampled vif.rdata right after the second @(posedge vif.clk), without the third wait?
Show Answer
It would race with reg_block's own update to rdata. reg_block schedules rdata to update on that same edge using a non-blocking assignment, which only settles after the edge finishes. Sampling it at that exact moment risks reading the value from before the update, not after - the same one-cycle-too-early mistake described back in the DUT article, just from the driver's side this time.
The Driver-Sequencer Handshake
None of the code above explains where tr actually comes from. seq_item_port is a TLM port built into every uvm_driver, and it connects to a matching seq_item_export built into every uvm_sequencer. The diagram below shows where each one lives and what crosses between them:
The two boxes themselves are connected once, during the agent's connect_phase - covered in UVM Agent. Once that connection exists, the driver uses three methods to drive the actual handshake every time it needs a new item:
| Method | Description |
|---|---|
get_next_item | Blocks until a request item is available from the sequencer. Must be followed by an item_done call to complete the handshake. |
try_next_item | Non-blocking version: returns null if no item is available yet, or a pointer to the item if one is. |
item_done | Non-blocking call that completes the handshake. Must be called after get_next_item, or after a successful try_next_item. |
The idea behind this handshake is to let the driver pull a series of transactions from the sequencer, one at a time, only ever asking for the next one once it's finished with the current one. The sequencer's finish_item call stays blocked the entire time, from the moment the driver calls get_next_item until the driver calls item_done() - so the two sides only ever have one transaction in flight at once. There are two ways to write the driver side of this exchange.
get_next_item Followed by item_done
This is the pattern reg_driver uses, and the one to reach for by default. The driver gets an item, drives it, then finishes the handshake by calling item_done(). On the sequencer's side, the matching finish_item call doesn't return until that item_done() arrives - so the two sides stay in lockstep, and the driver only ever operates when the sequencer actually has something for it.
virtual task run_phase(uvm_phase phase);
reg_transaction tr;
forever begin
// 1. Get next item from the sequencer
seq_item_port.get_next_item(tr);
// 2. Drive signals to the interface
@(posedge vif.clk);
vif.wr_en <= tr.wr_en;
vif.rd_en <= tr.rd_en;
vif.addr <= tr.addr;
vif.wdata <= tr.wdata;
// 3. Tell the sequencer that the driver has finished the current item
seq_item_port.item_done();
end
endtask
This is a simplified version of the full reg_driver task built earlier in this article - it leaves out the extra clock waits and the read-data capture, to keep the handshake itself the focus here rather than repeating the timing details already covered.
get Followed by put
The difference with this model is timing: the driver gets the next item and completes the sequencer-side handshake in one go, before it has actually finished driving the item. The sequencer's finish_item call returns as soon as get() happens, not after the driver is done. The driver indicates real completion later, using put().
virtual task run_phase(uvm_phase phase);
reg_transaction tr;
forever begin
// 1. finish_item in the sequence is unblocked here, immediately
seq_item_port.get(tr);
// 2. Drive signals to the interface
@(posedge vif.clk);
vif.wr_en <= tr.wr_en;
vif.rd_en <= tr.rd_en;
vif.addr <= tr.addr;
vif.wdata <= tr.wdata;
// 3. Indicate the item has actually finished, separately from step 1
seq_item_port.put(tr);
end
endtask
This pattern is used less often than get_next_item/item_done, and isn't needed for reg_driver. It's worth recognizing if you encounter it in other codebases, since the timing difference - handshake completing on get() rather than waiting for the driver - can otherwise be a confusing surprise.
Common Mistakes
Mistake #1: Calling item_done() without a matching get_next_item()
The handshake only works as a pair. Calling item_done() on its own, or calling it twice for one item, desynchronizes the driver from the sequencer - the sequencer's finish_item call is waiting for exactly one item_done per item it sent.
Mistake #2: Driving signals with only one clock wait
Wrong:
@(posedge vif.clk);
vif.wr_en <= tr.wr_en;
vif.addr <= tr.addr;
vif.wdata <= tr.wdata;
seq_item_port.item_done();
Right:
@(posedge vif.clk);
vif.wr_en <= tr.wr_en;
vif.addr <= tr.addr;
vif.wdata <= tr.wdata;
@(posedge vif.clk);
seq_item_port.item_done();
Why? Driving signals and immediately calling item_done() tells the sequencer the item is finished before reg_block has even seen the values on its next clock edge. The second wait gives the DUT the cycle it needs to actually act on what was driven.
Practice Exercise
Challenge: Add a Reset Check
Modify reg_driver's run_phase so the driver waits for rst_n to go high before entering its main forever loop, instead of potentially driving signals during reset.
Hint
You'll need a wait statement that blocks until a signal reaches a particular value, placed before the forever loop begins.
Solution
virtual task run_phase(uvm_phase phase);
reg_transaction tr;
@(posedge vif.rst_n);
forever begin
seq_item_port.get_next_item(tr);
...
end
endtask
@(posedge vif.rst_n) blocks until reset is released, so the driver never tries to drive a transaction while reg_block is still being held in reset.
Summary
In this article, you learned:
- A driver extends
uvm_driverand translates transaction objects into pin-level activity on an interface - A virtual interface handle is retrieved with
uvm_config_db::get()insidebuild_phase get_next_itemanditem_doneform a required pair that keeps the driver and sequencer in lockstep- Driving a signal and having the DUT act on it take separate clock edges - skipping a wait is a common source of subtle bugs
Going Deeper
- Scope strings and precedence in config_db: UVM Config Database
- The get_next_item()/item_done() handshake in detail: Using get_next_item()
- Every phase and its execution order: UVM Phases