3. 数字电路基础回顾(下):有限状态机(FSM)设计、复位与跨时钟域

好,咱们接着聊。上一节我们把组合逻辑和时序逻辑的基本功过了一遍,这一节要啃的,是数字电路里真正体现“设计”二字的地方——有限状态机、复位策略,还有那个让无数工程师头疼的跨时钟域问题。

这些东西,说白了就是数字系统的“骨架”和“神经系统”。你写RISC-V处理器,从取指到访存,每一步都离不开状态机;整个芯片要稳定工作,复位和时钟域处理必须干净。我当年第一次做多时钟域的项目,就因为少打了一拍同步,结果板子跑起来数据全是乱的,查了整整两天……嗯,从那以后,我再也不敢小看这些基础了。

本章核心脉络: 状态机(Moore vs Mealy)→ 复位策略(同步 vs 异步)→ 跨时钟域基础 → 阻塞与非阻塞赋值深度解析。这四个点环环相扣,是FPGA设计的“内功心法”。
第3章 知识体系结构 数字电路设计核心 FSM: Moore vs Mealy 复位策略 跨时钟域处理 阻塞/非阻塞赋值 状态编码 输出逻辑差异 时序 vs 组合输出 同步复位 异步复位 异步复位同步释放 双级同步器 亚稳态 握手/异步FIFO 组合逻辑 vs 时序 仿真行为差异 综合映射规则

3.1 有限状态机(FSM)设计:Moore与Mealy型

状态机这东西,你写处理器的时候几乎天天见。指令译码、流水线控制、总线仲裁……本质上都是一堆状态在跳转。我个人习惯把状态机分成两类:Moore型和Mealy型。区别在哪?说白了就一句话——输出是否依赖当前输入。

对比项Moore型Mealy型
输出依赖仅当前状态当前状态 + 当前输入
输出时序比状态变化晚一个时钟输入变化立即影响输出
状态数量通常较多通常较少
抗毛刺能力强(输出寄存)弱(组合输出)
典型应用控制通路、安全关键逻辑高速数据通路、协议解析

我在做RISC-V的流水线控制时,大部分控制信号用的是Moore型——比如写回使能、寄存器写地址,这些信号必须稳定一个周期,不能因为输入抖动就乱变。但有些地方,比如分支预测的跳转判断,我用Mealy型,因为输入一来就要立刻决定下一拍取哪条指令,多等一个周期就浪费一个流水线气泡。

来看个简单的例子。一个序列检测器,检测“101”序列:

// Moore型:输出在状态S3时拉高
always @(posedge clk or negedge rst_n) begin
  if (!rst_n) state <= S0;
  else case (state)
    S0: if (din) state <= S1;
    S1: if (!din) state <= S2;
    S2: if (din) state <= S3;
    S3: state <= (!din) ? S0 : S1;
  endcase
end
assign dout = (state == S3);

// Mealy型:状态更少,输出组合产生
always @(posedge clk or negedge rst_n) begin
  if (!rst_n) state <= S0;
  else case (state)
    S0: if (din) state <= S1;
    S1: if (!din) state <= S2;
    S2: if (din) state <= S0;
    default: state <= S0;
  endcase
end
assign dout = (state == S2) & din;  // 组合输出
我的建议: 新手先从Moore型入手,逻辑清晰,时序容易收敛。等你对时序分析有把握了,再在关键路径上用Mealy型来减少状态和延迟。记住一个原则——输出尽量寄存一拍,能省掉很多毛刺带来的麻烦。

3.2 同步复位与异步复位

复位,听起来简单,但坑不少。我见过有人整个芯片只用异步复位,结果复位释放时因为时钟和复位信号到达时间不同,导致寄存器进入亚稳态,整个系统初始化就挂了。

咱们先看两种复位的区别:

特性同步复位异步复位
触发条件时钟有效沿 + 复位有效复位信号有效立即触发
抗毛刺强(时钟采样过滤)弱(直接作用)
时序分析简单,作为数据路径需要分析复位恢复/移除时间
资源占用可能多一个MUX通常使用专用复位引脚
推荐场景大部分内部逻辑全局复位、IO接口

代码写法上,区别很明显:

// 同步复位
always @(posedge clk) begin
  if (!rst_n) q <= 1'b0;
  else        q <= d;
end

// 异步复位
always @(posedge clk or negedge rst_n) begin
  if (!rst_n) q <= 1'b0;
  else        q <= d;
end
我曾经踩过的坑: 异步复位的释放时间如果离时钟沿太近,寄存器可能无法正确采样复位值,导致部分寄存器复位了、部分没复位。解决方案是“异步复位,同步释放”——用一个两级同步器把复位信号同步到时钟域后再释放。这个电路在Xilinx和Altera的器件上都有原语支持,别自己手搓。

3.3 跨时钟域处理基础

现在的FPGA设计,几乎没有单时钟域的项目了。RISC-V核心跑几百兆,外设总线可能只有几十兆,两个时钟域之间传数据,处理不好就是亚稳态、数据错乱、系统死机。

跨时钟域的核心原则就一条:不要直接打信号。你想想看,一个快时钟域的信号,宽度可能只有1个快时钟周期,到慢时钟域里可能根本采不到。反过来,慢时钟域的信号变化,到快时钟域里可能被采到中间跳变沿。

常用的处理方法,按场景分:

  • 单比特控制信号: 两级同步器(打两拍)。这是最基础、最常用的方法。
  • 多比特数据总线: 异步FIFO(格雷码指针 + 双口RAM)。
  • 握手协议: req/ack 机制,适合低速、不定长数据传输。
  • 使能信号展宽: 把快时钟域的脉冲展宽到至少一个慢时钟周期。

两级同步器的代码,其实就几行:

// 两级同步器,用于单比特跨时钟域
always @(posedge clk_dst or negedge rst_n) begin
  if (!rst_n) begin
    sync_reg1 <= 1'b0;
    sync_reg2 <= 1'b0;
  end else begin
    sync_reg1 <= async_in;
    sync_reg2 <= sync_reg1;
  end
end
assign sync_out = sync_reg2;
注意: 两级同步器只能降低亚稳态概率,不能完全消除。对于超高速设计(比如超过500MHz),可能需要三级同步。另外,同步器会增加延迟,如果你的信号对延迟敏感,要考虑用其他方案。

3.4 阻塞赋值与非阻塞赋值深度解析

这个知识点,我每次面试必问。很多人写了几年Verilog,还是搞不清 = 和 <= 的区别。其实没那么玄乎,记住两条黄金法则就行:

  • 组合逻辑用阻塞赋值(=)——立即生效,顺序执行。
  • 时序逻辑用非阻塞赋值(<=)——并行赋值,在时钟沿统一更新。

为什么时序逻辑不能用阻塞赋值?我举个例子你就明白了:

// 错误示范:阻塞赋值写时序逻辑
always @(posedge clk) begin
  a = b;
  c = a;  // 这里c拿到的是a的新值,不是上一拍的值
end

// 正确示范:非阻塞赋值
always @(posedge clk) begin
  a <= b;
  c <= a;  // c拿到的是a的旧值,符合寄存器行为
end

用阻塞赋值写时序逻辑,你会得到一堆奇怪的移位寄存器行为,仿真和综合结果可能不一致。我当年刚学FPGA时,就因为这个bug调了整整一个下午,最后发现是赋值方式用错了。

再来看一个经典场景——移位寄存器:

// 非阻塞赋值:真正的移位寄存器
always @(posedge clk) begin
  shift_reg[0] <= din;
  shift_reg[1] <= shift_reg[0];
  shift_reg[2] <= shift_reg[1];
end

// 阻塞赋值:变成了一个buffer,所有寄存器同时更新为din
always @(posedge clk) begin
  shift_reg[0] = din;
  shift_reg[1] = shift_reg[0];
  shift_reg[2] = shift_reg[1];
end
核心总结: 非阻塞赋值模拟的是硬件寄存器的“采样-保持”行为——在时钟沿采样输入,保持到下一个时钟沿。阻塞赋值模拟的是组合逻辑的“立即传播”行为。两者混用,仿真结果就是一团浆糊。