UVM Project1: D Flip Flop
Hello to all verification enthusiasts! Projects are really important to understand UVM and let's go through this simple project with line by line explanation.
Testbench project1 is to verify a simple D flip flop. Design and verification codes are mentioned and can be run in EDA playground using simple instructions for better understanding.
D Flip Flip Design code:
module dff(
input clk,
input rst,
input [4:0] din,
output reg [4:0] qout
);
always@(posedge clk)
begin
//If reset, output is 0
if(rst)
qout <= 4'b0;
//Else output follows input
else
qout <= din;
end
endmodule
//Interface
interface dif;
logic clk;
logic rst;
logic [3:0] d;
logic [3:0] q;
endinterface
UVM Testbench for D Flip Flop:
Below diagram represents how our testbench should look like. NOTE the connections are not shown, it's a rough diagram of our Testbench.
7 Simple steps to create this testbench:
Create Transaction class
Create Sequencer, Driver, Monitor classes
Create Agent class and connect Sequencer and Driver
Create Scoreboard
Create Env class and connect scoreboard with agent class
Create Testbench class
Create a Sequence and Testcase
Create Top module
`STEP 1: Create Transaction class
Transaction class is a dynamic component of Testbench means it will not stay till end of simulation, so it comes under category of uvm_object and we will extend it from uvm_sequence_item. Since our D Flip Flop design has 4bit input and output ports, the transaction class should follow same because the motive of transaction class is to make a packet of input-output data. Create 4bit 2 variables called 'd' and 'q' where they represent input and output of D flip flop respectively. Also create a 1bit reset variable "rst". Since we want our inputs (here 'd' and 'rst' to be random, that's why these are declared random).
Now as per the guidelines, for a uvm_object, we need to write a new function with single argument of type string which is name of class. Inside this new function, call super.new() with same argument. The reason for calling super.new() is to call the new function of uvm_sequence_item. For example you then create another class by extending transaction class, you should call super.new() in the child class so the parent new() function is executed. Sometimes new() can also set some variables to default value, so it's important not to miss this.
Now the most important part here is to write uvm_object utility macro to register our class with factory. Here we have used begin and end with uvm_object_utils for field automation of all variables. If you don't know the importance of uvm_object_utils or why factory is important, search in scibot blogs for this topic. Inside begin and end, we're using `uvm_field macros for variables. The importance of these lines are this will enable automation on these variables like print, copy, compare etc. To know it in more detail, search for this topic in scibot blogs.
At end of the code, we've used the pre and post randomize function calls. This can be skipped but it's a good practice and this will make easier to understand how things are working when you look into log for these print statements.
class transaction extends uvm_sequence_item;
rand bit [3:0] d;
rand bit rst;
bit [3:0] q;
//New function
function new(string name="transaction");
super.new(name);
endfunction
//Object utility macro
`uvm_object_utils_begin(transaction)
`uvm_field_int(d, UVM_ALL_ON)
`uvm_field_int(rst, UVM_ALL_ON)
`uvm_field_int(q, UVM_ALL_ON)
`uvm_object_utils_end
//Pre randomize function with a print statement
function void pre_randomize();
`uvm_info(get_type_name(),$sformatf("Pre-Randomize D %0d Rst %0d ",d,rst), UVM_LOW);
endfunction
//Post randomize function with a print statement
function void post_randomize();
`uvm_info(get_type_name(),$sformatf("Post-Randomize D %0d Rst %0d ",d,rst), UVM_LOW);
endfunction
endclass
STEP 2A: Create Sequencer class
The role of Sequencer class is quite simple. It acts as an intermediator between 'Sequence' and 'Driver'. A Sequence is one which defines what type of packets and what functionality is to be checked. Sequencer generates these data transactions and send it to driver.
Sequencer is a dynamic component of Testbench as it will stay till the end of the simulation, so it comes under category of uvm_component. Sequencer class is created by extending by uvm_sequencer. Here we have created a type parameterized sequencer class. NOTE: Only active components of a Testbench can be type parameterized, meaning only Sequencer and Driver class can be type parameterized here.
Here since we don't have any variables for which we need automation, we only write uvm_component_utils to register our class with factory. The argument to this is name of sequencer class.
Now as per the guidelines, for a uvm_component, we need to write a new function with 2 arguments: first of type string which class name and second parent which is set to null. Inside this new function, call super.new() with same arguments. The reason for calling super.new() is same which is mentioned above in transaction class explanation.
Here we're just adding the build phase but this part can be skipped because we're not creating anything in build phase.
class dd_sequencer extends uvm_sequencer #(transaction);
`uvm_component_utils(dd_sequencer)
function new(string name="dd_sequencer", uvm_component parent = null);
super.new(name,parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
endfunction
endclass
STEP 2B: Create Driver class
The role of driver class is to get packets of transaction class from "Sequencer" and send it to DUT (Design Under Test; here D Flip Flop). So driver first requests packets from sequencer and then sends it to DUT.
Driver is a static component of Testbench, means it will stay there until end of simulation. So it comes under category of uvm_component and we create driver class by extending it from uvm driver dedicated class i.e uvm_driver class. Here you will notice we have type parameterized it with our transaction class. The advantage of this is it will give a handle 'req' of type 'transaction'. This important feature is always missed by everyone.
Here since we don't have any variables for which we need automation, we only write uvm_component_utils to register our class with factory. The argument to this is name of driver class.
As driver needs access to interface signals of DUT, we create a handle to virtual interface 'vif' of type 'dif' which is our d flip flop intrerface. This will be used to drive signals to DUT. If you're not clear with interface and why we're using virtual interface, I will suggest to search these terms in scibot blog to know in detail.
Now as per the guidelines, for a uvm_component, we need to write a new function with 2 arguments: first of type string which class name and second parent which is set to null. Inside this new function, call super.new() with same arguments. The reason for calling super.new() is same which is mentioned above in transaction class explanation.
Now the most important part here is 'uvm phases'. Because driver class is a component, it goes through multiple phases. If you're not clear with uvm_phases, search this term in scibot blogs to know more. We will be using build phase and run phase in driver class. Build phase to get the handle of virtual interface and run phase to drive the packets to virtual interface.
Remember, build_phase works from top to bottom, meaning the top classes will be built first like first Testbench, then agent, then driver. In build_phase, we always call super.build_phase with phase as argument. Now in short, super.build_phase is used to call the build phase of parent class and also it will call 'apply_config_settings' which in turn helps in overriding. This topic is much more discussed in detail in other blogs by scibot. Here you see after calling super.build_phase, we have if loop in which we're trying to get the handle of virtual interface "vif" from top level into our "vif" handle. If we don't get this handle from top level, we call uvm_error to report error. In build phase only, we create object of transaction class.
The syntax of run_phase is straight forward. It also requires only 1 argument like build_phase i.e. uvm_phase. Inside run_phase, we create a forever loop because we don't know how many packets will be requested and sent to DUT. As you already know, driver and sequencer have inbuilt TLM ports which will be connected in Agent class. Driver requests packet from Sequencer via "seq_item_port" and calling inbuilt function "get_next_item()". Once the packet is received, driver now has to drive the signals of virtual interface. Since we known the transaction class has only 2 inputs: 'd' and 'rst', so we drive the values of this packet on virtual interface. Last after driving, we have to call 'item_done()' function to represent the transaction is complete. The last line of code inside this forever loop is delay of 2 clock cycles. You can select any delay value but remember delay is important else everything will happen at 0 second and your simulation will be stuck.
class dd_driver extends uvm_driver #(transaction);
//transaction req; //this is created by type parameterization
`uvm_component_utils(dd_driver)
//Create handle to virtual interface
virtual dif vif;
//New function
function new(string name="dd_driver", uvm_component parent = null);
super.new(name,parent);
endfunction
//Build phase
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!(uvm_config_db#(virtual dif)::get(this,"","vif",vif)))
`uvm_error(get_type_name(), "No vif handle in driver");
req = transaction::type_id::create("req",this);
endfunction
//Run phase
virtual task run_phase(uvm_phase phase);
forever begin
//Reqesting packet from Sequencer
seq_item_port.get_next_item(req);
//Drive packet to dut
vif.d <= req.d;
vif.rst <= req.rst;
//Send item done to Sequencer
seq_item_port.item_done();
repeat(2) @(posedge vif.clk);
end
endtask
endclass
STEP 2C: Create Monitor class
Monitor is required to observe the response from the DUT. Monitor is a static component of Testbench, means it will stay there until end of simulation. So it comes under category of uvm_component and we create monitor class by extending it from uvm_monitor.
Here since we don't have any variables for which we need automation, we only write uvm_component_utils to register our class with factory. The argument to this is name of monitor class.
Because monitor monitors the response from DUT, it needs to send this to other components of Testbench like scoreboard for comparison. So we declare a uvm_analysis port 'send'. Later this port will be connected to Scoreboard at top level 'env' class. We also declare a handle to virtual interface because we need to monitor the response.
The new function is same and written here. Now the most important part here is 'uvm_phases'. We've declared build_phase. First we need to get the handle of virtual interface from top level. If we don't get this handle, error is raised. Now we create the object of 'req' and port 'send'.
The run_phase is made virtual. The reason behind this is if in future if you need to create a child class of this monitor class, the child class might need to override this definition of run_phase. In ru_phase, most important first we're waiting for delay of 2 clock cycles. NOTE this delay should be same as mentioned in driver class. Now we read the input and output signals of D flipf flop design via virtual interface signal and write it to the 'req' object. After that we send this 'req' packet via port 'send'.
class dd_monitor extends uvm_monitor;
`uvm_component_utils(dd_monitor)
//Analysis port for scoreboard
uvm_analysis_port#(transaction) send;
transaction req;
//Virtual interface
virtual dif vif;
function new(string name="dd_monitor", uvm_component parent = null);
super.new(name,parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!(uvm_config_db#(virtual dif)::get(this,"","vif",vif)))
`uvm_error(get_type_name(), "No vif handle in driver");
req = transaction::type_id::create("req",this);
send = new("send",this);
endfunction
virtual task run_phase(uvm_phase phase);
forever begin
repeat(2 )@(posedge vif.clk);
`uvm_info(get_type_name,$sformatf("Monitor sampling packages"), UVM_LOW)
req.d = vif.d;
req.q = vif.q;
req.rst = vif.rst;
send.write(req);
end
endtask
endclass
STEP 3: Create Agent class
Now the role of agent class is to encapsulate Monitor, Driver and Sequencer. It also comes under the category of uvm_component and we create agent class by extending it from uvm_agent.
Now since we've extended our class from uvm_agent, we get an inbuilt switch called 'is_active' which will tell if the Agent is active or Passive. We'll add this switch in automation macro '`uvm_field_enum' and register our class with factory.
Next steps are creating handle of driver, sequencer and monitor and writing new() function for uvm_component. Now in build_phase, we first check if 'is_active' is active or passive. This is set from top level. NOTE we don't need explicit 'get' here to receive the value of 'is_active'. If this is active agent, only then we create objects of Driver and Sequencer. Monitor is always created.
In connect_phase, same condition is checked. If the agent is active, only then connect the TLM ports of driver and sequencer using the below syntax. Note we have also called super.connect() which can be skipped here.
class dd_agent extends uvm_agent;
//uvm_active_passive_enum is_active; //created by uvm_agent
`uvm_component_utils_begin(dd_agent)
`uvm_field_enum(uvm_active_passive_enumj, is_active, UVM_ALL_ON)
`uvm_component_utils_end
//Create handles
dd_driver d;
dd_monitor m;
dd_sequencer s;
function new(string name="dd_agent", uvm_component parent = null);
super.new(name,parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(is_active == UVM_ACTIVE)
begin
d = dd_driver::type_id::create("d",this);
s = dd_sequencer::type_id::create("s",this);
end
m = dd_monitor::type_id::create("m",this);
endfunction
//Connect Driver and Sequencer
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if(is_active == UVM_ACTIVE)
d.seq_item_port.connect(s.seq_item_export);
endfunction
endclass
STEP 4: Create Scoreboard class
The main purpose of scoreboard for this design is to compare input and output values as per D flip flop table.
First we register our scoreboard class with factory and then declare a uvm_import port 'recv' to receive packets from Monitor. This 'recv' is connected with Monitor port in top env class.
Then the new function is written with for uvm_component syntax.
In the build_phase, we've to create the object of the 'recv' port. Then the most important part here is the 'write' function. The importance of this is because in Monitor, we send the packet via port using this syntax "port.write". So this 'write' function should be present in the class which want to receive this packet from Monitor. Inside this write function, we've coded the comparison logic. If reset is 0, then the output 'q' should be zero. Else if reset is 1, output 'q' should be same as input 'd'.
class dd_scoreboard extends uvm_scoreboard;
`uvm_component_utils(dd_scoreboard)
//Analysis import to get packets from Monitor
uvm_analysis_imp#(transaction, dd_scoreboard) recv;
function new(string name="dd_scoreboard", uvm_component parent = null);
super.new(name,parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
recv = new("recv",this);
endfunction
//Write function of import
virtual function void write(transaction req);
if(req.rst == 1)
begin
if(req.q == 0)
`uvm_info(get_type_name,$sformatf("scoreboard rst0 Pass D %0d Q %0d Rst %0d",req.d,req.q,req.rst), UVM_LOW)
else
`uvm_error(get_type_name,$sformatf("scoreboard rst0 Fail D %0d Q %0d Rst %0d",req.d,req.q,req.rst))
end
else
begin
if(req.q == req.d)
`uvm_info(get_type_name,$sformatf("scoreboard Pass D %0d Q %0d Rst %0d",req.d,req.q,req.rst), UVM_LOW)
else
`uvm_error(get_type_name,$sformatf("scoreboard Fail D %0d Q %0d Rst %0d",req.d,req.q,req.rst))
end
endfunction
endclass
STEP 5: Create Env class
Now we'll be creating an env class to encapsulate Agent and Scoreboard and connect scoreboard with monitor.
We follow the syntax of uvm_component here and extend the class from uvm_env. Register this class with factory and have a new function for component.
In this build phase, create object of Agent and Scoreboard. In the connect phase, using the below syntax, connect the monitor TLM port 'send' with Scoreboard's receiving port 'recv'.
class dd_env extends uvm_env;
`uvm_component_utils(dd_env)
dd_agent a;
dd_scoreboard sc;
function new(string name="dd_env", uvm_component parent = null);
super.new(name,parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
a = dd_agent::type_id::create("a",this);
sc = dd_scoreboard::type_id::create("sc",this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
a.m.send.connect(sc.recv);
endfunction
endclass
STEP 6: Create Testbench class
This class is optional here. You can skip this and directly invoke env class in your testcase. The purpose of this class comes when you have a complex design, then more than 1 env classes are there and you've to encapsulate all those classes under one class which is this Testbench class. The syntax of class is simple, we just create an object of env class here.
class dd_tb extends uvm_env;
`uvm_component_utils(dd_tb)
dd_env env;
function new(string name="dd_tb", uvm_component parent = null);
super.new(name,parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = dd_env::type_id::create("env",this);
endfunction
endclass
STEP 7A : Create a Sequence
Now sequence is something which will actually verify the DUT. Sequence tells what type of packets you want to create which will be sent to DUT later on. Now this sequence below creates 10 packets of type 'transaction' and reset is 0 in all packets.
Now remember, sequence is a uvm_object, so we create this class by extending it from uvm_sequence and following the uvm_object guidelines when registering to factory and for new function. To understand the code better, you need to understand the topic of "Raising objections in uvm" which you can search on scibot blog.
class seq1 extends uvm_sequence #(transaction);
`uvm_object_utils(seq1)
function new(string name="seq1");
super.new(name);
endfunction
virtual task pre_body();
begin
uvm_phase ph = get_starting_phase();
if(ph != null)
begin
starting_phase.raise_objection(this, get_type_name());
$display("Pre body raised");
end
$display("Entering pre body");
end
endtask
virtual task body();
req = transaction::type_id::create("req");
repeat(10)
begin
`uvm_info(get_type_name,$sformatf("Start Sequence"), UVM_LOW);
assert(req.randomize());
//Reset is 0
req.rst = 0;
`uvm_send(req);
`uvm_info(get_type_name,$sformatf("Sent D %0d RST %0d",req.d,req.rst), UVM_LOW);
end
`uvm_info(get_type_name,$sformatf("Seq1 Ends-------------------------------------"), UVM_LOW);
endtask
virtual task post_body();
if(starting_phase != null)
starting_phase.drop_objection(this, get_type_name());
endtask
endclass
STEP 7B : Create a Testcase
A sequence cannot run on its own. A testcase commands which sequence will run on which sequencer. For us, 'seq1' will be made to run on the sequencer. Now test is a uvm_component, so we will follow uvm_component syntax in registering with factory and new function.
Here, in build_phase, we first need to set 'is_active' as UVM_ACTIVE before calling super.build_phase(). Now to explain it better, when the test is executed, testebench and Agent is still not made, that's why in syntax of 'uvm_config_int::set', 'is_active' is written in double quotes. At this point of time, there's no switch like 'is_active'. After this when we call super.build(), this then starts building the hierarchy downwards like from testcases -> testbench -> env -> agent and so on. So super.build() in Agent class will actually call an inbuilt function to set 'is_active'. Then we create object of testbench and seq1.
Now in run task, we first raise objections (Read about Objections in Scibot blogs) and we run the seq1 'ss1' on sequencer via syntax (sequence.start(sequencer)). And after a delay of 10ns, we drop the objections.
class test1 extends uvm_component;
`uvm_component_utils(test1)
dd_tb tb;
seq1 ss1;
function new(string name="test1", uvm_component parent = null);
super.new(name,parent);
endfunction
function void build_phase(uvm_phase phase);
//Set is_active
uvm_config_int::set(this,"*","is_active", UVM_ACTIVE);
super.build_phase(phase);
tb = dd_tb::type_id::create("tb",this);
ss1 = seq1::type_id::create("ss1",this);
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info(get_type_name,$sformatf("SEQ1 starts"), UVM_LOW)
ss1.start(tb.env.a.s);
#10
phase.drop_objection(this);
endtask
endclass
STEP 8: Create Top module
The top module actually binds design with virtual interface, setup clock, set the virtual interface handle to below hierarchies and tells which testcase to run.
Here first we create a handle to dif (vif) which is our design interface. And then we connect design 'dff' with virtual interface signals 'vif'.
After, we initially set the clock of 'vif' to 0 and then in an 'always' procedural statement, we toggle the clock every 10ns.
Inside another 'initial' procedural statement, we first set the virtual interface handle. Now in the below syntax 'uv,_config_db', we set 'vif' at '*' meaning any component in below hierarchy can get the vif via adding' get' statement. To understand this better, go to Scibot blogs. After that, in 'run_test' we tell which testcase to execute.
The last procedural statement is added to dump the vcd files in EDA playground.
module tb_top;
dif vif();
dff design1(.clk(vif.clk), .din(vif.d), .rst(vif.rst), .qout(vif.q));
//Set clock
initial begin
vif.clk = 0;
end
always begin
#10 vif.clk = ~vif.clk;
end
//Run testcase
initial begin
uvm_config_db#(virtual dif)::set(null,"*","vif",vif);
run_test("test1");
end
//Dump waveforms
initial begin
$dumpfile("d.vcd");
$dumpvars;
end
endmodule
HOW TO RUN IT IN EDA PLAYGROUND?
To run this project in EDA playground, you can visit below link and can try running it.
EDA playground project link: https://www.edaplayground.com/x/H8ai
Thank you for reading till end and I hope this project is clear to my readers. Feel free to ask doubts via contact form or I would recommend via comments. Please do leave your feedback and Subscribe for more such posts. In coming days, I'll be creating complete UVM course from scratch to advanced level with multiple projects.
Thank you and have a wonderful day!
Comments