☰
Vivado FPGA 开发实战项目:从入门到上板
2026/10/21 2:28:50 网站建设 项目流程

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 软件安装

  1. 前往 AMD/Xilinx 官网下载Vivado ML Standard Edition(免费版,支持 Artix-7 等主流器件)。
  2. 安装时勾选目标器件系列(如 Artix-7),并安装Vivado HL WebPACK组件。
  3. 安装完成后,打开 Vivado,确认 License 状态正常。

提示:安装路径不要包含中文和空格,避免后续工程路径解析出错。

3. 实战项目:LED 流水灯 + UART 回环

为了让初学者快速跑通完整流程,我们选择两个经典模块组合成一个项目:

  • LED 流水灯:验证时钟分频与组合/时序逻辑。
  • UART 回环:验证串口收发与状态机设计。

3.1 工程创建

  1. 打开 Vivado,点击Create Project。
  2. 输入工程名led_uart_demo,选择存放路径。
  3. 选择目标器件(以 Nexys A7 的 xc7a100tcsg324-1 为例)。
  4. 创建完成后,在 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) ); endmodule

4.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 endmodule

4.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 endmodule

5. 约束文件与综合实现

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 综合与实现

  1. 点击Run Synthesis,等待综合完成。
  2. 综合通过后点击Run Implementation。
  3. 实现完成后点击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 endmodule

6.2 运行仿真

  1. 在 Sources 中右键 Testbench 文件,选择Set as Top。
  2. 点击Run Simulation → Run Behavioral Simulation。
  3. 在波形窗口中观察led的循环移位和uart_tx的输出,确认逻辑正确。
添加内部信号到波形窗口

默认情况下,波形窗口只显示 Testbench 顶层模块的端口信号(如clk、rst_n、uart_rx、uart_tx、led)。要观察counter、rx_data、tx等内部信号,需要手动添加:

  1. 在仿真运行后,打开Scope面板,展开top层级。
  2. 在top下找到u_rx(UART 接收模块)和u_tx(UART 发送模块)实例。
  3. 展开对应实例,选中counter、rx_data、tx、busy、bit_index等信号,右键选择Add to Wave Window。
  4. 也可以直接在波形窗口左侧的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. 上板调试

  1. 连接开发板 USB 线,点击Program Device,选择生成的.bit文件下载。
  2. 观察 LED 是否按 0.5s 间隔循环点亮。
  3. 打开串口助手(如 SSCOM、PuTTY),设置波特率 115200、8 数据位、无校验、1 停止位。
  4. 发送任意字符,观察串口是否原样回显,验证 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输出端口没有正确连接到任何逻辑。

修改步骤:

  1. 打开top.v,检查顶层模块端口声明是否完整,确认uart_tx、led等端口都已声明为output。
  2. 检查子模块例化部分,确认u_rx的.done(rx_done)、.data(rx_data)以及u_tx的.tx(uart_tx)等连接是否一一对应。
  3. 若端口位宽不匹配(如[7:0]与[3:0]),需统一位宽声明,例如将rx_data声明为wire [7:0]。
  4. 修改后重新点击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 位。

修改步骤:

  1. 打开Implementation → Report Timing Summary,查看具体违例路径的起点与终点。
  2. 若违例路径集中在clk_cnt等计数器上,说明组合逻辑过长,可尝试将计数器拆分为多级流水线,或在关键路径上插入寄存器打拍。
  3. 检查时钟约束是否正确,确认create_clock -period 10.000与板卡实际时钟频率一致。
  4. 若仍无法收敛,可适当降低时钟频率(如将 100 MHz 改为 50 MHz),或优化 UART 模块中的组合逻辑,减少路径延迟。
  5. 修改后重新运行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 侧逻辑。

预备知识点(建议提前预习):

  1. AXI-Lite 信号时序:熟悉AWVALID/AWREADY、WVALID/WREADY、BVALID/BREADY、ARVALID/ARREADY、RVALID/RREADY等握手信号的时序关系。
  2. Vivado IP Packager 使用:了解如何创建 IP 工程、配置接口、打包并添加到 Block Design。
  3. 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. 参考资料

以下是本教程涉及的核心官方文档与协议规范,建议在开发过程中按需查阅:

  1. AMD/Xilinx Vivado 官方用户指南(UG910、UG892):分别介绍 Vivado 的工程创建、综合、实现与仿真流程,是使用 Vivado 进行 FPGA 开发的基础参考。
  2. Nexys A7 板卡参考手册(含引脚定义):提供板载外设的引脚映射、时钟与接口说明,编写 XDC 约束文件时需对照此手册。
  3. AMBA AXI4-Lite 协议规范白皮书:定义 AXI-Lite 总线的读/写通道、握手信号与地址映射机制,是学习 Zynq 自定义 IP 设计的前置资料。
  4. Xilinx UG949(Vivado 设计流程指南):系统讲解从 RTL 到比特流的完整设计方法论,涵盖时序约束、功耗优化与调试技巧。
  5. 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 侧逻辑。

建议提前掌握的知识点:

  1. AXI-Lite 信号时序:熟悉AWVALID/AWREADY、WVALID/WREADY、BVALID/BREADY、ARVALID/ARREADY、RVALID/RREADY等握手信号的时序关系。
  2. Vivado IP Packager 使用:了解如何创建 IP 工程、配置接口、打包并添加到 Block Design。
  3. Xilinx SDK 基础操作:掌握新建应用工程、编写裸机程序、下载调试的基本流程。

建议在阅读下一篇教程前,先自行查阅 AXI 协议白皮书(如 AMBA AXI4-Lite 规范)和 Vivado 官方 IP 封装文档,打好基础,届时学习会更顺畅。

10. 总结

本文通过一个「LED 流水灯 + UART 回环」的完整实战项目,带你走通了 Vivado FPGA 开发的全部核心流程:环境搭建、工程创建、RTL 设计、约束编写、仿真验证、综合实现与上板调试。掌握这套流程后,你就能在此基础上扩展出更复杂的数字系统设计。建议动手把每个步骤都亲自跑一遍,遇到问题多查日志、多看波形,这是提升 FPGA 开发能力最有效的方式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询