Till the previous stage we had defined and created everything required within the register environment. However which agent is responsible for driving these register transactions hadn't been defined.
class my_env extends uvm_env;
`uvm_component_utils (my_env)
my_agent m_agent;
reg_env m_reg_env;
function new (string name = "my_env", uvm_component parent);
super.new (name, parent);
endfunction
virtual function void build_phase (uvm_phase phase);
super.build_phase (phase);
m_agent = my_agent::type_id::create ("m_agent", this);
m_reg_env = reg_env::type_id::create ("m_reg_env", this);
endfunction
virtual function void connect_phase (uvm_phase phase);
super.connect_phase (phase);
m_agent.m_mon.mon_ap.connect (m_reg_env.m_apb2reg_predictor.bus_in);
m_reg_env.m_ral_model.default_map.set_sequencer (m_agent.m_seqr, m_reg_env.m_reg2apb);
endfunction
endclass
Modport lists with directions are defined in an interface to impose certain restrictions on interface access within a module. The keyword modport
indicates that the directions are declared as if inside the module.
Syntax
modport [identifier] (
input [port_list],
output [port_list]
);
Shown below is the definition of an interface myInterface which has a few signals and two modport
declarations. The modport dut0 essentially states that the signals ack and sel are inputs and gnt and irq0 are outputs to whatever module uses this particular modport.
Similarly, another modport called dut1 is declared which states that gnt and irq0 are inputs and the other two are outputs for any module that uses modport dut1.
interface myInterface;
logic ack;
logic gnt;
logic sel;
logic irq0;
// ack and sel are inputs to the dut0, while gnt and irq0 are outputs
modport dut0 (
input ack, sel,
output gnt, irq0
);
// ack and sel are outputs from dut1, while gnt and irq0 are inputs
modport dut1 (
input gnt, irq0,
output ack, sel
);
endinterface
Example of named port bundle
In this style, the design will take the required correct modport definition from the interface object as mentioned in its port list. The testbench only needs to provide the whole interface object to the design.
module dut0 ( myinterface.dut0 _if);
...
endmodule
module dut1 ( myInterface.dut1 _if);
...
endmodule
module tb;
myInterface _if;
dut0 d0 ( .* );
dut1 d1 ( .* );
endmodule
Example of connecting port bundle
In this style, the design simply accepts whatever directional information is given to it. Hence testbench is responsible to provide the correct modport values to the design.
module dut0 ( myinterface _if);
...
endmodule
module dut1 ( myInterface _if);
...
endmodule
module tb;
myInterface _if;
dut0 d0 ( ._if (_if.dut0));
dut1 d1 ( ._if (_if.dut1));
endmodule
What is the need for a modport ?
Nets declared within a simple interface is inout
by default and hence any module connected to the same net, can either drive values or take values from it. In simple words, there are no restrictions on direction of value propagation. You could end up with an X on the net because both the testbench and the design are driving two different values to the same interface net. Special care should be taken by the testbench writer to ensure that such a situation does not happen. This can be inherently avoided by the use of modports.
Example of connecting to generic interface
A module
can also have a generic interface as the portlist. The generic handle can accept any modport passed to it from the hierarchy above.
module dut0 ( interface _if);
...
endmodule
module dut1 ( interface _if);
...
endmodule
module tb;
myInterface _if;
dut0 d0 ( ._if (_if.dut0));
dut1 d1 ( ._if (_if.dut1));
endmodule
Design Example
Lets consider two modules master and slave connected by a very simple bus structure. Assume that the bus is capable of sending an address and data which the slave is expected to capture and update the information in its internal registers. So the master always has to initiate the transfer and the slave is capable of indicating to the master whether it is ready to accept the data by its sready signal.
Interface
Shown below is an interface
definition that is shared between the master and slave modules.
interface ms_if (input clk);
logic sready; // Indicates if slave is ready to accept data
logic rstn; // Active low reset
logic [1:0] addr; // Address
logic [7:0] data; // Data
modport slave ( input addr, data, rstn, clk,
output sready);
modport master ( output addr, data,
input clk, sready, rstn);
endinterface
Design
Assume that the master simply iterates the address from 0 to 3 and sends data equal to the address multiplied by 4. The master should only send when the slave is ready to accept and is indicated by the sready signal.
// This module accepts an interface with modport "master"
// Master sends transactions in a pipelined format
// CLK 1 2 3 4 5 6
// ADDR A0 A1 A2 A3 A0 A1
// DATA D0 D1 D2 D3 D4
module master ( ms_if.master mif);
always @ (posedge mif.clk) begin
// If reset is applied, set addr and data to default values
if (! mif.rstn) begin
mif.addr <= 0;
mif.data <= 0;
// Else increment addr, and assign data accordingly if slave is ready
end else begin
// Send new addr and data only if slave is ready
if (mif.sready) begin
mif.addr <= mif.addr + 1;
mif.data <= (mif.addr * 4);
// Else maintain current addr and data
end else begin
mif.addr <= mif.addr;
mif.data <= mif.data;
end
end
end
endmodule
Assume that the slave accepts data for every addr and assigns them to internal registers. When the address wraps from 3 to 0, the slave requires 1 additional clock to become ready.
module slave (ms_if.slave sif);
reg [7:0] reg_a;
reg [7:0] reg_b;
reg reg_c;
reg [3:0] reg_d;
reg dly;
reg [3:0] addr_dly;
always @ (posedge sif.clk) begin
if (! sif.rstn) begin
addr_dly <= 0;
end else begin
addr_dly <= sif.addr;
end
end
always @ (posedge sif.clk) begin
if (! sif.rstn) begin
reg_a <= 0;
reg_b <= 0;
reg_c <= 0;
reg_d <= 0;
end else begin
case (addr_dly)
0 : reg_a <= sif.data;
1 : reg_b <= sif.data;
2 : reg_c <= sif.data;
3 : reg_d <= sif.data;
endcase
end
end
assign sif.sready = ~(sif.addr[1] & sif.addr[0]) | ~dly;
always @ (posedge sif.clk) begin
if (! sif.rstn)
dly <= 1;
else
dly <= sif.sready;
end
endmodule
The two design modules are tied together at a top level.
module d_top (ms_if tif);
// Pass the "master" modport to master
master m0 (tif.master);
// Pass the "slave" modport to slave
slave s0 (tif.slave);
endmodule
Testbench
The testbench will pass the interface handle to the design, which will then assign master and slave modports to its sub-modules.
module tb;
reg clk;
always #10 clk = ~clk;
ms_if if0 (clk);
d_top d0 (if0);
// Let the stimulus run for 20 clocks and stop
initial begin
clk <= 0;
if0.rstn <= 0;
repeat (5) @ (posedge clk);
if0.rstn <= 1;
repeat (20) @ (posedge clk);
$finish;
end
endmodule
Remember that the master initiates bus transactions and the slave captures data and stores it in its internal registers reg_* for the corresponding address.
Introduction covered the need for an interface, how to instantiate and connect the interface with a design. There are two ways in which the design can be written:
- By using an existing interface name to specifically use only that interface
- By using a generic interface handle to which any interface can be passed
Obviously, the generic method works best when interface definitions are updated to newer versions with a different name, and needs to support older designs that use it.
Example using a named bundle
In this case, the design references the actual interface name for access to its signals. The example below shows that both design modules myDesign and yourDesign declares a port in the port list called if0 of type myInterface to access signals.
module myDesign ( myInterface if0,
input logic clk);
always @ (posedge clk)
if (if0.ack)
if0.gnt <= 1;
...
endmodule
module yourDesign ( myInterface if0,
input logic clk);
...
endmodule
module tb;
logic clk = 0;
myInterface _if;
myDesign md0 (_if, clk);
yourDesign yd0 (_if, clk);
endmodule
Example using a generic bundle
A SystemVerilog interface
allows us to group a number of signals together and represent them as a single port. All these signals can be declared and maintained at a single place and be easily maintained. Signals within an interface are accessed by the interface instance handle.
Syntax
Interface blocks are defined and described within interface
and endinterface
keywords. It can be instantiated like a module with or without ports.
interface [name] ([port_list]);
[list_of_signals]
endinterface
Interfaces can also have functions, tasks, variables, and parameters making it more like a class template. It also has the ability to define policies of directional information for different module ports via the modport
construct along with testbench synchronization capabilities with clocking blocks. It can also have assertions, coverage recording and other protocol checking elements. Last but not the least, it can also contain initial
and always
procedures and continuous assign
statements.
A module cannot be instantiated in an interface ! But an interface can be instantiated within a module.
SystemVerilog is now popular as a HDL and let's see two cases where an interface is used with the same design in both Verilog and SystemVerilog. To keep things simple in this introductory example, we'll just create a simple interface.
Interface with a Verilog Design
Let us see how an interface can be used in the testbench and connected to a standard Verilog design with a portlist. The code shown below is a design of an up-down counter in Verilog. This module accepts a parameter to decide the width of the counter. It also accepts an input load value load that is loaded into the counter only when load_en is 1.

The counter starts counting down when the input down is 1 and otherwise it counts upwards. The rollover output indicates when the counter either transitions from a max_value to 0 or a 0 to max_value.
module counter_ud
#(parameter WIDTH = 4)
(
input clk,
input rstn,
input wire [WIDTH-1:0] load,
input load_en,
input down,
output rollover,
output reg [WIDTH-1:0] count
);
always @ (posedge clk or negedge rstn) begin
if (!rstn)
count <= 0;
else
if (load_en)
count <= load;
else begin
if (down)
count <= count - 1;
else
count <= count + 1;
end
end
assign rollover = &count;
endmodule
An interface called cnt_if is declared below with a parameterizable value as the width of the counter signal. This task also has a task init() to assign values
interface cnt_if #(parameter WIDTH = 4) (input bit clk);
logic rstn;
logic load_en;
logic [WIDTH-1:0] load;
logic [WIDTH-1:0] count;
logic down;
logic rollover;
endinterface
module tb;
reg clk;
// TB Clock Generator used to provide the design
// with a clock -> here half_period = 10ns => 50 MHz
always #10 clk = ~clk;
cnt_if cnt_if0 (clk);
counter_ud c0 ( .clk (cnt_if0.clk),
.rstn (cnt_if0.rstn),
.load (cnt_if0.load),
.load_en (cnt_if0.load_en),
.down (cnt_if0.down),
.rollover (cnt_if0.rollover),
.count (cnt_if0.count));
initial begin
bit load_en, down;
bit [3:0] load;
$monitor("[%0t] down=%0b load_en=%0b load=0x%0h count=0x%0h rollover=%0b",
$time, cnt_if0.down, cnt_if0.load_en, cnt_if0.load, cnt_if0.count, cnt_if0.rollover);
// Initialize testbench variables
clk <= 0;
cnt_if0.rstn <= 0;
cnt_if0.load_en <= 0;
cnt_if0.load <= 0;
cnt_if0.down <= 0;
// Drive design out of reset after 5 clocks
repeat (5) @(posedge clk);
cnt_if0.rstn <= 1;
// Drive stimulus -> repeat 5 times
for (int i = 0; i < 5; i++) begin
// Drive inputs after some random delay
int delay = $urandom_range (1,30);
#(delay);
// Randomize input values to be driven
std::randomize(load, load_en, down);
// Assign tb values to interface signals
cnt_if0.load <= load;
cnt_if0.load_en <= load_en;
cnt_if0.down <= down;
end
// Wait for 5 clocks and finish simulation
repeat(5) @ (posedge clk);
$finish;
end
endmodule
ncsim> run [0] down=0 load_en=0 load=0x0 count=0x0 rollover=0 [96] down=1 load_en=1 load=0x1 count=0x0 rollover=0 [102] down=0 load_en=0 load=0x9 count=0x0 rollover=0 [108] down=1 load_en=1 load=0x1 count=0x0 rollover=0 [110] down=1 load_en=1 load=0x1 count=0x1 rollover=0 [114] down=1 load_en=0 load=0xc count=0x1 rollover=0 [120] down=1 load_en=0 load=0x7 count=0x1 rollover=0 [130] down=1 load_en=0 load=0x7 count=0x0 rollover=0 [150] down=1 load_en=0 load=0x7 count=0xf rollover=1 [170] down=1 load_en=0 load=0x7 count=0xe rollover=0 [190] down=1 load_en=0 load=0x7 count=0xd rollover=0 Simulation complete via $finish(1) at time 210 NS + 0
Interface with a SystemVerilog design
Let us now see how an interface can be used in the testbench and be connected to a SystemVerilog design module. SystemVerilog allows a module
to accept an interface as the portlist instead of individual signals. In the design example shown below, we have substituted the portlist of counter_ud with an interface handle which is used to define design functionality.

`timescale 1ns/1ns
// This module accepts an interface object as the port list
module counter_ud #(parameter WIDTH = 4) (cnt_if _if);
always @ (posedge _if.clk or negedge _if.rstn) begin
if (!_if.rstn)
_if.count <= 0;
else
if (_if.load_en)
_if.count <= _if.load;
else begin
if (_if.down)
_if.count <= _if.count - 1;
else
_if.count <= _if.count + 1;
end
end
assign _if.rollover = &_if.count;
endmodule
The design instance is passed an interface handle called cnt_if and is used to drive inputs to the design from the testbench. The same interface handle can be used to monitor outputs from the design if required.
// Interface definition is the same as before
module tb;
reg clk;
// TB Clock Generator used to provide the design
// with a clock -> here half_period = 10ns => 50 MHz
always #10 clk = ~clk;
cnt_if cnt_if0 (clk);
// Note that here we just have to pass the interface handle
// to the design instead of connecting each individual signal
counter_ud c0 (cnt_if0);
// Stimulus remains the same as before
ncsim> run [0] down=0 load_en=0 load=0x0 count=0x0 rollover=0 [96] down=1 load_en=1 load=0x1 count=0x0 rollover=0 [102] down=0 load_en=0 load=0x9 count=0x0 rollover=0 [108] down=1 load_en=1 load=0x1 count=0x0 rollover=0 [110] down=1 load_en=1 load=0x1 count=0x1 rollover=0 [114] down=1 load_en=0 load=0xc count=0x1 rollover=0 [120] down=1 load_en=0 load=0x7 count=0x1 rollover=0 [130] down=1 load_en=0 load=0x7 count=0x0 rollover=0 [150] down=1 load_en=0 load=0x7 count=0xf rollover=1 [170] down=1 load_en=0 load=0x7 count=0xe rollover=0 [190] down=1 load_en=0 load=0x7 count=0xd rollover=0 Simulation complete via $finish(1) at time 210 NS + 0
What makes it different from Verilog ?
Verilog connects between different modules through its module ports. For large designs, this method of connection can become more time consuming and repetitious. Some of these ports may include signals related to bus protocols like AXI/AHB, clock and reset pins, signals to and from RAM/memory and to other peripheral devices.
Using Verilog Ports
This is the traditional way of port connection in Verilog.
module d_slave ( input clk,
reset,
enable,
// Many more input signals
output gnt,
irq,
// Many more output signals);
// Some design functionality
endmodule
module d_top ( [top_level_ports] );
reg [`NUM_SLAVES-1:0] clk; // Assume `NUM_SLAVES is a macro set to 2
reg [`NUM_SLAVES-1:0] tb_reset;
// Other declarations
d_slave slave_0 ( .clk (d_clk[0]), // These connections have to be
.reset (d_reset[0]) // repeated for all other slave instances
...
.gnt (d_gnt[0]),
... );
d_slave slave_1 ( ... );
d_slave slave_2 ( ... );
endmodule
Let us consider a scenario where there are twelve slaves in the design shown above. If there is a change made at the d_slave module ports, then the change has to be reflected in all the twelve slave instance connections in d_top as well.
Disadvantages
Some cons of using Verilog port method for connection are :
- Tedious to trace, debug and maintain
- Too easy to make or break design functionality
- Changes in design requirements may require modifications in multiple modules
- Duplication needed in multiple modules, communication protocols, and other places
Using SystemVerilog Interface
Note that the module d_top simply uses the interface to connect with the slave instances instead of repetitively declaring connection to each signal of the slave block as shown before.
interface slave_if (input logic clk, reset);
reg clk;
reg reset;
reg enable;
reg gnt;
// Declarations for other signals follow
endinterface
module d_slave (slave_if s_if);
// Design functionality
always (s_if.enable & s_if.gnt) begin // interface signals are accessed by the handle "s_if"
// Some behavior
end
endmodule
module d_top (input clk, reset);
// Create an instance of the slave interface
slave_if slave_if_inst ( .clk (clk),
.reset (reset));
d_slave slave_0 (.s_if (slave_if_inst));
d_slave slave_1 (.s_if (slave_if_inst));
d_slave slave_2 (.s_if (slave_if_inst));
endmodule
Now, if there is a change to one of the signals in the slave interface, it is automatically applied to all the instances. In SystemVerilog, the module portlist can also have a port with an interface type instead of the usual input
, output
and inout
.
Interface Array
In the example below an interface named myInterface with an empty port list is created and instantiated within the top level testbench module. It is also fine to omit the parenthesis for an empty port list and instead truncate the statement with a semicolon
// interface myInterface;
interface myInterface ();
reg gnt;
reg ack;
reg [7:0] irq;
...
endinterface
module tb;
// Single interface handle
myInterface if0 ();
// An array of interfaces
myInterface wb_if [3:0] ();
// Rest of the testbench
endmodule
A single interface called if0 can be instantiated and signals within this interface should be accessed by referencing this handle. This can then be used to drive and sample signals going to the DUT.
We can also have an array of interfaces. Here this array is referred by the name wb_if which has 4 instances of the interface.
module myDesign ( myInterface dut_if,
input logic clk);
always @(posedge clk)
if (dut_if.ack)
dut_if.gnt <= 1;
endmodule
module tb;
reg clk;
// Single interface handle connection
myInterface if0;
myDesign top (if0, clk);
// Or connect by name
// myDesign top (.dut_if(if0), .clk(clk));
// Multiple design instances connected to the appropriate
// interface handle
myDesign md0 (wb_if[0], clk);
myDesign md1 (wb_if[1], clk);
myDesign md2 (wb_if[2], clk);
myDesign md3 (wb_if[3], clk);
endmodule
When an interface is referenced as a port, the variables and nets in it are assumed to have ref
and inout
access respectively. If same identifiers are used as interface instance name and port name in the design, then implicit port connections can also be used.
module tb;
reg clk;
myInterface dut_if();
// Can use implicit port connection when all port signals have same name
myDesign top (.*);
endmodule
When values need to be assigned between two different data type variables, ordinary assignment might not be valid and instead a system task called $cast
should be used.
$cast
can be called as either a task or a function, the difference being that when used as a function, it returns a 1 if the cast is legal. It becomes useful in handling invalid assignments.
Click here for an example !
Syntax
function int $cast (targ_var, source_exp);
task $cast (targ_var, source_exp);
Here, targ_var is the target variable and source_exp is the source expression that should be evaluated and assigned to the target variable.
Calling as a task/function
When $cast
is called as a task, it will attempt to assign the source expression to the target variable and if it's invalid, a runtime error will occur and the target variable will remain unchanged.
When $cast
is called as a function, it will attempt to assign the source expression to the target variable and return 1 if it succeeds. It does not make the assignment if it fails and returns 0. Note that in this case there will be no runtime error, and the simulation will proceed with the unchanged value of the destination variable.