2.2 时序逻辑与有限状态机


2.2 时序逻辑与有限状态机

本节摘要:时序逻辑让电路"记住"历史——靠时钟沿触发、用触发器存状态。本节先建立时钟与复位的正确姿势,再写计数器与分频器热身,最后重点攻克有限状态机(FSM):三种状态编码的取舍、三段式写法,以及 Moore 与 Mealy 的工程选择。读完你能独立写出带状态的数字系统骨架。

电路怎么"记住"昨天的事

组合逻辑没有记忆,这是它最大的短板。计数器、协议状态机、数据缓冲——几乎一切有价值的数字系统都需要"记得自己进行到哪一步"。这个能力来自触发器:它在时钟沿(上升沿或下降沿)把当前输入采样并锁存,输出保持到下一个时钟沿。于是"上电以来的历史"就被压缩成了一串触发器里的比特。

这一节的全部内容,本质上是回答一个问题:如何规范地使用时钟,让电路按你预想的节拍推进。我们从最基础的三件套开始——时钟、复位、非阻塞赋值。

三件套:时钟、复位、<=

计数器:时序逻辑的第一课

// 8 位计数器:每个时钟上升沿加一,计满回零 module counter8( input clk, input rst_n, // 低电平异步复位 output reg [7:0] cnt ); always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= 8'd0; // 复位到 0 else cnt <= cnt + 1'b1; // 非阻塞赋值:下一拍才生效 end endmodule

拆解这段代码的语义:posedge clk or negedge rst_n 表示"时钟上升沿或复位下降沿触发"。复位信号一拉低,cnt 立即清零——异步复位,不等待时钟。正常工作时,每个上升沿采样一次 cnt + 1,但注意 <= 的语义:这一拍采样到的是上一拍的旧值,加完的新值要到下一拍才可见。这就是"寄存器在时钟沿更新"的精确含义。

同步复位还是异步复位

工程上两种复位都常见,各有一批拥趸:

方案 特点 典型风险
异步复位 复位信号一到立即生效,不依赖时钟 复位释放时若不在时钟沿附近,易出现亚稳态
同步复位 复位需等到时钟沿才生效 复位脉冲太短会漏采,且复位逻辑占用额外资源

工程惯例常采用"异步复位、同步释放":用两级触发器把异步复位信号同步后再分发——既保留异步复位的即时性,又规避释放时刻的亚稳态。这个细节到第 4 章讲跨时钟域时会完整展开,现在先记住:复位信号进入芯片后,不要直接连到所有触发器,先过一级同步。

分频器:把时钟切成你需要的样子

板载晶振通常是固定的 50MHz 或 100MHz,而设计里常常需要更慢的节拍。最朴素的办法是计数器分频——用计数器对高频时钟计数,取某个计数位的翻转作为低频时钟。但这里藏着一个新手最容易犯的错误:拿计数器输出位当"时钟"去驱动其他逻辑。

// 错误示范:用分频信号当时钟 always @(posedge clk) begin if (cnt == 24'd25000) begin cnt <= 0; clk_slow <= ~clk_slow; // 产生一个 1kHz 的"时钟" end else begin cnt <= cnt + 1; end end // 后续:always @(posedge clk_slow) ... ← 这种写法强烈不推荐

为什么不好?第一,clk_slow 是由普通触发器生成的信号,到达各触发器的路径延迟不可控,多级使用后时钟偏斜会越来越大;第二,它绕过了 FPGA 专门的全局时钟网络,时序分析工具难以约束。正确做法是用时钟使能:保持全局时钟统一,用一个使能信号控制"这一拍算不算数"。

// 推荐做法:单一时钟 + 时钟使能 module blink_en( input clk, input rst_n, output reg led ); reg [24:0] cnt; wire clk_en = (cnt == 25'd25000000); // 1Hz 使能脉冲 always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= 25'd0; else if (clk_en) cnt <= 25'd0; // 使能拍清零 else cnt <= cnt + 1'b1; end always @(posedge clk or negedge rst_n) begin if (!rst_n) led <= 1'b0; else if (clk_en) led <= ~led; // 每秒翻转一次 end endmodule

两条经验可以带走:全局只用一个时钟域里的主时钟,所有分频节拍用使能信号表达;PLL 产生的真正时钟例外,那属于第 4 章时钟管理的话题。这条规则能省掉你未来一半的时序问题。

有限状态机:数字系统的骨架

当逻辑需要"分阶段办事"——比如串口协议要经历空闲、起始、数据、停止四个阶段——计数器这种直线推进的模型就不够了,需要有限状态机。FSM 由三部分组成:状态寄存器(记住当前状态)、次态逻辑(根据输入算下一状态)、输出逻辑(根据状态和输入产生输出)。

三段式写法

业界最推崇的是三段式:第一段时序逻辑负责状态跳转,第二段组合逻辑计算次态,第三段负责输出。一个交通灯控制器的精简版如下:

// 三段式 FSM:红→绿→黄循环,绿黄两态受按键触发跳变(示意) module traffic_fsm( input clk, input rst_n, input car_wait, // 有车等待信号 output reg red, output reg green, output reg yellow ); localparam S_RED = 2'b00; localparam S_GREEN = 2'b01; localparam S_YELLOW = 2'b10; reg [1:0] state, next_state; // 第一段:状态寄存器(时序) always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= S_RED; else state <= next_state; end // 第二段:次态逻辑(组合) always @(*) begin next_state = state; // 默认保持 case (state) S_RED: next_state = S_GREEN; S_GREEN: if (car_wait) next_state = S_YELLOW; S_YELLOW: next_state = S_RED; default: next_state = S_RED; endcase end // 第三段:输出逻辑(组合) always @(*) begin red = 1'b0; green = 1'b0; yellow = 1'b0; // 默认值,防锁存器 case (state) S_RED: red = 1'b1; S_GREEN: green = 1'b1; S_YELLOW: yellow = 1'b1; endcase end endmodule

三段式的好处是职责清晰:状态寄存器绝对干净(只有时钟和复位驱动)、次态逻辑和输出逻辑各自独立,仿真调试时可以单独观察"为什么状态跳了/没跳"。第二段和第三段都遵循了 2.1 的防呆规则——赋默认值再写分支,不产生锁存器。

Moore 与 Mealy

输出逻辑如果只依赖当前状态,叫 Moore 机;如果还依赖当前输入,叫 Mealy 机。Moore 输出稳定(不随输入毛刺抖动),但需要多等一拍;Mealy 反应快(输入一变输出就变),但输出可能带毛刺。时序要求不苛刻时默认 Moore;需要"输入到来立刻响应"(如总线握手应答)才考虑 Mealy。

状态编码怎么选

编码 触发器数 组合逻辑 适用场景
二进制 最少 较多(需译码) 状态多、逻辑紧
格雷码 中等 状态相邻跳变、跨时钟域传输
独热码 最多(N 状态 N 触发器) 最少(无译码) 状态少(<16)、追求时序

独热码看似浪费,但每个状态就是一个触发器,跳变判断只需查一位,组合逻辑最浅、时序最容易收敛——所以状态机数量少时,工具默认就是独热编码,别去"优化"它。

⚠️ 常见坑:状态机写进 default 分支的缺失,或者状态位宽不足,导致非法状态"卡死"。惯例是在 default 里跳回复位态,并打开工具的非法状态检测。

本节要点回顾

  • 时序三件套:时钟沿采样、非阻塞赋值描述"下一拍生效"、复位给电路一个确定的起点。
  • 复位策略:异步复位、同步释放是工程惯例,复位信号要过同步器再分发。
  • 分频不要造时钟:全局单时钟 + 时钟使能是纪律,自己生成的时钟是时序噩梦。
  • 三段式 FSM:状态寄存器 / 次态逻辑 / 输出逻辑三段分离,默认值防锁存器。
  • Moore 优先:输出只依赖状态最稳;要快才用 Mealy 并承担毛刺。
  • 编码按需选:状态少用独热,状态多用二进制,跨时钟域用格雷。

下一步进入 2.3:模块写出来了,怎么证明它是对的?testbench 与功能仿真登场。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U