设计草稿

根据P5CPU设计,需要进行以下改变:

  • 乘除模块MDU,内带HI,LO两个寄存器。设置start,busy信号用于模拟多周期乘除法运算
  • DE模块,用于实现LW,LH,LB指令
  • BE模块,用于实现SW,SH,SB指令
  • 内存外置:将原IM和DM删除,并在mips主模块增加接口
  • 增加bne,slt等简单指令

乘除法八条指令

MDU模块

注意$signed即可。

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
- type = mult(u)
- {HI, LO} <= inputA * inputB;
- type = div(u)
- inputB != 0
- HI <= inputA % inputB;
- LO <= inputA / inputB;
- type = mfhi/mflo
- MDUres = HI/LO;
- type = mthi/mtlo
- HI/LO <= inputA;
```

#### 模拟多周期运算

- 当 E 级指令是`mult``multu``div``divu`指令时,Controller模块需要输出1个周期的`start`信号,表示乘除法计算开始;
- 当`start`信号结束后,如果之前的计算指令是`mult``multu`指令,MDU 模块需要连续输出 5 个周期的`busy`信号,表示正在计算`HI`寄存器和`LO`寄存器的值;如果之前的计算指令是 div divu 指令,则需要连续输出 10 个周期的`busy`信号;
- 当`start`信号或`busy`信号为 1 时,不进入 MDU 模块的指令正常流水,进入 MDU 模块的所有指令均阻塞在 D 级;

```Verilog
# Controller
assign start = (mult | multu | div | divu) ? 1'b1 : 1'b0;
# MDU
assign busy = (count != 4'h0) ? 1'b1 : 1'b0;
# mips
assign D_ismult = (D_type == 6'b010001 || D_type == 6'b010010 || D_type == 6'b010011 || D_type == 6'b010100 ||
D_type == 6'b010101 || D_type == 6'b010110 || D_type == 6'b010111 || D_type == 6'b011000) ? 1'b1 : 1'b0;

按半字/字节访存

DE模块

  • 若指令为lw:fixeddata = data
  • 若指令为lh:
    • addr[1] == 1'b0:fixeddata = data[15:0]
    • addr[1] == 1'b1:fixeddata = data[31:16]
  • 若指令为lb:
    • addr[1:0] == 2'b00:fixeddata = data[7:0]
    • addr[1:0] == 2'b01:fixeddata = data[15:8]
    • addr[1:0] == 2'b10:fixeddata = data[23:16]
    • addr[1:0] == 2'b11:fixeddata = data[31:24]

都需要进行signed_extend

DM模块

  • 将RegWrite改为4位,以独热码形式存储每八位是否该写入。

BE模块

  • output:RegWrite,fixedWD
    • 例:type == SB && addr[1:0] == 2'b10,则RegWrite = 4'b0100,fixedWD = {8'h00, data[7:0], 16'h0000}

主模块

内存外置

1
2
3
4
5
6
7
8
9
10
11
assign F_Instr = i_inst_rdata;
assign M_DMdata = m_data_rdata;
assign i_inst_addr = F_PC;
assign m_data_addr = M_MemAddr;
assign m_data_wdata = M_fixedMemData;
assign m_data_byteen = M_MemByteWrite;
assign m_inst_addr = M_PC;
assign w_grf_we = W_RegWrite;
assign w_grf_addr = W_A3;
assign w_grf_wdata = W_WD;
assign w_inst_addr = W_PC;

导入模块

  • PC
  • NPC
  • FDreg,DEreg,EMreg,MWreg
  • [D,E,M,W]Controller
  • GRF
  • EXT
  • CMP
  • ALU
  • MDU
  • DE
  • BE

测试方案

用了往届学长博客中的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
addi    $t0, $zero, 0x1234
sw $t0, 0($s0) # Mem[0] = 0x00001234
lw $t1, 0($s0) # Load word
add $t2, $t1, $zero
bne $t2, $t0, fail_mem

addi $t0, $zero, 0x80 # 0x00000080 (-128 if byte)
sb $t0, 4($s0) # Mem[4] low byte = 0x80
lb $t1, 4($s0) # Load byte
addi $t2, $zero, -128
bne $t1, $t2, fail_lb

lui $t0, 0xFFFF
ori $t0, $t0, 0xABCD # $t0 = 0xFFFFABCD
sh $t0, 8($s0) # Mem[8] half = 0xABCD
lh $t1, 8($s0) # Load half
bne $t1, $t0, fail_lh

# 乘除法

addi $t0, $zero, 10
addi $t1, $zero, -2
mult $t0, $t1 # 10 * -2 = -20
mflo $t2
addi $t3, $zero, -20
bne $t2, $t3, fail_mult

addi $t0, $zero, -1 # 0xFFFFFFFF
addi $t1, $zero, 2
multu $t0, $t1 # (2^32 - 1) * 2
mfhi $t2 # Hi 应为 1 (进位)
mflo $t3 # Lo 应为 0xFFFFFFFE (-2)
addi $t4, $zero, 1
bne $t2, $t4, fail_multu_h
addi $t4, $zero, -2
bne $t3, $t4, fail_multu_l

addi $t0, $zero, 17
addi $t1, $zero, 5
div $t0, $t1 # 17 / 5 = 3 ... 2
mflo $t2 # 商
mfhi $t3 # 余数
addi $t4, $zero, 3
bne $t2, $t4, fail_div
addi $t4, $zero, 2
bne $t3, $t4, fail_div

addi $t0, $zero, -20 # Large unsigned number
addi $t1, $zero, 10
divu $t0, $t1
mflo $t2
slt $t3, $t2, $zero
bne $t3, $zero, fail_divu

addi $t0, $zero, 0xAAAA
addi $t1, $zero, 0x5555
mtlo $t0
mthi $t1
mflo $t2
mfhi $t3
bne $t2, $t0, fail_mov
bne $t3, $t1, fail_mov

并非完整代码,只展示了测试到新代码的部分。

思考题

  • 为什么要单独的乘除法部件?为什么有独立的 HI/LO 寄存器?
    • 速度不匹配。加减法只需 1 个周期,乘除法通常需几十个周期。独立后可以实现独立计算。
    • 存取逻辑与一般寄存器不同。
  • 真实的流水线 CPU 是如何实现乘除法的?
    • 乘法:使用 Booth 算法 或 华莱士树 等硬件电路加速,通常也是流水线的(比如 4 级流水),吞吐率很高。
    • 除法:通常使用试商法(如 SRT 算法),比较慢,依然是现代 CPU 中最耗时的整数指令之一。
  • Busy 信号与阻塞处理?
    • 当 E 级指令是mult``multu``div``divu指令时,Controller模块需要输出1个周期的start信号,表示乘除法计算开始;
    • start信号结束后,如果之前的计算指令是mult``multu指令,MDU 模块需要连续输出 5 个周期的busy信号,表示正在计算HI寄存器和LO寄存器的值;如果之前的计算指令是 div divu 指令,则需要连续输出 10 个周期的busy信号;
    • start信号或busy信号为 1 时,不进入 MDU 模块的指令正常流水,进入 MDU 模块的所有指令均阻塞在 D 级;
  • 采用字节使能信号的方式处理写指令有什么好处?
    • 统一接口:无论写 1 个字节还是 4 个字节,数据线都可以固定为 32 位。
    • 不需要“先读出来、改掉 8 位、再写回去”这种复杂操作,硬件直接根据使能信号只修改内存中对应的字节。
  • 我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢?
    • 不是一字节,而是一字(32位)。
    • 按字(Word)读写效率最高。因为这是硬件的原生方式。按字节读写需要 CPU 内部额外进行“移位”和“拼接”操作,虽然周期数通常一样,但逻辑延迟略大。
  • 为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助?
    • 模块化:把 CPU 拆分成取指、译码、执行等独立模块。
    • 控制信号集束:用“真值表”的方式生成控制信号,而不是用大量的 if-else。
  • 在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?
    • 数据冒险:阻塞与转发。
    • Load-Use冒险:阻塞。
    • 分支冒险:延迟槽。
1
2
lw  $t1, 0($t0)
add $t2, $t1, $t3
  • 手动构造的样例,说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况。
    • 覆盖率优先:确保每一条指令至少执行一次。
    • 边界测试:测试极值(0, -1, 最大正数, 最小负数)以验证 溢出和扩展逻辑。
    • 特定冒险序列:人为构造距离为 1(相邻)和距离为 2(隔一条)的数据依赖,专门测试转发和阻塞单元。