概念
首先,要知道为什么要使用接口?带来了什么样的便利?随着设计的日益复杂,设计中的不同功能的设计会被切割成不同的模块,模块与模块之间通信端口随着设计的复杂程度增加而变得很多,测试平台与不同模块和子模块DUT之间的端口连接也就变得更加复杂。这时,如果我们仍然采用对每个端口一一连接的方式,显然是有很承重的负担,日后代码的维护工作也变得很困难。接口就起到一根很粗电缆线的作用,连接着设计与验证环境。
如下,当两个模块module之间不采用接口连接时:
//子模块1
module memMod( input logic req,
input logic clk,
input logic start,
input logic [1:0] mode,
input logic [7:0] addr,
inout wire [7:0] data,
output bit gnt,
output bit rdy );
logic avail;
...
endmodule
//子模块2
module cpuMod( input logic clk,
input logic gnt,
input logic rdy,
inout wire [7:0] data,
output logic req,
output logic start,
output logic [7:0] addr,
output logic [1:0] mode );
...
endmodule
//顶层模块,将模块间的信号做连接
module top;
logic req, gnt, start, rdy;
logic clk = 0;
logic [1:0] mode;
logic [7:0] addr;
wire [7:0] data;
memMod mem(req, clk, start, mode, addr, data, gnt, rdy);
cpuMod cpu(clk, gnt, rdy, data, req, start, addr, mode);
endmodule
如下,当设计之间使用interface做连接时:
interface simple_bus; // 定义接口
logic req, gnt;
logic [7:0] addr, data;
logic [1:0] mode;
logic start, rdy;
endinterface: simple_bus
module memMod( top_vif vif, //传递接口
input logic clk);
logic avail;
always @(posedge clk) a.gnt <= a.req & avail;//调用通过接口传递的参数
endmodule
module cpuMod( top_vif vif, input logic clk);
...
endmodule
//两个module的顶层模块
module top;
logic clk = 0;
simple_bus top_vif(); // 例化接口
memMod mem(top_vif, clk); // 写法一:将接口与module连接
cpuMod cpu(.top_vif(top_vif), .clk(clk)); //写法二:
endmodule
很明显对比发现,在设计的RTL中采用接口对端口的连接会更简便,维护也更容易。在验证环境中,发激励的driver和采样的monitor都需要直接与设计之间“沟通”,同样的,如果没有接口,试想每次写driver都要自己手动连接与设计上的信号,估计你会分分钟删库跑路吧。所以,以下将更多的是从验证的角度阐述接口的特点和使用。
一、特点
- 接口在module可以直接声明例化,但在class中只能声明为virtual。正是通过虚接口连接到硬件上,使得“软件”部分可以操作硬件设计;
- interface可以在module中声明和实例化,但是module不能存在interface中;
- 接口内可以定义参数、变量、initial和always块、function、task等;
- 接口内的信号需要DUT连接,因此要用四值逻辑来确保有x和z状态,而非二值逻辑。接口中最好可以使用 logic 做为信号的类型。因为logic可以直接赋值,而 wire 必须要被连续赋值语句来驱动。
二、modport
modport可以简单的理解成电线收纳盒。他可以将接口内的信号分组,并定义它们的方向。如下:
interface i2;
logic a, b, c, d;
modport master (input a, b, output c, d);
modport slave (output a, b, input c, d);
endinterface
module m (i2 vif);
...
endmodule
module s (i2 vif);
...
endmodule
module top;
i2 vif();//例化接口
//与设计中的信号做连接
m u1(vif.master);//连接方式一:直接连接接口中的对应modport
s u2(.vif(vif.slave));//连接方式二:
//将虚接口传递给验证组件
//1.如果是纯SV的环境,可以在test--> env --> agent 每一层都定义一个set_intf()函数,在其中将虚接口句柄做连接。
//2.如果是UVM环境,在这里可以通过uvm_config_db给对应层次路径传递一个虚接口,如下:
uvm_config_db#(virtual i2)::set(uvm_root::get(), "uvm_test_top.env.agent", "vif", vif);
endmodule
注意:modport起到的就是整理interface内部的信号的作用,所以它整理的信号必须要是事先在interface中定义好的信号。
三、clocking时钟块
时钟块可以保证接口内的信号可以同步地驱动和采样。每一个时钟块都有一个时钟表达式,对应一个时钟域。还可以通过default指定一个输出和输入信号采样的时钟偏移。
interface A_Bus( input logic clk );
logic req, gnt;
logic [7:0] addr, data;
clocking drv_cb @(posedge clk);
default input #1ps output #1ps;
input gnt;
output req, addr;
inout data;
endclocking
clocking mon_cb @(posedge clk);
default input #1ps;
input gnt, req, addr, data;
endclocking
endinterface
给clocking时钟块内input和output采样和驱动加上一个很小的延时,目的也就是为了防止采样时的竞争问题,具体可以看:clocking块解决采样时的竞争。
四、参数传递
interface可以和module一样,在其中定义参数,然后可以在例化的时候传递进来新的参数,极大的增加interface的重用性。如下:
interface simple_bus #(AWIDTH = 8, DWIDTH = 8)
(input logic clk); //定义参数,在例化时传递进来
logic req, gnt;
logic [AWIDTH-1:0] addr;
logic [DWIDTH-1:0] data;
logic [1:0] mode;
logic start, rdy;
modport master( input gnt, rdy, clk,
output req, addr, mode, start,
ref data,
import task masterRead(input logic [AWIDTH-1:0] raddr),
import task masterWrite(input logic [AWIDTH-1:0] waddr));
task masterWrite(input logic [AWIDTH-1:0] waddr);
...
endtask
task masterRead(input logic [AWIDTH-1:0] raddr);
...
endtask
endinterface
module top;
simple_bus #(.DWIDTH(16)) wide_intf(clk);
endmodule
注:modport中也可以将interface中的方法进行整理,用import将方法引入到modport中,它们对任何使用这个modport的块都是可见的。也就是说在上面的例子中,拿到这个虚接口 vif 的组件可以通过 vif.master.masterRead 的方式,直接调用接口内 masterRead() 任务。
五、SVA断言和断言覆盖率
对于一般的协议接口,因为协议有对时序是有要求,所以在接口中定义断言对协议的时序进行检查,来调试设计中遇到的问题。同时断言也可以触发功能覆盖率和断言覆盖率,所以也可以在接口中定义断言covergroup。
以一个APB协议的接口为例,如下:
interface apb_vif (input clk, input rstn);
logic [31:0] paddr;
logic pwrite;
logic psel;
logic penable;
logic [31:0] pwdata;
logic [31:0] prdata;
logic pready;
logic pslverr;
... //clocking的定义省略
//---------APB 读写操作的时序覆盖------------
covergroup apb_write_read @(posedge clk iff (rstn && penable));
write_read_order: coverpoint pwrite{
bins write_write = (1 => 1);
bins write_read = (1 => 0);
bins read_write = (0 => 1);
bins read_read = (0 => 0);
}
endgroup
//----------例化covergroup--------
initial begin
automatic apb_write_read cg = new();
end
//--------收集断言覆盖率--------
property p_write_read_burst_trans;
logic[31:0] addr;
@(posedge clk)
($rose(penable) && pwrite, addr=paddr)
|-> (##2 ($rose(penable) && !pwrite && addr==paddr));
endproperty: p_write_read_burst_trans
cover property(p_write_read_burst_trans);
//--------用断言检查时序---------
property p_paddr_no_x;
@(posedge clk)
psel |-> !$isunknown(paddr);
endproperty: p_paddr_no_x
assert property(p_paddr_no_x) else `uvm_error("ASSERT", "PADDR is unknown when PSEL is high")
//--------断言控制----------
initial begin
fork
forever begin
wait(rstn == 0);
$assertoff(); //关闭断言
wait(rstn == 1);
$asserton(); //打开断言
end
join_none
end
endinterface
六、产生时钟和复位信号
在一些设计中,我们可能需要单独的设置一个reset_agent,专门用于对设计的复位信号进行测试。由此我们可以定义一个reset_vif,用来产生时钟和复位信号的控制。如下:
interface reset_if(input logic clk);
logic reset;
//-----产生时钟-----
initial begin
logic clk <= 0;
forever begin
#5ns clk <= ~clk;
end
end
//------产生复位信号-----
task reset_dut;
reset = 1'b0;
#5ns;
reset = 1'b1;
repeat (100) @(negedge clk);
#2ns;
reset = 1'b0;
endtask
endinterface