4. Verilog基础回顾(下):时序逻辑、状态机与Testbench
好,咱们接着聊。上一节我们把组合逻辑捋了一遍,这一节重点就是时序逻辑了。说白了,组合逻辑的输出只取决于当前输入,而时序逻辑——嗯,它得“记住”过去的状态。这在FPGA里太关键了,因为高频交易系统本质上就是一个巨大的状态机,每个时钟周期都在判断“现在该干什么”。
4.1 时序逻辑的核心:时钟与复位
时钟,就是FPGA的心跳。所有时序逻辑都跟着时钟沿走。我个人习惯用上升沿敏感,也就是 posedge clk。为什么?因为大多数开发板、IP核都默认上升沿,保持一致能少踩坑。
核心原则:所有时序逻辑的赋值,都应该在 always @(posedge clk) 或 always @(negedge clk) 块里完成。千万别把组合逻辑和时序逻辑混在一个always块里,除非你很清楚自己在做什么。
复位呢?分两种:同步复位和异步复位。我见过不少新手在这上面翻车。
| 类型 | 写法 | 特点 |
|---|---|---|
| 同步复位 | always @(posedge clk)if (!rst_n) ... |
只在时钟沿检查复位,抗毛刺好 |
| 异步复位 | always @(posedge clk or negedge rst_n) |
复位信号立即生效,但容易受毛刺影响 |
我的建议:在交易系统里,我倾向于用异步复位、同步释放。既保证了复位及时性,又避免了亚稳态。具体做法是先把异步复位打两拍,再送给逻辑使用。
我曾经在一个高频行情解析模块里,因为复位信号没做同步处理,导致系统在上电瞬间频繁误触发。查了两天才定位到问题——从那以后,复位同步就成了我的“肌肉记忆”。
4.2 状态机设计(FSM)
状态机,说白了就是“根据当前状态和输入,决定下一步去哪”。FPGA里最常用的就是三段式状态机。为什么是三段?因为清晰、好维护、时序好收敛。
三段分别是:
- 第一段:时序逻辑,描述状态跳转(当前态 → 次态)
- 第二段:组合逻辑,描述次态逻辑(根据输入和当前态,算出次态)
- 第三段:时序逻辑,描述输出逻辑(根据当前态,输出控制信号)
你想想看,如果把状态跳转和输出混在一起写,后期要改一个输出条件,你可能得把整个always块重写。三段式就灵活多了。
// 三段式状态机示例:简单UART接收状态机
// 第一段:状态跳转
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: if (rx_start) next_state = START;
else next_state = IDLE;
START: next_state = DATA;
DATA: if (bit_cnt == 7) next_state = STOP;
else next_state = DATA;
STOP: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rx_done <= 1'b0;
else if (current_state == STOP)
rx_done <= 1'b1;
else
rx_done <= 1'b0;
end
注意:第二段是组合逻辑,一定要用 @(*) 或者 @(current_state or 输入信号),别写成 @(posedge clk)。否则综合出来就是时序逻辑,功能全乱。
我在做交易系统的TCP卸载引擎时,状态机有十几个状态。刚开始用两段式,结果每次加功能都提心吊胆。后来全部重构为三段式,代码量虽然多了点,但可读性和可维护性提升了一个档次。
4.3 同步设计与异步设计
同步设计,就是所有寄存器都在同一个时钟域里工作。异步设计,就是信号跨时钟域传递。在FPGA里,90%的问题都出在异步处理上。
为什么?因为亚稳态。当一个信号从一个时钟域进入另一个时钟域时,如果刚好在时钟沿附近变化,寄存器可能进入“既不是0也不是1”的中间状态。这个状态可能会传播出去,导致整个逻辑混乱。
解决办法?最经典的就是两级同步器:
// 跨时钟域同步:慢时钟域到快时钟域
always @(posedge clk_fast or negedge rst_n) begin
if (!rst_n) begin
sync_reg1 <= 1'b0;
sync_reg2 <= 1'b0;
end else begin
sync_reg1 <= data_slow;
sync_reg2 <= sync_reg1;
end
end
assign data_fast = sync_reg2;
打两拍之后,亚稳态的概率就降低到了可以忽略的程度。当然,如果是多bit数据跨时钟域,那就得用异步FIFO或者握手协议了。
避坑指南:我曾经在一个项目里,把跨时钟域的单bit信号直接用了,没做同步。结果系统跑着跑着就随机死机。后来用SignalTap抓波形,发现那个信号在时钟沿附近跳变,导致状态机进入了非法状态。从那以后,所有跨时钟域信号,我必加两级同步器,哪怕只是控制信号。
4.4 Testbench编写基础
写Testbench,说白了就是“模拟真实环境,看你的设计对不对”。我见过很多人写Testbench特别随意,结果仿真通过,上板就挂。其实Testbench也是有套路的。
一个合格的Testbench至少包含:
- 时钟生成:用
always #5 clk = ~clk;生成周期10ns的时钟 - 复位逻辑:先拉低复位,再释放
- 激励输入:模拟真实场景的输入序列
- 自动比对:用
$display或assert检查输出是否正确
// 简单Testbench示例
module tb_uart_rx();
reg clk;
reg rst_n;
reg rx_line;
wire rx_done;
wire [7:0] rx_data;
// 实例化待测模块
uart_rx u_dut (
.clk (clk),
.rst_n (rst_n),
.rx_line(rx_line),
.rx_done(rx_done),
.rx_data(rx_data)
);
// 时钟生成:100MHz
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 复位与激励
initial begin
rst_n = 0;
rx_line = 1;
#100;
rst_n = 1;
#20;
// 模拟UART起始位
rx_line = 0;
#87; // 波特率115200对应的位时间
// 发送数据0x55
rx_line = 1; #87;
rx_line = 0; #87;
rx_line = 1; #87;
rx_line = 0; #87;
rx_line = 1; #87;
rx_line = 0; #87;
rx_line = 1; #87;
rx_line = 0; #87;
// 停止位
rx_line = 1; #87;
#200;
// 检查结果
if (rx_done && rx_data == 8'h55)
$display("Test PASS: data = 0x%h", rx_data);
else
$display("Test FAIL: expected 0x55, got 0x%h", rx_data);
#100;
$finish;
end
endmodule
关键点:Testbench里不要用 #delay 来模拟组合逻辑延迟。真实FPGA里,组合逻辑延迟是纳秒级的,而仿真里 #delay 是理想延迟。正确的做法是用时钟沿来驱动所有变化。
我个人习惯在Testbench里加一个“自检”逻辑。就是仿真结束后,自动打印出“PASS”或“FAIL”。这样跑回归测试时,一眼就能看出哪个case没过。否则几百个测试用例,你一个个看波形,眼睛会瞎的。
4.5 本章知识体系
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个“思维导图”,复习时对照着看。
嗯,这一节内容不少。时钟复位是基础,状态机是骨架,同步异步是难点,Testbench是验证手段。把这四块吃透了,后面写交易系统的核心逻辑就会顺手很多。
最后说一句:别怕写状态机。刚开始可能觉得麻烦,但等你写过几个复杂的交易引擎后,你会发现状态机就是FPGA的“if-else”。清晰、可控、可调试——这就是为什么高频交易系统里到处都是状态机的原因。
公众号:蓝海资料掘金营,微信deep3321