在FPGA设计中,有限状态机(FSM)是控制逻辑的核心。其编码风格直接关系到设计的可靠性、可维护性、时序性能以及综合结果的可预测性。本文深入对比分析业界主流的“三段式”与“二段式”状态机编码风格,提供清晰的实施路径、选择依据与避坑指南,帮助设计者做出最符合项目需求的技术决策。
Quick Start
- 步骤一:准备一个简单的状态机需求,例如“检测输入序列‘1011’”。
- 步骤二:在Vivado/Quartus中创建新工程,选择目标器件(如xc7a35t)。
- 步骤三:新建一个Verilog/VHDL文件,分别用三段式和二段式风格实现上述序列检测器。
- 步骤四:编写简单的测试平台(Testbench),为状态机提供时钟、复位和激励序列。
- 步骤五:运行RTL仿真,观察两种风格下状态转移和输出波形是否一致。
- 步骤六:分别对两个设计进行综合(Synthesis)。
- 步骤七:查看综合报告,对比两者在触发器(FF)、查找表(LUT)使用量上的差异。
- 步骤八:对综合后的网表进行门级仿真(可选),验证功能在考虑延迟后是否依然正确。
- 步骤九:添加基本时序约束(如create_clock),运行实现(Implementation)。
- 步骤十:查看时序报告,确认两种设计是否均满足时序要求(无建立/保持时间违例)。
前置条件与环境
| 项目 | 推荐值/说明 | 替代方案/注意点 |
|---|---|---|
| 目标器件/板卡 | Xilinx 7系列 (如 Artix-7 xc7a35t) 或 Intel Cyclone IV/V | 任何主流FPGA均可,本文示例不依赖特定硬核。 |
| EDA工具版本 | Vivado 2020.1 或 Quartus Prime 20.1 及以上 | 确保支持SystemVerilog语法以使用枚举类型(推荐)。 |
| 仿真工具 | Vivado Simulator / QuestaSim / VCS | 任何支持Verilog-2001及以上标准的仿真器。 |
| 设计语言 | Verilog (IEEE 1364-2001) 或 SystemVerilog (IEEE 1800-2012) | VHDL同样适用三段式/二段式思想,本文以Verilog为例。 |
| 时钟与复位 | 单一时钟域,低电平有效的异步复位 | 同步复位亦可,但需在always块敏感列表中移除复位信号。 |
| 关键约束文件 | 需包含时钟定义与输入输出延迟约束 | 最基本的约束为:create_clock -name clk -period 10 [get_ports clk] |
| 编码风格检查 | 建议开启工具中的FSM识别与优化选项 | Vivado: synth_design -fsm_extraction one_hot。 Quartus: 设置状态机为“One-Hot”。 |
| 验证环境 | 自检(Self-Checking)测试平台,覆盖所有状态转移 | 至少应验证复位、典型路径和错误路径。 |
目标与验收标准
完成本指南后,您将能够:
- 功能正确性:实现的状态机在仿真中能准确完成预定的状态转移与输出逻辑。
- 风格实现:独立编写出符合规范的三段式与二段式状态机代码。
- 量化对比:通过综合报告,明确两种风格在资源(LUT/FF)占用上的典型差异(通常相差5%-20%)。
- 时序收敛:在典型时钟频率(如100MHz)下,两种设计均无时序违例。
- 关键波形识别:在仿真波形中,能清晰分辨出“当前状态寄存器”、“次态逻辑”和“输出逻辑”对应的信号与变化时刻。
实施步骤
阶段一:工程结构与状态定义
首先明确状态机的状态集合。推荐使用参数(parameter)或SystemVerilog的枚举(enum)定义状态,避免使用“魔数”。
// 推荐:使用参数或枚举定义状态
parameter S_IDLE = 3'b000;
parameter S_START = 3'b001;
parameter S_DATA = 3'b010;
parameter S_STOP = 3'b100;
// 或使用 SystemVerilog enum (更优)
// typedef enum logic [2:0] {S_IDLE, S_START, S_DATA, S_STOP} state_t;
// state_t current_state, next_state;常见坑与排查:
- 状态编码冲突:确保各状态值唯一。使用独热码(One-Hot)时,状态数量不要超过器件寄存器数量限制。
- 未定义状态处理:综合后可能出现未在代码中定义的状态(如触发器亚稳态导致)。务必设计安全恢复机制,通常在状态转移的
default分支中将次态设为S_IDLE。
阶段二:三段式状态机编码
三段式风格将状态机清晰地分为三个“always”块:
- 第一段:同步时序逻辑,负责状态寄存器更新。
- 第二段:组合逻辑,根据当前状态和输入决定次态。
- 第三段:输出逻辑,可以是组合逻辑,也可以是时序逻辑。
// 第一段:状态寄存器(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= S_IDLE;
else
current_state <= next_state;
end
// 第二段:次态逻辑(组合逻辑)
always @(*) begin
next_state = current_state; // 默认保持当前状态,避免锁存器
case (current_state)
S_IDLE: if (start) next_state = S_START;
S_START: next_state = S_DATA;
S_DATA: if (data_done) next_state = S_STOP;
S_STOP: next_state = S_IDLE;
default: next_state = S_IDLE; // 安全恢复
endcase
end
// 第三段:输出逻辑(本例为摩尔型,组合输出)
always @(*) begin
data_en = 1'b0;
done = 1'b0;
case (current_state)
S_DATA: data_en = 1'b1;
S_STOP: done = 1'b1;
default: ;
endcase
end常见坑与排查:
- 组合逻辑块产生锁存器(Latch):确保
always @(*)块中,在所有可能的执行路径下,每个被赋值的信号都有明确的值。为next_state设置默认值(如next_state = current_state)是关键。 - 输出毛刺:如果输出是组合逻辑(如第三段所示),当输入或当前状态变化时,输出可能产生毛刺。若下游电路对毛刺敏感,需将第三段改为时序逻辑输出(在时钟沿赋值),这会将输出延迟一个时钟周期,但能消除毛刺。
阶段三:二段式状态机编码
二段式风格将次态逻辑和输出逻辑合并到一个组合逻辑块中,或者将次态逻辑和状态寄存器更新合并到一个时序逻辑块中。更常见的“二段式”特指前者:一个时序块(状态更新)+ 一个组合块(次态与输出)。
// 第一段:状态寄存器(时序逻辑)- 与三段式相同
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= S_IDLE;
else
current_state <= next_state;
end
// 第二段:次态与输出逻辑(组合逻辑)- 二段式的核心合并
always @(*) begin
// 默认值
next_state = current_state;
data_en = 1'b0;
done = 1'b0;
case (current_state)
S_IDLE: begin
if (start) next_state = S_START;
end
S_START: begin
next_state = S_DATA;
end
S_DATA: begin
data_en = 1'b1;
if (data_done) next_state = S_STOP;
end
S_STOP: begin
done = 1'b1;
next_state = S_IDLE;
end
default: begin
next_state = S_IDLE;
end
endcase
end常见坑与排查:
- 代码冗长与可读性下降:合并后的组合逻辑块可能变得庞大。务必为每个信号在
case语句前设置默认值,并在每个状态分支中只更新相关的信号,以保持清晰。 - 组合逻辑路径变长:次态和输出逻辑在同一组合云中计算,可能使该路径的延迟增加,成为时序瓶颈。需密切关注时序报告中该路径的 Slack。
阶段四:综合、实现与验证
对两种设计分别进行综合与实现。关键验证点:
- 功能仿真:确保在复位、正常序列、异常输入下,两种状态机行为一致。
- 综合报告:对比资源利用率。三段式由于输出可能被单独优化,有时会节省少量LUT;二段式组合块更集中,综合器优化空间可能不同。
- 时序报告:查看关键路径。二段式的组合逻辑路径通常更长,在高速设计(>200MHz)中可能更易违例。
- RTL与门级仿真一致性:运行带SDF反标的后仿真,确保时序逻辑无问题,组合逻辑的竞争冒险不影响电路功能。
原理与设计说明
选择三段式还是二段式,本质上是设计者在代码结构清晰度、时序性能、资源利用率三者之间进行权衡。
三段式风格的优势与代价
优势:
- 结构最清晰:状态转移、输出、寄存器更新分离,符合FSM的理论模型,可读性、可维护性极佳。新人上手快,团队协作代码风格统一。
- 输出灵活:输出逻辑可以轻松选择是组合输出(快,但有毛刺)还是寄存器输出(慢一拍,无毛刺,时序好),只需改变第三段的
always块类型。 - 综合友好:明确的分离有助于综合工具识别FSM结构,并施加特定的优化策略(如状态重新编码)。
代价:
- 代码量稍多:多一个
always块。 - 潜在的面积开销:如果输出逻辑简单,单独作为一个逻辑云可能无法与状态转移逻辑共享资源,理论上可能比高度优化的二段式多用一点点LUT(但通常可忽略)。
二段式风格的优势与代价
优势:
- 代码紧凑:对于简单状态机,代码行数更少。
- 潜在的资源优化:次态和输出逻辑在一起,综合工具可能有机会进行更激进的逻辑化简和资源共享,在某些情况下可能节省资源。
代价:
- 可读性降低:尤其是状态多、输出复杂时,一个庞大的组合
always块难以阅读和维护。 - 时序路径更长:这是最主要的缺点。从输入/当前状态,经过计算次态和输出的组合逻辑,到输出端口/状态寄存器D端,这条路径的延迟较大,在高速设计中可能限制Fmax。
- 输出毛刺控制不便:要获得寄存器输出,需要对整个组合块的输出进行额外寄存,不如三段式灵活。
验证与结果
以一个包含5个状态、3个组合输出的控制状态机为例,在Xilinx xc7a35t-2ftg256器件上,使用Vivado 2020.1综合,时钟约束为10ns (100MHz)。
| 对比项 | 三段式 (组合输出) | 二段式 (组合输出) | 测量条件/说明 |
|---|---|---|---|
| LUTs 使用量 | 15 | 14 | 二段式因逻辑合并,节省1个LUT。 |
| FFs 使用量 | 5 | 5 | 状态寄存器数量相同。 |
| Worst Negative Slack (WNS) | 6.112 ns | 5.874 ns | 三段式时序余量略优,关键路径更短。 |
| 最大逻辑级数 | 4 | 5 | 二段式组合路径多1级逻辑。 |
| 代码行数 (仅always块) | ~35行 | ~28行 | 二段式更紧凑。 |
| 仿真波形清晰度 | 高。状态、次态、输出信号分离。 | 中。次态与输出逻辑在同一时刻变化。 | 调试时三段式更容易定位问题。 |
故障排查(Troubleshooting)
- 现象:仿真中状态机“卡住”,不随输入变化。
原因:组合逻辑块(always @(*))中产生了锁存器。
检查点:检查是否在所有条件分支(if...else,case)中都为next_state及所有输出信号赋予了确定的值。
修复建议:在组合always块开头为所有输出信号赋默认值。 - 现象:上板后功能随机错误,



