设计草稿

流水线架构

数据通路

5 级流水 CPU 分为 5 个阶段:取指阶段(F),译码阶段(D),执行阶段(E),存储阶段(M),写回阶段(W)。通过在两个阶段之间加入寄存器,来保存在前一周期中的上一阶段所传来的数据,同时在后一周期中为下一阶段提供数据。无论是数据还是信号,都需要在寄存器中进行保存,直至不再需要。

下面先从增加流水线寄存器入手,完善数据通路:

相对 P4,需要修改的地方为:

  • 新增流水线寄存器
  • NPC 模块输出 PC+4 改为输出 PC+8,并区分输入 F_PC 与 D_PC
  • ALU 不再有 Zero 信号,并且将判断相等提前到对 E_GPR_RD1 与 E_GPR_RD2 新增比较器

译码器

采用集中式译码:在取指令(F 级)时或者读取寄存器阵列信息(D 级)前,将所有的控制信号全部解析出,然后让其随着流水往后逐级传递。

流水线冒险

在流水线系统中,多条指令同时执行,当一条指令依赖于还没有结束的另一条指令的结果时,将发生冒险(hazard),也被称为“冲突”。冒险的类型,主要有结构冒险( Structural Hazard )、控制冒险( Control Hazard )和数据冒险( Data Hazard )三种。

结构冒险

结构冒险是指不同指令同时需要使用同一资源的情况。

此 CPU 的结构冒险在于 GPR 需要在 D 级和 W 级同时被使用(读写)时并且读和写的寄存器为同一个寄存器时。

解决方案

内部转发:GPR 是一个特殊的部件,它既可以视为 D 级的一个部件,也可以视为 W 级之后的流水线寄存器。基于这一特性,我们将对 GPR 采用内部转发机制。也就是说,当前 GPR 被写入的值会即时反馈到读取端上。

具体的说,当读寄存器时的地址与同周期写寄存器的地址相同时,我们将读取的内容改为写寄存器的内容,而不是该地址可以索引到的寄存器文件中的值。

方案实现

合并到数据冒险中解决(MUX_DataToReg 转发到 D_GPR_RD1 与 D_GPR_RD2)。

控制冒险

控制冒险,是指分支指令(如 beq )的判断结果会影响接下来指令的执行流的情况。在判断结果产生之前,我们无法预测分支是否会发生。然而,此时流水线还会继续取指,让后续指令进入流水线。这时就有可能导致错误的产生,即不该被执行的指令进入到了指令的执行流中。

解决方案

提前分支判断+延迟槽:在构建数据通路时均已实现(新增比较器、jal指令将PC+8赋值到$ra寄存器)。

(注:编译器保证延迟槽中的指令不能为分支或跳转指令。)

数据冒险

流水线之所以会产生数据冒险,就是因为后面指令需求的数据,正好就是前面指令供给的数据,而后面指令在需要使用数据时,前面供给的数据还没有存入寄存器堆,从而导致后面的指令不能正常地读取到正确的数据。

解决方案

阻塞+转发(AT 法)

A 模型

A 指 Address,也就是寄存器的地址(编号)。在转发的时候需要检验需求者和供给者的对应的 A 值是否相等,且不为 0。

T 模型

对于某一个指令的某一个数据需求,我们定义需求时间 Tuse 为:这条指令位于 D 级的时候,再经过多少个时钟周期就必须要使用相应的数据(定值)。

对于某个指令的数据产出,我们定义供给时间 Tnew 为:位于某个流水级某个指令,它经过多少个时钟周期可以算出结果并且存储到流水级寄存器里(动态变化)。

  • Tuse ≥ Tnew,说明需要的数据可以及时算出,可以通过转发来解决。
  • Tuse < Tnew,说明需要的数据不能及时算出,必须阻塞流水线解决。

方案实现

译码器

指令 E_Tnew 供给者 Tuse 需求者
add 1 rd 1 rs,rt
sub 1 rd 1 rs,rt
ori 1 rt 1 rs
lui 1 rt
lw 2 rt 1 rs(base)
sw 1(rs),2(rt) rs(base),rt
beq 0 rs,rt
jal 0 $ra
jr 0 rs

对于每种指令,根据上表在 D 级译出AT信息,并随着流水线流水。其中 Tuse 的值可能会随流水线阶段变化,具体为 M_Tuse = max(E_Tuse − 1, 0)W_Tuse = max(M_Tuse − 1, 0)

阻塞

当一条指令满足阻塞的必要时,需要将其阻塞在 D 级。

即需要实现:

  • 冻结 PC 值;
  • 冻结 D 级流水线寄存器的值;
  • 将 E 级流水线寄存器清零(等价于插入了一个 nop 指令)。

注意优先级顺序:复位信号 > 阻塞信号。

转发

全力转发。

供给者(需要写入寄存器的值)共有三种:

  • ALUResult:M_ALUResult,W_ALUResult

  • DM_RD:W_DM_RD

  • PC8:E_PC8,M_PC8,W_PC8

注:W_ALUResult,W_DM_RD,W_PC8 合并为 MUX_DataToReg。

需求者(使用或传递 rs 或 rt 寄存器值的功能部件和流水线寄存器):

  • D_GPR_RD1

  • D_GPR_RD2

  • ALU_SrcA

  • ALU_SrcB

  • DM_WD

控制

我们的控制器需要产生的信号包括但不限于冻结信号,刷新信号,供给者选择器信号,需求者选择器信号等。

为此新增一个冒险控制器

每当一条指令在 D 级译出AT 信息时,冒险控制器比较该指令的 <Tuse,rs,rt> 与前面每一级的指令的< Tnew,rt,rd,$ra>,若满足:

  • 该指令的<读寄存器> = 前面指令的<写寄存器>(前面指令选取满足上述条件的最靠后的指令),且
    • Tuse < Tnew,则进行阻塞(三个操作);
    • Tuse ≥ Tnew,则进行转发。

image-20241111164031216

测试方案

使用同学的数据生成器生成测试代码,然后用 bat 脚本进行 CPU 文件自动编译和重定向输出 ISE 与 Mars 的输出结果,最后用 VSCode 的文本比较功能进行对拍。

当随机生成数据的输出结果出现不一致时,猜测可能的错误并手动编写简短的代码以方便调试来查找错误,如:

1
2
3
4
5
6
ori $31, $0, 1
sw $31, 0($0)
ori $31, $0, 2
lw $31, 0($0)
nop
sw $31, 0($0)

以下是 bat 脚本内容:

1
2
3
4
5
6
7
8
9
10
11
@echo off
Rem This is a bat to generate outputs of ISE and Mars
main.exe
cd ..
java -jar mars.jar mc CompactDataAtZero a dump .text HexText P5/code.txt P5/std.asm
cd P5
fuse -nodebug -prj mips_stx_beh.prj -o mips.exe mips_tb > nul
mips.exe -nolog -tclbatch mips.tcl > outputs/ise_output.txt
cd ..
java -jar mars.jar P5/std.asm mc CompactLargeText coL1 ig> P5/outputs/mars_output.txt
echo "The program is over"

其中,main.exe是借助讨论区的.asm 代码随机生成程序。

line 5 输出 Mars 的 16 进制机器码;

line 7 编译 ISE 的 mips.v 模块;

line 8 将 ISE 的运行结果重定向输出;

line 10 将 Mars 的运行结果重定向输出。

在使用前根据需要更改相应的文件地址,然后直接点击该脚本文件即可运行。

思考题

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

beq判断相等的两个寄存器rsrt值需要改变但未更新,则beq需要阻塞以等待寄存器正确值的写入,这样与不提前分支判断无异。

1
2
add $a0,$0,$a1
beq $a0,$a2,label

2、因为延迟槽的存在,对于 jal 等需要将指令地址写入寄存器的指令,要写回 PC + 8,请思考为什么这样设计?

由于延迟槽的存在,jal指令的后一条指令会在jal发生跳转时依然被执行,所以在执行jr $ra的时候应跳转到jal的往后第二条指令,也就是PC + 8处。

3、我们要求大家所有转发数据都来源于流水寄存器而不能是功能部件(如 DM 、 ALU ),请思考为什么?

  • 转发源的数据应该是正确的数据(即可能是经过转发后的数据),而不是直接来自功能部件;
  • 保证转发源的一致性(即都来自流水线寄存器),使编程和调试更清晰。

4、我们为什么要使用 GPR 内部转发?该如何实现?

在同一周期内,需要对同一个 GRF 的寄存器进行读和写,由于写寄存器在时钟上升沿到来时才会发生,故读取的值为旧值,需要进行转发。

具体实现为在 GPR 的 RD1 和 RD2 处增加转发 MUX,以筛选正确的读取值进行流水。

5、我们转发时数据的需求者和供给者可能来源于哪些位置?共有哪些转发数据通路?

需求者 5 处:D_GPR_RD1,D_GPR_RD2,E_ALU_SrcA,E_ALU_SrcB,M_DM_WD

供给者 3 处:E 级(E_PC8),M 级(M_ALUResult,M_PC8),W 级(W_PC8,W_ALUResult,W_DM_RD)

6、在课上测试时,我们需要你现场实现新的指令,对于这些新的指令,你可能需要在原有的数据通路上做哪些扩展或修改?提示:你可以对指令进行分类,思考每一类指令可能修改或扩展哪些位置。

将指令大致按照功能分类:计算,跳转,存储

  • 计算:一般只需增加 ALU 的功能。

  • 跳转:跳转类一般分为以下几种:

    • 条件跳转+无条件链接

    • 条件跳转+条件链接

    • 条件跳转+(无)条件链接+不跳转时清空延迟槽

    条件跳转一般只需增加 CMP 模块中的判断功能即可。

    无条件链接类似jal$ra写入 PC+8。

    条件链接需要在 D 级根据 CMP 模块的判断结果来决定是否要链接,即 RegWrite 是否有效:

    1
    2
    3
    4
    5
    wire D_RegWrite_new = D_new ? (D_CMP_out ? 1'b1 : 1'b0) : D_RegWrite;
    E e(//......
    .D_RegWrite(D_RegWrite_new),
    //......
    );

    不跳转时清空延迟槽则需要根据当前CMP 模块输出结果判断是否清空D 级流水寄存器需要注意的是,如果当前正在处于stall状态时,不能清空延迟槽(stall说明前面指令的Tnew大于新指令的Tuse,即需要传入 CMP 模块的两个值的最新值还没有计算出来,因此还无法转发到 CMP 中):

    1
    wire D_clr = D_new & ~D_CMP_out & ~stall;
  • 存储:条件存储也就是从 DM 取出值之后,根据这个值是否满足某个 condition,再判断要往哪个寄存器写。一般分为以下几种:

    • condition 成立:将 DM 中的值写入 A 号寄存器

      condition 不成立:写入 B 号寄存器

    • condition 成立:将 DM 中的值写入 A 号寄存器

      condition 不成立:不写入(可以假设写入地址为0 号寄存器

    • 写入目标完全取决于 DM 的读取值(如将 DM 读取值的低 5 位作为写入目标)

    由于条件存储类指令只有到 M 级才知道写入的目标,故需要对 stall 信号的逻辑进行修改(现有的指令均在 D 级就知道要写入的目标寄存器)。引用学长的话说就是——“如果 D 级的指令要读寄存器,而且后面的新指令可能 要写这个寄存器,那么就 stall”

    1
    2
    3
    4
    5
    6
    7
    //第一种题型(eg:condition满足向rt写,否则写$ra)
    assign Stall_RS0_E1 = E_WE && (Tuse_RS0 && Tnew_E == 2'b01) && (E_new ? (D_rs == E_A3 || D_rs == 5'd31) : D_rs == E_A3) && D_rs != 5'd0 && ~(D_A3 == 5'd0 && D_WE);
    //第二种题型(eg:condition满足向31号写,否则不写)
    //按照第一种题型写成(new ? (D_rs == 5'd31 || D_rs == 5'd0) : D_rs == E_A3),可以与D_rs != 5'd0合并
    assign Stall_RS0_E1 = E_WE && (Tuse_RS0 && Tnew_E == 2'b01) && (E_new ? D_rs == 5'd31 : D_rs == E_A3) && D_rs != 5'd0 && ~(D_A3 == 5'd0 && D_WE);
    //第三种题型(eg:condition满足写入DM的读取值的低五位)
    assign Stall_RS0_E1 = E_WE && (Tuse_RS0 && Tnew_E == 2'b01) && (E_new ? 1'b1 : D_rs == E_A3) && D_rs != 5'd0 && ~(D_A3 == 5'd0 && D_WE);

    此外我们还需要在 M 级根据 DM 取出的值修改A3(GRF 写入地址):

    1
    2
    3
    4
    5
    6
    //第一种题型(eg:condition满足向rt写,否则写$ra)
    wire M_RegDst_new = M_new ? (condition ? M_rt : 5'd31) : M_RegDst;
    //第二种题型(eg:condition满足向31号写,否则不写)
    wire M_RegDst_new = M_new ? (condition ? 5'd31 : 5'd0) : M_RegDst;
    //第三种题型(eg:condition满足写入DM的读取值的低五位)
    wire M_RegDst_new = M_new ? (condition ? M_DM_RD : 5'd0) : M_RegDst; //M_DM_RD首次出现

    并修改相应的部件连接:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    W w(//......
    .M_RegDst(M_RegDst_new),
    //......
    )

    Adv_CTRL adv_ctrl(//......
    .M_RegDst(M_RegDst_new),
    //......
    )

7、确定你的译码方式,简要描述你的译码器架构,并思考该架构的优势以及不足。

集中式译码。分设控制器和冒险控制器,在 D 级译出指令种类和 AT 信息,并产生相应的控制信号、阻塞信号和转发信号,然后流水。

优势:只需要在初始对指令进行一次译码,减少了后续流水级的逻辑复杂度;

不足:流水的数据信息量大。

1、[P5 选做] 在冒险的解决中,我们引入了 AT 法,如果你有其他的解决方案,请简述你的思路,并给出一段指令序列,简单说明你是如何做到尽力转发的。

2、[P5 选做] 请详细描述你的测试方案及测试数据构造策略。

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

写在最后

  • 对于不写寄存器的指令的 Tuse 为 0,是否会有问题?

    如果使用D_RS0_1 = ...;这种写法感觉没问题。

  • 对于写$0的指令不需要阻塞,即在阻塞的条件加上&& ~(D_A3 == 5'd0 && D_WE)能否正确地节省时间?

    感觉可以。若涉及到条件存储,在 D 级无法知道要写入的寄存器,可能导致该阻塞的未阻塞。