前言

这是 bluebeanP5 回忆。

P5 开始,计组的难度暴增。课上的 P5 也是大量同学挂 P 的起点。

P5 要求我们设计一个五级流水线,并带有完备的转发、阻塞逻辑。

注:本篇博客中给出的实现方法均为笔者自己的实现方法。实际上的细节实现方法不唯一,笔者的方法也不一定是最优解。

流水线结构

五级流水线包含以下内容:

  • F级:进行取指令工作。
  • D级:进行指令的译码,并取出 GRF 中的数据。
  • E级:进行 ALU 运算操作。
  • M级:进行 DM 存取操作。
  • W级:将数据写回 GRF 中。

其中每两级之间都存在流水线寄存器进行级和级之间的数据交换。

借用 gxp 老师 ppt 中的图片,流水线结构如下所示:

数据冒险与转发

考虑以下指令:

1
2
I1: add $2 $0 $1
I2: add $3 $0 $2

I1I2 的指令在流水线中的流水情况可表示如下(nop 表示空指令):

F级 D级 E级 M级 W级
I1 nop nop nop nop
I2 I1 nop nop nop
nop I2 I1 nop nop
nop nop I2 I1 nop

我们可以看到,当 I1 处在 M 级时, I2 处在 E 级。I1 的数据要在 W 级才能写回 GRF,但是 I2 指令在 E 级就要使用 I1 提供的数据($2 的数据)。如果不采用转发逻辑,则 I2E 级进行计算时将会使用错误的数据进行计算。

这就是一种常见的数据冒险。解决数据冒险的办法是增加从 M 级到 E 级的数据通路,如下图所示(蓝线为新增数据通路):

在将数据传入 ALU 时判断是否产生数据冲突,若产生冲突则采用转发过来的数据。

除了上图所示的 M 级向 E 级转发,流水线中还应该包括 W 级向 E 级转发、W 级向 M 级转发、M 级向 D 级转发、W 级向 D 级转发以及寄存器堆的内部转发。

这里解释一下寄存器堆的内部转发:在同一时刻,我们可能对同一个寄存器既读又写。在这种情况,我们读出的数据应当是要写入的数据(写入的数据是更新的数据)。这就是寄存器堆的内部转发。

推荐将转发功能综合成一个模块。拿我的实现举例:

1
2
3
4
5
6
7
8
assign AD1E = A1useE == 1'b0 ? RD1E  :
A1E == 1'b0 ? 32'b0 :
A1E == A3M ? DataM :
A1E == A3W ? DataW : RD1E;
assign AD2E = A2useE == 1'b0 ? RD2E :
A2E == 1'b0 ? 32'b0 :
A2E == A3M ? DataM :
A2E == A3W ? DataW : RD2E;

解释一下各个 wire 的含义:

  • AD1EAD2EADAdventure_Data 的缩写,E 代表数据来自 E 级,12 分别是要传入 ALU 的两个操作数。
  • A1useEA2useEA1A2 分别是要使用到的两个寄存器编号,use 代表指令有没有用到寄存器。(如 addiA1use1A2use0,因为第二个操作数不来自寄存器堆)
  • RD1ERD2E:来自流水线寄存器的流水数据。
  • A1EA2E:要使用的寄存器编号。
  • A3MA3W:在 M 级和 W 级的指令各自的写入寄存器编号。
  • DataMDataW:分别来自 M 级和 W 级的数据。

如此便实现了 M 级和 W 级向 E 级的转发逻辑。其余的转发逻辑类似。可以集成到一个模块里。

阻塞

有些时候,转发不能处理所有的数据冲突。

举例而言:

1
2
I1: lw $1 0($0)
I2: add $2 $1 $0

假如不增加阻塞逻辑的话,指令流水如下所示:

F级 D级 E级 M级 W级
I1 nop nop nop nop
I2 I1 nop nop nop
nop I2 I1 nop nop
nop nop I2 I1 nop
nop nop nop I2 I1

乍看好像没有问题。但是要注意:lw 指令与其他通常的指令不同,要在 M 级才能产生新的数据。所以当 I1 处于 M 级时,E 级的 I2 是拿不到新数据的。 I1 必须要流水到 W 级才能够提供最新的数据。

于是转发处理不了这种数据冲突。所以我们添加阻塞逻辑。在解释阻塞之前,先看添加了阻塞逻辑之后的流水图:

F级 D级 E级 M级 W级
I1 nop nop nop nop
I2 I1 nop nop nop
nop I2 I1 nop nop
nop I2 nop I1 nop
nop nop I2 nop I1

可以看到,我们在 I1I2 之间插入了一条 nop,从而让 I2 阻塞在 D 级。这样当 I2 流水到 E 级时,就可以拿到 I1 提供的新数据了。

实现阻塞,具体而言,是在阻塞的时候冻结 F 级和 D 级,并且向 E 级传递 nop 指令(指令码全为 0)。

我们使用 AT 法判断是否需要阻塞。这部分教程讲的很详细。在实现的时候,可以在 D 级增加一个处理阻塞的模块,将各级指令产生数据的周期和需要使用数据的周期传入这个模块。模块内部使用寄存器记录处于 E 级和 W 级指令的 Tnew。在 D 级接收新的指令时,由这个模块判断新的指令与处于流水过程中的指令产生的数据冲突是否必须采用阻塞解决。若需要阻塞,则由这个模块产生阻塞信号(stall)。

关于 AT 法的更多细节可以参考教程。

跳转

因为我们是在 D 级译码,所以跳转指令的判断最早只能在 D 级。于是我们就在 D 级进行跳转指令的判断。

要注意,beq 指令要使用寄存器的数据,因此也需要对其进行转发和阻塞的判断。并且由于 beqTuse0 (在 D 级马上就要使用数据),因此理论上 beq 经常需要阻塞的情况。

在实现跳转时,我们需要在 D 级增加一个模块 NPC,其向 F 级提供跳转使能信号和跳转的目的地址。

延迟槽

当我们进行指令跳转时,由于是在 D 级做出的判断,按照单周期 CPU 的逻辑,处于 F 级中的指令(即跳转指令紧跟的下一条指令)理论上就需要作废。

所谓延迟槽,就是我们让这条按照单周期逻辑应该作废的指令不作废,从而提高指令的运行效率。

事实上,对于延迟槽我们不需要进行额外的操作。无为就可以了(乐)。

上机

P5 的上机难度暴增。许多同学都在 P5 献出了首挂。

上机前建议向流水线中增加额外的数据通路以供课上指令使用(在流水线寄存器中增加课上指令的通路)。

此外 P5 的课上数据点虽然也有课下的强测,但是分布不均,并且大多数测试点仍然是测试课上指令的,这一点与 P3P4 不同。

在课上要认真读题,确保自己读懂了题目再开始写代码。在增加指令的时候一定要仔细地考虑,将每一条指令相关的信号都进行相应的判断。如涉及跳转指令除了要修改 Controller 外,还要修改 NPC 中的跳转使能信号和跳转地址。

在课上不要过于拘泥于流水线的结构。有些课上指令十分阴间。比如要在 M 级才能得知写入寄存器的编号此类。

此外就是在课上之前要好好休息,休养精神。笔者因为当天过于疲惫,在课上睡着以至于献出了首挂。大家不要学我啊呜呜呜。

结语

P5 开始难度骤增。不过如果 P5 做的比较好,P6 就会简单很多。至于 P7,那就又是另一个故事了……

在这里特别感谢 yt 的帮助。没有 yt,就没有我的 p5 (哭)。