FPGA状态机设计一些避坑要点

文章来源:FPGA入门到精通

1、禁止状态机死锁

必须设置默认跳转逻辑,所有未定义的异常输入条件下,状态机自动跳回初始空闲状态,避免未知状态卡死不动;

2、异步输入信号同步化:外部按键、跨模块异步信号,必须先经过两级触发器同步打拍后,再接入状态机判断,避免亚稳态导致状态跳转错乱;

3、优先使用同步复位

FPGA状态机推荐同步复位,复位逻辑与时序时钟同步,时序更容易收敛,避免异步复位带来的时序毛刺和复位异常;

4、输出逻辑时序化处理

坚决避免组合逻辑直接输出,所有控制输出全部放在时序always块中赋值,彻底消除输出毛刺;

5、状态编码不手动硬写

Verilog代码中使用parameter宏定义状态编码,不直接写二进制数值,便于后期修改状态、维护代码。

代码示例

module fsm_best_practice (
    input  wire       clk,        // 时钟
    input  wire       rst_n,      // 同步复位,低有效(见要点3)
    input  wire       async_btn,  // 外部异步按键输入
    output reg        led_out     // 控制LED输出(时序化,见要点4)
);

    // =================================================
    // 要点5:状态编码全部使用 parameter 宏定义,不手写二进制值
    // =================================================
    // 状态定义(独热码示例)
    parameter IDLE     = 4'b0001;
    parameter PRESSED  = 4'b0010;
    parameter WAIT     = 4'b0100;
    parameter RELEASED = 4'b1000;

    // 状态寄存器
    reg [3:0] current_state, next_state;

    // =================================================
    // 要点2:异步输入信号同步化(两级触发器打拍)
    // =================================================
    reg sync_ff1, sync_ff2;
    wire btn_synced;

    always @(posedge clk) begin
        sync_ff1 <= async_btn;
        sync_ff2 <= sync_ff1;   // 两级同步后,输出稳定的 btn_synced
    end
    assign btn_synced = sync_ff2;  // 仅用于内部读取,实际连接为同步后信号

    // =================================================
    // 要点3:优先使用同步复位
    // 复位逻辑在时序 always 块内随 clk 触发,避免异步复位的时序问题
    // =================================================
    always @(posedge clk) begin
        if (!rst_n)
            current_state <= IDLE;   // 同步复位:状态回到 IDLE
        else
            current_state <= next_state;
    end

    // =================================================
    // 三段式状态机:下一状态组合逻辑
    // 要点1:必须设置 default 跳转逻辑,防止死锁
    // =================================================
    always @(*) begin
        // 默认下一状态为 IDLE,覆盖所有未显式列出的情况
        next_state = IDLE;  // 安全默认值

        case (current_state)
            IDLE: begin
                if (btn_synced)           // 使用同步后的信号
                    next_state = PRESSED;
                else
                    next_state = IDLE;
            end

            PRESSED: begin
                // 按下后进入等待,避免重复触发
                next_state = WAIT;
            end

            WAIT: begin
                if (!btn_synced)          // 等待按键释放
                    next_state = RELEASED;
                else
                    next_state = WAIT;
            end

            RELEASED: begin
                // 释放后返回 IDLE,完成一次按键响应
                next_state = IDLE;
            end

            // default 已在 case 前通过赋值覆盖,这里可省略,但习惯保留
            default: next_state = IDLE;
        endcase
    end

    // =================================================
    // 要点4:输出逻辑时序化处理 —— 全部放在带时钟的 always 块中
    // 坚决避免组合逻辑直接输出,消除毛刺
    // =================================================
    always @(posedge clk) begin
        if (!rst_n) begin
            led_out <= 1'b0;
        end else begin
            // 时序输出:只在进入 PRESSED 状态的同一时钟沿点亮 LED
            // 注意:这里使用 current_state 而非 next_state,保证已稳定跳转
            if (current_state == IDLE && next_state == PRESSED)
                led_out <= 1'b1;
            else if (current_state == RELEASED && next_state == IDLE)
                led_out <= 1'b0;
            // 其余情况保持不变,避免毛刺
        end
    end

endmodule