目录

第四章:单周期 CPU
4.1 现在可以公开的情报:一些前置知识
4.2 使用 Logisim 模拟单周期 CPU
4.3 使用 Verilog 模拟单周期 CPU
4.4 本章结语:再见,单周期 CPU !

第五章:五级流水线 CPU
5.1 现在可以公开的情报:一些新的前置知识
5.2 五级流水线 CPU 的基本构造
5.3 阻塞与转发
5.4 使用 Verilog 模拟五级流水线 CPU
5.5 五级流水线 CPU 实战:加指令练习
5.6 五级流水线 CPU 大升级:新指令与新模块
5.7 本章结语:再见,五级流水线 CPU ?

第四章:单周期 CPU

4.1 现在可以公开的情报:一些前置知识

终于!我们已经集齐了 Logisim 、Verilog 、MIPS 三大神器,准备开始我们漫长的 CPU 征途了!在我们正式开始之前,我们需要了解一些在上本书中没有学到的前置知识,也算是我们踏上征途前的最后准备吧!

【CPU】

不会有人已经快要动手搭 CPU 了,还不知道什么是 CPU 吧?(笑)没关系,我们赶紧了解一下,现在还来得及!

我们知道,我们编写的 C 语言程序要想能够运行,需要通过编译转换为汇编语言,再将汇编语言转换为机器码,这些流程 —— 都不是 CPU 的工作…… CPU 真正的工作是,将已经全部处理好的机器码作为输入,执行其中的汇编程序,达到修改寄存器和内存中的值的效果。这也正是我们要实现的内容。

事实上,我们一直在使用的 MARS 就是在模拟这个过程。我们在其中编写汇编程序,交给 MARS 来运行,就能够观察到寄存器和内存中发生的变化。在接下来的学习中,我们完全可以参照 MARS 执行指令的流程,从而能够更好地理解 CPU 的运行机制。当你在以后对 CPU 的运行逻辑产生困惑时,不妨回头看一下 MARS 是如何实现这个过程的,相信会对你有所帮助!

【空指令nop】

接下来,我们来认识一条特殊的指令 —— nopnop 指令的机器码是 0x00000000 ,一看就非常不简单。它的作用就是没有任何作用,如果我们查询每条指令的机器码,就会发现 nop 指令实际上等价于 sll $0, $0, 0 指令,确实没有任何作用。

那么你肯定会问,我们为什么需要一条没有任何作用的指令呢?我们先按下不表,到五级流水线 CPU 一章,你或许会再感受到它的存在的。

4.2 使用 Logisim 模拟单周期 CPU

接下来,我们就要正式开始搭建 CPU 了!我们本章的旅程主要分为两个部分:在本节,我们将要认识单周期 CPU 的全部架构,并使用 Logisim 完成一个属于自己的单周期 CPU ,实现 14 条 MIPS 指令:nop addu subu addiu xori lui lw sw beq bne j jal jr jalr ;在下节,我们将要将我们的单周期 CPU 移植到 Verilog 语言,实现两个版本的单周期 CPU。

在正式开始之前,还是要温馨提示一下,虽然本书已经给出了全部的实现细节,但还是希望你能够在看完每节,彻底了解了实现思路之后,挑战在不看本书的情况下自行完成 CPU ,毕竟你也不想查重被抓到小黑屋里接受审判吧只有这样才有自己独立完成一个大工程的成就感嘛!(笑)

【CPU中的存储模块:PC、IM、GRF、DM】

在上一节我们已经知道,CPU 要实现的目标就是正确修改寄存器和内存这些存储模块中的值,所以搞清楚 CPU 中有哪些存储模块,以及如何确保它们的值时刻正确,就是我们搭建 CPU 的第一要务!

于是,我们就要思考一下,CPU 在执行指令的过程中,都会用到哪些存储模块呢?当然了,从大范围上说,其实就是我们刚才提到的寄存器和内存两种。不过我们也可以将它们两个再细分一下,或许对我们接下来的搭建工作更有帮助。

提到寄存器,我们首先能够想到的肯定就是 $0$31 这 32 个寄存器了,我们将它们称为“寄存器堆”(GRF)。在执行绝大多数指令时,我们都需要从这些寄存器中取出值;同样,在很多情况下,我们也需要将计算的结果写入寄存器中。在后面的部分中,我们也将学习如何搭建这个庞大的 GRF 模块。

不过,在我们的 CPU 中,就只有这 32 个寄存器吗?让我们来仔细回忆一下,还记得有一个叫做 PC 的寄存器吗?它代表着当前正在执行指令的地址。在跳转指令的符号语言描述中,我们曾经用到过这个寄存器,在我们的 CPU 中,我们也会将其独立成一个 PC 模块,用于取指令的寻址。

当然,除了 GRF 和 PC 寄存器之外,我们还有一些独立的寄存器,例如 HI LO 寄存器和我们素未谋面的 CP0 寄存器,这些寄存器在我们后续添加相关模块的时候会讲到,在这里就暂且略过了~

接下来,我们再来研究一下内存这个大类。在 CPU 中,我们同样需要开辟一块存储空间来作为内存,这就是 DM 模块。

不过我们不要忘记了,在 MARS 中,内存实际上是被分为两块区域的:地址为 0x000000000x00003000.data 段作为自由存取的空间,而地址高于 0x00003000.text 段作为存储指令机器码的空间。

在我们的 CPU 中,为了便于搭建,我们将这两个部分拆成两个模块,我们将存储指令机器码的 .text 段从 DM 模块中分离出来,单独形成一个 IM 模块。这样两个部分的读写互不干扰,解决了很大一部分的冲突问题。

(顺带一提,我们的 CPU 中这种 IM 和 DM 分离的结构被称之为哈佛结构;相反,类似于 MARS 内存中不分离的结构被称之为冯·诺依曼结构或者普林斯顿结构

到目前为止,我们已经了解了我们这一阶段会用到的所有存储模块。在接下来的工作中,我们要做的就是通过一系列操作实现每一条指令,保证这些存储模块中的值正确无误即可!

【取指令:PC、NPC、IM模块】

如果我们想要让我们的 CPU 要执行一条汇编指令,第一步应该是是什么呢?当然是要正确取出这条指令了!于是,我们就需要搭建一个能够稳定输出当前执行指令的地址的装置 ——

首先,我们使用一个 32 位寄存器作为 PC 寄存器,表示当前指令的地址:

Picture Not Found!

我们知道,每当我们的 CPU 执行完一条指令,切换到下一条指令的时候,PC 寄存器的值都会随之更新到下一条指令的地址。也就是说,每当时钟到达上升沿的时候,我们就需要更新一次 PC 寄存器的值。

以最常见的非跳转指令为例,每当执行到下一条指令时,PC 寄存器的值都会 +4 ,还记得这个知识点吗?这是因为我们的 MIPS 指令都是 32 位,也就是 4 字节的,所以每两条指令之间的地址自然相差 4 。实际上,如果我们把 PC 寄存器的值看作状态机的状态,这就形成了一个非常经典的状态机:

Picture Not Found!

当然了,这个状态机肯定不止现在看上去这么简单。一方面,为了模仿 MARS 中指令在内存中的地址,我们的 CPU 中 PC 寄存器的初始值被要求设定为 0x00003000 ;另一方面,状态转移这一边也不可能只有 +4 一种情况,我们还要针对各种跳转添加不同的情况。看起来还是任重道远啊~

虽然我们确信已经找到了改进的思路,不过页面太小,我们好像实在有点画不下。于是,我们就可以考虑将电路拆成一个个模块,在模块内部实现其中的细节。例如,我们可以将 PC 寄存器整合为 PC 模块,将下方的状态转移部分整合为 NPC 模块,像下面这幅图片一样。你可能会觉得电路更加复杂了,是不是我在里面偷偷加了什么东西?实际上,这个电路和之前的是完全一致的:

Picture Not Found!

(这里为了方便观看,我把三个模块画到了同一张画布中,实际上它们应该在三张不同的画布中,相信聪明的你一定能理解的!)

上面这张图片中还是有很多细节的,以至于我需要单独拿出一大段文字,哪怕面临跑题的风险也要先解释一下:首先我们可以注意到,我在 main 模块中使用了 tunnel 元件连接 clkreset ,在之后的搭建中,我们的电路会越来越复杂,适当使用 tunnel 元件可以让我们的电路看起来更加清爽!

接下来是 main 模块中对 NPC 模块的使用,你可能已经发现了,这里的 NPC 模块的输入端 PC 在模块的右侧,而输出端 NPC 反而在模块的左侧。如果你问我为什么这样设计,当然是为了好看了(笑)!要想实现这个效果也并不困难,只需要在设置模块外观时,将输入端口和输出端口的位置调换就可以了。

最后我还想提醒你注意一下 PC 模块中的 clk 端口。我们可以发现这个接口并没有使用时钟元件作为输入端,而是使用了普通的 input 元件作为了输入端。这是因为它实际上只是模块中的一个接口,真正连接时钟的位置其实在主模块中。如果在 PC 模块中就使用时钟元件作为输入端的话,PC 模块在外观上就会比正常情况少一个 clk 接口,没有办法连接到主模块的时钟元件上了。

说了这么多,我们赶紧把话题拉回来,继续我们对 PC 和 NPC 这组状态机进行改造。首先我们可以为 PC 加上初始值。还记得我们在第一章讲过的双异或法吗?或许时间有些久远了,看看下面这幅图,不知道你能不能回忆起一些往事呢:

Picture Not Found!

接下来就是为 NPC 支持适配跳转指令了。在跳转指令中,绝对跳转指令没有任何跳转条件,实现起来也是非常简单。对于 jrjalr 指令,两条指令均是使用寄存器中 32 位的值作为跳转地址,也就是 NPC 模块的输出;而对于 jjal 两条指令,我们还需要将指令中的 26 位立即数左移 2 位,并与当前 PC 寄存器的值的高 4 位进行拼接,才能得到跳转的地址。

这里我们可以先将上面的 32 位寄存器值和 26 位立即数统统作为 NPC 模块的输入,在后续环节中我们再考虑如何为 NPC 模块准备这些数据。那么到目前为止,我们就将 NPC 模块改造成了这样:

Picture Not Found!

等等,下面这个 NPC_Sel 是哪里来的?定睛一看,原来是控制右侧的多路选择器 MUX 的选项,正是这个接口决定着下一条指令的地址是 PC + 4 ,还是 PC 与 26 位立即数 imm26 的复合,抑或是 GRF 中某个寄存器的值。在不久后我们搭建 Controller 模块时,就能够了解到它的来历了。不过我们现在还是争取来补全 NPC 的最后一块拼图 —— 相对跳转指令吧!

和绝对跳转指令不同的是,相对跳转指令是有条件的。当条件成立时执行跳转,下一条指令的地址变为 PC + 4 + sign_ext(imm16 << 2),当条件不成立时保持 PC + 4 。于是除了 NPC_Sel 这个选项,我们还需要额外引入一个 1 位的判断是否满足条件的接口,祖上传下来的规矩,喜欢把这个接口叫做 zero ,我也不知道为什么。

不过叫什么名字不要紧,最重要的是我接下来的一番话,想当年我就是因为没有参透这句话,差点在 P3 就翻了大车,也让 P3 成为了我唯一一个没有 AK 过的 P ,非常遗憾。这句话就是:当我们添加新的信号接口时,一定要注意新接口的值是否会对已有的指令产生影响! 例如在下面两种 zero 接口的实现中,哪一种实现方法中,zero 的值会对除相对跳转指令的其它指令也产生影响呢?

Picture Not Found!

Picture Not Found!

答案是下面一种,你答对了吗?我们观察两个电路,在上一幅电路图中,当 NPC_Sel 的值不为 3 ,也就是当前指令不是相对跳转指令时,zero 的值无论为多少,都与输出毫无关系;而在下一幅电路图中,我们会发现即使 NPC_Sel 的值为 1 或 2 ,即当前指令为绝对跳转指令时,也需要 zero 的值为 1 才能够正常跳转,否则依然会输出 PC + 4 !

这并不是说后者的实现是错误的,实际上,只要我们在根据指令类型为 zero 赋值时,记得将绝对跳转指令对应的 zero 值无条件赋为 1 ,即可正确实现电路。不过为了保险起见,也是为了我们在上机考试中加指令能够更加顺利,我们在之后的教程中将会使用前者的实现方法。

终于,我们完成了 PC 和 NPC 这组状态机,成功得到了当前指令的地址,在正确取出指令的路上迈出了一大步!接下来,我们来完成 IM 模块,将地址转换为 MIPS 指令的机器码。

我们知道,指令的存储需要用到一大片连续的地址空间,使用最高只有 32 位的寄存器堆可能并不是一个非常好的方法。好在我们还有两个量大管饱的高手能够解决这个问题,那就是 ROM 和 RAM ,在这里我们选择 ROM 作为存储指令机器码的载体。ROM 的特点是只能在程序运行前将内容一次性全部写入,而无法在程序运行过程中进行任何修改。在程序运行过程中,我们当然不希望程序遭到篡改,于是选择 ROM 就再合适不过了!

我们创建一个 IM 模块,在模块中放置一个 ROM 元件。接下来,我们就可以把汇编程序的机器码导入 ROM 中了。如果你忘记了怎么在 MARS 中将汇编程序的机器码导出为 txt 文件,以及如何在 Logisim 中将 txt 文件格式的机器码导入 ROM 中,那么快回头看一看上半本书的内容吧!我会等着你回来的!(笑)

我们观察可以发现,ROM 左侧有一个输入端 Address 端,右侧有一个输出端 Data 端。(下方的 Select 端我们暂时不需要用到)为了使 ROM 的 Data 端能够一次性输出 32 位的指令,我们在左下角的设置中,将 ROM 的 Data Bit Width 改为 32 。

Picture Not Found!

接下来我们来关注 ROM 左侧的 Address 端,Address 端输入的内容就是指令的地址吗?答案显然是否定的。最明显的一个问题,ROM 中指令机器码的首地址是 0 ,而不是 0x3000 ,所以我们在将地址输入 ROM 之前,需要事先减去一个 0x3000

然而这就可以了吗?我们试着给 ROM 的 Address 端赋不同的值作为输入,观察输出的结果:

Picture Not Found!

于是我们能够发现,相邻两条指令之间的输入值并不像指令地址一样相差 4 ,而是相差 1 。所以我们在 -3000 之后,还需要再除以 4 ,也就是右移 2 位。

结束了吗?似乎并没有。当我们满心欢喜以为做好了所有前期工作,准备设置 Address 端的位宽时,却发现我们最大只能设置 24 位的位宽!实际上,Logisim 中 ROM 的存储空间也是有上限的,所以我们只能委曲求全,牺牲一些地址空间,舍去地址中最高的几位了:

Picture Not Found!
(图中截取了地址的第 2 到 17 位,也就是保留了 16 位的 Address 位)

最后,我们把 IM 连接到电路上,就完成了我们 CPU 中取指令的全部内容!

Picture Not Found!

【控制器:Controller模块】

历经了千辛万苦,我们终于取出了要执行的指令。接下来,我们就要进入执行指令的阶段了。那么,我们该如何实现将如此多种指令的执行集成到一个 CPU 上呢?

首先我们肯定知道,每种指令的执行过程都是不同的(这不废话嘛)。即使我们将所有指令的所有共通之处都整合起来,也依然会出现许多差异,这就需要我们的 CPU 根据指令的种类进行分类讨论了。

于是我们就需要发明这样一个模块,它有两个功能:一是根据指令的机器码解析出指令的名称;二是根据解析出的名称为一些存在差异的接口赋值(例如 NPC 模块中的 NPC_Sel ,还记得吗)。那么恭喜你发明了 Controller 模块!

接下来,我们跟着上面的思路,分两步实现 Controller 。首先我们需要根据指令的机器码判断指令的名称。我们知道,对于绝大多数指令来说,通过最高 6 位的 OpCode 和最低 6 位的 Funct 都足以判断指令的类型了(当然像 bltz bgez 这种奇葩除外,不过我们目前可以不考虑这些)。于是我们从指令的机器码中分离出这两个部分,作为 Controller 的输入:

Picture Not Found!

在 Controller 中,我们将 OpCode 和 Funct 与各种指令的值进行比较。需要注意的是,只有当 OpCode 为 0 时,才允许继续比较 Funct ,否则可能会出现某种 I 型或 J 型指令的最低 6 位恰好和某指令的 Funct 相等的情况,导致我们的 CPU 将一条指令同时判定为两种类型的奇葩情况:

Picture Not Found!
nop 指令不需要单独判断,我们将 nop 指令与其它所有的未知指令并入一起,当所有指令的判断全部为 0 时即为这种情况)

我们不难发现,在这种结构下,上面的所有 tunnel 中最多只有 1 个值为 1 ,其余一定都为 0(当然也可能会出现全部为 0 的情况,比如 nop 指令),所以我们可以说这就是一种独热码。

接下来,我们要对每种指令规定每一个接口的值,也就是将这些 1 位的独热码转换为对应接口的输出。例如当指令为 addiu 时,NPC_Sel 接口的值应该等于多少;当指令为 beq 时,NPC_Sel 接口的值又应该等于多少。

或许你已经想到了一种非常简单的方法,那就是使用 Logisim 中自带的功能,将真值表一键转换为电路图。但是实际上我并不推荐这种做法,因为 Logisim 只能保证生成电路的正确性,却没有办法保证它的清晰度和可扩展性。当你紧张地坐在上机考试的考场上,面对着一坨乱七八糟的与或非门,如同不可名状之物缠绕在一起的时候,相信我,你的 SAN 值直接就掉光了!

相反地,我将提出一个更加工整简洁的方法!如果我们想根据不同情况对接口赋值,我们最先想到的一定是使用 MUX 元件。不过,MUX 元件的选择端要求我们输入数字序号,而不是一个一个的独热码,有没有什么元件能够快速解决这个问题呢?

或许你可以试试被尘封许久的优先编码器 Priority Encoder !上半本书中我好像还说过它的坏话,不过现在看来它还是有点用处的。对于独热码来说,独热码最多只有 1 个输入端的值为 1 ,那么优先编码器就会直接输出这个 1 所在的位置,也就实现了将独热码转换为顺序编码!

不过我们还需要注意一个细节,那就是还有可能会出现所有输入端均为 0 的情况,此时优先编码器的输出就会全变成 X 。为了解决这种情况,我们可以使用输出下方另外的一个 1 位输出进行辅助,当输入端全为 0 时输出为 0 ,否则输出为 1 ,搭配 MUX 元件进行选择,就可以实现对 nop 指令以及其它未实现指令的判断了:

Picture Not Found!

将独热码转换为数字之后,我们就可以将其接入 MUX 元件,根据指令在优先编码器左侧的顺序,为每条指令编写接口的值了。这种做法不仅支持无限堆叠接口,在上机时也能够更加轻松地完成加指令的任务:

Picture Not Found!

最后,我们修改 Controller 模块的外观,将输出端的 NPC_Sel 接口与 NPC 模块上的对应接口连接起来就可以了:

Picture Not Found!

Controller 模块作为控制每条指令行为的核心,在接下来加指令的过程中会不断添加接口,我们可以在这里设一个存档点,记得常回家看看~

【R型计算类指令与GRF、ALU模块】

接下来,我们开始为我们的 CPU 加入各种指令,我们从 addusubu 这两条 R 型计算类指令开始。我们分析这两条指令的执行流程,大致需要进行这样的几步操作:

  1. 取出 rs 和 rt 两个寄存器中的值 GPR[rs]GPR[rt]
  2. GPR[rs]GPR[rt] 进行运算;
  3. 将运算结果写入 rd 寄存器中。

其中,第 1 步和第 3 步都需要与寄存器堆 GRF 进行交互。我们来观察交互的需求:第 1 步要求我们同时向 GRF 输入两个 5 位的寄存器编号,并要求寄存器同时输出对应的两个寄存器中 32 位的值。我们将两个输入端记为 A1A2 ,将两个输出端记为 RD1RD2 ;第 3 步要求我们向 GRF 输入一个 5 位的寄存器编号,同时输入一个 32 位的值,要求 GRF 将这个值写入对应的寄存器中。于是我们将这两个输入端分别记为 A3WD

除了上面提到的 4 个输入端口和 2 个输出端口之外,GRF 还应该再设置一些端口,比如控制其中所有寄存器的 clkreset 端口,另外还应该有一个写使能 WE 端口。这个写使能端口的作用是什么呢?这就很考验我们的全局意识了!众所周知,虽然 addu subu 这些指令有向寄存器赋值的环节,然而并不是所有指令都是这样,例如 beq j 等指令,可以说是比比皆是。为了在执行后者这些指令时不把一些奇怪的值写到一些奇怪的寄存器中去,我们就需要为 GRF 加上 WE 这样一个写使能,当执行这些指令时置 0 ,这样就很好地解决了这个问题!

接下来我们来着手搭建一下 GRF 模块。可以预见的是,32 个寄存器必然会导致我们的电路图来到一个极其凶残的超大规模,不过其实都是一些重复性较强的简单工作,只不过是力气和时间的问题。为了让大家能够看清楚,接下来的样例图片中以 4 个寄存器作为例子,大家在搭建的时候自行扩展为 32 个寄存器即可~

首先,我们为我们的小寄存器堆添加时钟信号 clk 、复位信号 reset 和写使能 WE

Picture Not Found!

接下来,我们为小 GRF 加上 A1 A2 两个输入端和 RD1 RD2 两个输出端,使用 MUX 元件就可以轻松完成这个任务:

Picture Not Found!

这个走线实在是说不上优雅……算了无所谓了,我们继续为小 GRF 加上 A3WD 两个输入端,这一次我们使用许久不见的 DMX 元件:

Picture Not Found!

这里有一些小细节,所以我们在这里停顿。最明显的应该就是那根断掉的导线了,这可不是图片加载错误,是因为 0 号寄存器是不能被赋值的,所以我故意掐断了这根导线(笑)。

接下来还有一个细节,在图片中完全体现不出来,但是如果你是我的忠实读者,应该能想起一个非常非常遥远的 callback ,那就是要将 DMX 元件的 Three-state? 属性改为 Yes 。这个知识点在第一章我们初次遇见 DMX 元件的时候就强调过,这里再次提醒一下:DMX 元件在默认情况下,除了 A3 对应端口的输出值为 WD 的值之外,其余所有端口的输出值都为 0 。也就是说,每当时钟上升沿到来时,除了 A3 对应的寄存器会被赋值为 WD 以外,其余所有寄存器都会被赋值为 0 !这显然是错误的,好在使用上述解决方法进行操作之后,就可以正常模拟只为一个寄存器赋值的行为了。

这样,我们就完成了 GRF 模块,现在我们稍作修改一下它的外观,将它添加至主模块中:

Picture Not Found!

这里我们将指令的拆分进一步细化,按照 R 型指令分为了 OpCode 、rs 、rt 、rd 、shamt 和 Funct 六个部分;同时在 Controller 中加入了 GRF 写使能端的对应接口 RegWrite ,如下图所示。其中 0 表示指令执行过程中不会写寄存器,1 表示指令执行过程中会写寄存器:

Picture Not Found!

到目前为止,我们已经几乎完成了 R 型计算类指令三个步骤中的第 1 步和第 3 步,接下来我们来完成第 2 步,也就是对 GRF 输出的 RD1RD2 进行运算,再将结果输入回 WD 中。

不过我们知道,R 型计算类指令的种数相当之多(虽然我们目前并不会实现这么多),直接在主模块中计算的话,电路图就会变得非常杂乱,不如我们再创建一个模块,专门进行这项运算的工作,于是我们又发明了 ALU 模块!

ALU 模块的任务很简单,输入两个值 inputAinputB ,根据指令的类型进行不同的运算,输出一个结果 outputA 。说起来恐怕也就只有一个问题,我们应该如何或许指令的类型呢?当然还要得益于神通广大的 Controller 了!当然了,如果将十几个独热码全部作为 Controller 的输出端口,那可能有点太复杂了,不过我们早已经使用优先编码器把这些独热码转换为了数字,只需要让 Controller 直接输出这些数字,再由 ALU 作为输入就可以了:

Picture Not Found!

有了 type ,我们在编写 ALU 的路上就畅通无阻了,只需要注意将指令种类和代表它们的数字严格对应上就可以了:

Picture Not Found!
(这里当 MUX 的选项为 0 时,因为在 0 号接口没有输入值,所以输出为全 X 。因为 0 号接口对应的指令是 nop 或其它未实现指令,并不会用到 outputA 这个输出值,后续也会有其它指令同样不需要用到这个输出值,例如 beq 等指令,此时输出为全 X 均是正常现象)

最后,我们修改 ALU 模块的外观,放置在主模块中,连好相应的接口。自此,我们的 CPU 终于能够运行指令了!(虽然算上 nop 也只支持区区 3 条指令,不过万事开头难,还是可喜可贺,可喜可贺!)

Picture Not Found!

【I型计算类指令与EXT模块】

刚刚我们稍微体会了一下旗开得胜的喜悦,让我们继续吧!接下来要添加的 3 条新指令是 addiu xori 和马里奥的兄弟 lui

我们首先来思考一下这些 I 型计算类指令与刚才实现的 R 型计算类指令有什么区别。一个区别就是 ALU 的输入端 outputAoutputB 不再是源自 GRF 输出的 RD1RD2 ,而是 RD1 和一个立即数;另外一个区别则要从指令的结构上看出来了,那就是 I 型指令中要被赋值的寄存器,也就是连接 GRF 模块 A3 接口的寄存器,不再是 rd 寄存器,而是 rt 寄存器。

接下来我们来适配一下这两个功能,首先是支持 16 位立即数参与 ALU 计算。看起来只需要将指令的最低 16 位取出来,和 RD2 一起接入 inputB ,再加一个 MUX 元件和 Controller 接口选择即可。不过在此之前我们还是应该注意一下位宽问题,16 位的立即数终究还是和 32 位的 inputB 接口走不到一起,所以我们需要先将 16 位立即数扩展至 32 位。

那这里又有一些说法了,敏锐的你肯定想到了,既然是位扩展,肯定就会有符号扩展和无符号扩展之分。非常巧合的是,我们要新加的 3 个指令里,addiu 指令是符号扩展,xori 指令是无符号扩展,而 lui 指令则是两种扩展方式都可以!(当然是刻意的游戏设计,笑)

针对两种不同的扩展方式,我们可以将其整合为一个新的 EXT 模块,采用新的 Controller 接口进行选择:

Picture Not Found!
(其实我觉得这个 EXT 模块多少有点鸡肋了,不单独设置一个模块也没什么问题)

接下来,我们依旧是在 Controller 中增加接口 EXTop

Picture Not Found!

将 EXT 模块放入主模块中:

Picture Not Found!

接下来,为了节省版面,我们一次性完成两个 Controller 接口的添加:一个是控制 ALU 模块的 inputB 端口是接入 GRF 模块的 RD2 端口,还是接入 EXT 模块的 ext32 端口的 ALUsrc ;另外一个是控制 GRF 的 A3 端口是接入指令的 rt 部分,还是 rd 部分的 RegDst 。我们依旧是现在 Controller 模块中进行添加:

Picture Not Found!

我们在主模块中加入对应的选择:

Picture Not Found!

最后,不要忘了更新 ALU 模块中的计算部分。这里 addu 指令和 addiu 指令的运算是相同的,我们可以在右侧复用这个运算:

Picture Not Found!

这样我们就完成了 I 型计算类指令的追加!在后续的上机考试中,我们也会遇到各种有趣的加指令谜题。虽然谜题千奇百怪,不过只要按照我们刚才加指令的思路去思考,其实也没有看上去那么恐怖嘛!

【内存访存指令与DM模块】

接下来,我们来解决内存访存指令隔壁 lwsw 。不过在此之前,我们需要设计一下内存模块 DM 。

和 IM 模块不同的是,DM 模块不仅要能够读出内存中的内容,还要支持向内存中写入。所以我们采用可读可写的 RAM 元件作为 DM 的主体。仿照 IM 模块中的 ROM 元件,我们将 DM 中的 RAM 元件的地址位宽 Address Bit Width 设置为 16 位,数据位宽 Data Bit Width 设置为 32 位。此外,我们还要按照惯例,将数据接口 Data Interface 设置为 Separate load and store ports :

Picture Not Found!

首先,我们模仿 IM 模块,完成 DM 模块的读内存功能。因为在对内存进行访存的时候,我们的首地址不是 0x3000 而是 0x0000 ,所以自然也不需要在 DM 模块中将地址 -3000 ,而是直接右移两位就可以了:

Picture Not Found!

当然,只有读取作用的 DM 模块实在是太逊了,我们还要为它加上写入的功能,这就需要我们加上一堆接口,例如要写入的数据 WD ,以及时钟接口 clk 、复位接口 reset 。当然了,我们不能忘了写使能接口 WE ,毕竟不是所有的指令都有写内存的需求,在绝大多数指令的情况下,我们需要保证写使能是关闭的,防止将一些不正确的数据不小心漏进内存中:

Picture Not Found!

接下来,我们还是为 DM 模块设置好外观,加入主模块中:

Picture Not Found!

接下来我们考虑一下 DM 模块中 Data WD WE 接口该如何连接。

首先是 Data 接口。我们从内存中取出数据,是为了将其写入寄存器中,所以我们将 Data 接口与 GRF 模块的 WD 接口连接在一起。不过这个接口貌似已经有线路了,所以我们需要加入一个新的选择信号 MemtoReg 进行选择。

接下来是 WD 接口。我们将要输入内存中的数据来自哪里呢?在目前我们将要实现指令里,只有 sw 指令需要写内存,而它要写入的数据则来自 rt 寄存器中的内容。于是我们从 GRF 模块中的 RD2 端口取出 rt 寄存器中的数据之后,不仅需要将其传入 ALU 模块,还需要同时接入 DM 模块的 WD 接口。

最后是 WE 接口,它的值当然就要来自 Controller 模块了!于是,我们可以画出新的主模块电路图:

Picture Not Found!

接下来的操作也不必多说了,想必聪明的你已经轻车熟路了!那就是在 Controller 模块中增加接口:

Picture Not Found!

以及在 ALU 模块中增加相应的计算:

Picture Not Found!

这样,我们就完成了内存访存指令 lwsw

Picture Not Found!

到目前为止,我们已经完成了单周期 CPU 中的所有模块。在接下来加指令的练习中,我们只需要复刻一下之前已经操作过很多次的流程即可。我们可以简单复习一下,大概就是:分析指令的数据通路 -> 增加新的数据通路和选择信号 -> 在 Controller 模块中修改选择信号 -> 在 ALU 模块中加入新的计算。这样,我们就完成了加指令的全部流程!

【相对跳转指令】

刚才我们简单梳理了一下加指令的全部流程,现在我们就用跳转指令来练练手吧!

在相对跳转指令这一部分,我们要加入的是 beqbne 两条指令。我们首先来分析一下这两条指令的数据通路,我们首先需要将 rs 和 rt 输入 GRF 模块,取出 GPR[rs]GPR[rt] 两个值,传入 ALU 模块中。

这一步看起来和我们之前实现过的指令没什么区别,真正的区别在 ALU 模块中。我们并不需要 ALU 模块输出计算后的值,而是输出二者是否相等,也就是决定是否进行跳转的 zero 值,我们将其连接到 NPC 模块的 zero 接口上。配合设定好的 NPC_Sel 值,以及传入的 imm16 ,NPC 模块就可以计算出下一个周期要执行的指令的地址,这样我们就完成了这两条指令的执行。

理清思路后,我们就可以开始着手修改 Controller 模块和 ALU 模块,以及主模块中的数据通路了!

正常加指令的情况下,我们需要在 Controller 模块中加入对新指令 OpCode 和 Funct 的解析,以及为新指令适配已经加入的选择信号。不过这些工作我们在之前已经顺带做过了,而且这两条指令也没有加入新的选择信号,这里就略过修改 Controller 模块这个环节了。

接下来,我们修改 ALU 模块,为其增加新的 zero 接口:

Picture Not Found!

最后,我们在主模块中连接 zero 接口的两端,并向 NPC 模块传入 imm16 ,就完成了 beqbne 两条指令:

Picture Not Found!

就这么简单?就这么简单!有没有稍微感觉到,其实 CPU 并没有我们想象中的那么难?(笑)

【绝对跳转指令】

接下来,我们再挑战一下适配四条绝对跳转指令 j jr jal jalr 。相信刚刚速通了相对跳转指令的你现在一定是满怀信心,跃跃欲试了吧!

我们先速通一下 j 指令和 jr 指令。这两条指令不需要读写寄存器,不需要计算,不需要读写内存,真的是什么都不需要!我们已经保证了 Controller 模块能够解析这两条指令,NPC_Sel 接口额输出的值正确,GRF 模块和 DM 模块的写使能关闭,现在只需要将 NPC 的 imm26GRF 两个接口连好,就直接实现了这两条指令:

Picture Not Found!

接下来我们来重点关注一下 jaljalr 这两条指令。我们首先来看 jal 指令,它其实就是在 j 指令的基础上增加了“将 PC + 4 写入 31 号寄存器中”这条规则。然而我们仔细分析一下就会发现,无论是将 PC + 4 输入 GRF 模块的 WD 接口,还是将 31 输入 GRF 模块的 A3 接口,都需要我们新开一个选择信号才能实现。不过,我相信以我们现在实力,拿下 jal 这个麻烦鬼也依然是游刃有余!

于是,我们在 Controller 模块中编写接口,PCtoReg 接口负责选择 PC + 4 传入 GRF 模块的 WD 接口中;而 Regra 接口负责选择 31 号寄存器作为输入数据的寄存器:

Picture Not Found!

最后,我们在主模块中连接好全部的选择信号,于是我们就完成了 jal 这条指令:

Picture Not Found!
(为了能在 133% 的画面比例下画出 CPU ,真的是把画面运用到极限了(晕倒)我确信有一种优美的画法,但是画面太小,我画不下)

至于 jalr 指令,它是在 jr 指令的基础上加上了“将 PC + 4 写入 rd 寄存器中”这条规则。我们只需在 Controller 模块中设置好相应的选择信号即可(见上上图)。

至此,我们就完成了一个支持 14 种 MIPS 指令,并可轻松拓展到几乎所有常见 MIPS 指令的 CPU !

CPU 搭完真的很兴奋!我猜你大概率是人生中第一次完成这么复杂的数字电路,真的要好好鼓励一下自己,晚上吃点好的吧!

4.3 使用 Verilog 模拟单周期 CPU

接下来,我们试着把我们刚才设计的 CPU 移植到 Verilog 硬件语言上。在第三章我们就已经了解到了一个观点,那就是 Verilog 硬件语言实际上就是在描述电路。所以我们需要做的,其实只不过是将电路图“翻译”成电路,也是非常简单了!事不宜迟,我们趁热打铁,马上开始吧!

我们对 CPU 的移植,从各个模块开始,最终再将所有模块整合成到主模块中。如果你现在还不是特别清楚 ISE 中工程(.xise 文件)与模块(.v 文件)的区别,那么你可能需要稍作停顿,先到上半本书中复习一下,我在这里等着你~

另外,在 Verilog 版本的 CPU 中,课程组为了简便大家的工作量,对复位问题作出了一些细小的改动,我们也随着课程组的脚步一起来改动一下:首先是复位方式从异步复位改为同步复位,前者在 Logisim 中比较容易实现,而后者在 Verilog 中则更加简单,同时也降低了整个电路的复杂度。此外,课程组还保证了在开始执行指令之前,均会将 reset 接口置 1 一次,于是我们的寄存器就不需要使用 initial 块来进行初始化,而只需要在 always 块中写清楚如何重置就可以了。

首先我们从 PC 模块开始翻译。观察下面的图片,我们要实现的接口有:输入接口 input clk input reset input [31:0] NPC ,输出接口有 output [31:0] PC 。另外,我们还需要实现一个寄存器 reg [31:0] regPC ,初始值为 0x00003000 ,在每个时钟上升沿接受 NPC 作为输入,并输出 PC

Picture Not Found!

根据上面的条件,身经百炼的你,一定可以非常容易写出相应的对代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module PC(
input clk,
input reset,
input [31:0] NPC,
output [31:0] PC
);

reg [31:0] regPC;

always@(posedge clk) begin // 改用同步复位
if (reset) begin // 重置方法
regPC <= 32'h00003000;
end
else begin
regPC <= NPC;
end
end

assign PC = regPC;

endmodule

(这里使用了更适合 Verilog 的为寄存器赋初始值的方法,算是一种信达雅的翻译吧(笑),当然你也可以将 Logisim 中赋初始值的方法直译过来,完全没有问题!)

接下来是 NPC 模块,NPC 模块中没有寄存器,完全是组合逻辑,实在是太爽了:

Picture Not Found!

在组合逻辑的 assign 语句中,我们用三目运算符来代替 MUX 模块,写起来非常方便。在编写代码的过程中,我们可能会遇到很多自行定义的魔数(例如 NPC_Sel 接口的值为 0 、1 、2 、3 时分别表示哪种情况),为了使我们在上机时紧张的环境下看得更加清晰,我们可以采用宏定义或者 parameter 型变量的方法来为魔数取名:

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
module NPC(
input [31:0] PC,
input [15:0] imm16,
input [25:0] imm26,
input [31:0] GRF,
input [1:0] NPC_Sel,
input zero,
output [31:0] NPC
);

parameter PCPLUS4 = 2'b00,
IMM26 = 2'b01,
GRFconst = 2'b10,
IMM16 = 2'b11;

wire [31:0] sign_ext;
wire [31:0] NPC_PCplus4;
wire [31:0] NPC_imm16;
wire [31:0] NPC_imm26;
wire [31:0] NPC_GRF;

assign sign_ext = {{14{imm16[15]}}, imm16, 2'b00};
assign NPC_PCplus4 = PC + 32'h00000004;
assign NPC_imm16 = PC + 32'h00000004 + sign_ext;
assign NPC_imm26 = {PC[31:28], imm26, 2'b00};
assign NPC_GRF = GRF;

assign NPC = (NPC_Sel == PCPLUS4) ? NPC_PCplus4 :
(NPC_Sel == IMM26) ? NPC_imm26 :
(NPC_Sel == GRFconst) ? NPC_GRF :
(NPC_Sel == IMM16 && zero) ? NPC_imm16 :
NPC_PCplus4; // NPC_Sel == IMM16 && !zero

endmodule

实现了 PC 和 NPC 两个模块,我们就可以使用 IM 模块取出当前指令了:

Picture Not Found!

不过 Verilog 中并没有提供给我们类似于 ROM 和 RAM 这样的元件,但是好在我们可以通过定义寄存器数组 reg [31:0] ROM [0:4095] 的方法来实现指令的存储(不要在意 4096 这个大小,足够大即可,防止把内存撑爆)。其中 ROM 中的每个 32 位寄存器都存放着一条指令,在读取指令时,我们只需要提供数组的下标,即可取出对应位置的整条指令,其实和 Logisim 中地址转换的逻辑是相同的。

另外,我们还需要通过一些手段,将写有机器码的 txt 文件装载到 ROM 中,这时候我们就可以用到上半本书里提到过的 $readmemh("code.txt", ROM, 0, 4095); 语句了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module IM(
input [31:0] addr,
output [31:0] data
);

reg [31:0] ROM [0:4095];

wire [31:0] ROM_addr;

initial begin
$readmemh("code.txt", ROM, 0, 4095);
end

assign ROM_addr = ((addr - 32'h00003000) >> 2);

// 这里因为地址位数问题会出现强制位数转换,不过问题不大
assign data = ROM[ROM_addr];

endmodule

然后是巨大的寄存器堆 GRF 模块(大图一个屏幕放不下,还是放 4 个寄存器的小图):

Picture Not Found!

我们依然可以通过定义寄存器数组的方式解决,妈妈再也不用担心我的 GRF 太大画不下了!这里在同步复位的时候用到了最基础的 for 循环,正好我们也来回忆一下 for 循环的写法:

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
module GRF(
input clk,
input reset,
input WE,
input [4:0] A1,
input [4:0] A2,
input [4:0] A3,
input [31:0] WD,
output [31:0] RD1,
output [31:0] RD2
);

reg [31:0] GRF [0:31];

integer i;

always@(posedge clk) begin
if (reset) begin
for (i = 0; i < 32; i = i + 1) begin
GRF[i] <= 32'h00000000;
end
end
else begin
if (WE) begin
if (A3 != 5'b00000) begin // 注意这个细节
GRF[A3] <= WD;
end
end
end
end

assign RD1 = GRF[A1];
assign RD2 = GRF[A2];

endmodule

接下来是短小精悍的 EXT 模块,我们向其输入一个 16 位的立即数,以及 1 位的扩展方式,EXT 模块输出一个扩展后的 32 位数:

Picture Not Found!

这个模块一定远比你之前做过的 Verilog 练习题简单,所以就不多说了,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module EXT(
input [15:0] imm16,
input EXTop,
output [31:0] ext32
);

wire [31:0] zero_ext;
wire [31:0] sign_ext;

assign zero_ext = {16'h0000, imm16};
assign sign_ext = {{16{imm16[15]}}, imm16};

assign ext32 = (EXTop) ? sign_ext : zero_ext;

endmodule

接下来是 CPU 的心脏(我自封的)—— ALU 模块:

Picture Not Found!

依旧是组合逻辑,道理很简单,不过由于三目运算符串的结尾必须要有一个类似于 else 或者 default 的内容,所以并不支持直接不定义不会用到的选项,此时我们可以将输出设置为一些特殊值,来表示 CPU 可能出现了 bug ,这里选用的是全 0 :

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
module ALU(
input [31:0] inputA,
input [31:0] inputB,
input [5:0] type,
output [31:0] outputA,
output zero
);

parameter ADDU = 6'b000000,
SUBU = 6'b000001,
ADDIU = 6'b000010,
XORI = 6'b000011,
LUI = 6'b000100,
LW = 6'b000101,
SW = 6'b000110,
BEQ = 6'b000111,
BNE = 6'b001000,
J = 6'b001001,
JAL = 6'b001010,
JR = 6'b001011,
JALR = 6'b001100;

wire [31:0] AaddB;
wire [31:0] AsubB;
wire [31:0] AxorB;
wire [31:0] Bleftshift16;
wire [31:0] none;

assign AaddB = (inputA + inputB);
assign AsubB = (inputA - inputB);
assign AxorB = (inputA ^ inputB);
assign Bleftshift16 = (inputB << 16);
assign none = 32'h00000000;

assign outputA = (type == ADDU) ? AaddB :
(type == SUBU) ? AsubB :
(type == ADDIU) ? AaddB :
(type == XORI) ? AxorB :
(type == LUI) ? Bleftshift16 :
(type == LW) ? AaddB :
(type == SW) ? AaddB : none;

assign zero = ((type == BEQ) && (inputA == inputB)) ? 1'b1 :
((type == BNE) && (inputA != inputB)) ? 1'b1 : 1'b0;

endmodule

再之后是 DM 模块:

Picture Not Found!

我们依旧采用寄存器数组的方式实现 RAM(当然,这里的数组开得也比较小),只需要做一个最简单的寄存器赋值运算即可:

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
module DM(
input clk,
input reset,
input WE,
input [31:0] addr,
input [31:0] WD,
output [31:0] data
);

reg [31:0] RAM [0:3071];

wire [31:0] RAM_addr;

integer i;

assign RAM_addr = (addr >> 2);

always@(posedge clk) begin
if (reset) begin
for (i = 0; i < 3072; i = i + 1) begin
RAM[i] = 32'h00000000;
end
end
else begin
if (WE) begin
RAM[RAM_addr] <= WD;
end
end
end

// 这里依然会出现强制位数转换的问题,不过当然也是问题不大
assign data = RAM[RAM_addr];

endmodule

最后就是 CPU 的大脑(依旧是我自封的)—— Controller 模块:

Picture Not Found!
Picture Not Found!

毕竟是组合逻辑,其实难也难不到哪去,不过复杂度上还是有的,写代码的时候还是要格外细心:

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
77
78
79
80
81
82
83
84
85
86
module Controller(
input [5:0] OpCode,
input [5:0] Funct,
output [1:0] NPC_Sel,
output RegWrite,
output EXTop,
output ALUsrc,
output RegDst,
output MemWrite,
output MemtoReg,
output PCtoReg,
output Regra,
output [5:0] type
);

// 这里的定义一定要和 ALU 中的定义保持一致!
parameter ADDU = 6'b000000,
SUBU = 6'b000001,
ADDIU = 6'b000010,
XORI = 6'b000011,
LUI = 6'b000100,
LW = 6'b000101,
SW = 6'b000110,
BEQ = 6'b000111,
BNE = 6'b001000,
J = 6'b001001,
JAL = 6'b001010,
JR = 6'b001011,
JALR = 6'b001100;

wire addu;
wire subu;
wire addiu;
wire xori;
wire lui;
wire lw;
wire sw;
wire beq;
wire bne;
wire j;
wire jal;
wire jr;
wire jalr;

assign addu = (OpCode == 6'b000000 && Funct == 6'b100001) ? 1'b1 : 1'b0;
assign subu = (OpCode == 6'b000000 && Funct == 6'b100011) ? 1'b1 : 1'b0;
assign addiu = (OpCode == 6'b001001) ? 1'b1 : 1'b0;
assign xori = (OpCode == 6'b001110) ? 1'b1 : 1'b0;
assign lui = (OpCode == 6'b001111) ? 1'b1 : 1'b0;
assign lw = (OpCode == 6'b100011) ? 1'b1 : 1'b0;
assign sw = (OpCode == 6'b101011) ? 1'b1 : 1'b0;
assign beq = (OpCode == 6'b000100) ? 1'b1 : 1'b0;
assign bne = (OpCode == 6'b000101) ? 1'b1 : 1'b0;
assign j = (OpCode == 6'b000010) ? 1'b1 : 1'b0;
assign jal = (OpCode == 6'b000011) ? 1'b1 : 1'b0;
assign jr = (OpCode == 6'b000000 && Funct == 6'b001000) ? 1'b1 : 1'b0;
assign jalr = (OpCode == 6'b000000 && Funct == 6'b001001) ? 1'b1 : 1'b0;

// 这里 NPC_Sel 的值一定要和 NPC 中的定义保持一致!
assign NPC_Sel = (j | jal) ? 2'b01 :
(jr | jalr) ? 2'b10 :
(beq | bne) ? 2'b11 : 2'b00;
assign RegWrite = (addu | subu | addiu | xori | lui | lw | jal | jalr) ? 1'b1 : 1'b0;
assign EXTop = (addiu | lw | sw) ? 1'b1 : 1'b0;
assign ALUsrc = (addiu | xori | lui | lw | sw) ? 1'b1 : 1'b0;
assign RegDst = (addu | subu | jalr) ? 1'b1 : 1'b0;
assign MemWrite = (sw) ? 1'b1 : 1'b0;
assign MemtoReg = (lw) ? 1'b1 : 1'b0;
assign PCtoReg = (jal | jalr) ? 1'b1 : 1'b0;
assign Regra = (jal) ? 1'b1 : 1'b0;
// 这里将全 1 当作错误或未实现指令的输出(包括 nop 指令)
assign type = (addu) ? ADDU :
(subu) ? SUBU :
(addiu) ? ADDIU :
(xori) ? XORI :
(lui) ? LUI :
(lw) ? LW :
(sw) ? SW :
(beq) ? BEQ :
(bne) ? BNE :
(j) ? J :
(jal) ? JAL :
(jr) ? JR :
(jalr) ? JALR : 6'b111111;

endmodule

完成了所有的模块之后,我们就需要将所有模块连接在一起,在主模块上完成我们我们最终的成果:

Picture Not Found!

主模块的线路错综复杂,稍有不慎连错一根就可能导致整个 CPU 直接抽疯。而且 Verilog 也不会像 Logisim 一样直观,接错线并不一定会有特别明显的报错,所以我们还是要拿出 120% 的细心,像拆炸弹一样谨小慎微地连接好我们的电路:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
module mips(
input clk,
input reset
);

// PC and NPC
wire [31:0] pc;
wire [31:0] npc;
wire [31:0] pcplus4;

// IM
wire [31:0] Instr;
wire [5:0] OpCode;
wire [4:0] rs;
wire [4:0] rt;
wire [4:0] rd;
wire [4:0] shamt;
wire [5:0] Funct;
wire [15:0] imm16;
wire [25:0] imm26;

// Controller
wire [1:0] NPC_Sel;
wire RegWrite;
wire EXTop;
wire ALUsrc;
wire RegDst;
wire MemWrite;
wire MemtoReg;
wire PCtoReg;
wire Regra;
wire [5:0] type;

// GRF
wire [4:0] A1;
wire [4:0] A2;
wire [4:0] A3;
wire [31:0] WD_Reg;
wire [31:0] RD1;
wire [31:0] RD2;

// EXT
wire [31:0] ext32;

// ALU
wire [31:0] inputA;
wire [31:0] inputB;
wire [31:0] outputA;
wire zero;

// DM
wire [31:0] addr;
wire [31:0] WD_Mem;
wire [31:0] data;

PC PC (.clk(clk), .reset(reset), .NPC(npc), .PC(pc));
NPC NPC (.PC(pc), .NPC_Sel(NPC_Sel), .zero(zero), .imm16(imm16),
.imm26(imm26), .GRF(inputA), .NPC(npc));
IM IM (.addr(pc), .data(Instr));
Controller Controller (.OpCode(OpCode), .Funct(Funct), .RegDst(RegDst),
.ALUsrc(ALUsrc), .MemtoReg(MemtoReg), .RegWrite(RegWrite),
.MemWrite(MemWrite), .NPC_Sel(NPC_Sel), .EXTop(EXTop),
.PCtoReg(PCtoReg), .Regra(Regra), .type(type));
GRF GRF (.clk(clk), .reset(reset), .WE(RegWrite), .A1(A1), .A2(A2),
.A3(A3), .WD(WD_Reg), .RD1(RD1), .RD2(RD2));
EXT EXT (.imm16(imm16), .EXTop(EXTop), .ext32(ext32));
ALU ALU (.inputA(inputA), .inputB(inputB), .type(type), .outputA(outputA),
.zero(zero));
DM DM (.clk(clk), .reset(reset), .WE(MemWrite), .addr(addr), .WD(WD_Mem),
.data(data));

assign pcplus4 = pc + 32'h00000004;

assign OpCode = Instr[31:26];
assign rs = Instr[25:21];
assign rt = Instr[20:16];
assign rd = Instr[15:11];
assign shamt = Instr[10:6];
assign Funct = Instr[5:0];
assign imm16 = Instr[15:0];
assign imm26 = Instr[25:0];

assign A1 = rs;
assign A2 = rt;
assign A3 = (Regra) ? 5'b11111 :
(RegDst) ? rd : rt;
assign WD_Reg = (PCtoReg) ? pcplus4 :
(MemtoReg) ? data : outputA;

assign inputA = RD1;
assign inputB = (ALUsrc) ? ext32 : RD2;

assign addr = outputA;
assign WD_Mem = RD2;

endmodule

至此,我们就将我们的 CPU 成功移植到了 Verilog 上,是不是非常简单?收工收工!

4.4 本章结语:再见,单周期 CPU !

首先还是要恭喜你读到这里,这说明你已经打通了 CPU 的第一关,了解了 CPU 的整体结构和运行逻辑,用两种不同的方法实现了相当复杂的单周期 CPU !

在 Logisim 一节中,我们从实现每条指令的需求出发,设计了一个又一个的模块,了解到如此壮观的 CPU 世界中,每个不起眼的接口、导线都有着不可或缺的意义,等待着执行独属于自己的指令。

在 Verilog 一节中,我们则是按照模块的顺序,将每个模块逐个翻译为 Verilog 语言。这一节并没有写太多,大多数时间都只是在贴代码,不过我觉得这一节的内容确实也不是很难,因为基本没有涉及到任何新知识点,所以也确实没什么可讲的(其实甚至本来并没打算写这节)。如果你在 Verilog 一章的修炼到位的话,这一关确实可以速通了。

不过还是不要小看了 Verilog !因为在此之后,我们就要离开具象的 Logisim 的怀抱,使用抽象的 Verilog 来完成我们剩下的旅途了!接下来,你将面临的是计组实验课程中的究极大魔王 ——

第五章:五级流水线 CPU

5.1 现在可以公开的情报:一些新的前置知识

【单周期与流水线】

在上一章,我们已经实现了单周期 CPU ;在本章,我们要实现的则是流水线 CPU 。那么,究竟什么是单周期 CPU ,什么是流水线 CPU 呢?

打一个比较简单的比方。Kamonto 每天早上上学之前,需要吃早饭、刷牙、洗脸、洗头发、穿衣服。现在有 100 个这样的 Kamonto 要上学,每个 Kamonto 在完成吃早饭、刷牙、洗脸、洗头发、穿衣服这一整套流程之后,下一个 Kamonto 才可以开始吃早饭,进行自己的流程,这样就需要 100 个周期才能完成任务。这就是我们之前完成的单周期 CPU ,只有当一条指令执行完毕后,下一条指令才能开始执行。

不过,我们可以对 Kamonto 家进行一些适当的改造,将家里改为 5 个不同的分区:吃饭区、刷牙区、洗脸区、洗头区、穿衣区。这样,当每个 Kamonto 吃完早饭,开始刷牙的时候,下一个 Kamonto 就可以进入吃饭区开始吃饭,以此类推,可以实现 5 个 Kamonto 同时进行流程的效果,极大地提升了效率,妈妈再也不用担心 Kamonto 上学迟到了!

这就是五级流水线 CPU 的原理。在后面的实现过程中,我们会将原来的单周期 CPU 分为五个部分,当每条指令执行到第二部分时,就放下一条指令进入第一部分,这样就能够极大地提升指令的运行效率,当然也会极大地提升我们实现的难度(笑)

【延迟槽】

接下来我们来认识一个新的奇妙装置 —— 延迟槽。我们打开 MARS ,输入如下的 MIPS 汇编指令:

1
2
3
4
5
beq $0, $0, branch
li $s0, 1
li $s1, 2
branch:
li $s2, 3

我们观察这段指令,它的执行结果应该如何?当然是 $s0$s1 都没有被赋值,而 $s2 被赋值为 3 了!

接下来,我们选中 Settings 选项中的 Delayed branching ,开启延迟槽,再来执行一下刚才的指令,观察执行结果:

Picture Not Found!

我们在 Execute 界面选择单步执行,会发现在执行完 beq $0, $0, branch 指令后,并没有立即发生跳转,而是进入了下一条指令 li $s0, 1(并且变成了诡异的绿色):

Picture Not Found!

执行完 li $s0, 1 之后,跳转发生,正常进入标签 branch 的位置,执行 li $s2, 3 ,程序结束:

Picture Not Found!

我们从右侧的寄存器值中也可以看出,$s0$s2 分别被赋值为了 1 和 3 ,而 $s1 寄存器没有被赋值:

Picture Not Found!

到这里,聪明的你或许已经猜到了延迟槽的作用了,那就是在跳转发生时不立即跳转,而是先执行跳转指令的下一条指令,随后再“延迟”跳转。

(聪明的我猜到聪明的你肯定会灵机一动问出这样一个问题,那如果跳转指令的下一条指令还是跳转指令,会出现怎样的情况呢?答案是这是一种非法行为,不同的 CPU 得出的结果可能不相同,结果是不可预测的~)

在开启延迟槽时,一些指令的执行逻辑也会发生微妙的变化,这种变化主要体现在 J 型指令中:

首先最明显的就是 jal 指令和 jalr 指令的写寄存器值发生了变化。我们来执行下面一段指令:

1
2
3
4
5
jal branch
li $s0, 1
li $s1, 2
branch:
li $s2, 3

运行结果如下:

Picture Not Found!

可以看到,此时 $ra 寄存器中写入的地址是 0x00003008 ,并不是 jal 指令的下一条指令的地址,而是下两条指令的地址,其实也就是延迟槽中指令的下一条指令的地址。

其实也很好理解,因为在跳转之前,延迟槽指令已经执行过一次了,我们当然不希望使用 jr 指令返回之后,延迟槽指令再被执行一次,所以当然要从延迟槽指令的下一条指令开始执行了!

接下来还想介绍一个比较隐蔽的变化。我们知道,在 j 指令和 jal 指令跳转地址的计算中,我们需要使用当前 PC 值的最高 4 位进行补位。实际上,我们无论是用什么软件模拟 CPU ,都很难能够达到这么高的指令地址,所以 PC 值的最高 4 位一般总是 0x0 ,不会出现任何问题。

不过,开启延迟槽恰好会导致 PC 值的计算发生非常微小的变化。这个变化需要我们研读 MIPS 英文指令集才能够发现:

Picture Not Found!

我们关注图中的这两句话:

The remaining upper bits are the corresponding bits of the address of the instruction in the delay slot (not the branch itself).

This definition creates the following boundary case: When the jump instruction is in the last word of a 256MB region, it can branch only to the following 256 MB region containing the branch delay slot.

jal 指令的说明中也有这两句话,这里就不放图片了)

第一句话表明了,当开启延迟槽时,j 指令和 jal 指令的跳转地址计算中,PC 值的最高 4 位来自延迟槽中指令的 PC 值,而非 j 指令和 jal 指令本身的 PC 值。

第二句话指出了可能会出现的一个小问题,我们考虑这样一种情况:当 j 指令或 jal 指令的地址是 0x0ffffffc 时,延迟槽指令的地址就会是 0x10000000 ,两条指令的 PC 值最高 4 位并不相同。我们可能会以为跳转地址的范围应该是 0x000000000x0ffffffc ,但是实际上并非如此,跳转地址的最高 4 位应该从延迟槽指令中取得,也就是 0x100000000x1ffffffc

在接下来要实现的五级流水线 CPU 中,我们需要开启延迟槽,当然这也会为我们的电路带来一些细小的改变。不过可千万不要认为延迟槽是为了增加实现难度而故意设置的挑战,其实恰恰相反,延迟槽是为了方便我们的设计(同时也是为了增加 CPU 效率)才提出的!随着我们深入了解五级流水线的设计,我相信你就能够理解为什么延迟槽是个好东西了!

5.2 五级流水线 CPU 的基本构造

在这一节中,我们将通过 Logisim 中的简化电路来了解五级流水线 CPU 的基本构造。不过我们目前的单周期 CPU 就已经挤成一坨了,为了便于观看,接下来的图片将会省略部分不影响理解的 Controller 的选择接口,以及 clk 和 reset 接口。下面是省略了部分接口的单周期 CPU ,你能根据这幅图片理清指令的运行逻辑吗:

Picture Not Found!

【五级流水线的划分】

在上一节我们了解到,我们需要将指令执行流程的各个步骤分离出来,形成 5 个不同的功能区,于是我们将电路图划分成这样:

Picture Not Found!

看似非常奇葩的划分方法,接下来我们来按顺序分析一下:

从左往右第一个红框是第一个功能区,我们称之为 F 级。F 级包括 PC 、NPC 、IM 三个模块,作用是根据当前的 PC 值取出对应的指令,当然也是每条指令执行的起点。

第二个红框(不包括蓝框中的部分)是第二个功能区,我们称之为 D 级。可以看到它被蓝框抠掉了一大部分,剩下了半个 GRF 模块和一个 EXT 模块。它的作用是根据 A1 A2 接口的寄存器编号,从 GRF 中取出相应的寄存器值 RD1RD2 ;以及将 16 位立即数 imm16 拓展为 32 位,供下个功能区的 ALU 模块使用。

第三个红框和第四个红框分别是第三个功能区和第四个功能区,分别称之为 E 级和 M 级。两个功能区都只有一个模块,E 级的 ALU 模块负责计算,而 M 级的 DM 模块负责读写内存,非常简单朴素的两个功能区(但愿以后还是这样)。

最后就是奇形怪状的蓝框了,它就是第五个功能区,称为 W 级,负责将数据写回 GRF 模块中 A3 对应的寄存器。为了方便观看,我们在接下来的图片中将 W 级从 D 级中分离出来,放置在 M 级的下游:

Picture Not Found!

需要格外注意的是,图中的 GRF 模块看似分裂成了两块,实际上它们还是同一个模块,这里只是为了观看方便而进行的艺术化处理!!!

【流水线寄存器】

目前我们已经将我们的 CPU 分为了五个不同的功能区,然而当前的电路依然只能够满足一条指令在其中执行。为了让五个功能区能够分别执行自己的指令,我们需要在每两个功能区之间加上一个超大型寄存器,负责在每个时钟上升沿将上一级输出的所有内容原封不动流水到下一级,这样就能够实现每级一条指令的需求:

Picture Not Found!

以防你没有理解流水线寄存器的内部结构,其实就是为每根接入的导线用寄存器延迟一下,像下图这样,非常简单:

Picture Not Found!
(这里输入端的个数和位宽都是随便画的,可以根据需要自由调整)

【适配延迟槽、CMP模块】

在本章中,我们为五级流水线 CPU 启用了延迟槽,所以当然也需要做一些适当的改造。

首先,我们需要调整一下 jal 指令和 jalr 指令存储地址的行为。根据 MIPS 指令的定义,在开启延迟槽的情况下,执行 jal 指令和 jalr 指令时不再会将 PC + 4 的值存入寄存器中,而是存入 PC + 8 ,也就是 jal 指令或 jalr 指令后第二条指令的地址。所以我们应该将随指令一起流水的地址值改为 PC + 8 :

Picture Not Found!

接下来,我们来考虑一下跳转地址,也就是 NPC 模块输出值的计算问题。如果延迟槽没有开启,那么当跳转指令处在 F 级的时候,我们就需要计算出下一条要执行的指令的地址,也就是要向 NPC 模块提供所有输入接口( PC imm16 imm26 GRF NPC_Sel zero )的值,这样才能保证我们能够及时取出下一条要执行的指令。

当延迟槽开启时,我们就不需要这么着急了!当 F 级的指令是跳转指令时,由于延迟槽的存在,下一条要执行的指令的地址一定是当前的 PC 值加 4 ,所以我们可以等到跳转指令进入 D 级再来向 NPC 模块提供输入接口的值。

当跳转指令进入 D 级时,我们就需要着手向 NPC 模块提供信息了。从上面的图片中我们可以看出,imm16 imm26GRF 接口的值,正是从 D 级传给 NPC 模块的。需要注意的是,这里所有的值由 D 级向前传递至 F 级的 NPC 模块,都不需要经过流水线寄存器(如上图所示),因为我们要实现的功能就是使 D 级的跳转指令能够实时修改 F 级中 NPC 模块的信息,如果经过流水线寄存器延迟,就没办法在下个时钟上升沿之前改变即将进入 F 级的指令的地址了!

同样的,我们还需要在跳转指令位于 D 级时,向 NPC 模块提供 PC NPC_Selzero 接口的值,不过这几位就有点不听话了,多少都整了点小活,我们挨个来看看怎么回事:

首先是 PC 接口的值,当 D 级的指令是跳转指令时,我们可以从 D 级传回当前跳转指令的 PC 值,用于计算即将跳转到的地址;但在每个周期 F 级也都会传入一个 F 级当前指令的 PC 值,用于计算非跳转情况下下一条指令的地址,也就是 PC + 4 。我们知道,如果 D 级是跳转指令,由于延迟槽的开启,F 级指令的 PC 值一定等于 D 级指令的 PC 值加 4 ,所以我们实际上并不一定需要向 NPC 模块传递 D 级指令的 PC 值,当 D 级指令为跳转指令时,我们只需要将当前 F 级指令传递的 PC 值减去 4 ,即可得到 D 级相对跳转指令的 PC 值了!

(这一段非常拗口,不过还是一定要把其中的逻辑消化到位,务必要分清 F 级 PC 值和 D 级 PC 值的区别,搞清楚每种 NPC 计算方式中,用到的 PC 值是两种中的哪一种!)

下面是 NPC 计算的完整逻辑,可以假设 F 级是任意指令(当然如果 D 级是跳转指令的话,F 级就只能是非跳转指令了),D 级分别是非跳转指令、相对跳转指令、绝对跳转指令等各种情况,根据预期跳转的结果,思考一下 NPC 的计算值为什么是这样的:

1
2
3
4
5
6
7
8
9
10
if D 级为非跳转指令 or D 级为相对跳转指令,但不满足跳转条件 then
NPC = F_PC + 4 // 此时下一条要执行的指令一定是 F 级指令的下一条指令
else if D 级为相对跳转指令,且满足跳转条件 then
// 此时的 D_PC 一定等于 F_PC - 4
NPC = D_PC + 4 + signed_ext(D_imm16 << 2) = F_PC + signed_ext(D_imm16 << 2)
else if D 级为 j 指令或 jal 指令 then
// 注意这里 PC 的最高 4 位来自延迟槽中的指令,即 F_PC
NPC = (F_PC & 0xf0000000) + (D_imm26 << 2)
else if D 级为 jr 指令或 jalr 指令 then
NPC = GRF

接下来是 NPC_Sel 接口的值,这个值需要通过 Controller 模块产生,在后续讲解 Controller 模块时我们再作研究,现在暂且先放过它。我们目前只需要记住 NPC_Sel 接口的值是在 D 级产生的,而不是 F 级就可以了。

最后是 zero 接口的值,这也是一个比较麻烦的问题,因为从上面的图片中我们可以发现,zero 的值是在 E 级的 ALU 模块中产生的。延迟槽的存在让我们可以把 NPC 的计算拖到 D 级,而 zero 竟然想再拖到 E 级,这是万万不可以的,所以我们一定要想个办法,将 zero 的计算提前到 D 级。

这是能够做到的吗?我们思考一下,发现这当然是可以做到的!事实上,在相对跳转指令的跳转条件判断中,我们一般都是使用两个寄存器的值进行比较,而这两个值在 D 级中就已经能够取到了,我们只不过是把 比较 这个过程放在了 E 级的 ALU 模块而已。所以实际上我们只需要将 比较 这个过程从 E 级的 ALU 模块中抽离出来放在 D 级,就完全可以在 D 级就得到 zero 的值了!

于是我们仿照 ALU 模块,在 D 级建立一个相似的模块 CMP ,将 ALU 模块中 zero 的逻辑移至 CMP 模块中。从此,ALU 模块的 zero 接口正式告别了历史舞台:

Picture Not Found!

【Controller模块的改造】

终于,我们要来解决一个你肯定已经发现了很久的问题,那就是刚才我们分级的时候,好像把 Controller 模块给落下了!那么 Controller 模块究竟属于哪一级呢?

我们知道,Controller 模块是区分不同指令的核心,控制着 CPU 中的每一个选择信号,这些信号分散在 CPU 的各级中,在 D 级、E 级、M 级、W 级都会用到( F 级的 NPC_Sel 接口已调任至 D 级,望周知)。

面对这种情况,我们能够很自然地想到一种方法,那就是将 Controller 放在 D 级,在 D 级产生所有选择信号,接下来让这些信号随指令一起流水,像下图这样:

Picture Not Found!

这种做法其实是一种非常主流的做法,这样实现完全没有任何问题。不过我个人觉得流水每个选择信号写起来还是太麻烦了,所以我选择另外一种做法,那就是为每级都设置一个 Controller :

Picture Not Found!
(因为我喜欢这种做法,所以之后的内容中都会以这种架构进行演示)

那么到目前为止,我们就已经完成了所有最基础的五级流水线改造。是的,我们的工作还远远没有结束,甚至可以说是才刚刚开始!接下来的部分才是五级流水线 CPU 的精髓,令往届同学放弃爱情,为之彻底疯狂的 ——

5.3 阻塞与转发

【阻塞的概念】

不知道你是否已经发现了,当前我们的 CPU 还存在着一个极其严重的结构性问题,严重到只用两条 MIPS 指令就会出现 bug ,我们来看下面这个例子:

1
2
addiu $t1, $t0, 1
addiu $t2, $t1, 1

这两条指令正确执行结果应该是 $t1 寄存器被赋值为 1 ,$t2 寄存器被赋值为 2 。可是我们将其输入到我们的 CPU 中,却发现 $t1$t2 两个寄存器的值都是 1 !这是为什么呢?

我们来将两条指令放在五级流水线中观察。当前一条指令处于 D 级时,这条指令能够正确取出 $t0 寄存器的值(为默认值 0 )。但在下一个周期,前一条指令处于 E 级,正在计算 $t1 寄存器的值时,后一条指令就已经到达 D 级,此时前一条指令还没有将计算的结果写回 $t1 寄存器中,于是后一条指令取到的 $t1 寄存器的值仍然是默认值 0 ,这就导致了 $t2 寄存器的值出现计算错误!

当然,这个问题也有一个非常简单粗暴的解决办法。既然问题在于后一条指令读寄存器时,前一条指令还没有来得及写入寄存器的值,那么我们只需要让后一条指令稍作等待,等前一条指令离开 W 级之后,再让后一条指令进入 D 级读取就可以了。这种让后面的指令等待前面的指令提供寄存器值的解决方法,就被称之为“阻塞”。

当然,如果前一条指令继续流水,后一条指令原地不动,那两条指令之间就会出现空档,此时我们可以直接清空两条指令中间的数据,其实也就相当于在两条指令中间插入了空泡指令 nop

周期 F级 D级 E级 M级 W级
T0 addiu addiu
T1 addiu nop addiu
T2 addiu nop nop addiu
T3 addiu nop nop nop addiu
T4 addiu nop nop nop

(这下知道 nop 指令有什么用了吧)

【转发的概念】

然而,如果我们只使用阻塞,每当遇到类似的情况,CPU 都会停摆 3 个周期。实际上,当我们的 CPU 运行真正的汇编程序时,这种连续多条指令使用同一个寄存器的情况是非常常见的,所以只使用阻塞来解决问题还是有点太浪费时间了。那么有没有一种能够让 CPU 不停摆的解决方法呢?答案就是“转发”!

我们还是以之前的两条指令为例,仔细思考一下整个执行过程。前一条指令真的需要到 W 级才能得到将要写入 $t1 寄存器的值吗?实际上并非如此,这个要写入的值实际上在 E 级就已经完成计算,只不过尚未存入 GRF 模块中而已。后一条指令真的需要在 D 级就拿到 $t1 寄存器的值吗?其实也并不需要,只要在 E 级进入 ALU 模块之前拿到 $t1 寄存器的值,就完全来得及正常进行计算。

于是,我们可以建立一条“紧急传送通道”,让前一条指令刚进入 M 级,后一条指令刚进入 E 级时,前一条指令就将计算出来的 outputA 值直接传给后一条指令,作为 inputA 值再次输入 ALU 模块,刚好卡出了一个完美的 timing ,在 CPU 不停摆的情况下解决了问题,这种解决方法就是“转发”:

Picture Not Found!

这里要说明几个细节。首先就是转发的过程肯定是不需要经过流水线寄存器的,我们设计转发的目的就是构建一个最高效的数据通路,当然不会在这种地方浪费一个周期。

另外就是一个经典问题,为什么我们不能在 E 级 ALU 模块计算出数据后就直接转发,而是要等到 M 级才进行转发?我们可以这样理解,在真实的 CPU 中,为了提高 CPU 的运行效率,一个时钟周期的时间大约就等于运行时间最长的一级的运行时间,而数据在模块中的运行时间,几乎占据了该级运行时间的全部,数据在主模块中导线和 MUX 的时间几乎可以忽略不计。所以如果在模块的后面开始转发,就有可能会出现在一个周期中间甚至是末尾的时候数据发生改变的情况,此时转发终点处的数据可能来不及计算完(例如图中的转发终点,新数据还需要再经过一次 ALU 模块进行计算),下个时钟上升沿就已经到来,导致没能够赶上流水,数据出现错误;而只要保证转发起点在任何模块之前,就可以认为是在一个周期的刚开始就进行了转发,于是就不会出现上面那种来不及的情况了。

(实际上这种问题只存在于时钟周期巨短无比的情况下,我们的 CPU 因为时钟周期足够长,根本不可能发生这种情况,不过我们还是要假装自己在设计真正的 CPU ,遵守一下设计规范~)

当然了,我们的 CPU 在实际执行各种指令时,会出现的问题要比刚才的例子复杂得多。理论上来说,距离在三条指令之内,前面的指令要写的寄存器和后面的指令要读的寄存器相同时,都有可能会出现寄存器的冲突问题。最严重的情况下,可能会出现连续四条指令两两之间全部冲突。但是,只要我们使用转发的思想,就能够解决绝大多数的问题!

什么是转发的思想?对于两条可能会发生冲突的指令,我们不再去考虑前一条指令什么时候需要写寄存器,后一条指令什么时候需要读寄存器,而是去考虑前一条指令什么时候产生要写入寄存器的值,以及后一条指令什么时候需要用到寄存器的值。只要这两个时间节点满足一定条件,那么两条指令之间的冲突就是可以用转发来解决的!

【转发的触发条件】

(观前警告:本书中关于转发和阻塞的计算方法,与指导书中的计算方法并不相同,请注意辨别,千万不要学串了!)

接下来我们就来研究一下,当前后两条指令满足什么条件,我们就可以使用转发来解决两者之间的冲突呢?

首先当然需要满足一些基本条件。比如:前一条指令有要写的寄存器;后一条指令有要读的寄存器(如果有两个要读寄存器,分别计算即可);前一条指令要写的寄存器和后一条指令要读的寄存器相同;这个相同的寄存器不能是 0 号寄存器。这些前提条件看似极其显然,但在设计电路时万万不可或缺,否则就会出现胡乱转发的现象!(尤其是 0 号寄存器那条,让我回忆起新主楼通宵 debug 的夜晚,悲)

接下来就是一些奇妙的小计算。我们从直观上来想,如果前一条指令要写入寄存器的值产生得比较早,后一条指令要用到寄存器的值的时间比较晚,那么我们就能够及时地进行转发,卡上这个 timing ;如果前一条指令要写入寄存器的值产生得非常晚,后一条指令要用到寄存器的值的时间又很早,那就有可能赶不上转发,这时候就需要另想别的办法了。

在接下来的计算中,我们令 D 级、E 级、M 级、W 级分别为 0 、1 、2 、3 。

设前一条指令产生要写入寄存器的值的级次为 $t$(注意这里指的是可以进行转发的级别),例如 addiu 指令在 M 级可以进行转发,于是 addiu 指令的 $t$ 值为 2 ;

设后一条指令要用到 rs 寄存器的值的级次为 $t_{rs}$ ,要用到 rt 寄存器的值的级次为 $t_{rt}$ ,例如 addiu 指令在 E 级会用到 rs 寄存器的值,所以 $t_{rs}$ 的值为 1 ,addiu 指令不会用到 rt 寄存器的值(这里指不会读取 rt 寄存器),所以 $t_{rt}$ 的值不存在,不需要参与计算;

设两条指令所在级别的差为 $\Delta t$ ,例如两条相邻指令的 $\Delta t$ 值为 1 。

根据这些值,你能否给出满足转发条件的不等式呢?答案如下:

$$ t_{rs} \geq t - \Delta t,~~~ t_{rt} \geq t - \Delta t $$

其实推导过程也非常简单,看你能不能转过这个弯:对于前一条指令,当它刚好能够产生要写入的寄存器的值时,它所在的级次为 $t$ ,此时后一条指令所在的级次应该是 $t - \Delta t$ 。当这个级次小于等于 $t_{rs}$ 时,就来得及向 rs 寄存器的值进行转发;当这个级次小于等于 $t_{rt}$ 时,就来得及向 rt 寄存器的值进行转发。这好像是个洛伦兹变换

然而,在我们的 CPU 实际运行时,处于 D 级到 W 级的四条指令,两两之间都需要计算 $\Delta t$ ,写起来会十分复杂,于是我们将两指令之间的距离 $\Delta t$ 转换为 前一条指令到 D 级的距离 与 后一条指令到 D 级的距离 的差,即 $\Delta t = t_1 - t_2$ ,于是我们将不等式改为以下形式:

$$ t_{rs} - t_2 \geq t - t_1,~~~ t_{rt} - t_2 \geq t - t_1 $$

最后,我们设 $t_{rs} - t_2$ 为 $t_{rsuse}$ ,$t_{rt} - t_2$ 为 $t_{rtuse}$ ,$t - t_1$ 为 $t_{new}$ ,即可得到我们最终的计算公式:

$$ t_{rsuse} \geq t_{new},~~~ t_{rtuse} \geq t_{new} $$

非常简洁,有一种大道至简的美感~

接下来我们来分析一下具体的计算流程。我们的计算过程中最关键的指标是 $t_{rs}$ 、$t_{rt}$ 和 $t$ ,它们都是定值,不随指令的流水发生任何改变。

对于需要进入 CMP 模块进行比较的相对跳转指令,它们在 D 级就需要用到 rs 和 rt 寄存器的值,于是它们的 $t_{rs}$ 和 $t_{rt}$ 值均等于 0 ;对于 jrjalr 这些需要用到 rs 寄存器的值作为跳转地址的指令,它们需要在 D 级向 NPC 模块提供 rs 寄存器的值,于是它们的 $t_{rs}$ 值也为 0 ;对于需要在 E 级进入 ALU 模块计算的指令,参与计算的寄存器的 $t_{rs}$ 或 $t_{rt}$ 值为 1 ;对于 sw 等写内存指令,它们的 rt 寄存器值不参与 ALU 模块的运算,在 M 级存入 DM 模块时才会使用到,于是这些指令的 $t_{rt}$ 值为 2 。

在写寄存器的指令中,写入值由 ALU 模块计算产生的指令,在 M 级能够进行转发,于是它们的 $t$ 值为 2 ;写入值从 DM 模块中读取产生的指令,在 W 级能够进行转发,于是它们的 $t$ 值为 3 ;特殊地jaljalr 指令的写入值 PC + 8 从一开始就已经产生,于是它们的 $t$ 值为 0 。

下面是记录了目前所有指令 $t_{rs}$ 、$t_{rt}$ 和 $t$ 值的表格,你也可以尝试着先自己完成这幅表格,再与答案对照,加深自己的理解:

指令 addu subu addiu xori lui lw sw beq bne j jal jr jalr
$t_{rs}$ 1 1 1 1 X 1 1 0 0 X X 0 0
$t_{rt}$ 1 1 X X X X 2 0 0 X X X X
$t$ 2 2 2 2 2 3 X X X X 0 X 0

( X 表示相应的值不存在)

接下来,我们在计算 $t_{rsuse}$ 、$t_{rtuse}$ 和 $t_{new}$ 值时,只需要将 $t_{rs}$ 、$t_{rt}$ 和 $t$ 值减去指令对应的级次即可。为了防止 $t_{rs}$ 、$t_{rt}$ 、$t$ 值出现负数可能带来的莫名其妙的情况,我们规定若计算出的 $t_{rs}$ 、$t_{rt}$ 、$t$ 值小于 0 ,则直接记为 0 。这里我们不对这条规则的正确性作严格推导,因为它的推导需要用到后续阻塞部分的知识,我们目前只需要记住这条规则即可。

举个例子,对于 addiu 指令,它在 D 级的 $t_{rsuse}$ 值为 1 ,$t_{new}$ 值为 2 ;在 E 级的 $t_{rsuse}$ 值为 0 ,$t_{new}$ 值为 1 ;在 M 级的 $t_{rsuse}$ 值为 0 ,$t_{new}$ 值为 0 ;在 W 级的 $t_{rsuse}$ 值为 0 ,$t_{new}$ 值为 0 。整体呈现单调递减的趋势。

本部分的最后一个问题,对于两条处于 D 级到 W 级的指令,只要它们满足寄存器相同等基础条件,以及 $t_{rsuse} \geq t_{new}$ 或 $t_{rtuse} \geq t_{new}$ 的条件,就一定会发生转发吗?

理论上来说,只有当前面的指令的 $t_{new}$ 值等于 0 时,我们才需要进行转发;当前面的指令的 $t_{new}$ 值大于 0 时,这条指令还没有产生要写入的值,无法提供正确的转发值。

不过,在实际编写过程中,我们往往忽略该条件,无论前面的指令的 $t_{new}$ 是否为 0 都进行转发。这是因为,虽然此时的转发值大概率是个错误的值,但我们能保证这个错误的值不会被使用,且最终前面的指令一定能向后面的指令转发正确的值!

这是因为 前面的指令的 $t_{new}$ 值 和 后面的指令的 $t_{rsuse}$ 值(或 $t_{rtuse}$ 值,接下来统称为 $t_{use}$ 值)都在以每周期减 1 的速度减小。因为 $t_{use} \geq t_{new}$ ,所以当 $t_{new}$ 减小为 0 之前,$t_{use}$ 一定大于 0 ,于是就保证了错误的转发值不会被使用;另外,当 $t_{new}$ 减小到 0 时,$t_{use} \geq t_{new}$ 依然成立,所以能够保证最终一定能够进行正确的转发。

【转发的数据通路】

接下来,我们再来分析一下转发的数据通路,也就是来探究一下所有转发情况的起点和终点:

(提前公布一下答案,转发一共有 15 种不同的数据通路,你能将它们全部找到吗?)

首先我们来分析一下转发数据通路的终点。已经掌握了转发思路的你一定能够想到,终点所在的位置必然是需要使用到 rs 或 rt 寄存器的值的模块之前。这样的模块有哪些呢?CMP 模块和 ALU 模块的 inputAinputB 接口可能会用到 rs 和 rt 寄存器的值;当指令向内存中写入数据时,DM 模块的 WD 接口也需要用到 rt 寄存器的值,所以转发数据通路共有 5 种终点。

接下来是转发数据通路的起点。我们需要在每一级的开端,为每一条可能含有写入寄存器的值的导线创建转发数据通路。在之前我们找到了 ALU 模块的 outputA 端口,它的值可以在 M 级作为转发的起点。除此之外还有 GRF 的 WD 端口,即将写入 GRF 的值也可以在 W 级作为转发的起点。

我们已经找到了所有的数据通路了……吗?如果你认为你已经找全了,那你就要失去爱情了!说起为寄存器赋值,我们第一时间想到的就是 GRF -> ALU -> (DM) -> GRF 这条经典的数据通路,不过实际上还有另一条隐藏路线,作为全图中最长的一根导线大隐隐于市,它就是 PC + 8 !这条线路不经过我们刚才分析的任何一个转发数据通路的起点,从 F 级出发直达 W 级,在 jal 指令中为 $ra 寄存器赋值,在 jalr 指令中为 rd 寄存器赋值,所以我们还需要在每一级为这条不起眼的导线加上转发的数据通路!

实际上 PC + 8 的值在 D 级就已经可以转发,不过也没有指令需要在 F 级就用到寄存器的值,所以我们在 D 级可以不设转发通路的起点,不过在 E 级和 M 级都需要设置。事实上,我们在 W 级也可以不需要设置起点,因为此时 PC + 8 的值已经与经典数据通路的值汇合到了一起,不需要单独进行转发了。

综上所述,我们一共找到了 4 个转发数据通路的起点,5 个转发数据通路的终点,除去起点和终点在同一级,或者起点在终点后面的级次等不可能出现的情况,一共有下面 15 种转发数据通路:

Picture Not Found!

需要注意的是,我们的 CPU 有可能会出现两条指令同时向同一个寄存器转发的情况,例如下面三条指令:

1
2
3
lw $t1, 0($t0)
addiu $t1, $t0, 1
ori $t2, $t1, 1

lw 指令处于 W 级,addiu 指令处于 M 级,ori 指令处于 E 级时,lw 指令和 addiu 指令会同时向 ori 指令转发 $t1 寄存器的数据。此时,我们需要保证 ori 指令接收到的数据一定要是最新的,也就是 addiu 指令提供的数据。当一个转发数据通路的终点对应多个起点时,我们一定要注意它们之间的优先级关系!

【阻塞的触发条件】

在解决寄存器冲突问题时,我们研究了 $t_{rsuse} \geq t_{new}$ 和 $t_{rtuse} \geq t_{new}$ 的情况,此时可以转发进行处理;但当任何两级之间出现 $t_{rsuse} < t_{new}$ 或 $t_{rtuse} < t_{new}$ 的情况时,我们就无法通过及时转发来保证 CPU 完全不停摆了。为了保证指令执行的绝对正确性,我们只能稍微作出一些牺牲,暂时对我们的 CPU 进行阻塞。

在熟悉了转发条件的计算之后,我们能够非常简单地证明:在某个时刻,若两条指令满足 $t_{use} \geq t_{new}$ 的关系,那么在之后的任何时刻,这两条指令都一定满足 $t_{use} \geq t_{new}$ 的关系。

那么根据这个结论,我们就可以用反证法证明:在某个时刻,若两条指令满足 $t_{use} < t_{new}$ 的关系,则它们从一开始就应该满足 $t_{use} < t_{new}$ 的关系。

能理解其中的数学逻辑吗?没有理解也不要紧,其实我们只需要知道一个事实,那就是:我们只需要在后一条指令位于 D 级时,判断其是否需要阻塞即可!

于是,我们将 D 级指令的 $t_{rsuse}$ 和 $t_{rtuse}$ 分别与 E 级、M 级、W 级指令的 $t_{new}$ 进行比较,若 6 个比较中的任何一个满足 $t_{use} < t_{new}$ ,则需要对 D 级进行一个周期的阻塞。例如下面的两条指令:

1
2
lw $t1, 0($t0)
addiu $t2, $t1, 1

lw 指令运行至 E 级,addiu 指令运行至 D 级时,lw 指令的 $t_{new}$ 值为 2 ,而 addiu 指令的 $t_{rsuse}$ 值为 1 ,于是 $t_{rsuse} < t_{new}$ ,触发阻塞,lw 指令继续流水进入 M 级,而 addiu 指令停止在 D 级不动,在 E 级插入空泡指令 nop

在下一个周期,lw 指令运行至 M 级,addiu 指令依然位于 D 级,此时 lw 指令的 $t_{new}$ 值为 1 ,addiu 指令的 $t_{rsuse}$ 值为 1 ,于是 $t_{rsuse} \geq t_{new}$ ,即可进行转发。

同样是阻塞的方法,这种与转发衔接的阻塞不需要 CPU 停摆 3 个周期才能继续运行,只要阻塞到满足转发条件,直接进行转发即可。刚才的例子中只需要阻塞一个周期,当然也有需要连续阻塞两个周期的情况:

1
2
lw $t1, 0($t0)
beq $t1, $t2, branch

lw 指令运行至 E 级,beq 指令运行至 D 级时,lw 指令的 $t_{new}$ 值为 2 ,而 beqs 指令的 $t_{rsuse}$ 值为 0 ,所以需要连续阻塞两个周期,等到 lw 指令进入 W 级,$t_{new}$ 值为 0 时才可以进行转发。

【阻塞的实现细节】

接下来我们来分析一下这种阻塞的具体实现方法。当我们判断出需要将 D 级指令阻塞时,该如何操作各个模块和寄存器,使得能够实现阻塞的效果呢?

首先,我们需要确保阻塞期间的 PC 值不变。为了达到 CPU 停摆的效果,我们应当在下一个始终上升沿禁止 NPC 值写入 PC 模块,使 PC 值不会发生改变。所以我们需要为 PC 模块加入写使能接口 en ,当出现阻塞信号关闭 PC 模块的写使能。

另外,我们其实还需要保证在阻塞期间,NPC 值能够被正常计算。不过因为在阻塞期间,D 级的所有数据以及 F 级的 PC 值本来就不会发生任何变化,所以我们不需要对 NPC 模块进行改动。不过在之后涉及到清除 D 级指令时,我们就需要格外注意这个问题了!

接下来,我们还需要保证 D 级的所有数据不变。和 PC 模块相同,我们只需要为 FDreg 流水线寄存器加入写使能 en ,出现阻塞信号时关闭写使能即可。

然后是为 E 级注入空泡 nop 指令。如果像之前一样为 DEreg 流水线寄存器关闭写使能,这种做法肯定是行不通的,因为此时 E 级指令就会像 D 级指令一样留存在 E 级中。此时 DEreg 流水线寄存器真正需要的,是一个新的清除信号 clear ,当出现阻塞信号时,清除信号 clear 开启,将 DEreg 寄存器中的所有数据全部清空,这样就相当于在 E 级注入了一个新的 nop 指令。

最后是 M 级和 W 级,我们不需要对 EMreg 和 MWreg 两个流水线寄存器做任何更改,让 E 级和 M 级的指令自然流水到下一级即可。

到目前为止,我们终于学完了所有的五级流水线改造,从最基本的结构改造,到寄存器冲突的处理,我们的思维已经走了很远很远。现在是时候运用我们掌握的理论,开始用 Verilog 语言动手实践了!

5.4 使用 Verilog 模拟五级流水线 CPU

在上一节,我们通过简化的 Logisim 电路图,了解了将单周期 CPU 改造成五级流水线 CPU 的设计思路。在这一节,我们就来动手操作,在 Verilog 中实现这个精妙的系统!

首先,我们来实现 FDreg 、DEreg 、EMreg 、MWreg 这四个流水线寄存器,它们的具体实现思路非常简单,不过千万不要大意,一定要将需要流水的所有端口写全,具体可以参照这张完全体的电路图中流水线寄存器两侧的端口:

Picture Not Found!

另外,我们不要忘了阻塞对流水线寄存器的影响,所以我们需要为相应的寄存器加入写使能信号 en 和清空信号 clear(为了增强拓展性,这里将所有的流水线寄存器都加上了这两个信号,虽然在目前的实现中有些信号完全不会发生改变):

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
module FDreg(
input clk,
input reset,
input FD_en,
input FD_clear,
input [31:0] F_Instr,
input [31:0] F_PCplus8, // 注意这里是 pcplus8 ,而不再是 pcplus4
input [31:0] F_PC, // 为了拓展性,这里额外再流水一个 PC 值
// 这里寄存器的输出端直接与模块的输出端口相连,为了方便使用了 output reg
output reg [31:0] D_Instr,
output reg [31:0] D_PCplus8,
output reg [31:0] D_PC
);

always@(posedge clk) begin
if (reset | FD_clear) begin
D_Instr <= 32'h00000000;
D_PCplus8 <= 32'h00000000;
D_PC <= 32'h00000000;
end
else begin
if (FD_en) begin
D_Instr <= F_Instr;
D_PCplus8 <= F_PCplus8;
D_PC <= F_PC;
end
end
end

endmodule
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
module DEreg(
input clk,
input reset,
input DE_en,
input DE_clear,
input [31:0] D_Instr,
input [31:0] D_PCplus8,
input [31:0] D_PC,
input [31:0] D_RD1,
input [31:0] D_RD2,
input [4:0] D_A3,
input [31:0] D_ext32,
output reg [31:0] E_Instr,
output reg [31:0] E_PCplus8,
output reg [31:0] E_PC,
output reg [31:0] E_RD1,
output reg [31:0] E_RD2,
output reg [4:0] E_A3,
output reg [31:0] E_ext32
);

always@(posedge clk) begin
if (reset | DE_clear) begin
E_Instr <= 32'h00000000;
E_PCplus8 <= 32'h00000000;
E_PC <= 32'h00000000;
E_RD1 <= 32'h00000000;
E_RD2 <= 32'h00000000;
E_A3 <= 5'b00000;
E_ext32 <= 32'h00000000;
end
else begin
if (DE_en) begin
E_Instr <= D_Instr;
E_PCplus8 <= D_PCplus8;
E_PC <= D_PC;
E_RD1 <= D_RD1;
E_RD2 <= D_RD2;
E_A3 <= D_A3;
E_ext32 <= D_ext32;
end
end
end

endmodule
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
module EMreg(
input clk,
input reset,
input EM_en,
input EM_clear,
input [31:0] E_Instr,
input [31:0] E_PCplus8,
input [31:0] E_PC,
input [31:0] E_outputA,
input [31:0] E_RD2,
input [4:0] E_A3,
output reg [31:0] M_Instr,
output reg [31:0] M_PCplus8,
output reg [31:0] M_PC,
output reg [31:0] M_outputA,
output reg [31:0] M_RD2,
output reg [4:0] M_A3
);

always@(posedge clk) begin
if (reset | EM_clear) begin
M_Instr <= 32'h00000000;
M_PCplus8 <= 32'h00000000;
M_PC <= 32'h00000000;
M_outputA <= 32'h00000000;
M_RD2 <= 32'h00000000;
M_A3 <= 5'b00000;
end
else begin
if (EM_en) begin
M_Instr <= E_Instr;
M_PCplus8 <= E_PCplus8;
M_PC <= E_PC;
M_outputA <= E_outputA;
M_RD2 <= E_RD2;
M_A3 <= E_A3;
end
end
end

endmodule
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
module MWreg(
input clk,
input reset,
input MW_en,
input MW_clear,
input [31:0] M_Instr,
input [31:0] M_PCplus8,
input [31:0] M_PC,
input [4:0] M_A3,
input [31:0] M_outputA,
input [31:0] M_data,
output reg [31:0] W_Instr,
output reg [31:0] W_PCplus8,
output reg [31:0] W_PC,
output reg [4:0] W_A3,
output reg [31:0] W_outputA,
output reg [31:0] W_data
);

always@(posedge clk) begin
if (reset | MW_clear) begin
W_Instr <= 32'h00000000;
W_PCplus8 <= 32'h00000000;
W_PC <= 32'h00000000;
W_A3 <= 5'b00000;
W_outputA <= 32'h00000000;
W_data <= 32'h00000000;
end
else begin
if (MW_en) begin
W_Instr <= M_Instr;
W_PCplus8 <= M_PCplus8;
W_PC <= M_PC;
W_A3 <= M_A3;
W_outputA <= M_outputA;
W_data <= M_data;
end
end
end

endmodule

同样,我们也需要为 PC 模块加入使能信号 en

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module PC(
input clk,
input reset,
input PC_en,
input [31:0] NPC,
output [31:0] PC
);

reg [31:0] regPC;

always@(posedge clk) begin
if (reset) begin
regPC <= 32'h00003000;
end
else begin
if (PC_en) begin
regPC <= NPC;
end
end
end

assign PC = regPC;

endmodule

接下来,我们对 NPC 模块的计算逻辑进行修改:

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
module NPC(
input [31:0] F_PC, // 为了防止混淆,这里标注出传入的 PC 值为 F 级 PC 值
input [15:0] imm16,
input [25:0] imm26,
input [31:0] GRF,
input [1:0] NPC_Sel,
input zero,
output [31:0] NPC
);

parameter PCPLUS4 = 2'b00,
IMM26 = 2'b01,
GRFconst = 2'b10,
IMM16 = 2'b11;

wire [31:0] sign_ext;
wire [31:0] NPC_PCplus4;
wire [31:0] NPC_imm16;
wire [31:0] NPC_imm26;
wire [31:0] NPC_GRF;

assign sign_ext = {{14{imm16[15]}}, imm16, 2'b00};
assign NPC_PCplus4 = F_PC + 32'h00000004;
assign NPC_imm16 = F_PC + sign_ext; // 注意这里不再有 + 4
assign NPC_imm26 = {F_PC[31:28], imm26, 2'b00};
assign NPC_GRF = GRF;

assign NPC = (NPC_Sel == PCPLUS4) ? NPC_PCplus4 :
(NPC_Sel == IMM26) ? NPC_imm26 :
(NPC_Sel == GRFconst) ? NPC_GRF :
(NPC_Sel == IMM16 && zero) ? NPC_imm16 :
NPC_PCplus4; // NPC_Sel == IMM16 && !zero

endmodule

再接下来,我们将 ALU 模块中 zero 的判断正式移交给 CMP 模块,实现跳转判断的前移:

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
module ALU(
input [31:0] inputA,
input [31:0] inputB,
input [5:0] type,
output [31:0] outputA
);

// 这里可以省略一些不需要进入 ALU 计算的指令
parameter ADDU = 6'b000000,
SUBU = 6'b000001,
ADDIU = 6'b000010,
XORI = 6'b000011,
LUI = 6'b000100,
LW = 6'b000101,
SW = 6'b000110;

wire [31:0] AaddB;
wire [31:0] AsubB;
wire [31:0] AxorB;
wire [31:0] Bleftshift16;
wire [31:0] none;

assign AaddB = (inputA + inputB);
assign AsubB = (inputA - inputB);
assign AxorB = (inputA ^ inputB);
assign Bleftshift16 = (inputB << 16);
assign none = 32'h00000000;

assign outputA = (type == ADDU) ? AaddB :
(type == SUBU) ? AsubB :
(type == ADDIU) ? AaddB :
(type == XORI) ? AxorB :
(type == LUI) ? Bleftshift16 :
(type == LW) ? AaddB :
(type == SW) ? AaddB : none;

endmodule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module CMP(
input [31:0] inputA,
input [31:0] inputB,
input [5:0] type,
output zero
);

// 这里可以省略一些不需要进入 CMP 计算的指令
parameter BEQ = 6'b000111,
BNE = 6'b001000;

assign zero = ((type == BEQ) && (inputA == inputB)) ? 1'b1 :
((type == BNE) && (inputA != inputB)) ? 1'b1 : 1'b0;

endmodule

下一步就是对 Controller 模块的改造了!虽然我们确定了采取 D 级、E 级、M 级、W 级各一个 Controller 模块的策略,但同一种策略也有很多种不同的写法。在这里我们选取一种最便于上机加指令的写法,即 DController 、EController 、MController 、WController 全部引用同一个 Controller ,模块接口和内部结构完全相同,由主模块实现 $t_{rsuse}$ 、$t_{rtuse}$ 和 $t_{new}$ 的计算:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
module Controller(
input [5:0] OpCode,
input [5:0] Funct,
output [1:0] NPC_Sel,
output RegWrite,
output EXTop,
output ALUsrc,
output RegDst,
output MemWrite,
output MemtoReg,
output PCtoReg,
output Regra,
output [5:0] type,
output [3:0] t_rs,
output [3:0] t_rt,
output [3:0] t
);

// 这里的定义一定要和 ALU 、CMP 中的定义保持一致!
parameter ADDU = 6'b000000,
SUBU = 6'b000001,
ADDIU = 6'b000010,
XORI = 6'b000011,
LUI = 6'b000100,
LW = 6'b000101,
SW = 6'b000110,
BEQ = 6'b000111,
BNE = 6'b001000,
J = 6'b001001,
JAL = 6'b001010,
JR = 6'b001011,
JALR = 6'b001100;

wire addu;
wire subu;
wire addiu;
wire xori;
wire lui;
wire lw;
wire sw;
wire beq;
wire bne;
wire j;
wire jal;
wire jr;
wire jalr;

assign addu = (OpCode == 6'b000000 && Funct == 6'b100001) ? 1'b1 : 1'b0;
assign subu = (OpCode == 6'b000000 && Funct == 6'b100011) ? 1'b1 : 1'b0;
assign addiu = (OpCode == 6'b001001) ? 1'b1 : 1'b0;
assign xori = (OpCode == 6'b001110) ? 1'b1 : 1'b0;
assign lui = (OpCode == 6'b001111) ? 1'b1 : 1'b0;
assign lw = (OpCode == 6'b100011) ? 1'b1 : 1'b0;
assign sw = (OpCode == 6'b101011) ? 1'b1 : 1'b0;
assign beq = (OpCode == 6'b000100) ? 1'b1 : 1'b0;
assign bne = (OpCode == 6'b000101) ? 1'b1 : 1'b0;
assign j = (OpCode == 6'b000010) ? 1'b1 : 1'b0;
assign jal = (OpCode == 6'b000011) ? 1'b1 : 1'b0;
assign jr = (OpCode == 6'b000000 && Funct == 6'b001000) ? 1'b1 : 1'b0;
assign jalr = (OpCode == 6'b000000 && Funct == 6'b001001) ? 1'b1 : 1'b0;

// 这里 NPC_Sel 的值一定要和 NPC 中的定义保持一致!
assign NPC_Sel = (j | jal) ? 2'b01 :
(jr | jalr) ? 2'b10 :
(beq | bne) ? 2'b11 : 2'b00;
assign RegWrite = (addu | subu | addiu | xori | lui | lw | jal | jalr) ? 1'b1 : 1'b0;
assign EXTop = (addiu | lw | sw) ? 1'b1 : 1'b0;
assign ALUsrc = (addiu | xori | lui | lw | sw) ? 1'b1 : 1'b0;
assign RegDst = (addu | subu | jalr) ? 1'b1 : 1'b0;
assign MemWrite = (sw) ? 1'b1 : 1'b0;
assign MemtoReg = (lw) ? 1'b1 : 1'b0;
assign PCtoReg = (jal | jalr) ? 1'b1 : 1'b0;
assign Regra = (jal) ? 1'b1 : 1'b0;
// 这里将全 1 当作错误或未实现指令的输出(包括 nop 指令)
assign type = (addu) ? ADDU :
(subu) ? SUBU :
(addiu) ? ADDIU :
(xori) ? XORI :
(lui) ? LUI :
(lw) ? LW :
(sw) ? SW :
(beq) ? BEQ :
(bne) ? BNE :
(j) ? J :
(jal) ? JAL :
(jr) ? JR :
(jalr) ? JALR : 6'b111111;

// 这里的 t_rs, t_rt 和 t 的定义与之前推导中的定义相同,分别等于 D 级指令的 t_rsuse, t_rtuse 和 t_new
// 这里将全 1 当作该值不存在或未实现指令的输出(包括 nop 指令)
assign t_rs = (addu) ? 4'h1 :
(subu) ? 4'h1 :
(addiu) ? 4'h1 :
(xori) ? 4'h1 :
(lui) ? 4'hf :
(lw) ? 4'h1 :
(sw) ? 4'h1 :
(beq) ? 4'h0 :
(bne) ? 4'h0 :
(j) ? 4'hf :
(jal) ? 4'hf :
(jr) ? 4'h0 :
(jalr) ? 4'h0 : 4'hf;

assign t_rt = (addu) ? 4'h1 :
(subu) ? 4'h1 :
(addiu) ? 4'hf :
(xori) ? 4'hf :
(lui) ? 4'hf :
(lw) ? 4'hf :
(sw) ? 4'h2 :
(beq) ? 4'h0 :
(bne) ? 4'h0 :
(j) ? 4'hf :
(jal) ? 4'hf :
(jr) ? 4'hf :
(jalr) ? 4'hf : 4'hf;

assign t = (addu) ? 4'h2 :
(subu) ? 4'h2 :
(addiu) ? 4'h2 :
(xori) ? 4'h2 :
(lui) ? 4'h2 :
(lw) ? 4'h3 :
(sw) ? 4'hf :
(beq) ? 4'hf :
(bne) ? 4'hf :
(j) ? 4'hf :
(jal) ? 4'h0 :
(jr) ? 4'hf :
(jalr) ? 4'h0 : 4'hf;

endmodule

最后,我们来对主模块进行大刀阔斧的修改!首先,我们将主模块中的所有导线按流水线级次进行拆分:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
module mips(
input clk,
input reset
);

// F zone ////////////////////////////////
// PC and NPC
wire [31:0] F_PC;
wire [31:0] F_NPC;
wire [31:0] F_PCplus8; // 注意这里变为了 PCplus8

// IM
wire [31:0] F_Instr;

// D zone ////////////////////////////////
// D Controller
wire D_RegDst;
wire [1:0] D_NPC_Sel;
wire D_EXTop;
wire D_Regra;
wire [5:0] D_type;
wire [3:0] D_t_rs;
wire [3:0] D_t_rt;
wire [3:0] D_t;

// D Instruction
// 为了进行转发和阻塞的计算,每一级都需要解析一次指令
wire [31:0] D_Instr;
wire [5:0] D_OpCode;
wire [4:0] D_rs;
wire [4:0] D_rt;
wire [4:0] D_rd;
wire [4:0] D_shamt;
wire [5:0] D_Funct;
wire [15:0] D_imm16;
wire [25:0] D_imm26;

// GRF(R)
wire [4:0] D_A1;
wire [4:0] D_A2;
wire [4:0] D_A3;
wire [31:0] D_RD1;
wire [31:0] D_RD2;

// EXT
wire [31:0] D_ext32;

// CMP
wire D_zero;

// pipeline
// 从 F 级流水进入 D 级的数据
wire [31:0] D_PCplus8;
wire [31:0] D_PC;

// E zone ////////////////////////////////
// E Controller
wire E_ALUsrc;
wire [5:0] E_type;
wire [3:0] E_t_rs;
wire [3:0] E_t_rt;
wire [3:0] E_t;

// E Instruction
wire [31:0] E_Instr;
wire [5:0] E_OpCode;
wire [4:0] E_rs;
wire [4:0] E_rt;
wire [4:0] E_rd;
wire [4:0] E_shamt;
wire [5:0] E_Funct;
wire [15:0] E_imm16;
wire [25:0] E_imm26;

// ALU
wire [31:0] E_inputA;
wire [31:0] E_inputB;
wire [31:0] E_outputA;

// pipeline
// 从 D 级流水进入 E 级的数据
wire [31:0] E_PCplus8;
wire [31:0] E_PC;
wire [31:0] E_RD1;
wire [31:0] E_RD2;
wire [4:0] E_A3;
wire [31:0] E_ext32;

// M zone ////////////////////////////////
// M Controller
wire M_MemWrite;
wire [5:0] M_type;
wire [3:0] M_t_rs;
wire [3:0] M_t_rt;
wire [3:0] M_t;

// M Instruction
wire [31:0] M_Instr;
wire [5:0] M_OpCode;
wire [4:0] M_rs;
wire [4:0] M_rt;
wire [4:0] M_rd;
wire [4:0] M_shamt;
wire [5:0] M_Funct;
wire [15:0] M_imm16;
wire [25:0] M_imm26;

// DM
wire [31:0] M_addr;
wire [31:0] M_WD_Mem;
wire [31:0] M_data;

// pipeline
// 从 E 级流水进入 M 级的数据
wire [31:0] M_PCplus8;
wire [31:0] M_PC;
wire [31:0] M_outputA;
wire [31:0] M_RD2;
wire [4:0] M_A3;

// W zone ////////////////////////////////
// W Controller
wire W_MemtoReg;
wire W_RegWrite;
wire W_PCtoReg;
wire [5:0] W_type;
wire [3:0] W_t_rs;
wire [3:0] W_t_rt;
wire [3:0] W_t;

// W Instruction
wire [31:0] W_Instr;
wire [5:0] W_OpCode;
wire [4:0] W_rs;
wire [4:0] W_rt;
wire [4:0] W_rd;
wire [4:0] W_shamt;
wire [5:0] W_Funct;
wire [15:0] W_imm16;
wire [25:0] W_imm26;

// GRF(W)
wire [4:0] W_A3;
wire [31:0] W_WD_Reg;

// pipeline
// 从 E 级流水进入 M 级的数据
wire [31:0] W_PCplus8;
wire [31:0] W_PC;
wire [31:0] W_outputA;
wire [31:0] W_data;

endmodule

接下来引入转发与阻塞相关的信号:

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
module mips(
input clk,
input reset
);

// forward ///////////////////////////////
// fixed data
// 转发之后的修正值
wire [31:0] D_fixedRD1;
wire [31:0] D_fixedRD2;
wire [31:0] E_fixedRD1;
wire [31:0] E_fixedRD2;
wire [31:0] M_fixedRD2;

// tuse and tnew
wire [3:0] D_t_rsuse;
wire [3:0] D_t_rtuse;
wire [3:0] D_t_new;

wire [3:0] E_t_rsuse;
wire [3:0] E_t_rtuse;
wire [3:0] E_t_new;

wire [3:0] M_t_rsuse;
wire [3:0] M_t_rtuse;
wire [3:0] M_t_new;

wire [3:0] W_t_rsuse;
wire [3:0] W_t_rtuse;
wire [3:0] W_t_new;

// isPCplus8
// 判断转发的起点是否为 PC + 8
wire E_isPCplus8;
wire M_isPCplus8;

// forward
// 转发数据通路(15条)
wire D_RD1_from_E_PCplus8;
wire D_RD2_from_E_PCplus8;

wire D_RD1_from_M_PCplus8;
wire D_RD2_from_M_PCplus8;
wire E_RD1_from_M_PCplus8;
wire E_RD2_from_M_PCplus8;

wire D_RD1_from_M;
wire D_RD2_from_M;
wire E_RD1_from_M;
wire E_RD2_from_M;

wire D_RD1_from_W;
wire D_RD2_from_W;
wire E_RD1_from_W;
wire E_RD2_from_W;
wire M_RD2_from_W;

// stall /////////////////////////////////
// pipeline reg
// PC 模块和流水线寄存器控制信号
wire PC_en;
wire FD_en;
wire DE_en;
wire EM_en;
wire MW_en;

wire FD_clear;
wire DE_clear;
wire EM_clear;
wire MW_clear;

// stall
// 阻塞发生信号
wire D_stall;

endmodule

接下来导入所有模块:

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
module mips(
input clk,
input reset
);

// F zone ////////////////////////////////
PC PC (.clk(clk), .reset(reset), .PC_en(PC_en), .NPC(F_NPC), .PC(F_PC));
// 这里注意信号处于 F 级还是 D 级
NPC NPC (.F_PC(F_PC), .NPC_Sel(D_NPC_Sel), .zero(D_zero), .imm16(D_imm16),
.imm26(D_imm26), .GRF(D_fixedRD1), .NPC(F_NPC));
IM IM (.addr(F_PC), .data(F_Instr));
FDreg FDreg (.clk(clk), .reset(reset), .FD_en(FD_en), .FD_clear(FD_clear),
.F_Instr(F_Instr), .F_PCplus8(F_PCplus8), .F_PC(F_PC),
.D_Instr(D_Instr), .D_PCplus8(D_PCplus8), .D_PC(D_PC));

// D zone ////////////////////////////////
// 这里注意不要定义一堆 Controller 模块,所有 Controller 模块引用同一个就可以
// 不需要的选择信号可以不连接对应接口
Controller DController (.OpCode(D_OpCode), .Funct(D_Funct), .RegDst(D_RegDst),
.NPC_Sel(D_NPC_Sel), .EXTop(D_EXTop), .Regra(D_Regra),
.type(D_type), .t_rs(D_t_rs), .t_rt(D_t_rt), .t(D_t));
GRF GRF (.clk(clk), .reset(reset), .WE(W_RegWrite), .A1(D_A1), .A2(D_A2),
.A3(W_A3), .WD(W_WD_Reg), .RD1(D_RD1), .RD2(D_RD2));
EXT EXT (.imm16(D_imm16), .EXTop(D_EXTop), .ext32(D_ext32));
// 这里注意连接 inputA 和 inputB 接口的是修正值 D_fixedRD1 和 D_fixedRD2
CMP CMP (.inputA(D_fixedRD1), .inputB(D_fixedRD2), .type(D_type), .zero(D_zero));
// 这里注意连接 D_RD1 和 D_RD2 接口的也是修正值 D_fixedRD1 和 D_fixedRD2
DEreg DEreg (.clk(clk), .reset(reset), .DE_en(DE_en), .DE_clear(DE_clear),
.D_Instr(D_Instr), .D_PCplus8(D_PCplus8), .D_PC(D_PC),
.D_RD1(D_fixedRD1), .D_RD2(D_fixedRD2), .D_A3(D_A3), .D_ext32(D_ext32),
.E_Instr(E_Instr), .E_PCplus8(E_PCplus8), .E_PC(E_PC),
.E_RD1(E_RD1), .E_RD2(E_RD2), .E_A3(E_A3), .E_ext32(E_ext32));

// E zone ////////////////////////////////
Controller EController (.OpCode(E_OpCode), .Funct(E_Funct), .ALUsrc(E_ALUsrc),
.type(E_type), .t_rs(E_t_rs), .t_rt(E_t_rt), .t(E_t));
ALU ALU (.inputA(E_inputA), .inputB(E_inputB), .type(E_type), .outputA(E_outputA));
// 这里注意连接 E_RD2 接口的是修正值 E_fixedRD2
EMreg EMreg (.clk(clk), .reset(reset), .EM_en(EM_en), .EM_clear(EM_clear),
.E_Instr(E_Instr), .E_PCplus8(E_PCplus8), .E_PC(E_PC),
.E_outputA(E_outputA), .E_RD2(E_fixedRD2), .E_A3(E_A3),
.M_Instr(M_Instr), .M_PCplus8(M_PCplus8), .M_PC(M_PC),
.M_outputA(M_outputA), .M_RD2(M_RD2), .M_A3(M_A3));

// M zone ////////////////////////////////
Controller MController (.OpCode(M_OpCode), .Funct(M_Funct), .MemWrite(M_MemWrite),
.type(M_type), .t_rs(M_t_rs), .t_rt(M_t_rt), .t(M_t));
DM DM (.clk(clk), .reset(reset), .WE(M_MemWrite), .addr(M_addr), .WD(M_WD_Mem),
.data(M_data));
MWreg MWreg (.clk(clk), .reset(reset), .MW_en(MW_en), .MW_clear(MW_clear),
.M_Instr(M_Instr), .M_PCplus8(M_PCplus8), .M_PC(M_PC),
.M_A3(M_A3), .M_outputA(M_outputA), .M_data(M_data),
.W_Instr(W_Instr), .W_PCplus8(W_PCplus8), .W_PC(W_PC),
.W_A3(W_A3), .W_outputA(W_outputA), .W_data(W_data));

// W zone ////////////////////////////////
Controller WController (.OpCode(W_OpCode), .Funct(W_Funct), .MemtoReg(W_MemtoReg),
.RegWrite(W_RegWrite), .PCtoReg(W_PCtoReg), .type(W_type),
.t_rs(W_t_rs), .t_rt(W_t_rt), .t(W_t));

endmodule

接下来是一些 wire 型变量的赋值:

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
module mips(
input clk,
input reset
);

// F zone ////////////////////////////////
assign F_PCplus8 = F_PC + 32'h00000008;

// D zone ////////////////////////////////
assign D_OpCode = D_Instr[31:26];
assign D_rs = D_Instr[25:21];
assign D_rt = D_Instr[20:16];
assign D_rd = D_Instr[15:11];
assign D_shamt = D_Instr[10:6];
assign D_Funct = D_Instr[5:0];
assign D_imm16 = D_Instr[15:0];
assign D_imm26 = D_Instr[25:0];

assign D_A1 = D_rs;
assign D_A2 = D_rt;
assign D_A3 = (D_Regra) ? 5'b11111 :
(D_RegDst) ? D_rd : D_rt;

// E zone ////////////////////////////////
assign E_OpCode = E_Instr[31:26];
assign E_rs = E_Instr[25:21];
assign E_rt = E_Instr[20:16];
assign E_rd = E_Instr[15:11];
assign E_shamt = E_Instr[10:6];
assign E_Funct = E_Instr[5:0];
assign E_imm16 = E_Instr[15:0];
assign E_imm26 = E_Instr[25:0];

// 这里还是要注意将 RD1 和 RD2 替换为修正值 E_fixedRD1 和 E_fixedRD2
assign E_inputA = E_fixedRD1;
assign E_inputB = (E_ALUsrc) ? E_ext32 : E_fixedRD2;

// M zone ////////////////////////////////
assign M_OpCode = M_Instr[31:26];
assign M_rs = M_Instr[25:21];
assign M_rt = M_Instr[20:16];
assign M_rd = M_Instr[15:11];
assign M_shamt = M_Instr[10:6];
assign M_Funct = M_Instr[5:0];
assign M_imm16 = M_Instr[15:0];
assign M_imm26 = M_Instr[25:0];

assign M_addr = M_outputA;
// 这里注意...算了我不说了,你来接
assign M_WD_Mem = M_fixedRD2;

// W zone ////////////////////////////////
assign W_OpCode = W_Instr[31:26];
assign W_rs = W_Instr[25:21];
assign W_rt = W_Instr[20:16];
assign W_rd = W_Instr[15:11];
assign W_shamt = W_Instr[10:6];
assign W_Funct = W_Instr[5:0];
assign W_imm16 = W_Instr[15:0];
assign W_imm26 = W_Instr[25:0];

assign W_WD_Reg = (W_PCtoReg) ? W_PCplus8 :
(W_MemtoReg) ? W_data : W_outputA;

endmodule

最后就是激动人心的转发与阻塞部分了:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
module mips(
input clk,
input reset
);

// forward ///////////////////////////////
// fixed data
// 这里一定要注意多个转发同时发生时的优先级,即转发起点 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;
assign D_fixedRD2 = (D_RD2_from_E_PCplus8) ? E_PCplus8 :
(D_RD2_from_M_PCplus8) ? M_PCplus8 :
(D_RD2_from_M) ? M_outputA :
(D_RD2_from_W) ? W_WD_Reg : D_RD2;
assign E_fixedRD1 = (E_RD1_from_M_PCplus8) ? M_PCplus8 :
(E_RD1_from_M) ? M_outputA :
(E_RD1_from_W) ? W_WD_Reg : E_RD1;
assign E_fixedRD2 = (E_RD2_from_M_PCplus8) ? M_PCplus8 :
(E_RD2_from_M) ? M_outputA :
(E_RD2_from_W) ? W_WD_Reg : E_RD2;
assign M_fixedRD2 = (M_RD2_from_W) ? W_WD_Reg : M_RD2;

// 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;

assign M_t_rsuse = (M_t_rs == 4'hf) ? 4'hf :
(M_t_rs >= 4'h2) ? (M_t_rs - 4'h2) : 4'h0;
assign M_t_rtuse = (M_t_rt == 4'hf) ? 4'hf :
(M_t_rt >= 4'h2) ? (M_t_rt - 4'h2) : 4'h0;
assign M_t_new = (M_t == 4'hf) ? 4'hf :
(M_t >= 4'h2) ? (M_t - 4'h2) : 4'h0;

assign W_t_rsuse = (W_t_rs == 4'hf) ? 4'hf :
(W_t_rs >= 4'h3) ? (W_t_rs - 4'h3) : 4'h0;
assign W_t_rtuse = (W_t_rt == 4'hf) ? 4'hf :
(W_t_rt >= 4'h3) ? (W_t_rt - 4'h3) : 4'h0;
assign W_t_new = (W_t == 4'hf) ? 4'hf :
(W_t >= 4'h3) ? (W_t - 4'h3) : 4'h0;

// isPCplus8
// 这里直接硬编码了 jal 指令和 jalr 指令的 type
assign E_isPCplus8 = (E_type == 6'b001010) || (E_type == 6'b001100);
assign M_isPCplus8 = (M_type == 6'b001010) || (M_type == 6'b001100);

// 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;

assign D_RD1_from_M_PCplus8 = (M_isPCplus8 && 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 : 1'b0;
assign D_RD2_from_M_PCplus8 = (M_isPCplus8 && 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 : 1'b0;
assign E_RD1_from_M_PCplus8 = (M_isPCplus8 && M_A3 != 5'b00000 && E_rs == M_A3 && E_t_rsuse != 4'hf && M_t_new != 4'hf && E_t_rsuse >= M_t_new) ? 1'b1 : 1'b0;
assign E_RD2_from_M_PCplus8 = (M_isPCplus8 && M_A3 != 5'b00000 && E_rt == M_A3 && E_t_rtuse != 4'hf && M_t_new != 4'hf && E_t_rtuse >= M_t_new) ? 1'b1 : 1'b0;

assign D_RD1_from_M = (~M_isPCplus8 && 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 : 1'b0;
assign D_RD2_from_M = (~M_isPCplus8 && 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 : 1'b0;
assign E_RD1_from_M = (~M_isPCplus8 && M_A3 != 5'b00000 && E_rs == M_A3 && E_t_rsuse != 4'hf && M_t_new != 4'hf && E_t_rsuse >= M_t_new) ? 1'b1 : 1'b0;
assign E_RD2_from_M = (~M_isPCplus8 && M_A3 != 5'b00000 && E_rt == M_A3 && E_t_rtuse != 4'hf && M_t_new != 4'hf && E_t_rtuse >= M_t_new) ? 1'b1 : 1'b0;

assign D_RD1_from_W = (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 : 1'b0;
assign D_RD2_from_W = (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;
assign E_RD1_from_W = (W_A3 != 5'b00000 && E_rs == W_A3 && E_t_rsuse != 4'hf && W_t_new != 4'hf && E_t_rsuse >= W_t_new) ? 1'b1 : 1'b0;
assign E_RD2_from_W = (W_A3 != 5'b00000 && E_rt == W_A3 && E_t_rtuse != 4'hf && W_t_new != 4'hf && E_t_rtuse >= W_t_new) ? 1'b1 : 1'b0;
assign M_RD2_from_W = (W_A3 != 5'b00000 && M_rt == W_A3 && M_t_rtuse != 4'hf && W_t_new != 4'hf && M_t_rtuse >= W_t_new) ? 1'b1 : 1'b0;

// stall /////////////////////////////////
// pipeline reg
assign PC_en = (D_stall) ? 1'b0 : 1'b1;
assign FD_en = (D_stall) ? 1'b0 : 1'b1;
assign DE_en = 1'b1;
assign EM_en = 1'b1;
assign MW_en = 1'b1;

assign FD_clear = 1'b0;
assign DE_clear = (D_stall) ? 1'b1 : 1'b0;
assign EM_clear = 1'b0;
assign MW_clear = 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;

endmodule

终于终于终于!我们在 Verilog 上完成了五级流水线 CPU 的改造!这一次的任务真的说不上很轻松了,还是要稍微休息一下 —— 不过这次要记得回来,因为我们在下一节还要做一些练习,帮助你通过上机实验!

5.5 五级流水线 CPU 实战:加指令练习

这应该是本书中第一次正式拿出整整一节的篇幅讲解上机实验相关内容。大概是因为 P5 和 P6 的上机实验确实非常困难,没有事先的准备几乎一定无法通过,如果没有掌握技巧的话可能会一路挂到底。—— 挂了两回 P5 的 Kamonto 如此说道。

本节中出现的所有指令,有些是 MIPS 指令集里面真实存在的指令,有些是仿照往年助教清奇的思路虚构出来的指令。这些指令都仅用于本节中的练习,不会在后续的 CPU 中再次出现。

【计算类指令:slt】

首先,我们来从一个最简单的计算类指令 slt 开始练习,这是一个存在于 MIPS 指令集中的指令,下面是它的符号语言描述:

Code Block
 if GPR[rs] < GPR[rt] then
    GPR[rd] ← 031 || 1
 else
    GPR[rd]← 032
 endif

接下来,我们通过这条非常简单的指令,感受一下计算类指令的加指令流程。

无论是什么指令,我们加指令的第一步肯定都是在 Controller 中走固定流程。首先,我们根据题目中给出的 OpCode 和 Funct 为其编写解析逻辑:

1
2
3
4
// Controller
parameter SLT = 6'bXXXXXX;
wire slt;
assign slt = (OpCode == 6'bXXXXXX && Funct == 6'bXXXXXX) ? 1'b1 : 1'b0;

(这里的 6'bXXXXXX 根据题目要求和你的实现自行填写即可)

接下来,我们要为新指令适配所有的选择信号。事实上,对于一个普普通通的 R 型计算类指令来说,除了在 ALU 模块中的运算逻辑不同之外,它的其它所有运行逻辑都和 addu subu 等一众指令完全相同,所以我们完全可以仿照这些指令来为其添加选择信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Controller
assign NPC_Sel = (j | jal) ? 2'b01 :
(jr | jalr) ? 2'b10 :
(beq | bne) ? 2'b11 : 2'b00;
// 只要出现 addu 和 subu ,我们就可以直接无脑跟
assign RegWrite = (addu | subu | slt | addiu | xori | lui | lw | jal | jalr) ? 1'b1 : 1'b0;
assign EXTop = (addiu | lw | sw) ? 1'b1 : 1'b0;
assign ALUsrc = (addiu | xori | lui | lw | sw) ? 1'b1 : 1'b0;
assign RegDst = (addu | subu | slt | jalr) ? 1'b1 : 1'b0;
assign MemWrite = (sw) ? 1'b1 : 1'b0;
assign MemtoReg = (lw) ? 1'b1 : 1'b0;
assign PCtoReg = (jal | jalr) ? 1'b1 : 1'b0;
assign Regra = (jal) ? 1'b1 : 1'b0;
// 不要忘了在 type 处也添加 slt 的信号
assign type = (addu) ? ADDU :
(subu) ? SUBU :
...... :
(slt) ? SLT : 6'b111111;

随后是对 t_rs t_rtt 的修改,我们同样也可以仿照 addu 指令和 subu 指令进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Controller
// 在 E 级的 ALU 中首次使用 rs 寄存器和 rt 寄存器
assign t_rs = (addu) ? 4'h1 :
(subu) ? 4'h1 :
...... :
(slt) ? 4'h1 : 4'hf;

assign t_rt = (addu) ? 4'h1 :
(subu) ? 4'h1 :
...... :
(slt) ? 4'h1 : 4'hf;

// 在 M 级能够开始转发要写入寄存器的值
assign t = (addu) ? 4'h2 :
(subu) ? 4'h2 :
...... :
(slt) ? 4'h2 : 4'hf;

到这里,聪明的你可能已经发现了,既然上面的步骤对于每条新指令都是必不可少的,那我们其实完全可以在考试前就在代码中准备好新指令的模板,在考试时填入数据就可以了!

这时候有人可能会问,每条指令的名称都各不相同,我们难不成还能在考试之前预测到新指令的名称吗?NONONO ,仔细想想,其实我们并不一定需要使用指令的名称作为变量名,只不过这种写法比较便于维护罢了。既然只是上机时临时加的一条指令,我们想规定它叫什么名字都可以,比如我的新指令总是叫 newb ,没有为什么,就是很 newb ~

(不过我们毕竟还是在书里,接下来的指令还是用指令名作为变量名,看得会更加清晰一点)

下一步就是在 ALU 模块中添加新运算了。这一步往往是计算类指令的难点,其中的计算逻辑可能非常复杂(当然 slt 指令的计算逻辑还算是非常简单)。不过好在只需要在 ALU 模块中完成,不需要处理阻塞和转发的问题,所以在所有类型的加指令问题中,计算类指令可以说是最简单的一种了:

1
2
3
4
5
6
7
8
9
10
// ALU
parameter SLT = 6'bXXXXXX;
wire [31:0] ABslt;
// 这里一定要注意使用 $signed 进行有符号比较!
// 比较结果为真返回 1 ,为假返回 0
assign ABslt = ($signed(inputA) < $signed(inputB));
assign outputA = (type == ADDU) ? AaddB :
(type == SUBU) ? AsubB :
...... :
(type == SLT) ? ABslt : none;

最后,我们再来检查一下,slt 指令不需要跳转,所以不需要修改 NPC 模块和 CMP 模块,同时也不涉及到新的阻塞和转发问题。于是,我们就如此丝滑地完成了一个新指令的添加!可喜可贺,可喜可贺!

【更难的计算类指令:clo】

接下来,我们来挑战一条难度稍高的计算类指令 cloclo 指令也是 MIPS 指令集中存在的一条指令,它的作用是计算 rs 寄存器中的数字有多少个先导 1 ,即从最高位开始有多少个连续的 1 ,下面是它的符号表示:

Code Block
temp ← 32
for i in 31 .. 0
    if GPR[rs]i = 0 then
        temp ← 31 - i
        break
    endif
endfor
GPR[rd] ← temp

可以看到 clo 的符号表示中出现了 for 循环,这让我们隐隐有一丝后背发凉,不过我们还是先完成 Controller 模块的改造,依旧是和 addu subu 指令非常相似(除了 clo 指令不需要用到 rt 寄存器),这里就不细说了:

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
// Controller
parameter CLO = 6'bXXXXXX;
wire clo;
assign clo = (OpCode == 6'bXXXXXX && Funct == 6'bXXXXXX) ? 1'b1 : 1'b0;

assign NPC_Sel = (j | jal) ? 2'b01 :
(jr | jalr) ? 2'b10 :
(beq | bne) ? 2'b11 : 2'b00;
assign RegWrite = (addu | subu | clo | addiu | xori | lui | lw | jal | jalr) ? 1'b1 : 1'b0;
assign EXTop = (addiu | lw | sw) ? 1'b1 : 1'b0;
assign ALUsrc = (addiu | xori | lui | lw | sw) ? 1'b1 : 1'b0;
assign RegDst = (addu | subu | clo | jalr) ? 1'b1 : 1'b0;
assign MemWrite = (sw) ? 1'b1 : 1'b0;
assign MemtoReg = (lw) ? 1'b1 : 1'b0;
assign PCtoReg = (jal | jalr) ? 1'b1 : 1'b0;
assign Regra = (jal) ? 1'b1 : 1'b0;
assign type = (addu) ? ADDU :
(subu) ? SUBU :
...... :
(clo) ? CLO : 6'b111111;

assign t_rs = (addu) ? 4'h1 :
(subu) ? 4'h1 :
...... :
(clo) ? 4'h1 : 4'hf;

// 注意 clo 指令不需要使用 rt 寄存器
assign t_rt = (addu) ? 4'h1 :
(subu) ? 4'h1 :
...... :
(clo) ? 4'hf : 4'hf;

assign t = (addu) ? 4'h2 :
(subu) ? 4'h2 :
...... :
(clo) ? 4'h2 : 4'hf;

接下来就是 ALU 模块的新计算了,尝试一下,你能用 for 循环写出 clo 指令的计算逻辑吗(注意,Verilog 中的 for 循环没有 break 跳出循环的语法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
reg [31:0] temp;
reg flag;
integer i;

always @(*) begin
temp = 32'h00000020;
flag = 1'b0;
// 注意 Verilog 中没有 i-- 的语法!
for (i = 31; i >= 0; i = i - 1) begin
if (flag == 1'b0 && inputA[i] == 1'b0) begin
temp = 31 - i;
flag = 1'b1;
end
end
end

parameter CLO = 6'bXXXXXX;
assign outputA = (type == ADDU) ? AaddB :
(type == SUBU) ? AsubB :
...... :
(type == CLO) ? temp : none;

由于 Verilog 的 for 循环中不存在 break 语句,所以我们需要对代码进行一些小改造,加入一个 flag 变量,手动实现跳过循环的效果。

如果不会用 Verilog 写怎么办?三步之内,必有解药!这个世界上就不存在不能用组合逻辑改写的 for 循环:

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
wire [31:0] temp;
assign temp = (inputA[31] == 1'b0) ? 32'h00000000 :
(inputA[31] == 1'b1 && inputA[30] == 1'b0) ? 32'h00000001 :
(inputA[30] == 1'b1 && inputA[29] == 1'b0) ? 32'h00000002 :
(inputA[29] == 1'b1 && inputA[28] == 1'b0) ? 32'h00000003 :
(inputA[28] == 1'b1 && inputA[27] == 1'b0) ? 32'h00000004 :
(inputA[27] == 1'b1 && inputA[26] == 1'b0) ? 32'h00000005 :
(inputA[26] == 1'b1 && inputA[25] == 1'b0) ? 32'h00000006 :
(inputA[25] == 1'b1 && inputA[24] == 1'b0) ? 32'h00000007 :
(inputA[24] == 1'b1 && inputA[23] == 1'b0) ? 32'h00000008 :
(inputA[23] == 1'b1 && inputA[22] == 1'b0) ? 32'h00000009 :
(inputA[22] == 1'b1 && inputA[21] == 1'b0) ? 32'h0000000a :
(inputA[21] == 1'b1 && inputA[20] == 1'b0) ? 32'h0000000b :
(inputA[20] == 1'b1 && inputA[19] == 1'b0) ? 32'h0000000c :
(inputA[19] == 1'b1 && inputA[18] == 1'b0) ? 32'h0000000d :
(inputA[18] == 1'b1 && inputA[17] == 1'b0) ? 32'h0000000e :
(inputA[17] == 1'b1 && inputA[16] == 1'b0) ? 32'h0000000f :
(inputA[16] == 1'b1 && inputA[15] == 1'b0) ? 32'h00000010 :
(inputA[15] == 1'b1 && inputA[14] == 1'b0) ? 32'h00000011 :
(inputA[14] == 1'b1 && inputA[13] == 1'b0) ? 32'h00000012 :
(inputA[13] == 1'b1 && inputA[12] == 1'b0) ? 32'h00000013 :
(inputA[12] == 1'b1 && inputA[11] == 1'b0) ? 32'h00000014 :
(inputA[11] == 1'b1 && inputA[10] == 1'b0) ? 32'h00000015 :
(inputA[10] == 1'b1 && inputA[9] == 1'b0) ? 32'h00000016 :
(inputA[9] == 1'b1 && inputA[8] == 1'b0) ? 32'h00000017 :
(inputA[8] == 1'b1 && inputA[7] == 1'b0) ? 32'h00000018 :
(inputA[7] == 1'b1 && inputA[6] == 1'b0) ? 32'h00000019 :
(inputA[6] == 1'b1 && inputA[5] == 1'b0) ? 32'h0000001a :
(inputA[5] == 1'b1 && inputA[4] == 1'b0) ? 32'h0000001b :
(inputA[4] == 1'b1 && inputA[3] == 1'b0) ? 32'h0000001c :
(inputA[3] == 1'b1 && inputA[2] == 1'b0) ? 32'h0000001d :
(inputA[2] == 1'b1 && inputA[1] == 1'b0) ? 32'h0000001e :
(inputA[1] == 1'b1 && inputA[0] == 1'b0) ? 32'h0000001f :
32'h00000020;

(实际上,这道题还有更优雅的位运算组合逻辑写法,有兴趣可以尝试一下)

千万不要嘲笑这种做法,当你被 Verilog 中一堆不存在的语法搞崩溃时,这种方法真的是暴力但好用!

这样,我们就完成了 clo 指令的添加。因为这两条指令都不涉及到额外的阻塞转发的问题,所以和单周期加指令也没什么区别,看起来实在是人畜无害。不过接下来的指令可能就要上一些强度了 ——

【跳过延迟槽:bgtzl】

接下来,我们来实现指令 bgtzlbgtz 指令我们都已经很熟了,就是 branch if greater than zero ,即当 rs 寄存器中的值大于 0 时进行跳转,否则不跳转。我们现在要实现的指令在此基础上多了一个 l ,指的是 likely ,意思是在不跳转的情况下,跳过延迟槽中的指令,从延迟槽指令的下一条指令继续执行。

bgtzl 指令也是 MIPS 指令集中存在的指令,它的符号语言如下:

Code Block
if GPR[rs] > 032 then
    PC ← PC + 4 + sign_extend(offset || 02)
else
    NullifyCurrentInstruction()
endif

题目中并没有给出跳过延迟槽指令的具体实现方法,而是用一个函数草草了事,需要我们自行分析实现的流程。

或许你会想到在执行 bgtzl 指令时,如果不满足跳转条件,则将 NPC 值从 PC + 4 改为 PC + 8 ,然而这种方法真的可行吗?

答案是否定的。因为此时 bgtzl 指令位于 D 级,而延迟槽中的指令已经位于 F 级,所以此时更改 NPC 的值,跳过的并不是延迟槽指令,而是延迟槽指令的下一条指令!

既然此时延迟槽指令已经位于 F 级,我们显然就没办法将其扼杀在 NPC 的摇篮中了,而是应该考虑将 F 级的延迟槽指令清除掉,或者说是替换为空泡指令 nop

聪明的你肯定想到了,仿照阻塞时的行为,对流水线寄存器和 PC 模块进行适当的关闭写使能、清空寄存器,即可实现产生 nop 指令的效果!于是经过一番思考,你得出结论:当 D 级的 bgtzl 指令产生跳过延迟槽信号(假设为 signal 信号)时,我们只需要在下一个时钟上升沿使用 clear 信号清空 FDreg 流水线寄存器即可:

(这里不同于阻塞信号,阻塞信号是插入 nop 指令,所以应该将插入位置之前的指令锁定住,即关闭写使能;这里是将延迟槽指令替换为 nop 指令,所以不需要关闭 PC 模块的写使能)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// CMP
module CMP(
input [31:0] inputA,
input [31:0] inputB,
input [5:0] type,
output zero,
output signal
);

parameter BEQ = 6'b000111,
BNE = 6'b001000,
BGTZL = 6'bXXXXXX;

// 对 bgtzl 使用有符号比较吧!
assign zero = ((type == BEQ) && (inputA == inputB)) ? 1'b1 :
((type == BNE) && (inputA != inputB)) ? 1'b1 :
((type == BGTZL) && ($signed(inputA) > $signed(32'h00000000))) ? 1'b1 : 1'b0;

assign signal = (type == BGTZL) && ($signed(inputA) <= $signed(32'h00000000)) ? 1'b1 : 1'b0;

endmodule
1
2
// mips
CMP CMP (.inputA(D_fixedRD1), .inputB(D_fixedRD2), .type(D_type), .zero(D_zero), .signal(signal));

这样就可以了吗?真的可以了吗?如果是这样的话,你还是太小看五级流水线 CPU 了!给个提示,假如 bgtzl 将要被阻塞在 D 级,同时产生了跳过延迟槽信号,在下个周期会出现什么情况呢?

如果阻塞信号 D_stall 和跳过延迟槽信号 signal 同时产生,那么 FDreg 流水线寄存器的使能端 en 会被关闭,同时清除信号 clear 也会开启。那么这两个信号谁的优先级比较高呢?不幸的是,clear 信号的优先级比较高。所以 FDreg 流水线寄存器会优先清除掉全部的内容,也就是会清除掉 bgtzl 指令本身,完成了一次壮烈的自我毁灭。

于是为了防止这种极其诡异的情况产生,我们规定当阻塞信号 D_stall 开启时,跳过延迟槽信号 signal 必须关闭:

1
2
3
4
// mips
wire fixedsignal;
assign fixedsignal = (signal & (~D_stall));
assign FD_clear = (fixedsignal) ? 1'b1 : 1'b0;

这样,我们就完成了 bgtzl 指令最核心的跳过延迟槽部分!随后,我们只需要补全 Controller 的规定流程即可:

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
parameter BGTZL = 6'bXXXXXX;
wire bgtzl;
assign bgtzl = (OpCode == 6'bXXXXXX) ? 1'b1 : 1'b0;

// 仿照 beq 和 bne 指令即可
assign NPC_Sel = (j | jal) ? 2'b01 :
(jr | jalr) ? 2'b10 :
(beq | bne | bgtzl) ? 2'b11 : 2'b00;
assign RegWrite = (addu | subu | addiu | xori | lui | lw | jal | jalr) ? 1'b1 : 1'b0;
assign EXTop = (addiu | lw | sw) ? 1'b1 : 1'b0;
assign ALUsrc = (addiu | xori | lui | lw | sw) ? 1'b1 : 1'b0;
assign RegDst = (addu | subu | jalr) ? 1'b1 : 1'b0;
assign MemWrite = (sw) ? 1'b1 : 1'b0;
assign MemtoReg = (lw) ? 1'b1 : 1'b0;
assign PCtoReg = (jal | jalr) ? 1'b1 : 1'b0;
assign Regra = (jal) ? 1'b1 : 1'b0;
assign type = (addu) ? ADDU :
(subu) ? SUBU :
...... :
(bgtzl) ? BGTZL : 6'b111111;

assign t_rs = (addu) ? 4'h1 :
...... :
(beq) ? 4'h0 :
...... :
(bgtzl) ? 4'h0 : 4'hf;

// 这里不要忘记了 bgtzl 不需要 rt 寄存器
assign t_rt = (addu) ? 4'h1 :
...... :
(beq) ? 4'h0 :
...... :
(bgtzl) ? 4'hf : 4'hf;

assign t = (addu) ? 4'h2 :
...... :
(beq) ? 4'hf :
...... :
(bgtzl) ? 4'hf : 4'hf;

这样,我们就搞定了 bgtzl 指令!有点上强度了,对不对?

【未定写使能:movz】

写还是不写,这是个问题。—— Kamonto 如是说道。

接下来,我们来看一种从未在课下出现过的奇葩指令 movz :当 rt 寄存器中的值等于 0 时,将 rs 寄存器中的值赋给 rd 寄存器;否则不进行任何赋值操作。

movz 依旧是 MIPS 指令集中的指令,其符号语言如下:

Code Block
if GPR[rt] = 0 then
    GPR[rd] ← GPR[rs]
endif

看似巨简单无比,不过它既然能出现在这里,肯定是有它的原因的(笑)。我们先按照正常流程来走一走,看看在哪一步会出现问题。首先我们修改 Controller 模块:

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
// Controller
parameter MOVZ = 6'bXXXXXX;
wire movz;
assign movz = (OpCode == 6'bXXXXXX && Funct == 6'bXXXXXX) ? 1'b1 : 1'b0;

assign NPC_Sel = (j | jal) ? 2'b01 :
(jr | jalr) ? 2'b10 :
(beq | bne) ? 2'b11 : 2'b00;
// 这里我们无法判断是否要写寄存器
assign RegWrite = (addu | subu | ??? | addiu | xori | lui | lw | jal | jalr) ? 1'b1 : 1'b0;
assign EXTop = (addiu | lw | sw) ? 1'b1 : 1'b0;
assign ALUsrc = (addiu | xori | lui | lw | sw) ? 1'b1 : 1'b0;
assign RegDst = (addu | subu | movz | jalr) ? 1'b1 : 1'b0;
assign MemWrite = (sw) ? 1'b1 : 1'b0;
assign MemtoReg = (lw) ? 1'b1 : 1'b0;
assign PCtoReg = (jal | jalr) ? 1'b1 : 1'b0;
assign Regra = (jal) ? 1'b1 : 1'b0;
assign type = (addu) ? ADDU :
(subu) ? SUBU :
...... :
(movz) ? MOVZ : 6'b111111;

// 注意这里 GPR[rs] 虽然在 ALU 模块中不需要进行运算,但我们并没有转发到 M 级 RD1(或者说 outputA )的数据通路,所以 t_rs 只能填写 E 级
assign t_rs = (addu) ? 4'h1 :
(subu) ? 4'h1 :
...... :
(movz) ? 4'h1 : 4'hf;

// GPR[rt] 在 E 级的 ALU 模块中进行与 0 的比较,其实在 CMP 模块中比较也可以,但 t_rt 值大一些有利于减少阻塞
assign t_rt = (addu) ? 4'h1 :
(subu) ? 4'h1 :
...... :
(movz) ? 4'h1 : 4'hf;

// 这里我们同样无法判断是否会产生写入寄存器的值
assign t = (addu) ? 4'h2 :
(subu) ? 4'h2 :
...... :
(movz) ? ??? : 4'hf;

可以看到,我们在修改 Controller 模块时遇到了两个问题,RegWrite 信号和 t 值均无法通过指令名直接确定,而是需要我们计算才能够得出,所以我们需要从 ALU 模块中传出一个计算值,动态地修改这两个值。

(关于 t 值是否需要动态修改的问题,如果在不写寄存器的情况下不将 t 值置为 0xf ,那么就有可能会将 GPR[rs] 的值转发给需要读 rd 寄存器的指令,然而我们实际上并不会将 GPR[rs] 写入 rd 寄存器,这就出现了错误,所以必须动态修改 t 值!)

在 ALU 模块中,我们新开一个 1 位的输出信号 signal ,当不满足写入条件时,signal 信号置 1 ,随流水线一直流水到 W 级,直到 movz 指令执行完毕。当 signal 信号为 1 时,当前指令的 t 值将被修改为 0xf ,当指令位于 W 级时,RegWrite 会被关闭。

(这里又涉及到老生常谈的新信号适配之前指令的问题了。如果我们的设计是满足写入条件时,signal 信号置 1 ,那就要为每个写寄存器的指令都加上恒为 1 的 signal 信号,否则之前的其它所有指令都会因为 signal 没有置 1 而被关闭 GRF 模块的写使能!)

于是,我们的 Controller 模块中就可以按满足赋值条件的情况进行设置,当不满足赋值条件时,由 signal 信号进行动态修改:

1
2
3
4
5
6
7
// Controller
assign RegWrite = (addu | subu | movz | addiu | xori | lui | lw | jal | jalr) ? 1'b1 : 1'b0;

assign t = (addu) ? 4'h2 :
(subu) ? 4'h2 :
...... :
(movz) ? 4'h2 : 4'hf;
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
// ALU
module ALU(
input [31:0] inputA,
input [31:0] inputB,
input [5:0] type,
output [31:0] outputA,
output signal
);

parameter ADDU = 6'b000000,
SUBU = 6'b000001,
......,
MOVZ = 6'bXXXXXX;

......

assign outputA = (type == ADDU) ? AaddB :
(type == SUBU) ? AsubB :
...... :
(type == MOVZ) ? inputA : none;

// movz 指令不满足跳转条件的情况
assign signal = (type == MOVZ && inputB != 32'h00000000) ? 1'b1 : 1'b0;

endmodule
1
2
// mips
ALU ALU (.inputA(E_inputA), .inputB(E_inputB), .type(E_type), .outputA(E_outputA), .signal(signal));

接下来,我们为 EMreg 和 MWreg 流水线寄存器添加 signal 的流水(这一步太简单了,不想写代码了)。

在 W 级,我们修改 GRF 模块的写使能,当 signal 信号为 1 时,RegWrite 关闭:

1
2
3
4
5
6
// mips
wire W_fixedRegWrite;
assign W_fixedRegWrite = (RegWrite & (~W_signal));

GRF GRF (.clk(clk), .reset(reset), .WE(W_fixedRegWrite), .A1(D_A1), .A2(D_A2),
.A3(W_A3), .WD(W_WD_Reg), .RD1(D_RD1), .RD2(D_RD2));

在 E 级、M 级、W 级 t_new 值的计算中,当该级的 signal 为 1 时,t_new 值修改为 0xf

1
2
3
4
5
6
7
8
9
10
11
12
// mips
assign E_t_new = (E_signal) ? 4'hf :
(E_t == 4'hf) ? 4'hf :
(E_t >= 4'h1) ? (E_t - 4'h1) : 4'h0;

assign M_t_new = (M_signal) ? 4'hf :
(M_t == 4'hf) ? 4'hf :
(M_t >= 4'h2) ? (M_t - 4'h2) : 4'h0;

assign W_t_new = (W_signal) ? 4'hf :
(W_t == 4'hf) ? 4'hf :
(W_t >= 4'h3) ? (W_t - 4'h3) : 4'h0;

这样,我们就完成了这个“简单”的指令。是不是非常意外?这就是五级流水线的复杂之处!

偷偷告诉你,其实这个题有一个偷鸡的做法,那就是检测到 signal 之后将 A3 直接改为 0 号寄存器。管你 RegWritet 是多少,0 号寄存器,吸收两米以下的氮磷钾,不写入、不阻塞、不转发!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// mips
wire [4:0] E_fixedA3;
assign E_fixedA3 = signal ? 5'b00000 : E_A3;

EMreg EMreg (.clk(clk), .reset(reset), .EM_en(EM_en), .EM_clear(EM_clear),
.E_Instr(E_Instr), .E_PCplus8(E_PCplus8), .E_PC(E_PC),
.E_outputA(E_outputA), .E_RD2(E_fixedRD2), .E_A3(E_fixedA3),
.M_Instr(M_Instr), .M_PCplus8(M_PCplus8), .M_PC(M_PC),
.M_outputA(M_outputA), .M_RD2(M_RD2), .M_A3(M_A3));

assign D_stall = (E_fixedA3 != 5'b00000 && D_rs == E_fixedA3 && D_t_rsuse != 4'hf && E_t_new != 4'hf && D_t_rsuse < E_t_new) ? 1'b1 :
(E_fixedA3 != 5'b00000 && D_rt == E_fixedA3 && 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;

直接秒了,我还是推荐大家这么做的,非常爽。

实际上,上机考试中更容易出的一种题目是某种跳转指令,如果满足跳转条件,就将 PC + 8 写入 $ra 寄存器中;如果不满足跳转条件,就完全不写。不过我认真的研读了 MIPS 指令集,发现貌似并没有这样的指令,所以选了一个思路相同的指令。

简单讲一下上面题目的思路,在 CMP 模块中输出 signal 信号,如果 signal 信号为 1 ,则将 D_A3 改为 0 号寄存器就可以了~(注意不要忘记填写 isPCplus8 !)

【未定寄存器:lwtxr】

lwtxr 指令是一个完全虚构的指令,虽然这种类型的指令完全不存在于 MIPS 指令集中(大概吧),但却是每次上机几乎都必有的题目。

lwtxr 指令会像 lw 指令一样从内存中取出一个字,但是写入的寄存器有所不同,记取出的字的最低 5 位为 temp ,取 temprt 中的较大者作为写入的寄存器编号。它的符号语言如下:

Code Block
addr ← GPR[base] + sign_ext(offset) 
data ← memory[addr]
temp ← data4:0
target ← (temp > rt) ? temp : rt
GPR[target] ← data

不知道第一位想出这种指令的助教是何等的天才,之前的 movz 指令还是在写和不写之间纠结,现在的 lwtxr 指令已经上升到了根本不知道写哪个寄存器的层次!

既然真的是这样,那问题或许也好办了。如果我完全不知道我要写哪个寄存器,那除非后面一条指令是 j 指令或者 jal 指令,或者要读的寄存器全都是 0 号寄存器的指令,否则只要是需要读寄存器的指令,统统都阻塞在 D 级就好了!等到在 M 级计算出 M_A3 的值,在 M 级再进行转发:

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
// mips
wire [31:0] M_fixedA3;
assign M_fixedA3 = (M_type == LWTXR && M_data[4:0] > M_A3) ? M_data[4:0] : M_A3;

MWreg MWreg (.clk(clk), .reset(reset), .MW_en(MW_en), .MW_clear(MW_clear),
.M_Instr(M_Instr), .M_PCplus8(M_PCplus8), .M_PC(M_PC),
.M_A3(M_fixedA3), .M_outputA(M_outputA), .M_data(M_data),
.W_Instr(W_Instr), .W_PCplus8(W_PCplus8), .W_PC(W_PC),
.W_A3(W_A3), .W_outputA(W_outputA), .W_data(W_data));

// forward
// 新指令不属于 isPCplus8 指令,不需要更改这部分的转发
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;

assign D_RD1_from_M_PCplus8 = (M_isPCplus8 && 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 : 1'b0;
assign D_RD2_from_M_PCplus8 = (M_isPCplus8 && 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 : 1'b0;
assign E_RD1_from_M_PCplus8 = (M_isPCplus8 && M_A3 != 5'b00000 && E_rs == M_A3 && E_t_rsuse != 4'hf && M_t_new != 4'hf && E_t_rsuse >= M_t_new) ? 1'b1 : 1'b0;
assign E_RD2_from_M_PCplus8 = (M_isPCplus8 && M_A3 != 5'b00000 && E_rt == M_A3 && E_t_rtuse != 4'hf && M_t_new != 4'hf && E_t_rtuse >= M_t_new) ? 1'b1 : 1'b0;

// 如果 M 级指令是 lwtxr 指令,需要使用修正过后的 A3 值
assign D_RD1_from_M = (~M_isPCplus8 && M_fixedA3 != 5'b00000 && D_rs == M_fixedA3 && D_t_rsuse != 4'hf && M_t_new != 4'hf && D_t_rsuse >= M_t_new) ? 1'b1 : 1'b0;
assign D_RD2_from_M = (~M_isPCplus8 && M_fixedA3 != 5'b00000 && D_rt == M_fixedA3 && D_t_rtuse != 4'hf && M_t_new != 4'hf && D_t_rtuse >= M_t_new) ? 1'b1 : 1'b0;
assign E_RD1_from_M = (~M_isPCplus8 && M_fixedA3 != 5'b00000 && E_rs == M_fixedA3 && E_t_rsuse != 4'hf && M_t_new != 4'hf && E_t_rsuse >= M_t_new) ? 1'b1 : 1'b0;
assign E_RD2_from_M = (~M_isPCplus8 && M_fixedA3 != 5'b00000 && E_rt == M_fixedA3 && E_t_rtuse != 4'hf && M_t_new != 4'hf && E_t_rtuse >= M_t_new) ? 1'b1 : 1'b0;

assign D_RD1_from_W = (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 : 1'b0;
assign D_RD2_from_W = (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;
assign E_RD1_from_W = (W_A3 != 5'b00000 && E_rs == W_A3 && E_t_rsuse != 4'hf && W_t_new != 4'hf && E_t_rsuse >= W_t_new) ? 1'b1 : 1'b0;
assign E_RD2_from_W = (W_A3 != 5'b00000 && E_rt == W_A3 && E_t_rtuse != 4'hf && W_t_new != 4'hf && E_t_rtuse >= W_t_new) ? 1'b1 : 1'b0;
assign M_RD2_from_W = (W_A3 != 5'b00000 && M_rt == W_A3 && M_t_rtuse != 4'hf && W_t_new != 4'hf && M_t_rtuse >= W_t_new) ? 1'b1 : 1'b0;

// stall
// 如果 E 级指令是 lwtxr 指令,直接阻塞
// 如果 M 级指令是 lwtxr 指令,需要使用修正过后的 A3 值
assign D_stall = (E_type == LWTXR) ? 1'b1 :
(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_fixedA3 != 5'b00000 && D_rs == M_fixedA3 && D_t_rsuse != 4'hf && M_t_new != 4'hf && D_t_rsuse < M_t_new) ? 1'b1 :
(M_fixedA3 != 5'b00000 && D_rt == M_fixedA3 && 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;

然后恭喜你,你通过了 19 / 20 个点,有一个测试点 TLE 了!这种类型的题目,特点就是疯狂卡 TLE ,只要稍微多阻塞几次,直接自动爆炸!

什么?难道还有更少的阻塞方式吗?这就需要我们仔细看题目了。题目中要求写入的寄存器是 temprt 中的较大值,而 rt 的值是我们一开始就知道的。也就是说,我们虽然不能够精准选出写入的寄存器,但我们能够肯定的是,只要是编号比 rt 更小的寄存器,都不可能会是写入的寄存器!

(当然,也有题目是所有寄存器都可能会写到,那就像上面那样写就可以了,实在不行就加个 D 级指令不读寄存器的情况的特判)

所以我们在 lwtxr 指令位于 E 级时,不能够无脑阻塞,而是只有当 D 级指令要读的寄存器可能是要写的寄存器是,才会进行阻塞,也就是像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
// mips
// stall
// 如果 E 级指令是 lwtxr 指令,则当 D_rs 不是 0 号寄存器,且 D_t_rsuse 存在,且 D_rs 大于等于 E_rt(即 E_A3 )时才阻塞
// 需要注意在 E 级指令不是 lwtxr 指令时,在判断条件前加上 E_type != LWTXR ,避免被上一条判断漏下来的 lwtxr 指令进入接下来的判断!
// 如果 M 级指令是 lwtxr 指令,需要使用修正过后的 A3 值
assign D_stall = (E_type == LWTXR && D_rs != 5'b00000 && D_t_rsuse != 4'hf && D_rs >= E_A3) ? 1'b1 :
(E_type == LWTXR && D_rt != 5'b00000 && D_t_rtuse != 4'hf && D_rt >= E_A3) ? 1'b1 :
(E_type != LWTXR && 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_type != LWTXR && 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_fixedA3 != 5'b00000 && D_rs == M_fixedA3 && D_t_rsuse != 4'hf && M_t_new != 4'hf && D_t_rsuse < M_t_new) ? 1'b1 :
(M_fixedA3 != 5'b00000 && D_rt == M_fixedA3 && 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;

这样,我们就完成了 lwtxr 指令中最为困难的部分,这也提示我们在上机时遇到同种类型的指令时,一定要思考是否有哪些寄存器一定不会作为被赋值的寄存器,在 E 级阻塞时将它们排除掉即可。

接下来我们就来照例补全一下 lwtxr 指令的 Controller 模块和 ALU 模块吧!不说话,直接放代码:

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
// Controller
parameter LWTXR = 6'bXXXXXX;
wire lwtxr;
assign lwtxr = (OpCode == 6'bXXXXXX) ? 1'b1 : 1'b0;

// 仿照 lw 指令进行填写即可
assign NPC_Sel = (j | jal) ? 2'b01 :
(jr | jalr) ? 2'b10 :
(beq | bne) ? 2'b11 : 2'b00;
assign RegWrite = (addu | subu | addiu | xori | lui | lw | lwtxr | jal | jalr) ? 1'b1 : 1'b0;
assign EXTop = (addiu | lw | lwtxr | sw) ? 1'b1 : 1'b0;
assign ALUsrc = (addiu | xori | lui | lw | lwtxr | sw) ? 1'b1 : 1'b0;
assign RegDst = (addu | subu | jalr) ? 1'b1 : 1'b0;
assign MemWrite = (sw) ? 1'b1 : 1'b0;
assign MemtoReg = (lw | lwtxr) ? 1'b1 : 1'b0;
assign PCtoReg = (jal | jalr) ? 1'b1 : 1'b0;
assign Regra = (jal) ? 1'b1 : 1'b0;
assign type = (addu) ? ADDU :
(subu) ? SUBU :
...... :
(lwtxr) ? LWTXR : 6'b111111;

assign t_rs = (addu) ? 4'h1 :
...... :
(lw) ? 4'h1 :
...... :
(lwtxr) ? 4'h1 : 4'hf;

assign t_rt = (addu) ? 4'h1 :
...... :
(lw) ? 4'hf :
...... :
(lwtxr) ? 4'hf : 4'hf;

// 这里能够转发要写入的数据的时间是固定的,只不过不知道要写入哪个寄存器而已
assign t = (addu) ? 4'h2 :
...... :
(lw) ? 4'h3 :
...... :
(lwtxr) ? 4'h3 : 4'hf;
1
2
3
4
5
6
7
// ALU
parameter LWTXR = 6'bXXXXXX;
assign outputA = (type == ADDU) ? AaddB :
...... :
(type == LW) ? AaddB :
...... :
(type == LWTXR) ? AaddB : none;

至此,我们就学会了 P5 和 P6 中最常考的几种题型,尤其是后几种题型中对阻塞和转发的改造,在考试的高压环境下很难全部想出来,所以最好多看几遍,理解其中的思路和原理,争取在考到的时候也能够快速写出!

5.6 五级流水线 CPU 大升级:新指令与新模块

在本节,我们将会对我们的五级流水线 CPU 进行一些有趣的小升级,通过加入一些新指令和新模块实现更多的功能!果然还是这种随时可以看到成果的小任务最受大家欢迎了!(打个广告,所以快来选择 OS 的 Shell 挑战性任务吧!)

【实现按半字、字节访存】

在这一部分,我们来加入几条新的指令:sh lh sb lb 指令,用于实现对内存按半字和字节的访存。

我们先来快速过一下加指令的基本流程:首先修改 Controller 模块,解析指令,添加合适的选择信号,设置 t_rs t_rt t 接口的值;接下来是在 CMP 模块和 ALU 模块中加入合适的运算,显然对于这几条指令来说,CMP 模块是不需要改动的,我们只需要修改 ALU 模块即可。

聪明的你一定想到了,上面的所有流程中,除了 Controller 模块中的解析指令以外,其它的所有流程,sh 指令和 sb 指令都与 sw 指令相同,lh 指令和 lb 指令都与 lw 指令相同。这就非常奇怪了,明明是不同的指令,执行流程难不成是相同的吗?

实际上,为了能够区别执行这几条指令,我们需要对 DM 模块进行一些微调,并且在 DM 模块的前后进行一些预处理和再加工。

我们首先来看比较简单的 lh 指令和 lb 指令。我们知道,我们的 DM 模块在取内存时是非常简单粗暴的,将 GPR[rs] + signed_ext(imm16) 直接右移两位,取出相对应的一整个字,也就是 4 个字节。

这种方法对于最低两位必须是 00 的 lw 指令非常好用,然而对于 lh 指令和 lb 指令,DM 模块也只能够取出 要取出的半字或字节 所在的一整个字。例如在下面的地址中:

0x0 - 0x3 0x4 - 0x7 0x8 - 0xb 0xc - 0xf
0x12345678 0x00abcdef 0x00114514 0x01919810

我们使用 lb $t0, 5($0) 指令,想取出地址为 0x5 的 0xcd 字节,将 0x000000cd 赋值给 $t0 寄存器。然而 DM 模块经过运算得出 5 >> 2 的值为 1 ,于是只能取出编号为 1 的字,也就是 0x00abcdef 一整个字,作为输出结果。

所以我们就需要将取出的整个字进行再加工,根据指令类型和地址最低两位,计算出正确的读内存结果。我们设置一个新的模块进行这个操作,取名为 DE 模块:

1
2
3
4
5
6
7
8
module DE(
input [31:0] addr,
input [31:0] data,
input [4:0] type,
output [31:0] fixed_data
);

endmodule

在了解了预期输入和输出之后,DE 模块的逻辑就不难理解了,总结来说可以写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if 指令为 lw then
fixed_data = data
else if 指令为 lh and addr[1] == 1'b0 then
fixed_data = signed_ext(data[15:0])
else if 指令为 lh and addr[1] == 1'b1 then
fixed_data = signed_ext(data[31:16])
else if 指令为 lb and addr[1:0] == 2'b00 then
fixed_data = signed_ext(data[7:0])
else if 指令为 lb and addr[1:0] == 2'b01 then
fixed_data = signed_ext(data[15:8])
else if 指令为 lb and addr[1:0] == 2'b10 then
fixed_data = signed_ext(data[23:16])
else if 指令为 lb and addr[1:0] == 2'b11 then
fixed_data = signed_ext(data[31:24])

根据这个逻辑,我们就不难写出相应的代码了:

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
module DE(
input [31:0] addr,
input [31:0] data,
input [5:0] type,
output [31:0] fixed_data
);

// 这里和 Controller 中 type 的定义一致即可
parameter LW = 6'b000101,
LH = 6'b001101,
LB = 6'b001110;

wire [15:0] half_data;
wire [7:0] byte_data;

assign half_data = (type == LH && addr[1] == 1'b0) ? data[15:0] :
(type == LH && addr[1] == 1'b1) ? data[31:16] : 16'h0000;

assign byte_data = (type == LB && addr[1:0] == 2'b00) ? data[7:0] :
(type == LB && addr[1:0] == 2'b01) ? data[15:8] :
(type == LB && addr[1:0] == 2'b10) ? data[23:16] :
(type == LB && addr[1:0] == 2'b11) ? data[31:24] : 8'h00;

assign fixed_data = (type == LW) ? data :
(type == LH) ? {{16{half_data[15]}}, half_data} :
(type == LB) ? {{24{byte_data[7]}}, byte_data} : data;

endmodule

接下来,我们来实现相对复杂一些的 sh 指令和 sb 指令。和之前的问题相同,我们目前的 DM 模块只支持一次性存入一整个字,所以如何向内存中存入一个半字或一个字节,同时又不破坏字内其它部分,就成了一个比较棘手的问题。例如还是之前的地址:

0x0 - 0x3 0x4 - 0x7 0x8 - 0xb 0xc - 0xf
0x12345678 0x00abcdef 0x00114514 0x01919810

假设我想使用 sb $t0, 5($0) 指令将 $t0 寄存器中的值 0x00000012 存入内存中,期望地址为 0x4 - 0x7 的字变为 0x00ab12ef 。但我们的 DM 模块会直接将整个寄存器中的值全部存入编号为 1 的字中,整个字就变成了 0x00000012 ,连最基本的 0x12 字节的位置都错了。我们进行一些预处理,在存入前将数据调整为 0x00001200 ,再存入内存中,虽然 0x12 字节的位置正确了,但 0xab0xef 两个字节却被覆盖掉了,依然没有得到正确结果。

针对这种问题,有一种比较直观的解决方案,那就是先将 0x00abcdef 整个字取出来,与调整过的数据 0x00001200 嵌合成 0x00ab12ef 之后,再将整个字写回内存。不过这种方法实际上问题很大,在一个周期内进行了一次读内存操作,又进行了一次写内存操作,会极大拖慢 CPU 的执行效率。(当然我们实际上不需要考虑 CPU 的效率问题,其实不让这么写的主要原因是后面在做内存外置的时候接口会对不上,笑)

所以我们选取另外一种解决方案,那就是改造 DM 模块的写内存方式。我们将原来的写使能信号 MemWrite 一分为四,为一个字中的每个字节都设置一个写使能信号。在上面的例子中,我们输入预处理后的数据 0x00001200 和写使能信号 0b0010 ,于是 0x00abcdef 中的第 0 、2 、3 字节保持不变,只将 0x00001200 的第 1 个字节写入 0x00abcdef 的第 1 个字节中,得到了 0x00ab12ef 的结果。

首先,我们改造一下 DM 模块,为每个字节添加写使能信号:

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
module DM(
input clk,
input reset,
// 写使能由 1 位变为 4 位
input [3:0] WE,
input [31:0] Addr,
input [31:0] WD,
output [31:0] Data
);

reg [31:0] RAM [0:3071];

wire [31:0] RAM_Addr;

integer i;

assign RAM_Addr = (Addr >> 2);

always@(posedge clk) begin
if (reset) begin
for (i = 0; i < 3072; i = i + 1) begin
RAM[i] = 32'h00000000;
end
end
else begin
if (WE[0] == 1'b1) begin
RAM[RAM_Addr][7:0] <= WD[7:0];
end
if (WE[1] == 1'b1) begin
RAM[RAM_Addr][15:8] <= WD[15:8];
end
if (WE[2] == 1'b1) begin
RAM[RAM_Addr][23:16] <= WD[23:16];
end
if (WE[3] == 1'b1) begin
RAM[RAM_Addr][31:24] <= WD[31:24];
end
end
end

assign Data = RAM[RAM_Addr];

endmodule

接下来,我们来完成数据的预处理和写使能生成的部分,我们将这一部分也整合成模块,取名为 BE 模块:

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
module BE(
input [31:0] addr,
input [31:0] data,
input [5:0] type,
output [3:0] MemByteWrite,
output [31:0] fixed_data
);

// 依旧是 Controller 中 type 的定义一致即可
parameter SW = 6'b000110,
SH = 6'b001111,
SB = 6'b010000;

assign MemByteWrite = (type == SW) ? 4'b1111 :
(type == SH && addr[1] == 1'b0) ? 4'b0011 :
(type == SH && addr[1] == 1'b1) ? 4'b1100 :
(type == SB && addr[1:0] == 2'b00) ? 4'b0001 :
(type == SB && addr[1:0] == 2'b01) ? 4'b0010 :
(type == SB && addr[1:0] == 2'b10) ? 4'b0100 :
(type == SB && addr[1:0] == 2'b11) ? 4'b1000 : 4'b0000;

assign fixed_data = (type == SW) ? data :
(type == SH && addr[1] == 1'b0) ? {16'h0000, data[15:0]} :
(type == SH && addr[1] == 1'b1) ? {data[15:0], 16'h0000} :
(type == SB && addr[1:0] == 2'b00) ? {24'h000000, data[7:0]} :
(type == SB && addr[1:0] == 2'b01) ? {16'h0000, data[7:0], 8'h00} :
(type == SB && addr[1:0] == 2'b10) ? {8'h00, data[7:0], 16'h0000} :
(type == SB && addr[1:0] == 2'b11) ? {data[7:0], 24'h000000} : data;

endmodule

到这里,我们就已经完成了绝大部分的工作,接下来就是走一下 Controller 模块和 ALU 模块的固定流程了:

(当然了,既然 BE 模块已经能够输出 DM 模块的写使能,那么 MemWrite 信号就不需要 Controller 模块代劳了,从中删去即可)

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
// Controller
parameter ADDU = 6'b000000,
......,
LW = 6'b000101,
SW = 6'b000110,
......,
LH = 6'b001101,
LB = 6'b001110,
SH = 6'b001111,
SB = 6'b010000;

wire lh;
wire lb;
wire sh;
wire sb;

assign lh = (OpCode == 6'b100001) ? 1'b1 : 1'b0;
assign lb = (OpCode == 6'b100000) ? 1'b1 : 1'b0;
assign sh = (OpCode == 6'b101001) ? 1'b1 : 1'b0;
assign sb = (OpCode == 6'b101000) ? 1'b1 : 1'b0;

// 仿照 lw 和 sw 指令进行填写即可
// 这里注意删掉 MemWrite 选择信号
assign NPC_Sel = (j | jal) ? 2'b01 :
(jr | jalr) ? 2'b10 :
(beq | bne) ? 2'b11 : 2'b00;
assign RegWrite = (addu | subu | addiu | xori | lui | lw | lh | lb | jal | jalr) ? 1'b1 : 1'b0;
assign EXTop = (addiu | lw | lh | lb | sw | sh | sb) ? 1'b1 : 1'b0;
assign ALUsrc = (addiu | xori | lui | lw | lh | lb | sw | sh | sb) ? 1'b1 : 1'b0;
assign RegDst = (addu | subu | jalr) ? 1'b1 : 1'b0;
assign MemtoReg = (lw | lh | lb) ? 1'b1 : 1'b0;
assign PCtoReg = (jal | jalr) ? 1'b1 : 1'b0;
assign Regra = (jal) ? 1'b1 : 1'b0;
assign type = (addu) ? ADDU :
(subu) ? SUBU :
...... :
(lh) ? LH :
(lb) ? LB :
(sh) ? SH :
(sb) ? SB : 6'b111111;

assign t_rs = (addu) ? 4'h1 :
...... :
(lw) ? 4'h1 :
(sw) ? 4'h1 :
...... :
(lh) ? 4'h1 :
(lb) ? 4'h1 :
(sh) ? 4'h1 :
(sb) ? 4'h1 : 4'hf;

assign t_rt = (addu) ? 4'h1 :
...... :
(lw) ? 4'hf :
(sw) ? 4'h2 :
...... :
(lh) ? 4'hf :
(lb) ? 4'hf :
(sh) ? 4'h2 :
(sb) ? 4'h2 : 4'hf;

assign t = (addu) ? 4'h2 :
...... :
(lw) ? 4'h3 :
(sw) ? 4'hf :
...... :
(lh) ? 4'h3 :
(lb) ? 4'h3 :
(sh) ? 4'hf :
(sb) ? 4'hf : 4'hf;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ALU
parameter ADDU = 6'b000000,
......,
LW = 6'b000101,
SW = 6'b000110,
......,
LH = 6'b001101,
LB = 6'b001110,
SH = 6'b001111,
SB = 6'b010000;
assign outputA = (type == ADDU) ? AaddB :
...... :
(type == LW) ? AaddB :
(type == SW) ? AaddB :
...... :
(type == LH) ? AaddB :
(type == LB) ? AaddB :
(type == SH) ? AaddB :
(type == SB) ? AaddB : none;

最后,我们在主模块中声明 DE 和 BE 模块,修改相应接口即可:

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
module mips(
input clk,
input reset
);

// M zone ////////////////////////////////
wire [31:0] M_fixedWD_Mem;
wire [31:0] M_fixeddata;
wire [3:0] M_MemByteWrite;

// M zone ////////////////////////////////
// 删除 Controller 中 MemWrite 的定义
Controller MController (.OpCode(M_OpCode), .Funct(M_Funct), .type(M_type),
.t_rs(M_t_rs), .t_rt(M_t_rt), .t(M_t));
BE BE (.addr(M_addr), .data(M_WD_Mem), .type(M_type), .MemByteWrite(M_MemByteWrite),
.fixed_data(M_fixedWD_Mem));
DM DM (.clk(clk), .reset(reset), .WE(M_MemByteWrite), .addr(M_addr), .WD(M_fixedWD_Mem),
.data(M_data));
DE DE (.addr(M_addr), .data(M_data), .type(M_type), .fixed_data(M_fixeddata));
MWreg MWreg (.clk(clk), .reset(reset), .MW_en(MW_en), .MW_clear(MW_clear),
.M_Instr(M_Instr), .M_PCplus8(M_PCplus8), .M_PC(M_PC),
.M_A3(M_A3), .M_outputA(M_outputA), .M_data(M_fixeddata),
.W_Instr(W_Instr), .W_PCplus8(W_PCplus8), .W_PC(W_PC),
.W_A3(W_A3), .W_outputA(W_outputA), .W_data(W_data));
endmodule

这样,我们就完成了 sh lh sb lb 指令的添加!完成了一个小任务,真的很有成就感!

【内存外置】

其实直到现在,我们的 CPU 的设计都存在着一个很明显的 bug 。如果你理论知识学得还不错的话,可能就会产生这样一个问题:内存是 CPU 的一部分吗?

所以你现在知道了,内存其实并不是 CPU 的一部分。我们之前实现的其实并不只是一个 CPU ,而是 CPU 和内存的结合体。现在,为了维护 CPU 的独立性,我们忍痛割爱,将内存部分从我们的设计中踢出去,只留下一些接口藕断丝连:

1
2
3
4
5
6
7
8
9
10
11
12
module mips(
input clk,
input reset,
input [31:0] i_inst_rdata,
input [31:0] m_data_rdata,
output [31:0] i_inst_addr,
output [31:0] m_data_addr,
output [31:0] m_data_wdata,
output [3:0] m_data_byteen
);

endmodule

我们可以看到主模块中多出了 6 个接口,这些都是曾经连接在 IM 模块和 DM 模块上的导线。在这两个模块被发配至主模块外以后,我们就需要通过 output 端口向两个模块发送数据,从 input 端口接受两个模块传回的信息。

当然,欲练此功,必先 XX ,我们要做的第一步就是删除 IM 模块和 DM 模块:

Picture Not Found!
(当然,不要忘了删除主模块中 IM 模块和 DM 模块的引用~)

接下来,我们将原本接在两个模块接口上的导线,改接到主模块的接口:

IM 模块的改接相对来说比较简单,我们将原本的输入端 addr 改接到 output 端口 i_inst_addr ,将原本的输出端 data 改接到 input 端口 i_inst_rdata 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module mips(
input clk,
input reset,
input [31:0] i_inst_rdata,
input [31:0] m_data_rdata,
output [31:0] i_inst_addr,
output [31:0] m_data_addr,
output [31:0] m_data_wdata,
output [3:0] m_data_byteen
);

// 注意这里等号两边千万不能写反了,要想清楚数据是由谁传给谁
assign i_inst_addr = F_PC;
assign F_Instr = i_inst_rdata;

endmodule

DM 模块因为有读写两个部分,所以稍显复杂,不过问题不大,我们只要搞清楚新的输入输出端口对应原来 DM 模块的哪个接口,就可以无痛完成改接:原本的地址输入端 addr 改接至 m_data_addr ,写入数据输入端 WD 改接至 m_data_wdata ,写使能输入端 MemByteWrite 改接至 m_data_byteen ,读取数据输出端 data 改接至 m_data_rdata

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module mips(
input clk,
input reset,
input [31:0] i_inst_rdata,
input [31:0] m_data_rdata,
output [31:0] i_inst_addr,
output [31:0] m_data_addr,
output [31:0] m_data_wdata,
output [3:0] m_data_byteen
);

assign m_data_addr = M_addr;
assign m_data_wdata = M_fixedWD_Mem;
assign m_data_byteen = M_MemByteWrite;
assign M_data = m_data_rdata;

endmodule

这样,我们不费吹灰之力就完成了内存的外置。在之后我们还会遇到更多的外置模块,其实并没有我们想象中的那么复杂,只需要将对应的数据通过输入输出端口进行传输就可以了!

【实现乘除法指令】

接下来等待着我们的,是 P6 的大型规则怪谈 —— 乘除法指令~

我们要实现的乘除法指令一共有 8 条,分别是 mult multu div divu mfhi mflo mthi mtlo 指令。前 4 条指令负责计算乘除法,将结果存入 HI 寄存器和 LO 寄存器中;后 4 条指令负责读取和写入 HI 寄存器和 LO 寄存器。

事实上,如果想直接使用上一节加指令的流程来实现这 8 条指令的话,肯定是不太行的,毕竟我们目前的 CPU 中并没有 HILO 这两个寄存器,所以当然还需要加入一些奇妙的模块才能拿下这几条指令!

于是我们来了解一个新的模块 —— MDU 模块!MDU 模块位于 E 级,和 ALU 模块并列。实际上,MDU 模块有点像 ALU 模块和 GRF 模块的结合体。一方面,它需要像 ALU 磨矿一样输入两个值 inputAinputB 进行计算;另一方面,它又包含两个寄存器 HILO ,所以需要像 GRF 模块一样支持读取和写入这两个寄存器。

那么接下来,我们就来设计一下这个新出炉的 MDU 模块:

首先是计算部分,我们根据不同指令的 type 值,对 inputAinputB 进行不同的运算。和 ALU 指令稍微有些不同的地方是,我们并不需要将运算的结果输出,而是在当前时钟上升沿将结果存入 HILO 寄存器中:

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
module MDU(
input clk,
input reset,
input [31:0] inputA,
input [31:0] inputB,
input [5:0] type
);

reg [31:0] HI;
reg [31:0] LO;

parameter MULT = 6'b010001,
MULTU = 6'b010010,
DIV = 6'b010011,
DIVU = 6'b010100;

always@(posedge clk) begin
if (reset) begin
HI <= 32'h00000000;
LO <= 32'h00000000;
end
else begin
// 有符号运算一定要记得加 $signed !
if (type == MULT) begin
// 这里是一个很有趣的写法
{HI, LO} <= $signed(inputA) * $signed(inputB);
end
else if (type == MULTU) begin
{HI, LO} <= inputA * inputB;
end
else if (type == DIV) begin
// 这里保证了除数不能为 0
if (inputB != 32'h00000000) begin
HI <= $signed(inputA) % $signed(inputB);
LO <= $signed(inputA) / $signed(inputB);
end
end
else if (type == DIVU) begin
if (inputB != 32'h00000000) begin
HI <= inputA % inputB;
LO <= inputA / inputB;
end
end
end
end

endmodule

接下来是存取部分,和 GRF 略有不同的是,我们一次最多只会读取一个寄存器,而不是 GRF 中的两个;同时,我们也不需要设置一个输入接口判断需要读取或写入的寄存器,从指令名中即可获取到这些信息:

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
module MDU(
input clk,
input reset,
input [31:0] inputA,
input [31:0] inputB,
input [5:0] type,
output [31:0] outputB
);

reg [31:0] HI;
reg [31:0] LO;

parameter MULT = 6'b010001,
MULTU = 6'b010010,
DIV = 6'b010011,
DIVU = 6'b010100,
MFHI = 6'b010101,
MFLO = 6'b010110,
MTHI = 6'b010111,
MTLO = 6'b011000;

always@(posedge clk) begin
if (reset) begin
HI <= 32'h00000000;
LO <= 32'h00000000;
end
else begin
if (type == MTHI) begin
HI <= inputA;
end
else if (type == MTLO) begin
LO <= inputA;
end
else if (type == MULT) begin
{HI, LO} <= $signed(inputA) * $signed(inputB);
end
else if (type == MULTU) begin
{HI, LO} <= inputA * inputB;
end
else if (type == DIV) begin
if (inputB != 32'h00000000) begin
HI <= $signed(inputA) % $signed(inputB);
LO <= $signed(inputA) / $signed(inputB);
end
end
else if (type == DIVU) begin
if (inputB != 32'h00000000) begin
HI <= inputA % inputB;
LO <= inputA / inputB;
end
end
end
end

assign outputB = (type == MFHI) ? HI :
(type == MFLO) ? LO :
32'h00000000;

endmodule

这样,我们就非常轻松地完成了 MDU 模块的设计!接下来当然还是固定流程了,不过因为这些指令和我们之前已经实现的指令都不太相同,所以就需要我们稍微动一动脑了。不过我相信这么简单的小任务,对于聪明的你来说一定不是问题!

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
77
78
79
80
81
82
83
84
85
86
87
88
// Controller
parameter ADDU = 6'b000000,
......,
MULT = 6'b010001,
MULTU = 6'b010010,
DIV = 6'b010011,
DIVU = 6'b010100,
MFHI = 6'b010101,
MFLO = 6'b010110,
MTHI = 6'b010111,
MTLO = 6'b011000;

wire mult;
wire multu;
wire div;
wire divu;
wire mfhi;
wire mflo;
wire mthi;
wire mtlo;

assign mult = (OpCode == 6'b000000 && Funct == 6'b011000) ? 1'b1 : 1'b0;
assign multu = (OpCode == 6'b000000 && Funct == 6'b011001) ? 1'b1 : 1'b0;
assign div = (OpCode == 6'b000000 && Funct == 6'b011010) ? 1'b1 : 1'b0;
assign divu = (OpCode == 6'b000000 && Funct == 6'b011011) ? 1'b1 : 1'b0;
assign mfhi = (OpCode == 6'b000000 && Funct == 6'b010000) ? 1'b1 : 1'b0;
assign mflo = (OpCode == 6'b000000 && Funct == 6'b010010) ? 1'b1 : 1'b0;
assign mthi = (OpCode == 6'b000000 && Funct == 6'b010001) ? 1'b1 : 1'b0;
assign mtlo = (OpCode == 6'b000000 && Funct == 6'b010011) ? 1'b1 : 1'b0;

assign NPC_Sel = (j | jal) ? 2'b01 :
(jr | jalr) ? 2'b10 :
(beq | bne) ? 2'b11 : 2'b00;
assign RegWrite = (addu | subu | addiu | xori | lui | lw | lh | lb | jal | jalr | mfhi | mflo) ? 1'b1 : 1'b0;
assign EXTop = (addiu | lw | lh | lb | sw | sh | sb) ? 1'b1 : 1'b0;
assign ALUsrc = (addiu | xori | lui | lw | lh | lb | sw | sh | sb) ? 1'b1 : 1'b0;
assign RegDst = (addu | subu | jalr | mfhi | mflo) ? 1'b1 : 1'b0;
assign MemtoReg = (lw | lh | lb) ? 1'b1 : 1'b0;
assign PCtoReg = (jal | jalr) ? 1'b1 : 1'b0;
assign Regra = (jal) ? 1'b1 : 1'b0;
assign type = (addu) ? ADDU :
(subu) ? SUBU :
...... :
(mult) ? MULT :
(multu) ? MULTU :
(div) ? DIV :
(divu) ? DIVU :
(mfhi) ? MFHI :
(mflo) ? MFLO :
(mthi) ? MTHI :
(mtlo) ? MTLO : 6'b111111;

// mult multu div divu 指令需要在 E 级的 MDU 模块中进行计算
// mthi mtlo 指令需要在 E 级的 MDU 模块中写入 HI LO 寄存器
assign t_rs = (addu) ? 4'h1 :
...... :
(mult) ? 4'h1 :
(multu) ? 4'h1 :
(div) ? 4'h1 :
(divu) ? 4'h1 :
(mfhi) ? 4'hf :
(mflo) ? 4'hf :
(mthi) ? 4'h1 :
(mtlo) ? 4'h1 : 4'hf;

// mult multu div divu 指令需要在 E 级的 MDU 模块中进行计算
assign t_rt = (addu) ? 4'h1 :
...... :
(mult) ? 4'h1 :
(multu) ? 4'h1 :
(div) ? 4'h1 :
(divu) ? 4'h1 :
(mfhi) ? 4'hf :
(mflo) ? 4'hf :
(mthi) ? 4'hf :
(mtlo) ? 4'hf : 4'hf;

// mfhi mflo 从 HI LO 寄存器读出的值在 M 级可以开始转发
assign t = (addu) ? 4'h2 :
...... :
(mult) ? 4'hf :
(multu) ? 4'hf :
(div) ? 4'hf :
(divu) ? 4'hf :
(mfhi) ? 4'h2 :
(mflo) ? 4'h2 :
(mthi) ? 4'hf :
(mtlo) ? 4'hf : 4'hf;

(由于这些指令都不需要进入 ALU 模块,所以我们可以不对 ALU 模块做任何修改)

然而,我们目前的电路还存在着一个非常明显的问题,你发现了吗?那就是我们并没有将 MDU 模块的 outputB 端口接入电路中!所以我们需要在 E 级加入一个新的 Controller 模块的选择端口,通过当前指令的 type 来判断是否使用 outputB 值代替 outputA 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module Controller(
input [5:0] OpCode,
input [5:0] Funct,
output [1:0] NPC_Sel,
output RegWrite,
output EXTop,
output ALUsrc,
output RegDst,
output MemWrite,
output MemtoReg,
output PCtoReg,
output Regra,
output mffix,
output [5:0] type,
output [3:0] t_rs,
output [3:0] t_rt,
output [3:0] t
);

assign mffix = (mfhi | mflo) ? 1'b1 : 1'b0;

endmodule

最后,我们将主模块中引用 MDU 模块,并写入 outputAoutputB 合流的逻辑就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// mips
wire E_mffix;
wire [31:0] E_outputB;
wire [31:0] E_fixedoutputA;

assign E_fixedoutputA = mffix ? E_outputB : E_outputA;

// E zone ////////////////////////////////
Controller EController (.OpCode(E_OpCode), .Funct(E_Funct), .ALUsrc(E_ALUsrc),
.mffix(E_mffix), .type(E_type), .t_rs(E_t_rs), .t_rt(E_t_rt),
.t(E_t));
MDU MDU (.clk(clk), .reset(reset), .inputA(E_inputA), .inputB(E_inputB), .type(E_type),
.outputB(E_outputB));
// 这里注意连接 E_outputA 接口的是修正值 E_fixedoutputA
EMreg EMreg (.clk(clk), .reset(reset), .EM_en(EM_en), .EM_clear(EM_clear),
.E_Instr(E_Instr), .E_PCplus8(E_PCplus8), .E_PC(E_PC),
.E_outputA(E_fixedoutputA), .E_RD2(E_fixedRD2), .E_A3(E_A3),
.M_Instr(M_Instr), .M_PCplus8(M_PCplus8), .M_PC(M_PC),
.M_outputA(M_outputA), .M_RD2(M_RD2), .M_A3(M_A3));

这样,我们就完成了 MDU 模块和 8 个乘除法指令!这回是真的超简单了,甚至对于现在这个水平的我们来说有点太简单了,所以我们的课程组为我们设计了一系列规则怪谈,来挑战我们的水平 ——

【使用乘除法指令模拟多周期运算】

在真实的 CPU 中,乘除法的计算是极其缓慢的(相信你也在写程序时听说过把乘除法换成位运算这种操作吧~),所以为了不让乘除法计算指令( mult multu div divu 指令)拖慢 CPU 的最短运行周期,我们不要求这些指令在一个周期内就执行完毕,而是允许它们连续执行好几个周期。

这样做有的好处还是非常明显的,一是我们刚才提过的,CPU 的最短运行周期可以不受影响;二是当乘除法计算指令在 MDU 模块中计算时,虽然当前指令没有执行完毕,但所有不进入 MDU 模块的指令都可以正常流水,而不需要进行任何阻塞。

在乘除法计算的过程中,因为 mult multu div divu 这四条指令不会写入 GRF 中的寄存器,其它不进入 MDU 模块的指令也不会读取 HI LO 寄存器,所以它们之间不存在任何的数据冲突!

当然了,如果在 MDU 模块正在进行乘除法计算的时候出现了新的乘除法指令,如果出现的是 mfhi 指令或 mflo 指令,那就只能将指令阻塞在 D 级,等待计算结果写入 HI 寄存器和 LO 寄存器了;如果出现的是 mult multu div divu 指令,我们可以提前终止当前的运算,毕竟我们接下来要执行的指令会将 HI 寄存器和 LO 寄存器全部覆盖掉。

如果出现的是 mthi 指令或 mtlo 指令,那就有些难搞了,因为它们并不会将 HI 寄存器和 LO 寄存器全部覆盖,所以我们也不能直接终止当前的运算。最简单粗暴的方法当然也是将它们阻塞在 D 级,但如果要追求效率的话,可以为 HI 寄存器和 LO 寄存器分别设置一个 reg 型变量 HIbanLOban ,置 1 时表示禁止写入相应的该寄存器,防止当前正在执行的乘除法运算将已经修改过的寄存器值写回来(当然,当乘除法计算开始时,需要记得将 HIbanLOban 置回 0 )。当 HIbanLOban 的值均为 1 ,即 HI 寄存器和 LO 寄存器全部被 mthi 指令和 mtlo 指令覆写时,我们当然也就可以终止当前的运算了。

为了模拟多周期的运算,课程组为我们设计了一系列规则怪谈,让我们来看看都有什么:

  1. 当 E 级指令是 mult multu div divu 指令时,Controller 模块需要输出 1 个周期的 1 位 start 信号,表示乘除法计算开始;
  2. start 信号结束后,如果之前的计算指令是 mult multu 指令,MDU 模块需要连续输出 5 个周期的 busy 信号,表示正在计算 HI 寄存器和 LO 寄存器的值;如果之前的计算指令是 div divu 指令,则需要连续输出 10 个周期的 busy 信号;
  3. start 信号或 busy 信号为 1 时,不进入 MDU 模块的指令正常流水,进入 MDU 模块的所有指令均阻塞在 D 级;
  4. 为了降低实现难度,保证 MDU 模块在进行乘除法计算时只会出现 mfhi mflo 两种乘除法指令(所以不需要考虑提前终止运算或禁止写入寄存器的情况,这也呼应了第 3 条规则,无脑阻塞就可以了)。

虽然规则很复杂,不过要实现的内容却并不是很多,说白了只有 3 项:start 信号、busy 信号,以及无脑阻塞!

在 Controller 模块中,我们加入新的选择信号 start ,并将其输入 MDU 模块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Controller
module Controller(
input [5:0] OpCode,
input [5:0] Funct,
output [1:0] NPC_Sel,
output RegWrite,
output EXTop,
output ALUsrc,
output RegDst,
output MemWrite,
output MemtoReg,
output PCtoReg,
output Regra,
output mffix,
output start,
output [5:0] type,
output [3:0] t_rs,
output [3:0] t_rt,
output [3:0] t
);

assign start = (mult | multu | div | divu) ? 1'b1 : 1'b0;

endmodule
1
2
3
4
5
6
7
8
9
// mips
wire E_start;

// E zone ////////////////////////////////
Controller EController (.OpCode(E_OpCode), .Funct(E_Funct), .ALUsrc(E_ALUsrc),
.mffix(E_mffix), .start(E_start), .type(E_type), .t_rs(E_t_rs),
.t_rt(E_t_rt), .t(E_t));
MDU MDU (.clk(clk), .reset(reset), .inputA(E_inputA), .inputB(E_inputB), .type(E_type),
.start(E_start), .outputB(E_outputB));

在 MDU 模块中,我们根据输入的 start 信号触发 busy 信号。在时钟上升沿,如果 start 信号为 1 ,则将 reg 型变量 count 置为 5 或 10 ,并且每周期递减,若 count 值不为 0 ,则输出 busy 信号:

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
77
78
module MDU(
input clk,
input reset,
input [31:0] inputA,
input [31:0] inputB,
input [4:0] type,
input start,
output [31:0] outputB,
output busy
);

reg [31:0] HI;
reg [31:0] LO;
reg [3:0] count;

parameter MULT = 6'b010001,
MULTU = 6'b010010,
DIV = 6'b010011,
DIVU = 6'b010100,
MFHI = 6'b010101,
MFLO = 6'b010110,
MTHI = 6'b010111,
MTLO = 6'b011000;

always@(posedge clk) begin
if (reset) begin
HI <= 32'h00000000;
LO <= 32'h00000000;
count <= 4'h0;
end
else begin
if (type == MTHI) begin
HI <= inputA;
end
else if (type == MTLO) begin
LO <= inputA;
end
else if (type == MULT) begin
{HI, LO} <= $signed(inputA) * $signed(inputB);
end
else if (type == MULTU) begin
{HI, LO} <= inputA * inputB;
end
else if (type == DIV) begin
if (inputB != 32'h00000000) begin
HI <= $signed(inputA) % $signed(inputB);
LO <= $signed(inputA) / $signed(inputB);
end
end
else if (type == DIVU) begin
if (inputB != 32'h00000000) begin
HI <= inputA % inputB;
LO <= inputA / inputB;
end
end

if (start) begin
if (type == MULT || type == MULTU) begin
count <= 4'h5;
end
else if (type == DIV || type == DIVU) begin
count <= 4'ha;
end
end

if (count != 4'h0) begin
count <= count - 4'h1;
end
end
end

assign outputB = (type == MFHI) ? HI :
(type == MFLO) ? LO :
32'h00000000;

assign busy = (count != 4'h0) ? 1'b1 : 1'b0;

endmodule

最后,我们在主模块中修改 MDU 模块的引用,并加入一条新的阻塞条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// mips
wire E_busy;
wire D_ismult;

// E zone ////////////////////////////////
MDU MDU (.clk(clk), .reset(reset), .inputA(E_inputA), .inputB(E_inputB), .type(E_type),
.start(E_start), .outputB(E_outputB), .busy(E_busy));

// stall ////////////////////////////////
// 判断 D 级指令是否为 8 条乘除法指令,这里对 D_type 采用了硬编码
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;

// 当 D 级指令是 8 条乘除法指令,且 start 信号或 busy 信号为 1 时,直接阻塞
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 :
(D_ismult & (E_start | E_busy)) ? 1'b0;

这样,我们就实现了完全体的乘除法指令!恭喜你完成了本书中最困难的第五章!

5.7 本章结语:再见,五级流水线 CPU ?

可能你会发现本节标题竟然是个问号,当然再见是不可能再见的,接下来我们还要再和五级流水线 CPU 缠斗一章(笑)。

能够看到这里,说明你已经闯过了本书中最凶险的一片领域,想必也花费了不少的时间和精力吧!想当年,我在这一章的内容中花费了 4 周,因为我 P5 挂了两次(悲);今年写这本书的时候,我也是花费了很长的一段时间,总是想在这么难的一章中把每个细节讲到透彻。你我都已经尽了非常大的努力了!

如果你已经挂过几回了,千万不要灰心!要相信,即使是一次又一次的挫败,你的能力也是在不断积累的。能够一路闯到这里,现在的你已经变得比之前任何时候的你都更加强大。休息一下,然后重整旗鼓,去面对最后的挑战吧!

如果你已经被迫止步于此,也不要为此过于悲伤,毕竟理论课的成绩才占计组课程的大头,我们还有着翻盘的机会,从现在开始准备起来吧!计组课程虽然是一门大课重课,然而研究的方向更偏向于硬件,可能会和很多同学感兴趣和擅长的领域并不重合,不要因为第一门专业课的失手就选择放弃,相信你能够找到自己热爱的领域,在亲手辛勤浇灌的土地上闪闪发光!

终章:带中断异常的五级流水线 CPU

To Be Continued……