简介:针对数字电路初学者最常见的困惑——仿真时输入信号从哪里来、如何加到波形窗口,本文以1位全加器为例,讲清测试激励文件的编写要点与reg/wire的分工,梳理建工程、编译、加载仿真、添加信号、运行观察的六步通用流程,并附真值表核对方法与常见问题排查清单,帮助读者独立完成一次规范的Verilog仿真。
数字电路的仿真验证是硬件开发流程中不可跳过的一环,而初学者最常见的困惑往往不是语言本身,而是:仿真工具打开之后,输入信号到底从哪里来?为什么波形窗口里没有信号可以“添加”?本文围绕这个问题,以1位全加器为例,从概念、代码到操作流程完整走一遍Verilog仿真,帮助读者建立规范的验证习惯。文中操作以业界主流HDL仿真工具的通用流程为例描述,具体菜单文字可能因工具与版本略有差异,但环节顺序基本一致。
许多初学者第一次接触HDL仿真时,会下意识地想在波形窗口里像编辑图片一样手动画出输入波形。事实上,主流HDL仿真遵循“事件驱动”机制:输入激励由测试激励文件在代码中定义,仿真器只负责按时间推进、计算信号变化并展示结果。波形窗口本质上是“示波器”,用于观察,而不是用来绘制激励的“画板”(个别工具虽然提供图形化的激励编辑辅助,但那不是标准流程,也不利于回归测试)。
因此,“添加输入信号”的正确打开方式分为两步:
这里隐含一条重要的类型规则:激励文件中,连接到被测模块输入端的信号要声明为reg(因为它需要在过程块中被赋值),连接到输出端的信号要声明为wire(因为它由被测模块驱动)。分不清reg与wire的分工,是“波形不出来”或“编译报错”的高发原因。
一次完整的Verilog仿真由三部分组成:
此外,仿真工具通常以“工作库”组织编译产物,默认库名一般约定为work,源文件编译后生成的仿真单元存放在其中,加载仿真时也是从该库中选取顶层模块。
// 功能:1位全加器module full_adder (input a, // 加数Ainput b, // 加数Binput cin, // 进位输入output sum, // 和output cout // 进位输出);assign sum = a ^ b ^ cin;assign cout = (a & b) | (a & cin) | (b & cin);endmodule
逻辑很直观:三个输入中有奇数个1时sum为1;有两个及以上1时产生进位cout。它是纯组合逻辑,输入规模小(3个输入共2³=8种组合),非常适合用来理解“穷举激励”的思想。
`timescale 1ns/1psmodule tb_full_adder();reg a, b, cin;wire sum, cout;// 实例化待测模块(命名端口关联,可读性更好)full_adder u_adder (.a (a ),.b (b ),.cin (cin ),.sum (sum ),.cout(cout));// 遍历8种输入组合,每10ns切换一组initial begin$display("Starting simulation...");$monitor("Time=%0t: a=%b b=%b cin=%b -> sum=%b cout=%b",$time, a, b, cin, sum, cout);a=0; b=0; cin=0; #10;a=0; b=0; cin=1; #10;a=0; b=1; cin=0; #10;a=0; b=1; cin=1; #10;a=1; b=0; cin=0; #10;a=1; b=0; cin=1; #10;a=1; b=1; cin=0; #10;a=1; b=1; cin=1; #10;$display("Simulation finished.");$finish;endendmodule
几个关键点值得展开:
📌 实践建议:两个文件放入同一文件夹(例如自建的D:/sim_demo/full_adder目录),且文件名与模块名严格一致——工程化管理时通常依赖这一约定来定位编译单元。
第1步:新建工程
通过File → New → Project(或类似入口)创建工程,填写工程名与存放路径,工作库保持默认。工程化管理的意义在于统一记录文件清单与编译顺序,便于后续一键重编译。
第2步:添加设计文件
在弹出的添加文件向导中选择“添加已有文件”,把full_adder.v与tb_full_adder.v一并加入(多数工具支持按住Ctrl多选)。注意:激励文件必须加入工程,否则编译时会报“找不到测试模块”。
第3步:编译全部文件
执行“编译全部”操作。若有语法错误,信息窗口会给出文件名、行号与错误原因,逐条修正后重新编译,直到全部通过。编译的本质是把文本形式的HDL翻译为仿真器可执行的中间表示,并放入工作库。
第4步:加载仿真(关键一步)
选中测试激励文件tb_full_adder.v(或其中的tb_full_adder模块),执行“启动仿真/加载仿真”操作。这里最容易踩坑:一定要选择testbench作为仿真顶层,而不是设计模块full_adder。若错选设计模块,仿真里没有任何激励源,波形自然一片空白。加载过程中仿真器会完成层次化实例的展开,并检查端口连接与位宽匹配。
第5步:将信号添加到波形窗口(对应标题问题)
加载成功后,界面通常分为源代码窗口、结构层次窗口、对象/信号窗口。操作路径:
顶层信号也可以直接选中添加。若设计中含有内部中间变量,同样可按此方法加入观察,这对调试复杂模块非常有用。建议按“输入在上、输出在下”的顺序排列信号,并利用分组命名功能整理,提升波形可读性。
第6步:运行仿真并观察
通过工具栏按钮或命令行执行运行操作:可以每次运行固定时长(例如100ns)分段观察,也可以一次性运行到$finish触发仿真结束。观察时善用两个技巧:一是先缩放到全局视图看整体节拍,再放大看局部细节;二是拖动光标测量时间差,核对相邻输入组合之间是否严格间隔10ns。再次强调:波形窗口是只读的,若要修改激励,必须回到激励代码中修改,重新编译后重新加载仿真。
仿真是否正确,要拿“金标准”对照——全加器真值表:
| a | b | cin | sum | cout |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 1 | 1 |
验证方法有两个层面:
掌握基础流程后,可以逐步升级激励写法:
always #5 clk = ~clk;生成时钟,配合复位激励,即可验证寄存器传输级设计。功能覆盖率、断言等概念则构成现代数字验证方法学的基础,可在实践中逐步引入。回到最初的问题:仿真工具里如何“添加输入信号”?标准答案是——输入信号由测试激励文件中的reg变量驱动产生,仿真加载后,再通过层次窗口把信号添加到波形窗口观察。记住这条主线:“写激励 → 建工程 → 编译 → 加载testbench → 添加信号 → 运行观察 → 对照真值表验证”,你就掌握了数字电路仿真的通用工作流。下一步,可以尝试把组合逻辑换成带时钟的时序模块,体会时钟与复位激励的写法,验证能力将随之上一个台阶。