Verilog中状态机编码方式选择对FPGA面积和速度的权衡

FPGA小白
文章2026-07-13
27 0 0 0

Quick Start

  1. 打开 Vivado(2024.2 或更新版本),创建一个新工程,器件选择 XC7K325T-2FFG900。
  2. 在工程中新建一个 Verilog 文件,命名为 fsm_encoding_demo.v。
  3. 编写一个 4 状态(S0, S1, S2, S3)的简单状态机,使用独热码(one-hot)编码。
  4. 运行综合(Synthesis),查看综合后的资源利用率报告(LUT、FF 数量)。
  5. 修改状态机编码为二进制码(binary),重新综合,对比资源变化。
  6. 对两种编码分别运行实现(Implementation),查看时序报告(WNS、Fmax)。
  7. 观察并记录:独热码通常使用更多 FF、更少 LUT;二进制码使用更少 FF、更多 LUT。
  8. 验收点:独热码 Fmax 高于二进制码约 10%~30%(示例值,以实际综合结果为准)。

前置条件与环境

项目推荐值说明替代方案
器件/板卡Xilinx Kintex-7 (XC7K325T)主流中高端 FPGA,LUT/FF 资源丰富Artix-7 / Spartan-7(资源受限时效果更明显)
EDA 版本Vivado 2024.2支持综合选项 -fsm_extraction 与 -fsm_encodingVivado 2018.3+ / ISE(已过时,不推荐)
仿真器Vivado Simulator 或 ModelSim用于功能仿真验证状态机跳转正确性VCS / Questa
时钟/复位100 MHz 系统时钟,异步复位(高有效)确保时序约束真实反映设计50 MHz / 200 MHz
接口依赖无外部接口,纯内部状态机避免 I/O 时序干扰对比可添加简单输出指示(如 LED)
约束文件XDC 文件:create_clock -period 10.000 [get_ports clk]时钟周期 10 ns,对应 100 MHz按实际板卡调整

目标与验收标准

  • 功能点:状态机在两种编码下均能正确完成 S0→S1→S2→S3→S0 循环跳转,输出信号(如 state_out)与预期一致。
  • 性能指标:
    • 独热码实现:WNS ≥ 0(无时序违例),Fmax ≥ 150 MHz(示例值)。
    • 二进制码实现:WNS ≥ 0,Fmax ≥ 120 MHz(示例值,通常低于独热码)。
  • 资源对比:
    • 独热码:FF 数量 ≈ 4(每个状态 1 个 FF),LUT 数量 ≤ 8。
    • 二进制码:FF 数量 ≈ 2(2 位寄存器),LUT 数量 ≤ 12。
  • 验收方式:运行综合后查看“Utilization Report”中的 Slice LUTs 与 Slice Registers 数量;运行实现后查看“Timing Summary”中的 WNS 与 Fmax。

实施步骤

工程结构与关键模块

创建一个顶层模块 fsm_encoding_demo,内部实例化两个子模块:fsm_onehot 和 fsm_binary。两个子模块实现相同的状态跳转逻辑,仅在编码方式上不同。顶层模块提供时钟、复位和使能信号,并输出两个状态机的当前状态。

// fsm_encoding_demo.v - 顶层模块
module fsm_encoding_demo (
    input  wire       clk,
    input  wire       rst_n,      // 低有效复位
    input  wire       en,         // 使能信号
    output wire [3:0] state_onehot,  // 独热码状态输出(4位)
    output wire [1:0] state_binary   // 二进制状态输出(2位)
);

    // 实例化独热码状态机
    fsm_onehot u_onehot (
        .clk     (clk),
        .rst_n   (rst_n),
        .en      (en),
        .state   (state_onehot)
    );

    // 实例化二进制码状态机
    fsm_binary u_binary (
        .clk     (clk),
        .rst_n   (rst_n),
        .en      (en),
        .state   (state_binary)
    );

endmodule

逐行说明

  • 第 1 行:注释,标明文件名和模块功能。良好的注释习惯有助于代码维护。
  • 第 2 行:模块声明,端口列表开始。模块名与文件名保持一致,便于工具识别。
  • 第 3 行:时钟输入,wire 类型,默认。时钟是时序逻辑的驱动源。
  • 第 4 行:复位输入,低有效(rst_n 中的 n 表示 negative)。低有效复位在 FPGA 中常见,与高有效复位在综合时可能映射到不同的复位网络。
  • 第 5 行:使能输入,用于控制状态机是否跳转。使能信号可以降低动态功耗。
  • 第 6 行:独热码状态输出,4 位宽。4 个状态对应 4 位,每次只有 1 位为 1。
  • 第 7 行:二进制状态输出,2 位宽。2 位可以表示 4 个状态。
  • 第 9 行:实例化独热码状态机模块。实例名 u_onehot 应具有描述性。
  • 第 10–14 行:端口连接,使用 .端口名(信号) 语法。注意顶层信号与子模块端口名称一致,避免混淆。
  • 第 16 行:实例化二进制码状态机模块。
  • 第 17–21 行:端口连接,与独热码实例类似。
  • 第 23 行:模块结束。

独热码状态机实现

// fsm_onehot.v - 独热码状态机
module fsm_onehot (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    output reg  [3:0] state
);

    // 状态定义:独热码
    localparam S0 = 4'b0001,
               S1 = 4'b0010,
               S2 = 4'b0100,
               S3 = 4'b1000;

    // 状态跳转逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= S0;
        else if (en)
            case (state)
                S0: state <= S1;
                S1: state <= S2;
                S2: state <= S3;
                S3: state <= S0;
                default: state <= S0;
            endcase
    end

endmodule

逐行说明

  • 第 1 行:注释,标明模块名称和编码方式。
  • 第 2 行:模块声明。
  • 第 3–6 行:端口声明,与顶层模块一致。
  • 第 7 行:output reg [3:0] state,状态寄存器声明为 reg 类型,因为在 always 块中被赋值。独热码需要 4 位寄存器。
  • 第 9–12 行:使用 localparam 定义状态常量,独热码每个状态只有 1 位为 1。S0=4’b0001, S1=4’b0010, S2=4’b0100, S3=4’b1000。注意位宽与状态数匹配。
  • 第 14 行:always 块,敏感列表包含时钟上升沿和复位下降沿(因为 rst_n 低有效)。
  • 第 15 行:复位条件 if (!rst_n),当复位信号为低时执行。
  • 第 16 行:复位状态为 S0(4’b0001)。
  • 第 17 行:else if (en),使能有效时才跳转,否则保持当前状态。
  • 第 18–23 行:case 语句,根据当前状态决定下一状态。注意 default 分支将非法状态引导到 S0,增强鲁棒性。
  • 第 25 行:模块结束。

二进制码状态机实现

// fsm_binary.v - 二进制码状态机
module fsm_binary (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    output reg  [1:0] state
);

    // 状态定义:二进制码
    localparam S0 = 2'b00,
               S1 = 2'b01,
               S2 = 2'b10,
               S3 = 2'b11;

    // 状态跳转逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= S0;
        else if (en)
            case (state)
                S0: state <= S1;
                S1: state <= S2;
                S2: state <= S3;
                S3: state <= S0;
                default: state <= S0;
            endcase
    end

endmodule

逐行说明

  • 第 1–6 行:模块声明与端口,与独热码版本类似,但 state 位宽为 2 位。
  • 第 7 行:output reg [1:0] state,2 位寄存器即可表示 4 个状态。
  • 第 9–12 行:二进制编码:S0=2’b00, S1=2’b01, S2=2’b10, S3=2’b11。编码紧凑,但状态跳转时可能有多位同时变化(如 S1→S2:01→10,两位都变)。
  • 第 14–25 行:always 块结构与独热码完全相同,仅状态位宽不同。注意 case 语句中比较的 state 是 2 位,与独热码的 4 位不同。

综合与实现设置

在 Vivado 中,综合选项可以强制指定编码方式,但为了对比公平,建议让工具自动推断。在“Synthesis Settings”中,将 -fsm_extraction 设为 one_hot 或 binary 可以强制编码。本实验中,我们让工具自动选择(auto),然后通过修改 RTL 编码来对比。

# 约束文件 top.xdc
create_clock -period 10.000 [get_ports clk]
set_property PACKAGE_PIN ... [get_ports clk]   ;# 根据板卡修改
set_property IOSTANDARD LVCMOS33 [get_ports clk]

逐行说明

  • 第 1 行:创建时钟约束,周期 10 ns,对应 100 MHz。这是时序分析的基础。
  • 第 2 行:指定时钟引脚的位置,需要根据实际板卡原理图修改。如果只是纯仿真,可以省略。
  • 第 3 行:设置 I/O 标准为 LVCMOS33,与板卡电压匹配。

常见坑与排查

  • 坑 1:复位极性错误。如果顶层使用高有效复位而子模块使用低有效,状态机可能无法正确复位。检查所有模块的复位极性一致。
  • 坑 2:状态位宽不匹配。独热码状态位宽必须等于状态数,二进制码位宽必须 ≥ log2(状态数)。位宽错误会导致综合错误或仿真异常。
  • 坑 3:缺少 default 分支。case 语句没有 default 会生成锁存器或导致综合警告。始终添加 default 分支,将非法状态引导到已知状态。
  • 坑 4:使能信号未同步。如果 en 来自异步域,需要先同步再使用,否则可能导致亚稳态。本实验 en 与 clk 同源,无需同步。

验证与仿真

编写 testbench 验证两种状态机的功能。关键点:复位后状态为 S0;每个时钟上升沿且 en=1 时状态跳转;en=0 时状态保持。

// testbench.v
module tb_fsm_encoding;

    reg  clk, rst_n, en;
    wire [3:0] state_onehot;
    wire [1:0] state_binary;

    fsm_encoding_demo u_dut (
        .clk          (clk),
        .rst_n        (rst_n),
        .en           (en),
        .state_onehot (state_onehot),
        .state_binary (state_binary)
    );

    initial begin
        clk = 0;
        forever #5 clk = ~clk;  // 100 MHz
    end

    initial begin
        rst_n = 0; en = 0;
        #20 rst_n = 1;
        #10 en = 1;
        #100 en = 0;
        #40 en = 1;
        #80 $finish;
    end

    // 监控状态变化
    always @(posedge clk) begin
        if (en) begin
            $display("Time=%0t: binary=%b", $time, state_onehot, state_binary);
        end
    end

endmodule

逐行说明

  • 第 1 行:testbench 模块,无端口。
  • 第 3 行:声明激励信号为 reg 类型。
  • 第 4–5 行:声明观察信号为 wire 类型。
  • 第 7–13 行:实例化待测设计(DUT)。
  • 第 15–18 行:时钟生成,周期 10 ns,占空比 50%。
  • 第 20–25 行:复位和使能时序:先复位 20 ns,然后使能 100 ns,再关闭使能 40 ns,再使能 80 ns 后结束。
  • 第 27–31 行:在每个时钟上升沿打印两个状态机的状态,用于观察是否同步跳转。

原理与设计说明

为什么独热码更快?

独热码每次状态跳转时,只有 2 位发生变化(当前状态位从 1→0,下一状态位从 0→1)。在 FPGA 中,状态寄存器的输出直接连接到下一状态译码逻辑。由于每个状态对应一个独立的触发器,译码逻辑只需要比较少数几个位(通常只需要检查当前状态位是否为 1),因此组合逻辑级数少,路径延迟小,Fmax 更高。

二进制码在状态跳转时可能有多位同时变化(例如从 01→10,两位都翻转)。译码逻辑需要比较所有状态位,组合逻辑更复杂,路径延迟更大。此外,多位同时变化时可能产生毛刺(glitch),虽然寄存器采样可以消除,但组合逻辑输出不稳定会增加动态功耗。

为什么二进制码面积更小?

二进制码使用 log2(N) 个触发器表示 N 个状态,而独热码使用 N 个触发器。对于 4 状态机,二进制码需要 2 个 FF,独热码需要 4 个 FF。状态数越多,差距越大(例如 16 状态:二进制码 4 FF,独热码 16 FF)。

但独热码的译码逻辑更简单,使用的 LUT 更少。在资源受限的设计中(如 LUT 数量是瓶颈),独热码可能反而总面积更小。实际工程中需要综合后对比。

Trade-off 总结

编码方式FF 数量LUT 数量Fmax适用场景
独热码高(N 个)低高高速设计、状态数少(≤16)
二进制码低(log2(N) 个)高低面积敏感、状态数多(≥16)
格雷码低(log2(N) 个)中中跨时钟域传输(每次仅 1 位变化)

对于 4~8 个状态,独热码通常是最佳选择。对于 16 个以上状态,二进制码或格雷码更优。具体选择应结合综合报告和时序约束。

验证与结果

以下是在 Vivado 2024.2 中,对 XC7K325T-2FFG900 综合实现后的示例结果(实际值以您的工程为准)。

指标独热码二进制码测量条件
Slice LUTs610仅状态机逻辑,无其他模块
Slice Registers (FF)42状态寄存器
Fmax (MHz)185142时钟约束 10 ns,WNS ≥ 0
Worst Negative
分类
技术分享
标签
Verilog状态机编码方式
浏览 27收藏 0赞 0评论 0
分享:

相关推荐

同频道 · 相近分类

暂无相关推荐

作者

FPGA小白查看主页

同分类阅读

文章

延伸阅读与实操

  • 文章 + 课程联动深度文章常对应体系课章节,可一键选课。
  • 学习产出可参考笔记与作业案例在学习产出广场持续更新。

探索全站