FPGA逃不过的testbench
testbench做这三件事:
产生模拟激励(波形);
将产生的激励加入到被测试模块中并观察其响应;
将输出响应与期望值相比较;
一、先搞懂:Testbench是什么?
Testbench(测试平台)是用于验证Verilog/VHDL设计代码(DUT:Design Under Test)的仿真代码,本质是“给DUT输入激励,捕获输出,验证是否符合预期”。
- 核心作用:无需上板,在仿真工具(ModelSim/QuestaSim)中验证逻辑是否正确(比如状态机是否按预期切换、输出是否毛刺);
- 新手关键:Testbench是纯仿真代码,不会被综合成硬件,可自由用
initial、#延时、force/release等仿真语法。
二、Testbench的通用结构(固定模板)
所有Testbench都遵循这个框架,直接套即可:
`timescale 1ns/1ps // 仿真时间单位/精度(ns是单位,ps是精度) module tb_xxx; // 模块名一般加tb_前缀,无输入输出 // 1. 定义仿真信号(与DUT的端口一一对应) reg clk; // 时钟(reg型,因为要在initial/always中赋值) reg rst_n; // 复位 reg key_in; // 按键输入 wire key_out; // 消抖输出(wire型,由DUT驱动) // 2. 例化待测试模块(DUT) key_fsm u_key_fsm( // 例化名u_xxx(规范) .clk (clk), .rst_n (rst_n), .key_in (key_in), .key_out (key_out) ); // 3. 生成时钟激励(比如50MHz,周期20ns) initial begin clk = 1'b0; forever #10 clk = ~clk; // 每10ns翻转一次,周期20ns end // 4. 生成复位+输入激励(核心:模拟真实场景) initial begin // 第一步:初始化信号 rst_n = 1'b0; // 先复位 key_in = 1'b1; // 按键默认松开(高电平) #200; // 等待200ns(让复位稳定) // 第二步:释放复位 rst_n = 1'b1; #1000; // 空闲状态等待1000ns // 第三步:模拟按键按下(带抖动) key_in = 1'b0; // 按下 #50000; // 抖动50us(小于消抖1ms) key_in = 1'b1; // 抖动 #30000; key_in = 1'b0; // 稳定按下 #1500000; // 按下1.5ms(超过消抖1ms) // 第四步:模拟按键松开(带抖动) key_in = 1'b1; // 松开 #40000; // 抖动40us key_in = 1'b0; // 抖动 #20000; key_in = 1'b1; // 稳定松开 #1500000; // 松开1.5ms // 第五步:结束仿真 $stop; // 暂停仿真($finish是退出) end // 5. 可选:打印状态/输出(方便调试) initial begin $monitor("时间=%0t, 当前状态=%b, key_out=%b", $time, u_key_fsm.current_state, key_out); end endmodule三、手把手写「按键消抖状态机」的Testbench
结合上一篇的key_fsm模块,完整Testbench代码+注释如下:
// 第一步:定义仿真时间尺度(必须放在最前面) `timescale 1ns/1ps // 1ns是时间单位,1ps是仿真精度(精度≤单位) // 第二步:定义Testbench模块(无输入输出) module tb_key_fsm; // 1. 声明信号:与DUT端口一一对应(reg驱动输入,wire接收输出) reg clk; // 50MHz时钟(周期20ns) reg rst_n; // 低电平复位 reg key_in; // 按键输入(模拟按下/松开/抖动) wire key_out; // 消抖后输出(由DUT输出) // 2. 例化待测试的状态机模块(DUT) // 格式:模块名 例化名(端口映射); key_fsm u_key_fsm( .clk (clk), // 时钟信号连接 .rst_n (rst_n), // 复位信号连接 .key_in (key_in), // 按键输入连接 .key_out (key_out) // 消抖输出连接 ); // 3. 生成时钟激励(50MHz,周期20ns) // initial:只执行一次的仿真语句(仿真特有的) initial begin clk = 1'b0; // 初始时钟低电平 forever #10 clk = ~clk; // 每10ns翻转一次,周期20ns(forever:无限循环) end // 4. 生成复位+按键输入激励(模拟真实按键行为) initial begin // 阶段1:复位初始化(仿真开始先复位) rst_n = 1'b0; // 复位拉低 key_in = 1'b1; // 按键默认松开(高电平) #200; // 等待200ns(让复位稳定,#是延时语法) // 阶段2:释放复位,进入空闲状态 rst_n = 1'b1; #1000; // 空闲1000ns(观察状态是否为IDLE) // 阶段3:模拟按键按下(带机械抖动) key_in = 1'b0; // 第一次检测到按下 #50000; // 抖动50us(小于消抖1ms,状态应停在KEY_DOWN) key_in = 1'b1; // 抖动(假松开) #30000; key_in = 1'b0; // 稳定按下 #1500000; // 按下1.5ms(超过消抖1ms,状态应到KEY_STABLE,key_out=1) // 阶段4:模拟按键松开(带机械抖动) key_in = 1'b1; // 第一次检测到松开 #40000; // 抖动40us(状态应停在KEY_UP) key_in = 1'b0; // 抖动(假按下) #20000; key_in = 1'b1; // 稳定松开 #1500000; // 松开1.5ms(状态回到IDLE,key_out=0) // 阶段5:重复一次按键(验证稳定性) key_in = 1'b0; #1500000; key_in = 1'b1; #1500000; // 阶段6:结束仿真 $stop; // 暂停仿真(ModelSim中可查看波形) // $finish; // 直接退出仿真(新手用$stop,方便看波形) end // 5. 调试辅助:打印关键信息(可选,但新手推荐) // $monitor:每次信号变化时打印,$time是仿真时间 initial begin $display("仿真开始!"); $monitor("时间=%0t | 当前状态=%b | key_in=%b | key_out=%b", $time, u_key_fsm.current_state, key_in, key_out); end endmodule四、Testbench核心语法(新手必记)
语法 | 作用 | 例子 |
| 定义仿真时间单位/精度 |
|
| 单次执行的仿真块(生成激励) |
|
| 无限循环(生成时钟) |
|
| 仿真延时(单位由 |
|
| 信号变化时打印(调试) |
|
| 暂停/退出仿真 |
|
| Testbench中:输入用reg,输出用wire |
|
五、仿真调试步骤(ModelSim为例)
新手最容易卡“仿真怎么跑”,这里给极简步骤:
- 新建工程:ModelSim中新建工程,添加
key_fsm.v(状态机)和tb_key_fsm.v(Testbench); - 编译代码:编译两个文件(无报错即可);
- 启动仿真:右键
tb_key_fsm,选择“Simulate”; - 添加波形:在仿真窗口找到
clk、rst_n、key_in、key_out、u_key_fsm.current_state,拖到波形窗口; - 运行仿真:点击“Run”(运行),再点击“Zoom Full”(全屏显示波形);
- 验证逻辑:
- 复位阶段:
rst_n=0时,current_state=IDLE,key_out=0; - 按键按下抖动:
current_state=KEY_DOWN,key_out仍为0; - 稳定按下1ms后:
current_state=KEY_STABLE,key_out=1; - 按键松开抖动:
current_state=KEY_UP,key_out=0; - 稳定松开1ms后:
current_state=IDLE。
- 复位阶段:
六、状态机+Testbench的避坑指南
- Testbench常见错误:
- ❌ 漏写
timescale:仿真时间混乱; - ❌ 输入信号用
wire:Testbench中输入必须用reg(initial/always只能赋值reg); - ❌ 时钟周期算错:50MHz时钟周期是20ns(
#10翻转),别写成#5; - ❌ 复位没给够时间:复位至少延时2-3个时钟周期(比如
#200)。
- ❌ 漏写
- 状态机仿真调试重点:
- ✅ 先看
current_state是否按预期切换(比如IDLE→KEY_DOWN→KEY_STABLE); - ✅ 再看输出
key_out是否只有KEY_STABLE状态为1(摩尔机特性); - ✅ 验证消抖逻辑:抖动阶段
key_out不触发,只有稳定1ms后才输出。
- ✅ 先看
七、进阶练习(从易到难)
- 入门级:修改Testbench,模拟“连续按两次按键”,验证输出是否正确;
- 进阶级:给LED流水灯状态机写Testbench(状态机控制LED亮灭顺序);
- 提升级:给UART接收状态机写Testbench(模拟发送1个字节,验证是否解析正确)。
总结
Testbench的核心是“模拟真实输入,验证输出是否符合预期”,先套模板(时钟+复位+输入激励+例化DUT),再结合状态机的“状态切换”重点验证。
先把「按键消抖+Testbench」跑通,看波形确认状态机逻辑正确,再尝试写其他状态机的Testbench——练2-3个例子后,Testbench的写法就会形成肌肉记忆,状态机的调试也会更顺手。