【芝士】VLSI第七讲:有限状态机
上一篇我们聊了 Verilog 的向量、存储器和运算符,这些是把数字电路"摆好零件"的基础。这一篇要啃的,是数字电路里最值得花时间的设计模式——有限状态机(FSM, Finite State Machine)。说白了,FSM 就是给硬件画"流程图":用几个离散状态描述系统处在哪个环节,靠外部输入决定下一步跳到哪里。你身边能看到的——自动售货机、交通信号灯、电梯升降控制、键盘消抖电路、CPU 控制器——本质上都是 FSM。================================================================
一、先用直觉理解 FSM:自动售货机
================================================================
想象便利店里的售货机:插币、选商品、出货、找零。它内部其实有几种"状态":
· 待机:等待投币
· 已投币:等待选择商品
· 出货中:正在出货
· 找零中:正在退钱
每一个状态都是一个明确的工作模式;状态之间的跳转靠外部事件(投币、按键、出货完成)触发;每个状态可以绑一些动作(亮灯、出货电机转、找零电磁阀开)。这就是一个典型的 FSM。
FSM 是"表示系统中有限个状态及这些状态之间的转移和动作的模型"。一句话拆成三个关键词:
· 有限个状态:状态数量是有限的(不像模拟电路,状态可以连续取值)
· 状态之间的转移:状态不是凭空变的,必须靠转移条件
· 每个状态可以对应一些动作:状态机可以"输出"一些信号告诉外部
把售货机套回去:状态是"待机/已投币/...",转移条件是"投币/按键/出货完成",动作就是"亮灯/出货/找零"。这一套模型几乎能套到所有"分阶段做不同事情"的电路上。
================================================================
二、Moore 和 Mealy:输出到底看谁?
================================================================
这是 FSM 里最容易混、也是最常被问到的一个分类问题。两种状态机的差别只有一句话:
· Moore 型:输出只取决于当前状态(不看输入)
· Mealy 型:输出取决于当前状态 + 当前输入
================================================================
对比一下配图1和配图2——Moore 型那个输出组合逻辑 C2 没有输入引脚,Mealy 型那个 C2 有输入引脚。这就是结构上的差别,反映到公式上:
Moore:输出 = f(状态)
Mealy:输出 = f(状态, 输入)
再用售货机打个比方:
· Moore 型:售货机"正在出货"的时候,指示灯是亮的——指示灯只跟"出货中"这个状态绑定,跟你按不按键无关
· Mealy 型:售货机在"待机"状态时,你按下"退币"键,立刻退币——输出"退币动作"既要看状态是"待机",又要看你按了"退币"键
实际写代码的时候,Moore 的输出放在状态寄存器后面单独写(一个 always_comb 只看 state_reg),Mealy 的输出放在次态逻辑里(同一个 case 既算 state_next 又算输出)。这两种风格都能跑出对的结果,但波形上有差别:Mealy 的输出比 Moore 早一拍出现,因为输入一变化它就变化,不需要等下一个时钟沿。这一点第六节会用代码实测对比。
[注] 这两种分类不是说谁更好,是看场景。控制类的电路(比如状态指示灯、流程控制)通常用 Moore,反应更稳;需要快速响应的协议电路(比如 UART 接收、串行数据解析)通常用 Mealy,反应更灵敏但要注毛刺。
================================================================
三、状态图怎么画?两种等价画法
================================================================
设计 FSM 的第一步不是写代码,是画状态图。教材给了两种画法,看着不一样,其实是一回事。
3.1 状态转移图(State Transition Diagram)
最直观的那种:每个状态画一个圆圈,圆圈里写状态名和 Moore 输出;从状态 A 到状态 B 画一条带箭头的弧线,弧线上标"什么条件下跳转 + Mealy 输出"。
它的优点是"画起来快、看起来明白",缺点是状态一多(10+)就乱成一团,弧线互相交叉看不清。
3.2 算法状态机图(ASM 图)
更像程序流程图:每个状态用一个矩形框表示(状态框),中间可以夹判断框(菱形)测条件,Mealy 输出用一个圆角框(条件框)放在判断框后边。
它的优点是"层次清楚,能表示复杂的状态转移逻辑",缺点是画起来比较繁琐。教材上是先讲状态转移图再讲 ASM 图,但实际工程里两种往往混用:主流程画状态转移图,复杂判断条件那一段抽出来画 ASM 图。
3.3 选哪种画法
我自己的习惯是:3-5 个状态用状态转移图,5 个以上用 ASM 图。重要的不是"画得多漂亮",而是"状态转移条件是否穷举、是否互斥"。任何一个状态漏掉了某些输入条件的处理,综合器可能会推断出锁存器(latch),这是大坑,仿真能过、综合烧到板子上就出错。
[注] 状态转移图上每条弧线上的条件要互斥。比如状态 A 在 sw=1 时跳 B、sw=0 时跳 C,这两个条件就是互斥的;如果两条弧线都标了 sw=1,综合器会报"条件不互斥"的警告。
================================================================
四、FSM 怎么写代码?二段式写法
================================================================
教材上贴的那段代码其实已经是最规范的写法了,叫"二段式 FSM"——把状态寄存器和次态逻辑分开写。直接上代码(假设是 3 个状态 s0/s1/s2):
// 1. 状态编码
localparam s0 = 2'b00,
s1 = 2'b01,
s2 = 2'b10;
// 2. 状态寄存器(时序逻辑)
always_ff @(posedge clk or posedge reset) begin
if (reset) state_reg <= s0;
else state_reg <= state_next;
end
// 3. 次态逻辑(组合逻辑)
always_comb begin
case (state_reg)
s0: if (a) state_next = s2;
else state_next = s1;
s1: if (a) state_next = s1;
else state_next = s0;
s2: state_next = s0;
default: state_next = s0;
endcase
end
这个写法的好处:
· 状态寄存器清清楚楚就在那里,看到 always_ff 就知道是触发器
· 次态逻辑是纯组合逻辑,用 always_comb 综合器不会出 latch
· 输出可以单独写一个 always_comb(Moore),或者塞在次态逻辑里(Mealy)
// Moore 输出(单独一段,可读性更好)
always_comb begin
case (state_reg)
s0, s1: y1 = 1'b1;
default: y1 = 1'b0;
endcase
end
================================================================
五、状态编码怎么选?二进制 vs 独热码
================================================================
`localparam s0 = 2'b00, s1 = 2'b01, s2 = 2'b10`,这是"二进制编码"。但实际工程里还有另一种主流编码——"独热码(one-hot)":
// 二进制编码
localparam s0 = 2'b00, s1 = 2'b01, s2 = 2'b10;// 2 位
// 独热码
localparam s0 = 3'b001, s1 = 3'b010, s2 = 3'b100;// 3 位
两种都能用,但适合场景不同:
· 二进制编码:状态数多的时候节省触发器(比如 8 个状态用 3 位而不是 8 位)
· 独热码:状态数少的时候综合出的电路更快(每个状态单独一个触发器,译码逻辑简单)
现代 FPGA 的触发器资源基本都不缺(一个 7 系列 FPGA 有几十万个 LUT 和触发器),所以大多数情况下"独热码"是更常用的选择。状态数超过 16 的时候才会考虑二进制。
[注] Vivado 默认会自动把 FSM 优化成独热码,所以在 Vivado 工程里写 `localparam s0 = 2'b00;` 综合出来可能也是独热码。但写代码的时候养成"显式独热"的习惯会更可控、综合报告里也更清楚。
================================================================
六、实战 1:序列检测器 "1101"
================================================================
理论讲完了,看两个实战例子。第一个是经典的"序列检测器"——检测串行输入里有没有出现 "1101" 这个序列。
设计思路很直接:用一个状态记住"已经匹配到第几位了"。
s0:还没收到 1(初始)
s1:收到 1
s2:收到 11
s3:收到 110
s4:收到 1101(命中,输出 1)
每个状态在收到不同输入时跳转到下一个状态。Moore 型需要 5 个状态,最后一个状态专门用来"输出 1"。
完整代码:
localparam s0 = 3'b000,
s1 = 3'b001,
s2 = 3'b010,
s3 = 3'b011,
s4 = 3'b100;
always_ff @(posedge clk or posedge reset)
if (reset) state_reg <= s0;
else state_reg <= state_next;
always_comb begin
case (state_reg)
s0: if (din)state_next = s1; else state_next = s0;
s1: if (din)state_next = s2; else state_next = s0;
s2: if (~din) state_next = s3; else state_next = s2;
s3: if (din)state_next = s4; else state_next = s0;
s4: if (~din) state_next = s1; else state_next = s0;
default: state_next = s0;
endcase
end
// Moore 输出(单独一段,命中 s4 时输出 1)
assign sout = (state_reg == s4);
Mealy 版本的同样逻辑只需要 4 个状态(输出在 s3 上完成):
s0:初始
s1:收到 1
s2:收到 11
s3:收到 110(此时若 din=1 命中,立刻输出 1)
Mealy 输出的关键代码:
s3: if (din) begin state_next = s0; sout = 1'b1; end
else state_next = s0;
实测对比两段代码的波形:Moore 版的 sout 是和时钟同步的(在 s4 状态停留一个周期);Mealy 版的 sout 是异步的(din 一变它就变),可以比 Moore 早一个时钟周期出现命中脉冲。这种时序差别在协议电路里有时是优势(响应快),有时是坑(毛刺可能传到下游组合逻辑)。
[注] Mealy 输出的"sout 比 Moore 早一拍"是个常见的考点。我自己做题的时候经常被问到——记住一个判断技巧:如果输出写在状态寄存器之后(always_comb 只看 state_reg),就是 Moore;写在次态 case 里(看了输入 din),就是 Mealy。
================================================================
七、实战 2:按键消抖
================================================================
第二个例子比序列检测器更贴近实际工程——按键消抖。机械按键按下时会有几毫秒的抖动(金属触点反复弹跳),如果直接读按键信号,单次按可能被识别成多次。
设计思路:用 FSM + 一个 10ms 定时器。定时器每 10ms 滴答一次,FSM 用这个滴答信号来确认"按键信号是否已经稳定了"。如果稳定了,才更新输出。
整个状态机有 8 个状态:
zero :按键稳定在 0
wait1_1~3:等待按键稳定在 1(每过 10ms 推进一步)
one :按键稳定在 1
wait0_1~3:等待按键稳定在 0(每过 10ms 推进一步)
完整代码:
typedef enum logic {
ZERO = 3'b000, WAIT1_1 = 3'b001, WAIT1_2 = 3'b010,
WAIT1_3 = 3'b011, ONE = 3'b100,
WAIT0_1 = 3'b101, WAIT0_2 = 3'b110, WAIT0_3 = 3'b111
} state_t;
state_t state_reg, state_next;
always_ff @(posedge clk or posedge reset)
if (reset) state_reg <= ZERO;
else state_reg <= state_next;
always_comb begin
state_next = state_reg;
db = 1'b0;
case (state_reg)
ZERO: if (sw) state_next = WAIT1_1;
WAIT1_1: if (~sw) state_next = ZERO;
else if(m_tick) state_next = WAIT1_2;
WAIT1_2: if (~sw) state_next = ZERO;
else if(m_tick) state_next = WAIT1_3;
WAIT1_3: if (~sw) state_next = ZERO;
else if(m_tick) state_next = ONE;
ONE: begin db = 1'b1; if (~sw) state_next = WAIT0_1; end
WAIT0_1: begin db = 1'b1; if (sw)state_next = ONE;
else if(m_tick) state_next = WAIT0_2; end
WAIT0_2: begin db = 1'b1; if (sw)state_next = ONE;
else if(m_tick) state_next = WAIT0_3; end
WAIT0_3: begin db = 1'b1; if (sw)state_next = ONE;
else if(m_tick) state_next = ZERO; end
default: state_next = ZERO;
endcase
end
时序图:
设计上有几个关键点:
· 等待过程中如果 sw 出现反向跳变,立即回到原稳定态(防抖的核心)
· 计数器 (m_tick) 只在 sw 保持时推进;sw 一变,前面的等待就作废
· 30ms 的延迟其实比教材说的"20ms 稳定"更保险(抖动通常 < 10ms,留 30ms 是工程上的余量)
这个例子在工程上用得非常多,FPGA 板卡上的按键、旋钮编码器、限位开关几乎都要做这一步。前面写代码生成的那两个文件(debounce_fsm.sv 和 key_debounce.sv)就是这部分的完整可综合实现。
按键消抖的源码放在附件了,设置一币下载主要想统计下下载量
在这也能遇到ic牛马么,可以讲讲axi ahb这些,工作时会用的比较多
页:
[1]