1. 引言
FPGA(现场可编程门阵列)凭借其可重构、低延迟、高并行的特性,在通信、图像处理、人工智能加速、汽车电子等领域应用广泛。Xilinx(现属 AMD)推出的 Vivado 设计套件,是目前业界最主流的 FPGA 开发工具之一。本文以一个完整的实战项目为主线,带你从环境搭建、代码编写、仿真验证一路走到上板调试,掌握 Vivado FPGA 开发的核心流程。
2. 开发环境准备
2.1 硬件选型
- 开发板:推荐 Xilinx Artix-7 系列(如 Nexys A7、Basys 3)或 Zynq-7000 系列,性价比高、资料丰富。
- 下载器:板载 USB-JTAG 即可,无需额外购买。
- 外设:LED、按键、拨码开关、UART 接口、HDMI 接口等,便于验证不同功能。
2.2 软件安装
- 前往 AMD/Xilinx 官网下载Vivado ML Standard Edition(免费版,支持 Artix-7 等主流器件)。
- 安装时勾选目标器件系列(如 Artix-7),并安装Vivado HL WebPACK组件。
- 安装完成后,打开 Vivado,确认 License 状态正常。
提示:安装路径不要包含中文和空格,避免后续工程路径解析出错。
3. 实战项目:LED 流水灯 + UART 回环
为了让初学者快速跑通完整流程,我们选择两个经典模块组合成一个项目:
- LED 流水灯:验证时钟分频与组合/时序逻辑。
- UART 回环:验证串口收发与状态机设计。
3.1 工程创建
- 打开 Vivado,点击Create Project。
- 输入工程名
led_uart_demo,选择存放路径。 - 选择目标器件(以 Nexys A7 的 xc7a100tcsg324-1 为例)。
- 创建完成后,在 Sources 面板中Add Sources,新建 Verilog 文件。
创建完成后,Vivado 会自动生成如下工程目录结构:
led_uart_demo/ ├── led_uart_demo.srcs/ # 源码目录(Vivado 自动生成) │ ├── sources_1/ # 设计源文件 │ │ ├── new/ # 用户新建的 RTL 文件 │ │ │ ├── top.v # 顶层模块 │ │ │ ├── uart_rx.v # UART 接收模块 │ │ │ └── uart_tx.v # UART 发送模块 │ │ └── ip/ # IP 核目录(使用 IP 时生成) │ │ └── ... # 例化的 IP 核工程 │ ├── constrs_1/ # 约束文件目录 │ │ └── new/ # 用户新建的 XDC 约束文件 │ │ └── top.xdc # 引脚与时钟约束 │ └── sim_1/ # 仿真文件目录 │ └── new/ # 用户新建的 Testbench │ └── top_tb.v # 仿真测试文件 ├── led_uart_demo.runs/ # 综合、实现、比特流生成结果 │ ├── synth_1/ # 综合结果 │ ├── impl_1/ # 实现结果 │ └── ... ├── led_uart_demo.cache/ # 工程缓存(可删除,不影响工程) ├── led_uart_demo.hw/ # 硬件管理器相关文件 ├── led_uart_demo.xpr # Vivado 工程主文件(双击打开) └── led_uart_demo.gen/ # 生成的 IP 相关文件说明:
srcs下的sources_1、constrs_1、sim_1分别对应 Vivado 的Design Sources、Constraints、Simulation Sources三个分组,后续添加文件时注意放入正确的分组。
4. 代码实现
4.1 顶层模块
module top( input wire clk, // 100MHz 系统时钟 input wire rst_n, // 低电平复位 input wire uart_rx, // 串口接收 output wire uart_tx, // 串口发送 output reg [3:0] led // 4 个 LED ); // 流水灯控制 reg [25:0] counter; always @(posedge clk or negedge rst_n) begin if (!rst_n) counter <= 26'd0; else counter <= counter + 1'b1; end always @(posedge clk or negedge rst_n) begin if (!rst_n) led <= 4'b0001; else if (counter == 26'd49_999_999) // 0.5s 翻转一次 led <= {led[2:0], led[3]}; end // UART 回环 wire [7:0] rx_data; wire rx_done; uart_rx #(.CLK_FREQ(100_000_000), .BAUD_RATE(115200)) u_rx ( .clk (clk), .rst_n (rst_n), .rx (uart_rx), .data (rx_data), .done (rx_done) ); uart_tx #(.CLK_FREQ(100_000_000), .BAUD_RATE(115200)) u_tx ( .clk (clk), .rst_n (rst_n), .data (rx_data), .send (rx_done), .tx (uart_tx) ); endmodule4.2 UART 接收模块
module uart_rx #( parameter CLK_FREQ = 100_000_000, parameter BAUD_RATE = 115200 )( input wire clk, input wire rst_n, input wire rx, output reg [7:0] data, output reg done ); localparam BAUD_CNT = CLK_FREQ / BAUD_RATE; reg [3:0] bit_index; reg [15:0] clk_cnt; reg rx_sync1, rx_sync2; reg rx_reg; reg busy; // 同步打拍,消除亚稳态 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_sync1 <= 1'b1; rx_sync2 <= 1'b1; end else begin rx_sync1 <= rx; rx_sync2 <= rx_sync1; end end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin busy <= 1'b0; clk_cnt <= 16'd0; bit_index <= 4'd0; data <= 8'd0; done <= 1'b0; rx_reg <= 1'b1; end else begin done <= 1'b0; if (!busy) begin // 检测起始位(下降沿) if (rx_sync2 == 1'b0) begin busy <= 1'b1; clk_cnt <= 16'd0; bit_index <= 4'd0; end end else begin clk_cnt <= clk_cnt + 1'b1; // 在每位中点采样 if (clk_cnt == BAUD_CNT / 2 - 1) begin rx_reg <= rx_sync2; end if (clk_cnt == BAUD_CNT - 1) begin clk_cnt <= 16'd0; if (bit_index == 0) begin // 起始位,忽略 bit_index <= bit_index + 1'b1; end else if (bit_index <= 8) begin data[bit_index - 1] <= rx_reg; bit_index <= bit_index + 1'b1; end else begin // 停止位 busy <= 1'b0; done <= 1'b1; end end end end end endmodule4.3 UART 发送模块
module uart_tx #( parameter CLK_FREQ = 100_000_000, parameter BAUD_RATE = 115200 )( input wire clk, input wire rst_n, input wire [7:0] data, input wire send, output reg tx ); localparam BAUD_CNT = CLK_FREQ / BAUD_RATE; reg [3:0] bit_index; reg [15:0] clk_cnt; reg [7:0] data_reg; reg busy; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tx <= 1'b1; busy <= 1'b0; clk_cnt <= 16'd0; bit_index <= 4'd0; data_reg <= 8'd0; end else begin if (!busy && send) begin busy <= 1'b1; data_reg <= data; bit_index <= 4'd0; tx <= 1'b0; // 起始位 end else if (busy) begin clk_cnt <= clk_cnt + 1'b1; if (clk_cnt == BAUD_CNT - 1) begin clk_cnt <= 16'd0; if (bit_index < 8) begin tx <= data_reg[bit_index]; bit_index <= bit_index + 1'b1; end else if (bit_index == 8) begin tx <= 1'b1; // 停止位 bit_index <= bit_index + 1'b1; end else begin busy <= 1'b0; end end end end end endmodule5. 约束文件与综合实现
5.1 引脚约束(XDC)
以 Nexys A7 为例,创建约束文件top.xdc:
set_property PACKAGE_PIN E3 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] create_clock -period 10.000 -name sys_clk [get_ports clk] set_property PACKAGE_PIN J15 [get_ports uart_rx] set_property IOSTANDARD LVCMOS33 [get_ports uart_rx] set_property PACKAGE_PIN K15 [get_ports uart_tx] set_property IOSTANDARD LVCMOS33 [get_ports uart_tx] set_property PACKAGE_PIN H17 [get_ports {led[0]}] set_property PACKAGE_PIN K17 [get_ports {led[1]}] set_property PACKAGE_PIN M14 [get_ports {led[2]}] set_property PACKAGE_PIN N15 [get_ports {led[3]}] set_property IOSTANDARD LVCMOS33 [get_ports {led[*]}]注意:不同开发板的引脚定义不同,请以板卡原理图为准。
5.2 综合与实现
- 点击Run Synthesis,等待综合完成。
- 综合通过后点击Run Implementation。
- 实现完成后点击Generate Bitstream,生成比特流文件。
6. 仿真验证
6.1 编写 Testbench
module top_tb; reg clk; reg rst_n; reg uart_rx; wire uart_tx; wire [3:0] led; top dut( .clk (clk), .rst_n (rst_n), .uart_rx(uart_rx), .uart_tx(uart_tx), .led (led) ); initial begin clk = 0; forever #5 clk = ~clk; // 100MHz end initial begin rst_n = 0; uart_rx = 1; #100; rst_n = 1; // 发送 0x55,验证回环 send_byte(8'h55); #2000; $finish; end task send_byte(input [7:0] byte); integer i; begin uart_rx = 0; // 起始位 #8680; for (i = 0; i < 8; i = i + 1) begin uart_rx = byte[i]; #8680; end uart_rx = 1; // 停止位 #8680; end endtask endmodule6.2 运行仿真
- 在 Sources 中右键 Testbench 文件,选择Set as Top。
- 点击Run Simulation → Run Behavioral Simulation。
- 在波形窗口中观察
led的循环移位和uart_tx的输出,确认逻辑正确。
添加内部信号到波形窗口
默认情况下,波形窗口只显示 Testbench 顶层模块的端口信号(如clk、rst_n、uart_rx、uart_tx、led)。要观察counter、rx_data、tx等内部信号,需要手动添加:
- 在仿真运行后,打开Scope面板,展开
top层级。 - 在
top下找到u_rx(UART 接收模块)和u_tx(UART 发送模块)实例。 - 展开对应实例,选中
counter、rx_data、tx、busy、bit_index等信号,右键选择Add to Wave Window。 - 也可以直接在波形窗口左侧的Name区域右键,选择Add Signals,在弹出的对话框中搜索并添加所需信号。
提示:添加内部信号后,建议点击Restart重新运行仿真,再点击Run All,这样波形会从 0 时刻开始完整记录,便于观察 UART 收发时序。
波形特征说明
下图展示了 UART 回环仿真中关键信号的典型时序(可用文字描述代替截图):
clk ─╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ uart_rx ───────╲___╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ 起始位 0x55 的 8 个数据位 停止位 rx_data ───────XXXXXXXXXXXXXXXXXXXXXXXX─────── 采样完成后变为 0x55 rx_done ───────________________________╱‾‾‾‾‾‾ rx_done 在停止位采样后拉高一拍 uart_tx ───────╲___╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ 回环发送同样的 0x55 帧从波形中可以直观看到:
- 起始位:
uart_rx从高电平拉低,标志一帧数据的开始。 - 数据位:
uart_rx依次呈现 0x55 的 8 个比特(LSB 在前),rx_data在采样完成后更新为0x55。 - 停止位:
uart_rx恢复高电平,rx_done拉高一拍,触发uart_tx回环发送。 - 回环输出:
uart_tx输出与uart_rx相同的帧格式,验证收发链路正确。
7. 上板调试
- 连接开发板 USB 线,点击Program Device,选择生成的
.bit文件下载。 - 观察 LED 是否按 0.5s 间隔循环点亮。
- 打开串口助手(如 SSCOM、PuTTY),设置波特率 115200、8 数据位、无校验、1 停止位。
- 发送任意字符,观察串口是否原样回显,验证 UART 回环功能。
8. 常见问题与排查
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
综合报错[Synth 8-3331] | 端口未连接或位宽不匹配 | 检查顶层模块端口与子模块例化 |
| 上板后 LED 不亮 | 引脚约束错误或时钟未约束 | 核对 XDC 引脚,添加create_clock |
| 串口乱码 | 波特率不匹配或时钟频率错误 | 核对CLK_FREQ与BAUD_RATE参数 |
| 时序收敛失败 | 时钟频率过高或逻辑过长 | 降低频率、增加流水线级数 |
综合报错[Synth 8-3331]日志示例
当顶层模块端口与子模块例化不匹配时,Vivado 综合阶段会报出类似下面的错误日志:
[Synth 8-3331] design "top" has unconnected port "rx_data" [Synth 8-3331] design "top" has unconnected port "rx_done" [Synth 8-3331] design "top" has unconnected port "led" ERROR: [Synth 8-3331] design "top" has unconnected port "uart_tx"关键错误行定位:[Synth 8-3331] design "top" has unconnected port "xxx"中的xxx就是未连接的端口名。例如报错指向uart_tx,说明顶层模块的uart_tx输出端口没有正确连接到任何逻辑。
修改步骤:
- 打开
top.v,检查顶层模块端口声明是否完整,确认uart_tx、led等端口都已声明为output。 - 检查子模块例化部分,确认
u_rx的.done(rx_done)、.data(rx_data)以及u_tx的.tx(uart_tx)等连接是否一一对应。 - 若端口位宽不匹配(如
[7:0]与[3:0]),需统一位宽声明,例如将rx_data声明为wire [7:0]。 - 修改后重新点击Run Synthesis,观察日志中是否还有
[Synth 8-3331]报错。
时序收敛失败日志示例
当设计时序无法满足约束时,实现阶段会报出类似下面的时序错误日志:
[Vivado 12-501] Cannot meet timing constraints [Vivado 12-502] 1 timing constraint is not met [Vivado 12-503] Worst slack: -0.456 ns [Vivado 12-504] Failing endpoint: u_rx/clk_cnt_reg[15]/C [Vivado 12-505] Failing path: clk (rise) to u_rx/clk_cnt_reg[15]/D关键错误行定位:[Vivado 12-503] Worst slack: -0.456 ns中的负值-0.456 ns表示时序违例的严重程度;[Vivado 12-504] Failing endpoint: u_rx/clk_cnt_reg[15]/C则直接指出违例路径的终点寄存器,这里是 UART 接收模块中的clk_cnt计数器第 15 位。
修改步骤:
- 打开Implementation → Report Timing Summary,查看具体违例路径的起点与终点。
- 若违例路径集中在
clk_cnt等计数器上,说明组合逻辑过长,可尝试将计数器拆分为多级流水线,或在关键路径上插入寄存器打拍。 - 检查时钟约束是否正确,确认
create_clock -period 10.000与板卡实际时钟频率一致。 - 若仍无法收敛,可适当降低时钟频率(如将 100 MHz 改为 50 MHz),或优化 UART 模块中的组合逻辑,减少路径延迟。
- 修改后重新运行Run Implementation,确认
Worst slack变为正值。
9. 进阶方向
完成本项目后,你可以继续探索以下方向:
- 图像处理:基于 HDMI 接口实现摄像头采集与 VGA 显示。
- 高速接口:学习 PCIe、Ethernet、DDR3/DDR4 控制器设计。
- 片上系统:使用 Zynq 平台搭建 ARM + FPGA 异构系统。
- HLS 开发:用 C/C++ 描述算法,通过 Vivado HLS 自动生成 RTL。
实战项目预告:基于 Zynq 的 AXI-Lite 接口自定义 IP 设计
下一篇教程将带你实现一个更进阶的实战项目——基于 Zynq 的 AXI-Lite 接口自定义 IP 设计。该项目将把 FPGA 逻辑封装成标准 AXI 外设,挂载到 Zynq 的 PS(处理系统)总线上,实现 ARM 与 PL(可编程逻辑)之间的数据交互。
学习目标:
- 掌握 AXI 总线协议:理解 AXI-Lite 的读/写通道、握手信号与地址映射机制。
- 掌握 IP 封装流程:学会使用 Vivado IP Packager 将自定义 RTL 封装为可复用的 IP 核。
- 掌握 SDK 驱动开发:在 Xilinx SDK 中编写 C 语言驱动,通过 AXI 寄存器控制 PL 侧逻辑。
预备知识点(建议提前预习):
- AXI-Lite 信号时序:熟悉
AWVALID/AWREADY、WVALID/WREADY、BVALID/BREADY、ARVALID/ARREADY、RVALID/RREADY等握手信号的时序关系。 - Vivado IP Packager 使用:了解如何创建 IP 工程、配置接口、打包并添加到 Block Design。
- Xilinx SDK 基础操作:掌握新建应用工程、编写裸机程序、下载调试的基本流程。
建议在阅读下一篇教程前,先自行查阅 AXI 协议白皮书(如 AMBA AXI4-Lite 规范)和 Vivado 官方 IP 封装文档,打好基础。
对比方案:状态机 vs 计数器实现流水灯
在 4.1 节中,我们使用计数器实现流水灯:通过一个 26 位计数器分频产生 0.5 s 的节拍,再对led做循环移位。这是最直观、最省资源的方式。除此之外,还可以用状态机来描述流水灯的各个状态,两种设计风格各有优劣。
| 对比维度 | 计数器实现 | 状态机实现 |
|---|---|---|
| 资源占用 | 低,仅需 1 个计数器 + 移位寄存器 | 略高,需要状态寄存器 + 状态译码逻辑 |
| 可读性 | 代码简洁,逻辑直观,适合简单流水 | 状态清晰,便于表达复杂时序与多模式切换 |
| 扩展性 | 扩展多模式(如变速、反向)需额外加判断 | 天然适合扩展,新增状态即可增加功能 |
| 适用场景 | 简单循环、固定节拍的流水灯 | 需要多状态切换、按键控制、模式选择的场景 |
下面给出状态机实现流水灯的核心 Verilog 代码片段(约 20 行),帮助读者理解不同设计风格:
// 状态机实现流水灯:4 个状态循环移位 reg [1:0] state, next_state; localparam S0 = 2'd0, S1 = 2'd1, S2 = 2'd2, S3 = 2'd3; always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= S0; else state <= next_state; end always @(*) begin case (state) S0: next_state = S1; S1: next_state = S2; S2: next_state = S3; S3: next_state = S0; default: next_state = S0; endcase end always @(posedge clk or negedge rst_n) begin if (!rst_n) led <= 4'b0001; else begin case (state) S0: led <= 4'b0001; S1: led <= 4'b0010; S2: led <= 4'b0100; S3: led <= 4'b1000; endcase end end可以看到,状态机把「当前亮哪个灯」抽象为独立状态,后续若要加入按键切换方向、调节速度等需求,只需在状态转移逻辑中增加分支即可,扩展性明显优于计数器方案。对于本项目这种固定节拍的简单流水灯,计数器实现更简洁高效;而当你需要设计更复杂的控制逻辑时,状态机是更合适的选择。
10. 参考资料
以下是本教程涉及的核心官方文档与协议规范,建议在开发过程中按需查阅:
- AMD/Xilinx Vivado 官方用户指南(UG910、UG892):分别介绍 Vivado 的工程创建、综合、实现与仿真流程,是使用 Vivado 进行 FPGA 开发的基础参考。
- Nexys A7 板卡参考手册(含引脚定义):提供板载外设的引脚映射、时钟与接口说明,编写 XDC 约束文件时需对照此手册。
- AMBA AXI4-Lite 协议规范白皮书:定义 AXI-Lite 总线的读/写通道、握手信号与地址映射机制,是学习 Zynq 自定义 IP 设计的前置资料。
- Xilinx UG949(Vivado 设计流程指南):系统讲解从 RTL 到比特流的完整设计方法论,涵盖时序约束、功耗优化与调试技巧。
- UART 串口通信协议标准文档:说明异步串口的帧格式、波特率与起始/停止位定义,是理解本教程 UART 收发模块设计的基础。
11. 总结
下篇预告:基于 Zynq 的 AXI-Lite 接口自定义 IP 设计
恭喜你完成了「LED 流水灯 + UART 回环」这个入门实战项目!在下一篇教程中,我们将进入更进阶的领域——基于 Zynq 的 AXI-Lite 接口自定义 IP 设计。
核心内容:该项目将把 FPGA 逻辑封装成标准 AXI 外设,挂载到 Zynq 的 PS(处理系统)总线上,实现 ARM 与 PL(可编程逻辑)之间的数据交互。你将学会使用 Vivado IP Packager 将自定义 RTL 封装为可复用的 IP 核,并在 Xilinx SDK 中编写 C 语言驱动,通过 AXI 寄存器控制 PL 侧逻辑。
建议提前掌握的知识点:
- AXI-Lite 信号时序:熟悉
AWVALID/AWREADY、WVALID/WREADY、BVALID/BREADY、ARVALID/ARREADY、RVALID/RREADY等握手信号的时序关系。 - Vivado IP Packager 使用:了解如何创建 IP 工程、配置接口、打包并添加到 Block Design。
- Xilinx SDK 基础操作:掌握新建应用工程、编写裸机程序、下载调试的基本流程。
建议在阅读下一篇教程前,先自行查阅 AXI 协议白皮书(如 AMBA AXI4-Lite 规范)和 Vivado 官方 IP 封装文档,打好基础,届时学习会更顺畅。
10. 总结
本文通过一个「LED 流水灯 + UART 回环」的完整实战项目,带你走通了 Vivado FPGA 开发的全部核心流程:环境搭建、工程创建、RTL 设计、约束编写、仿真验证、综合实现与上板调试。掌握这套流程后,你就能在此基础上扩展出更复杂的数字系统设计。建议动手把每个步骤都亲自跑一遍,遇到问题多查日志、多看波形,这是提升 FPGA 开发能力最有效的方式。