跳转至

流水线 CPU 的设计和实现

流水线 CPU 的设计和实现较为复杂,下面的描述可能较为粗略。如果想看非常具体的描述讲解,则请同学们仔细听老师上课讲解,同时也可以去看计算机组成与设计 硬件/软件接口(RISC-V,第 5 版,学校书店有)这本书第 4 章的相关内容,上面有非常详细的流水线 CPU 的设计(以及习题)。

下面还是以 addi 为例,重新回顾流水线 CPU 的设计方法。

流水线 CPU 中的大部分的控制信号是由 ID 段生成,一段一段传到后续阶段进行处理的。因此,在设计的时候,我们可以从 WB 阶段开始,倒着考虑。这样可以保证在设计信号的时候不会漏掉信号。

老师上课讲以及书上都是正着的方式,这个可能会好理解一些,因为这是指令执行的时间顺序。我们在这里默认大家都已经对流水线有了一定的了解,如果还不太清楚流水线的工作原理的话,请先听老师讲解或看书学习。

WB 阶段

对于 addi 指令的 WB 阶段,需要做的是将 alu 计算得到的 result 写回寄存器中,因此 WB 阶段需要的信号如下:

  • rf_wen = 1: 表示是否写寄存器(ID 段在解析指令后给出)
  • rf_wdata = alu_result: 写寄存器的数据(EXE 段计算后得出)
  • rf_waddr = rd: 写寄存器的编号(ID 段在解析指令后给出)

具体实现方式可以是将 mem_wb_reg 的对应信号直接引到 regfile 的写端口上。

MEM 阶段

对于 addi 指令的 MEM 阶段,不需要做任何事情,因此需要的信号如下:

  • mem_en = 0: 表示是否有内存操作(ID 段在解析指令后给出)

对 lb、sb、sw 等指令,MEM 则需要发起 Wishbone 请求并等待完成。

EXE 阶段

对于 addi 指令的 EXE 阶段,需要生成立即数,同时在 ALU 中进行加法运算,因此需要的信号如下:

  • rf_rdata_a/rf_rdata_b: 表示寄存器读取 rs1,rs2 地址的结果(ID 段在读取寄存器后给出)
  • inst: 指令,用于生成立即数(IF 段在读取 SRAM 后给出)
  • imm_type = I: 立即数种类(ID 段在解析指令后给出)
  • alu_op = ADD: alu 运算的种类(ID 段在解析指令后给出)
  • use_rs2 = false: 表示第二个 operand 是立即数还是 rs2 的读取结果(ID 段在解析指令后给出)

同时需要给出 rf_wdata = alu_result,以供 WB 阶段使用

rf_wen 和 rf_waddr 虽然不参与 ALU 运算,但也要随结果继续传递。对于分支指令,还需要随指令保存其 PC,以便计算相对于该指令地址的分支目标。

ID 阶段

这个阶段对于所有指令都需要生成对应的控制信号,同时完成寄存器的读取。对于 addi 指令,需要的信号和对应的值如下:

  • inst = inst
  • rf_raddr_a = rs1 segment of instruction
  • rf_raddr_b = rs2 segment of instruction
  • imm_type = I
  • alu_op = ADD
  • use_rs2 = false
  • mem_en = 0
  • rf_wen = 1
  • rf_waddr = rd segment of instruction

这些信号的含义在上面已经介绍过了,因此在这里不再重新说明。

注意到需要进行可能的冲突处理,因此,若位于 EXE/MEM/WB 阶段的指令要写寄存器,且 ID 阶段的指令需要读这个寄存器,我们则不应该让 ID 阶段的指令进入下一阶段 (EXE)。否则这里将会读到错误(旧)的寄存器数据。

为此我们需要插入气泡。这里给出一个较为简单的气泡实现方案:使用指令addi zero, zero, 0填充流水线(输出的 inst 为32'h00000013,再生成对应的控制信号)即可。当然,你可以换成任意一个既不写寄存器也不访问内存的指令来填充。注意 zero 寄存器的特殊处理。

IF 阶段

这个阶段对于所有指令都需要进行 SRAM 的读取,从而获取对应的指令。

这里的实现相当于实验 4 的 Wishbone Master的一部分:以 PC 为字节地址,发起一次 32 位读请求,在有效请求得到 ACK 时接收指令。等待期间保持请求地址和控制信号不变。

取指可能需要多个周期(因为 wishbone 请求无法在一个周期内完成)。在新指令尚未返回时,不能把未完成的读数据当作有效指令送入 ID,因此需要向流水线中插入气泡。

在 IF 阶段还要完成 pc 的更新,由于这里只介绍 addi 指令的实现,因此直接让 pc <= pc + 4 即可。但由于还有其他的指令会修改 pc,因此建议将 pc_mux 实现成一个单独的模块,通过控制信号控制 pc 的更新。

最后,在实现这些模块之后,在 top 中完成连线即可。

这样我们就完成了一个能执行 addi 指令的流水线 CPU 了。

总结

addi 指令需要的流水线信号如下表:

信号名 值 生成阶段 最后使用阶段 含义
inst 指令 IF EXE 指令编码
rf_raddr_a rs1 ID ID 读取寄存器编号
rf_raddr_b rs2 ID ID 读取寄存器编号
rf_rdata_a ?? ID EXE 读取寄存器结果
rf_rdata_b ?? ID EXE 读取寄存器结果
imm_type I ID EXE 立即数种类
alu_op ADD ID EXE ALU 操作
use_rs2 0 ID EXE 使用立即数/rs2
mem_en 0 ID MEM 是否在 MEM 段访存
rf_wen 1 ID WB 是否写寄存器
rf_waddr rd ID WB 写寄存器编号
rf_wdata result EXE WB 写寄存器数据

以上仅是一种对于 addi 指令可行的设计,同学们在实现时可以按照需要,自己设计对应的流水线信号。同时也可以自己调整功能所在的流水线段。

本节的核心是让每条指令的数据和控制信号始终对应。信号一旦产生,就需要保存在沿途的流水线寄存器中,直到最后使用它的阶段。

学完这节内容我需要做什么?

画出自己的流水线结构,为各条指令列出信号的生成阶段和最后使用阶段,确定每级寄存器保存的内容,并结合后两节设计仲裁、暂停和冲刷逻辑。

作者:cuibst (85.29%), 于越洋 (13.73%), Jiajie Chen (0.98%)