设计要求

处理器应为五级流水线设计,支持如下指令集:

1
2
3
4
5
add, sub, and, or, slt, sltu, lui
addi, andi, ori
lb, lh, lw, sb, sh, sw
mult, multu, div, divu, mfhi, mflo, mthi, mtlo
beq, bne, jal, jr

请注意,所有运算类指令均暂不考虑因溢出而产生的异常。

除加减法外的指令均以指令集规定的行为为准,可参看 MIPS-C 指令集(该指令集描述针对单周期,注意单周期和流水线有些许指令行为描述不同,但最终结果是一致的),加减法按无符号处理(不考虑溢出)。若对本次实验有任何疑问请及时在讨论区提出。

设计草稿

新增指令 AT 信息

新增指令顺序:

  • 先加入涉及 MUDI 的乘除法指令,完善相应的数据通路及(冒险)控制信号;
  • 再加入涉及 ALU 的运算指令,对 ALU 做出相应修改;
  • 再加入访存指令,新增 BE 扩展模块与 RE 扩展模块;
  • 最后加入跳转指令,修改 CMP 模块。

乘除法相关指令

指令 E_Tnew 供给者 Tuse 需求者
mult 1 rs,rt
multu 1 rs,rt
div 1 rs,rt
divu 1 rs,rt
mfhi 1 rd
mflo 1 rd
mthi 1 rs
mtlo 1 rs

ALU 运算指令

指令 E_Tnew 供给者 Tuse 需求者
and 1 rd 1 rs,rt
or 1 rd 1 rs,rt
slt 1 rd 1 rs,rt
sltu 1 rd 1 rs,rt
addi 1 rt 1 rs
andi 1 rt 1 rs

访存指令

指令 E_Tnew 供给者 Tuse 需求者
lb 2 rt 1 rs(base)
lh 2 rt 1 rs(base)
sb 1(rs),2(rt) rs(base),rt
sh 1(rs),2(rt) rs(base),rt

跳转指令

指令 E_Tnew 供给者 Tuse 需求者
bne 0 rs,rt

新增模块

乘除法模块(MUDI)

信号端口 方向 功能
clk I 时钟信号
reset I 复位信号
D1[31:0] I 乘除运算数 D1
D2[31:0] I 乘除运算数 D2
Op[1:0] I 乘除法选择信号
Start I 启动信号
LO[31:0] O lo输出
HI[31:0] O hi输出
Busy O 延迟信号。
  • 自 Start 信号有效后的第 1 个 clock 上升沿开始,乘除法部件开始执行运算,同时将 Busy 置位为 1。

  • 在运算结果保存到 HI 寄存器和 LO 寄存器后,Busy 位清除为 0。

  • 当 Busy 信号或 Start 信号为 1 时,mult, multu, div, divu, mfhi, mflo, mthi, mtlo 等乘除法相关的指令均被阻塞在 D 流水级。

  • 数据写入 HI 寄存器或 LO 寄存器,均只需 1 个时钟周期。

  • 运算数可能为有符号数,也有可能为无符号数

BE

BE 扩展模块需放在与数据存储器交互数据之前,用于产生字节写使能信号和满足需要的写入 DM 数据。

用于处理存储内存指令(如sb):

  • 生成字节写使能信号传入 DM
  • 将 DM_WD 输出为满足需要的数据
信号 方向 描述
Addr[31:0] I 计算出来的地址值
Op[1:0] I 选择信号
GPR_RD2[31:0] I GPR[rt]
Byteen[3:0] O 字节使能
DM_WD[31:0] O 写入 DM 的数据

数据扩展模块(RE)

对于 lblh 来说,我们需要额外增加一个数据扩展模块。这个模块把从数据存储器读出的数据做符号扩展。

用于处理读取内存指令(如lb):

  • 对 DM 读取的数据进行相应的选择扩展
信号 方向 描述
A[1:0] I 最低两位地址
Din[31:0] I 输入的 32 位数据
Op[2:0] I 数据扩展控制码
000:无扩展
001:无符号字节数据扩展
010:符号字节数据扩展
011:无符号半字数据扩展
100:符号半字数据扩展
Dout[31:0] O 扩展后的 32 位数

删除模块

IM 和 DM 外置,CPU 内只需提供相应的接口。

IM(外置)

信号 方向 描述
i_inst_addr[31:0] I 需要进行取指操作的流水级 PC(一般为 F 级)
i_inst_rdata[31:0] O i_inst_addr 对应的 32 位指令

DM(外置)

信号 方向 描述
m_data_addr[31:0] I DM 的相应地址
m_data_wdata[31:0] I 需要写入 DM 的数据
m_data_byteen[3:0] I 四位字节使能
m_inst_addr[31:0] I M 级 PC
m_data_rdata[31:0] O DM 存储的相应数据

m_data_byteen[3:0] 是字节使能信号,其最高位到最低位分别与 m_data_wdata[31:24][23:16][15:8][7:0] 对应(即一位对应一个字节)。

m_data_byteen 的任意一位为 1,则代表当前需要写入内存,也就是说可以用 |m_data_byteen 代替数据存储器的写使能信号。

测试方案

使用往届学长学姐的自动评测机 COKiller,对于出现错误的指令,设计简短的测试代码进行调试。

思考题

  1. 为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器?

    乘除法运算时会有延迟,若整合进 ALU 会导致其它使用 ALU 的指令必须阻塞,而设计单独的乘除法部件可以让 CPU 在执行乘除运算时依然流水其它使用 ALU 的指令,大大提高运行效率。

  2. 真实的流水线 CPU 是如何使用实现乘除法的?请查阅相关资料进行简单说明。

    在真实的流水线 CPU 中,乘除法的实现通常涉及以下几个方面:

    • 乘法实现

      • 乘法通常由若干个较小的乘法单元组成,这些单元可能是组合逻辑。每个周期计算特定的几位,然后依次累加起来,最终在几个周期后得到正确的最终结果。这种实现方式允许乘法操作在多个周期内分散执行,从而不会阻塞整个 CPU 流水线。
      • 乘法操作可以通过移位和累加的方式实现。在二进制乘法中,每一位的数字代表需要移动的位数,通过移位和累加来完成乘法运算。
    • 除法实现

      • 除法通常使用试商法,也是使用组合逻辑在一个周期内计算 4 位左右的商。经过 8 个周期正好可以计算结束。这种实现方式允许除法操作在多个周期内逐步完成,避免了单个周期内完成大量计算导致的延迟。
      • 除法操作可以通过移位和累减的方式实现。首先对齐除数和被除数,对齐后相减,如果结果大于等于 0,则记录商 1;如果结果小于 0,则记录商 0。之后右移,结果作为下次运算的被除数,并将商左移。循环这个过程,直到除数移回原位,并记录余数。
    • 处理 Busy 信号

      • 在流水线 CPU 中,乘除法操作可能会产生 Busy 信号,这表示操作尚未完成,需要等待。当 Busy 信号为 1 时,CPU 会暂停流水线,直到操作完成。这种阻塞可能会导致 CPU 的整体效率降低,因此需要合理设计以减少其影响。
    • 字节使能信号

      • 在处理写指令时,采用字节使能信号的方式可以提高代码的清晰性和统一性。这样,在shsb的时候只需要传入数据和使能信号,不需要在顶层操控 DM 的具体行为,实现高内聚、低耦合。

    综上所述,真实的流水线 CPU 通过分散计算、试商法、移位累加/累减等技术实现乘除法,同时通过合理的信号控制和数据路径设计来处理 Busy 信号和字节能,以保持流水线的效率和 CPU 的整体性能。

  3. 请结合自己的实现分析,你是如何处理 Busy 信号带来的周期阻塞的?

    1
    2
    wire Stall_MUDI = (E_Busy || E_Start) && use_mudi;
    assign stall = (原来的阻塞条件) || Stall_MUDI;
  4. 请问采用字节使能信号的方式处理写指令有什么好处?(提示:从清晰性、统一性等角度考虑)

    m_data_byteen[3:0] 是字节使能信号,其最高位到最低位分别与 m_data_wdata[31:24][23:16][15:8][7:0] 对应(即一位对应一个字节),行为清晰。若 m_data_byteen 的任意一位为 1,则代表当前需要写入内存,也就是说可以用 |m_data_byteen 代替数据存储器的写使能信号。由于四位写使能信号,所以不论什么指令,只要某一字节处需要写,则 Byteen 对应的位置 1,有很好的统一性。

  5. 请思考,我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢?

    不是,写入和读取的都是经过处理的一字。

    当指令序列涉及大量按字节读和写的指令时。

  6. 为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助?

    • 将操作相似的指令合并,统一生成控制信号,如:

      1
      2
      3
      wire load = (lb || lh || lw);
      wire store = (sb || sh || sw);
      assign ALUSrcOp = (ori || load || store || lui || addi || andi) ? 2'b01 : 2'b0;
    • 使用宏定义,使控制信号更加直观,如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      //define.v
      `define ub 3'b001
      `define sb 3'b010
      `define uh 3'b100
      `define sh 3'b101
      //RE.v
      //......
      case(Op)
      `ub:begin
      tb = Din[7+8*A -: 8];
      Dout = {24'b0,tb};
      end
      `sb:begin
      tb = Din[7+8*A -: 8];
      Dout = {{24{tb[7]}},tb};
      end
      //......
      endcase
  7. 在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?

    • 乘除法指令冲突:

      1
      2
      mult $a2,$a1
      mflo $a0

      必要时采取阻塞:

      1
      2
      wire Stall_MUDI = (E_Busy || E_Start) && use_mudi;
      assign stall = Stall_RS || Stall_RT || Stall_MUDI;
    • 写寄存器指令与乘除指令冲突:

      1
      2
      3
      addi $a0,$0,666
      or $a1,$a0,$0
      mult $a2,$a0,$a1

      对 MUDI 模块的输入源采用转发。

    • 其余冲突类似 P5。

  8. 如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证 覆盖 了所有需要测试的情况;如果你是 完全随机 生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何 结合了随机性 达到强测的效果。

    手动构造:针对出错的指令进行特定的构造;

    完全随机:测试数据很难测到极端值,如 0xffffffff 等的情况。

  9. [P5、P6 选做] 请评估我们给出的覆盖率分析模型的合理性,如有更好的方案,可一并提出。

写在最后

搬运自 Hyggge’s Blog

P6 的计算会涉及到乘除模块,也相对比较简单。需要注意 madd、maddu、msub、msubu 等指令(roife 博客。

  • madd为例(将两个数有符号相乘,计算结果与之前的 HI、LO 寄存器中的值相加,而不是覆盖),如果是以下写法会出现问题

    1
    2
    3
    4
    //错误写法1
    {HI_temp, LO_temp} <= {HI, LO} + $signed(A) * $signed(B);
    //错误写法2
    {HI_temp, LO_temp} <= {HI, LO} + $signed($signed(A) * $signed(B));
    • 错误写法 1出现问题的原因是: 位拼接{HI, LO}默认被当做无符号数, 无符号性传递到$signed(A) * $signed(B),因此即使使用了$signed()还是会被当成无符号数进行乘法运算
    • 错误写法 2出现问题的原因是:虽然使用了$signed()屏蔽了外界符号性的传入,但是也屏蔽了位宽信息的传入,所以$signed($signed(A) * $signed(B))的结果实际上是 32 位(因为$signed(A)$signed(B)都是 32 位,又没有外界位宽信息的传入,因此结果被强制规定为 32 位),即高 32 位的数据被截去,在参与后续运算时自然会出现问题。
  • 为了避免上述情况,我们需要在错误写法 2的最外层$signed()中人为传入 64 位位宽信息,学长博客的写法如下——

    1
    2
    3
    4
    //正确写法1
    {HI_temp, LO_temp} <= {HI, LO} + $signed($signed(64'd0) + $signed(A) * $signed(B));
    //正确写法2
    {HI_temp, LO_temp} <= {HI, LO} + $signed($signed({{32{A[31]}}, A[31]}) * $signed({{32{B[31]}}, B[31]}));

    我认为还可以对错误写法 1进行修改——

    1
    2
    //正确写法3
    {HI_temp, LO_temp} <= $sigend({HI, LO}) + $signed(A) * $signed(B);