一、FPGA内部结构:从逻辑单元到计算引擎

说实话,我第一次接触FPGA时,觉得这东西就是个「万能芯片」。后来做量化交易的低延迟系统,才真正理解它的内部构造有多精妙。咱们先看看FPGA到底由什么组成。

1.1 基本逻辑单元:LUT与FF

FPGA最核心的两个元件,就是查找表(LUT)和触发器(FF)。

  • LUT(查找表):说白了就是一个可编程的真值表。你给它输入,它直接输出结果。比如4输入LUT,内部有16个存储位,可以实现任意4输入逻辑函数。我在做行情解析时,就用LUT实现了协议头部的快速匹配,比CPU查表快了一个数量级。
  • FF(触发器):用来存储1位数据。它只在时钟边沿采样输入,然后保持输出。所有时序逻辑都靠它。

嗯,这里有个关键点:LUT做组合逻辑,FF做时序逻辑。两者配合,就能实现任何数字电路。

1.2 存储与运算:BRAM与DSP

光有LUT和FF还不够,FPGA里还有两个重量级资源:

资源用途我踩过的坑
BRAM(块RAM)存储数据,容量大,速度快曾经用LUT搭FIFO,结果资源爆了。后来改用BRAM,省了80%的逻辑资源。
DSP(数字信号处理单元)做乘法、加法、累加做期权定价模型时,DSP的流水线特性让我少写了很多时序逻辑。

我个人习惯:能用BRAM就别用LUT存数据,能用DSP就别自己搭乘法器。这是FPGA设计的黄金法则。

1.3 知识体系总览

下面这张图,是我自己总结的FPGA内部结构关系。你看一眼就明白了:

FPGA内部结构关系图 FPGA芯片 LUT(查找表) 组合逻辑实现 FF(触发器) 时序逻辑存储 BRAM(块RAM) 数据存储 DSP(数字信号处理) 乘加运算 LUT + FF = 任意逻辑电路 | BRAM + DSP = 高性能计算引擎

核心要点:FPGA设计就是合理分配这四种资源。LUT和FF做控制逻辑,BRAM存数据,DSP做运算。别搞混了。

二、Verilog基础语法:写代码就像搭积木

Verilog这语言,说白了就是描述硬件连接关系的文本。跟C语言完全两码事——它是并行的,不是顺序执行的。

2.1 模块与端口

每个Verilog文件就是一个模块(module)。模块有输入输出端口,就像芯片的引脚。

module led_controller(
    input  wire clk,      // 时钟信号
    input  wire rst_n,    // 复位信号(低有效)
    output reg  led       // LED输出
);
    // 内部逻辑写在这里
endmodule

我个人习惯:端口声明时,input/output写在前面,wire/reg类型紧跟其后。这样代码可读性最好。

2.2 三种描述方式

Verilog有三种描述方式,你都得会:

  • 结构化描述:直接例化门电路。比如 and u1(out, a, b);。实际项目中很少用,太底层了。
  • 数据流描述:用assign语句。适合组合逻辑。比如 assign led = a & b;
  • 行为级描述:用always块。最常用,能描述时序逻辑和复杂组合逻辑。

我的建议:新手先掌握行为级描述。always块配合if-else、case语句,能解决90%的问题。

2.3 阻塞赋值与非阻塞赋值

这是新手最容易翻车的地方。我曾经在项目里用错了赋值方式,导致仿真和实际跑的结果不一样,查了两天才找到原因。

  • 阻塞赋值(=):顺序执行,用于组合逻辑。
  • 非阻塞赋值(<=):并行执行,用于时序逻辑。

记住一个原则:写时序逻辑(always @(posedge clk))用非阻塞赋值,写组合逻辑用阻塞赋值。别混用。

三、Vivado开发流程:从代码到比特流

Vivado是Xilinx的FPGA开发工具。说实话,刚开始用的时候我觉得它太笨重了,但用顺手后才发现功能确实强大。

3.1 标准开发流程

  1. 创建工程:选芯片型号,添加源文件。
  2. 编写代码:写Verilog,添加约束文件(XDC)。
  3. 综合(Synthesis):把Verilog转成网表。说白了就是翻译成LUT和FF的连接关系。
  4. 实现(Implementation):布局布线。把网表映射到实际芯片上。
  5. 生成比特流:生成下载文件(.bit)。
  6. 下载调试:烧录到FPGA,用ILA抓波形看结果。

注意:综合和实现阶段,一定要看警告信息。我曾经忽略了一个时序警告,结果板子跑起来数据全是错的。血的教训。

3.2 约束文件怎么写

约束文件(XDC)用来告诉工具,你的信号对应哪个引脚、时钟频率是多少。

# 时钟约束
create_clock -period 10.000 [get_ports clk]

# 引脚约束
set_property PACKAGE_PIN U18 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]

set_property PACKAGE_PIN J15 [get_ports led]
set_property IOSTANDARD LVCMOS33 [get_ports led]

嗯,这里要注意:时钟约束一定要写对。10ns对应100MHz,如果你板子上的晶振是50MHz,那就写20.000。

四、第一个LED闪烁程序

好了,理论说完了,咱们动手写个LED闪烁程序。这是FPGA界的「Hello World」。

4.1 代码实现

module led_blink(
    input  wire clk,      // 50MHz时钟
    input  wire rst_n,    // 复位
    output reg  led       // LED输出
);

    // 计数器:从0计数到最大值
    reg [24:0] cnt;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 25'd0;
        else if (cnt == 25'd24_999_999)
            cnt <= 25'd0;
        else
            cnt <= cnt + 1'b1;
    end
    
    // LED翻转:每0.5秒翻转一次
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            led <= 1'b0;
        else if (cnt == 25'd24_999_999)
            led <= ~led;
        else
            led <= led;
    end

endmodule

4.2 代码解析

这段代码的逻辑很简单:

  • 50MHz时钟,周期20ns。要闪烁周期1秒(亮0.5秒,灭0.5秒),需要计数2500万次。
  • 计数器从0到24999999,每计满一次,LED翻转一次。
  • 复位时,计数器清零,LED熄灭。

避坑指南:计数器位宽一定要算对。25位计数器最大值是33,554,431,够用。如果你用24位,最大值才16,777,215,计不到2500万。我曾经犯过这个错,LED死活不闪,查了半天才发现是计数器溢出了。

4.3 上板验证

写完代码后,在Vivado里走一遍综合、实现、生成比特流。下载到板子上,如果LED以1秒周期闪烁,恭喜你,成功了!

如果没闪,别慌。先检查:

  • 时钟引脚对不对?
  • 复位引脚有没有拉高?
  • LED引脚有没有接反?(有些板子是低电平亮)

我个人习惯:第一次上板,先写个最简单的程序——让LED常亮。确认硬件连接没问题后,再写闪烁程序。这样能快速定位问题。


好了,FPGA的基础知识和开发环境就讲到这里。记住:理论是骨架,实践是血肉。赶紧打开Vivado,把LED点亮吧。

专注资料整理