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的时钟
  • 复位逻辑:先拉低复位,再释放
  • 激励输入:模拟真实场景的输入序列
  • 自动比对:$displayassert 检查输出是否正确
// 简单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 本章知识体系

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个“思维导图”,复习时对照着看。

Verilog基础(下) 时钟与复位 同步复位 异步复位 状态机(FSM) 一段式 三段式(推荐) 两段式 同步与异步设计 同步设计 异步处理(亚稳态) 两级同步器 Testbench编写 时钟/复位生成 激励输入 自动比对 核心:时序逻辑 = 时钟沿 + 寄存器 + 状态

嗯,这一节内容不少。时钟复位是基础,状态机是骨架,同步异步是难点,Testbench是验证手段。把这四块吃透了,后面写交易系统的核心逻辑就会顺手很多。

最后说一句:别怕写状态机。刚开始可能觉得麻烦,但等你写过几个复杂的交易引擎后,你会发现状态机就是FPGA的“if-else”。清晰、可控、可调试——这就是为什么高频交易系统里到处都是状态机的原因。


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