3. 数字电路基础回顾(下):有限状态机(FSM)设计、复位与跨时钟域
好,咱们接着聊。上一节我们把组合逻辑和时序逻辑的基本功过了一遍,这一节要啃的,是数字电路里真正体现“设计”二字的地方——有限状态机、复位策略,还有那个让无数工程师头疼的跨时钟域问题。
这些东西,说白了就是数字系统的“骨架”和“神经系统”。你写RISC-V处理器,从取指到访存,每一步都离不开状态机;整个芯片要稳定工作,复位和时钟域处理必须干净。我当年第一次做多时钟域的项目,就因为少打了一拍同步,结果板子跑起来数据全是乱的,查了整整两天……嗯,从那以后,我再也不敢小看这些基础了。
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; // 组合输出
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
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;
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