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设计中的位置:
状态机通常分两种: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