Verilog仿真入门:如何添加输入信号并查看波形

作者:很菜不狗2026.11.06 20:06浏览量:0

简介:针对数字电路初学者最常见的困惑——仿真时输入信号从哪里来、如何加到波形窗口,本文以1位全加器为例,讲清测试激励文件的编写要点与reg/wire的分工,梳理建工程、编译、加载仿真、添加信号、运行观察的六步通用流程,并附真值表核对方法与常见问题排查清单,帮助读者独立完成一次规范的Verilog仿真。

数字电路的仿真验证是硬件开发流程中不可跳过的一环,而初学者最常见的困惑往往不是语言本身,而是:仿真工具打开之后,输入信号到底从哪里来?为什么波形窗口里没有信号可以“添加”?本文围绕这个问题,以1位全加器为例,从概念、代码到操作流程完整走一遍Verilog仿真,帮助读者建立规范的验证习惯。文中操作以业界主流HDL仿真工具的通用流程为例描述,具体菜单文字可能因工具与版本略有差异,但环节顺序基本一致。

一、先纠正一个常见误区:输入信号不是“画”出来的

许多初学者第一次接触HDL仿真时,会下意识地想在波形窗口里像编辑图片一样手动画出输入波形。事实上,主流HDL仿真遵循“事件驱动”机制:输入激励由测试激励文件在代码中定义,仿真器只负责按时间推进、计算信号变化并展示结果。波形窗口本质上是“示波器”,用于观察,而不是用来绘制激励的“画板”(个别工具虽然提供图形化的激励编辑辅助,但那不是标准流程,也不利于回归测试)。

因此,“添加输入信号”的正确打开方式分为两步:

  1. 在测试激励文件中,用reg类型变量为被测模块的输入端口提供驱动;
  2. 仿真加载后,把这些信号(连同输出信号)添加到波形窗口进行观察。

这里隐含一条重要的类型规则:激励文件中,连接到被测模块输入端的信号要声明为reg(因为它需要在过程块中被赋值),连接到输出端的信号要声明为wire(因为它由被测模块驱动)。分不清reg与wire的分工,是“波形不出来”或“编译报错”的高发原因。

二、仿真环境的三要素

一次完整的Verilog仿真由三部分组成:

  • 设计文件(DUT):被验证的功能模块,例如全加器full_adder.v;
  • 测试激励文件(testbench):一个没有端口的顶层模块,内部实例化DUT并产生激励,例如tb_full_adder.v;
  • 时间刻度声明:`timescale 1ns/1ps,前者规定时间单位(#10代表10个时间单位,即10ns),后者规定仿真精度(1ps),两者共同决定延时表达与事件调度的精细程度。

此外,仿真工具通常以“工作库”组织编译产物,默认库名一般约定为work,源文件编译后生成的仿真单元存放在其中,加载仿真时也是从该库中选取顶层模块。

三、示例设计:1位全加器

  1. // 功能:1位全加器
  2. module full_adder (
  3. input a, // 加数A
  4. input b, // 加数B
  5. input cin, // 进位输入
  6. output sum, // 和
  7. output cout // 进位输出
  8. );
  9. assign sum = a ^ b ^ cin;
  10. assign cout = (a & b) | (a & cin) | (b & cin);
  11. endmodule

逻辑很直观:三个输入中有奇数个1时sum为1;有两个及以上1时产生进位cout。它是纯组合逻辑,输入规模小(3个输入共2³=8种组合),非常适合用来理解“穷举激励”的思想。

四、测试激励文件编写详解

  1. `timescale 1ns/1ps
  2. module tb_full_adder();
  3. reg a, b, cin;
  4. wire sum, cout;
  5. // 实例化待测模块(命名端口关联,可读性更好)
  6. full_adder u_adder (
  7. .a (a ),
  8. .b (b ),
  9. .cin (cin ),
  10. .sum (sum ),
  11. .cout(cout)
  12. );
  13. // 遍历8种输入组合,每10ns切换一组
  14. initial begin
  15. $display("Starting simulation...");
  16. $monitor("Time=%0t: a=%b b=%b cin=%b -> sum=%b cout=%b",
  17. $time, a, b, cin, sum, cout);
  18. a=0; b=0; cin=0; #10;
  19. a=0; b=0; cin=1; #10;
  20. a=0; b=1; cin=0; #10;
  21. a=0; b=1; cin=1; #10;
  22. a=1; b=0; cin=0; #10;
  23. a=1; b=0; cin=1; #10;
  24. a=1; b=1; cin=0; #10;
  25. a=1; b=1; cin=1; #10;
  26. $display("Simulation finished.");
  27. $finish;
  28. end
  29. endmodule

几个关键点值得展开:

  1. 顶层无端口。testbench自身就是整个仿真的“世界边界”,不需要对外接口。
  2. 赋初值很重要。若reg未初始化,仿真开始时其值为未知态x,波形上会显示为不定状态,容易误判为电路故障。
  3. #10是激励的“节拍”。如果所有赋值之间不写延时,8种组合会在0时刻瞬间全部执行完,波形上只能看到最后一组。
  4. $display与$monitor分工不同:$display在执行到该行时立即打印一次;$monitor则持续监控参数列表中的信号,任一信号变化就自动打印一行,适合记录激励切换前后的输出快照。注意$monitor在0时刻也会打印一次初值。
  5. $finish结束仿真。若不调用,initial块执行完后仿真将停滞,需要人工干预才能停止。

📌 实践建议:两个文件放入同一文件夹(例如自建的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步:将信号添加到波形窗口(对应标题问题)
加载成功后,界面通常分为源代码窗口、结构层次窗口、对象/信号窗口。操作路径:

  1. 在结构层次窗口中展开顶层,找到实例u_adder;
  2. 选中该实例后,信号窗口会列出其全部端口与内部信号;
  3. 全选或挑选a、b、cin、sum、cout,通过右键菜单中的“添加到波形”类选项送入波形窗口。

顶层信号也可以直接选中添加。若设计中含有内部中间变量,同样可按此方法加入观察,这对调试复杂模块非常有用。建议按“输入在上、输出在下”的顺序排列信号,并利用分组命名功能整理,提升波形可读性。

第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

验证方法有两个层面:

  1. 控制台文本核对:$monitor输出的每一行与上表逐一对照;
  2. 波形图形核对:每10ns输入切换一组,sum应等于a、b、cin的异或,cout在任意两个输入同时为1时拉高。特别关注两组“临界”情形:a=1,b=1,cin=0与a=0,b=1,cin=1,它们分别考验“本位进位”与“传递进位”两条路径。

七、常见问题排查清单

  1. 编译报“找不到模块/未定义”:文件未加入工程、未编译,或文件名与模块名不一致。
  2. 加载后波形空白或瞬间结束:十有八九是仿真顶层选成了设计模块而非testbench。
  3. 信号显示为红色或x态:激励中忘记赋初值,或存在未驱动的信号;检查端口连接是否遗漏。
  4. 只看到最后一组输入:赋值语句之间漏写#延时,所有激励在0时刻同时生效。
  5. 波形长度不够、看不全8组:运行时长不足,延长运行时间或直接运行到仿真结束。
  6. 修改代码后波形毫无变化:先重新编译,再重新加载仿真;直接重跑用的仍是旧编译结果。
  7. 端口位宽不匹配等告警:检查实例化端口映射,推荐始终使用命名端口关联而非位置关联,降低错位风险。

八、进阶:让激励写得更专业

掌握基础流程后,可以逐步升级激励写法:

  1. 用for循环穷举:对3位输入用integer循环变量配合for语句生成8种组合,代码更短且不易漏组合;
  2. 用task封装激励:把“施加一组输入并等待”封装成任务,激励主体变成一行行可读的调用;
  3. 自校验激励:在testbench中按真值表计算期望值,与实际输出自动比对,发现不一致时打印错误信息,实现“跑完即知对错”,而不必人工逐段读波形;
  4. 面向时序电路扩展:后续接触时序逻辑时,用always #5 clk = ~clk;生成时钟,配合复位激励,即可验证寄存器传输级设计。功能覆盖率、断言等概念则构成现代数字验证方法学的基础,可在实践中逐步引入。

九、小结

回到最初的问题:仿真工具里如何“添加输入信号”?标准答案是——输入信号由测试激励文件中的reg变量驱动产生,仿真加载后,再通过层次窗口把信号添加到波形窗口观察。记住这条主线:“写激励 → 建工程 → 编译 → 加载testbench → 添加信号 → 运行观察 → 对照真值表验证”,你就掌握了数字电路仿真的通用工作流。下一步,可以尝试把组合逻辑换成带时钟的时序模块,体会时钟与复位激励的写法,验证能力将随之上一个台阶。