多周期 MIPS 处理器的设计

⚠ 转载请注明出处:作者:ZobinHuang,更新日期:Dec.21 2021


知识共享许可协议

    本作品ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。


目录

有特定需要的内容直接跳转到相关章节查看即可。

    Section 1. 基本思路:介绍了设计多周期 MIPS 处理器的基本思路;
        1.1 单周期处理器的弊端
        1.2 多周期处理器
        1.2 设计思路

    Section 2. 数据通路:介绍了多周期 MIPS 处理器数据通路的基本设计;
        2.1 State Elements
        2.2 读取指令
        2.3 读取源操作数
        2.4 拓展立即数宽度
        2.5 计算内存地址
        2.6 从内存中加载数据
        2.7 写寄存器
        2.8 决定下一条指令的地址
        2.9 增加对指令 sw 的支持
        2.10 增加对 R-Type 指令的支持
        2.11 增加对 beq 指令的支持

    Section 3. 控制单元:介绍了多周期 MIPS 处理器 Control Unit 的基本设计,其本质是一个 Moore 型状态机;
        3.1 基本思路
        3.2 Main Decoder 设计基本思路
        3.3 读取指令
        3.4 解码
        3.5 计算内存地址
        3.6 读取内存
        3.7 写内存
        3.8 执行 R-Type 指令
        3.9 执行 beq 指令
        3.10 执行 addi 指令
        3.11 执行 j 指令

    Section 4. 性能分析
        4.1 程序运行时间
        4.2 指令运行时间

1. 基本思路

1.1 单周期处理器的弊端

    在上一篇文章 单周期 MIPS 处理器的设计 中,我们完成了一个针对 MIPS 指令子集的单周期处理器的设计。对于单周期的 MIPS 处理器,我们可以看到有三个弊端:

  1. 处理器的时钟周期完全取决于运行时间最长的那条指令所需要的时间,比如我们在上一篇文章 单周期 MIPS 处理器的设计 中分析过的 lw,对于其它不需要那么多访存操作的指令来说,这样的设计无异于降低了效率;
  2. 在单周期处理器的设计中我们使用到了 3 个加法器,然而加法器是一种昂贵的电路;
  3. 在单周期处理器的设计中我们把 Instruction Memory 和 Data Memory 进行了分离,然而在现代系统中通常是使用统一的内存器件以存储指令和数据;

1.2 多周期处理器

    在本文中,我们将介绍 多周期处理器 (Multi-cycle Processor),它通过把一条指令的执行过程的执行过程拆分为若干个子步骤的方式来执行一条指令。在每个子步骤中,处理器可以读取/写入内存、Register File,也可以使用 ALU。这样一来:

  1. 不同的指令将会消耗不同数量的子步骤,因此操作更简单的指令可以更快地完成执行过程;
  2. 处理器只设计了 1 个加法器,加法器在不同的需求下可以被重复使用;
  3. 处理器会从统一的内存中读写指令和数据;

1.3 设计思路

    我们设计多周期 MIPS 处理器的思路将遵循我们设计单周期处理器的思路:先使用组合逻辑串联 State Elements 来设计数据通路。与设计单周期处理器不同的是,我们在设计多周期处理器的数据通路的时候将会引入 Non-architectural State Element (i.e. 程序员不可见寄存器) 来暂存两个子步骤之间的中间结果。在完成数据通路的设计后,我们会进而对 Control Unit 进行设计,由于 Control Unit 会在一条指令的不同子步骤上生成不同的控制信号,以在同一套硬件上完成各个子步骤的执行过程,因此在多周期处理器中,Control Unit 实际上是一个 有限状态机,而不像单周期处理器一样,是一个组合逻辑电路。

    在完成多周期处理器的设计之后,我们同样会演示向基础设计中添加对新指令的支持。我们在最后同样也会进行性能分析,并将多周期处理器与单周期处理器的性能进行比较。

2. 数据通路

2.1 State Elements

    如上所示,是我们在多周期的处理器中所使用的 State Element,与单周期处理器大体相同。不同的是,我们使用了 Intruction 和 Data 合二为一的单块大内存。

    下面我们对一条指令在多周期处理器中的执行过程进行分析。

2.2 读取指令

    我们假设我们当前处于第 `n` 个时钟周期。我们要做的第一件事情就是从 PC 中获取指令在内存中的地址,并且从 Memory 中获取这条指令,如上图所示。注意到我们在获取这条指令后,我们把该指令存储在一个 Non-architectural 的 Instruction Register 中,以供后续周期继续使用。值得注意的是,该寄存器会收到一个控制信号 `IRWrite`,在输出的指令需要被更新的时候,该信号会被使能,以让读取的指令被寄存器 Instruction Register 在 `Instr_{31:0}` 上进行输出。

2.3 读取源操作数

    与我们在分析单周期处理器时一样,我们先假设我们读取出来的指令是一条 lw 指令,后面我们会慢慢地对其它指令增加相关支持。对于 lw 指令来说,它的下一步是去寄存器中读取存储有内存单元基地址的寄存器值。因此如上图所示,在第 `n+1` 个周期,我们把 lw 指令的 rs 字段 (i.e. `Instr_{25:21}`) 送上了 Register File 的寄存器选择端口 A1,以让 Register File 在其输出端口 RD1 上输出相应的寄存器的值,并且把该寄存器的值保存在一个 Non-architectural 的寄存器 A 中。

2.4 拓展立即数宽度

    我们同样需要扩展 lwimm 字段 (i.e. `Instr_{15:0}`) 所携带的 16-bits 立即数字段。如上图所示,同样还是在第 `n+1` 个周期,我们把 lw 指令的 imm 字段 (i.e. `Instr_{15:0}`) 送上了符号扩展器,并且输出符号扩展后的立即数结果 `SignImm`,作为内存单元地址的偏移量。由于 `SignImm` 是对 `Instr_{31:0}` 的一次组合逻辑运算的结果,在运行当前指令的过程中并不会发生改变,因此此处我们不需要使用 Non-architectural 的寄存器来进行缓存。

2.5 计算内存地址

    在拿到 32-bits 的内存单元基地址和 32-bits 的偏移量 `SignImm` 后,我们就需要把这两个数值送入 ALU 中进行加法运算。如上图所示,在第 `n+2` 个周期,我们把 Non-architectural 寄存器 A 的暂存结果和 `SignImm` 送入了 ALU 的 SrcASrcB 输入端口,在收到控制信号 `ALUControl` 控制的情况下,我们在其输出口 ALUResult 上获得了计算结果值,并将其暂存在 Non-architectural 寄存器 ALUOut 中。

2.6 从内存中加载数据

    在第 `n+3` 个周期,我们就需要从 Memory 中读取出上个时钟周期中由 ALU 输出值作为地址指定的内存单元中存储的值。如上图所示,我们在 Memory 的地址输入端口 A 之前加入了一个多路选择器,并受控制信号 `Io rD` (i.e. Instruction or Data) 的控制: 如果为 0,则送上 Memory 地址线的是我们上面在读取指令的时候,由 PC 寄存器指定的指令地址; 如果为 1,则送上 Memory 地址线的是由 Non-architectural 寄存器 ALUOut 暂存的值。显然对于 lw 指令来说,运行到这一步,我们的控制信号此时应该为 1。

    从 Memory 中读取出响应的数据后,我们的数据会被缓存到 Non-architectural 寄存器 Data 中。注意到由于控制信号 `IRWrite` 在当前周期并未被使能,所以 Memory 输出的数据并不会被更新到 Non-architectural 寄存器 Instruction Register 中。

    这样的设计使得 MIPS 处理器在处理 lw 指令的时候可以复用对内存的访问,不需要将 Instruction Memory 和 Data Memory 进行拆分。

2.7 写寄存器

    最后,在第 `n+4` 个周期,我们将保存在 Non-architectural 寄存器 Data 中的数据值写入到由 lw 指令的 rt 字段 (i.e. `Instr_{20:16}`) 指定的寄存器中,如上图所示。

2.8 决定下一条指令的地址

    在单周期处理器中,我们使用了额外的加法器以实现对 PC 的自增操作。而在多周期处理器的设计中,实际上我们可以在 ALU 单元空闲的时候实现对 PC 值的计算。为了实现这样的功能,如上图所示,我们在 ALU 的两个输入 SrcASrcB 之前引入了两个多路选择器:

    对于 SrcA 来说,我们使用控制信号 `ALUSrcA` 来控制这个多路选择器: 如果信号为 0,则送入 SrcA 的是当前 PC 输出的值; 如果信号为 1,那么送入 SrcA 的就是我们上面构建的 Non-architectural 寄存器 A 输出的值。

    对于 SrcB 来说,我们使用控制信号 `ALUSrcB_{1:0}` 来控制这个多路选择器。由于我们在后面在扩展其它指令的时候,还需要增加对送入 SrcB 的信号的选择,因此此处我们添加的是一个 4 路多路选择器。当 `ALUSrcB_{1:0}` 为 01 的时候,送上 SrcB 将是常数 4,以实现对 PC 自增 4 的功能。

    另外,我们还引入了一个新的控制信号 `PCWrite`,在需要更新 PC 寄存器的时钟周期,该控制信号将被使能。

    这样一来,我们就完成了数据通路上对指令 lw 的支持。

2.9 增加对指令 sw 的支持

    为了增加对 sw 指令的支持,我们要做的工作实际上很简单:和单周期处理器一样,对 Memory 的写入数据恒定只能来自于 Register File 的 RD2 端口,因此我们将指令的 rt 字段 (i.e. `Instr_{20:16}`) 输入 Register File 的输入端口 A2,并将相应的输出端口 RD2 输出的值缓存到 Non-architectural 寄存器 B 中,并且将该 Non-architectural 寄存器输出的值连接到 Memory 的写入数据输入端口 WD 上。另外,我们还使用了一个控制信号 `MemWrite`,以控制对 Memory 的写入使能。

2.10 增加对 R-Type 指令的支持

    上面我们完成了多周期 MIPS 处理器对 I-Type 指令 lwsw 的支持,在本节中我们将看到其对 R-Type 指令的支持。R-Type 指令实现了对两个源寄存器 (i.e. 由 rsrt 字段指定) 的读取,以及对一个目的寄存器 (i.e. 由 rd 字段指定) 的写入。对于 R-Type 指令的支持,我们在数据通路上主要有两个更新。

    首先,不同于 I-Type 指令,在 rt 字段 (i.e. `Instr_{20:16}`) 中指定写入的寄存器地址,R-Type 指令利用 rd 字段 (i.e. `Instr_{15:11}`) 来指定,因此我们在 Register File 的寄存器选择输入端口 A3 之前加上了一个多路选择器以进行选择,并且使用控制信号 `RegDst` 以进行控制: 当信号为 0 时,选择的是 I-Type 指令使用的 rt 字段作为写入寄存器地址; 当信号为 1 时,选择的是 R-Type 指令的 rd 字段。

    其次,对于写入寄存器的数据的来源,lw 指令来源于 Memory 中读取出来的数据,而对于大多数 R-Type 指令来说,则来自于 ALU 的计算结果。因此同理我们在 Register File 的寄存器数据输入端口 WD3 中加入了一个多路选择器,并使用控制信号 `Memt oReg` 以进行控制: 当信号为 0 时,输入寄存器的值来自于 ALU 的计算结果; 当信号为 1 时,输入控制器的值来自于 Memory 的读取结果。

    在引入这两个多路选择器部件之后,我们的数据通路就支持 R-Type 指令了。

2.11 增加对 beq 指令的支持

    beq 指令是一条 I-Type 指令,它对由 rt 字段 (i.e. `Instr_{20:16}`) 和 rs 字段 (i.e. `Instr_{25:21}`) 指定的两个寄存器中存储的值进行减法操作,如果减法结果为 0 (i.e. 两个值相等),则对 PC 值执行: `PC = PC + 4 + SignImm \times 4` 的操作; 否则则进行上面我们讨论过的单纯的 add 4 操作。

    在单周期处理器中,我们使用了一个单独的加法器来实现 beq 指令下的 PC 值计算。在多周期处理器中,我们将复用我们数据通路上唯一的加法器资源。基本思路是:我们首先在一个 ALU 空闲的时钟周期 (i.e. 假设为周期 `P`) 内计算出 `PC + 4` 的值,然后写入到 PC 寄存器; 然后在另一个 ALU 空闲的时钟周期 (i.e. 假设为周期 `Q`) 中,利用先前计算好的 `PC + 4` 值来进一步计算 `PC = PC + 4 + SignImm \times 4` 值。

    我们现在先对与 PC 相关的控制信号的产生做一个梳理:我们现在先定义一个控制信号 `Branch`,以标志分支指令的到来。当 `Branch` 和 ALU 的标志位输出 `Zero` 都为真时,我们应该更新 PC 寄存器。同时我们在上面还定义过控制信号 `PCWrite`,以实现运行常规指令下的 PC 更新。因此,我们对 PC 更新的控制信号的修改如上图所示,首先将 `Branch` 和 ALU 的标志位输出 `Zero` 进行相与,以得到运行 beq 指令下 PC 更新的条件; 然后同时将该条件信号和 `PCWrite` 信号相或,以实现两种不同情况 (i.e. [1] 运行 beq 指令; [2] 运行常规指令) 下的对 PC 进行更新的使能信号输出。

    基于上述思路,我们假设我们在周期 `P` 中已经完成了 `PC+4` 的运算,并且将该值存储于 PC 寄存器中。在周期 `Q` 中,我们通过控制信号 `ALUALUSrcB_{1:0}`计算出 `PC = PC + 4 + SignImm \times 4` 的值,并且保存到 Non-architectural 寄存器 ALUOut 中。注意到在该寄存器之后,我们添加了一个多路选择器,并且使用控制信号 `PCSrc` 予以控制: 当控制信号为 0 时,写入 PC 寄存器输入端口 PC' 的是当前 ALU 的直接计算结果; 当控制信号为 1 时,写入 PC 寄存器输入端口 PC' 的是上一轮时钟周期计算得到的 ALU 结果 (i.e. 保存在 Non-architectural 寄存器 ALUOut 中)。显然的,此时该信号应该为 1。并且这也暗示着,我们需要在周期 `Q+1` 中立刻判断 beq 指令指定的两个寄存器中存储的值是否相等,若相等则输出相应的 `Zero` 信号,来使能实际控制信号 `PCEn` 以实现 `PC = PC + 4 + SignImm \times 4` 的写入。如果我们不在周期 `Q+1` 中做这件事情,我们将错过 `PC = PC + 4 + SignImm \times 4` 这个计算结果。

3. 控制单元

3.1 基本思路

    在上面设计数据通路的时候我们不难发现,多周期处理器的控制单元将会是一个较为复杂的 FSM,针对不同的指令都会有不同的控制步骤流程。在本节中我们将看到我们是如何设计这个复杂 FSM 的。

    在单周期处理器中,我们设计的控制单元由 Main Decoder 和 ALU Decoder 组成,并且是一个组合逻辑电路,受指令中的 OpcodeFunct 字段控制以输出相应的控制信号。而在多周期处理器的设计中,如上图所示,我们保留了 ALU Decoder 的设计,它作为一个组合逻辑电路继续输出 `ALUControl_{2:0}` 来控制 ALU 的行为; 而对于 Main Decoder 来说,它是一个时序逻辑电路,它输出的控制信号取决于: (1) 当前正在运行的指令 以及 (2) 当前正在运行的指令到达的步骤,因此是一个有限状态机。

    如下所示,是 ALU Decoder 的真值表。

`ALUOp`
Funct
`ALUControl` (含义)
00
X
010 (add)
X1
X
110 (sub)
1X: 由 Funct 字段决定 ALU 功能
1X
100000 (add)
010 (add)
1X
100010 (sub)
110 (sub)
1X
100100 (and)
000 (and)
1X
100101 (or)
001 (or)
1X
101010 (slt)
111 (set less than)

    如下所示,是 Control Unit 和数据通路的整体连接图。

    下面我们对 Main Decoder 的设计展开讨论。

3.2 Main Decoder 设计基本思路

    如上所示,Main Controller 的输入可以视为指令的 OpcodeFunct 字段,输出是 (1) 多路选择器选择信号 以及 (2) Non-architectural 寄存器使能信号。回顾我们在 时序逻辑电路基础 中讨论的关于 Moore 型和 Mealy 型状态机的模型。我们在下面会看到,对于 Main Controller 来说,它的输出仅取决于 FSM 所处的状态,因此我们本节中设计的 FSM 将会是一个 Moore 型状态机。并且,状态和状态之间的转移是由时钟信号驱动的。如果存在状态转移的分支,那么具体转移到哪个状态将会由 OpcodeFunct 字段来决定。

    下面我们将重新分析一条指令的执行过程,但我们将把目光转向 Control Unit 的状态转移和相关控制信号输出。值得注意的是,在下面的分析时,一条指令执行的各个阶段,我们只会展示相关的控制信号线,并不会展示所有的控制信号线。

3.3 读取指令

    如上图所示,执行一条指令的第一个状态 S0: Fetch 是: 处理器根据 PC 输出的地址值从内存中读取得到相应的指令,并且完成 PC 的自增 4 的操作。每当处理器被重置的时候,FSM 都会重新来到这个状态下。如上图所示,此时:

  1. 控制信号 `Io rD` 将会为 0,以使得送上 Memory 地址线的是 PC 寄存器的输出 PC';
  2. 控制信号 `IRWrite` 将会被使能,以使得从 Memory 读取出来的指令会被存储到 Non-architectural 寄存器 IR 中去;
  3. 为了基于当前的 `PC` 值计算出 `PC+4` 值,控制信号 `ALUOp` 会为 00,控制信号 `ALUSrcA` 会为 0,控制信号 `ALUSrcB` 会为 01,这样一来 `PC+4` 值就会被 ALU 计算得出;
  4. 控制信号 `PCSrc` 将会为 0,以将当前周期中 ALU 的计算结果输出到 PC 寄存器的输入端口 PC' 上去;
  5. 控制信号 `PCWrite` 将会为 1,因此 `PCEn` 信号为 1,以使能将新得到的 `PC+4` 的计算结果写入到 PC 寄存器中。

    以上所有控制信号可以归纳为如下所示:

3.4 解码

    在完成指令的读取后,我们现在的处境是:

  1. 我们在 Non-architectural 寄存器 IR 中存储并输出了新读取出来的指令 `Instr_{31:0}`;
  2. PC 寄存器已经完成了自增 4 的操作;

    来到当前状态 S1: Decode,我们的操作首先是进行解码: 根据 OpcodeFunct 字段来决定 FSM 下一步进行转移的状态。我们在这里将使用一个额外单独的状态来接收来自 Non-architectural 寄存器 IR 输出的指令中包含的 OpcodeFunct 字段,作为 Moore 状态机的输入信号以实现下一个跳转状态的选择。一开始我想不懂为什么解码需要使用一个单独的状态,"浪费" 一个时钟周期来进行等待。事实是,在当前时钟周期开始的时候,Non-architectural 寄存器 IR 才开始输出读取到的指令,而作为时序逻辑电路的 Control Unit,其输出的控制信号的值只有在时钟上升沿到来的时候才会发生改变,因此倘若我们想根据 IR 的输出来改变 Control Unit 的控制信号,则必须先等待 IR 的输出,以作为 Control Unit 的输入,因此我们必须等待。

    同时在当前状态,我们的另一个任务是,在不清楚指令类型的情况下,根据潜在的 rsrt 字段从 Register File 中读取相关寄存器中保存的值,同时使用符号扩展器扩展潜在的 imm 字段,在其输出上得到 `SignImm`。

    如上图所示,是当前状态下的数据通路。注意到我们在进行上述两个操作的时候,我们并不需要输出任何控制信号,因此如下图所示,我们在当前状态的圆圈中是没有任何输出的。

    在结束当前状态进入下一个状态时,我们的 Control Unit 将会把 OpcodeFunct 字段作为输入,用于决定跳转的下一状态,我们在下面分别会看到不同的跳转分支。

3.5 计算内存地址

    假设我们读取到的指令是 lwsw 指令。来到当前状态 S2: MemAdr,此时处理器的任务是完成由 rs 字段指定的寄存器的值和完成符号扩展的立即数值 `SignImm` 的相加操作。因此如上图所示,我们首先控制 `ALUSrcA` 信号为 1,`ALUSrcB_{1:0}` 信号为 10 来选择正确的送入 ALU 的数据源,同时我们输出 `ALUout` 信号为 00 来使得 ALU 完成的是加法操作。完成加法操作后,计算结果将被保存到 Non-architectural 寄存器 ALUOut 中。

    如下图所示,是当前状态的转移和输出情况。

3.6 读取内存

    如果我们读取到的是一条 lw 指令,那么我们下一步操作将是根据在上一个状态计算得到的内存单元地址,从 Memory 中读取相应的数据。如下图所示是状态转移情况。首先我们会来到 S3: MemRead 状态,我们控制 `Io rD` 信号为 1,以选择送上 Memory 地址线的是 Non-architectural 寄存器 ALUOut 输出的值,该值是我们在上一个状态计算得到的地址值。

    接着我们会来到 S4: MemWriteback 状态,该状态的任务是实现将数据向寄存器中的写入。我们控制 `RegDst` 信号为 0,以使得送上 Register File 写寄存器地址的是 lw 指令的 rt 字段 (i.e. `Instr_{20:16}`); 控制 `Memt oReg` 信号为 1,使得送上 Register File 写数据端口的是来自上一个状态 S3: MemRead 我们从 Memory 读取得到的数据。同时我们还需要打开 `RegWrite` 信号,以实现对 Register File 的写入。

    在完成对 Register File 的写入后,lw 指令的执行也就结束了,我们的下一个状态将回到 S0: Fetch

3.7 写内存

    如果我们读取到的是一条 sw 指令,那么我们下一步操作将是以 Non-architectural 寄存器 ALUOut 输出的值作为写入的内存单元的地址,写入由 rt 字段指定的寄存器的值,该寄存器的值已经在 S1: Decode 之后就一直输出在 Non-architectural 寄存器 B 之中。

    如上图所示是运行 sw 指令相应的状态转移图。我们在 S2: MemAdr 完成内存地址的计算后,来到 S5: MemWrite 状态。我们控制 `Io rD` 信号为 1,以选择送上 Memory 地址线的是 Non-architectural 寄存器 ALUOut 输出的值,该值是我们在上一个状态计算得到的地址值。同时我们还需要打开 `MemWrite` 信号,以实现对 Memory 写入的控制。

    在完成对 Memory 的写入后,sw 指令的执行也就结束了,我们的下一个状态将回到 S0: Fetch

3.8 执行 R-Type 指令

    回到解码状态 S1: Decode 完成之后,如果处理器发现它读取到的指令是一条 R-Type 指令,那么处理器的任务将是对两个寄存器的值进行计算之后写入到另一个寄存器中去。

    因此,如上图所示,我们首先来到 S6: Execute 状态,我们控制 `ALUSrcA` 信号为 1,`ALUSrcB` 信号为 00,以将 Register File 输出的两个寄存器值送上 ALU 进行运算。同时我们指定 `ALUOp` 信号为 10,以使得 ALU 的运算行为根据 funct 字段来决定,上面 基本思路 中我们已经给出了 ALU Decoder 的真值表。在完成运算后,ALU 的运算结果将被保存在 Non-architectural 寄存器 ALUOut 中。

    完成 ALU 运算后,我们来到 S7: ALU Writeback 状态,我们此时的任务是把运算结果写回 Register File。因此,我们控制 `RegDst` 信号为 1,以选择送上 Register File 地址线的是 R-Type 指令中的 rd 字段 (i.e. `Instr_{15:11}`); 控制 `Memt oReg` 信号为 0,以控制送上 Register File 写入数据端口的数据来自于存储在 Non-architectural 寄存器 ALUOut 中的上一状态 ALU 计算结果。同时我们使能 `RegWrite` 信号,以实现对 Register File 的写入操作。

3.9 执行 beq 指令

    回到指令获取状态 S0: Fetch 完成之后。如果我们读取到的指令是一条 beq 跳转指令,那么处理器的任务是对两个寄存器的值进行减法操作,并且根据计算结果完成跳转操作,这个过程需要进行两次 ALU 运算: (1) 对寄存器的减法操作 和 (2) 计算新跳转地址。回顾我们上面在 解码 所进行的过程,我们发现在状态 S1: Decode 中,ALU 运算单元是空闲的。因此处于提高 CPU 器件利用率,避免增加额外时钟周期的目的考虑,我们的思路是: 我们可以在状态 S1: Decode 中首先完成对新跳转地址的运算 (p.s. 我们在 S0: Fetch 中已经完成了 `PC+4` 值的计算)。在 S1: Decode 中完成对新跳转地址的计算之后,我们再进入一个新的状态中完成对两个寄存器值的运算,并且得到相应的控制信号,以决定是否在下一个时钟上升沿到来的时候,将新地址写入 PC 寄存器中。

    基于上述思路,如上图所示,我们对 S1: Decode 状态输出的控制信号进行修改: 我们控制 `ALUSrcA` 信号为 0,`ALUSrcB` 信号为 11,以将 `PC+4` 和 `SignImm` 送入 ALU 进行运算,同时我们指定 `ALUOp` 信号为 00,以使得 ALU 进行加法操作。在计算完成后,我们会在 Non-architectural 寄存器 ALUOut 中保存 `PC+4+SignImm` 的值。

    完成了 S1: Decode 状态后,Control Unit 就能够判断读取的指令的类型了。如果处理器发现读取的指令是一条 beq 指令,那么它将来到 S8: Branch 状态。在这个状态下,我们的任务是实现对两个指定的寄存器的值的运算,并且输出相应的控制信号来决定是否更新 PC 寄存器 `PC+4+SignImm` 的值。因此,我们控制 `ALUSrcA` 信号为 1,`ALUSrcB` 信号为 00,以将 Register File 输出的两个寄存器值送上 ALU 进行运算。同时我们指定 `ALUOp` 信号为 01,以使得 ALU 进行减法运算。并且,我们还需要使能 `Branch` 信号为 1,以使得当 ALU 运算标志信号 `Zero` 信号为 1 时,控制 PC 寄存器写入的信号 `PCEn` 能为 1。另外,我们还需要使能 `PCSrc` 信号,以使得在 beq 跳转条件成立时,写入 PC 寄存器的值来源于上一个状态 S1: Decode ALU 运算的结果。

    S8: Branch 状态完成后,beq 指令的运行就结束了,处理器将会回到 S0: Fetch 状态,读取出刚刚完成跳转的位置的指令进行下一指令的运行。

    现在,我们已经得到了 Control Unit 的 Moore 状态转移图,如下所示。

    下面我们将增加对另外两条指令 addij 的支持。

3.10 执行 addi 指令

    在完成 S1: Decode 后,我们如果发现读取的指令是一条 addi 指令,那么我们将进入 S9: ADDI 状态。addi 指令的任务是完成对一个寄存器值 (i.e. 由 rs, `Instr_{25:21}` 指定) 和一个立即数值 (i.e. 由 imm, `Instr_{15:0}` 指定) 的加法操作,并写入另一个寄存器 (i.e. 由 rt, `Instr_{20:16}` 指定) 中。

    如上图所示,来到 S9: ADDI 状态,我们首先应该先完成 ALU 运算。因此,我们控制 `ALUSrcA` 信号为 1,`ALUSrcB` 信号为 10,以将指定寄存器值和 `SignImm` 送上 ALU 进行运算。同时我们指定 `ALUOp` 信号为 00,以使得 ALU 进行加法运算。在完成运算之后,运算结果将被保存在 Non-architectural 寄存器 ALUOut 中。

    在完成 ALU 运算后,我们来到 S10: ADDIWriteback 状态,我们此时将上一个状态计算得到的值写回 Register File 的相应寄存器中。我们控制 `RegDst` 信号为 0,以使得送上 Register File 的写寄存器选择端口来自于 addi 指令的 rt 字段 (i.e. `Instr_{20:16}`),同时我们控制 `Memt oReg` 信号为 0,使得写入 Register File 的数据来源于上一个时钟周期 ALU 的计算结果。我们还需要使能 `RegWrite` 信号,以完成对 Register File 的写入操作。

3.11 执行 j 指令

    j 指令是 J-Type 指令,其用于向 PC 寄存器中写入一个新的地址值。基于指令 j 进行跳转时,PC 中存储的 32-bits 地址值的最低 2 位 `PC_{1:0}` 恒为 0 (i.e. 存储的指令是 32-bits 对齐的),接下来的 26 bits `PC_{[27:2]}` 是从指令 jimm 字段 (i.e. `Instr_{[25:0]}`) 中进行提取的,最高的 4 bits `PC_{[31:28]}` 保留的是 PC 寄存器中原先的最高 4 位。

    由于我们上面设计的数据通路并没有实现上面 j 指令描述的计算逻辑,因此,我们需要对数据通路进行修改。如上图所示,为了完成上述复杂的 PC 值计算方法,我们拓展了写入 PC 寄存器输入端口 PC' 的来源: 我们将控制信号 `PCSrc` 拓展成为 `PCSrc_{1:0}`,同时相应的多路选择器也进行了扩展。当 `PCSrc_{1:0}` 为 10 时,写入 PC 的值将是上述 j 指令合成的 PC 值。

    如上所示,在完成 S1: Decode 状态后,如果处理器发现读取的指令是一条 j 指令,那么将会进入 S11: Jump 状态。在这个状态下,我们控制 `PCSrc` 信号为 10,以选择正确的写入 PC 寄存器的来源,同时我们还会使能 `PCWrite` 信号,以实现对 PC 寄存器的写入。同时值得注意的是,由于我们对 `PCSrc` 实现了扩展,先前我们对 `PCSrc` 控制信号的使用也被修改成为了相应的值,在图中已经用蓝色标出。

    现在,我们已经完成了对多周期 MIPS 处理器的设计。

4. 性能分析

4.1 程序运行时间

    相比于单周期 MIPS 处理器,多周期处理器不同的是,不同的指令有不同的运行时钟周期: beqj 指令消耗 3 个时钟周期,swaddi 和 R-Type 指令消耗 4 个时钟周期,lw 指令消耗 5 个时钟周期。因此,不同的指令有不同的 CPI 值。针对不同的评估程序,有不同的指令成分组成。因此,我们在讨论多周期处理器的性能之前,我们必须先确认我们用于评估处理器性能的程序的成分。计算方法很简单:

`\overline{CPI} = \sum_{Instr}Rate_{Instr} \cdot CPI_{Instr}`

4.2 指令运行时间

    让我们回到对一条指令的运行时间上来。同样是分析 Critical Path,对于一条指令的运行来说,在一个时钟周期内,处理器需要执行 ALU 操作、读/写 Memory 和 读/写 Register File 等操作。我们假设:

  1. 对 Register File 的访问快于对 Memory 的访问;
  2. 对 Memory 的写操作快于对 Memory 的读操作

    在一条最糟糕的指令的一个步骤中,它首先需要从内存中读取相应的数据,并且将其送入 Register File 中,然后根据 Register File 的输出进行 ALU 运算。因此对于一个时钟周期来说,我们可以得到约束其最小值的等式:

`T_c \geq t_{pcq} + t_{m ux} + max(t_{ALU}+t_{m ux}, t_{mem}) + t_{setup}`

    其中 `t_{pcq}` 是传播时间; `t_{m ux}` 是 Memory 地址线前多路选择器的传播延迟; `t_{ALU}+t_{m ux}` 是输入源多路选择器和 ALU 计算的共同延迟; `t_{mem}` 是 Memory 的读取时间; `t_{setup}` 是建立时间。