4、FPGA逻辑设计基础:Verilog/VHDL快速入门、流水线设计思想、状态机与并行处理

做高频交易系统这么多年,我越来越觉得FPGA是个神奇的东西。它不像CPU那样一条指令一条指令地跑,也不像GPU那样批量计算。FPGA的核心理念就四个字:硬件可编程。说白了,你是在用代码画电路。

这一章,我带大家快速过一遍FPGA逻辑设计的几个核心概念。别怕,咱们不讲那些枯燥的理论,我尽量用做项目的经验来讲。

4.1 Verilog/VHDL快速入门:选哪个?

先回答一个经典问题:Verilog还是VHDL?

我个人习惯用Verilog。为什么?因为做高频交易的大多数团队都用Verilog。VHDL语法更严谨,但写起来啰嗦。你想想看,在FPGA上做行情解析,代码量本来就大,再用VHDL那种Ada风格的语法,写起来真有点累。

不过,这不是说VHDL不好。我在一个项目中遇到过一位老工程师,他用VHDL写的状态机,那叫一个清晰。所以我的建议是:做高频交易,主学Verilog,能看懂VHDL就行

来看一个最简单的Verilog例子——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

这段代码,说白了就是描述了一个硬件行为:时钟上升沿来的时候,把d的值传给q。复位信号低电平时,q清零。

注意那个<=符号,这叫非阻塞赋值。嗯,这里要注意:在时序逻辑里一定要用非阻塞赋值。我曾经见过一个新手,在always块里用了阻塞赋值=,结果仿真没问题,上板子就跑飞了。查了两天才发现是赋值方式的问题。

4.2 流水线设计思想:把大任务拆开

流水线,这是FPGA设计的灵魂。为什么?因为FPGA的时钟频率通常比CPU低很多(200-500MHz vs 3-5GHz),但它的优势是并行。流水线就是利用并行来换取吞吐量。

我举个例子。假设你要计算 A*B + C*D。如果直接写:

result = A*B + C*D;

在FPGA里,这需要一级乘法器加一级加法器。乘法器延迟大,整个路径的延迟可能达到10ns,时钟频率只能跑到100MHz。

如果用流水线呢?

// 第一级:乘法
reg [31:0] mul1, mul2;
always @(posedge clk) begin
    mul1 <= A * B;
    mul2 <= C * D;
end

// 第二级:加法
reg [31:0] result;
always @(posedge clk) begin
    result <= mul1 + mul2;
end

这样,每级逻辑的延迟只有5ns左右,时钟频率可以跑到200MHz。虽然计算结果要晚一个时钟周期才出来,但吞吐量翻了一倍。

核心思想:流水线是用寄存器把组合逻辑切分成多段,每段延迟变小,时钟频率就能提高。代价是增加了延迟(latency),但换来了更高的吞吐量(throughput)。

在高频做市系统里,流水线无处不在。比如行情解析,从网口数据进来,到解析出买卖盘口,中间可能有十几级流水线。每一级只做一点点事,但整体吞吐量能达到线速。

4.3 状态机与并行处理

状态机,这是FPGA里最常用的控制结构。说白了,就是让硬件知道「现在该干什么」。

我画了一张图,展示状态机在FPGA设计中的位置:

FPGA逻辑设计核心知识体系 Verilog/VHDL 硬件描述语言 组合逻辑 + 时序逻辑 流水线设计 面积换速度 寄存器切分组合逻辑 状态机 + 并行 控制流 + 数据流 多路并行处理 高频做市应用:行情解析 → 信号生成 → 订单发送 关键指标 延迟 < 100ns 设计原则 确定性 + 可预测性 常见陷阱 竞争冒险 / 亚稳态

状态机通常分两种:Moore型和Mealy型。Moore型的输出只取决于当前状态,Mealy型的输出还取决于输入。在高频交易里,我更喜欢用Moore型,因为它的输出稳定,不会因为输入抖动而产生毛刺。

来看一个简单的状态机例子——检测UDP包头的状态机:

localparam IDLE      = 2'b00;
localparam HEADER    = 2'b01;
localparam PAYLOAD   = 2'b10;
localparam CHECKSUM  = 2'b11;

reg [1: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 = start_of_frame ? HEADER : IDLE;
        HEADER:   next_state = header_done   ? PAYLOAD : HEADER;
        PAYLOAD:  next_state = payload_done  ? CHECKSUM : PAYLOAD;
        CHECKSUM: next_state = checksum_done ? IDLE : CHECKSUM;
        default:  next_state = IDLE;
    endcase
end

这段代码,说白了就是让FPGA知道:现在在收UDP包的哪个部分。每个状态只做一件事,逻辑清晰,时序也好分析。

我的经验:写状态机时,一定要把状态转移和输出逻辑分开。状态转移用时序逻辑(always @(posedge clk)),输出逻辑用组合逻辑(always @(*))。这样代码可读性强,也方便后期优化时序。

并行处理,这是FPGA的看家本领。CPU是串行的,一个时钟周期只能执行一条指令。FPGA呢?它可以同时做加法、乘法、查表、比较……所有操作都在同一个时钟周期内完成。

举个例子。在行情解析中,你需要同时检查多个价格档位是否满足交易条件。用CPU,你得循环遍历。用FPGA,你可以把每个档位的检查逻辑都复制一份,并行执行。结果在一个时钟周期内就出来了。

注意:并行不是万能的。资源是有限的。一个中等规模的FPGA(比如Xilinx KU060)也就几十万个LUT。如果你把每个逻辑都复制100份,资源很快就用完了。所以,并行度要和资源匹配

我曾经在一个项目中,为了追求极致的低延迟,把行情解析的每个字节都单独做了一个处理单元。结果呢?资源爆了,综合都过不了。后来改成每4个字节一组并行处理,延迟只增加了1个时钟周期,但资源占用降了60%。

4.4 小结:记住这三句话

好了,这一章的内容就这些。我总结三句话,你记住就行:

  • Verilog是画电路的语言,不是写软件。脑子里要有寄存器、组合逻辑、时钟域这些概念。
  • 流水线是用寄存器换频率。延迟增加一点,吞吐量翻倍,值。
  • 状态机管控制,并行管数据。状态机告诉硬件该干什么,并行让硬件同时干很多事。

下一章,我们会深入讲FPGA的时序约束和静态时序分析。那是真正让FPGA跑起来的关键。到时候我会分享一些我在调试时序问题时的血泪史。


公众号:蓝海资料掘金营,微信deep3321