4. Verilog基础语法(下):过程块、时序逻辑、状态机

好,咱们接着聊。上一节我们把Verilog的组合逻辑和连续赋值讲完了,说白了就是那些assignalways @(*)的用法。但真正做交易系统,光有组合逻辑可不行——你得有记忆,得有状态,得知道“上一笔订单成交了没有”。这就引出了我们今天的主角:过程块、时序逻辑、状态机

我个人习惯把这三样东西称为“FPGA设计的骨架”。你想想看,一个低延迟交易系统里,从行情解析到订单生成,哪个环节不需要状态机来调度?哪个环节不需要时序逻辑来锁存数据?嗯,今天我们就把它彻底讲透。

4.1 过程块:always的两种灵魂

Verilog里最核心的过程块就是always。它有两种写法,对应两种完全不同的硬件行为:

  • 组合逻辑型always @(*) —— 敏感列表里没有时钟沿,输出只随输入变化。
  • 时序逻辑型always @(posedge clk) —— 敏感列表里有时钟上升沿或下降沿,输出在时钟边沿更新。

我在项目中遇到过不少新手,把这两种混着写,结果综合出来一堆奇怪的锁存器。记住一个原则:如果你想让信号在时钟边沿变化,就用posedge clk;如果只是组合逻辑,就用@(*)

重要提醒always @(*)里的*是自动推导敏感列表,意思是“所有输入信号变化时都触发”。千万别手动写敏感列表,容易漏信号,导致仿真和综合结果不一致。

4.2 时序逻辑:让电路拥有记忆

时序逻辑的核心是寄存器(Flip-Flop)。在Verilog里,我们通过always @(posedge clk)来生成寄存器。看个最简单的例子:

// 一个带同步复位的D触发器
reg [7:0] data_reg;

always @(posedge clk) begin
    if (rst) begin
        data_reg <= 8'b0;
    end else begin
        data_reg <= data_in;
    end
end

这里有个细节:非阻塞赋值<=。为什么时序逻辑里要用<=而不是=?说白了,非阻塞赋值模拟了硬件寄存器的并行行为——所有赋值在时钟沿同时发生。如果你用阻塞赋值=,那综合出来的结果可能跟你想象的不一样。

我的小技巧:写时序逻辑时,一律用<=。写组合逻辑时,一律用=。这个习惯帮我避免了很多仿真和综合不一致的坑。

4.3 状态机:交易系统的调度核心

状态机(FSM)是低延迟交易系统的灵魂。从TCP连接管理到订单生命周期,从行情快照解析到风险检查,几乎每个模块背后都有一个状态机在默默工作。

我常用的状态机写法是三段式。为什么是三段?因为清晰、好维护、时序好收敛。咱们直接看代码:

// 三段式状态机示例:一个简单的订单状态机
module order_fsm (
    input  wire       clk,
    input  wire       rst,
    input  wire       new_order,
    input  wire       ack_received,
    output reg        send_order,
    output reg        order_done
);

    // 第一段:状态定义
    localparam IDLE      = 2'b00;
    localparam SENDING   = 2'b01;
    localparam WAITING   = 2'b10;
    localparam DONE      = 2'b11;

    reg [1:0] state, next_state;

    // 第二段:状态转移(时序逻辑)
    always @(posedge clk) begin
        if (rst) begin
            state <= IDLE;
        end else begin
            state <= next_state;
        end
    end

    // 第三段:次态逻辑和输出逻辑(组合逻辑)
    always @(*) begin
        // 默认值
        next_state = state;
        send_order = 1'b0;
        order_done = 1'b0;

        case (state)
            IDLE: begin
                if (new_order) begin
                    next_state = SENDING;
                    send_order = 1'b1;
                end
            end
            SENDING: begin
                next_state = WAITING;
            end
            WAITING: begin
                if (ack_received) begin
                    next_state = DONE;
                    order_done = 1'b1;
                end
            end
            DONE: begin
                next_state = IDLE;
            end
            default: begin
                next_state = IDLE;
            end
        endcase
    end

endmodule

你看,三段式把状态寄存器次态逻辑输出逻辑分得清清楚楚。我在做低延迟交易系统时,所有状态机都这么写。为什么?因为调试方便——你可以在波形里直接看到当前状态,也能看到下一状态,定位问题快得很。

我曾经踩过的坑:有一次做订单路由模块,状态机里漏写了default分支。结果在某个极端情况下,状态跳到了一个未定义的值,整个模块死锁了。从那以后,我每个状态机都加上default,哪怕只是回到IDLE状态。

4.4 状态机设计中的常见陷阱

做交易系统,状态机出问题就是真金白银的损失。我总结几个常见陷阱:

  • 状态编码选择:小状态机用二进制编码(节省寄存器),大状态机用独热码(节省组合逻辑,时序更好)。我个人习惯,状态数少于8个用二进制,多于8个用独热码。
  • 输出信号毛刺:组合逻辑输出容易有毛刺。如果输出信号要驱动关键路径(比如发送网络包),建议用寄存器打一拍再输出。
  • 状态机死锁:一定要有超时机制。比如等待ACK的状态,如果一直等不到,应该超时重发或报错。

4.5 本章知识体系

下面这张图把本章的核心逻辑串起来了。你看,从过程块出发,分支出组合逻辑和时序逻辑,而状态机则是两者的结合体:

Verilog基础语法(下)知识体系 过程块 (always) 组合逻辑 always @(*) 时序逻辑 always @(posedge clk) 阻塞赋值 (=) | 敏感列表 | 锁存器避免 非阻塞赋值 (<=) | 寄存器 | 同步复位 状态机 (FSM) 三段式写法 状态编码 死锁避免

嗯,这张图把今天的内容串起来了。你想想看,从always出发,分支出组合逻辑和时序逻辑,最后汇聚到状态机——这就是Verilog数字设计的核心脉络。

好了,今天的内容就到这里。记住,写状态机时多想想“如果这个状态永远等不到下一个事件怎么办”,多留个心眼,你的交易系统就会更稳健。

专注资料整理