设计草稿

根据P4CPU设计,将CPU划分为F,D,E,M,W五级。

前置准备

由于延迟槽的存在,需将mips主模块中的PCplus4改为PCplus8.

模块层面

Controller

为了使每一级都能及时读取Controller传递出的信号,可以在每一级都设置一个单独的Controller模块。

同时,在Controller模块中定义每个指令的t_rs,t_rt,t数值

指令 trs trt t
add 1 1 2
sub 1 1 2
ori 1 F 2
lui F F 2
lw 1 F 3
sw 1 2 F
beq 0 0 F
jal F F 0
jr 0 F F
nop

F表示不需要的信号,在Verilog中定义为4’hf.

添加流水级之间的寄存器

  • FDreg

    • Instr
    • PC/PCplus8
  • DEreg

    • Instr及Controller信号
    • PC/PCplus8
    • RD1/RD2(ALU中用到)
    • A3
    • ext32
  • EMreg

    • Instr及Controller信号
    • PC/PCplus8
    • ALUres
    • RD2(DM中用到)
    • A3
  • MWreg

    • Instr及Controller信号
    • PC/PCplus8
    • ALUres
    • A3
    • DMdata

为了便于进行阻塞和转发的管理,还要为每一个中间寄存器加上en信号和reset信号:由D_stall信号调控,D_stall == 1时,PC_en = 0FD_en = 0DE_clear = 0,用于模拟阻塞,将PC和F中指令拦住,并将D到E的指令置nop。(D_stall如何调控下文再谈)

PC修改

  • 加入使能信号PC_en

NPC修改

  • 将输入PC改为F_PC
  • 相对跳转指令不需再 + 4(F_PC本身就等于PC + 4了)

ALU & CMP

此前相对跳转的指令在E级才能出结果,实在太晚了,现在将ALU其中的部分移动到D级的新模块——CMP中。

主模块

导线

  • F级
    • PC/NPC/PCplus8
    • Instr
  • D级
    • Controller信号
    • trs,trt,t
    • Instr及其分线
    • GRF(A1,A2,A3,RD1,RD2)
    • EXT(ext32)
    • CMP(zero)
    • F to D
  • E级
    • PC/PCplus8
    • Controller信号
    • trs,trt,t
    • Instr及其分线
    • ALU(inputA,inputB,ALUres)
    • D to E
  • M级
    • PC/PCplus8
    • Controller信号
    • trs,trt,t
    • Instr及其分线
    • DM(MemAddr,MemData,DMdata)
    • E to M
  • W级
    • PC/PCplus8
    • Controller信号
    • trs,trt,t
    • Instr及其分线
    • GRF(inputA,inputB,ALUres)
    • M to W

转发与阻塞导线

  • 十五条通路:

    • 起点:E_PCplus8 , M_PCplus8 , ALUres , DMdata
    • 终点:D_RD1 , D_RD2 , E_RD1 , E_RD2 , MemAddr
    • 去除五条同级&向后转发的
  • 五个修正值:

    • D_fixedRD1
    • D_fixedRD2
    • E_fixedRD1
    • E_fixedRD2
    • M_fixedRD2
  • 判断转发起点是否为PCplus8

    • E_isPCplus8
    • M_isPCplus8
  • tuse & tnew (D,E,M,W)

  • en & clear & D_stall

导入模块

  • PC
  • NPC
  • IM
  • FDreg,DEreg,EMreg,MWreg
  • [D,E,M,W]Controller
  • GRF
  • EXT
  • CMP
  • ALU
  • DM

wire变量的赋值

大体与P4相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
assign D_A1 = D_rs;
assign D_A2 = D_rt;
assign D_A3 = (D_Regra) ? 5'b11111 :
(D_RegDst) ? D_rd : D_rt;

assign E_inputA = E_fixedRD1;
assign E_inputB = (E_ALUsrc) ? E_ext32 : E_fixedRD2;

assign M_addr = M_ALUres;
assign M_MemData = M_fixedRD2;

assign W_WD = (W_PCtoReg) ? W_PCplus8 :
(W_MemtoReg) ? W_data : W_ALUres;

(仅列出部分关键的赋值)

转发与阻塞

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
//优先级 E > M > W
assign D_fixedRD1 = (D_RD1_from_E_PCplus8) ? E_PCplus8 :
(D_RD1_from_M_PCplus8) ? M_PCplus8 :
(D_RD1_from_M) ? M_outputA :
(D_RD1_from_W) ? W_WD_Reg : D_RD1;
...

//tuse and tnew
assign D_t_rsuse = D_t_rs;
assign D_t_rtuse = D_t_rt;
assign D_t_new = D_t;

assign E_t_rsuse = (E_t_rs == 4'hf) ? 4'hf :
(E_t_rs >= 4'h1) ? (E_t_rs - 4'h1) : 4'h0;
assign E_t_rtuse = (E_t_rt == 4'hf) ? 4'hf :
(E_t_rt >= 4'h1) ? (E_t_rt - 4'h1) : 4'h0;
assign E_t_new = (E_t == 4'hf) ? 4'hf :
(E_t >= 4'h1) ? (E_t - 4'h1) : 4'h0;
...

//isPCplus8 (110为jal的编码)
assign E_isPCplus8 = (E_type == 6'b000110)
assign M_isPCplus8 = (M_type == 6'b000110)

// forward
// 判断条件:1. 是否为 isPCplus8
// 2. 寄存器编号是否为 0
// 3. 转发起点和终点寄存器是否相同
// 4. t_use 值是否存在
// 5. t_new 值是否存在
// 6. t_use 是否大于等于 t_new
assign D_RD1_from_E_PCplus8 = (E_isPCplus8 && E_A3 != 5'b00000 && D_rs == E_A3 && D_t_rsuse != 4'hf && E_t_new != 4'hf && D_t_rsuse >= E_t_new) ? 1'b1 : 1'b0;
assign D_RD2_from_E_PCplus8 = (E_isPCplus8 && E_A3 != 5'b00000 && D_rt == E_A3 && D_t_rtuse != 4'hf && E_t_new != 4'hf && D_t_rtuse >= E_t_new) ? 1'b1 : 1'b0;
...

// stall
// 判断条件:1. 寄存器编号是否为 0
// 2. 转发起点和终点寄存器是否相同
// 3. t_use 值是否存在
// 4. t_new 值是否存在
// 5. t_use 是否小于 t_new
assign D_stall = (E_A3 != 5'b00000 && D_rs == E_A3 && D_t_rsuse != 4'hf && E_t_new != 4'hf && D_t_rsuse < E_t_new) ? 1'b1 :
(E_A3 != 5'b00000 && D_rt == E_A3 && D_t_rtuse != 4'hf && E_t_new != 4'hf && D_t_rtuse < E_t_new) ? 1'b1 :
(M_A3 != 5'b00000 && D_rs == M_A3 && D_t_rsuse != 4'hf && M_t_new != 4'hf && D_t_rsuse < M_t_new) ? 1'b1 :
(M_A3 != 5'b00000 && D_rt == M_A3 && D_t_rtuse != 4'hf && M_t_new != 4'hf && D_t_rtuse < M_t_new) ? 1'b1 :
(W_A3 != 5'b00000 && D_rs == W_A3 && D_t_rsuse != 4'hf && W_t_new != 4'hf && D_t_rsuse < W_t_new) ? 1'b1 :
(W_A3 != 5'b00000 && D_rt == W_A3 && D_t_rtuse != 4'hf && W_t_new != 4'hf && D_t_rtuse < W_t_new) ? 1'b1 : 1'b0;

清空延迟槽:FDclear = (D_opcode == 6'bXXXXXX & !D_zero & !D_stall) ? 1'b1 : 1'b0

测试方案

用了往届学长博客中的MIPS自动生成评测机进行测试。

另手动构造了一些特殊数据,如:$0寄存器,接近16位和32位立即数范围边缘的数,向前,自身,向后跳转等。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# LUI, ORI
init:
lui $1, 0x1234 # $1 = 0x12340000
ori $1, $1, 0x5678 # $1 = 0x12345678
ori $2, $0, 10 # $2 = 0x0A
ori $3, $0, 20 # $3 = 0x14
nop

# ADD, SUB
arithmetic:
add $4, $2, $3 # $4 = 0x1E
sub $5, $3, $2 # $5 = 0x0A

ori $0, $0, 0xFFFF # Try writing $0
add $6, $0, $2 # $6 = 0x0A (Verify $0 remains 0)

# Forwarding
forwarding_alu:
add $7, $2, $3 # $7 = 0x1E
add $8, $7, $2 # $8 = 0x28 (EX-EX Forwarding)

sub $9, $3, $2 # $9 = 0x0A
nop
add $10, $9, $2 # $10 = 0x14 (MEM-EX Forwarding)

# SW, LW
memory:
sw $1, 0($0) # Mem[0] = 0x12345678
sw $8, 4($0) # Mem[4] = 0x28

add $11, $0, $3 # $11 = 0x14
sw $11, 8($0) # Mem[8] = 0x14 (Store Forwarding)

lw $12, 0($0) # $12 = 0x12345678
lw $13, 8($0) # $13 = 0x14

# Stall
stall_test:
lw $14, 4($0) # $14 = 0x28
add $15, $14, $2 # $15 = 0x32 (Stall 1 cycle + Forwarding)

# BEQ
branch_test:
beq $2, $3, error # Not Taken
ori $16, $0, 1 # $16 = 1 (Pass Not Taken)

beq $2, $2, jump_ok # Taken
ori $16, $0, 0xBAD # Should be skipped

error:
ori $30, $0, 0xDEAD # Fail flag
j end

jump_ok:
ori $17, $0, 2 # $17 = 2 (Pass Taken)

# JAL, JR
jal_test:
jal my_func # Call
nop # Delay Slot

ori $18, $0, 3 # $18 = 3 (Return Success)
j end_test
nop

my_func:
add $19, $4, $5 # $19 = 0x28
jr $31 # Return
nop

# END
end_test:
ori $20, $0, 0xACE # Final Success Flag
end:
beq $0, $0, end
nop

执行 $8 的计算时:观察 $7 的结果是否直接从 ALU 的输出(或者流水线寄存器)“旁路”到了 $8 的 ALU 输入端。如果不转发,$8 可能会算错。

执行 $15 的计算时:观察 PC 是否在 add $15… 这一行保持不变一个周期(或者插入了一个 nop 气泡),等待 $14 从数据存储器读出来。

执行 beq $2, $2 时:观察下一条指令是否被清除(Flush)或者是否正确执行了延迟槽指令(取决于你的 CPU 是否实现了延迟槽)。

执行 jr $31 时:观察 PC 是否正确跳转回了 jal 指令的下下条地址。

思考题

  • 我们使用提前分支判断的方法尽早产生结果来减少因不确定而带来的开销,但实际上这种方法并非总能提高效率,请从流水线冒险的角度思考其原因并给出一个指令序列的例子。

    • 提前分支判断可能引发控制冒险,当分支预测错误时,流水线中已取出的后续指令需要 flush,导致气泡产生,反而降低效率。
    1
    2
    3
    4
    beq $t0, $t1, label
    add $t2, $t3, $t4
    sub $t5, $t6, $t7
    label: and $t8, $t9, $t10
  • 因为延迟槽的存在,对于 jal 等需要将指令地址写入寄存器的指令,要写回 PC + 8,请思考为什么这样设计?

    • 在 MIPS 流水线中,jal指令执行时,延迟槽的指令会被执行,jal指令的下下条指令地址是PC + 8。
  • 我们要求所有转发数据都来源于流水寄存器而不能是功能部件(如 DM、ALU),请思考为什么?

    • 流水寄存器保存了每个阶段的稳定数据。功能部件在工作时数据是动态变化的,若从功能部件转发,无法保证数据的稳定性和可预测性,会导致数据冲突或错误。
  • 我们为什么要使用 GPR 内部转发?该如何实现?

    • 原因:当寄存器的写操作和读操作在同一周期发生时,避免数据冲突,保证读取到的是最新写入的数据。
    • 实现:增加内部转发通路,当检测到同一周期内有对同一寄存器的写和读操作时,将写数据直接转发给读端口。
  • 我们转发时数据的需求者和供给者可能来源于哪些位置?共有哪些转发数据通路?

    • 十五条通路:
      • 起点:E_PCplus8 , M_PCplus8 , ALUres , DMdata
      • 终点:D_RD1 , D_RD2 , E_RD1 , E_RD2 , MemAddr
      • 去除五条同级&向后转发的
  • 在课上测试时,我们需要你现场实现新的指令,对于这些新的指令,你可能需要在原有的数据通路上做哪些扩展或修改?提示:你可以对指令进行分类,思考每一类指令可能修改或扩展哪些位置。

    • ALU & EXT 等功能模块中的计算逻辑
    • 定义新的trs,trt,t
    • Controller中添加新的信号
    • 添加新的阻塞信号
    • 爆改通路,比如将DM中数据接到寄存器的选择等
  • 简要描述你的译码器架构,并思考该架构的优势以及不足。

    • 我使用控制信号驱动型,代码量小但可读性差,不易于bug排查