Testbench编写与Vivado Simulator的基本操作
Testbench编写
Testbench 是一种用Verilog或者systemVerilog语言编写的程序或模块,编写testbench的主要目的是为了对使用硬件描述语言(HDL)设计的电路UUT(unit under test)进行仿真验证,测试设计电路的功能、部分性能是否与预期的目标相符。testbench进行测试的过程如下: 1) 产生模拟激励(波形); 2) 将产生的激励加入到被测试模块并观察其输出响应; 3) 将输出响应与期望进行比较,从而判断设计的正确性。
Testbench 的主要组件如下:
- 时间表声明:指定所有延迟的时间单位,`timescale 1ns / 10ps,时间单位为1ns,精度为10ps;
- Module:它定义了测试文件的top模块,测试文件的top模块通常没有输入输出端口,测试是直接监控寄存器和线网这些内部信号的活动;
- 内部信号:它将驱动激励信号进入 UUT 并监控 UUT 的响应,信号驱动和监控
- UUT 实例化;
- initial语句激励生成:编写语句以创建激励波形和程序块;
- 系统输入输出,自我测试语句,能报告数值,错误和警告,如readmemb,fwrite等系统函数;
此处推荐两个好用的VSCODE插件,Verilog-HDL/SystemVerilog/Bluespec SystemVerilog 与TerosHDL,能够便捷的例化模块与编写testbench。文章最后会附带一个简易的sfc_slave.v的testbench,以帮助大家能够快速掌握tesbench编写。
当完成Testbench编写后,我们需要对UUT进行仿真验证与代码debug,对于FPGAer来说,常见的Verilog仿真器有两家,QuestaSim(前身ModelSim,其母公司Mentor Graphics被Siemens EDA收购),第二就是Vivado开发软件自带的 Simulator,Vivado Simulator是一款硬件描述语言事件驱动的仿真器,支持功能仿真和时序仿真,支持VHDL、Verilog、SystemVerilog和混合语言仿真。此外Vivado也支持调用其他的工具进行仿真,如图所示。
Vivado Simulator
我们以Vivado Simulator为例,工程创建好后,可运行行为级仿真(behavioral simulation),在成功地综合和实现之后,可以运行功能仿真(functional simulation)和时序仿真(timing simulation)。功能仿真中,在Flow Navigator中点击Run Simulation,弹出菜单中选择behavioral simulation,会出现simulator的仿真界面。在讲述具体的仿真操作前,浅要说一下功能仿真与时序仿真的差别。
综合后功能仿真 :综合后通用的逻辑转换为器件相关的原语,综合后会生成一个功能网表,后仿可以确保综合优化不会影响到设计的功能性,此时使用UNISIM库。注(UNISIM库是Xilinx提供的一个非常重要的仿真库,它包含了Xilinx FPGA器件中的原语(Primitives)和宏原语(Macro Primitives)的模型。这些模型用于在仿真时模拟FPGA器件的行为,确保设计的功能验证可以在仿真环境中准确地进行);实现后功能仿真:实现后设计已经在硬件中完成布局和布线工作,实现后功能仿真可以确保物理布局布线优化不会影响到设计的功能正确性。
综合/ 实现后时序仿真:该仿真使用器件模型中估算的时间延迟,但不包括内部连线延迟。通用的逻辑综合后,可以估算的布线和组件间延迟。使用此仿真可以查看潜在的时序严苛路径,如异步路径时序错误。
最上面的工具栏中显示了控制仿真过程的常用功能按钮:
Restart:从0时刻开始重新运行仿真;
Run All:运行仿真一直到处理完所有event或遇到指令指示停止仿真 ;
Run For:按照设定的时间运行仿真,每点击一次都运行指定时长;
Step:运行仿真直到下一个HDL状态;
Break:暂停仿真运行;
Relaunch Simulation:重新编译仿真源文件且restart仿真,修改源代码后只需要Relaunch即可,不必关闭仿真再重新打开运行。
中间为Scope与objects窗口,Vivado Simulator中将一个完整设计中的testbench层次划分称作一个scope。在Scope窗口中可以看到设计结构的top-down关系,选中一个scope后,该scope中所有的对象(reg,wire,parameter,localparam,interger)都会显示在Object窗口中。可以选择将Object窗口中的对象添加到波形窗口中(add to wave window),这样便可以观察到设计中的内部信号,具体常用的功能描述如下。
Add to Wave Window:将所有状态为可见的verilog对象添加到波形窗口;
Go to Source Code:打开定义选中scope的源码;
Go to Instantiation Source Code:打开实例化选中实例的源代码;
Log to Wave Database:可以选中记录当前scope的对象。相关数据会存储在project_name.sim/sim_1/behav目录下的wdb文件中。
Radix:设置Objects窗口中选定对象的值的显示数字格式,包括默认、2进制(Binary)、16进制(Hexadecimal)、8进制(Octal)、ASCII码、无符号10进制(Unsigned Decimal)、带符号10进制(Signed Decimal)和符号量值(Signed Magnitude)。
右侧绿色波形为具体仿真的波形Wave窗口,当运行仿真后,会自动打开一个波形窗口,默认显示仿真顶层模块中的Verilog对象的波形配置。窗口中的HDL对象和分组情况称作一个波形配置,可以将当前配置保存为wcfg文件,下次运行仿真时就不需要重新添加仿真对象或分组。窗口中还有游标(小红框中的即为游标marker)、记号、时间尺等功能帮助设计者测量时间。wave窗口也同样有一些常用功能。
go to source code :跳转到被选择信号的定义处;
Show in object Window:在Object窗口中高亮选定的对象;
Find/Find Value:搜寻某一对象/搜索对象中的某值;
Ungroup:拆分group或虚拟总线(virtual bus);
Rename/Name:前者设置用户自定义的对象显示名称,后者选中名称的显示方式:long(显示所处层次结构)、short(仅显示信号名称)、custom(Rename设置的名称);
Waveform Style:设置波形显示为数字方式或模拟方式;
Signal Color:设置波形的显示颜色;
Divider Color:设置隔离带的颜色;
Reverse Bit Order:将选定对象的数值bit显示顺序反转;
New Virtual Bus:将选定对象的bit组合为一个新的逻辑向量;
New Group:将选定对象添加到一个group中,可以像文件夹一样排列;
New Divider:在波形窗口中添加一个隔离带,将信号分开,便于观察。
除此之外,Vivado不得不提的还有提供了一些便捷特性帮助设计者分析波形中的数据:拆分(divider)、群组(group)、虚拟总线(virtual buses)。这些对象都是为了提高观察波形的便捷性,帮助设计观察仿真结果。虚拟总线是将多个信号组合为一个总线显示。
Divider:divider用来隔离不同的Verilog对象,点击信号的右键菜单中->New Divider,会在其下方创建一条隔离带,delete即可删除。
Group:一个Group相当于一个容器,将相关的波形对象组合在一个文件夹中。选中想要添加的对象,右键->New Group即可建立一个新组。注意选中group后delete会删除掉该组和内部所有的对象,如果只是想解散组,使用右键菜单中的Ungroup。
Virtual Bus:可以将多个标量或向量组合在一起作为虚拟总线显示,按顺序选中要添加的对象,右键->New Virtual Bus。同样delete会删除掉虚拟总线所有对象。
使用IP核自带Testbench进行仿真
最简单学会使用Vivado Simulator的方法在于实践。Vivado中IP Catalog内的部分IP核都提供了一个TestBench,如dds_compoler, fir_compiler等可用于单独仿真该IP核。在设计中可以使用这个TestBench来仿真测试IP核的功能是否正确。在产生IP核的输出文件时,可以看到该IP核是否包含TestBench,查阅该文件可以学习不少TestBench的设计编写方法。
在Sources窗口的Hierarchy标签下,在Simulation Sources文件夹中打开IP核的层次结构(点击前面的小箭头,或右键->IP Hierarchy->Show IP Hierarchy),TestBench文件名格式为tb_ipname。将该Testbench设置为顶层仿真模块,右键->Set as Top:
cpp
//------------------------------------------------------------------------------
//! @copyright
//! @title SFC_WRAPPER_TB
//! @file tb\spi_master_slave_sim\spi_sim_rdid\sfc_wrapper_tb.sv
//! @date 07/09, 2024
//! @version V0.0.1
//! @brief sfc_wrapper_tb module
//! @details spi standard slave read flash ID
//------------------------------------------------------------------------------
`timescale 1ns / 1ns
module sfc_wrapper_tb;
// Parameters
//signals
logic clk_sys;
logic rst_sys;
logic spi_cs;
logic [1:0] spi_qmode;
logic [7:0] spi_dummy;
logic spi_clk;
logic spi_so;
logic spi_si;
logic [7:0] command;
logic [7:0] command_shiter;
//instantiation
sfc_wrapper # (
.SPI_MODE(3),
.DUMMY(0)
)
sfc_wrapper_inst (
.sys_rst(rst_sys),
.spi_cs(spi_cs),
.spi_clk(spi_clk),
.spi_so(spi_so),
.spi_si(spi_si)
);
//reset and clock
always #5 spi_clk = ! spi_clk ;
initial begin
force spi_qmode = 2'b11;
force spi_dummy = 8'd0;
force command = 8'h9f;
spi_clk <= 1'b0;
rst_sys <= 1'b1;
spi_cs <= 1'b1;
repeat (10) @(posedge spi_clk) rst_sys <= 1'b0;
repeat (40) @(posedge spi_clk) spi_cs <= 1'b0;
repeat (300) @(posedge spi_clk) spi_cs <= 1'b1;
repeat (400) @(posedge spi_clk);
$finish();
end
always @(posedge spi_clk or posedge rst_sys) begin
if (rst_sys) begin
// spi_si <= 1'b0;
command_shiter <= command;
end
else if(spi_cs == 1'b0) begin
// spi_si <= command_shiter[7];
command_shiter <= {command_shiter[6:0],1'b0};
end
else begin
// spi_si <= 1'b0;
command_shiter <= command;
end
end
assign spi_si = (spi_cs == 1'b0)?command_shiter[7]:1'b0;
endmodule