SV学习(3)——接口interface、modport、时钟块clocking
SV学习(3)——接口interface、modport、时钟块clocking
========
========
1. 接口interface
SV引入了一个重要的数据类型:interface。主要作用有两个,一是简化模块之间的连接;二是实现类和模块之间的通信。
使用接口使得连接更加简洁而不易出差,如果需要在一个接口中放入一个新的信号,就只需要在接口定义和实际使用这个接口的模块中做对应的修改,而不需要改变其他模块。接口不可以例化,但是可以使用接口的指针,找到接口的实例,然后再找接口实例中的信号。
接口使用方法:
- 在interface的端口列表只需要定义时钟、复位等公共信号,或者不定义任何端口信号,转而在变量列表中定义各个需要跟DUT和TB连接的logic变量。
- interface可以用参数化方式提高复用性(parameter)
- 对于有对应interface的DUT和TB,在其例化的时候,也只需要传递匹配的interface变量名即可完成interface的变量传递
由于接口即可以在硬件上(module)使用,又可以在软件上(class)使用,interface作为SV中唯一的硬件和软件环境的媒介交互,modport可以进一步限定信号传输的方向,避免端口连接的错误。
接口中使用task和function:
- 接口可以包含task和function,也可以在接口外部或者内部
- 如果task和function定义在模块中,使用层次结构名称,它们必须在接口中声明为extern或在modport中声明为export
- 多个模块的任务名不可以重复
下面举两个接口的例子:
-
使用interface接口的一位全加器
`timescale 1ns / 1ns interface if_port (input bit clk); // 声明接口 logic a, b, cin, sum, cout; // 声明所有的连接线 clocking cp @ (posedge clk); // 声明在同一个时钟变化下,连接线的方向 output a, b, cin; endclocking clocking cn @ (negedge clk); // 下降沿出发 input a, b ,cin, sum, cout; endclocking modport simulus (clocking cp); // 声明端口的输入输出 modport adder (input a, b, cin, output cout, sum); modport monitor (clocking cn); endinterface module simulus (if_port.simulus port); // 使用接口的激励模块 always @ (port.cp) begin port.cp.a <= $random() % 2; port.cp.b <= $random() % 2; port.cp.cin <= $random() % 2; end endmodule: simulus module adder (if_port.adder port); // 一位加法器 // assign {port.cout, port.sum} = {port.a + port.b + port.cin}; // 好久没写代码,犯了这样的错 assign {port.cout, port.sum} = port.a + port.b + port.cin; endmodule: adder module monitor (if_port.monitor mon); // 检测模块,在时钟的下降沿打印结果 always @ (mon.cn) begin $display ("%d + %d + %d = %d %d", mon.cn.a, mon.cn.b, mon.cn.cin, mon.cn.cout, mon.cn.sum, $time); end endmodule module top ( ); timeunit 1ns; timeprecision 1ns; bit clk = 0;; if_port port (clk); // 实例化所有模块,并连接接口 simulus sim (port.simulus); adder add (port.adder); monitor mon (port.monitor); always #10 clk = ~clk; endmodule
-
使用interface接口的读存储器
// interface_example interface membus ( // 声明接口 input logic clk ); // port和module的port一样,用于top的 // 声明用于内部模块例化的连接 logic mrdy ; logic wen ; logic ren ; logic [ 7: 0] addr ; logic [ 7: 0] c2m_data; logic [ 7: 0] m2c_data; wor status ; // 对于确定了方向的可以在interface的port里面声明,如clk, // 而对于随着模块不同、方向不同的用logic先声明,然后再用modport指明具体方向 task reply_read ( input logic [ 7: 0] data, integer delay ); #delay; @ (negedge clk) mrdy = 1'b0; m2c_data = data; // slave回复数据 @ (negedge clk) mrdy = 1'b1; endtask task read_memory ( input logic [ 7: 0] raddr , output logic [ 7: 0] data ); @ (posedge clk); ren = 1'b0; addr = raddr; // master申请读数据 @ (negedge mrdy); @ (posedge clk); data = m2c_data; // master得到数据 ren = 1'b1; endtask // 在接口中使用modport,将信号分组并指定方向 modport master ( output wen, ren, addr, c2m_data, status, input mrdy, m2c_data, import read_memory ); modport slave ( input wen, ren, addr, c2m_data, status, output mrdy, m2c_data, import reply_read ); endinterface /* *************** mem_core *************** */ module mem_core (membus.slave mb); // 用salve接口声明 logic [ 7: 0] mem [ 255: 0]; initial begin for (int i = 0; i < 256; i++) mem[i] = i; end assign mb.status = 1'b0; always @ (negedge mb.ren) mb.reply_read (mem[mb.addr], 100); endmodule /* *************** cpu_core *************** */ module cpu_core (membus.master mb); // 用master接口声明 assign mb.status = 1'b0; initial begin logic [ 7: 0] read_data; mb.read_memory (8'b0001_0000, read_data); $display ("Read Result", $time, read_data); end endmodule /* *************** top *************** */ module top (); wor status; // wor:当有多个驱动源驱动wor型数据时,将产生线或结构 logic clk = 1'b0; membus mb (clk); // mem_core mem (mb.slave); mem_core mem (mb); cpu_core cpu (mb.master); initial begin for (int i = 0; i <= 255; i++) #1 clk = ~clk; end endmodule
关于interface的使用,大概知道了怎么用,接口中端口的调用没问题,比如在顶层top中实例化mem模块和cpu模块,这俩的端口连接可以用接口实现,在interface接口中,把端口的变量名声明好,再用modport将信号分组并指定输入输出方向;
但是具体的细节不太了解,比如在接口中定义了任务,如何在各自的模块中调用任务,看别人博客说是要添加export声明,但是我却用了import解决了,不懂,但是成了
2. modport
使用modport将接口中的信号分组
实现一个简单的仲裁器接口,并在仲裁器中使用接口,
// 简单接口
interface arb_if (input bit clk);
logic [1:0] grant, request;
logic rst;
endinterface
// 仲裁器
module arb (arb_if arbif);
...
always @(posedge arbif.clk or posedge arbif.rst) begin
if (arbif.rst == 1'b1)
arbif.grant <= 2'b00;
else
arbif.grant <= next_grant;
end
...
endmodule
// 测试平台
module test (arb_if arbif);
...
initial begin
...
@(posedge arbif.clk);
arbif.request <= 2'b01;
$display ("@%0t: Drove req = 01", $time);
repeat (2) @(posedge arbif.clk);
if (arbif.grant != 2'b01)
$display ("@%0t: a1: grant != 2'b01", $time);
$finish;
end
...
endmodule
// 顶层
module top;
bit clk;
always #5 clk = ~clk;
arb_if arbif(clk);
arb a1(arbif);
test t1(arbif);
endmodule
上面arb在接口中使用了点对点的无信号方向的连接方式;
下面在接口中使用modport结构能将信号分组并指定方向;
// 使用mofport的接口
interface arb_if (input bit clk);
logic [1:0] grant, request;
logic rst;
modport TEST (output request, rst,
input grant, clk);
modport DUT (input request, rst, clk,
output grant);
modport MONITOR (input request, grant, rst, clk);
endinterface
// 仲裁器
module arb (arb_if.DUT arbif);
...
endmodule
// 测试平台
module test (arb_if.TEST arbif);
...
endmodule
// 顶层
module top;
bit clk;
always #5 clk = ~clk;
arb_if arbif(clk);
arb a1(arbif);
test t1(arbif);
endmodule
两种顶层模块是一样的,因为modpoer在模块首部指明,在模块例化时就不需要指明;
在接口中不能例化模块,但是可以例化其他接口
3. 时钟块clocking
3.1. 驱动和采用的竞争问题
在RTL设计中,在同一个时间片内,可能会有一个信号同时被读取和写入,那么读取到的数据有可能会是旧数值也有可能是新数值,用非阻塞赋值 <= 就可以解决这个问题。但是在测试程序中,不能确保采集到DUT产生的最新值;
接口块可以使用时钟块来指定同步信号相对于时钟的时序。
在时钟上升沿采样信号,只看vld信号(1->0)就会疑惑采样到的是1还是0,在RTL仿真时,是看不到真实的物理时序信息的,但实际采样到的是1;真实电路对应的时vld_actual信号,它的变化会较clk有一个detal-cycle的延迟,这样看在时钟上升沿采集到的是1。
我们可以人为的设置加大delay,这样看波形的时候就会显示vld_actual的,让待采样数据的沿变不和时钟的沿变在一起。
还有一种方法就是,设置采样vld信号的时间,设置在时钟上升沿之前 #t 采样,这样采出来的数据也是准确的;
同样也可以设置信号输出时间,设置在时钟上升沿之后 #t 输出,这样输出的数据也是准确的;
就类似与模拟建立保存时间,在时序上在波形上,看到肉眼可见的延迟。