本文是《FPGA入门到实战》专栏第11篇。上一篇介绍了建立时间、保持时间和 XDC 约束,本篇进入板级调试:当仿真结果正确,但程序下载到 FPGA 后仍然运行异常时,如何使用 Vivado ILA 观察芯片内部信号、设置触发条件并定位问题。读完本篇,你将能独立完成"添加调试信号 → 生成比特流 → 连接硬件 → 触发采样 → 分析波形"的完整流程。
1. 为什么需要 ILA
FPGA 中的大多数信号都位于芯片内部,既没有连接到 LED,也没有连接到示波器探针。仿真阶段可以观察任意信号,但实际下板后,真实时钟、外设时序、跨时钟域和板级连接都会引入新的问题。
常见现象包括:
- 仿真波形正确,下载后 LED 或外设没有反应
- UART、SPI 等接口偶发丢数,但无法稳定复现
- 状态机进入异常状态,不知道由哪个输入触发
- FIFO 出现
full或empty异常 - 跨时钟域信号偶发错误,普通功能仿真无法暴露
- 计数器运行了,但输出值与预期不一致
传统方法是把内部信号临时连接到 IO,再用示波器或逻辑分析仪观察。这种方法会占用引脚,而且能观察的信号数量很少。ILA 可以直接采样 FPGA 内部信号,并通过 JTAG 将采样数据传回 Vivado。
ILA 的完整名称是 Integrated Logic Analyzer,即集成逻辑分析仪。可以把它理解成放在 FPGA 芯片内部的逻辑分析仪:
text
用户逻辑 ──→ ILA 采样存储 ──→ JTAG ──→ Vivado Hardware Manager
↑
触发条件与采样时钟
ILA 主要由三部分组成:
- 采样时钟:决定何时采集 Probe 信号
- Probe 探针:连接需要观察的内部信号
- 触发逻辑与采样存储器:满足条件时保存触发点前后的波形
2. ILA 与仿真、示波器的区别
| 调试方式 | 观察对象 | 优点 | 局限 |
|---|---|---|---|
| 行为仿真 | RTL 信号 | 信号全部可见、可重复、定位方便 | 不能反映真实板级环境 |
| ILA | FPGA 内部数字信号 | 不占用普通 IO,可观察状态机、计数器和总线 | 会消耗 BRAM/LUT,只能在采样时钟边沿观察 |
| 示波器 | 外部模拟/数字波形 | 能观察电压、上升时间、抖动和信号完整性 | 难以观察芯片内部状态 |
| 外部逻辑分析仪 | 板级数字接口 | 适合观察 SPI、UART 等外部总线 | 需要引出测试点,通道数量有限 |
工程中通常不是四选一,而是组合使用:
- 先通过仿真验证 RTL 功能
- 下板后用 ILA 检查内部数据流和控制状态
- 若怀疑电气问题,再使用示波器观察外部波形
ILA 只能看到采样时钟边沿捕获到的数字值,不能替代示波器分析电压幅度、边沿质量和模拟抖动。
3. 本篇实操目标
下面使用一个带故障注入输入的计数器作为示例。模块正常工作时不断计数;当 fault_in 有效时,计数器错误地清零。我们要使用 ILA 捕获清零瞬间,并观察触发前后的 counter、enable 和 fault_in。
3.1 示例模块
verilog
module ila_counter_demo (
input wire clk , // 系统时钟
input wire rstn , // 低有效复位
input wire enable , // 计数使能
input wire fault_in, // 故障注入
output wire [31:0] count // 当前计数值
);
(* MARK_DEBUG = "TRUE" *) reg [31:0] counter;
(* MARK_DEBUG = "TRUE" *) wire enable_debug;
(* MARK_DEBUG = "TRUE" *) wire fault_debug;
assign enable_debug = enable;
assign fault_debug = fault_in;
assign count = counter;
always @(posedge clk or negedge rstn)
begin
if (rstn == 1'b0)
begin
counter <= 32'd0;
end
else if (fault_in == 1'b1)
begin
counter <= 32'd0;
end
else if (enable == 1'b1)
begin
counter <= counter + 32'd1;
end
else
begin
counter <= counter;
end
end
endmodule
代码中的 MARK_DEBUG 属性告诉 Vivado:综合时保留这些网络,并把它们作为候选调试信号。属性不会改变原有逻辑功能,但会影响优化结果和资源占用,因此只应标记真正需要观察的信号。
3.2 为什么不直接观察所有信号
ILA 并不是"信号越多越好"。每增加一个 Probe 位宽、每提高一档采样深度,都会增加片上存储资源消耗。
例如观察以下信号:
text
counter 32 bit
state 4 bit
data_valid 1 bit
data_ready 1 bit
总位宽 38 bit
若采样深度为 4096,至少需要保存 38 × 4096 位数据,还要加上触发和控制逻辑。大型工程中若一次标记数百个信号,很容易明显增加 BRAM 占用并影响布局布线。
正确做法是围绕问题建立"最小观察集合":
- 一个表示数据本身的信号
- 一个或两个握手/使能信号
- 当前状态或错误标志
- 能够精确复现问题的触发信号
4. 添加 ILA 的三种方法
Vivado 中常用的 ILA 添加方式有三种。
4.1 使用 MARK_DEBUG 属性
直接在 RTL 中标记信号:
verilog
(* MARK_DEBUG = "TRUE" *) wire [7:0] rx_data;
(* MARK_DEBUG = "TRUE" *) wire rx_valid;
完成综合后,打开综合设计并执行 Set Up Debug,Vivado 会列出这些信号,然后自动插入调试核。
这种方式的优点:
- 调试信号与源码对应关系清晰
- 重新综合后不容易丢失
- 适合长期保留的关键状态信号
需要注意:不要在不需要调试时长期保留大量 MARK_DEBUG,否则可能限制综合器优化。
4.2 在综合后设计中选择信号
不修改 RTL,执行以下步骤:
- 运行综合
- 点击 Open Synthesized Design
- 在 Netlist 或 Schematic 中找到目标网络
- 右键选择 Mark Debug
- 点击 Set Up Debug 完成探针配置
这种方式适合临时排查问题,但工程重新综合或网络名称改变后,需要重新确认探针。
4.3 手动例化 ILA IP 核
在 IP Catalog 中搜索 ILA,配置探针数量、位宽和采样深度,然后像普通模块一样例化:
verilog
ila_0 u_ila (
.clk (clk ),
.probe0 (counter ),
.probe1 (enable ),
.probe2 (fault_in )
);
手动例化的优势是接口固定、行为明确,适合调试结构长期保留在工程中的情况;缺点是每次改变探针位宽都可能需要重新定制 IP。
对于初学者,本篇推荐使用 MARK_DEBUG + Set Up Debug,操作直观且不需要手动管理 ILA 端口。
5. 使用 Set Up Debug 插入 ILA
5.1 运行综合
点击左侧 Flow Navigator 中的:
text
SYNTHESIS → Run Synthesis → Open Synthesized Design
综合完成后,在 Netlist 窗口中可以看到 counter、enable_debug 和 fault_debug。如果源码已添加 MARK_DEBUG,这些信号通常会带有调试标记。
5.2 打开 Set Up Debug
在综合设计界面点击:
text
Tools → Set Up Debug
向导会显示候选网络。确认需要观察的信号已经加入 Probe 列表:
| Probe | 位宽 | 用途 |
|---|---|---|
counter[31:0] |
32 | 观察计数值 |
enable_debug |
1 | 判断计数是否允许 |
fault_debug |
1 | 作为故障触发条件 |
5.3 选择采样时钟
ILA 的采样时钟必须满足以下要求:
- 时钟稳定且持续运行
- 时钟频率位于器件和设计允许范围内
- 被观察信号最好与采样时钟处于同一时钟域
- 触发前采样时钟已经启动
本例所有信号都在 clk 时钟域,因此选择 clk 作为 ILA 时钟。
如果用 A 时钟采样 B 时钟域信号,波形可能漏掉窄脉冲,也可能在时钟域交界处采到不稳定值。多时钟域工程通常需要为不同域分别设置 ILA,或先通过 CDC 电路把关键事件同步到采样域。
5.4 设置采样深度
常见采样深度包括 1024、2048、4096、8192 等。深度越大,可观察的时间窗口越长,但会消耗更多 BRAM。
假设采样时钟是 100 MHz:
| 采样深度 | 可观察总时间 |
|---|---|
| 1024 | 10.24 μs |
| 4096 | 40.96 μs |
| 16384 | 163.84 μs |
首次调试建议从 1024 或 4096 开始。如果目标事件间隔很长,不应盲目把深度调到最大,而应设计一个事件标志作为触发条件,只捕获问题附近的波形。
完成向导后保存约束,Vivado 会插入 ILA 调试核。随后执行:
text
Run Implementation → Generate Bitstream
6. 连接开发板并下载
6.1 打开 Hardware Manager
比特流生成完成后点击:
text
Open Hardware Manager → Open Target → Auto Connect
连接成功后,Hardware 窗口会显示 JTAG 链中的 FPGA 器件。
6.2 下载 BIT 与 LTX 文件
包含 ILA 的工程通常会生成两类关键文件:
.bit:FPGA 配置数据.ltx:调试探针与内部网络的映射信息
正常情况下 Vivado 会自动关联两者。如果下载后找不到 ILA,重点检查:
- 当前
.bit是否由包含 ILA 的最新工程生成 .ltx是否与.bit同一次生成- 是否误用了旧目录中的比特流
- ILA 的采样时钟是否正在运行
选择器件并点击 Program Device 完成下载。下载后 Hardware 窗口中应出现类似 hw_ila_1 的调试核。
7. 设置触发条件并采集波形
7.1 立即触发
第一次连接 ILA 时,可以先不设置条件,点击 Run Trigger Immediate 立即采样。这样可以确认:
- ILA 核工作正常
- 采样时钟正在运行
- Probe 信号不是全部固定为 X 或常量
counter在持续变化
7.2 条件触发
为了捕获故障发生瞬间,把 fault_debug 设置为:
text
fault_debug == 1
然后点击 Run Trigger 。ILA 进入等待状态,当 fault_in 变为高电平时触发并返回波形。
波形中应看到:
- 触发前
counter持续递增 fault_debug在触发点变为 1- 下一个有效时钟沿后
counter清零 fault_debug恢复后,若enable_debug为 1,计数器重新计数
7.3 调整触发位置
触发位置决定采样缓存中"触发前"和"触发后"各保留多少数据。
- 触发点放在中间:同时观察原因和结果,最常用
- 触发点靠后:保留更多触发前历史,适合追溯错误来源
- 触发点靠前:保留更多触发后波形,适合观察后续恢复过程
如果只看到故障结果,却看不到故障产生前的状态,应把触发位置向后移动,增加触发前采样比例。
7.4 总线显示格式
对于计数器、地址、数据等多位信号,可以在波形窗口中修改显示进制:
- Binary:适合观察单 bit 和状态标志
- Hexadecimal:适合观察总线和寄存器
- Unsigned Decimal:适合观察计数值
- Signed Decimal:适合观察补码数据
同一个 32'hFFFF_FFFF,按无符号显示是 4294967295,按有符号显示是 -1。分析算法数据时必须选择正确的显示格式。
8. 用 ILA 定位常见问题
8.1 状态机卡死
建议观察:
current_state- 关键输入条件
- 超时计数器
- 握手信号
valid/ready
触发条件可以设置为非法状态,或设置为某个状态持续时间超过阈值后产生的 timeout 标志。
不要只观察 current_state。如果没有同时观察状态转移条件,只能知道"卡在哪里",无法知道"为什么没有离开"。
8.2 UART 接收错误
建议观察:
- 同步后的
uart_rx - 波特率采样使能
- 位计数器
- 接收移位寄存器
rx_valid
触发条件可以设为 rx_valid == 1,或者在检测到起始位后触发。若采样深度有限,应优先捕获一帧完整数据,而不是长期无条件采样。
8.3 FIFO 溢出或读空
建议观察:
wr_en、rd_enfull、empty- 数据计数值
- 上游
valid/ready
典型触发条件:
text
(wr_en == 1) && (full == 1)
或者:
text
(rd_en == 1) && (empty == 1)
这样可以直接捕获违规读写发生前后的数据流状态。
8.4 AXI-Stream 数据停滞
AXI-Stream 发生一次有效传输的条件是:
text
tvalid == 1 && tready == 1
若数据链路停滞,需要同时观察 tvalid、tready、tdata 和上游/下游状态机。只看 tdata 是否变化无法判断数据是否真正完成传输。
9. VIO:在 Vivado 中控制内部信号
ILA 用于观察,VIO(Virtual Input/Output)用于在 Hardware Manager 中读写内部信号。
典型用途:
- 手动产生模块启动脉冲
- 在线修改测试模式
- 控制内部复位
- 修改测试数据或阈值
- 读取少量状态信号
例如将 VIO 输出连接到测试使能:
verilog
wire test_enable;
wire clear_error;
vio_0 u_vio (
.clk (clk ),
.probe_in0 (error_flag ),
.probe_out0 (test_enable),
.probe_out1 (clear_error)
);
VIO 非常适合调试,但不要让调试核成为正式产品运行所必需的控制接口。量产设计应由寄存器、外部接口或软件驱动完成正常控制。
10. 常见问题与排查方法
10.1 下载后看不到 ILA
依次检查:
- 是否重新运行了综合、实现和比特流生成
- 下载的是否为最新
.bit .ltx是否与.bit匹配- Set Up Debug 是否真正保存并写入工程
- JTAG 是否连接到正确器件
10.2 ILA 一直等待,无法触发
可能原因:
- 触发条件根本不会发生
- 探针位宽或进制设置错误
- 触发信号位于其他时钟域,窄脉冲被漏采
- 被测模块仍处于复位状态
- 采样时钟没有运行
先使用立即触发确认时钟和探针正常,再逐步增加触发条件。不要一开始就设置多个复杂条件。
10.3 波形全部为 0
重点检查:
- 模块时钟是否正常
- 复位是否一直有效
- Probe 是否连接到综合后的正确网络
- 输入信号是否真的进入 FPGA
- 管脚约束和 IO 电平标准是否正确
10.4 加入 ILA 后时序变差
ILA 会增加信号扇出、布线压力和 BRAM 使用。处理方法:
- 减少 Probe 数量和位宽
- 降低采样深度
- 不要跨很远区域抓取大量信号
- 为不同模块分别建立小型 ILA
- 调试结束后移除不需要的调试核
- 重新检查 Timing Summary,不能因为"只是调试"就忽略时序违规
10.5 重新综合后探针消失
综合器可能优化、合并或重命名内部网络。对必须稳定保留的信号使用 MARK_DEBUG;对组合表达式可先增加一个有明确名称的中间信号,再标记该信号。
11. 工程调试建议
11.1 先提出问题,再选择信号
错误方式:看到什么信号就全部加入 ILA。
正确方式:先写出需要验证的判断,例如:
UART 丢数是否由 FIFO 满导致?
围绕这个问题,只观察 UART 接收有效、FIFO 写使能、FIFO 满标志和数据计数值。观察集合越聚焦,波形越容易解释。
11.2 给异常建立专用触发标志
若异常条件很复杂,可以在 RTL 中生成一个单 bit 调试标志:
verilog
wire fifo_write_error;
assign fifo_write_error = fifo_wr_en & fifo_full;
ILA 直接对 fifo_write_error == 1 触发,比在界面中配置多个条件更清晰,也方便后续复用。
11.3 记录调试配置
建议在调试记录中写清楚:
- 使用的比特流时间和 Git 提交
- Probe 列表与位宽
- 采样时钟和采样深度
- 触发条件与触发位置
- 复现步骤
- 波形结论
否则几天后即使再次捕获到相同波形,也可能无法确认它对应哪一版逻辑。
11.4 调试完成后重新验证
移除 ILA 可能改变布局布线结果。正式交付前应:
- 移除不需要的调试信号
- 重新综合和实现
- 确认时序收敛
- 重新执行板级功能测试
12. 本篇总结
本篇完成了 Vivado ILA 在线调试的完整流程:
- 通过
MARK_DEBUG或手动例化选择内部信号 - 在综合后设计中使用 Set Up Debug 插入 ILA
- 选择稳定的采样时钟并合理设置采样深度
- 生成并下载匹配的
.bit与.ltx文件 - 使用立即触发验证链路,再使用条件触发捕获异常
- 根据状态、数据和握手信号分析问题原因
- 调试完成后移除多余探针并重新进行时序验证
ILA 最重要的能力不是"看到更多信号",而是在正确的时刻捕获与问题有关的最小信号集合。当你能够根据故障现象设计触发条件时,FPGA 板级调试就不再是反复猜测和重新烧录,而会变成一套可以验证、可以复现的工程流程。
下一篇将进入按键消抖实战,从机械按键的物理抖动开始,完成同步、滤波、边沿检测与仿真验证。
参考资料
- AMD Vivado Design Suite User Guide: Programming and Debugging(UG908)
- AMD Vivado Design Suite User Guide: Using Constraints(UG903)