Featured image of post 北航CO_P5-verilog搭建五级流水线CPU

北航CO_P5-verilog搭建五级流水线CPU


流水线CPU设计方案

概述

本次课下我根据P4的代码进行了大规模重构,加入了五级流水线寄存器和转发的控制通路,最后针对转发和阻塞编写了冒险处理模块。

指令说明

本文实现的CPU包含的指令与P4相同。

R型指令

  • add
  • sub
  • jr

实际上实现的指令相当于addusubu,因为题目明确指出不考虑溢出

I型指令

  • ori
  • lw
  • sw
  • beq
  • lui

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转发信号的产生逻辑如下。

1
2
3
4
assign Fwd_AOp = (E_rs == 5'b0) ? 3'b011:
   (M_RegWrite && E_rs == M_WA) ? 3'b010:
   (W_RegWrite && E_rs == W_WA) ? 3'b001:
                                  3'b000;

需要注意的是,我这里的控制信号并没有完全决定转发情况,因为我还有一部分选择是在转发模块内完成的,不过这里就不展示了。

阻塞控制思路

使用Tuse、Tnew大法。

Tuse表示数据到了 D 级之后还需要多少个周期要使用,每个指令的Tuse是固定不变的。 Tnew表示数据还有多长时间产生,会随着数据的流水动态的减少。具体实现方法是每次Tnew经过流水线寄存器传递的时候做判断,如果非0则减1。

HazardUnit模块会对D级的Tuse信号(因为课程要求一律在D级阻塞)和每一级的Tnew信号。如果Tuse < Tnew,那么说明当前需要的值还没有生成,必须阻塞流水线直到对应的值产生(即后续的Tnew通过递减与Tuse相等),反之,则说明可以通过转发解决,无需阻塞。

测试方案

我在搭建和debug时手动编写了一系列测试样例,我把他合了起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
ori $ra, $0, 0xffff
jal label
add $s1, $ra, $0
label:

lui $s0,1
add $s0,$s0,$s0
add $s0,$s0,$s0
add $s0,$s0,$s0

ori $s0,$0,1
jal bbb
add $s0,$s0,$s0
add $s1,$s0,$s0
bbb:
ori $s1,$0,1
jr $ra
ori $s2,$0,1


jal aaa
add $ra,$ra,$ra
aaa:
add $ra,$ra,$ra

ori $s0,$0,2
ori $s1,$0,1
sw $s0,0($0)
lw $s1,0($0)
add $s1,$s1,$s1
add $s1,$s1,$s1
add $s1,$s1,$s1

ori $s1,$0,1
jal dest
add $s1,$s1,$s1
add $s1,$s1,$s1
dest:
add $s1,$s1,$s1
jr $ra
nop

ori $s0,$0,1
ori $s1,$0,1
beq $s1,$s0,dst
add $s1,$s1,$s1
add $s1,$s1,$s1
add $s1,$s1,$s1
dst:
add $s1,$s1,$s1

思考题

  1. 我们使用提前分支判断的方法尽早产生结果来减少因不确定而带来的开销,但实际上这种方法并非总能提高效率,请从流水线冒险的角度思考其原因并给出一个指令序列的例子。 答:以beq为例,我们通过提前beq的数据比较使其Tuse减为0,虽然这在控制冒险角度减少了因为默认跳转不发生需要清除的指令,但也提高了阻塞率,例如在下面这一情况:

    1
    2
    
    add $s0,$s0,$s0
    beq $s0,$s1,label
    
  2. 因为延迟槽的存在,对于 jal 等需要将指令地址写入寄存器的指令,要写回 PC + 8,请思考为什么这样设计? 答:因为在存在延迟槽的情况下,跳转发生的时候下一条指令即PC+4指向的指令已经进入了流水线,只有写入PC+8而非PC+4,才能防止跳回时这条指令被重复执行。

  3. 我们要求大家所有转发数据都来源于流水寄存器而不能是功能部件(如 DM 、 ALU ),请思考为什么? 答:因为如果转发数据来源于功能部件,这会拉长流水线寄存器间功能部件得到稳定输出值的时间,这会增大流水线的最小周期,从而限制流水线频率,影响运行效率,违背了流水线设计的初衷。

  4. 我们为什么要使用 GPR 内部转发?该如何实现? 答:因为当一个寄存器在流水线中同时被读写时会引发数据冒险,于是我们将即将写入寄存器的W_WD数据转发到读取寄存器的D_RD1、D_RD2,这样就实现了寄存器内部转发。

  5. 我们转发时数据的需求者和供给者可能来源于哪些位置?共有哪些转发数据通路? 答:在我的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
  6. 在课上测试时,我们需要你现场实现新的指令,对于这些新的指令,你可能需要在原有的数据通路上做哪些扩展或修改?提示:你可以对指令进行分类,思考每一类指令可能修改或扩展哪些位置。 答:我会将指令分为计算型指令、跳转型指令、访存型指令。首先是改变数据通路,和单周期类似,尽可能用现成的数据通路进行扩建(这点和P4不同,因为新增数据通路会导致必须重写大批转发)。完成数据通路之后增加对应的Tuse,Tnew和对应的控制信号。

  7. 确定你的译码方式,简要描述你的译码器架构,并思考该架构的优势以及不足。 答:我的译码方式是集中式译码。在D级生成所有的控制信号并逐级向后传递。 优势:编写简单,速度更快,关键路径更短。 不足:没有那么模块化。

使用 Hugo 构建
主题 StackJimmy 设计