Verilog编程技巧01——如何编写三段式状态机

前言
Verilog编程技巧系列文章将聚焦于介绍Verilog的各种编程范式或者说技巧,编程技巧和编程规范有部分重合,但并非完全一样。规范更注重编码的格式,像变量命名、缩进、注释风格等,而编程技巧则更偏重更直观易读、更便于维护、综合效果更好的Verilog/SV代码写法,像:如何编写状态机、如何进行参数化设计、如何进行流水线设计等。可以说编程技巧就是一些编程套路,熟练掌握这些技巧可帮助我们更高效的完成FPGA开发工作。
本文是Verilog编程技巧系列的第一篇文章,介绍了如何编写三段式状态机。
在FPGA设计中,三段式状态机因其结构清晰、可靠性高等特点,成为实现状态机的最佳方式。本文主要说明了以下问题:
-
如何评价一个状态机写的好不好; -
为什么三段式状态机更好; -
为什么更推荐独热码; -
第三段输出逻辑应该怎么写。
最后,本文分享了Verilog与SV的三段式状态机模板。通过阅读本文,你将能够更深入的理解三段式状态机的写法及其背后的原理。
本文主要参考了Clifford E. Cummings的文章,他是数字电路设计领域的技术先驱,享有一定国际声誉。他的文章可从此网站(界面如下图)下载:http://www.sunburst-design.com/papers/。他的一些关于状态机的文章给本文的一些结论提供了理论和实验支撑。

一. 什么是状态机
状态机是描述系统行为的数学模型。包含状态
(系统某时刻状况)、转移
(状态间切换)、事件
(触发转移因素)和动作
(转移时执行的操作),用于分析和设计系统逻辑,在多领域广泛应用。
依据状态数量,状态机可分为两类:
特征 | 有限状态机(FSM) | 无限状态机(ISM) |
---|---|---|
全称与简称 | Finite State Machine,简称FSM | Infinite State Machine,简称ISM |
状态数量 | 有限 | 无限 |
转移方式 | 离散、明确条件 | 可能连续(如基于变量变化) |
典型应用 | 离散逻辑控制 | 连续系统或复杂动态系统 |
在HDL编码时,我们构建的总是有限状态机,所以这里我们说的状态机默认是指有限状态机
。
依据输出与当前状态的关系,状态机也可分为两类:
状态机类型 | 输出决定因素 | 特点描述 |
---|---|---|
Moore型(摩尔型) | 仅取决于当前状态 | 任意时刻,只要状态确定,输出就确定,输出与输入无关 |
Mealy型(米利型) | 取决于当前状态和输入信号 | 输出不仅和当前状态有关,还受当前输入的影响 |
这两种状态机的命名均来源于人名,看着比较奇怪。
实际应用中,无需刻意去区分状态机属于哪种类型
,没有什么意义。
在Verilog/SV编码中,状态机应用极其广泛,它几乎可以描述所有逻辑,也就是说如果你想,所有的模块的内部逻辑都可以写成状态机。所以,研究如何编写更好的状态机很有必要。
二. 一段、二段、三段与四段式状态机
在Verilog/SV中,状态机按功能可以分为三个部分:
-
状态转移
:负责在不同的状态之间进行切换,它决定了状态机如何从当前状态迁移到下一个状态。 -
状态判断
:根据当前状态和输入信号来确定下一个状态是什么。 -
输出逻辑
:根据当前状态机的状态来产生相应的输出信号。
根据这三部分在代码中用几个always块来描述,可将状态机分为以下几种:
2.1 一段式状态机
状态转移、状态判断与输出逻辑全都写在同一个always块中,称为一段式状态机
。示例如下:
// 状态定义
localparam S0 = 1'b0, S1 = 1'b1;
reg state;
// 状态转移、判断与输出
always @(posedge clk) begin
if (~rstn) {state, out} <= {S0, 1'b0};
else
case (state)
S0: if (in) state <= S1; else state <= S0; out <= 1'b0;
S1: if (in) {state, out} <= {S0, 1'b1}; else state <= S0;
default: {state, out} <= {S0, 1'b0};
endcase
end
出于排版的考虑,去掉了非必要的begin-end,尽量缩短了代码行数,下同。
2.2 二段式状态机
状态转移写在一个时序always块中
,状态判断与输出写在一个组合always块中
,称为二段式状态机。示例如下:
// 状态定义
localparam S0 = 1'b0, S1 = 1'b1;
reg state, next;
// 第一段:时序逻辑,状态转移
always @(posedge clk) begin
if (~rstn) state <= S0;
else state <= next;
end
// 第二段:组合逻辑,次态和输出逻辑
always @(*) begin
case (state)
S0: if (in) {next, out} = {S1, 1'b0}; else {next, out} = {S0, 1'b0};
S1: if (in) {next, out} = {S0, 1'b1}; else {next, out} = {S0, 1'b0};
default: {next, out} = {S0, 1'b0};
endcase
end
2.3 三段式状态机
状态转移写在一个时序always块中
,状态判断写在一个组合always块中
,输出写在一个时序或组合always块中
,称为三段式状态机。示例如下:
// 状态定义
localparam S0 = 1'b0, S1 = 1'b1;
reg state, next;
// 第一段:时序逻辑,状态转移
always @(posedge clk) begin
if (~rstn) state <= S0;
else state <= next;
end
// 第二段:组合逻辑,计算次态
always @(*) begin
case (state)
S0: next = in ? S1 : S0;
S1: next = in ? S0 : S0;
default: next = S0;
endcase
end
// 第三段:时序逻辑,输出逻辑
always @(posedge clk) begin
if (~rstn) out_a <= 1'b0;
else out_a <= (state == S1 && in) ? 1'b1 : 1'b0;
end
// 第三段:组合逻辑,输出逻辑
always @(*) begin
out_b = (state == S1 && in) ? 1'b1 : 1'b0;
end
对于第三段,因为时序逻辑的输出相对组合逻辑会慢一个时钟周期
,如果对输出延时有极高的要求,则可以考虑用组合逻辑。但通常还是建议用时序逻辑
,时序逻辑的输出更加稳定(无竞争和冒险),也更便于整体的时序分析和约束。
2.4 四段式状态机
四段式状态机是将三段式状态机的输出写成两个always块,一个固定为组合always块,写输出使能逻辑;另一个固定为时序always块,写输出逻辑。示例如下:
// 前面与三段式相同,仅第三段输出逻辑并分为两段
// 组合逻辑,输出使能
always @(*) begin
out_en = (state == S1 && in);
end
// 时序逻辑,输出逻辑
always @(posedge clk) begin
if (~rstn)
out <= 1'b0;
else if (out_en)
out <= 1'b1;
else
out <= 1'b0;
end
从实现的功能上来说,三段式和四段式完全相同,但在设计理念
、灵活性
和可维护性
等方面,两者存在明显区别:
对比维度 | 三段式状态机 | 四段式状态机 |
---|---|---|
核心思想 | 输出与状态/输入直接耦合,无中间控制信号。 | 引入输出使能信号(out_en ),分离输出条件与赋值逻辑。 |
代码结构 | 输出逻辑与状态转移混合在时序逻辑中。 | 输出使能(组合逻辑)与输出赋值(时序逻辑)分离,模块化更清晰。 |
输出条件修改 | 需直接修改时序逻辑中的条件判断,可能影响其他逻辑。 | 只需修改组合逻辑中的out_en ,输出赋值逻辑保持不变。 |
扩展性 | 新增输出条件需修改时序逻辑,可能导致冗余。 | 新增条件只需扩展out_en 逻辑,不影响输出赋值部分。 |
适用场景 | 简单场景(如固定序列检测、单一输出控制)。 | 复杂场景(如多条件输出、动态使能控制)。 |
总的来说,四段式状态机在负责场景有一些代码维护上的优势。
根据2019 CummingsSNUG2019SV_FSM 状态机设计.pdf
中的内容,四段式状态机在复杂逻辑的综合中会有一定优势,但三段式代码更加简洁,更符合直观逻辑,所以在FPGA开发中,作者推荐总是优先使用三段式
,仅在复杂场景开发的最后,将三段式改为四段式,以提升综合效果。
综合效果
:指的是综合工具对不同代码写法的优化支持程度。现代综合工具对四段式会有更好的优化效果,使得四段式在资源效率和时序性能方面通常优于三段式,但具体效果还需实测。
综上,针对FPGA开发,本文仅讨论三段式状态机,如需针对复杂场景进行优化,各位同学可根据需要将三段式改为四段式
,并实际比较综合后的资源效率和时序性能确定最终方案。
三. 什么样的状态机是好的状态机?
换句话说,评价一个状态机好坏的标准是什么
?一般来说,可以从以下4个维度进行评价:
-
可维护性:代码易于修改、扩展,适应需求变更。
-
可读性:代码结构清晰、逻辑紧凑,避免冗余,便于阅读理解。
-
可调试性:支持高效的仿真验证,便于在仿真或实测时快速定位问题。
-
综合效果:对综合工具友好,代码在转化为硬件时的面积、速度与功耗等性能指标优异。
上述四种状态机从这四个维度进行评价,表格如下:
指标 | 一段式 | 二段式 | 三段式 | 四段式 |
---|---|---|---|---|
可维护性 | ★ | ★★★ | ★★★ | ★★★ |
可读性 | ★ | ★★★ | ★★★ | ★★ |
可调试性 | ★ | ★★ | ★★★ | ★★★ |
综合效果 | ★★★ | ★ | ★★★ | ★★★ |
说明:
-
一颗星(★)表示“差”,两颗星(★★)表示“良”,三颗星(★★★)表示“优” 。 -
四段式的综合效果在非常复杂场景可能略好于三段式,但绝大多数场景下,三段式的综合效果同样优异,两者区别不大,所以两者都标注为三颗星。
对于一个功能需求来说,最重要的两件事是开发效率
和模块性能
。可维护性、可读性和可调试性影响开发效率,综合效果则影响模块性能。
综上,我们可以得出结论:
-
永远不应该用一段式状态机
:一段式状态机逻辑不清,导致代码难以维护、调试和扩展,极易成为"屎山代码"的一部分。其唯一优势(综合效果高)完全无法抵消后期维护的灾难性代价。即使在非常简单的场景中,也不应该使用。 -
不推荐用二段式状态机
:二段式虽然分离了部分逻辑,但输出仍依赖组合逻辑,存在 输出毛刺风险和 时序收敛困难,且可调试性和综合效果低于三段式,没有独特的应用场景优势。 -
不推荐直接用四段式状态机
:四段式虽然逻辑分层更彻底,但会导致 代码冗余和 开发效率下降。它仅适用于极少数复杂场景(例如多路独立输出需严格时序控制),对大多数设计属于"过度设计"。 -
仅推荐使用三段式状态机
:三段式是经过验证的最佳实践,没有短板。
四. 状态编码的类型和优缺点
状态编码指的是状态机的不同状态用怎样的二进制去表示,状态编码一般有以下几种类型:
-
二进制码(Binary):用二进制自然序列表示状态,N个状态需满足 2^N ≥ S
(S为状态总数)。8个状态编码为000, 001, 010, 011, 100, 101, 110, 111
。 -
格雷码(Gray Code):相邻状态仅有一位不同,形成循环单步跳变。3位格雷码为 000, 001, 011, 010, 110, 111, 101, 100
。 -
独热码(One-Hot):每个状态独占一个比特位,总位数等于状态数(N = S)。3个状态编码为 100, 010, 001
。 -
零空闲独热码(Zero-Idle One-Hot):独热码基础上引入全零( 000...
)作为空闲状态,其他状态为单一高位。5个状态编码为0000(空闲态), 1000, 0100, 0010, 0001
。 -
独冷码(One-Cold):与独热码相反,每个状态由唯一一个低电平位(0)表示,其他位为1。3个状态编码为 011, 101, 110
。 -
约翰逊码(Johnson Code):也称为扭环码(Twisted Ring Code),是一种特殊的环形移位码,相邻状态通过左移或右移,然后补移出位的反码生成,最终形成一个环。3位右移约翰逊码为 000, 100, 110, 111, 011, 001
(循环)。 -
混合编码(Hybrid Encoding):高位独热码 + 低位二进制码。状态数>20时,平衡资源与性能。
在FPGA设计中,零空闲独热码、独冷码与独热码的资源消耗和实现逻辑高度相似,而约翰逊码和混合编码的实际应用场景较为局限。因此,工程中的核心选择通常集中在以下三种编码:二进制码、格雷码和独热码。这三种编码方式的优缺点对比如下表:
指标 | 二进制码(Binary) | 格雷码(Gray Code) | 独热码(One-Hot) |
---|---|---|---|
时序逻辑资源消耗 (触发器,FF) | ★★★★☆ 极低:仅需 ⌈log2(N)⌉ 个触发器(N为状态数) | ★★★☆☆ 低:触发器数与二进制相同,但需额外触发器存储转换码(可选) | ★★☆☆☆ 高:需 N 个触发器,但FPGA中触发器资源充足 |
组合逻辑资源消耗 (LUT) | ★★☆☆☆ 中高:状态跳变需复杂组合逻辑(如状态译码器) | ★★★☆☆ 中:需异或门实现码值转换 | ★★★★☆ 极低:状态跳变仅需单比特操作 |
时序性能 | ★★☆☆☆ 中:多比特翻转限制频率 | ★★★☆☆ 中高:单比特跳变,但转换逻辑增加延迟 | ★★★★★ 最优:单比特跳变,路径最短,适合高频 |
功耗 | ★★☆☆☆ 高:多比特翻转动态功耗大 | ★★★★☆ 低:单比特翻转降低功耗 | ★★★☆☆ 中:静态功耗略高(触发器多),动态功耗低 |
容错性 | ★☆☆☆☆ 差:非法状态多,需额外检测 | ★★★☆☆ 中:非法状态较少 | ★★★★☆ 优:非法状态极易检测(非单比特为1) |
可读性 | ★★★☆☆ 中:自然顺序易理解,跳变逻辑复杂 | ★★☆☆☆ 中低:编码非自然排列,需查表辅助 | ★★★★★ 极优:每个状态对应唯一比特,仿真直观 |
推荐优先级 | ★★☆☆☆ - 资源敏感型设计 - 简单状态机 | ★★★☆☆ - 低功耗/跨时钟域 - 异步FIFO | ★★★★★ - FPGA高频设计 - 状态数≤16 |
星级
说明:
-
★★★★★: 强烈推荐(显著优势,无替代方案) -
★★★★☆: 推荐(优势明显,适用场景明确) -
★★★☆☆: 一般推荐(需权衡利弊) -
★★☆☆☆: 不推荐(仅限特定场景) -
★☆☆☆☆: 避免使用(缺陷明显)
由此可以得出结论
:
-
二进制码(★★☆☆☆): -
仅限极简设计(如ASIC或低成本FPGA),需承担时序和功耗风险。 -
添加输出寄存器过滤毛刺。
-
-
格雷码(★★★☆☆): -
低功耗/跨时钟域专用,如异步FIFO指针同步,但需手动添加转换逻辑。 -
避免在复杂状态机中滥用。
-
-
独热码(★★★★★): -
FPGA设计首选,因LUT资源占用低、时序性能最优、容错性高,完美适配FPGA架构。 -
仅需注意状态数≤16(超过时可混合编码)。
-
所以,在FPGA中进行状态机设计时,总是应该使用独热码
。
五. 第三段输出应该用组合逻辑还是时序逻辑?
指标 | 组合逻辑输出 | 时序逻辑输出 |
---|---|---|
优点 | ✅ |