2. 数字逻辑基础回顾:组合逻辑与时序逻辑、触发器与寄存器、时钟与复位、有限状态机

各位同学,在正式进入 RISC-V 流水线设计之前,咱们得先把数字逻辑的底子夯实了。你想想看,五级流水线说白了就是一堆组合逻辑和时序逻辑的巧妙组合。如果这些基本概念没吃透,后面遇到时序违例、亚稳态这些问题,你会很头疼的。

我个人习惯,每次开始一个新项目,都会先画一张数字逻辑的思维导图。今天我就带大家快速过一遍这些核心概念,顺便分享一些我在实际项目中踩过的坑。

核心要点: 组合逻辑决定“做什么”,时序逻辑决定“什么时候做”。两者缺一不可。

2.1 组合逻辑 vs 时序逻辑

先说说组合逻辑。它的输出只取决于当前的输入,没有记忆功能。比如一个简单的与门,输入 A 和 B 都是 1,输出就是 1;输入变了,输出立刻跟着变。

时序逻辑就不一样了。它的输出不仅取决于当前输入,还取决于电路之前的状态。说白了,它有“记忆”。触发器就是最典型的时序逻辑单元。

特性 组合逻辑 时序逻辑
输出依赖 仅当前输入 当前输入 + 历史状态
记忆能力 无 有(靠触发器)
典型电路 加法器、多路选择器、译码器 计数器、寄存器、状态机
时钟依赖 不依赖 依赖时钟沿
延迟特性 门延迟(传播延迟) 时钟到输出延迟(Tco)

我在项目中遇到过一个问题:一个同学把组合逻辑的反馈环路当成了时序逻辑来用,结果仿真怎么都不对。记住,组合逻辑的反馈会形成锁存器(Latch),这在大多数 ASIC 设计中是要避免的。

2.2 触发器与寄存器

触发器(Flip-Flop)是时序逻辑的基石。我们最常用的是 D 触发器,它在时钟上升沿采样输入 D,然后输出 Q 保持这个值直到下一个时钟沿。

寄存器呢?其实就是一组 D 触发器并联在一起,共享同一个时钟和复位信号。比如一个 32 位的寄存器,就是 32 个 D 触发器排排坐。

我的小技巧: 写 Verilog 时,always @(posedge clk) 块里赋值的变量,综合出来就是触发器。如果你不小心在组合逻辑块里给同一个变量赋值,综合工具会给你生成锁存器,这可是个大坑。

来看看 D 触发器的 Verilog 描述:

// 带异步复位的 D 触发器
module d_flip_flop (
    input  wire clk,
    input  wire rst_n,
    input  wire d,
    output reg  q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

注意看,我用了非阻塞赋值 <=。这是时序逻辑的标准写法。如果用阻塞赋值 =,仿真行为会完全不同,综合出来的电路也可能不是你想要的。

2.3 时钟与复位

时钟是数字系统的“心跳”。所有时序逻辑都跟着时钟沿走。时钟的频率决定了系统能跑多快,时钟的抖动和偏斜直接影响时序裕量。

复位呢?它让电路回到一个已知的初始状态。常用的复位方式有两种:

  • 异步复位: 复位信号不依赖时钟,只要复位有效,输出立刻清零。优点是响应快,缺点是对复位信号的毛刺敏感。
  • 同步复位: 复位信号只在时钟沿有效时起作用。优点是抗干扰能力强,缺点是需要复位信号保持足够长的时间。

我曾经在一个项目中,因为异步复位信号没有做同步处理,导致芯片在某个极端工况下出现了复位失败。从那以后,我养成了一个习惯:所有异步复位信号进入模块之前,必须用两级触发器做同步。

避坑指南: 我曾经见过一个设计,复位信号直接连到了几千个触发器的复位端,结果复位网络的扇出太大,导致复位时间过长。建议用复位树或者复位同步器来分散负载。

时钟和复位的关系,可以用下面这张图来理解:

时钟与复位结构图 时钟源 时钟缓冲器 触发器1 触发器2 复位源 复位同步器 时钟树保证所有触发器 收到同一时钟沿 复位同步器消除 异步复位的亚稳态

2.4 有限状态机

有限状态机(FSM)是数字设计里最强大的工具之一。它让电路能够“记住”自己处在什么状态,并根据输入决定下一步去哪。

FSM 分两种:

  • Moore 型: 输出只取决于当前状态。比如一个简单的计数器,输出就是当前计数值。
  • Mealy 型: 输出取决于当前状态和输入。比如一个序列检测器,检测到特定序列时输出才拉高。

我个人习惯,在 RISC-V 流水线控制器的设计中,大部分用 Moore 型。为什么?因为 Moore 型的输出更稳定,不会因为输入毛刺而抖动。但 Mealy 型在某些场景下更高效,比如需要快速响应的握手协议。

编码方式的选择: 状态编码有三种常用方式——二进制编码、格雷编码、独热编码。二进制编码最省触发器,但组合逻辑复杂;独热编码正好相反,触发器多但组合逻辑简单。在 FPGA 设计中,我倾向于用独热编码,因为 FPGA 的触发器资源丰富,而且独热编码的译码逻辑简单,时序更容易收敛。

来看一个简单的 FSM 例子:一个 3 状态的 Moore 型状态机,检测输入序列 "101"。

// 序列检测器:检测 "101"
module seq_detector (
    input  wire clk,
    input  wire rst_n,
    input  wire din,
    output reg  dout
);
    // 状态编码(独热码)
    localparam IDLE = 3'b001,
               S1   = 3'b010,
               S2   = 3'b100;

    reg [2:0] state, next_state;

    // 状态寄存器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= IDLE;
        else
            state <= next_state;
    end

    // 次态逻辑(组合逻辑)
    always @(*) begin
        next_state = IDLE;  // 默认状态
        case (state)
            IDLE: next_state = din ? S1 : IDLE;
            S1:   next_state = din ? S1 : S2;
            S2:   next_state = din ? S1 : IDLE;
            default: next_state = IDLE;
        endcase
    end

    // 输出逻辑(Moore 型)
    always @(*) begin
        dout = (state == S2) ? 1'b1 : 1'b0;
    end
endmodule

注意看,我把次态逻辑和输出逻辑分开了。这是我一直坚持的写法——三段式状态机。状态寄存器、次态逻辑、输出逻辑各写一个 always 块。这样代码清晰,综合出来的电路也规整,调试的时候一眼就能看出问题在哪。

避坑指南: 我曾经接手过一个项目,里面的状态机把次态逻辑和输出逻辑混在一起写。结果状态跳转没问题,但输出总是多了一个时钟周期的延迟。排查了整整两天才发现是组合逻辑里不小心用了非阻塞赋值。从那以后,我坚持三段式写法,再也没出过这种问题。

好了,数字逻辑基础就回顾到这里。这些概念在后面的流水线设计中会反复用到。特别是状态机,RISC-V 的控制单元本质上就是一个复杂的状态机。把这些基础打牢,后面学起来会轻松很多。