UVM Driver
In the last article, we learnt about UVM sequencer which acts a bridge between sequence and driver. But sequencer just manages the flow of transaction. How these transactions are further sent to the DUT? UVM provides UVM Driver which is responsible for converting these transactions into actual signals which can be sent to DUT. Let's deep dive more into UVM Driver in this article.
What is UVM Driver
UVM Driver is a UVM component which receives transactions (uvm_sequence_item) and translates them into pin-level activity that follows the DUT's protocol.
For example, imagine that we have a sequence which generates apb_transaction
item. Driver is responsible to:
- Request for a new transaction item from the sequencer and receives it.
- Drive the corresponding signals on the APB interface with correct timing and protocol-specific behaviour.
Defining a UVM Driver
We can't use the base uvm_driver
class directly like we did for sequencer. We need to define our own driver which is a sub-class of uvm_driver
class. Let's see how we Drivers are defined.
1. Declaring driver class
UVM driver is declared by extending uvm_driver
parameterized class. Similar to UVM sequencer, drivers also has to 2 class parameter (REQ
and RSP
).
class my_driver extends uvm_driver#(my_seq_item);
2. Factory registration and constructor definition
We need to register the driver with the UVM factory and define constructor as below.
`uvm_component_utils(my_driver)
function new(string name, uvm_component parent)
super.new(name, parent);
endfunction
3. Virtual interface handle
Interface is not a new UVM concept but comes from System Verilog. We have seen earlier how interface provides a seamless way for the test bench to interact with the DUT avoiding any race conditions and driving and sampling signals at correct edges.
virtual my_interface vif_handle;
As driver interacts with DUT directly, it needs interface and is a must for any driver.
As Interface is static in nature we can not directly instantiate it in the class. Rather we will create a instance in the tb_top and pass it throughuvm_config_db
which we will learn in detail in later articles.
Sequencer Driver connection
We saw in the last article that sequencer provides seq_item_export
for connecting with driver. uvm_driver
provides seq_item_port
which connects with seq_item_export
.
This port provides with different methods through which driver can fetch transactions from sequencer. Lets deep dive into this:
get_next_item()
This is a blocking task. When the driver calls get_next_item(output REQ req)
, the driver execution is suspended till the driver receives a transaction from sequencer. Once the driver receives the transaction req
, driver proceeds further and drives corresponding signal towards DUT.
item_done()
This is a non_blocking
method and is called by the driver once the driver has finished driving all the signals. This is an indication to the sequencer that the driver is ready to receive next transaction. We can optionally pass a response transaction rsp
of type RSP
. This response can be used by the sequence to generate a next packet.
get()
This is also a blocking method. When get(output REQ req)
is called by driver, item_done()
is implicitly called and thus need not to be called manually. While using get()
we can't send response packet back to sequence.
peek()
This method helps check if there is any new transaction available without consuming it. peek()
is also a blocking method. Subsequent call to peek()
will fetch the same transaction until the transaction is consumed by calling get_next_item()
or get()
.
Handshake between Sequence, Sequencer and Driver
Understanding the handshake between the Sequence, Sequencer and Driver is important. Any wrong implementation can lead to hang in the test bench or lead to Sequencer sending wrong transaction to the Driver.
Below is the flow:
- When sequence is ready to send a new transaction, it sends a request to sequencer by calling
start_item()
method. - Sequencer waits for the request from driver. When driver calls
get_next_item()
orget()
, the sequencer receives a request from driver for a new packet. - Sequencer will now check if there is any sequence trying to send a transaction. If there is none, it will wait for the request from sequence. If there are more than one sequence trying to send packet, sequencer will grant permission to any one of the sequence based on arbitration method.
- Once the sequence receives a grant, it will generate a packet. When
finish_item()
is called the transaction is actually sent to the sequencer. - Sequencer will forward the transaction as is to the driver.
- Driver receives the transaction and drives the signal to the DUT. Once it has finished driving signals, driver will call
item_done()
to indicate the same.
In case driver had calledget()
then there is no need to callitem_done()
in step 6.
This handshake mechanism ensures that Sequence, Sequencer and Driver remains synchronised and neither of them is overwhelmed by excessive requests. This means that if Driver is not able to process any new transaction, transaction will be blocked until Driver gets freed. Similarly, if Sequence is not ready with a new transaction it would block the entire flow.
Wrong implementation of the handshake can lead to test bench hang or wrong transaction being sent to driver.
Example
import uvm_pkg::*;
class driver extends uvm_driver#(transaction);
`uvm_component_utils(driver)
virtual alu_if 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 alu_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "Virtual interface not found")
endfunction
task run_phase(uvm_phase phase);
super.run_phase(phase);
forever begin
seq_item_port.get_next_item(req);
drive_item(req);
seq_item_port.item_done();
end
endtask
task drive_item(transaction req);
vif.OPA <= req.OPA;
vif.OPB <= req.OPB;
vif.cin <= req.cin;
vif.cmd <= req.cmd;
vif.ce <= req.ce;
vif.mode <= req.mode;
endtask
endclass
Interface
As Interface is already covered in our SV article, we won't go over the details again. For this example we will create a very basic interface as below.
interface alu_if(input logic clk);
logic [7:0] OPA, OPB;
logic cin, mode, ce;
logic [3:0] cmd;
logic [2:0] egl;
logic [8:0] res;
logic cout, err, oflow;
endinterface
TB Top
Let's modify the test bench top to add driver in the TB and see how the TB hierarchy changes. We will also be able to appreciate the different arbitration method in the sequencer.
`include "package.svh"
`include "uvm_macros.svh"
`include "interface.sv"
class env extends uvm_component;
`uvm_component_utils(env)
sequencer custom_seqr;
uvm_sequencer#(transaction) base_seqr;
random_seq rnd_seq1, rnd_seq2, rnd_seq3;
int use_arbitratoin_method;
driver drv;
// Constructor:new
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction: new
// function: build
function void build_phase(uvm_phase phase);
super.build_phase(phase);
custom_seqr = sequencer::type_id::create("custom_seqr", this);
base_seqr = uvm_sequencer#(transaction)::type_id::create("base_seqr", this);
drv = driver::type_id::create("drv", this);
rnd_seq1 = random_seq::type_id::create("rnd_seq1");
rnd_seq2 = random_seq::type_id::create("rnd_seq2");
rnd_seq3 = random_seq::type_id::create("rnd_seq3");
endfunction: build_phase
// function: connect_phase
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
drv.seq_item_port.connect(custom_seqr.seq_item_export);
endfunction: connect_phase
// function: end_of_elaboration_phase
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
uvm_top.print_topology();
endfunction: end_of_elaboration_phase
// task: run_phase
task run_phase(uvm_phase phase);
super.run_phase(phase);
phase.raise_objection(this);
fork
rnd_seq1.start(custom_seqr, null, 20);
rnd_seq2.start(custom_seqr, null, 10);
rnd_seq3.start(custom_seqr, null, 20);
join
phase.drop_objection(this);
endtask: run_phase
// function: start_of_simulation_phase
function void start_of_simulation_phase(uvm_phase phase);
super.start_of_simulation_phase(phase);
// set the below variable (1-6) to try different arbitration methods
use_arbitratoin_method = 6;
// ---------------------------------------
case(use_arbitratoin_method)
1: custom_seqr.set_arbitration(UVM_SEQ_ARB_FIFO);
2: custom_seqr.set_arbitration(UVM_SEQ_ARB_RANDOM);
3: custom_seqr.set_arbitration(UVM_SEQ_ARB_WEIGHTED);
4: custom_seqr.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
5: custom_seqr.set_arbitration(UVM_SEQ_ARB_STRICT_RANDOM);
6: custom_seqr.set_arbitration(UVM_SEQ_ARB_USER);
default: custom_seqr.set_arbitration(UVM_SEQ_ARB_FIFO);
endcase
endfunction: start_of_simulation_phase
endclass: env
// temp tb_top
module temp_top();
bit clk;
alu_if alu_if_inst(clk);
always #5 clk = ~clk;
initial begin
run_test("env");
end
// Set the virtual interface for driver
initial begin
uvm_config_db#(virtual alu_if)::set(null, "*", "vif", alu_if_inst);
end
endmodule: temp_top
Try this code in EDA PlaygroundOutput
# KERNEL: UVM_INFO /home/build/vlib1/vlib/uvm-1.2/src/base/uvm_root.svh(583) @ 0: reporter [UVMTOP] UVM testbench topology:
# KERNEL: ----------------------------------------------------------
# KERNEL: Name Type Size Value
# KERNEL: ----------------------------------------------------------
# KERNEL: uvm_test_top env - @336
# KERNEL: base_seqr uvm_sequencer - @490
# KERNEL: rsp_export uvm_analysis_export - @499
# KERNEL: seq_item_export uvm_seq_item_pull_imp - @617
# KERNEL: arbitration_queue array 0 -
# KERNEL: lock_queue array 0 -
# KERNEL: num_last_reqs integral 32 'd1
# KERNEL: num_last_rsps integral 32 'd1
# KERNEL: custom_seqr sequencer - @353
# KERNEL: rsp_export uvm_analysis_export - @362
# KERNEL: seq_item_export uvm_seq_item_pull_imp - @480
# KERNEL: arbitration_queue array 0 -
# KERNEL: lock_queue array 0 -
# KERNEL: num_last_reqs integral 32 'd1
# KERNEL: num_last_rsps integral 32 'd1
# KERNEL: drv driver - @627
# KERNEL: rsp_port uvm_analysis_port - @646
# KERNEL: seq_item_port uvm_seq_item_pull_port - @636
# KERNEL: ----------------------------------------------------------
# KERNEL:
# KERNEL: UVM_INFO /home/runner/sequencer.sv(20) @ 0: uvm_test_top.custom_seqr [custom_seqr] running custom sequencer...
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [RND_SEQ] Sent packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(21) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Sending packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [RND_SEQ] Sent packet
Let's decode the output:
- In the testbench hierarchy we can see a new component
driver
with namedrv
. - When the
run_phase
starts, all 3 sequences requests for a grant. - At 0ns, driver requests for a transaction. Due to our custom arbitration method, sequencer grants access to
rnd_seq3
. rnd_seq3
sends transaction to driver and driver drives the signal to the DUT @5ns.- At 5ns, driver calls
item_done()
and again on next iteration callsget_next_item()
requesting a new transaction. - At 5ns, sequencer has grant request from
rnd_seq2
andrnd_seq1
. Sequencer gives grant tornd_seq2
. rnd_seq2
sends a transaction which is driven by the driver and driver request for a new transaction @10ns.- At 7ns,
rnd_seq3
requests for a grant again. - At 10ns, sequencer has grant request from
rnd_seq3
andrnd_seq1
. Sequencer grants access tornd_seq3
. - This goes on until all transaction has been sent by all the sequences.
Conclusion
In this article we went through how to declare UVM Driver using uvm_driver
base class. We also saw various methods through which we can pull transaction from the sequencer and how the handshake happens between Sequence, Sequencer and Driver. In the next article we will see how we monitors the actual activity happening in the DUT interface.