每日签到 地址发布 老王说明书 宣传中心 繁/简
此板块只作为纯讨论

正经话题,不搞色情!贤者时间必备
查看: 1.4K|回复: 1
收起左侧

[学习讨论] 【芝士】VLSI第七讲:有限状态机

[复制链接]
发表于 2026-8-10 09:55:03 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?免费注册

x
上一篇我们聊了 Verilog 的向量、存储器和运算符,这些是把数字电路"摆好零件"的基础。这一篇要啃的,是数字电路里最值得花时间的设计模式——有限状态机(FSM, Finite State Machine)。说白了,FSM 就是给硬件画"流程图":用几个离散状态描述系统处在哪个环节,靠外部输入决定下一步跳到哪里。你身边能看到的——自动售货机、交通信号灯、电梯升降控制、键盘消抖电路、CPU 控制器——本质上都是 FSM。


================================================================
一、先用直觉理解 FSM:自动售货机
================================================================

想象便利店里的售货机:插币、选商品、出货、找零。它内部其实有几种"状态":

  · 待机:等待投币
  · 已投币:等待选择商品
  · 出货中:正在出货
  · 找零中:正在退钱

每一个状态都是一个明确的工作模式;状态之间的跳转靠外部事件(投币、按键、出货完成)触发;每个状态可以绑一些动作(亮灯、出货电机转、找零电磁阀开)。这就是一个典型的 FSM。

FSM 是"表示系统中有限个状态及这些状态之间的转移和动作的模型"。一句话拆成三个关键词:

  · 有限个状态:状态数量是有限的(不像模拟电路,状态可以连续取值)
  · 状态之间的转移:状态不是凭空变的,必须靠转移条件
  · 每个状态可以对应一些动作:状态机可以"输出"一些信号告诉外部

把售货机套回去:状态是"待机/已投币/...",转移条件是"投币/按键/出货完成",动作就是"亮灯/出货/找零"。这一套模型几乎能套到所有"分阶段做不同事情"的电路上。

================================================================
二、Moore 和 Mealy:输出到底看谁?
================================================================

这是 FSM 里最容易混、也是最常被问到的一个分类问题。两种状态机的差别只有一句话:

  · Moore 型:输出只取决于当前状态(不看输入)
  · Mealy 型:输出取决于当前状态 + 当前输入

1-Moore型状态机.png
================================================================
2-Mealy型状态机.png

对比一下配图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)

4-按键防抖状态机.png

最直观的那种:每个状态画一个圆圈,圆圈里写状态名和 Moore 输出;从状态 A 到状态 B 画一条带箭头的弧线,弧线上标"什么条件下跳转 + Mealy 输出"。

它的优点是"画起来快、看起来明白",缺点是状态一多(10+)就乱成一团,弧线互相交叉看不清。

3.2 算法状态机图(ASM 图)

3-算法状态机(ASM)图.png

更像程序流程图:每个状态用一个矩形框表示(状态框),中间可以夹判断框(菱形)测条件,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 [1:0] 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 [2:0] 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 [2:0] {
      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

时序图:
5-仿真结果.png

设计上有几个关键点:

  · 等待过程中如果 sw 出现反向跳变,立即回到原稳定态(防抖的核心)
  · 计数器 (m_tick) 只在 sw 保持时推进;sw 一变,前面的等待就作废
  · 30ms 的延迟其实比教材说的"20ms 稳定"更保险(抖动通常 < 10ms,留 30ms 是工程上的余量)

这个例子在工程上用得非常多,FPGA 板卡上的按键、旋钮编码器、限位开关几乎都要做这一步。前面写代码生成的那两个文件(debounce_fsm.sv 和 key_debounce.sv)就是这部分的完整可综合实现。


按键消抖的源码放在附件了,设置一币下载主要想统计下下载量


key_debounce.zip

2.77 KB, 下载次数: 0

售价: 1 软妹币    [购买]

评分

参与人数 1软妹币 +150 收起 理由
navebayes + 150

查看全部评分

本帖被以下淘专辑推荐:

回复

使用道具 举报

发表于 2026-8-10 21:44:24 | 显示全部楼层
在这也能遇到ic牛马么,可以讲讲axi ahb这些,工作时会用的比较多
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 免费注册
点击进行验证

本版积分规则

我们不生产资源,只做资源的搬运工。

广告合作-tags标签-app下载-Archiver-小黑屋-