4. 逻辑设计基础(二):状态机设计、FSM编码风格与三态门
好,咱们接着聊。上一章我们把组合逻辑和时序逻辑理了一遍,这一章要啃的,是FPGA逻辑设计里真正见功夫的地方——状态机。
说实话,我见过太多工程师,写代码一把好手,一到状态机就翻车。不是跑飞了,就是资源爆了。为什么?因为状态机这东西,看着简单,门道其实很深。
4.1 Moore与Mealy状态机:选哪个?
先搞清楚两个基本概念。Moore机和Mealy机,区别就一句话:输出跟谁有关。
- Moore机:输出只取决于当前状态。说白了,状态变了,输出才变。
- Mealy机:输出取决于当前状态 和 输入。输入一变,输出可能立刻跟着变。
我个人的习惯是:能用Moore就用Moore。为什么?因为Moore机的输出和时钟沿同步,时序上干净利落,不容易出毛刺。我在做星载数传模块时,就吃过Mealy机的亏——输入信号抖动了一下,输出直接冒了个短脉冲,差点把后级电路搞崩。
但Mealy机也不是一无是处。它响应快,同样的功能,Mealy机可能比Moore机少用几个状态。比如你要做一个序列检测器,检测“1011”,用Mealy机可能4个状态就够了,Moore机得5个。
核心原则:
- 对输出时序要求严苛的场合(比如控制信号),优先选Moore
- 对延迟敏感、状态数受限的场合,可以考虑Mealy
- 宇航级设计,我建议默认用Moore,除非有明确理由
来看个简单的Moore机例子——一个两状态的反转电路:
// Moore型状态机示例
module moore_fsm (
input clk,
input rst_n,
input en,
output reg out
);
typedef enum logic {S0, S1} state_t;
state_t state, next_state;
// 状态寄存器
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= S0;
else
state <= next_state;
end
// 次态逻辑
always_comb begin
next_state = state;
case (state)
S0: if (en) next_state = S1;
S1: if (en) next_state = S0;
endcase
end
// 输出逻辑(只与当前状态有关)
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
out <= 1'b0;
else
case (state)
S0: out <= 1'b0;
S1: out <= 1'b1;
endcase
end
endmodule
注意看,输出out是在时钟沿下更新的,这就是Moore机的典型特征。
4.2 FSM编码风格:二进制、格雷码、独热码
状态编码这事儿,很多新手不重视。觉得“反正能跑就行”。嗯,我以前也这么想,直到有一次项目里状态机跑飞了,查了三天才发现是编码方式选错了。
三种常见编码,各有各的脾气:
| 编码方式 | 状态数N所需触发器数 | 速度 | 功耗 | 可靠性 |
|---|---|---|---|---|
| 二进制编码 | log₂(N) | 中等 | 低 | 一般 |
| 格雷码 | log₂(N) | 中等 | 极低 | 较高 |
| 独热码 | N | 高 | 高 | 高 |
二进制编码:最省触发器,但组合逻辑复杂。状态跳转时可能有多位同时变化,容易产生毛刺。我一般只在状态数很少(比如4个以下)时用。
格雷码:相邻状态只有一位变化。这在跨时钟域传输时特别有用。我记得做某个星载接口时,状态机需要跨时钟域传递状态信息,用格雷码编码,直接省掉了一堆同步器。
独热码:每个状态一个触发器,译码逻辑极简单。速度最快,但触发器用得最多。FPGA里触发器资源相对丰富,所以独热码在FPGA设计中很常见。我曾经在某个高速SerDes控制器里,状态机跑了500MHz,不用独热码根本跑不动。
我的建议:
- 宇航级设计,优先考虑独热码或格雷码
- 如果状态数超过16个,独热码的触发器开销太大,可以考虑格雷码
- 如果状态机需要跨时钟域,必须用格雷码
来看个独热码的例子:
// 独热码状态机示例
module onehot_fsm (
input clk,
input rst_n,
input start,
output reg done
);
localparam IDLE = 4'b0001;
localparam BUSY = 4'b0010;
localparam DONE = 4'b0100;
localparam ERROR = 4'b1000;
reg [3:0] state, next_state;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
always_comb begin
next_state = state;
case (1'b1)
state[0]: if (start) next_state = BUSY; // IDLE
state[1]: next_state = DONE; // BUSY
state[2]: next_state = IDLE; // DONE
state[3]: next_state = IDLE; // ERROR
default: next_state = IDLE;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
done <= 1'b0;
else
done <= (state == DONE);
end
endmodule
看到没?case (1'b1)这种写法,就是独热码的典型风格。每个状态只检查对应的那一位,组合逻辑极简单。
注意:独热码状态机一定要写default分支!否则如果因为单粒子翻转导致状态跑飞,状态机可能永远回不来。我曾经在测试中遇到过,一个独热码状态机没写default,辐照试验时状态直接变成了全0,卡死了。从那以后,我的所有状态机都加了default和非法状态检测。
4.3 三态门与双向IO
三态门,说白了就是输出可以有三个状态:0、1、高阻(Z)。高阻态意味着输出引脚被“断开”了,不驱动总线。
在FPGA里,三态门主要用在两个地方:
- 双向IO:比如数据总线,同一根线既当输入又当输出
- 共享总线:多个设备挂在同一根线上,同一时间只能有一个设备驱动
FPGA内部其实没有真正的三态门——内部走的是多路选择器。三态门只存在于IO单元里。所以,不要在FPGA内部逻辑里写三态,那是仿真能过、综合会报错的东西。
来看一个标准的三态双向IO模型:
// 三态双向IO示例
module bidir_io (
inout wire data, // 双向数据线
input dir, // 方向控制:1=输出,0=输入
input data_out, // 要输出的数据
output data_in // 读入的数据
);
// 三态输出缓冲器
assign data = dir ? data_out : 1'bz;
// 输入缓冲器(始终读取)
assign data_in = data;
endmodule
这里的关键是inout wire和1'bz。dir为1时,data输出data_out的值;dir为0时,data变成高阻,此时外部可以驱动这根线,我们通过data_in读到它的值。
宇航级设计中的三态门注意事项:
- 三态门在辐照环境下容易出问题。高阻态可能被辐射干扰成弱0或弱1
- 我建议:能不用的地方尽量不用三态。实在要用,加上弱上拉或弱下拉
- 双向IO的方向切换时,要加死区时间——先让输出变高阻,等一个时钟周期再切换方向
- FPGA内部不要用三态逻辑,综合工具会把它转成MUX,反而浪费资源
我曾经在一个星载数据采集项目中,用了双向IO来复用地址和数据线。第一次流片回来,发现总线冲突——两个设备同时驱动了同一根线。查了半天,是方向控制信号的时序没对齐。后来加了一个周期的死区,问题就解决了。
嗯,这一章的内容就到这儿。状态机设计是FPGA逻辑的核心中的核心,编码方式的选择直接影响可靠性。三态门虽然简单,但用不好就是坑。下一章我们讲同步设计与跨时钟域处理,那才是真正考验功力的地方。