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 - 1i == 0 led = 10, i = 1 切到 LED2
cnt == CNT_MAX - 1i == 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. IBUFDSclk 卡死了
  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 开发的学习曲线确实陡,但踩过的坑都会变成经验。希望这篇博文能帮你少走一些弯路。


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

相关推荐
博览鸿蒙8 小时前
FPGA开发使用的语言
fpga开发·fpga
明德扬9 小时前
机器人多摄像头如何做FPGA多路视频聚合?
学习·fpga开发·fpga
ktd0071 天前
复旦微开发环境搭建流程备忘录
fpga开发
ERROR:991 天前
陈工,试试Agent开发FPGA
fpga开发
威嵌神州1 天前
复旦微 FMQL100TAI 开发 8 问:仿真器、系统、接口高频问题解答
人工智能·嵌入式硬件·fpga开发
北京青翼科技1 天前
青翼基于PCIe的VU13P FPGA信号处理板
fpga开发·fpga开发板·图像处理卡·pcie板卡
szxinmai主板定制专家1 天前
NXP LS1046A 硬件方案: LS1046A+FPGA 异构加速架构实践
大数据·图像处理·人工智能·嵌入式硬件·fpga开发
Riwuarua2 天前
从近似0基础开始FPGA开发 -- part.14 JESD204协议、IP核与嵌入式初始化
fpga开发·jesd
第二层皮-合肥3 天前
Vivado 自动生成的 AXI‑Lite 从机 IP 的操作步骤
fpga开发