3. RISC-V指令集架构入门

好,我们进入第三章。这一章的内容,说白了就是数字逻辑的“肌肉记忆”——时序逻辑和状态机。你写RISC-V软核,CPU的流水线、控制单元、总线交互,全得靠这些东西撑起来。我个人习惯把这一章看作“从组合逻辑到数字系统的分水岭”。

本章核心脉络:寄存器 vs 锁存器 → 复位策略 → 状态机模板 → UART实战。一条线串下来,全是硬核干货。

3.1 寄存器与锁存器:一字之差,天壤之别

很多初学者搞不清寄存器和锁存器。我当年也踩过坑——有一次做SPI从机,综合完发现面积比预期大了一倍,查了半天,原来是代码里不小心写了个锁存器。嗯,从那以后我对这俩东西就特别敏感。

对比项 寄存器 (Flip-Flop) 锁存器 (Latch)
触发方式 时钟边沿触发(上升沿/下降沿) 电平触发(高电平/低电平)
时序特性 同步输出,只在时钟沿变化 透明模式,电平期间输出随输入变
综合结果 D触发器 + 时钟网络 锁存器单元(通常不推荐)
FPGA资源 使用专用FF单元 用LUT+反馈路径模拟,不稳定
设计推荐 ✅ 时序逻辑首选 ❌ 除非特殊低功耗场景,否则避免

⚠️ 锁存器是怎么“意外”产生的?

最常见的坑:在组合逻辑块(always @(*))里,if-else或case语句没有覆盖所有分支。比如if没有else,综合工具就会推断:当条件不满足时,输出要保持原值——这不就是锁存器吗?

来看一个反面教材:

// ❌ 错误示例:会综合出锁存器
always @(*) begin
  if (sel)
    q = d;  // 没有else分支,sel=0时q保持原值 → 锁存器
end

// ✅ 正确写法:覆盖所有分支
always @(*) begin
  if (sel)
    q = d;
  else
    q = 1'b0;  // 或者给默认值
end

你想想看,在RISC-V的寄存器堆实现里,如果意外生成了锁存器,整个CPU的时序都会乱套。所以我的原则是:写组合逻辑时,if-else和case一定要给全默认值。

3.2 同步复位 vs 异步复位:选哪个?

复位策略,每个工程师都有自己的偏好。我个人的经验是:没有绝对的好坏,看场景。

同步复位

// 同步复位:只在时钟上升沿检查复位信号
always @(posedge clk) begin
  if (rst_n)
    q <= d;
  else
    q <= 1'b0;
end
  • 优点:抗毛刺能力强,复位信号可以综合为普通数据路径
  • 缺点:需要时钟存在才能复位,复位信号宽度必须大于一个时钟周期
  • 适用场景:高速设计、复位信号来自片外且可能抖动

异步复位

// 异步复位:复位信号独立于时钟,立即生效
always @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    q <= 1'b0;
  else
    q <= d;
end
  • 优点:复位响应快,不需要时钟也能复位
  • 缺点:对毛刺敏感,存在复位释放时的时序问题(recovery/removal检查)
  • 适用场景:上电初始化、安全关键电路

💡 我的建议:在RISC-V软核中,我通常采用异步复位、同步释放的策略。既保证了上电时能快速复位,又避免了异步复位释放时的亚稳态问题。代码模板如下:

// 异步复位、同步释放
reg rst_sync1, rst_sync2;

always @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    rst_sync1 <= 1'b0;
    rst_sync2 <= 1'b0;
  end else begin
    rst_sync1 <= 1'b1;
    rst_sync2 <= rst_sync1;
  end
end

// 使用 rst_sync2 作为内部复位
always @(posedge clk or negedge rst_n) begin
  if (!rst_sync2)
    q <= 1'b0;
  else
    q <= d;
end

3.3 三段式状态机:写状态机的“标准答案”

状态机是数字系统的“大脑”。在RISC-V中,取指、译码、执行、访存、写回——每个流水级的状态切换,本质上就是一个复杂状态机。

我强烈推荐三段式状态机写法。为什么?因为它把组合逻辑和时序逻辑分得清清楚楚,可读性强,综合结果也稳定。

三段式状态机结构 第一段 时序逻辑 状态跳转 第二段 组合逻辑 次态判断 第三段 组合/时序逻辑 输出产生 三段式状态机代码模板 // 第一段:状态寄存器(时序逻辑) always @(posedge clk or negedge rst_n) if (!rst_n) state <= IDLE; else state <= next_state; // 第二段:次态逻辑(组合逻辑) always @(*) begin case (state) IDLE: next_state = start ? DATA : IDLE; // ... 其他状态 endcase end

来看一个完整的UART接收状态机示例:

// UART接收器 - 三段式状态机
module uart_rx_fsm (
  input  wire       clk,
  input  wire       rst_n,
  input  wire       rx,           // 串行输入
  input  wire       baud_tick,    // 波特率时钟(16倍过采样)
  output reg  [7:0] rx_data,      // 接收数据
  output reg        rx_done       // 接收完成标志
);

  // 状态定义
  localparam IDLE     = 2'b00,
             START    = 2'b01,
             DATA     = 2'b10,
             STOP     = 2'b11;

  reg [1:0] state, next_state;
  reg [3:0] bit_cnt;      // 位计数器
  reg [3:0] sample_cnt;   // 采样计数器
  reg [7:0] shift_reg;    // 移位寄存器

  // 第一段:状态跳转
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
      state <= IDLE;
    else
      state <= next_state;
  end

  // 第二段:次态逻辑
  always @(*) begin
    next_state = state;  // 默认保持
    case (state)
      IDLE:   if (!rx) next_state = START;
      START:  if (baud_tick && sample_cnt == 4'd7) next_state = DATA;
      DATA:   if (baud_tick && bit_cnt == 4'd8) next_state = STOP;
      STOP:   if (baud_tick && sample_cnt == 4'd15) next_state = IDLE;
    endcase
  end

  // 第三段:输出逻辑(这里用组合逻辑输出)
  always @(*) begin
    rx_done = (state == STOP && baud_tick && sample_cnt == 4'd15);
  end

  // 辅助计数器(单独时序块)
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      sample_cnt <= 4'd0;
      bit_cnt    <= 4'd0;
      shift_reg  <= 8'd0;
      rx_data    <= 8'd0;
    end else begin
      case (state)
        IDLE: begin
          sample_cnt <= 4'd0;
          bit_cnt    <= 4'd0;
        end
        START: begin
          if (baud_tick) begin
            if (sample_cnt == 4'd7) sample_cnt <= 4'd0;
            else sample_cnt <= sample_cnt + 1'b1;
          end
        end
        DATA: begin
          if (baud_tick && sample_cnt == 4'd15) begin
            sample_cnt <= 4'd0;
            shift_reg  <= {rx, shift_reg[7:1]};  // LSB first
            if (bit_cnt == 4'd7) bit_cnt <= 4'd0;
            else bit_cnt <= bit_cnt + 1'b1;
          end else if (baud_tick) begin
            sample_cnt <= sample_cnt + 1'b1;
          end
        end
        STOP: begin
          if (baud_tick) begin
            if (sample_cnt == 4'd15) begin
              sample_cnt <= 4'd0;
              rx_data <= shift_reg;  // 锁存数据
            end else begin
              sample_cnt <= sample_cnt + 1'b1;
            end
          end
        end
      endcase
    end
  end

endmodule

💡 三段式的好处:

  • 第一段只管状态跳转,代码极简,不容易出错
  • 第二段纯组合逻辑,综合工具容易优化
  • 第三段输出可以灵活选择组合输出(响应快)或时序输出(无毛刺)

3.4 UART接收模块:从理论到实战

UART(通用异步收发器)是嵌入式系统最常用的串行通信协议之一。在RISC-V软核中,UART通常是第一个外设——因为调试全靠它打印信息。

UART帧格式

字段 起始位 数据位(LSB first) 校验位(可选) 停止位
位数 1 bit 5~8 bits 0/1 bit 1/1.5/2 bits
电平 高→低跳变 数据位 奇/偶校验 高电平

接收时序要点

  1. 起始位检测:检测到rx线从高到低跳变,确认是真正的起始位(需要去毛刺)
  2. 过采样:通常用16倍波特率时钟,在每位的中点采样(第7、8、9个采样点取多数决)
  3. 数据移位:LSB first,每收到一个bit就移入移位寄存器
  4. 停止位检查:停止位必须是高电平,否则报帧错误

⚠️ 我曾经踩过的坑:

有一次做UART接收,波特率时钟和主时钟是异步的。我直接用主时钟域采样rx信号,结果偶尔会采到毛刺,导致起始位误判。后来加了两级同步器 + 边沿检测才解决。记住:跨时钟域信号必须先同步!

完整UART接收模块结构

// UART接收顶层模块
module uart_rx (
  input  wire       clk,          // 系统时钟
  input  wire       rst_n,        // 异步复位
  input  wire       rx,           // 串行输入
  input  wire       baud_clk_en,  // 波特率时钟使能(16x)
  output reg  [7:0] data_out,     // 并行数据输出
  output reg        data_valid,   // 数据有效标志
  output reg        frame_err     // 帧错误标志
);

  // 信号同步
  reg rx_sync1, rx_sync2;
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rx_sync1 <= 1'b1;
      rx_sync2 <= 1'b1;
    end else begin
      rx_sync1 <= rx;
      rx_sync2 <= rx_sync1;
    end
  end

  // 边沿检测
  wire rx_falling = rx_sync2 & !rx_sync1;

  // 状态机实例化(使用上面三段式状态机)
  // ... 状态机代码同上

endmodule

📌 本章要点回顾:

  • 寄存器 vs 锁存器:组合逻辑一定要给全分支,否则综合出锁存器
  • 复位策略:异步复位同步释放,兼顾速度和可靠性
  • 三段式状态机:状态跳转、次态逻辑、输出逻辑分离,代码清晰易维护
  • UART接收:过采样、中点采样、同步器——细节决定成败