FPGA 调试实录:LED 闪烁程序从踩坑到仿真验证的完整经验总结

FPGA 调试实录:LED 闪烁程序从踩坑到仿真验证的完整经验总结

从"灯闪得不对"到"仿真没波形",这篇文章记录了一个 FPGA 新手在 LED 工程里踩过的坑。文章按「先给完整代码 → 再逐行解释 → 最后集中回答重点问题」的顺序组织,方便查阅。


一、最终代码

1.1 LED.v

verilog 复制代码
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Module Name: LED
// Description: 两个 LED 每秒交替闪烁(200MHz 时钟)
//              带 CNT_MAX 参数,仿真时可覆盖成小值
// Target:      Xilinx Artix-7 XC7A35T-FGG484
//////////////////////////////////////////////////////////////////////////////////

module LED #(
    parameter CNT_MAX = 200000000    // 默认 2 亿(1 秒 @ 200MHz)
)(
    input  sys_clk_p,
    input  sys_clk_n,
    input  sys_rstn,
    output reg [1:0] led
);

    // ===== 差分时钟转单端 =====
    wire clk;

`ifdef SIMULATION
    // 仿真时:直接用 sys_clk_p 当时钟,绕开 IBUFDS
    assign clk = sys_clk_p;
`else
    // 综合时:用 IBUFDS 转差分时钟
    IBUFDS #(
        .DIFF_TERM("FALSE"),
        .IBUF_LOW_PWR("TRUE"),
        .IOSTANDARD("DEFAULT")
    ) IBUFDS_inst (
        .O (clk),
        .I (sys_clk_p),
        .IB(sys_clk_n)
    );
`endif

    // ===== 计数器 =====
    reg [31:0] cnt;
    always @(posedge clk) begin
        if(!sys_rstn)
            cnt <= 0;
        else if(cnt < CNT_MAX - 1)
            cnt <= cnt + 1;
        else
            cnt <= 0;
    end

    // ===== LED 状态切换 =====
    reg i;
    always @(posedge clk) begin
        if(!sys_rstn) begin
            led <= 2'b01;
            i   <= 0;
        end
        else if(cnt == CNT_MAX - 1) begin
            if(i == 0) begin
                led <= 2'b10;
                i   <= 1;
            end
            else begin
                led <= 2'b01;
                i   <= 0;
            end
        end
        // 其他时刻不写 led,自动保持
    end

endmodule

1.2 LED_tb.v

verilog 复制代码
`timescale 1ns / 1ps
`define SIMULATION               // ← 关键:启用仿真分支,绕开 IBUFDS

module LED_tb();

    reg  sys_clk_p;
    reg  sys_rstn;
    wire [1:0] led;

    // ===== 例化被测模块,CNT_MAX 改小,仿真瞬间看到切换 =====
    LED #(
        .CNT_MAX(10)             // 仿真时只数 10 个周期 = 50ns
    ) u_LED (
        .sys_clk_p(sys_clk_p),
        .sys_clk_n(~sys_clk_p),  // 反相模拟差分负端
        .sys_rstn  (sys_rstn),
        .led       (led)
    );

    // ===== 产生 200MHz 时钟(周期 5ns)=====
    initial begin
        sys_clk_p = 0;
        forever #2.5 sys_clk_p = ~sys_clk_p;
    end

    // ===== 复位激励 =====
    initial begin
        sys_rstn = 0;
        #100;                    // 复位保持 100ns
        sys_rstn = 1;            // 释放复位
        #1000;                   // 跑 1us,足够看 20 次切换
        $stop;                   // 停止仿真
    end

    // ===== 监视输出 =====
    initial begin
        $monitor("Time = %0t ns, led = %b", $time, led);
    end

endmodule

1.3 配套 XDC

tcl 复制代码
# ===== 配置属性 =====
set_property CFGBVS VCCO [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property BITSTREAM.GENERAL.COMPRESS true [current_design]
set_property BITSTREAM.CONFIG.CONFIGRATE 50 [current_design]
set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4 [current_design]
set_property BITSTREAM.CONFIG.SPI_FULL_EDGE Yes [current_design]

# ===== 时钟约束(关键!)=====
create_clock -period 5.000 -name sys_clk [get_ports sys_clk_p]

# ===== 引脚约束 =====
set_property -dict {PACKAGE_PIN R4  IOSTANDARD DIFF_SSTL15} [get_ports sys_clk_p]
set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33}    [get_ports sys_rstn]
set_property -dict {PACKAGE_PIN W22 IOSTANDARD LVCMOS33}    [get_ports {led[0]}]
set_property -dict {PACKAGE_PIN Y22 IOSTANDARD LVCMOS33}    [get_ports {led[1]}]

二、逐行解释

2.1 LED.v 逐行分析

verilog 复制代码
`timescale 1ns / 1ps

第 1 行: 时间单位 1ns,精度 1ps。仿真时 #数字 以此为基准。综合忽略。

verilog 复制代码
module LED #(
    parameter CNT_MAX = 200000000    // 默认 2 亿(1 秒 @ 200MHz)
)(
    input  sys_clk_p,
    input  sys_clk_n,
    input  sys_rstn,
    output reg [1:0] led
);

第 3~10 行: 模块定义 + 参数 + 端口。

端口 方向 说明
sys_clk_p input 差分时钟正端(200MHz)
sys_clk_n input 差分时钟负端
sys_rstn input 复位,低有效
led[1:0] output reg 2 位 LED 输出

parameter CNT_MAX = 200000000:默认值 2 亿,仿真时可通过 #(.CNT_MAX(10)) 覆盖。

verilog 复制代码
wire clk;

第 13 行: 声明内部单端时钟线,用于驱动后续 always 块。

verilog 复制代码
`ifdef SIMULATION
    assign clk = sys_clk_p;
`else
    IBUFDS #(
        .DIFF_TERM("FALSE"),
        .IBUF_LOW_PWR("TRUE"),
        .IOSTANDARD("DEFAULT")
    ) IBUFDS_inst (
        .O (clk),
        .I (sys_clk_p),
        .IB(sys_clk_n)
    );
`endif

第 15~27 行: 差分时钟处理,分两条路。

条件 走哪条 原因
```ifdef SIMULATION`` 成立 assign clk = sys_clk_p; 仿真时绕过 IBUFDS
不成立(综合) IBUFDS 原语 硬件差分输入缓冲

IBUFDS 参数:

参数 值 含义
DIFF_TERM "FALSE" 不用内部 100Ω 终端电阻
IBUF_LOW_PWR "TRUE" 低功耗模式
IOSTANDARD "DEFAULT" 默认电平,由 XDC 指定

端口:

端口 方向 连接
.O 输出 clk
.I 输入 sys_clk_p
.IB 输入 sys_clk_n(B = Bar,反相端)
verilog 复制代码
reg [31:0] cnt;
always @(posedge clk) begin
    if(!sys_rstn)
        cnt <= 0;
    else if(cnt < CNT_MAX - 1)
        cnt <= cnt + 1;
    else
        cnt <= 0;
end

第 29~38 行: 计数器。

条件 动作 说明
复位有效 cnt <= 0 清零
cnt < CNT_MAX - 1 cnt <= cnt + 1 累加
cnt == CNT_MAX - 1 cnt <= 0 归零,完成一轮

关键: 数到 CNT_MAX - 1 就归零,所以一轮是 CNT_MAX 个周期。

verilog 复制代码
reg i;
always @(posedge clk) begin
    if(!sys_rstn) begin
        led <= 2'b01;
        i   <= 0;
    end
    else if(cnt == CNT_MAX - 1) begin
        if(i == 0) begin
            led <= 2'b10;
            i   <= 1;
        end
        else begin
            led <= 2'b01;
            i   <= 0;
        end
    end
end

第 40~57 行: LED 状态切换。

条件 动作 说明
复位有效 led = 01, i = 0 初始状态
cnt == CNT_MAX - 1 且 i == 0 led = 10, i = 1 切到 LED2
cnt == CNT_MAX - 1 且 i == 1 led = 01, i = 0 切回 LED1
其他 不写 led 自动保持

关键: i 只在 0 和 1 之间交替,输出 01 ↔ 10,不会出现 00。

verilog 复制代码
endmodule

第 59 行: 模块结束。


2.2 LED_tb.v 逐行分析

verilog 复制代码
`timescale 1ns / 1ps

第 1 行: 时间单位和精度,同 LED.v。

verilog 复制代码
`define SIMULATION

第 2 行: 关键! 定义仿真宏,让 LED.v 里的 ```ifdef SIMULATION`` 成立,走 assign clk = sys_clk_p;,绕开 IBUFDS。

⚠️ 必须写在 module 外面,文件最顶部,否则不生效。

verilog 复制代码
module LED_tb();

第 4 行: 测试模块定义,没有端口。Testbench 不和外部通信,只产生激励。

verilog 复制代码
reg  sys_clk_p;
reg  sys_rstn;
wire [1:0] led;

第 6~8 行: 信号声明。

信号 类型 为什么
sys_clk_p reg TB 主动产生时钟,必须能赋值
sys_rstn reg TB 主动产生复位,必须能赋值
led wire 由被测模块驱动,TB 只观察
verilog 复制代码
LED #(
    .CNT_MAX(10)
) u_LED (
    .sys_clk_p(sys_clk_p),
    .sys_clk_n(~sys_clk_p),
    .sys_rstn  (sys_rstn),
    .led       (led)
);

第 10~17 行: 例化被测模块。

部分 值 说明
LED 模块名 要例化的模块类型
#(.CNT_MAX(10)) 参数覆盖 仿真时数 10 个周期切换
u_LED 实例名 给这个实例起的名字
端口连接 --- 把 TB 信号连到模块端口

~sys_clk_p: 反相模拟差分负端。仿真里 ~ 可以作用于 reg。

verilog 复制代码
initial begin
    sys_clk_p = 0;
    forever #2.5 sys_clk_p = ~sys_clk_p;
end

第 19~22 行: 产生 200MHz 时钟。

时刻 事件
0ns sys_clk_p = 0
2.5ns 翻转为 1
5ns 翻转为 0
... 每 2.5ns 翻转一次

周期 = 5ns → 200MHz。

verilog 复制代码
initial begin
    sys_rstn = 0;
    #100;
    sys_rstn = 1;
    #1000;
    $stop;
end

第 24~30 行: 产生复位激励。

时刻 事件 说明
0ns sys_rstn = 0 复位有效
0~100ns 保持 0 复位期间 led 固定 01
100ns sys_rstn = 1 释放复位
100~1100ns 逻辑工作 LED 每 50ns 切换一次
1100ns $stop 停止仿真
verilog 复制代码
initial begin
    $monitor("Time = %0t ns, led = %b", $time, led);
end

第 32~34 行: 监视输出。

  • $monitor:只要 led 变化就打印
  • %0t:时间,%b:二进制
  • 输出示例:Time = 150 ns, led = 10
verilog 复制代码
endmodule

第 36 行: 模块结束。


2.3 配套 XDC 逐行分析

tcl 复制代码
set_property CFGBVS VCCO [current_design]

配置 Bank 电压跟随 VCCO。

tcl 复制代码
set_property CONFIG_VOLTAGE 3.3 [current_design]

配置引脚电压 3.3V。

tcl 复制代码
set_property BITSTREAM.GENERAL.COMPRESS true [current_design]

比特流压缩,文件更小。

tcl 复制代码
set_property BITSTREAM.CONFIG.CONFIGRATE 50 [current_design]

配置速率 50MHz(从 Flash 加载比特流的速度,不是系统时钟)。

tcl 复制代码
set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4 [current_design]

SPI 位宽 4(Quad-SPI,四线并行加载)。

tcl 复制代码
set_property BITSTREAM.CONFIG.SPI_FULL_EDGE Yes [current_design]

SPI 全沿采样。

tcl 复制代码
create_clock -period 5.000 -name sys_clk [get_ports sys_clk_p]

关键! 时钟约束:周期 5ns = 200MHz。差分时钟只约束 P 端,N 端自动关联。

tcl 复制代码
set_property -dict {PACKAGE_PIN R4  IOSTANDARD DIFF_SSTL15} [get_ports sys_clk_p]
set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33}    [get_ports sys_rstn]
set_property -dict {PACKAGE_PIN W22 IOSTANDARD LVCMOS33}    [get_ports {led[0]}]
set_property -dict {PACKAGE_PIN Y22 IOSTANDARD LVCMOS33}    [get_ports {led[1]}]

引脚约束:

端口 引脚 电平
sys_clk_p R4 DIFF_SSTL15(差分)
sys_rstn R14 LVCMOS33
led[0] W22 LVCMOS33
led[1] Y22 LVCMOS33

{led[0]} 用大括号包起来,因为 Tcl 里方括号有特殊含义。


2.4 补充:parameter 的两种写法

Verilog 里,parameter 有两种合法写法,功能完全等价。

写法 A:参数写在模块名后(推荐)
verilog 复制代码
module Decode #(
    parameter Width = 1,
    parameter Polarity = 1
)(
    input [Width-1:0] A,
    output F
);
    ...
endmodule
写法 B:参数写在模块内部(老写法)
verilog 复制代码
module Decode(A, F);
    parameter Width = 1, Polarity = 1;
    ...
endmodule
对照表
对比项 写法 A 写法 B
位置 模块名后 #(...) 模块名后 (...) 之内
可读性 ✅ 更好 ⚠️ 要往下翻
能否覆盖 ✅ 可以 ✅ 可以
年代 Verilog-2001 之后 Verilog-1995
推荐度 ⭐⭐⭐ ⭐
例化时的两种覆盖方式

按位置覆盖:

verilog 复制代码
Decode #(4, 0) D1(A4, F16);
位置 对应参数 值
第 1 个 Width 4
第 2 个 Polarity 0

按名字覆盖(推荐):

verilog 复制代码
Decode #(.Width(4), .Polarity(0)) D1(A4, F16);
写法 名称 优点 缺点
#(4, 0) 按位置 简短 顺序不能错
#(.Width(4), .Polarity(0)) 按名字 顺序无关,清晰 写起来长
经典教材里的例子
verilog 复制代码
module Decode(A, F);
    parameter Width = 1, Polarity = 1;
    ...
endmodule

module Top;
    wire [3:0]  A4;
    wire [4:0]  A5;
    wire [15:0] F16;
    wire [31:0] F32;

    Decode #(4, 0) D1(A4, F16);   // Width=4, Polarity=0
    Decode #(5)    D2(A5, F32);   // Width=5, Polarity=默认 1
endmodule

结论: 你之前看到的 module LED #(parameter CNT_MAX = ...) 是写法 A,截图里的 module Decode(A, F); parameter Width = 1; 是写法 B,两者完全等价,新代码推荐写法 A。


三、重点问题回答

Q1:为什么仿真不能和实际一致?

仿真是"逻辑推演",不是"物理等待"。

维度 仿真 实际硬件
时间本质 逻辑推演 物理振荡
1 秒需要 2 亿次逻辑计算 2 亿次物理振荡
电脑耗时 可能几十分钟 真实 1 秒
能否加速 ✅ 改参数即可 ❌ 只能等

仿真器本质是软件,每模拟一个时钟周期都要消耗 CPU 时间。200MHz 时钟意味着每秒 2 亿次翻转,电脑扛不住。

Q2:forever #2.5 不是已经设好时钟了吗?为什么还要改 CNT_MAX?

时钟和 LED 切换是两码事。

项目 由谁决定 你的设置
时钟周期 TB 里的 forever #2.5 5ns(200MHz)
LED 切换周期 LED.v 里的 CNT_MAX 默认 1 秒

时钟跑得快,不代表 LED 切换得快。 时钟设得再快,CNT_MAX = 2 亿 还是要数 2 亿下才切换。把 CNT_MAX 改成 10,几微秒就能看到切换。

Q3:u_LED 是什么?

是你给例化出来的 LED 模块起的名字(实例名)。

概念 例子 作用
模块名 LED 定义设计(图纸)
实例名 u_LED 标识这次例化(造出来的设备)
参数 CNT_MAX 可覆盖的配置值
端口 sys_clk_p 等 模块对外的连接点

u_ 只是习惯前缀,不写也能跑,但推荐保留。实例名必须唯一,不能和别的信号重名。

Q4:#(.CNT_MAX(10)) 有必要改吗?

有必要,强烈建议保留。

方案 CNT_MAX 仿真切换周期 跑 1us 能看到什么
改 10 50ns 20 次切换 ✅
不改 200,000,000 1 秒 0 次切换 ❌

不改的话,仿真要跑 2 亿个周期才切换一次,电脑可能卡几十分钟甚至几小时。改了之后,50ns 切换一次,几微秒就能验证逻辑。综合时 Testbench 不参与,板子上仍然是真实 1 秒。

Q5:仿真没波形怎么办?

先检查 sys_clk_p 有没有翻转。

波形里 sys_clk_p 一直为 0,说明:

  1. ```define SIMULATION`` 没写或位置不对
  2. IBUFDS 把 clk 卡死了
  3. sys_clk_p 声明成了 wire

解决: 在 LED_tb.v 最顶部加 define SIMULATION``,在 `LED.v` 里加 ifdef SIMULATION`` 分支。

Q6:set_property 怎么用?为什么有的要加 -dict?

基本语法:

tcl 复制代码
set_property <属性名> <属性值> [<对象>]

不加 -dict(一次设一个属性):

tcl 复制代码
set_property PACKAGE_PIN W22 [get_ports led[0]]
set_property IOSTANDARD LVCMOS33 [get_ports led[0]]

加 -dict(一次设多个属性):

tcl 复制代码
set_property -dict {PACKAGE_PIN W22 IOSTANDARD LVCMOS33} [get_ports led[0]]

结论: -dict 只是批量设置的语法糖,功能等价于多条单属性命令。

Q7:current_design 是什么?

代表"当前正在处理的那个设计(整个 FPGA 工程)"。

对象 写法 作用范围
整个设计 [current_design] 全局配置(电压、比特流等)
某个端口 [get_ports led[0]] 单个引脚/信号

结论: 全局配置用 current_design,引脚约束用 get_ports。

Q8:XDC 的时钟约束必须加吗?

必须加,尤其你的时钟是 200MHz。

tcl 复制代码
create_clock -period 5.000 -name sys_clk [get_ports sys_clk_p]

不加的话 Vivado 不做时序分析,200MHz 下容易出问题。

Q9:XDC 的时钟约束加上后,cnt 还用改吗?

要改!两回事。

项目 作用 改不改
create_clock -period 5.000 告诉 Vivado:时钟周期是 5ns ✅ 要加
cnt < 50000000 代码里数 50000000 个周期切换 LED ✅ 也要改

结论: XDC 加约束是为了时序分析 ,代码改 cnt 是为了真正 1 秒切换,两者必须配套。

Q10:BITSTREAM.CONFIG.CONFIGRATE 50 是设置时钟为 50MHz 吗?

不是! 这是配置速率,指 FPGA 从 Flash 加载比特流时的 SPI 时钟频率。

概念 作用阶段 由谁决定
配置时钟(CONFIGRATE) FPGA 上电加载时 XDC 里的 CONFIGRATE
系统时钟(sys_clk) FPGA 正常运行时 板上晶振 + create_clock

Q11:增量编译可以加快哪个地方的速度?

主要加速实现阶段(Implementation),特别是布局布线(Place & Route)。

阶段 是否加速 说明
综合(Synthesis) ❌ 基本不加速 综合看的是逻辑,不是物理位置
实现(Implementation) ✅ 主要加速点 布局布线是大头,能省 30%~70% 时间
生成比特流(Bitstream) ❌ 不加速 只是打包数据,本来就快

Q12:改完代码没生效怎么办?

改了东西后,必须重新走完整流程:

复制代码
Run Synthesis → Run Implementation → Generate Bitstream → Program Device
改了什么 需要重新综合? 需要重新实现? 需要重新生成 bit?
Verilog 代码 ✅ ✅ ✅
XDC 引脚/时钟 ❌ ✅ ✅
XDC 配置属性 ❌ ❌ ✅
只改注释 ❌ ❌ ❌

少任何一步,板子上跑的还是旧版本。

Q13:parameter 为什么有的写在模块名后,有的写在模块里?

两种写法都合法,功能等价。

写法 位置 年代 推荐度
A module 名字 #(parameter ...) (...) Verilog-2001 之后 ⭐⭐⭐
B module 名字 (...); parameter ...; Verilog-1995 ⭐

例化时都能用 #(...) 覆盖:

  • 按位置:Decode #(4, 0) D1(A4, F16);
  • 按名字:Decode #(.Width(4), .Polarity(0)) D1(A4, F16);

推荐用按名字覆盖,顺序无关,可读性好。


四、总结:FPGA 新手最容易踩的坑

# 问题 原因 正确做法
1 闪快 4 倍 时钟频率搞错 确认 200MHz,改 cnt 阈值
2 误认 CONFIGRATE 配置时钟 ≠ 系统时钟 用 create_clock 约束系统时钟
3 改完没生效 没走完整流程 综合→实现→bit→下载
4 编译太慢 FPGA 编译成硬件 增量编译 + 仿真先行
5 仿真没波形 IBUFDS 无仿真模型 加 ```ifdef SIMULATION`` 绕开
6 仿真跑不动 CNT_MAX 太大 用参数覆盖改小
7 parameter 写法混淆 两种写法都合法 推荐写在模块名后,按名字覆盖

五、最后的建议

  1. 先仿真,再上板。仿真几秒能看出的问题,不要花十分钟去综合实现。
  2. 改完代码走完整流程。综合 → 实现 → 生成 bit → 下载,一步不能少。
  3. 增量编译能省时间。改动小的时候效果明显。
  4. 确认板子参数。时钟频率、封装、引脚号,都要查手册确认。
  5. XDC 约束和代码阈值必须配套。时钟约束只管时序分析,计数器阈值才决定实际时间。
  6. 仿真时用参数覆盖缩短周期。仿真不是模拟真实时间,而是验证逻辑。
  7. parameter 推荐写在模块名后,用按名字覆盖。可读性好,顺序无关。

FPGA 开发的学习曲线确实陡,但踩过的坑都会变成经验。希望这篇博文能帮你少走一些弯路。


本文基于真实调试过程整理,代码已简化,仅保留关键逻辑。

相关推荐
hahaha601612 小时前
3NRD降噪算法FPGA实现8并行度设计
fpga开发
松小白song13 小时前
一次“载电流显示为 0“的排查经历:负值钳位引发的踩坑
fpga开发
yingheceping16 小时前
复旦微FPGA开发板厂商TOP5甄选榜:2026选型参考
fpga开发·fpga
NamoAmitabha12818 小时前
PL 端的 GPIO IP(AXI GPIO)---PS控制----deepseek
fpga开发·zynq--ps
hahaha601621 小时前
3D降噪算法YUV降噪FPGA实现
fpga开发
Hello-FPGA2 天前
基于RFSOC FPGA ZCU216 的CoaXPress 采集demo
fpga开发
9527华安2 天前
FPGA基于ADV7182解码PAL视频,Video Processing Subsystem去隔行,提供6套工程源码和技术支持
fpga开发·音视频·pal·adv7182
hahaha60162 天前
数字图像时域滤波或者3D降噪算法FPGA实现
算法·计算机视觉·fpga开发
GateWorld2 天前
Xilinx 7系列FPGA时钟网络详解
fpga开发·时钟网络·xilinx 7系列
szxinmai主板定制专家2 天前
X86 工控机 vs RK3588 异构主控:AMR 控制器 BOM、散热、长期运维对比
运维·stm32·单片机·嵌入式硬件·fpga开发·am62x