在展开验证环境的构建之前,我们需要先了解模块的端口定义以及在SV环境下得例化。在这里, 我们以MCDF(multi-channel data formatter)中的寄存器模块ctrl_regs为例,来看看常见的模块定义方式有哪些。
模块定义
Verilog 模块定义1
module ctrl_regs1(clk_i,rstn_i,
cmd_i,cmd_addr_i,cmd_data_i,cmd_data_o,
slv0_len_o,slv1_len_o,slv2_len_o,
slv0_prio_o,slv1_prio_o,slv2_prio_o,
slv0_avail_i,slv1_avail_i,slv2_avail_i,
slv0_en_i,slv1_en_i,slv2_en_i);
input clk_i,rstn_i;
input [1:0] cmd_i;
input [7:0]cmd_addr_i;
input [31:0] cmd_data_i;
output [31:0] cmd_data_o;
input [7:0] slv0_avail_i,slv1_avail_i,slv2_avail_i;
output [2:0] slv0_len_o,slv1_len_o,slv2_len_o;
output [1:0] slv0_prio_o,slv1_prio_o,slv2_prio_o;
output slv0_en_o,slv1_en_o,slv2_en_o;
endmodule
Verilog 模块定义2
module ctrl_regs2(
input clk_i,
input rstn_i,
input [1:0] cmd_i,
input [7:0]cmd_addr_i,
input [31:0] cmd_data_i,
output [31:0] cmd_data_o,
input [7:0] slv0_avail_i,
input [7:0] slv1_avail_i,
input [7:0] slv2_avail_i,
output [2:0] slv0_len_o,
output [2:0] slv1_len_o,
output [2:0] slv2_len_o,
output [1:0] slv0_prio_o,
output [1:0] slv1_prio_o,
output [1:0] slv2_prio_o,
output slv0_en_o,
output slv1_en_o,
output slv2_en_o
);
endmodule
上面的两种定义方式是Verilog设计常见的做法,区别在于端口的方向可以在端口声明时定义,或者在端口声明完毕后再定义。我们再来看一看,如果用VHDL来定义ctrl_regs的接口会怎么来定义。
VHDL 模块定义1
entity ctrl_regs3 is
port(
clk_i : in std_logic;
rstn_i : in std_logic;
cmd_i : in std_logic_vector(1 downto 0);
cmd_addr_i : in std_logic_vector(7 downto 0);
cmd_data_i : in std_logic_vector(31 downto 0);
cmd_data_o : out std_logic_vector(31 downto 0);
slv0_avail_i : in std_logic_vector(7 downto 0);
slv1_avail_i : in std_logic_vector(7 downto 0);
slv2_avail_i : in std_logic_vector(7 downto 0);
slv0_len_o : out std_logic_vector(2 downto 0);
slv1_len_o : out std_logic_vector(2 downto 0);
slv2_len_o : out std_logic_vector(2 downto 0);
slv0_prio_o : out std_logic_vector(1 downto 0);
slv1_prio_o : out std_logic_vector(1 downto 0);
slv2_prio_o : out std_logic_vector(1 downto 0);
slv0_en_o : out std_logic;
slv1_en_o : out std_logic;
slv2_en_o : out std_logic
);
end ctrl_regs3;
VHDL 模块定义2
package mcdf_pkg is
type reg2arb_t is record
slv0_prio : std_logic_vector(1 downto 0);
slv1_prio : std_logic_vector(1 downto 0);
slv2_prio : std_logic_vector(1 downto 0);
end record;
type reg2fmt_t is record
slv0_len : std_logic_vector(2 downto 0);
slv1_len : std_logic_vector(2 downto 0);
slv2_len : std_logic_vector(2 downto 0);
end record;
end mcdf_pkg;
entity ctrl_regs4 is
port(
clk_i : in std_logic;
rstn_i : in std_logic;
cmd_i : in std_logic_vector(1 downto 0);
cmd_addr_i : in std_logic_vector(7 downto 0);
cmd_data_i : in std_logic_vector(31 downto 0);
cmd_data_o : out std_logic_vector(31 downto 0);
slv0_avail_i : in std_logic_vector(7 downto 0);
slv1_avail_i : in std_logic_vector(7 downto 0);
slv2_avail_i : in std_logic_vector(7 downto 0);
reg2fmt_o : out reg2fmt_t;
reg2arb_o : out reg2arb_t;
slv0_en_o : out std_logic;
slv1_en_o : out std_logic;
slv2_en_o : out std_logic
);
end ctrl_regs4;
从上面两种VHDL端口定义的方式来看,第一种VHDL定义方式与之前的Verilog定义方式一致,而第二种定义方式需要作特别说明。
由于VHDL的数据类型中,有record类型,该类型作为硬件定义初衷是为了做硬件信号集束(signal collection)的。例如,上面首先定义了一个包mcdf_pkg,而在其中定义了两种数据类型:reg2fmt_t和reg2arb_t。随后,在ctrl_regs端口定义时,使用了这两种数据端口类型,进而使得ctrl_regs模块送给formatter模块的信号被集束在一个新定义的数据类型中。
对于稍后我们会提到的模块例化,如果MCDF的各个模块均为VHDL定义描述的,那么对于接口类型是record定义的模块在于其它相邻模块连接时,可以通过相同的record类型用来做信号对接,也可以通过信号分散赋值的形式进行连接。
我们需要额外注意的是,如果遇到了Verilog模块与VHDL模块的连接,或者Verilog模块中例化VHDL的时候,需要对VHDL record类型进行特别处理。
所以,我们接下来进入模块的例化部分。这里,我们将ctrl_regs的testbench称之为ctrl_regs_tb,首先我们需要对ctrl_regs进行例化。
模块例化
对Verilog ctrl_reg2的例化:
logic clk_s;
logic rstn_s;
logic [ 1:0] cmd_s;
logic [ 7:0] cmd_addr_is;
logic [31:0] cmd_data_i_s;
logic [31:0] cmd_data_o_s;
logic [ 7:0] slv0_avail_s;
logic [ 7:0] slv1_avail_s;
logic [ 7:0] slv2_avail_s;
logic [ 2:0] slv0_len_s;
logic [ 2:0] slv1_len_s;
logic [ 2:0] slv2_len_s;
logic [ 1:0] slv0_prio_s;
logic [ 1:0] slv1_prio_s;
logic [ 1:0] slv2_prio_s;
logic slv0_en_s;
logic slv1_en_s;
logic slv2_en_s;
ctrl_regs2 regs2_inst(
.clk_i (clk_s ),
.rstn_i (rstn_s ),
.cmd_i (cmd_s ),
.cmd_addr_i (cmd_addr_is ),
.cmd_data_i (cmd_data_i_s),
.cmd_data_o (cmd_data_o_s),
.slv0_avail_i (slv0_avail_s),
.slv1_avail_i (slv1_avail_s),
.slv2_avail_i (slv2_avail_s),
.slv0_len_o (slv0_len_s ),
.slv1_len_o (slv1_len_s ),
.slv2_len_o (slv2_len_s ),
.slv0_prio_o (slv0_prio_s ),
.slv1_prio_o (slv1_prio_s ),
.slv2_prio_o (slv2_prio_s ),
.slv0_en_o (slv0_en_s ),
.slv1_en_o (slv1_en_s ),
.slv2_en_o (slv2_en_s )
);
对于VHDL的 ctrl_reg3的例化也同上面对ctrl_regs2的例化,而需要注意的是,如果SV或者Verilog作为顶层,来例化含有record类型接口的时候,我们建议通用的方法是,验证人员需要首先新建立一个VHDL wrapper来作为一个盒子用来将ctrl_regs4的两种record类型接口reg2fmt_o和reg2arb_o进一步转化为通用的std_logic_vector类型,即ctrl_reg3的接口形式。否则,如果在SV或者Verilog内部直接例化含有record类型接口,经常会有接口类型无法匹配的编译问题。
对于VHDL record接口类型在Verilog中例化的问题,一些工具厂商如MentorGraphics的仿真工具QuestaSim提出了Verilog/VHDL/SV数据定义包混用的编译选项-mixedsvvh,用来做工具一侧的支持,而有些工具商则没有提出类似方案。所以就测试平台的移植性来看,我们依然建议通过首先改造VHDL record接口,再者进行验证平台内DUT实例化的方式来进行。
参数使用
在IP设计中,经常会遇到参数化的模块。例如,我们可以将ctrl_regs模块的地址宽度和数据宽度参数化,从而得到这样的端口定义:
参数化端口定义:
module ctrl_regs5
#(parameter int addr_width = 8,
parameter int data_width = 32)
(
...
input [addr_width-1:0]cmd_addr_i,
input [data_width-1:0] cmd_data_i,
output [data_width:0] cmd_data_o,
...
);
endmodule
对此,我们可以在模块例化时再决定端口的宽度,例如:
ctrl_regs5 #(.addr_width(16)) regs5_inst( ... );
上面的例子在模块例化时,将地址宽度addr_width修改为16,而保持了数据宽度data_width的默认值32。
参数修改
对设计和验证环境引入参数的优点在于,通过参数可以更方便地调整结构和数据类型,而不需要修改对象内部的定义。
除了我们上面提到的模块参数可以在例化时修改以外,我们还可以在什么时候对参数加以修改呢?在讨论之前,我们首先需要阐述关于目前主流HDL仿真器编译仿真代码的过程。通过对比不同仿真器(simulator),我们可以统一将代码编译运行的过程分为三个部分:
- 编译阶段(compilation):工具通过阅读目标代码,进行语法和语义分析分析,将每个模块分别编入库中(library)。
- 建模阶段(elaboration):工具将各个模块按照设计集成关系最终组成顶层模块。这一过程包括了各个模块(module)的例化、接口(interface)例化、程序(program)例化、层次集成、计算参数、解决层次信号引用、建立模块连接等。这一过程发生在了编译阶段之后,仿真阶段之前,类似于软件编译的link阶段。
- 仿真阶段(simulation):通过读取建模阶段的对象文件,建立硬件RTL模型和验证环境,通过周期驱动(cycle-driven)或者事件驱动(event-driven)的方式来进行仿真。
所以从数据修改的手段上来看,参数修改可以在编译阶段修改即通过模块例化时的参数传入的方式进行,也可以在建模阶段通过工具提供的参数修改的elaboration命令选项来修改。参数无法在仿真阶段进行修改。
不同仿真器对于这三个阶段的执行方式是不相同的,例如QuestaSim会首先执行compilation将各个模块编译到库中,而接下来的仿真阶段实际上会首先进行内置的elaboration环节,所以参数可以在仿真阶段通过命令行修改。而VCS则将上述三个阶段独立开来,使得compilation与elaboration可以通过仿真前的命令行单独执行,而simualtion阶段则直接运行已经建立好的模型。
事先清楚不同仿真器对于上述阶段的处理,就明白了针对不同的仿真器何时可以修改参数。QuestaSim需要在仿真阶段修改,而VCS需要在独立的elaboration阶段修改。
宏定义
除了parameter的方式,我们还可以通过宏定义的方式进行参数化设计。例如上面的例子ctrl_regs5可以修改为宏定义的方式:
`define ADDR_WIDTH 6
`define DATA_WIDTH 32
module ctrl_regs6
(
...
input [`ADDR_WIDTH-1:0] cmd_addr_i,
input [`DATA_WIDTH-1:0] cmd_data_i,
output [`DATA_WIDTH:0] cmd_data_o,
...
);
endmodule
针对宏定义的形式,用户如果要修改上面端口的宽度,必须在compilation阶段完成。这使得通常,针对宏定义的形式,我们会将公共使用的宏存放在公共的空间作为头文件(header file),在编译之前通过修改宏的定义,或者调用不同的头文件来决定端口宽度,或者别的设计和环境参数。
通过认识如何进行模块的例化,我们已经可以在首先将DUT置入到测试平台中,下一节课我们将对SV的重要特性——接口(interface)进行讨论,从而掌握如何通过接口让硬件部分DUT与随后的验证环境相连接。
谢谢你对路科验证的关注,也欢迎你分享和转发真正的技术价值,你的支持是我们保持前行的动力。