该方式则定义了三个interface: regs_cf_if、regs_ini_if和regs_rsp_if。该三个interface的功能划分更加明确:
- regs_cr_if:时钟和复位的接口,用来供给ctrl_regs和其它两个接口。
- regs_ini_if:ctrl_regs读写接口,也由于该接口模拟了集成以后外部控制器的作用,我们将其称之为initiator(发起端)接口,即该接口主动发起读写请求。
- regs_rsp_if:ctrl_regs配置和状态反馈接口,该接口时为了能够及时响应ctrl_regs在相应寄存器经过读写以后对所对应模块的功能配置或者状态返回。我们也将其称之为responsor(响应端)接口。
那么这两种方式从实现方式来看,方式2看起来需要做更多接口的定义,而且似乎将一件连接环境的事情变得更复杂了,是这样的吗?从实现的工作量来看,方式2确实会引入更多的工作,但从日后ctrl_regs的testbench在更高层级的集成复用来看,现在按照ctrl_regs边界信号进行有效的划分会对日后的工作提供更多的方便,这一点也会在以后的篇章关于《SV的系统集成篇》中得到体现。
那么,下面是关于上述三个interface的定义:
interface regs_cr_if;
logic clk;
logic rstn;
endinterface: regs_cr_if
interface regs_ini_if
#(parameter int addr_width = 8,
parameter int data_width = 32);
logic clk;
logic rstn;
logic [ 1:0] cmd;
logic [addr_width-1:0] cmd_addr;
logic [data_width-1:0] cmd_data_w;
logic [data_width-1:0] cmd_data_r;
endinterface: regs_ini_if
interface regs_rsp_if;
logic clk;
logic rstn;
logic [ 7:0] slv0_avail;
logic [ 7:0] slv1_avail;
logic [ 7:0] slv2_avail;
logic [ 2:0] slv0_len;
logic [ 2:0] slv1_len;
logic [ 2:0] slv2_len;
logic [ 1:0] slv0_prio;
logic [ 1:0] slv1_prio;
logic [ 1:0] slv2_prio;
logic slv0_en;
logic slv1_en;
logic slv2_en;
endinterface: regs_rsp_if
在上述interface定义中我们需要注意以下几点:
- interface可以定义input/output/inout端口,或者如果这些接口对不同连接模块的方向不相同时,可以将这些端口定义在interface实体中,这样的信号定义本身没有方向。
- 我们建议将interface上信号数据类型定义为logic,而非wire或者reg。这么做是因为SV的logic类型本身扩展了传统的reg类型,使得其也可以像wire进行连线。值得注意的一点是,唯一不能使用logic变量的地方是含有多驱动(multi-drive)的场景,这时候你必须使用连线类型,例如wire。
- 此外,我们需要再次强调,interface中的信号如果用来同DUT相连接,那么应该必须是四值逻辑即logic/reg/wire等,而不应该是二值逻辑bit/int/byte等。这么做的考虑是确保在今后的stimulator到DUT的驱动场景或者DUT到monitor的数据采集场景中,硬件部分的'X'或者'Z'信号不会被缺省转换以致丢失。
- interface也可以定义parameter来方便扩展复用。
在实现了interface的端口定义之后,我们进一步深入interface的其它应用。首先,我们提到了regs_cr_if的功能是提供时钟和复位信号的,这里我们可以在regs_cr_if中产生时钟和复位信号。
interface regs_cr_if;
logic clk;
logic rstn;
initial begin
clk <= 0;
forever begin
#5ns clk <= !clk;
end
end
initial begin
#20ns;
rstn <= 1;
#40ns;
rstn <= 0;
#40ns;
rstn <= 1;
end
endinterface: regs_cr_if
从上面的例子可以看到,interface也可以包含过程语句(always和initial)以及连续赋值语句,这对更高层级的建模和testbench应用都有好处。从上面的regs_cr_if中可以看到通过两个简单的initial过程语句可以在interface内部产生出时钟和复位信号。
接下来我们看三个interface与DUT的实际连接关系:
// interface 例化
regs_cr_if cr_if();
regs_ini_if ini_if();
regs_rsp_if rsp_if();
// 时钟和复位信号
assign ini_if.clk = cr_if.clk;
assign ini_if.rstn = cr_if.rstn;
assign rsp_if.clk = cr_if.clk;
assign rsp_if.rstn = cr_if.rstn;
// ctrl_regs例化及同interface连接
ctrl_regs regs(
.clk_i (cr_if.clk ),
.rstn_i (cr_if.rstn ),
.cmd_i (ini_if.cmd ),
.cmd_addr_i (ini_if.cmd_addr ),
.cmd_data_i (ini_if.cmd_data_w ),
.cmd_data_o (ini_if.cmd_data_r ),
.slv0_avail_i (rsp_if.slv0_avail ),
.slv1_avail_i (rsp_if.slv1_avail ),
.slv2_avail_i (rsp_if.slv2_avail ),
.slv0_len_o (rsp_if.slv0_len ),
.slv1_len_o (rsp_if.slv1_len ),
.slv2_len_o (rsp_if.slv2_len ),
.slv0_prio_o (rsp_if.slv0_prio ),
.slv1_prio_o (rsp_if.slv1_prio ),
.slv2_prio_o (rsp_if.slv2_prio ),
.slv0_en_o (rsp_if.slv0_en ),
.slv1_en_o (rsp_if.slv1_en ),
.slv2_en_o (rsp_if.slv2_en )
);
interface本身既需要同DUT连接, 也需要同stimulator和monitor连接,那么对于不同对象来讲,连接的信号方向也是不相同的。考虑到interface中声明了的信号自身没有方向,这就使得同一个信号可能会连错或者反向驱动的问题。
interface为了限制不同对象对其信号的访问权限和方向,通过modport来做进一步的声明来确立信号应该连接的方向。这里,我们以regs_ini_if为例:
interface regs_ini_if;
... // 省略了接口信号声明
modport dut(
input cmd, cmd_addr, cmd_data_w,
output cmd_data_r
);
modport stim(
input cmd_data_r,
output cmd, cmd_addr, cmd_data_w
);
modport mon(
input cmd, cmd_addr, cmd_data_w, cmd_data_r
);
endinterface: regs_ini_if
在regs_ini_if实体中,进一步定义了3个modport:dut,stim和mon。它们分别用来作为DUT,stimulator和mon的“插座”,这样一方面澄清了各个对象可以连接的interface信号,也通过限制的方向避免了反向驱动的问题。如果形象地来理解modport的作用,可以将interface作为一个插排,而各个modport作为针对不同对象的插槽,这使得针对更复杂的连线问题可以得到简化。
如果regs_ini_if通过modport来进一步约束信号的连接,那么上面的代码实例对于DUT的连接就转换为下面的部分:
ctrl_regs2 regs(
.clk_i (cr_if.clk ),
.rstn_i (cr_if.rstn ),
.cmd_i (ini_if.dut.cmd ),
.cmd_addr_i (ini_if.dut.cmd_addr ),
.cmd_data_i (ini_if.dut.cmd_data_w ),
.cmd_data_o (ini_if.dut.cmd_data_r ),
.slv0_avail_i (rsp_if.dut.slv0_avail ),
.slv1_avail_i (rsp_if.dut.slv1_avail ),
.slv2_avail_i (rsp_if.dut.slv2_avail ),
.slv0_len_o (rsp_if.dut.slv0_len ),
.slv1_len_o (rsp_if.dut.slv1_len ),
.slv2_len_o (rsp_if.dut.slv2_len ),
.slv0_prio_o (rsp_if.dut.slv0_prio ),
.slv1_prio_o (rsp_if.dut.slv1_prio ),
.slv2_prio_o (rsp_if.dut.slv2_prio ),
.slv0_en_o (rsp_if.dut.slv0_en ),
.slv1_en_o (rsp_if.dut.slv1_en ),
.slv2_en_o (rsp_if.dut.slv2_en )
);
可以看到,除了时钟和复位信号意外,其它信号与ini_if和rsp_if内部信号连接时需要再进一步通过modport dut来连接。与之前的点对点连接方式比较,modport可以使所有相关的信号集中在同一个地方描述,减少出错的概率,但也需要额外地对这些不同的信号进行分类集合。
在介绍完interface主要的使用情况以后,我们下一节将介绍testbench的“外包装”——program(程序块),来看一看program与module的比较,以及它们之间可能存在的竞争问题。
谢谢你对路科验证的关注,也欢迎你分享和转发真正的技术价值,你的支持是我们保持前行的动力。