3. Verilog基础与HFT风格:组合逻辑与时序逻辑、非阻塞赋值、状态机设计、流水线风格
好,咱们进入第三章。这一章,说白了就是FPGA设计的“内功心法”。你可能会觉得Verilog语法谁不会啊?但我要告诉你,在高频交易这个领域,同样的语法,写出来的东西可能天差地别。我见过太多人把FPGA当软件写,结果时序一塌糊涂,跑不到高频。
今天我就带你看看,HFT风格下的Verilog到底该怎么写。咱们不讲那些花里胡哨的语法糖,就讲最核心、最实用的东西。
3.1 组合逻辑与时序逻辑:你得先分清楚
很多新手搞不清楚组合逻辑和时序逻辑的区别。我打个比方:组合逻辑就像一根水管,水(信号)流进去,马上就从另一头流出来。时序逻辑呢?它像一个水桶,你得等时钟沿来了,才能把水舀进去存起来。
在HFT系统里,这个区别至关重要。为什么?因为组合逻辑的延迟是“不可控”的,它完全取决于门电路的传播延迟。而时序逻辑的延迟,是可控的,它由时钟周期决定。
核心原则:在HFT设计中,能用时序逻辑的地方,尽量别用组合逻辑。因为时序逻辑的时序更容易分析和收敛。
举个例子,一个简单的加法器:
// 组合逻辑加法
assign sum = a + b;
// 时序逻辑加法
always @(posedge clk) begin
sum_reg <= a + b;
end
第一个写法,sum的延迟取决于a和b的变化时刻,以及加法器的物理延迟。第二个写法,sum_reg在时钟沿之后才更新,延迟固定为一个时钟周期。在HFT的流水线里,我们更喜欢第二种——因为它好算,好约束。
我的经验:我曾经在一个项目里,为了省一个时钟周期的延迟,用了大量的组合逻辑。结果呢?时序分析报告一片红,最后不得不花了两天时间改代码,把组合逻辑拆成流水线。得不偿失啊!
3.2 非阻塞赋值:这不是选择题,是必答题
在always块里,什么时候用阻塞赋值(=),什么时候用非阻塞赋值(<=)?这个问题,我面试过的人里,十个有八个答不全。
我的规则很简单,就两条:
- 描述组合逻辑(always @(*))时,用阻塞赋值(=)。
- 描述时序逻辑(always @(posedge clk))时,用非阻塞赋值(<=)。
为什么?因为非阻塞赋值模拟了硬件中寄存器的行为——所有赋值在时钟沿同时发生。你想想看,如果在一个时钟沿,多个寄存器同时更新,它们读到的都是“上一个时刻”的值。非阻塞赋值正好模拟了这个过程。
来看一个经典的错误例子:
// 错误写法:在时序逻辑里用了阻塞赋值
always @(posedge clk) begin
b = a;
c = b;
end
这段代码综合出来是什么?是两个串联的寄存器吗?不是!它综合出来只有一个寄存器,因为b和c在同一个时钟沿被连续赋值,c直接拿到了a的值。这跟你的设计意图完全不符。
正确的写法应该是:
// 正确写法:非阻塞赋值
always @(posedge clk) begin
b <= a;
c <= b;
end
这样,b在时钟沿拿到a的值,c在同一个时钟沿拿到b的旧值。这才是两个真正的寄存器。
避坑指南:我曾经在一个复杂的交易引擎里,因为一个always块里混用了阻塞和非阻塞赋值,导致仿真和实际行为不一致。那个bug查了我整整一个下午。从那以后,我每个always块都只用一种赋值方式,绝不混用。
3.3 状态机设计:HFT里的状态机要“快”
状态机是数字电路设计的骨架。在HFT里,状态机的设计风格直接影响系统的吞吐量和延迟。
我常用的状态机风格有两种:
- 一段式:所有逻辑写在一个always块里。简单,但可读性差,不利于维护。
- 三段式:状态转移、次态逻辑、输出逻辑分开写。清晰,易维护,时序好。
在HFT系统里,我强烈推荐三段式。为什么?因为三段式把组合逻辑和时序逻辑分得清清楚楚,综合工具更容易优化,时序也更容易收敛。
来看一个简单的例子:一个交易订单的状态机。
// 三段式状态机示例
// 第一段:状态转移(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
// 第二段:次态逻辑(组合逻辑)
always @(*) begin
case (current_state)
IDLE: begin
if (new_order)
next_state = SENT;
else
next_state = IDLE;
end
SENT: begin
if (ack_received)
next_state = CONFIRMED;
else if (nack_received)
next_state = REJECTED;
else
next_state = SENT;
end
CONFIRMED: next_state = IDLE;
REJECTED: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合逻辑)
assign order_sent = (current_state == SENT);
assign order_done = (current_state == CONFIRMED) || (current_state == REJECTED);
我的习惯:在状态机的输出逻辑里,我尽量用组合逻辑直接输出,而不是在时序逻辑里寄存。这样可以减少一个时钟周期的延迟。当然,前提是时序能收敛。如果时序紧张,我会在输出后面加一级寄存器。
3.4 流水线风格:HFT的“加速器”
流水线,是HFT FPGA加速的核心思想。说白了,就是把一个复杂的计算拆成多个步骤,每个步骤在一个时钟周期内完成。这样,虽然单个数据的延迟增加了,但系统的吞吐量可以做到每个时钟周期输出一个结果。
我画了一张图,帮你理解流水线的概念:
你看,数据1、数据2、数据3在不同的流水线级里同时处理。虽然数据1从输入到输出需要3个时钟周期,但每个时钟周期都能输出一个结果。这就是流水线的威力。
在HFT里,我们经常把订单处理、行情解析、风险检查这些步骤都做成流水线。比如一个简单的订单处理流水线:
// 3级流水线:接收 -> 校验 -> 发送
always @(posedge clk) begin
// Stage 1: 接收
stage1_data <= rx_data;
stage1_valid <= rx_valid;
// Stage 2: 校验
stage2_data <= stage1_data;
stage2_valid <= stage1_valid;
// 在stage2做校验逻辑
check_result <= (stage1_data.price > 0) && (stage1_data.qty > 0);
// Stage 3: 发送
stage3_data <= stage2_data;
stage3_valid <= stage2_valid & check_result;
// 在stage3发送
tx_data <= stage3_data;
tx_valid <= stage3_valid;
end
关键点:流水线的每一级之间必须用寄存器隔开。这些寄存器就是流水线寄存器,它们保证了每一级的计算不会互相干扰。
嗯,说到流水线,我想起一个项目。当时我们做的是一个高频行情解析模块,需要把UDP包里的行情数据解析出来。最开始我们写了一个巨大的组合逻辑块,结果时序跑不到200MHz。后来改成5级流水线,轻松跑到300MHz以上。这就是流水线的价值。
好了,这一章的内容就到这里。记住我今天讲的这几个核心点:分清组合和时序逻辑、用好非阻塞赋值、写三段式状态机、用流水线换吞吐量。这些都是HFT FPGA设计的基石。下一章,咱们聊聊更深入的东西。