流水线CPU设计方案
概述
本次课下我根据P4的代码进行了大规模重构,加入了五级流水线寄存器和转发的控制通路,最后针对转发和阻塞编写了冒险处理模块。
指令说明
本文实现的CPU包含的指令与P4相同。
R型指令
addsubjr
实际上实现的指令相当于
addu和subu,因为题目明确指出不考虑溢出
I型指令
orilwswbeqlui
J型指令
jal
空指令
nop
功能模块设计(含流水线寄存器)
以下功能模块排列按照所在阶段从前往后排布。
IFU
相对P4,加入了使能信号和PC+8的值,分别是为了应对阻塞时冻结PC和跳转延迟槽的问题。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| clk | input | 时钟信号 | 1 |
| reset | input | 复位信号 | 1 |
| en | input | 使能信号 | 1 |
| next_pc | input | 下一个指令地址 | 32 |
| PC | output | 当前指令地址寄存器 | 32 |
| instr | output | 当前指令 | 32 |
| PC_8 | output | 当前指令地址 + 8 的值 | 32 |
IF_ID
第一个流水线寄存器。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| clk | input | 时钟信号 | 1 |
| reset | input | 复位信号 | 1 |
| en | input | 使能信号 | 1 |
| F_instr | input | 取指阶段的当前指令 | 32 |
| F_PC | input | 取指阶段的当前指令地址寄存器 | 32 |
| F_PC8 | input | 取指阶段的当前指令地址 + 8 | 32 |
| D_instr | output | 译码阶段的当前指令 | 32 |
| D_PC | output | 译码阶段的当前指令地址寄存器 | 32 |
| D_PC8 | output | 译码阶段的当前指令地址 + 8 | 32 |
Controller
加入了新信号Tuse和Tnew的产生逻辑,这两个信号将用于判断阻塞,后面将会详细分析。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| opcode | input | 操作码,决定操作类型 | 6 |
| funct | input | 功能码,辅助操作码决定功能 | 6 |
| NPCOp | output | 下一个PC的操作码选择 | 3 |
| RegWrite | output | 寄存器写使能信号 | 1 |
| ALUOp | output | ALU操作选择信号 | 3 |
| MemWrite | output | 内存写使能信号 | 1 |
| ExtOp | output | 符号扩展操作选择 | 1 |
| WAOp | output | 写地址选择信号 | 2 |
| WDOp | output | 写数据选择信号 | 2 |
| BOp | output | 分支操作信号 | 1 |
| Tuse | output | 使用时间信号 | 2 |
| Tnew | output | 新数据时间信号 | 2 |
NPC
NPC是个特殊的模块,因为他不严格位于某个特定的阶段,他的输入既有F阶段的PC,也有D阶段的PC,所以编写具体逻辑的时候需要考虑,例如b和j型指令在跳转时要用到的是PC+8而非PC+4。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| F_PC | input | 取指阶段的当前指令地址寄存器 | 32 |
| D_PC | input | 译码阶段的当前指令地址寄存器 | 32 |
| offset | input | 分支偏移量 | 16 |
| instr_26 | input | 跳转指令的地址 | 26 |
| ra_data | input | 返回地址寄存器数据 | 32 |
| zero | input | 零标志信号 | 1 |
| NPCOp | input | 下一个PC的操作码选择 | 3 |
| next_pc | output | 计算出的下一个指令地址 | 32 |
GRF
几乎不变的模块。注意这里如果把always @(posedge)换成always @(negedge)就等同于实现寄存器内部转发。不过这种操作显得有些邪道了(),而且寄存器内部转发也不麻烦,所以我没有采用。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| clk | input | 时钟信号 | 1 |
| reset | input | 复位信号 | 1 |
| RegWrite | input | 寄存器写使能信号 | 1 |
| RA1 | input | 读寄存器地址1 | 5 |
| RA2 | input | 读寄存器地址2 | 5 |
| WA | input | 写寄存器地址 | 5 |
| WD | input | 写入的数据 | 32 |
| RD1 | output | 读出的数据1 | 32 |
| RD2 | output | 读出的数据2 | 32 |
| PC | input | 当前指令地址,用于显示 | 32 |
CMP
为了提高BEQ的运行效率特意准备的模块,小小的可爱捏。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| D1 | input | 第一个比较数据 | 32 |
| D2 | input | 第二个比较数据 | 32 |
| out | output | 比较结果,1表示相等,0表示不相等 | 1 |
ID_EX
这真是一个大到可怕的模块(),因为前一个步骤产生的控制信号和运算信号太多了,当然也有我为了可扩展性而加入的一些无用信号有关。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| clk | input | 时钟信号 | 1 |
| reset | input | 复位信号 | 1 |
| clr | input | 清除信号 | 1 |
| D_PC | input | 译码阶段的当前指令地址 | 32 |
| D_PC8 | input | 译码阶段的当前指令地址 + 8 | 32 |
| D_RD1 | input | 译码阶段读出的数据1 | 32 |
| D_RD2 | input | 译码阶段读出的数据2 | 32 |
| D_extimm | input | 译码阶段扩展后的立即数 | 32 |
| D_Funct | input | 译码阶段功能码 | 6 |
| D_rs | input | 译码阶段的源寄存器地址1 | 5 |
| D_rt | input | 译码阶段的源寄存器地址2 | 5 |
| D_rd | input | 译码阶段的目的寄存器地址 | 5 |
| D_offset | input | 译码阶段的偏移量 | 16 |
| E_PC | output | 执行阶段的当前指令地址 | 32 |
| E_PC8 | output | 执行阶段的当前指令地址 + 8 | 32 |
| E_RD1 | output | 执行阶段的读出数据1 | 32 |
| E_RD2 | output | 执行阶段的读出数据2 | 32 |
| E_extimm | output | 执行阶段扩展后的立即数 | 32 |
| E_Funct | output | 执行阶段功能码 | 6 |
| E_rs | output | 执行阶段的源寄存器地址1 | 5 |
| E_rt | output | 执行阶段的源寄存器地址2 | 5 |
| E_rd | output | 执行阶段的目的寄存器地址 | 5 |
| E_offset | output | 执行阶段的偏移量 | 16 |
| D_RegWrite | input | 译码阶段的寄存器写使能信号 | 1 |
| D_ALUOp | input | 译码阶段的ALU操作选择信号 | 3 |
| D_MemWrite | input | 译码阶段的内存写使能信号 | 1 |
| D_WAOp | input | 译码阶段的写地址选择信号 | 2 |
| D_WDOp | input | 译码阶段的写数据选择信号 | 2 |
| D_BOp | input | 译码阶段的分支操作信号 | 1 |
| E_RegWrite | output | 执行阶段的寄存器写使能信号 | 1 |
| E_ALUOp | output | 执行阶段的ALU操作选择信号 | 3 |
| E_MemWrite | output | 执行阶段的内存写使能信号 | 1 |
| E_WAOp | output | 执行阶段的写地址选择信号 | 2 |
| E_WDOp | output | 执行阶段的写数据选择信号 | 2 |
| E_BOp | output | 执行阶段的分支操作信号 | 1 |
| Tnew | input | 新数据时间信号 | 2 |
| E_Tnew | output | 执行阶段的新数据时间信号 | 2 |
ALU
和我P4设计文档对比可以看出判断是否相等的zero信号不在ALU里实现了,因为我单独定义了CMP模块。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| ALUOp | input | ALU操作选择信号 | 3 |
| A | input | 第一个操作数 | 32 |
| B | input | 第二个操作数 | 32 |
| ALU_res | output | ALU运算结果 | 32 |
EX_MEM
继续往前传。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| clk | input | 时钟信号 | 1 |
| reset | input | 复位信号 | 1 |
| E_ALU_res | input | 执行阶段的ALU运算结果 | 32 |
| E_PC | input | 执行阶段的当前指令地址 | 32 |
| E_PC_8 | input | 执行阶段的当前指令地址 + 8 | 32 |
| E_MemWD | input | 执行阶段的内存写数据 | 32 |
| E_WA | input | 执行阶段的写地址 | 5 |
| E_offset | input | 执行阶段的偏移量 | 16 |
| M_ALU_res | output | 存储阶段的ALU运算结果 | 32 |
| M_PC | output | 存储阶段的当前指令地址 | 32 |
| M_PC_8 | output | 存储阶段的当前指令地址 + 8 | 32 |
| M_MemWD | output | 存储阶段的内存写数据 | 32 |
| M_WA | output | 存储阶段的写地址 | 5 |
| M_offset | output | 存储阶段的偏移量 | 16 |
| E_RegWrite | input | 执行阶段的寄存器写使能信号 | 1 |
| E_MemWrite | input | 执行阶段的内存写使能信号 | 1 |
| E_WAOp | input | 执行阶段的写地址选择信号 | 2 |
| E_WDOp | input | 执行阶段的写数据选择信号 | 2 |
| M_RegWrite | output | 存储阶段的寄存器写使能信号 | 1 |
| M_MemWrite | output | 存储阶段的内存写使能信号 | 1 |
| M_WAOp | output | 存储阶段的写地址选择信号 | 2 |
| M_WDOp | output | 存储阶段的写数据选择信号 | 2 |
| E_Tnew | input | 执行阶段的新数据时间信号 | 2 |
| M_Tnew | output | 存储阶段的新数据时间信号 | 2 |
DM
不变。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| PC | input | 当前指令地址,用于显示 | 32 |
| clk | input | 时钟信号 | 1 |
| reset | input | 复位信号 | 1 |
| MemWrite | input | 内存写使能信号 | 1 |
| WA | input | 写地址 | 32 |
| WD | input | 写入的数据 | 32 |
| RD | output | 读出的数据 | 32 |
MEM_WB
前进。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| clk | input | 时钟信号 | 1 |
| reset | input | 复位信号 | 1 |
| M_ALU_res | input | 存储阶段的ALU运算结果 | 32 |
| M_PC | input | 存储阶段的当前指令地址 | 32 |
| M_PC_8 | input | 存储阶段的当前指令地址 + 8 | 32 |
| M_MemRD | input | 存储阶段的内存读取数据 | 32 |
| M_WA | input | 存储阶段的写地址 | 5 |
| M_offset | input | 存储阶段的偏移量 | 16 |
| W_ALU_res | output | 回写阶段的ALU运算结果 | 32 |
| W_PC | output | 回写阶段的当前指令地址 | 32 |
| W_PC_8 | output | 回写阶段的当前指令地址 + 8 | 32 |
| W_MemRD | output | 回写阶段的内存读取数据 | 32 |
| W_WA | output | 回写阶段的写地址 | 5 |
| W_offset | output | 回写阶段的偏移量 | 16 |
| M_RegWrite | input | 存储阶段的寄存器写使能信号 | 1 |
| M_WAOp | input | 存储阶段的写地址选择信号 | 2 |
| M_WDOp | input | 存储阶段的写数据选择信号 | 2 |
| W_RegWrite | output | 回写阶段的寄存器写使能信号 | 1 |
| W_WAOp | output | 回写阶段的写地址选择信号 | 2 |
| W_WDOp | output | 回写阶段的写数据选择信号 | 2 |
| M_Tnew | input | 存储阶段的新数据时间信号 | 2 |
| W_Tnew | output | 回写阶段的新数据时间信号 | 2 |
MUX
这个模块包含了功能部件几乎所有的非跳转MUX。和各级流水线寄存器都有关。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| rt | input | 源寄存器地址2 | 5 |
| rd | input | 目的寄存器地址 | 5 |
| WAOp | input | 写地址选择信号 | 2 |
| MemRD | input | 内存读取数据 | 32 |
| offset | input | 偏移量 | 16 |
| ALU_res | input | ALU运算结果 | 32 |
| PC_8 | input | 当前指令地址 + 8 | 32 |
| WDOp | input | 写数据选择信号 | 2 |
| RD2 | input | 读出的数据2 | 32 |
| Ext_imm16 | input | 扩展后的16位立即数 | 32 |
| BOp | input | 分支操作信号 | 1 |
| B | output | 选择的操作数B | 32 |
| WD | output | 写入的数据 | 32 |
| A3 | output | 写入寄存器地址 | 5 |
搭建转发数据通路
转发是P5最大的难关和主要任务。因为无脑阻塞可以速杀几乎所有的问题,但为了通过评测,必须使用转发来提高运行效率!
我对每一个接收转发的位置都构建了对应的转发模块,这使得结构清晰。
ForwardRD1
处理从E级、M级和W级到D_RD1的转发。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| W_WD | input | 回写阶段的数据 | 32 |
| M_ALU_res | input | 存储阶段的ALU运算结果 | 32 |
| M_PC_8 | input | 存储阶段的当前指令地址 + 8 | 32 |
| M_offset | input | 存储阶段的偏移量 | 16 |
| M_WDOp | input | 存储阶段的写数据选择信号 | 2 |
| E_PC_8 | input | 执行阶段的当前指令地址 + 8 | 32 |
| E_offset | input | 执行阶段的偏移量 | 16 |
| E_WDOp | input | 执行阶段的写数据选择信号 | 2 |
| D_RD1 | input | 译码阶段读出的数据1 | 32 |
| Fwd_RD1Op | input | 转发选择控制信号 | 3 |
| Fwd_RD1 | output | 转发后的数据 | 32 |
ForwardRD2
处理从E级、M级和W级到D_RD2的转发。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| W_WD | input | 回写阶段的数据 | 32 |
| M_ALU_res | input | 存储阶段的ALU运算结果 | 32 |
| M_PC_8 | input | 存储阶段的当前指令地址 + 8 | 32 |
| M_offset | input | 存储阶段的偏移量 | 16 |
| M_WDOp | input | 存储阶段的写数据选择信号 | 2 |
| E_PC_8 | input | 执行阶段的当前指令地址 + 8 | 32 |
| E_offset | input | 执行阶段的偏移量 | 16 |
| E_WDOp | input | 执行阶段的写数据选择信号 | 2 |
| D_RD2 | input | 译码阶段读出的数据2 | 32 |
| Fwd_RD2Op | input | 转发选择控制信号 | 3 |
| Fwd_RD2 | output | 转发后的数据 | 32 |
ForwardA
处理从M级和W级到E_A的转发。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| W_WD | input | 回写阶段的数据 | 32 |
| M_ALU_res | input | 存储阶段的ALU运算结果 | 32 |
| M_PC_8 | input | 存储阶段的当前指令地址 + 8 | 32 |
| M_offset | input | 存储阶段的偏移量 | 16 |
| M_WDOp | input | 存储阶段的写数据选择信号 | 2 |
| A | input | 原始操作数A | 32 |
| Fwd_AOp | input | 转发选择控制信号 | 3 |
| Fwd_A | output | 转发后的数据 | 32 |
ForwardB
处理从M级和W级到E_B的转发。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| W_WD | input | 回写阶段的数据 | 32 |
| M_ALU_res | input | 存储阶段的ALU运算结果 | 32 |
| M_PC_8 | input | 存储阶段的当前指令地址 + 8 | 32 |
| M_offset | input | 存储阶段的偏移量 | 16 |
| M_WDOp | input | 存储阶段的写数据选择信号 | 2 |
| B | input | 原始操作数B | 32 |
| Fwd_BOp | input | 转发选择控制信号 | 3 |
| Fwd_B | output | 转发后的数据 | 32 |
转发、阻塞信号的生成思路
我的转发和阻塞控制信号由同一个控制模块HazardUnit产生。通过输入所需要的信息,产生控制对应寄存器进行阻塞(即插入NOP指令)和控制转发模块选择正确转发数据的信号。
| 信号名 | 方向 | 描述 | 位宽 |
|---|---|---|---|
| D_RegWrite | input | 译码阶段的寄存器写使能信号 | 1 |
| E_RegWrite | input | 执行阶段的寄存器写使能信号 | 1 |
| M_RegWrite | input | 存储阶段的寄存器写使能信号 | 1 |
| W_RegWrite | input | 回写阶段的寄存器写使能信号 | 1 |
| D_rs | input | 译码阶段的源寄存器地址1 | 5 |
| D_rt | input | 译码阶段的源寄存器地址2 | 5 |
| E_rs | input | 执行阶段的源寄存器地址1 | 5 |
| E_rt | input | 执行阶段的源寄存器地址2 | 5 |
| E_WDOp | input | 执行阶段的写数据选择信号 | 2 |
| E_WA | input | 执行阶段的写地址 | 5 |
| M_WA | input | 存储阶段的写地址 | 5 |
| M_WDOp | input | 存储阶段的写数据选择信号 | 2 |
| W_WA | input | 回写阶段的写地址 | 5 |
| Fwd_RD1Op | output | 译码阶段读出数据1的转发控制信号 | 3 |
| Fwd_RD2Op | output | 译码阶段读出数据2的转发控制信号 | 3 |
| Fwd_AOp | output | 执行阶段操作数A的转发控制信号 | 3 |
| Fwd_BOp | output | 执行阶段操作数B的转发控制信号 | 3 |
| Tuse | input | 当前指令的使用时间 | 2 |
| E_Tnew | input | 执行阶段的新数据时间信号 | 2 |
| M_Tnew | input | 存储阶段的新数据时间信号 | 2 |
| W_Tnew | input | 回写阶段的新数据时间信号 | 2 |
| IFU_en | output | 指令获取单元的使能信号 | 1 |
| IF_ID_en | output | IF/ID流水寄存器的使能信号 | 1 |
| ID_EX_clr | output | ID/EX流水寄存器的清除信号 | 1 |
转发控制思路
由于我们前面已经把控制信号沿流水线不断传递,所以我们只需要比对对应阶段的控制信号就可以得知应该是否应该转发、应该转发哪里的值。例如我对于A转发信号的产生逻辑如下。
|
|
需要注意的是,我这里的控制信号并没有完全决定转发情况,因为我还有一部分选择是在转发模块内完成的,不过这里就不展示了。
阻塞控制思路
使用Tuse、Tnew大法。
Tuse表示数据到了 D 级之后还需要多少个周期要使用,每个指令的Tuse是固定不变的。 Tnew表示数据还有多长时间产生,会随着数据的流水动态的减少。具体实现方法是每次Tnew经过流水线寄存器传递的时候做判断,如果非0则减1。
HazardUnit模块会对D级的Tuse信号(因为课程要求一律在D级阻塞)和每一级的Tnew信号。如果Tuse < Tnew,那么说明当前需要的值还没有生成,必须阻塞流水线直到对应的值产生(即后续的Tnew通过递减与Tuse相等),反之,则说明可以通过转发解决,无需阻塞。
测试方案
我在搭建和debug时手动编写了一系列测试样例,我把他合了起来。
|
|
思考题
-
我们使用提前分支判断的方法尽早产生结果来减少因不确定而带来的开销,但实际上这种方法并非总能提高效率,请从流水线冒险的角度思考其原因并给出一个指令序列的例子。 答:以
beq为例,我们通过提前beq的数据比较使其Tuse减为0,虽然这在控制冒险角度减少了因为默认跳转不发生需要清除的指令,但也提高了阻塞率,例如在下面这一情况:1 2add $s0,$s0,$s0 beq $s0,$s1,label -
因为延迟槽的存在,对于 jal 等需要将指令地址写入寄存器的指令,要写回 PC + 8,请思考为什么这样设计? 答:因为在存在延迟槽的情况下,跳转发生的时候下一条指令即PC+4指向的指令已经进入了流水线,只有写入PC+8而非PC+4,才能防止跳回时这条指令被重复执行。
-
我们要求大家所有转发数据都来源于流水寄存器而不能是功能部件(如 DM 、 ALU ),请思考为什么? 答:因为如果转发数据来源于功能部件,这会拉长流水线寄存器间功能部件得到稳定输出值的时间,这会增大流水线的最小周期,从而限制流水线频率,影响运行效率,违背了流水线设计的初衷。
-
我们为什么要使用 GPR 内部转发?该如何实现? 答:因为当一个寄存器在流水线中同时被读写时会引发数据冒险,于是我们将即将写入寄存器的W_WD数据转发到读取寄存器的D_RD1、D_RD2,这样就实现了寄存器内部转发。
-
我们转发时数据的需求者和供给者可能来源于哪些位置?共有哪些转发数据通路? 答:在我的CPU中,需求者是D级的寄存器读取值和E级的ALU输入值,供给者可能来自于:
- E级的16位立即数(
lui)和PC+8(jal)。 - M级的16位立即数(
lui)、PC+8(jal)和ALU计算结果。 - W级的写入数据。
转发数据通路包括:
- E到D
- M到D
- W到D
- M到E
- W到E
- E级的16位立即数(
-
在课上测试时,我们需要你现场实现新的指令,对于这些新的指令,你可能需要在原有的数据通路上做哪些扩展或修改?提示:你可以对指令进行分类,思考每一类指令可能修改或扩展哪些位置。 答:我会将指令分为计算型指令、跳转型指令、访存型指令。首先是改变数据通路,和单周期类似,尽可能用现成的数据通路进行扩建(这点和P4不同,因为新增数据通路会导致必须重写大批转发)。完成数据通路之后增加对应的Tuse,Tnew和对应的控制信号。
-
确定你的译码方式,简要描述你的译码器架构,并思考该架构的优势以及不足。 答:我的译码方式是集中式译码。在D级生成所有的控制信号并逐级向后传递。 优势:编写简单,速度更快,关键路径更短。 不足:没有那么模块化。