3、FPGA基础:FPGA架构简介、Verilog/VHDL基础、开发工具链(Vivado/Quartus)

好,咱们进入正题。FPGA 这东西,说白了就是一块可以让你随心所欲定义逻辑的芯片。不像 CPU 那样指令一条条跑,FPGA 是硬件级别的并行执行。你想想看,在金融行情解码这种场景下,纳秒级的延迟差异可能就是几百万的盈亏,FPGA 的并行能力简直就是为这个量身定做的。

3.1 FPGA 架构简介

我第一次接触 FPGA 是在做高频交易网关的时候。当时团队用 CPU 软解码,延迟始终压不进 10 微秒。后来换成 FPGA,直接降到了几百纳秒。嗯,差距就是这么夸张。

FPGA 内部主要由这几块构成:

  • 逻辑单元(Logic Elements, LE):这是最基础的积木。每个 LE 包含一个查找表(LUT)和一个触发器(Flip-Flop)。LUT 实现组合逻辑,触发器实现时序逻辑。
  • 可编程互连(Programmable Interconnect):把各个逻辑单元连起来的“高速公路”。布线的好坏直接影响时序。
  • 块 RAM(Block RAM, BRAM):片上的存储资源。我习惯用它来做 FIFO 或者小容量的查找表。注意,BRAM 的读写延迟是固定的,这点在低延迟设计中很关键。
  • DSP 切片(DSP Slice):专门做乘加运算的硬核。行情解码里做校验和或者价格计算时,DSP 比用 LUT 拼出来的快得多。
  • 高速收发器(Transceiver):用来接 10G/25G/100G 以太网的。组播分发全靠它。

核心要点:FPGA 的并行性来源于它的“空间换时间”思想。你写的每一行代码,最终都会映射到芯片上的一块物理区域。多个模块同时运行,互不干扰。

下面这张图是我自己画的 FPGA 内部结构简图,帮你快速建立整体印象:

FPGA 内部架构简图 逻辑单元阵列 (LE Array) LUT + Flip-Flop 块 RAM BRAM DSP 乘加器 可编程互连 (Programmable Interconnect) 高速收发器 (Transceiver) 配置逻辑 & 时钟管理

3.2 Verilog/VHDL 基础

我个人更偏爱 Verilog,语法简洁,上手快。VHDL 更严谨,适合大型团队。但在低延迟领域,两者都能用,关键看你怎么写。

3.2.1 模块化设计

每个 FPGA 设计都是从模块开始的。一个模块就是一个硬件单元。举个例子,一个简单的 D 触发器:

module d_flip_flop (
    input  wire clk,
    input  wire rst_n,
    input  wire d,
    output reg  q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

注意看,always @(posedge clk) 这个写法,它描述的是一个时序逻辑。在 FPGA 里,它会映射到一个触发器和一些组合逻辑。我曾经见过新手把组合逻辑和时序逻辑混在一起写,结果时序跑不过,板子调了三天。

3.2.2 阻塞赋值 vs 非阻塞赋值

这是个经典坑。我刚开始学的时候也栽过。

  • 阻塞赋值 (=):用于组合逻辑。赋值立即生效。
  • 非阻塞赋值 (<=):用于时序逻辑。所有赋值在 always 块结束时同时生效。

避坑指南:我曾经在一个项目中,把组合逻辑和时序逻辑写在同一个 always 块里,用了混合赋值方式。结果仿真没问题,上板后数据全乱套。后来老老实实分开写,一个 always 块只做一件事。

3.2.3 状态机

行情解码里,状态机是核心。比如解析一个以太网帧,你需要经历:前导码 -> 帧起始 -> 目的 MAC -> 源 MAC -> 类型/长度 -> 载荷 -> FCS。每个状态对应一个硬件阶段。

localparam IDLE      = 3'd0;
localparam MAC_DST   = 3'd1;
localparam MAC_SRC   = 3'd2;
localparam ETH_TYPE  = 3'd3;
localparam PAYLOAD   = 3'd4;
localparam FCS       = 3'd5;

reg [2:0] state, next_state;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= next_state;
end

always @(*) begin
    case (state)
        IDLE:     next_state = (valid_in) ? MAC_DST : IDLE;
        MAC_DST:  next_state = (byte_cnt == 5) ? MAC_SRC : MAC_DST;
        // ... 其他状态
        default:  next_state = IDLE;
    endcase
end

你想想看,这种三段式状态机写法,综合出来的电路清晰,时序也好收敛。我习惯把状态转移和输出逻辑分开,调试的时候一目了然。

3.3 开发工具链(Vivado / Quartus)

Xilinx 用 Vivado,Intel 用 Quartus。两者流程大同小异,但细节上各有千秋。

环节 Vivado Quartus
综合(Synthesis) 支持多种综合策略,默认性能优先 综合速度较快,但优化选项略少
实现(Implementation) 布局布线可精细控制,支持增量编译 布局布线相对自动化,手动干预空间小
时序分析 报告详细,支持多时钟域分析 时序报告直观,但跨时钟域分析稍弱
调试 集成 ILA(逻辑分析仪),方便抓信号 Signal Tap 逻辑分析仪,功能类似

3.3.1 综合与实现

综合是把 Verilog/VHDL 代码转换成网表(门级电路)。实现则是把网表映射到具体的 FPGA 资源上。我个人的习惯是:

  • 先跑一次综合,看看资源占用和警告。
  • 再跑实现,重点看时序报告。
  • 如果时序不满足,回头改代码,而不是盲目调工具选项。

小技巧:在 Vivado 里,我常用 report_timing_summary 命令快速查看最差路径。如果 setup slack 是负的,说明你的逻辑路径太长了。试试在关键路径上插入流水线寄存器。

3.3.2 时序约束

没有约束的 FPGA 设计,就像没有导航的船。你永远不知道它能不能跑到目标频率。

# 创建时钟约束
create_clock -name sys_clk -period 5.000 [get_ports clk]  # 200 MHz

# 输入延迟约束
set_input_delay -clock sys_clk -max 2.0 [get_ports data_in]
set_input_delay -clock sys_clk -min 0.5 [get_ports data_in]

# 输出延迟约束
set_output_delay -clock sys_clk -max 1.5 [get_ports data_out]
set_output_delay -clock sys_clk -min 0.2 [get_ports data_out]

嗯,这里要注意:输入输出延迟的值,需要根据你的外部芯片数据手册来填。我曾经因为偷懒没填输出延迟,结果板级信号眼图惨不忍睹。

3.3.3 仿真与调试

仿真分功能仿真和时序仿真。功能仿真看逻辑对不对,时序仿真看实际跑起来会不会出问题。我一般流程是:

  1. 写 testbench,覆盖正常和异常情况。
  2. 跑功能仿真,确认行为正确。
  3. 综合后跑门级仿真,检查时序问题。
  4. 上板后用 ILA/Signal Tap 抓实际信号。

说白了,仿真能发现 90% 的问题,剩下 10% 靠上板调试。但上板调试成本高,所以仿真阶段多花点时间绝对值得。

总结一下:FPGA 基础这块,架构是骨架,Verilog/VHDL 是血肉,工具链是手脚。三者缺一不可。在金融行情解码这个场景里,你不仅要会写代码,还要理解代码怎么映射到硬件,怎么优化时序,怎么用工具定位问题。这些经验,都是在一次次流片和上板调试中积累出来的。

好,这一章就到这里。记住,FPGA 设计没有银弹,多动手、多踩坑,自然就熟了。