2026-07-07 21:45:01

完整教程:FPGA零基础入门:TestBench编写完全指南

FPGA零基础入门:TestBench编写完全指南前言 你是否刚开始接触FPGA,写完代码却不知道如何测试?或者直接把代码下载到板子上看效果,结果出了问题还要反复修改、反复下载?这篇文章就是为你准备的!我们将从零开始,手把手教你编写TestBench,让你的FPGA学习之路更加轻松~

什么是TestBench?为什么要学它? 形象比喻想象一下,你设计了一辆汽车(FPGA设计),在把它开上真正的马路之前,你肯定要先在测试场地试一试对吧?TestBench就是这个"测试场地"!

在这个虚拟的测试场地里,你可以:

自由控制输入:比如模拟踩油门、刹车 观察输出结果:看看车速、转向是否正常 发现潜在问题:在上路前就把Bug修好❗ 为什么不能直接下载到板子上测试?很多初学者(包括我最开始)都犯过这个错误:

写完代码直接下载到FPGA板子发现不对劲,再改代码再下载,再测试…这样做的问题:

⏰ 时间成本高:每次下载编译要等很久 调试困难:板子上看不到内部信号 可能损坏硬件:严重的逻辑错误可能烧坏芯片正确的流程应该是:

写代码 → 写TestBench → 仿真测试 → 修改完善 → 下载到板子

TestBench的四大核心任务一个完整的TestBench需要完成这四件事:

1️⃣ 实例化待测模块(DUT)DUT = Design Under Test(被测设计),就是把你要测试的模块"召唤"出来

2️⃣ 编写测试激励给模块的输入端口送信号,就像给汽车踩油门一样

3️⃣ 观察输出结果通过波形窗口或终端打印,看看输出是否符合预期

4️⃣ 自动化验证(进阶)让程序自动比对实际输出和预期输出,发现不一致就报警

TestBench基本结构先来看一个最简单的TestBench骨架:

`timescale 1ns/1ns // 时间单位和精度

module testbench; // TestBench模块名(通常无输入输出)

// 1. 信号声明

reg 输入信号; // 输入用reg类型

wire 输出信号; // 输出用wire类型

// 2. 实例化待测模块

待测模块名 实例名(

.端口1(信号1),

.端口2(信号2)

);

// 3. 生成时钟信号(如果需要)

initial begin

clk = 0;

forever #10 clk = ~clk; // 每10个时间单位翻转一次

end

// 4. 生成测试激励

initial begin

// 初始化

输入信号 = 0;

// 施加激励

#20 输入信号 = 1;

#30 输入信号 = 0;

// 结束仿真

#100 $stop;

end

// 5. 监控输出(可选)

initial begin

$monitor("时间=%t, 输入=%b, 输出=%b", $time, 输入信号, 输出信号);

end

endmodule

⏱️ 第一关:时间刻度(timescale) 这是什么?timescale告诉仿真器:"1个时间单位"等于多长的真实时间。

语法格式

`timescale 时间单位/时间精度

实例讲解

`timescale 10ns/1ns // 单位10ns,精度1ns

module testbench;

reg set;

initial begin

#1 set = 0; // 延时1个时间单位 = 10ns

#1.8 set = 1; // 延时1.8个时间单位 = 18ns

end

endmodule

解释:

#1:延时1个时间单位 → 实际延时10ns#1.8:延时1.8个时间单位 → 实际延时18ns(会按精度1ns舍入)⚠️ 重要提示时间单位和精度的可选值:

只能是:1、10、100单位可以是:s(秒)、ms(毫秒)、us(微秒)、ns(纳秒)、ps(皮秒)、fs(飞秒)精度必须 ≤ 时间单位实用建议:

`timescale 1ns/1ns // ✅ 推荐:ns级精度足够大多数场景

`timescale 1ns/1ps // ❌ 不推荐:精度太高,仿真慢,通常用不到

第二关:生成时钟信号时序逻辑电路必须有时钟,TestBench中生成时钟非常简单!

方法一:使用forever循环(最常用)

parameter Period = 20; // 时钟周期20ns

reg clk;

initial begin

clk = 0;

forever #(Period/2) clk = ~clk; // 每10ns翻转一次

end

解释:

Period = 20:时钟周期20ns → 频率50MHz#(Period/2):延时半个周期(10ns)后翻转forever:一直循环,直到仿真结束方法二:使用always(不推荐初学者)

parameter Period = 20;

reg clk;

initial clk = 0;

always #(Period/2) clk = ~clk;

小练习**问题:**如果我想生成一个100MHz的时钟,周期应该设置为多少?

点击查看答案

// 100MHz → 周期 = 1/100MHz = 10ns

parameter Period = 10;

第三关:initial语句块initial是TestBench的核心工具,用来编写测试激励。

关键特性只执行一次:从仿真开始时(时间0)执行不可综合:只用于仿真,不能下载到FPGA可并发运行:可以写多个initial块,它们同时开始执行块内顺序执行:一个initial块内部的语句按顺序执行 基本示例

reg reset, enable;

wire [7:0] data_out;

initial begin

// 初始化信号

reset = 1;

enable = 0;

// 复位10ns

#10 reset = 0;

// 100ns后使能

#100 enable = 1;

// 200ns后停止仿真

#200 $stop;

end

多个initial块协作

// 块1:生成时钟

initial begin

clk = 0;

forever #5 clk = ~clk;

end

// 块2:生成复位

initial begin

reset = 1;

#15 reset = 0;

end

// 块3:测试激励

initial begin

data_in = 8'h00;

#30 data_in = 8'hAA;

#50 data_in = 8'h55;

#100 $finish;

end

时序图示意:

时间: 0 5 10 15 20 25 30 ...

clk: _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_...

reset: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|________________...

data_in: 00 | AA ...

️ 第四关:常用系统函数系统函数都以$开头,是仿真器提供的特殊功能。

1. $finish vs $stop函数功能使用场景$finish停止仿真并退出测试完成$stop暂停仿真需要手动检查某个状态

initial begin

// ... 测试代码 ...

wait(data_ready); // 等待数据准备好

$stop; // 暂停,可以手动查看波形

end

2. $display - 打印信息

initial begin

$display("仿真开始!");

// 打印变量值

$display("时间=%t, a=%b, b=%d", $time, signal_a, signal_b);

// 自动换行

$display("Hello");

$display("World"); // 会在下一行显示

end

格式说明符:

%t:时间(配合

t

i

m

e

time或

time或realtime)%b:二进制%d:十进制%h:十六进制%o:八进制%c:ASCII字符%s:字符串3. $monitor - 监控信号变化**特点:**只在信号变化时自动打印

initial begin

$monitor("时间=%t, clk=%b, data=%h", $time, clk, data);

end

对比:

// $display:每次都打印

always @(posedge clk) begin

$display("clk上升沿"); // 每个时钟都打印

end

// $monitor:只在变化时打印

initial begin

$monitor("data=%d", data); // data变化时才打印

end

4. $timeformat - 格式化时间显示

initial begin

// 以ns为单位,小数点后1位,后缀"ns",最小宽度12

$timeformat(-9, 1, "ns", 12);

$monitor("%t: clk=%b", $realtime, clk);

// 输出: " 10.5ns: clk=1"

end

参数说明:

第1个参数:单位(0=秒, -3=毫秒, -6=微秒, -9=纳秒, -12=皮秒)第2个参数:小数位数第3个参数:单位后缀字符串第4个参数:最小显示宽度 完整实例:移位寄存器测试现在我们通过一个完整的例子,把所有知识点串起来!

待测模块:移位寄存器功能说明:

5位移位寄存器reset=1时清零load=1时加载数据根据sel选择移位方向

module shift_reg(

input clock,

input reset,

input load,

input [1:0] sel,

input [4:0] data,

output reg [4:0] shiftreg

);

always @(posedge clock) begin

if (reset)

shiftreg <= 5'b00000;

else if (load)

shiftreg <= data;

else begin

case (sel)

2'b00: shiftreg <= shiftreg; // 保持不变

2'b01: shiftreg <= shiftreg << 1; // 左移

2'b10: shiftreg <= shiftreg >> 1; // 右移

default: shiftreg <= shiftreg;

endcase

end

end

endmodule

完整TestBench

`timescale 1ns/1ns

module testbench;

// 1. 信号声明

reg clock;

reg reset;

reg load;

reg [1:0] sel;

reg [4:0] data;

wire [4:0] shiftreg;

// 2. 实例化待测模块

shift_reg dut (

.clock(clock),

.reset(reset),

.load(load),

.sel(sel),

.data(data),

.shiftreg(shiftreg)

);

// 3. 生成时钟信号(周期100ns, 频率10MHz)

initial begin

clock = 0;

forever #50 clock = ~clock; // 每50ns翻转

end

// 4. 生成测试激励

initial begin

// 初始化

reset = 1;

load = 0;

sel = 2'b00;

data = 5'b00000;

// 复位200ns

#200 reset = 0;

// 加载数据:00001

load = 1;

#200 data = 5'b00001;

// 左移测试

#100 sel = 2'b01;

load = 0;

// 右移测试

#200 sel = 2'b10;

// 结束仿真

#1000 $stop;

end

// 5. 监控输出

initial begin

// 设置时间格式

$timeformat(-9, 1, "ns", 12);

// 打印表头

$display("===========================================");

$display("Time | Clk | Rst | Ld | Sel | Data | ShiftReg");

$display("===========================================");

// 监控信号变化

$monitor("%t | %b | %b | %b | %b | %b | %b",

$realtime, clock, reset, load, sel, data, shiftreg);

end

endmodule

预期波形分析

时间段 | 操作 | shiftreg变化

0-200ns | 复位 | 00000

200-300ns | 加载00001 | 00001

300-500ns | 左移2次 | 00010 → 00100

500-1500ns | 右移多次 | 00010 → 00001 → 00000

进阶技巧:自动化验证手动看波形太累?让程序帮你自动检查!

基本思路预先定义"正确答案"在关键时刻检查实际输出不匹配就报错 示例代码

initial begin

// 测试用例1:加法器测试

a = 4'd5;

b = 4'd3;

#10; // 等待计算完成

// 检查结果

if (sum == 4'd8) begin

$display("✅ 测试通过:5+3=8");

end else begin

$display("❌ 测试失败:期望8,实际%d", sum);

$stop; // 暂停仿真,方便调试

end

end

使用数组批量测试

integer i;

reg [7:0] test_vectors [0:9]; // 10组测试数据

reg [7:0] expected_results [0:9];

initial begin

// 初始化测试数据

test_vectors[0] = 8'h12; expected_results[0] = 8'h24;

test_vectors[1] = 8'h34; expected_results[1] = 8'h68;

// ...

// 批量测试

for (i = 0; i < 10; i = i + 1) begin

data_in = test_vectors[i];

#10;

if (data_out == expected_results[i]) begin

$display("测试%0d:通过", i);

end else begin

$display("测试%0d:失败(期望%h,实际%h)",

i, expected_results[i], data_out);

$stop;

end

end

$display("所有测试通过! ");

$finish;

end

⚠️ 新手常见错误❌ 错误1:输入信号用wire

// ❌ 错误

wire data_in; // 输入应该用reg!

// ✅ 正确

reg data_in;

**原因:**reg类型可以赋值,wire不能在initial或always块中直接赋值。

❌ 错误2:忘记初始化

// ❌ 错误:clk初始值不确定

reg clk;

always #5 clk = ~clk; // 可能一直是x

// ✅ 正确

reg clk;

initial clk = 0;

always #5 clk = ~clk;

❌ 错误3:时间单位太小

// ❌ 不推荐:仿真太慢

`timescale 1ps/1fs

// ✅ 推荐:够用就行

`timescale 1ns/1ns

❌ 错误4:没有停止条件

// ❌ 错误:仿真会一直运行

initial begin

clk = 0;

forever #5 clk = ~clk;

end

// 没有$stop或$finish!

// ✅ 正确

initial begin

#1000 $finish; // 1000ns后结束

end

实用模板模板1:基础测试

`timescale 1ns/1ns

module tb_模块名;

// 信号声明

reg clk, rst;

// ... 其他信号

// 实例化DUT

模块名 dut (/* 端口连接 */);

// 生成时钟

initial begin

clk = 0;

forever #10 clk = ~clk;

end

// 测试激励

initial begin

rst = 1;

#100 rst = 0;

// ... 测试代码

#1000 $finish;

end

// 监控输出

initial $monitor("%t: ...", $time);

endmodule

模板2:自检测试

`timescale 1ns/1ns

module tb_模块名;

// 信号声明

reg clk, rst;

integer pass_count, fail_count;

// 实例化DUT

模块名 dut (/* 端口连接 */);

// 时钟生成

initial begin

clk = 0;

forever #10 clk = ~clk;

end

// 测试主程序

initial begin

pass_count = 0;

fail_count = 0;

// 初始化

rst = 1;

#100 rst = 0;

// 测试用例1

test_case_1();

// 测试用例2

test_case_2();

// 报告结果

$display("======== 测试结果 ========");

$display("通过:%d, 失败:%d", pass_count, fail_count);

$finish;

end

// 测试用例任务

task test_case_1;

begin

// ... 施加激励

#10;

// 检查结果

if (output_signal == expected_value) begin

$display("✅ 测试1通过");

pass_count = pass_count + 1;

end else begin

$display("❌ 测试1失败");

fail_count = fail_count + 1;

end

end

endtask

endmodule

学习路线建议第一阶段:掌握基础(1-2周)✅ 理解TestBench的作用✅ 会写简单的initial块✅ 能生成时钟和基本激励✅ 会用$display打印信息第二阶段:熟练运用(2-4周)✅ 能为中等复杂度模块写TestBench✅ 会用$monitor监控信号✅ 理解时间建模✅ 能看懂仿真波形第三阶段:进阶提升(1-2个月)✅ 掌握自动化验证技巧✅ 会写可重用的TestBench✅ 学习SystemVerilog验证方法✅ 了解UVM验证方法论 实战练习练习1:全加器测试编写TestBench测试一个1位全加器,要求:

穷举所有8种输入组合自动检查输出是否正确打印测试报告 参考答案

`timescale 1ns/1ns

module tb_full_adder;

reg a, b, cin;

wire sum, cout;

integer i;

full_adder dut(a, b, cin, sum, cout);

initial begin

$display("===== 全加器测试 =====");

for (i = 0; i < 8; i = i + 1) begin

{a, b, cin} = i;

#10;

$display("a=%b b=%b cin=%b → sum=%b cout=%b",

a, b, cin, sum, cout);

end

$finish;

end

endmodule

练习2:计数器测试为一个4位加法计数器编写TestBench,要求:

生成复位信号计数20个时钟周期检查是否按0-15循环计数 学习声明本文是学习知识星球**「FPGA从入门到精通」**后按个人理解整理的学习笔记,内容可能存在理解不够深入或不够完善之处。

文章内容综合参考了多个优质教程和官方文档,包括但不限于:

FPGA Tutorial - How to Write a Basic Verilog TestbenchCSDN博客 - Testbench编写指南系列Digikey - Introduction to FPGA Testbenches and SimulationHardwareBee - The Ultimate Guide to FPGA Test Benches如果你希望获取更系统、更专业的FPGA与数字电路知识,建议前往原知识星球学习更完整的课程内容。

笔记整理有限,原创内容无限

相关资源推荐书籍:

《Writing Testbenches: Functional Verification of HDL Models》《Verilog HDL设计实用教程》推荐网站:

FPGA TutorialFPGA InsightsDoulos - Verilog Tutorial仿真工具:

ModelSim / QuestaSim (商业)Icarus Verilog + GTKWave (开源)Vivado Simulator (Xilinx免费) 结语TestBench是FPGA学习中不可或缺的技能。虽然一开始可能觉得写TestBench很麻烦,但当你的设计越来越复杂时,你就会发现它的价值!

记住:

先仿真,后综合 - 这是铁律! 从简单开始 - 不要一上来就写复杂的验证环境 多练习 - 每写一个模块,就写一个TestBench祝你在FPGA学习之路上越走越远!

如有疑问或发现错误,欢迎指正交流!

乌兰图雅在《乘风2026》初舞台演唱的具体是哪几首歌?
给对方QQ转账设置了24小时延迟到账,中途可以撤回吗