Vivado 从零入门:让第一次接触 FPGA 的人也能看懂
很多人第一次打开 Vivado,第一感觉不是“我可以开始写代码了”,而是“这到底是什么东西?”
界面里有 Synthesis、Implementation、Bitstream、IP Integrator、Hardware Manager、Constraints、Timing、Run、Generate Output Products……每个词都像认识,又好像完全不懂。
如果你以前写过 C、Python、Java,可能会下意识把 Vivado 当成一个“写代码、编译、运行”的软件 IDE。但 FPGA 开发不是普通软件开发。Vivado 不是把代码编译成 CPU 执行的程序,而是把你写的硬件描述,变成 FPGA 芯片内部真实的数字电路连接。
这篇文章的目标很简单:
让一个完全不懂 Vivado 的人,知道 Vivado 是干什么的,知道一个 FPGA 工程从创建到烧录到底经历了什么,也知道常见报错和困惑应该怎么理解。
一、先把最重要的概念讲清楚
1. Vivado 是什么?
Vivado 是 AMD/Xilinx FPGA 的主要开发工具。你可以把它理解为一个“数字电路设计工厂”。
在 Vivado 里,你通常会做这些事:
一句话总结:
Vivado 不是普通代码编辑器,它是把“硬件描述”变成“FPGA 内部电路配置文件”的工具。
2. FPGA 不是 CPU
这句话非常重要。
CPU 执行软件时,一般是一条指令一条指令顺序执行。比如 C 语言里:
a = 1;
b = 2;
c = a + b;
CPU 会按照指令流程一步一步运行。
但 FPGA 不是这样。FPGA 更像一堆可以重新连接的逻辑门、触发器、查找表、RAM、DSP、时钟网络和 IO 管脚。你写的 Verilog/VHDL,不是告诉 CPU“先做什么、后做什么”,而是在描述:
我要在芯片里搭一个什么样的电路。
所以很多新手会困惑:
“为什么我写了一个 always 块,感觉不像 C 语言一样执行?”
“为什么多个 always 块好像是同时运行的?”
“为什么赋值有阻塞赋值和非阻塞赋值?”
“为什么不能随便用 for 循环?”
根本原因是:你写的不是普通程序,而是电路描述。
3. bitstream 是什么?
Vivado 最终生成的常见文件叫 bitstream,后缀通常是 .bit。
它不是软件程序,也不是 exe 文件。它更像是一份“FPGA 内部布线说明书”。
FPGA 芯片上有大量可配置资源。bitstream 会告诉 FPGA:
- 哪些查找表要实现什么逻辑;
- 哪些触发器要被使用;
- 哪些 IO 管脚接到哪个内部信号;
- 哪些时钟网络被使用;
- 哪些 BRAM、DSP、PLL、MMCM、AXI IP 被启用;
- 这些资源之间怎么连线。
所以你点击“Program Device”之后,本质上是把 .bit 文件下载到 FPGA,让 FPGA 按照你的设计重新配置自己。
二、Vivado 工程的完整流程
一个普通 Vivado 工程大致经历下面这条线:
创建工程
↓
添加设计代码 RTL
↓
添加约束文件 XDC
↓
仿真 Simulation
↓
综合 Synthesis
↓
实现 Implementation
↓
生成比特流 Generate Bitstream
↓
连接开发板 Hardware Manager
↓
烧录 Program Device
如果是 Zynq,比如 Zynq-7000,流程可能会多几步:
创建 Vivado 工程
↓
创建 Block Design
↓
添加 ZYNQ7 Processing System
↓
配置 PS,连接 DDR、MIO、FCLK 等
↓
添加 PL 逻辑或 AXI 外设
↓
生成 bitstream
↓
导出硬件 XSA
↓
在 Vitis 中写 ARM 端软件
很多初学者会把这些步骤混在一起。下面我们逐个拆开讲。
三、安装 Vivado 前要注意什么?
1. 版本不要随便选
Vivado 版本很多,比如 2018.3、2020.2、2022.2、2023.2、2024.2、2025.x 等。
新手最容易犯的错误是:
板卡资料用的是一个版本,自己安装另一个版本,然后照教程做时发现界面对不上。
建议:
- 如果你是跟开发板教程学习,优先安装教程指定的 Vivado 版本;
- 如果只是自己新建 RTL 工程,使用较新的稳定版本也可以;
- 如果你使用的是老款 Zynq-7000 或 7 系列 FPGA,注意确认当前 Vivado 是否仍然支持你的器件;
- 如果板卡厂家提供了工程例程,尽量用厂家推荐版本打开。
版本不一致不一定不能用,但初学阶段很容易因为菜单变化、IP 版本升级、工程自动迁移而迷路。
2. 安装路径尽量简单
建议安装路径和工程路径不要有中文、空格或特殊字符。
推荐:
D:/Xilinx/Vivado
D:/fpga_project/led_blink
不推荐:
D:/我的工程/FPGA 学习/第一个工程
C:/Users/张三/Desktop/新建文件夹
不是说一定会出错,而是 FPGA 工具链比较复杂,路径中出现中文或特殊字符时,Tcl 脚本、IP 生成、仿真器、第三方工具可能出现奇怪问题。新手最好先避开这些坑。
3. 驱动要装好
开发板通常通过 JTAG 下载程序。如果 Vivado 的 Hardware Manager 找不到板子,常见原因是:
- USB 线只支持充电,不支持数据;
- JTAG 驱动没有安装;
- 板子没有上电;
- JTAG 模式跳线不对;
- 板卡电源不足;
- Windows 设备管理器中驱动异常;
- 使用了虚拟机但 USB 没有正确直通。
如果你打开 Hardware Manager 后看不到硬件目标,不要急着怀疑代码,先检查线、电源、驱动和跳线。
四、新建一个 Vivado 工程
下面以最简单的 LED 闪烁工程为例。
1. 打开 Vivado
启动 Vivado 后,点击:
Create Project
然后进入 New Project 向导。
2. Project Name
输入工程名,例如:
led_blink
工程路径建议放在:
D:/fpga_project/led_blink
勾选或不勾选“Create project subdirectory”都可以。新手建议勾选,让 Vivado 自动创建工程文件夹。
注意:工程名也尽量只用英文、数字、下划线。
3. Project Type
选择:
RTL Project
意思是:这个工程主要使用 Verilog、VHDL、SystemVerilog 等硬件描述语言。
下面通常会有一个选项:
Do not specify sources at this time
新手可以勾选。这样可以先创建空工程,再慢慢添加源码和约束文件。
4. 选择器件或开发板
这是非常关键的一步。
Vivado 需要知道你最终要把电路放到哪一颗芯片里。选择错了,后面轻则无法下载,重则引脚约束全部不对。
有两种选择方式:
方式一:Boards
如果 Vivado 已经安装了你的开发板 Board Files,可以在 Boards 里直接选开发板型号。
优点是方便,Vivado 会知道板上有哪些外设、接口和部分预设。
方式二:Parts
如果没有 Board Files,就选择具体芯片型号。
你需要从开发板手册、原理图或芯片丝印中确认:
- 芯片系列;
- 芯片型号;
- 封装;
- 速度等级;
- 温度等级。
例如 Zynq-7000 开发板常见芯片可能类似:
xc7z020clg400-2
这个型号不能随便猜。
尤其是封装,比如 clg400,如果选错,管脚编号就会错,XDC 约束会对不上。
五、不要急着写代码,先看懂 Vivado 工程里有什么
很多教程到了这里会直接让你新建一个 led_blink.v,然后贴一段 Verilog 代码。
但对新手来说,这样非常容易懵,因为你还不知道 Vivado 工程里每个区域到底在干什么。
所以在写代码之前,我们先认识 Vivado 工程最常用的几个部分。
创建好工程后,Vivado 左边一般会看到一个叫 Flow Navigator 的区域。这里是整个 FPGA 开发流程的入口。
常用按钮大概有这些:
Project Manager 工程管理
IP Integrator 图形化搭建 IP 系统,Zynq 常用
Simulation 仿真
RTL Analysis RTL 分析
Synthesis 综合
Implementation 实现
Program and Debug 下载和调试
新手阶段先重点关注这几个:
Project Manager
Simulation
Synthesis
Implementation
Program and Debug
也就是说,刚开始你可以先不用管 IP Integrator、Block Design、AXI、Zynq PS、Vitis。
我们先把最基础的纯 PL 逻辑跑通。
如果你用的是 Zynq 开发板,比如 ACZ702,也不要紧。Zynq 虽然里面有 ARM 处理器,但它也有 FPGA 逻辑部分,也就是 PL。
我们第一阶段只用 PL,把它当普通 FPGA 来用。
六、Sources 窗口到底是什么?
Vivado 工程里最重要的窗口之一叫 Sources。
它一般分成几类:
Design Sources
Constraints
Simulation Sources
Utility Sources
新手最常用的是前三个。
1. Design Sources:放你的硬件设计代码
这里放的是 Verilog、VHDL、SystemVerilog 等设计文件。
比如你写了一个 LED 控制模块:
led_on.v
或者:
led_blink.v
它就应该放在 Design Sources 下面。
Design Sources 里的代码最终会被 Vivado 拿去综合,变成 FPGA 里的真实电路。
2. Constraints:放约束文件
这里放的是 .xdc 文件。
XDC 文件的作用非常重要,它告诉 Vivado:
你的代码里的信号,要连接到 FPGA 芯片的哪个真实管脚。
比如你代码里写了:
output wire led
Vivado 并不知道这个 led 要接到开发板上哪个 LED 灯。
你必须在 XDC 文件里告诉它:
set_property PACKAGE_PIN 某个管脚 [get_ports led]
所以:
Verilog 负责描述电路逻辑;
XDC 负责描述这个电路怎么连接到真实板子。
没有 XDC,Vivado 不知道你的 LED、按键、时钟、串口到底接在哪里。
3. Simulation Sources:放仿真文件
这里放 testbench,也就是仿真测试文件。
testbench 不会被烧进 FPGA。
它只是用来在电脑里模拟你的电路运行情况。
比如你想在上板前看看 LED 信号会不会翻转,就可以写一个仿真文件。
七、什么叫“顶层模块”?
这是新手特别容易搞混的一个概念。
FPGA 工程里可以有很多模块。
比如你以后可能会写:
uart_rx.v
uart_tx.v
fifo.v
pwm.v
led_blink.v
但 Vivado 最终必须知道:
哪个模块是整个设计的最外层入口?
这个最外层入口就叫 Top Module,中文常说“顶层模块”。
你可以把顶层模块想象成一块电路板的总接口。
它直接面对 FPGA 芯片的外部管脚。
例如:
module led_on(
output wire led
);
assign led = 1'b1;
endmodule
这里的 led_on 就可以作为顶层模块。
如果你的顶层模块有这些端口:
input wire clk,
input wire rst_n,
output wire led
那么 Vivado 会认为这个设计对外有三个接口:
clk 输入时钟
rst_n 输入复位
led 输出 LED 控制信号
然后你就必须在 XDC 里给这些接口分配真实管脚。
八、为什么建议文件名和模块名一致?
Verilog 语法上不强制文件名和模块名一致。
比如文件叫:
abc.v
里面模块可以叫:
module led_on();
这在语法上可以。
但是新手强烈建议保持一致:
文件名:led_on.v
模块名:led_on
这样不容易乱。
Vivado 的层级、报错、仿真、顶层设置都会更清楚。
九、先不要做 LED 闪烁,先做“点亮 LED”
很多入门教程一上来就是 LED 闪烁。
但 LED 闪烁其实已经涉及这些知识:
时钟
计数器
寄存器
时钟频率
翻转
复位
对完全新手来说,这一步有点快。
所以我们先做更简单的实验:
让一个 LED 一直亮。
这个实验的目的不是为了炫技,而是为了确认:
工程能不能创建成功;
器件型号有没有选对;
XDC 管脚约束有没有写对;
Vivado 能不能综合、实现、生成 bitstream;
开发板能不能连接;
程序能不能下载进去;
LED 极性有没有搞清楚。
这些都通了,再做闪烁就顺很多。
十、写第一个最简单的 Verilog:点亮 LED
新建一个 Verilog 文件:
led_on.v
代码如下:
module led_on(
output wire led
);
assign led = 1'b1;
endmodule
这段代码非常简单。
它的意思是:
让 led 这个输出信号一直等于 1。
也就是一直输出高电平。
十一、但是 LED 不一定是高电平点亮
这里有一个很重要的坑。
有些开发板上的 LED 是:
高电平点亮
也就是说:
assign led = 1'b1;
LED 会亮。
但有些开发板上的 LED 是:
低电平点亮
也就是说:
assign led = 1'b0;
LED 才会亮。
这和开发板电路有关。
例如 LED 可能一端接 FPGA 管脚,另一端接 GND。
也可能一端接电源,另一端通过 FPGA 管脚拉低点亮。
所以如果你下载后 LED 没亮,不一定是工程错了。
可以试一下:
assign led = 1'b0;
如果 1 不亮,0 亮,那说明这个 LED 是低电平有效。
这里先不要纠结,先记住:
LED 亮不亮,既和代码有关,也和开发板电路有关。
十二、给 LED 分配真实管脚
现在代码里有了一个输出信号:
output wire led
但是 Vivado 还不知道这个 led 对应开发板上的哪个 LED。
所以要添加 XDC 文件。
新建一个约束文件:
led_on.xdc
内容类似下面这样:
set_property PACKAGE_PIN <LED对应的FPGA管脚> [get_ports led]
set_property IOSTANDARD LVCMOS33 [get_ports led]
注意,这里的:
<LED对应的FPGA管脚>
不能照抄,必须换成你开发板手册或原理图里的真实管脚。
比如你的板卡手册里如果写:
LED0 接 FPGA 某某管脚
你就要把那个管脚填进去。
XDC 最重要的原则是:
[get_ports led] 里的 led,必须和 Verilog 顶层模块里的端口名一模一样。
如果 Verilog 里写的是:
output wire led0
那 XDC 里就必须写:
[get_ports led0]
不能写成:
[get_ports led]
否则 Vivado 会报错:
No ports matched 'led'
意思是它在顶层模块里找不到叫 led 的端口。
十三、设置顶层模块
如果 Vivado 没有自动识别顶层模块,你需要手动设置。
在 Sources 窗口中找到:
led_on.v
右键点击,选择:
Set as Top
这样 Vivado 就知道:
led_on 是整个工程的最顶层模块。
如果顶层模块设置错了,后面 XDC 可能匹配不到端口,综合结果也可能不是你想要的。
十四、运行 Synthesis:综合
现在可以点击:
Run Synthesis
综合的意思是:
把 Verilog 描述的逻辑,转换成 FPGA 可以实现的底层电路。
例如:
assign led = 1'b1;
综合后 Vivado 会知道:
led 这个输出口需要固定输出高电平。
综合阶段常见错误有:
语法写错;
模块名写错;
顶层模块设置错;
端口名和约束文件对不上;
多个地方驱动同一个信号;
用了不可综合的写法。
如果综合失败,不要急着重新建工程,先看下方 Messages 里的红色 ERROR。
十五、运行 Implementation:实现
综合成功后,继续点击:
Run Implementation
实现的意思是:
把综合出来的电路真正放进 FPGA 芯片内部资源里,并完成布局布线。
综合只是说明这个逻辑可以变成电路。
实现则进一步决定:
这个电路放在 FPGA 的哪个区域;
用哪些 LUT;
用哪些触发器;
走哪些内部连线;
输出信号连到哪个 IO 管脚;
时序是否能满足。
对 LED 点亮这种简单工程来说,实现一般很容易通过。
十六、生成 Bitstream
实现成功后,点击:
Generate Bitstream
这一步会生成 .bit 文件。
.bit 文件就是最终要下载到 FPGA 里的配置文件。
它不是普通软件程序,而是 FPGA 的配置数据。
你可以理解成:
告诉 FPGA 内部资源应该怎么连接、怎么工作的一份配置文件。
十七、连接开发板并下载
开发板连接电脑后,打开:
Open Hardware Manager
然后点击:
Open Target
→ Auto Connect
如果连接成功,会看到识别出来的 FPGA 或 Zynq 设备。
然后点击:
Program Device
选择刚生成的 .bit 文件,点击 Program。
如果一切正常,LED 应该会亮或者灭。
如果 LED 没反应,先不要慌,按下面顺序检查:
1. 板子是否上电;
2. USB/JTAG 线是否正常;
3. Vivado 是否识别到设备;
4. 器件型号是否选对;
5. XDC 管脚是否写对;
6. LED 是高电平点亮还是低电平点亮;
7. 是否选错了 bit 文件;
8. 是否下载到了正确设备。
十八、点亮 LED 成功后,再做 LED 闪烁
现在我们再升级到 LED 闪烁。
LED 闪烁比 LED 点亮多了一个核心概念:
时间。
FPGA 本身不会凭空知道“一秒钟”有多长。
它只能根据时钟来计数。
假设开发板有一个 50 MHz 时钟。
50 MHz 的意思是:
每秒钟有 50,000,000 个时钟周期。
如果我们想让 LED 每 0.5 秒翻转一次,那么就需要数:
50,000,000 / 2 = 25,000,000 个时钟周期。
数到 25,000,000,就把 LED 状态取反一次。
这样 LED 就会:
亮 0.5 秒
灭 0.5 秒
亮 0.5 秒
灭 0.5 秒
看起来就是 1 秒闪烁一次。
十九、LED 闪烁代码
新建文件:
led_blink.v
代码如下:
module led_blink(
input wire clk,
input wire rst_n,
output reg led
);
parameter CLK_FREQ_HZ = 50_000_000;
parameter BLINK_HZ = 1;
localparam HALF_PERIOD_COUNT = CLK_FREQ_HZ / (BLINK_HZ * 2);
reg [31:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
counter <= 32'd0;
led <= 1'b0;
end else begin
if (counter >= HALF_PERIOD_COUNT – 1) begin
counter <= 32'd0;
led <= ~led;
end else begin
counter <= counter + 1'b1;
end
end
end
endmodule
二十、一行一行解释 LED 闪烁代码
1. 模块端口
module led_blink(
input wire clk,
input wire rst_n,
output reg led
);
这里定义了三个端口:
clk 输入时钟
rst_n 输入复位,低电平有效
led 输出 LED 控制信号
clk 是 FPGA 逻辑运行的节拍。
rst_n 是复位信号。
led 是控制 LED 的输出。
2. 参数
parameter CLK_FREQ_HZ = 50_000_000;
parameter BLINK_HZ = 1;
这里定义了两个参数。
CLK_FREQ_HZ 表示输入时钟频率。
如果你的开发板时钟是 50 MHz,就写:
50_000_000
如果是 100 MHz,就写:
100_000_000
BLINK_HZ 表示希望 LED 每秒闪几次。
3. 半周期计数值
localparam HALF_PERIOD_COUNT = CLK_FREQ_HZ / (BLINK_HZ * 2);
LED 闪烁一次包括两个阶段:
亮
灭
所以要翻转两次才算一个完整周期。
如果想 1 Hz 闪烁,且时钟是 50 MHz:
HALF_PERIOD_COUNT = 50,000,000 / 2 = 25,000,000
也就是说,每数 25,000,000 个时钟周期,LED 翻转一次。
4. 计数器
reg [31:0] counter;
这是一个 32 位计数器。
为什么要 32 位?
因为 25,000,000 这个数比较大,普通 8 位计数器最多只能数到 255,不够用。
32 位可以数到很大的数,足够这里使用。
5. 时钟触发 always 块
always @(posedge clk or negedge rst_n)
这句话表示:
当 clk 出现上升沿,或者 rst_n 出现下降沿时,执行里面的逻辑。
更准确地说,它描述的是一组触发器的行为。
posedge clk 表示时钟上升沿。
negedge rst_n 表示复位信号下降沿。
因为 rst_n 是低电平有效复位,所以写的是 negedge rst_n。
6. 复位逻辑
if (!rst_n) begin
counter <= 32'd0;
led <= 1'b0;
end
当 rst_n = 0 时,系统复位。
复位时:
counter 清零;
led 输出 0。
如果你的 LED 是低电平点亮,复位时可能 LED 会亮。
如果你的 LED 是高电平点亮,复位时 LED 会灭。
这取决于板子电路。
7. 正常计数
if (counter >= HALF_PERIOD_COUNT – 1) begin
counter <= 32'd0;
led <= ~led;
end else begin
counter <= counter + 1'b1;
end
这段逻辑意思是:
如果计数器数够了,就清零,并且 LED 翻转;
如果还没数够,就继续加 1。
~led 的意思是取反:
原来是 0,就变成 1;
原来是 1,就变成 0。
这样 LED 就会周期性变化。
二十一、LED 闪烁工程的 XDC
现在顶层模块有三个端口:
clk
rst_n
led
所以 XDC 里至少要约束这三个端口。
示例:
set_property PACKAGE_PIN <时钟管脚> [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
create_clock -period 20.000 -name sys_clk [get_ports clk]
set_property PACKAGE_PIN <复位按键管脚> [get_ports rst_n]
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]
set_property PACKAGE_PIN <LED管脚> [get_ports led]
set_property IOSTANDARD LVCMOS33 [get_ports led]
这里的管脚都必须从开发板手册或原理图里查。
二十二、create_clock 为什么必须写?
这句非常重要:
create_clock -period 20.000 -name sys_clk [get_ports clk]
它告诉 Vivado:
clk 是一个周期为 20 ns 的时钟。
周期 20 ns 对应频率 50 MHz。
换算关系是:
频率 = 1 / 周期
常见时钟对应关系:
100 MHz → 10 ns
50 MHz → 20 ns
25 MHz → 40 ns
125 MHz → 8 ns
如果你的板子系统时钟是 50 MHz,就写:
create_clock -period 20.000 -name sys_clk [get_ports clk]
如果是 100 MHz,就写:
create_clock -period 10.000 -name sys_clk [get_ports clk]
如果时钟约束写错,Vivado 的时序分析就会不准确。
二十三、如果板子没有单独复位按键怎么办?
有些开发板没有你想用的复位按键,或者你暂时不想用复位。
那可以先写一个不带复位的版本:
module led_blink(
input wire clk,
output reg led
);
parameter CLK_FREQ_HZ = 50_000_000;
parameter BLINK_HZ = 1;
localparam HALF_PERIOD_COUNT = CLK_FREQ_HZ / (BLINK_HZ * 2);
reg [31:0] counter = 32'd0;
always @(posedge clk) begin
if (counter >= HALF_PERIOD_COUNT – 1) begin
counter <= 32'd0;
led <= ~led;
end else begin
counter <= counter + 1'b1;
end
end
endmodule
对应 XDC 就只需要约束:
clk
led
也就是:
set_property PACKAGE_PIN <时钟管脚> [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
create_clock -period 20.000 -name sys_clk [get_ports clk]
set_property PACKAGE_PIN <LED管脚> [get_ports led]
set_property IOSTANDARD LVCMOS33 [get_ports led]
新手如果复位管脚不确定,可以先用这个版本。
二十四、为什么代码里端口和 XDC 必须对应?
假设 Verilog 顶层模块是:
module led_blink(
input wire clk,
output reg led
);
那么 XDC 里只能写:
[get_ports clk]
[get_ports led]
如果你写:
[get_ports sys_clk]
[get_ports led0]
Vivado 会找不到,因为顶层模块里根本没有 sys_clk 和 led0。
所以要记住:
XDC 约束的是顶层模块的端口,不是你随便起的名字。
二十五、什么时候需要仿真?
如果只是点亮 LED,仿真意义不大,因为逻辑太简单。
但从 LED 闪烁开始,就建议学习仿真。
仿真的作用是:
不用下载到开发板,就能先在电脑里观察信号变化。
例如你可以看:
clk 是否翻转;
counter 是否计数;
led 是否按预期取反;
复位是否生效。
二十六、写一个简单 testbench
新建仿真文件:
tb_led_blink.v
代码如下:
`timescale 1ns / 1ps
module tb_led_blink;
reg clk;
wire led;
led_blink #(
.CLK_FREQ_HZ(100),
.BLINK_HZ(1)
) uut (
.clk(clk),
.led(led)
);
initial begin
clk = 0;
forever #5 clk = ~clk;
end
initial begin
#1000;
$stop;
end
endmodule
这里故意把 CLK_FREQ_HZ 设置成 100,而不是 50,000,000。
原因是:
真实 50 MHz 计数太久,仿真波形要等很长时间才看到 LED 翻转。
仿真时把参数缩小,方便观察。
二十七、testbench 写完以后怎么用?
前面我们写了一个 tb_led_blink.v,但光写好文件还不够。
Vivado 不会自动知道你这个文件是用来仿真的,也不会自动帮你看波形。
接下来我们一步一步看 testbench 怎么使用。
二十八、先确认你的工程里有两个文件
做 LED 闪烁仿真时,至少应该有两个 Verilog 文件:
led_blink.v 真正要烧进 FPGA 的设计文件
tb_led_blink.v 只用于仿真的测试文件
它们的作用完全不一样。
led_blink.v 是你的真实电路设计。
以后综合、实现、生成 bitstream 时,Vivado 会用它。
tb_led_blink.v 是测试平台。
它只在电脑仿真时使用,不会烧进 FPGA。
所以在 Vivado 的 Sources 窗口里,最好是这样:
Design Sources
└── led_blink.v
Simulation Sources
└── tb_led_blink.v
Constraints
└── led_blink.xdc
新手特别容易把 tb_led_blink.v 放到 Design Sources 里,这是不对的。
testbench 应该放在 Simulation Sources 下面。
二十九、怎样把 testbench 加到 Simulation Sources?
在 Vivado 左侧 Sources 窗口中,右键点击:
Simulation Sources
然后选择:
Add Sources
弹出窗口后选择:
Add or create simulation sources
然后点击 Next。
如果你已经写好了 tb_led_blink.v,就选:
Add Files
把 tb_led_blink.v 加进去。
如果还没写,就选:
Create File
新建一个 Verilog 文件,名字写:
tb_led_blink
注意不要手动写成 tb_led_blink.v.v。
Vivado 会自动加 .v 后缀。
最后点击 Finish。
加完以后,应该能在 Sources 里看到:
Simulation Sources
└── sim_1
└── tb_led_blink
三十、确认 led_blink.v 是设计顶层
你的真实设计文件应该在:
Design Sources
里面。
例如:
module led_blink(
input wire clk,
input wire rst_n,
output reg led
);
这个 led_blink 是真正的设计模块。
在 Design Sources 里,右键 led_blink.v,选择:
Set as Top
如果 Set as Top 是灰色的,并且它已经在最上层显示,一般说明它已经是顶层了。
注意:
led_blink.v 是综合/实现/烧录用的顶层;
tb_led_blink.v 是仿真用的顶层。
这两个“顶层”不是一回事。
Vivado 在综合时会用 led_blink。
Vivado 在仿真时会用 tb_led_blink。
三十一、确认 tb_led_blink 是仿真顶层
在 Simulation Sources 里找到:
tb_led_blink.v
右键它,选择:
Set as Top
如果它已经是仿真顶层,就不用管。
为什么 testbench 要作为仿真顶层?
因为 testbench 里面没有输入输出端口,它会自己产生时钟、复位,并且例化你的 led_blink 模块。
例如:
module tb_led_blink;
reg clk;
reg rst_n;
wire led;
led_blink uut (
.clk(clk),
.rst_n(rst_n),
.led(led)
);
endmodule
这里的 uut 是:
Unit Under Test
意思是“被测试的模块”。
也就是说:
tb_led_blink 负责模拟外部环境;
led_blink 是被测试的真实电路。
三十二、带复位版本的 led_blink
如果你现在的 LED 闪烁代码带复位,可以写成这样:
module led_blink #(
parameter CLK_FREQ_HZ = 50_000_000,
parameter BLINK_HZ = 1
)(
input wire clk,
input wire rst_n,
output reg led
);
localparam HALF_PERIOD_COUNT = CLK_FREQ_HZ / (BLINK_HZ * 2);
reg [31:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
counter <= 32'd0;
led <= 1'b0;
end else begin
if (counter >= HALF_PERIOD_COUNT – 1) begin
counter <= 32'd0;
led <= ~led;
end else begin
counter <= counter + 1'b1;
end
end
end
endmodule
这里有三个端口:
clk
rst_n
led
所以 testbench 也必须连接这三个端口。
三十三、带复位版本的 testbench
对应的 tb_led_blink.v 可以这样写:
`timescale 1ns / 1ps
module tb_led_blink;
reg clk;
reg rst_n;
wire led;
led_blink #(
.CLK_FREQ_HZ(100),
.BLINK_HZ(1)
) uut (
.clk(clk),
.rst_n(rst_n),
.led(led)
);
initial begin
clk = 1'b0;
forever #5 clk = ~clk;
end
initial begin
rst_n = 1'b0; // 一开始复位
#30;
rst_n = 1'b1; // 30ns 后释放复位
#1000;
$stop;
end
endmodule
三十四、为什么仿真里 CLK_FREQ_HZ 写 100?
真实板子上的时钟可能是 50MHz。
如果真实参数写:
.CLK_FREQ_HZ(50_000_000)
LED 半秒才翻转一次。
仿真器要跑非常久,你才看到一次 LED 变化。
所以仿真时我们故意把参数改小:
.CLK_FREQ_HZ(100)
这样 LED 很快就会翻转,方便观察。
这不会影响你真正上板。
因为这是 testbench 里给仿真用的参数。
你真正烧到 FPGA 时,顶层 led_blink.v 里的默认参数仍然可以是:
50_000_000
三十五、#5 clk = ~clk 是什么意思?
testbench 里面有这一段:
initial begin
clk = 1'b0;
forever #5 clk = ~clk;
end
它的意思是:
clk 初始为 0;
每隔 5ns 翻转一次;
0 变 1,1 变 0,一直循环。
所以时钟波形是:
0ns clk = 0
5ns clk = 1
10ns clk = 0
15ns clk = 1
20ns clk = 0
因为每 5ns 翻转一次,所以完整周期是 10ns。
时钟周期 = 10ns
频率 = 100MHz
这里的 100MHz 只是仿真时钟,不一定等于板子上的真实时钟。
三十六、rst_n 为什么先是 0,后变 1?
testbench 里面有这一段:
initial begin
rst_n = 1'b0;
#30;
rst_n = 1'b1;
#1000;
$stop;
end
意思是:
仿真刚开始,rst_n = 0,系统处于复位状态;
等待 30ns;
rst_n = 1,释放复位;
继续仿真 1000ns;
停止仿真。
因为我们的设计里写的是:
if (!rst_n)
所以:
rst_n = 0 → 复位
rst_n = 1 → 正常运行
这就模拟了真实板子上按下复位键、松开复位键的过程。
三十七、运行 Behavioral Simulation
testbench 加好以后,点击 Vivado 左侧 Flow Navigator:
Simulation
→ Run Simulation
→ Run Behavioral Simulation
注意选的是:
Run Behavioral Simulation
不要选错成 Post-Synthesis Simulation 或 Post-Implementation Simulation。
新手阶段先只看行为仿真。
Behavioral Simulation 的意思是:
根据 Verilog 行为描述直接模拟电路运行,还没有经过综合和布局布线。
它主要用来检查你的逻辑思路是否正确。
三十八、打开仿真后应该看到什么?
仿真启动后,Vivado 会打开一个新界面,里面通常有几个重要窗口:
Scope
Objects
Waveform
Tcl Console
最重要的是:
Waveform
这里会显示信号波形。
你应该能看到:
clk
rst_n
led
如果没有看到 counter,不用慌。
有些内部信号不会自动显示,需要手动添加。
三十九、怎样把 counter 加到波形里?
在仿真窗口左侧找:
Scope
展开层级,通常会看到类似:
tb_led_blink
└── uut
这里的 uut 就是你例化的 led_blink。
点击 uut,然后在 Objects 窗口里应该能看到:
clk
rst_n
led
counter
选中 counter,右键:
Add to Wave Window
这样 counter 就会出现在波形窗口里。
四十、如果波形看不到变化怎么办?
有时仿真只跑了一点点时间,LED 还没来得及翻转。
你可以在 Tcl Console 里输入:
run 1000ns
或者:
run 10us
也可以点工具栏里的运行按钮。
如果想重新从 0 开始仿真,可以输入:
restart
run 1000ns
如果波形太挤,看不清,可以点击:
Zoom Fit
也就是放大镜自动适配按钮。
四十一、怎么判断仿真是对的?
你应该看到这些现象:
1. clk 一直翻转
clk 应该是规则的方波:
0 → 1 → 0 → 1 → 0 → 1
如果 clk 不动,说明 testbench 的时钟生成有问题。
2. rst_n 先低后高
一开始:
rst_n = 0
过一段时间后:
rst_n = 1
这表示系统先复位,然后释放复位。
3. counter 在复位时清零
当 rst_n = 0 时,counter 应该是:
0
释放复位后,counter 开始:
0, 1, 2, 3, 4 …
4. led 会周期性翻转
当 counter 数到设定值后,led 应该翻转:
0 → 1 → 0 → 1
因为 testbench 里我们把 CLK_FREQ_HZ 改成了 100,所以 LED 应该很快变化。
如果 led 一直不变,常见原因是:
仿真时间太短;
复位没有释放;
参数设置太大;
端口连接错;
counter 没有加入波形看不到内部变化。
四十二、为什么仿真不用 XDC?
这是一个新手常见疑问。
行为仿真主要看 Verilog 逻辑,所以它一般不关心真实 FPGA 管脚。
也就是说,仿真时不需要知道:
clk 接 U18
rst_n 接 F20
led 接 G17
这些是上板时才需要的。
XDC 主要用于:
综合
实现
时序分析
生成 bitstream
上板下载
而 testbench 是在电脑里模拟逻辑,不是真的接开发板,所以它不需要真实管脚。
四十三、仿真通过后,再去综合和烧录
如果仿真里看到:
clk 正常翻转;
rst_n 能复位;
counter 能计数;
led 能翻转;
那说明你的逻辑基本正确。
然后再回到 Vivado 主界面,按正常流程:
Run Synthesis
→ Run Implementation
→ Generate Bitstream
→ Open Hardware Manager
→ Program Device
注意:
仿真通过 ≠ 上板一定成功
仿真只能证明逻辑大概率没问题。
上板还要看:
XDC 管脚是否正确;
IOSTANDARD 是否正确;
时钟管脚是否正确;
复位按键是否接对;
LED 高低电平是否符合板子电路;
开发板是否正常连接。
四十四、仿真文件和上板文件的区别再总结一次
很多新手会把仿真和上板混在一起,所以这里再总结一下。
led_blink.v
这是设计文件。
会参与综合、实现、生成 bitstream。
最终会变成 FPGA 里的真实电路。
tb_led_blink.v
这是仿真文件。
只在电脑里模拟使用。
不会烧进 FPGA。
led_blink.xdc
这是约束文件。
告诉 Vivado 顶层端口接到哪些真实 FPGA 管脚。
所以三者关系是:
testbench 负责在电脑里“假装外部世界”
Verilog 设计文件负责描述真实电路
XDC 负责把真实电路接到开发板管脚
四十五、如果你的工程没有复位,testbench 要怎么写?
如果你的 led_blink.v 是无复位版本:
module led_blink(
input wire clk,
output reg led
);
reg [25:0] counter = 26'd0;
always @(posedge clk) begin
counter <= counter + 1'b1;
led <= counter[25];
end
endmodule
那 testbench 就不能再连接 rst_n,因为你的模块没有这个端口。
对应 testbench 应该写:
`timescale 1ns / 1ps
module tb_led_blink;
reg clk;
wire led;
led_blink uut (
.clk(clk),
.led(led)
);
initial begin
clk = 1'b0;
forever #5 clk = ~clk;
end
initial begin
#1000;
$stop;
end
endmodule
也就是说:
设计模块有什么端口,testbench 就连接什么端口。
模块没有 rst_n,testbench 就不能写:
.rst_n(rst_n)
否则 Vivado 会报端口连接错误。
四十六、最常见的 testbench 报错
1. 找不到模块 led_blink
报错类似:
module 'led_blink' not found
可能原因:
led_blink.v 没有加进工程;
模块名写错;
testbench 里例化的名字和真实模块名不一致。
如果真实模块叫:
module led_blink
testbench 里就要写:
led_blink uut (…)
不能写成:
led_blink_top uut (…)
除非你的真实模块就叫这个名字。
2. 端口不存在
报错类似:
port rst_n does not exist
原因是 testbench 连接了模块里没有的端口。
比如设计模块是:
module led_blink(
input wire clk,
output reg led
);
但 testbench 写了:
.rst_n(rst_n)
这就会报错,因为 led_blink 里面没有 rst_n 这个端口。
3. 波形里 LED 不变
常见原因:
仿真时间太短;
计数参数太大;
复位一直没有释放;
时钟没有生成;
没有把内部信号加入波形。
解决方法:
把仿真参数改小;
run 更长时间;
检查 rst_n 是否变成 1;
检查 clk 是否在翻转;
把 counter 加入波形观察。
4. testbench 被误综合了
如果你把 tb_led_blink.v 放进了 Design Sources,Vivado 可能会把它当成要烧进 FPGA 的设计。
这会导致各种奇怪问题。
解决方法:
右键 tb_led_blink.v
→ Remove File from Project
然后重新 Add Sources
→ Add or create simulation sources
把它加到 Simulation Sources。
四十七、这一节真正要掌握什么?
学 testbench 不只是为了点一下仿真按钮,而是为了建立一个重要习惯:
上板之前,先在电脑里验证逻辑。
完整流程应该是:
写 led_blink.v
→ 写 tb_led_blink.v
→ Run Behavioral Simulation
→ 看 clk、rst_n、counter、led 波形
→ 确认逻辑正确
→ Run Synthesis
→ Run Implementation
→ Generate Bitstream
→ 下载到开发板
以后你做 UART、PWM、状态机、FIFO、AXI 外设,都要用类似的思路。
先仿真,再上板。
这样调试效率会高很多。





