数字IC设计工程师不应该仅仅是个设计工程师,而是既要能验证更要会设计。此处的验证是专属于设计工程师的验证,不是有了此步的验证就不需要验证工程师了。此处的验证是设计工程师对工作负责的体现,也是对团队其他队友的尊重,更是对自己的检验。此处的验证是指设计工程师在自己设计完成后,需要对自己设计模块主要功能的检验,只有模块主要功能检验通过了才能交给验证工程师,由他们发现可能隐藏的问题。
对自己设计的模块要进行初步的验证,能看懂验证工程师的验证环境是对设计工程师更高的要求。而现在很多公司的验证环境都是UVM的验证环境,也就是设计工程师也需要能看明白UVM的验证环境。
在这里,先浅薄的记录下设计工程师的testbench.v的有关笔记,然后再简单记录下作为设计工程师怎样去看UVM的验证环境更方便快速。当然,深知这点东西太少了,后续会继续学习前进。
1 设计工程师的testbench
设计工程师在设计好独立的模块后需要简单打一个验证环境来验证该模块能达到主要的需求,在这个过程中这些系统任务或者系统函数task,readmemh, readmemb, display, fprintf, while, wait, dump fsdb, ifdefine, ifndefine。
1.1 生成波形文件
在testbench.v里需要有生成波形文件的部分,这样才能在打开verdi时查看波形。常用的波形文件有.fsdb文件和.vcd文件,而这里使用的是.fsdb文件,因此需要在testbench.v文件中加入生成.fsdb文件的语句。常使用的是fsdbDumpfile()函数和fsdbDumpvars()函数,使用举例如下所示:
verilog
module testbench;
reg clk;
reg [7:0] data;
initial begin
clk = 0;
data = 8'h00;
// 指定波形文件名称
$fsdbDumpfile("wave.fsdb");
// 选择需要记录的信号,
//0表示记录所有层次的信号,1表示仅记录当前模块顶层信号
//testbench指定需要记录信号的模块名
$fsdbDumpvars(0, testbench);
// 仿真运行
#100 $finish;
end
always #5 clk = ~clk;
always @(posedge clk) data <= data + 1;
endmodule
关于这两个函数的使用可以参考以下链接:verilog的dumpfile和dumpvar系统任务详解_weixin_30415113的博客-CSDN博客
https://blog.csdn.net/weixin_30415113/article/details/98062528
注1:testbench可以尝试换成例化的module名,如果在顶层下,需要按照hierarchy来引用。
注2:该链接中讲的是生成.vcd文件的两个函数的用法,但在生成波形方面,fsdbDumpfile()函数和dumpfile()函数功能一样,可参考链接中的使用方法;fsdbDumpvars()函数和dumpvar()函数功能一样,可以参考连接中的使用方法。
注3:dump波形还可以指定特定信号或者触发条件的波形。
1.2 使用task(任务)函数
使用task会很方便,减少代码量,增加debug的效率。当然task是针对重复执行的功能而建立才能达到减少代码量,增加debug的效率。
task的基本语法如下:
verilog
task <task_name>;
input [<width>] <input_port_name>;
output [<width>] <output_port_name>;
inout [<width>] <inout_port_name>;
begin
// 任务主体代码
end
endtask
task调用的基本语法如下:
verilog
<task_name> (<input_arguments>, <output_arguments>, <inout_arguments>);
task的使用举例如下:
verilog
task add_numbers;
input [7:0] a, b;
output [7:0] sum;
begin
sum = a + b;
end
endtask
// 调用任务
reg [7:0] result;
initial begin
add_numbers(8'h10, 8'h20, result);
$display("Sum is %h", result);
end
需要注意task与function的区别。
1.3 readmemh, readmemb
这两个系统函数在需要load大量数据到存储器中,或者仿真需要输入大量数据时非常有用,其使用方法如下:
verilog
$readmemh("filename", memory_array, start_addr, end_addr);
这个是读取十六进制的数据到memory_array中,这里的filename是需要包含数据文件的绝对地址,目的是告诉工具使用的具体数据文件。start_addr和end_addr都是可选,用于指定读取数据的起止地址。数据文件有一定的格式,每行一个十六进制数据。memory_array定义为reg,需要指明位宽和深度,如下所示:
verilog
reg [7:0] mem [0:3];
initial begin
$readmemh("data_part.hex", mem, 1, 2);
end
readmemb和readmemh的使用方法一致,读取的数据文件为二进制文件。
1.4 display, fprintf
在仿真过程中需要打印一些指示信息,也可能将数据打印输出到文件进行数据分析,这两个系统函数就会被用到。
$display多用于打印提示信息,其基本语法如下:
verilog
$display(format_string, arg1, arg2, ...);
format_string:格式化字符串,支持类似 C 语言的格式符(如%d、%h、%b等)。arg1, arg2, ...:可选参数,与格式符一一对应。
使用举例如下:
verilog
reg [7:0] data = 8'hA5;
$display("Decimal: %d, Hex: %h, Binary: %b", data, data, data);
运行代码结果如下:
nix
Decimal: 165, Hex: a5, Binary: 10100101
$display会自动换行。
需要将仿真数据打印输出到文件可以用$fprint系统函数,其基本语法如下所示。
verilog
$fprint(file_descriptor, format_string, arguments...);
file_descriptor:通过$fopen打开文件后返回的文件描述符。注:fopen打开文件对应着fclose关闭文件,这需要配合使用。format_string:格式化字符串,类似于 C 语言的printf格式。arguments:需要写入的变量或表达式。
使用举例如下:
verilog
integer fd;
initial begin
fd = $fopen("output.txt", "w"); // "w" 表示写入模式
if (!fd) $display("文件打开失败");
else begin
$fprint(fd, "数据写入测试: %d\n", 42);
$fclose(fd);
end
end
1.5 while
当在某些条件成立时需要一直执行某些指令时需要用到while,当某些条件达到时需要退出while循环。
其基础语法如下:
verilog
while (condition) begin
// 循环体语句
end
在验证环境中可用于生成某些测试激励,也可以用于条件判断。
在Verilog中不存在break跳出循环,但在systemVerilog中可以用break跳出循环。
1.6 wait
在验证时可能需要在某个条件成立时执行某些操作,它时一种电平敏感的延迟控制机制,和基于事件的@触发机制不同,需要区分开。其基本语法如下所示:
verilog
wait (condition) statement;
wait和@的区别如下:
verilog
wait (enable == 1); // 等待enable变为高电平
@(posedge clk); // 等待clk的上升沿
1.7 ifdef, ifndef
在一个testbench里面可能需要验证多个功能,而每个功能是分开验证的,这时使用宏定义就十分有用,在需要验证的功能下打开某些代码模块,否则就关闭。其使用举例如下:
verilog
`define MODE_SIMULATION
module test;
`ifdef MODE_SIMULATION
initial $display("Simulation mode enabled");
`else
initial $display("Synthesis mode enabled");
`endif
endmodule
1.8 testbench.v示例
verilog
`timescale 10ps/10ns
module testbench;
reg clk; //例化顶层输入端口定义为reg型
reg rst_n;
wire [7:0] data_out;//例化顶层输出端口定义为wire型
reg [31:0] mem_data [0:255];
integer log_file;
initial begin
$fsdbDumpfile("wave.fsdb");
$fsdbDumpvars(0, testbench);
log_file = $fopen("sim_log.txt", "w");
$fdisplay(log_file, "Simulation started at %t", $time);
end
task initialize;
input [7:0] init_value;
begin
$display("Initializing with value %h", init_value);
data_in = init_value;
#10;
end
endtask
task read_memory;
$readmemh("memory_init.hex", mem_data);
$display("Memory initialized from file");
endtask
initial begin
clk = 0;
rst_n = 0;
read_memory();
initialize(8'hFF);
#20 rst_n = 1;
$fprintf(log_file, "Reset released at %t\n", $time);
while ($time < 1000) begin
@(posedge clk);
$display("Cycle %d: data_out = %h", $time/10, data_out);
$fprintf(log_file, "Cycle %d: %h\n", $time/10, data_out);
end
$fclose(log_file);
$finish;
end
always #5 clk = ~clk;
wait_for_data: wait (data_out == 8'hAA)
$display("Target data pattern detected at %t", $time);
dut u_dut(
.clk(clk),
.rst_n(rst_n),
.data_out(data_out)
);
endmodule
这是针对IC设计工程师可以用的简单的,用verilog语言写成的testbench.v例子,但在实际工程项目中遇到的模块会很复杂,需要较多的处理机制。因此当一个IC设计工程师渐渐的从小模块到大模块时,需要能处理更复杂的情况,即推荐进一步学习systemVerilog语言,了解学习UVM等更深的工作技能。
SystemVerilog: 打印显示之数据格式控制及特殊字符详解_systemverilog display-CSDN博客
7.2 Verilog 文件操作 | 菜鸟教程 (runoob.com)
2 数字设计工程师的UVM入手
这里讲通过makefile来执行编译仿真。那么作为一个设计工程师,可以从makefile文件入手,通过makefile文件找到编译仿真的执行命令。一般会有一个compile文件用将我们的代码(设计的代码和验证环境的代码)编译成可执行文件生成.simv的仿真文件。
|----------|---------|------|
| makefile | compile | simv |
通过makefile文件找到相应make执行命令下的编译文件和仿真文件。一般编译文件命名中会包含compile关键词,编译文件是将验证环境的文件,芯片功能实现相关的RTL设计文件和相应的编译命令。执行该编译文件会产生相应的仿真文件simv,可以给仿真文件传递相关仿真功能的配置参数,因此同一个仿真文件可以对应不同的仿真功能case,这个通过UVM_TESTNAME这个关键词来实现,其用法如下所示:
bash
simv +UVM_TESTNAME=my_test +UVM_VERBOSITY=UVM_HIGH
下面给出一个简单的例子,从compile文件到仿真simv。
compile.csh的主要内容:
set comp_opt = "+define+PROJ_ENV"
vcs -full64 -sverilog +v2k -kdb -lca -debug_access+all -debug_region=lib+cell -p $VERDI_HOME/share/PLI/VCS/linux64/novas.tab $VERDI_HOME/share/PLI/VCS/linux64/pli.a -override_timescale=1ns/1ps +vcs+lic+wait +libext+.sv+.v +incdir+$VCS_HOME/etc/uvm-1.2/src $VCS_HOME/etc/uvm-1.2/src/uvm_pkg.sv $VCS_HOME/etc/uvm-1.2/src/dpi/uvm_dpi.cc +incdir+$PROJ_PATH/reg/env $PROJ_PATH/regr/env/project_uvc_pkg.svh $PROJ_PATH/regr/tb/project_regr_tb_top.v -f $PROJ_PATH/rsrc/filelists/project_top_flist.f -cm line+cond_fsm+tgl+branch ${comp_opt} +define_VIRAGE_IGNORE_RESET +define_VIRAGE_FAST_VERILOG +define+SIM_NO_UPF +notimingcheck +nospecify -l vcs_compile.log -CFLAGS -DVCS +define+SPI_CYC=50 -top project_regr_tb_top
compile.csh里面包含了验证testbench------project_regr_tb_top.v,也包含了需求功能相关的RTL设计代码------project_top_flist.f,还包含了UVM环境相关的文件------uvm_pkg.sv/uvm_dpi.cc,其中在project_regr_tb_top.v中也包含了uvm环境相关的文件。
编译后生成simv文件,通过给仿真文件传递配置参数实现不通过能的验证共用同一个编译文件。
./simv +opt1=$opt_1 +opt2=$opt_2 +UVM_TESTNAME=project_case_1 -l project_case_1.log +runtime=400 +disable_monitor=$disable_monitor +ENABLE_DUMP_FSDB
在uvm环境中,特别需要关注的文件有test.svh和sequence.svh这两个文件。其中test.svh文件中找和UVM_TESTNAME相同的class,这个class定义了该test case先后执行的操作名字。在test.svh这个文件中找到test case先后操作的名字后,可以在sequence.svh这个文件中找到相应操作执行的命令------寄存器配置,等待指示等。
下面是test.svh的举例。
class project_case_1 extends project_base_test;
`uvm_component_utils(project_case_1)
.
.//变量的声明
.
//class的定义
project_receive_data_sequence read_date_req;
.
function .....
.
.
.
endfunction
task main_phase(uvm_phase phase);
.
.
fork begin
.
.
.
.
end
begin
.
.
.
.
end
join
.
.
.
endtask
.
.
endclass: project_case_1
下面是sequence.svh文件的简单举例。
class project_receive_data_sequence extends project_base_sequenc;
`uvm_object_utils(project_receive_date_sequence)
//变量声明
.
.
.
function
.
.
.
endfunction
virtual task body();
.
.
.
.
.
.
endtask
endclass: project_receive_data_sequence
关于UVM中phase的理解可参考如下链接:
芯片验证 | UVM的phase机制_uvm phase-CSDN博客
关于fork-join,fork-join_any,fork-join_none区别的动态图可参考如下链接:
systemverilog在for循环中使用fork_join和fork_join_none的区别 - 知乎 (zhihu.com)
关于fork-join,fork-join_any,fork-join_none区别的比较好的链接如下:
SV中,fork-join,fork-join_any、fork-join_none的理解_fork join any-CSDN博客
SystemVerilog中fork join 和 for循环嵌套问题探讨 - IVY_Liu - 博客园 (cnblogs.com)
为自己的学习记录到这里吧,写得不好不全,还望各大神多点鼓励......谢谢。