04 A Simple Implementation Scheme
这一节我们会使用上一节的数据通路并添加一些简单的控制元件构建简单实现,这种简单实现将会覆盖 lw
sw
beq
add
sub
and
or
。
The ALU Control
RISC-V 的 ALU 有如下四种控制输入。根据指令分类,ALU 会执行四种功能中的一种。对于加载和存储指令,ALU 通过加法计算内存地址。对于 R 指令,ALU 会根据 7 bits funct7 字段( bits 31:25)和 3 bits funct3 字段(bits 14:12)执行四种功能的一种。对于有条件的分支跳转,ALU 执行减法然后判断结果是否为零。
ALU control lines | Function |
---|---|
0000 | AND |
0001 | OR |
0010 | add |
0110 | subtract |
我们使用一个小型控制单元来生成 4 bits ALU 的输入,该控制单元的输入是 funct7、funct3 字段以及一个 2 bits 控制字段,这 2 bits 称之为 ALUOp。ALUOp 表示对于加载和存储指令要执行加法(00),对于 beq
做减法并判断是否为零(01),根据 funct7 和 funct3 字段决定操作(10)。ALU 的控制单元输出 4 bits 信号,如下面的表所示来控制 ALU。
下图展示了 ALUOp、funct7、funct3 是如何控制 ALU 的。
这种多级解码是一种常见实现技术,主控制单元生成 ALUOp,结合其他字段生成 ALU 控制信号,最终控制 ALU。多级设计可以减少主控制单元的大小,使用若干更小的控制单元可以减少控制单元的延迟。这种优化非常必要,因为控制单元的延迟往往决定了时钟周期。
有好几种方式实现 ALUOp 和 funct 字段到 4 bits ALU 控制比特的映射。因为只有当 ALUOp 是 10 时我们才需要关注 funct 字段,可以使用很少的逻辑就能识别可能得子集并生成适当的 ALU 控制信号。
作为设计逻辑的一步,一个有用的方式就是列一个真值表(truth table
),将我们感兴趣的 ALUOp 和 funct 字段列出来。完整的真值表很大,但是幸好我们仅仅需要列出其中很小的一部分,将不感兴趣的部分都忽略掉。
不关心或者说不会影响真值表的项称为不关心项(don't-care terms
),在下表中,使用 X 表示,表示最终结果不依赖于当前列。比如当 ALUOp 是 00 时,ALU 控制 bits 结果是 0010,与 fucnt 字段无关,那么后者就是不关心项。
一旦列出了真值表,那么就可以优化并设计门电路了。这些过程非常机械。
Designing the Main Control Unit
现在,已经有了一个使用 funct 字段和 2 bits 控制信号作为输入的 ALU,接着看一下其他部分的设计。开始之前,我们先确定指令字段和上一节中图 4.11 数据通路所需要的控制线。要理解如何将指令的字段关联到数据通路,我们先回顾一下四种指令类别:算术、加载、存储、条件分支。如下图所示。
观察这些指令,我们可以得到如下结论:
- opcode 字段,始终是 bits 6:0,对于不同的 opcode,funct3 和 funct7 字段是 opcode 的拓展。
- 第一个操作数寄存器总是位于 bits 19:15,即 rs1。对于加载和存储指令,这个字段指定了基地址寄存器。
- 第二个操作数寄存器总是位于 bits 14:20,即 rs2。对于存储指令,表示要被拷贝数据的寄存器。
- 对于加载、存储、分支指令,其他操作数是 12 bits 的偏移量。
- 目标寄存器总是 bits 11:7,即 rd。
第二章介绍的设计原则——简单有利于规律性——在这里也试用。
插播一段信息。与 MIPS 相比,RISC-V 指令格式看起来要复杂,但是实际上却简化了硬件。这能帮助提高一些 RISC-V 实现的时钟周期,特别是 4.6 节介绍的流水线版本。
下图是 MIPS 算术指令和数据转移指令的格式。
MIPS 需要一个 2:1 的多路复用器来指定哪些 bits 表示写数据的寄存器。这并不必要。多路复用起可能会使得时钟周期变长。
RISC-V 的寄存器地址始终不变,虽然格式有点奇怪,因为与 MIPS 相比立即数 bits 不连续,但是汇编器隐藏了这种复杂性,而使得硬件受益。
下一个例子就更奇怪了。下图是两种指令格式,SB 和 S,UJ 和 U,各个字段大小一样,但是立即数是两种不同的格式,bits 旋转了一下。
这样设计简化了硬件,但是汇编器需要做更多的事情。下图显示了不同指定下立即数的 bits。如果 SB 和 S 格式一样,UJ 和 U 格式一样,那么最后一行表示每个输出 bit 对应的唯一输入个数,而后者决定了多路复用器的端口数。
相应的,如果分开设计,那么能够减少多路复用器的端口数,对于 bits 19:12,从 3:1 减少到 2:1,bits 10:1 从 4:1 减少到 2:1。虽然看起来很奇怪,但是简化了 18 个多路复用器。对于高端处理器而言,这个节省没有差别,但是对于低端处理器而言却意义重大,不过重担转移给了汇编器。
接着继续数据通路。下图展示了如何添加 ALU 控制块、状态元件的写入信号、数据存储的读信号、多路复用器的控制信号。多路复用器有两个输入,每一个都需要一条控制线。
与 4.11 相比,这张图的主要修改在于中间底部的 ALU 控制单元,多路复用器对应的 0/1 输入信号,以及标识出了指令的 bits 范围。多路复用器的六个信号由淡蓝色字标识。下图描述了这六种控制线的功能。
上图是功能描述,下面讨论如何设置它们。除了 PCSrc 之外,仅仅依赖于 opcode 和 funct 字段。对于 PCSrc,如果指令是分支跳转且 ALU 输出是零,那么需要设置该信号,即对控制单元的信号(分支跳转)和 ALU 输出的 Zero 信号做 AND 运算。
至此,八个信号(六个涉及多路复用器的信号和两个 ALUOp)均可以由控制单元的输入信号(opcode bits 6:0)来设置。下图是添加了控制单元和控制信号的数据通路。
在列出控制单元的真值表之前,我们先来非正式地定义一下控制功能。由于控制线的设置仅仅依赖于 opcode,那么对于每一个 opcode 值,每个控制信号应该是 0,1 或者是无关项。下图展示了对于每一个 opcode,这些控制信号应该如何设置。这些信息可以从图 4.12,4.20,4.21 直接推导得到。
Operation of the Datapath
在继续控制单元之前,我们先来看看每一个指令是如何使用数据通路的。在下面几个流程图中,控制信号和激活的数据通路都会被高亮出来。即使控制线没有被高亮,信号为 0 的多路复用器也有明确的操作。如果多路复用器的任意组成部分是断言的,那么对应信号会被高亮。
下图是 R 指令(比如 add x1 x2 x3
)的数据通路的使用情况。尽管所有的事情发生在一个时钟周期,但是我们可以想象成四个步骤。
- 取指令,PC 自增。
- 从寄存器堆中读取
x2 x3
的内容,同时,主要控制单元根据 opcode 设置控制线。 - ALU 对从寄存器堆中读取的数据进行运算,使用 opcode 的部分生成 ALU 功能(选择一个运算)。
- 将 ALU 的输出结果写会寄存器堆
x1
寄存器中。
加载指令,比如 lw x1, offset(x2)
很类似。下图是操作流程。类似的,可以将其分成五个部分。
- 从指令存储中取指令,PC 自增。
- 从寄存器堆中读取
x2
的值。 - ALU 计算从寄存器堆中读取的值与指令中 12 bits 有符号扩展数(
offset
)之和。 - ALU 的结果用作数据存储的地址。
- 将数据存储的数据写到寄存器堆(
x1
)中。
最后,展示分支跳转(比如 beq x1, x2, offset
)操作。和 R 指令类似,不过 ALU 的输出用于确定 PC 被回写的值是 PC +4 还是跳转目标。
- 取指令,PC 自增。
- 从寄存器堆中读取
x1 x2
的值。 - ALU 对从寄存器堆中读取的值做减法操作。右上角的加法器将 PC 的值与 12 bits 有符号扩展数
offset
左移一位的值相加,结果是分支跳转目标。 - ALU 输出的 Zero 信息被用于决定哪一个加法器的结果会被写会 PC。
Finalizing Control
控制功能的精确定义就是表格 4.22。输出是控制线,输入是 opcode。因此可以基于 opcode 列一个输出的真值表。如下图所示。
Why a Single-Cycle Implementation is not Used Today
上述简单实现能工作,但是不高效。原因是对于每一个指令而言,时钟周期必须是一样长。最长的路径决定了时钟周期。大概率是加载指令,因为它使用了五个功能单元:指令存储、寄存器堆、ALU、数据存储、寄存器堆。尽管 CPI 是 1,但是由于时钟周期太长,简单实现的性能还是很差。
使用固定周期的单一周期设计的代价比较大,不过对于很小的指令集而言也是可以接受的。早期的计算机指令集很小,使用了这种技术。不过如果我们试图实现浮点数单元或者更复杂的指令集,这种方法就不可行了。
由于我们必须假设时钟周期等于所有指令最差情况下的延迟,那么试图减少常见情况的延迟而不提升最差情况的延迟对提升整体性能而言没有帮助。单一周期的实现违反了第一章所述的原则——优化常见情况。
在 4.6 节,我们会尝试另外一种技术实现,称为流水线,使用的数据通路与单一周期的数据通路很类似,但是具有更高的吞吐,更高效。流水线是通过同时执行多个指令来提高效率的。