4 Processor
约 4622 个字 20 张图片 预计阅读时间 15 分钟
Abstract
计组的这个章节是要完成信电配套的计组实验课lab30必须要学会的部分;
前人的笔记真的写的相当好了,作为后辈俺进行了一些整合,因此有了这份Note。
参考(几乎是照抄)
咸鱼暄前辈的Note
NoughtQ前辈的Note
由于整合后的风格和之前都差的很多了,所以就发在这里而没有提pr
在整合这份笔记的时候还用了Gitzip插件,一并致谢。
Warning
本文档正在完善!!现在还处于抄写阶段QAQ
4.1 DataPath
4.1.1 Overview
Info
这一部分有个大致概念就好,后面会具体再分析。
RISC-V指令集架构下的最最简单的CPU应该要实现以下核心指令:
- 内存引用指令(memory-reference instructions):
ld、sd... - 算术逻辑指令(arithmetic-logical instructions)/R型指令:
add、sub、and、or... - 条件分支指令(conditional branch instructions):
beq...
下面简单介绍一下如何实现这些指令:
- 最开始的两步是相同的:
- 取指(IF):根据PC(程序计数器)值(即当前指令的地址),从指令内存中获取当前周期下需要执行的指令
- 译码(ID):根据指令的字段识别指令类型,并从单个或多个寄存器内读取数据(load指令只需读取一个寄存器即可),也可以不读取数据(比如
jal指令)
- 后面的步骤则因指令类型而异:
- 执行(EX):大多数指令都会用到ALU,但是有不同的目的:
- 内存引用指令:地址计算(利用加法)
- 算术逻辑指令:执行算术逻辑运算
- 条件分支指令:判断两数的大小关系,同时计算候选的跳转地址(利用减法)
- 无条件分支指令:计算跳转地址(但不是在ALU上完成的)
- 访存(MEM)/写回(WB):ALU完成任务后,后续操作的区别会更大:
- 内存引用指令:
- 加载指令:读取数据(访存 + 写回)
- 存储指令:写下数据(访存)
- 算术逻辑指令:将ALU的计算结果写入寄存器中(写回)
- 条件分支指令:既没有访存也没有写回
- 无条件分支指令:一般情况下,返回地址(PC + 4)会被写入寄存器
x1里(写回);若不用返回地址,也可以将返回地址传给x0
- 内存引用指令:
- 执行(EX):大多数指令都会用到ALU,但是有不同的目的:
注:综上,绝大多数指令只需四步即可完成,只有加载相关的指令需要五步。
这是一张 RISC-V 核心指令的实现图(并不完整),之后我们会用这张图进行分析。

在进入正式的DataPath讲解之前,我们先回顾一下第二章学过的指令及指令格式:

4.1.2 R 型指令
add,sub,and,or这几个 R 型指令总共需要访问 3 个寄存器,如下图所示:

- (1) 处取出指令,
[6:0](funct7)被送到 Control 产生对应的控制信号,我们稍后可以看到;[19:15],[24:20],[11:7]分别对应rs1,rs2,rd,被连入 Registers 这个结构,对应地Read data 1和Read data 2两处的值即变为rs1,rs2的值; - (2) 处 MUX 在
ALUSrc = 0的信号作用下选择Read data 2作为 ALU 的输入与Read data 1进行运算,具体的运算由ALU control提供的信号指明(我们在 4.1.3 小节 讨论这个话题)。运算结果在ALU result中。
Tip
ALUSrc:决定 ALU 的第 2 个操作数——低电平时 ALU 获取第 2 个寄存器的值,高电平时 ALU 获取立即数
- (3) 处 MUX 在
MemtoReg = 0的信号作用下选择ALU result作为写回 Register 的值,连到 (4) 处;在 (5) 处RegWrite = 1信号控制下,该值写入到rd寄存器中。
这就是 R 型指令的运行过程。执行完指令后 PC 会 +4,我们在 4.1.4 小节 讨论这一操作的实现。
Info
有聪明的小朋友可能会问,为什么需要 RegWrite 这个控制信号呢?非常简单,因为 Write register 和 Write data 这两条线一直连着对应的端口,但不是每个指令都需要写入寄存器。如果在没必要的时候写入,就篡改原先的寄存器值了。Reg files 需要知道什么时候需要写入寄存器,因此只有当 RegWrite = 1 时才会被对应地写入。
聪明的小朋友可能又会问了,为什么 PC 寄存器也要写入(插播:PC寄存器不是那个大大的存储指令地址的地方!是下条指令的地址,会变的!) ,但是不需要控制信号呢?非常简单,因为 PC 在 每个 时钟周期都会被写入,所以只需要在时钟的 每个 上升沿或者下降沿触发就好了(我们采取的设计是下降沿触发,我们在 4.2.3 小节 再讨论为什么这样设计),不需要额外的控制信号了。
4.1.3 ALU Control
为什么要有ALU Control?
我们之前在第三章里设计的ALU整合了add/subtract/and/or,除了操作数,需要Control信号来告诉ALU到底执行什么指令!
在之前的章节里,我们设计的 ALU 需要这样的控制结构:

我们列一下需要使用 ALU 的指令的表格(我们在):

我们根据这个表列出真值表:

其中可以发现,标为绿色的列的取值要么是 0 要么是无关的,因此它们并不影响结果。
根据这个真值表构造门电路,就可以构造出 ALU control 单元了。如图中所示,该单元依赖 Control 单元给出的 ALUOp 信号以及 I[30, 14:2] :

Info
ALU control 模块可以这样实现:

需要理解的是,我们并不是根据机器码来构造电路的,而是相反:电路的设计参考了很多方面的问题,机器码应当主要迎合电路使其设计更加方便。
4.1.4 跳转指令与 Imm Gen 模块
- 在一条指令运行完后,如果不发生跳转,PC + 4,否则 PC 跳转到 PC + offset 的位置去。这个过程是如何完成的呢?看下图:

跳转分为两部分:判断条件是否成立/跳转到对应语句
4.1.4.1 判断条件是否成立
- (1) 中有两个加法器,一个的结果是 PC + 4,另一个是 PC + offset,其中 offset 是来自当前 instruction 的;这两个加法器通过 MUX 送给 PC
- MUX 的控制信号来自 (2), (2) 是一个与门,即当且仅当两个输入信号都为真时才会输出 1,从而在上述 MUX 中选择跳转。 (2) 的两个输入分别来自:
- (5) 这个 ALU 的 Zero 信号,这是第 3 章中我们设计的可以用来实现
beq的结构;我们讨论过实现beq其实就是计算rs1 - rs2判断其是否为 0,所以这里根据 Zero 是否为 0 就能判断两个寄存器是否相等 - (4) 处 Control 给出的
Branch信号,即如果这个语句是跳转语句,那么对应的信号会置为 1
- (5) 这个 ALU 的 Zero 信号,这是第 3 章中我们设计的可以用来实现
也就是说,当且仅当语句确实是 beq 而且 Zero 信号的值确实为 1 时才会进行跳转。
4.1.4.2 跳转到对应语句
- 再来看看当进行跳转的时候, (3) 处的 offset 来自哪里。我们可以看到,实际上这个 offset 来自于
I[31:0],也就是整个指令;它被传给 Imm Gen 模块,将指令中的立即数符号扩展到 64 位;然后在 (3) 处左移 1 位,再与 PC 相加。
SB-Type
回顾一下我们之前学的SB-Type【条件分支(inst rs1, rs2, imm[12:1])】:
| Name | 7 bits | 5 bits | 5 bits | 3 bits | 5 bits | 7 bits |
| ------- | -------------- | ------ | ------ | ------ | --------------- | ------ |
| SB-type | immed[12,10:5] | rs2 | rs1 | funct3 | immed[4:1,11] | opcode |
- 可以看到,指令里一共有
imm[12:1]的立即数字段,缺了个imm[0],这是因为RISC-V 指令总是 2 字节对齐,所以末尾一定是0。那么为了让尽可能多的位表示立即数,因而规定imm[0] = 0。因此立即数能够表示的范围为-4096\~4094(-1000\~0FFE),且都是偶数。- 因此,我们在设计这里的跳转的时候,也需要**考虑这一点
Imm Gen 模块
Imm Gen 模块在load 类指令、 store 类指令和 branch 类指令里都要用到。这里我们先看branch类里,这个模块在参与什么事情:
- 将指令中的立即数符号扩展到 64 位(为什么?)
为什么符号扩展到64位?
- 为什么扩展?
- ALU需要位数对齐(很显然,是逐位扔进加法器的)
- 为什么符号扩展?
- 我们来看看这个立即数后面都会参与什么运算:
- 算术运算 (
addi,slti) - 内存访问(
base+offset) - branch(
PC+offset)
- 算术运算 (
- 一看就知道大家都是有符号运算 ac01
- 在 (3) 处左移 1 位(为什么?SB-Type的特殊性)
- 与 PC 相加。
其中2.3. 都是在图示的(3):Add模块处实现的。
关于Imm Gen模块怎么区分三类指令
-
为什么要区分?
A:这三类指令虽然都包含立即数,但它们在32位指令中的编码格式完全不同: -
I-type (Load):立即数是连续的12位
I[31:20]。 - S-type (Store):立即数被拆分成了两部分
I[31:25]和I[11:7]。 - B-type (Branch):立即数被拆分得更零散,分布在
I[31],I[7],I[30:25]和I[11:8]。 - 怎么区分?—— 看
I[6:5]
| 指令类型 | 例子 | 格式 | Opcode I[6:0] |
关键位 I[6:5] |
|---|---|---|---|---|
| Load | ld, lw |
I-type | 0000011 |
00 |
| Store | sd, sw |
S-type | 0100011 |
10 |
| Branch | beq, bne |
B-type | 1100011 |
11 |
4.1.5 Load 指令和 Store 指令
最后我们来看Load和Store指令:

4.1.5.1 Load 指令
load 指令的作用是从内存中读取数据,并将其存入一个寄存器。 如:ld rd, offset(rs1)
- (1) 处取出指令,控制单元(Control)识别出这是一条
load指令。指令的[19:15]字段对应rs1(基地址寄存器),其地址被送入Registers结构,对应的值从Read data 1端口读出。 -
指令中用于表示
offset的部分被送入 Imm Gen 模块进行符号扩展,生成一个64位的立即数。 -
(2) 处 MUX 在
ALUSrc = 1的控制信号作用下,选择Imm Gen扩展后的立即数作为 ALU 的第二个输入。ALU 将Read data 1(rs1的值)与该立即数(offset)相加,计算出最终要访问的内存地址。运算结果在ALU result中。 -
计算出的地址(
ALU result)被送入Data memory的Address端口。在控制信号MemRead = 1的作用下,Data memory从该地址读取数据。 -
(3) 处 MUX 在
MemtoReg = 1的控制信号作用下,选择从Data memory读出的数据(MUX 的输入1),并准备将其写回寄存器。 -
(4) 处写回,上一步 MUX 选择的数据被送到
Registers的Write data端口。指令的[11:7]字段(对应目标寄存器rd地址)被送到Write register端口。最后,在RegWrite = 1信号作用下,从内存中取出的数据被成功写入目标寄存器rd中。
4.1.5.2 Store 指令
store 指令的作用是将一个寄存器中的数据存入内存的特定位置,如:sd rs2, offset(rs1)
-
(1) 处取出指令,控制单元识别出这是一条
store指令。[19:15]字段对应rs1(基地址寄存器),其值从Read data 1读出;[24:20]字段对应rs2(源数据寄存器),其值从Read data 2读出。指令中的offset部分同样被 Imm Gen 模块符号扩展到64位。 -
(2) 处 MUX 与
load指令一样,在ALUSrc = 1信号作用下选择Imm Gen的输出。ALU 将Read data 1的值(rs1)与立即数(offset)相加,计算出要写入的内存地址。运算结果在ALU result中。 -
计算出的地址(
ALU result)被送入Data memory的Address端口。与load不同的是,rs2的值(从Read data 2读出)送到了Data memory的Write data端口。 -
在控制信号
MemWrite = 1的作用下,Data memory将Write data端口上的数据(即rs2的值)写入由Address端口指定的内存单元中。 -
对于
store指令,它不需要写回任何结果到寄存器堆。因此,控制信号RegWrite会被设置为0,(4) 处的写回操作不会发生,从而保证了没有寄存器被错误地修改。
4.1.6 Control
看完上述若干小节,control 单元的设计也非常显然了。我们很容易给出如下真值表:

后面就是连电路的工作了。连出来长这样:

4.2 Pipeline
4.2.1 Intro
在小学奥数中我们就学过,并行能够提高整体的效率,例如这个洗衣服的例子:

对于单个工作,流水线技术并没有缩短其运行时间;但是由于多个工作可以并行地执行,流水线技术可以更好地压榨资源,使得它们被同时而不是轮流使用,在工作比较多的时候可以增加整体的 吞吐率 throughput,从而减少了完成整个任务的时间。
在本例中,由于流水线开始和结束的时候并没有完全充满,因此吞吐率不及原来的 4 倍(4 来自于例子中有 4 个步骤);但是当工作数足够多的时候,吞吐率就几乎是原来的 4 倍了。
回到 RISC-V 中来,一个指令通常被划分为 5 个阶段:
- IF, Inst Fetch,从内存中获取指令
- ID, Inst Decode,读取寄存器、指令译码
- EX, Execute,计算操作结果和/或地址
- MEM, Memory,内存存取(如果需要的话)
- WB, Write Back,将结果写回寄存器(如果需要的话)
各阶段会用到的组件如下图所示(这个图还有很多问题,我们后面慢慢讨论~),可以看到这些部分是可以并行执行的(比如 Reg File 可以一边读一边写):

其加速的核心主旨是,它们都希望一个周期能完成一条指令,但是单周期 CPU 的一个周期需要承担一个指令的所有步骤;而流水线技术引入后,由于它可以并行地同时执行五个阶段的步骤,所以此时的周期只需要一个阶段的长度。总的来说,单周期 CPU 的时钟周期由总耗时最长的指令决定,流水线 CPU 的时钟周期由耗时最长的指令阶段(IF, ID 等)决定。
也就是说,我们本来是在一个周期中完成一个指令,而现在是在一个周期中完成五个不同指令的不同阶段。当然,每个时钟周期的长度也需要足够任何一个阶段完成执行。
RISC-V 也有很多流水线友好的设计,例如:
- 所有 RISC-V 的指令长度相同,这可以方便
IF和ID的工作 - RISC-V 的指令格式比较少,而且源寄存器、目标寄存器的位置相同
- 只在 load 或 store 指令中操作 data memory 而不会将存取的结果做进一步运算,这样就可以将
MEM放在比较后面的位置;如果还能对结果做运算则还需要一个额外的阶段,此时流水线的变长并没有什么正面效果
Hazards 指的是阻止下一个指令在下一个时钟周期完成的一些情况。主要分为三种:
- Structure hazards
- 一个被请求的资源仍处于忙碌状态(大多数现代处理器基本不存在这样的问题,可以通过设计ISA解决)
- Data hazards
- 需要等待上一个指令完成数据读写(本质上是由于指令之间的依赖关系产生的。如果有一条正在执行的指令要将结果写入寄存器文件,那么后面的指令不能依赖于它写入之前的那个操作数。)
- Control hazards
- 一些控制取决于上一条指令的结果(在分支指令中比较常见,若下一条指令在分支指令之后,那么如果分支指令被执行进行了跳转,下一条指令可能无效。)
4.2.2 Structure Hazards
① 两条或多条指令需要同时访问同一个物理资源,从而产生竞争
解决方式:
a) 大家轮流使用。意味着一些指令需要暂停(stall),释放之后再用 —— 会导致效率降低
b) (Final) 增加更多硬件资源,一人用一个 (例如在单周期CPU的设计中,取指令地址的PC+4和R-指令的ALU用的不是同一个ALU)
② 不同指令需要读取同一个物理资源,产生冲突
解决方式:
设置两个独立的读端口和一个独立的写端口,可以实现一个时钟周期内最多三次并发访问(两次读,一次写),从而避免了寄存器访问的结构性冒险
③ 当指令获取(Instruction Fetch)和数据访问(如 load/store 指令)需要同时访问内存时,如果只有一个内存单元,就会产生冲突
解决方式:
- 使用两个内存:
- 在处理器芯片上有一些内存块,包含了从主内存复制的指令副本和数据副本,并有两种独立的方式访问他们,就不必争夺主内存。
- 当我们获取指令(IMEM)的时候,另一条指令可以同时执行读取或者写入的操作

因此,以上问题都可以通过ISA设计和硬件设计解决(以及内存+内存副本)。
4.2.3 Data Hazards
让我们利用【单时钟周期流水线图(single-clock-cycle pipeline diagrams)】来回顾一下之前学的单周期CPU。

适用于Pipeline-流水线的改造:插入寄存器

在取指结束后,插入两个寄存器:
- 一个用于程序计数器(PC)
- 一个用于指令的寄存器(\(\text{inst}_{\text{ID}}\))
因为五级流水线有五条指令在同时执行,所以我们会在下游保存之前获取的四条指令的副本。
每个流水线寄存器都必须保存与在该特定阶段执行的指令相对应的位,因此该阶段需要保存指令+对应的控制位。
另一个设计的不同是:我们实际上在内存访问阶段才执行PC+4,移到下一条命令。