2. 时序逻辑与有限状态机:寄存器建模、阻塞与非阻塞赋值、同步/异步复位、FSM(Moore/Mealy)设计与编码风格

大家好,我是你们的讲师。今天我们来聊聊数字设计里最核心的一块——时序逻辑与有限状态机。说实话,这部分内容你要是吃透了,RISC-V处理器的主干也就拿下了。我当年刚入行时,在这上面栽过不少跟头,今天就把这些经验掰开揉碎了讲给你听。

2.1 寄存器建模:数字电路的“记忆单元”

寄存器是什么?说白了,就是数字电路里用来“记住”东西的单元。组合逻辑的输出只取决于当前输入,而寄存器的输出还取决于它之前的状态。你想想看,没有寄存器,CPU怎么记住你上一条指令是什么?

在Verilog里,我们通常用 always @(posedge clk) 来描述一个寄存器。我个人习惯把这种写法叫做“时钟沿触发的赋值”。

// 一个简单的D触发器建模
reg [7:0] data_reg;

always @(posedge clk) begin
    data_reg <= data_in;
end

这里有个小细节:reg 类型并不一定代表寄存器。在组合逻辑的 always 块里,reg 其实综合出来是线网。只有像上面这样,在时钟沿触发的块里赋值,它才是真正的寄存器。

我的经验: 在项目中,我习惯把寄存器命名加上 _reg_ff 后缀。比如 pc_regstate_ff。这样看代码时,一眼就能分清哪些是时序逻辑,哪些是组合逻辑。

2.2 阻塞赋值与非阻塞赋值:一个让我熬夜的坑

阻塞赋值 = 和非阻塞赋值 <=,是Verilog新手最容易搞混的地方。我曾经在一个项目里,因为用错了赋值方式,导致仿真结果和实际芯片行为不一致,整整排查了两天。

核心原则其实很简单:

  • 组合逻辑用阻塞赋值 =:它“阻塞”了后面的语句,必须等当前语句执行完,才能执行下一条。这符合组合逻辑的硬件行为——信号是立即传递的。
  • 时序逻辑用非阻塞赋值 <=:它不阻塞,所有赋值在时钟沿到来时“同时”发生。这模拟了寄存器的行为——所有寄存器在同一时钟沿采样输入,然后更新输出。

来看一个对比的例子:

// 错误写法:时序逻辑用了阻塞赋值
always @(posedge clk) begin
    a = b;  // 先执行
    c = a;  // 此时a已经更新,c拿到的是新值
end
// 综合出来:a和c变成了两个串联的寄存器,而不是并行更新

// 正确写法:时序逻辑用非阻塞赋值
always @(posedge clk) begin
    a <= b;  // 同时采样
    c <= a;  // 同时采样,c拿到的是a的旧值
end
// 综合出来:a和c是两个并行的寄存器,行为符合预期
避坑指南: 我曾经见过一个同事,在同一个 always 块里混用 =<=。仿真能过,但综合出来的电路逻辑完全错了。记住:同一个always块里,要么全用阻塞,要么全用非阻塞,不要混用!

2.3 同步复位与异步复位:选哪个?

复位,就是让电路回到一个已知的初始状态。在RISC-V处理器里,PC寄存器必须在上电时复位到入口地址,否则CPU一启动就乱跳。

两种复位方式,各有千秋:

特性 同步复位 异步复位
触发条件 只在时钟沿检查复位信号 复位信号有效时立即复位,与时钟无关
优点 抗毛刺能力强,时序分析简单 复位响应快,不需要时钟也能复位
缺点 需要时钟才能复位,复位信号必须满足建立时间 对毛刺敏感,可能引起亚稳态
典型写法 always @(posedge clk) always @(posedge clk or negedge rst_n)

我个人在项目中更倾向于使用异步复位、同步释放的方式。说白了,就是复位信号异步地把寄存器清零,但释放时同步到时钟域,避免复位释放时的亚稳态问题。

// 异步复位、同步释放的经典写法
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_reg <= 8'h00;  // 异步复位
    end else begin
        data_reg <= data_in; // 同步工作
    end
end
重要提醒: 在RISC-V处理器设计中,复位信号通常用于初始化PC、寄存器堆、状态机等关键模块。我建议所有控制相关的寄存器都加上复位,而数据通路中的寄存器(比如流水线中的数据寄存器)可以根据需要选择是否复位,以节省面积。

2.4 有限状态机:数字系统的“大脑”

有限状态机(FSM)是数字设计的灵魂。你想想看,CPU的指令译码、总线仲裁、通信协议,本质上都是一个状态机在驱动。FSM分为两种:Moore机和Mealy机。

2.4.1 Moore机 vs Mealy机

  • Moore机:输出只取决于当前状态。说白了,状态变了,输出才变。
  • Mealy机:输出取决于当前状态和输入。输入一变,输出可能立刻变,不等时钟沿。

用一张图来理解:

Moore机 输入 → 下一状态逻辑 状态寄存器 输出逻辑(仅状态) Mealy机 输入 → 下一状态逻辑 状态寄存器 输出逻辑(状态+输入)

嗯,这里要注意:Mealy机因为输出直接受输入影响,所以通常比Moore机少一个时钟周期的延迟。但代价是输出可能有毛刺,需要额外处理。

2.4.2 FSM编码风格:三段式写法

我强烈推荐使用三段式状态机。为什么?因为可读性强,容易维护,而且综合工具优化得好。我在项目中用过两段式,后来发现调试时简直想骂人——状态和输出混在一起,改一个地方要动好几处。

三段式FSM的模板:

// 第一段:状态寄存器(时序逻辑)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= next_state;
end

// 第二段:下一状态逻辑(组合逻辑)
always @(*) begin
    case (state)
        IDLE:   if (start) next_state = READ;
                else       next_state = IDLE;
        READ:   if (done)  next_state = DONE;
                else       next_state = READ;
        DONE:   next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(组合逻辑或时序逻辑)
always @(*) begin
    case (state)
        IDLE:   data_out = 8'h00;
        READ:   data_out = data_in;
        DONE:   data_out = 8'hFF;
        default: data_out = 8'h00;
    endcase
end
我的习惯: 状态编码我通常用独热码(One-Hot)或格雷码(Gray)。独热码适合状态数少于16个的FSM,译码简单、速度快;格雷码适合状态数多的情况,相邻状态切换时只有一位变化,功耗低。在RISC-V的流水线控制中,我常用独热码,因为状态少、追求速度。

2.5 实战经验:一个RISC-V指令译码器的FSM设计

在RISC-V处理器中,指令译码就是一个典型的状态机。比如加载指令(LW)需要多个周期:取指、译码、访存、写回。每个阶段就是一个状态。

我曾经在一个项目中,把译码状态机写成了两段式,结果在调试时发现输出总是晚一个周期。后来改成三段式,问题立刻解决。你想想看,三段式把“状态跳转”和“输出”分开,调试时只需要看 statenext_state 的波形,一目了然。

核心总结:
  • 寄存器建模:always @(posedge clk) + 非阻塞赋值
  • 复位策略:异步复位、同步释放,控制寄存器必复位
  • FSM设计:三段式写法,状态、跳转、输出分离
  • Moore vs Mealy:输出延迟不同,根据需求选择

好了,这一章的内容就到这里。记住,时序逻辑是数字电路的骨架,状态机是它的灵魂。把这些基础打牢,后面设计RISC-V处理器时,你会觉得游刃有余。

专注资料整理