$ cat ~ / posts /jsjzcyl /jz4 5.9k Words ~ 22 Mins
cover.png
计组学习笔记3

#计组学习笔记3

exdoubled Lv5

单周期处理器中,每条指令都在一个时钟周期内完成

优点是控制简单,CPI=1

但缺点:

  • 时钟周期由最慢指令决定
  • 很多指令实际上不需要这么长时间
  • 需要多个加法器
  • 指令存储器和数据存储器通常要分开
  • 功能部件利用率低

最长的通常是 lw 指令: \[ PC \rightarrow Instruction\ Memory \rightarrow Register\ File \rightarrow ALU \rightarrow Data\ Memory \rightarrow Register\ File \]

如果时钟周期按 lw 设置,那么 j、R 型、beq 等指令都被迫等同样长的周期,效率很低

多周期处理器的思路是:

把一条指令拆成多个阶段,每个阶段用一个较短的时钟周期完成

这样不同指令可以使用不同数量的时钟周期

例如:

  • lw:5 个周期
  • R 型指令:4 个周期
  • beq:3 个周期
  • j:3 个周期

多周期处理器的 CPI 大于 1,但时钟周期明显变短,并且硬件可以复用

和单周期的性能取舍

单周期处理器的优点是每条指令 CPI=1,但是时钟周期等于最长指令时间

多周期处理器把一条指令拆成多个短周期,CPI 变大,但时钟周期缩短

所以不能只看 CPI,还要看: \[ CPU时间=IC\times CPI\times T \]

在访存时间很长的假设下,多周期未必一定比单周期快

因为多周期中很多指令仍要经过多个周期,而访存周期本身仍是瓶颈

多周期设计更重要的意义是:

  • 解释指令执行如何被分阶段控制
  • 展示功能部件复用
  • 引出 FSM 控制器
  • 为异常处理和流水线设计做铺垫

多周期的基本思想

多周期设计一般遵循:

  • 每个阶段最多完成一次访存、一次寄存器读写或一次 ALU 运算
  • 每个阶段的中间结果在下个时钟开始时保存
  • 功能部件在不同周期重复使用
  • 控制器按状态输出控制信号

和单周期相比,多周期处理器会增加一些临时寄存器和多路选择器

典型新增状态元件包括:

  • IR:指令寄存器,保存当前指令
  • MDR:存储器数据寄存器,保存读出的数据
  • AB:保存寄存器堆读出的两个操作数
  • ALUOutTarget:保存 ALU 计算出的地址或分支目标

重点强调 IRBranchTarget/TargetPCWriteIRWrite 等控制信号

核心区别是:

单周期中一条指令的数据流一次走完

多周期中数据流分阶段走,每个阶段结束后把必要结果暂存起来

状态

一个阶段就是一个状态

多周期处理器中,一条指令由多个状态完成

在指令译码前,所有指令经过的状态相同:

  • 取指令
  • 译码和读寄存器

译码后,根据 op 字段进入不同执行状态

比如:

  • lw/sw 进入地址计算状态
  • R 型进入 ALU 执行状态
  • beq 进入分支完成状态
  • ori/addiu 进入立即数运算状态
  • j 进入跳转状态

所以多周期控制器的本质是一个按指令类型分叉的状态机

临时寄存器

单周期中,数据可以从一个部件一路通过组合逻辑传到另一个部件

而多周期中,数据要跨周期使用,因此必须暂存

例如 lw

  • 第 2 周期读出的 R[rs] 要在第 3 周期用于地址计算,所以要存在 A
  • 第 4 周期从存储器读出的数据要在第 5 周期写寄存器,所以要存在 MDR
  • 指令字段要在多个周期中反复使用,所以要存在 IR

没有这些临时寄存器,后续周期就无法知道当前指令是什么,也无法使用前面周期产生的结果

竞争问题

在理想模型里,寄存器堆和存储器都像在时钟边沿写入

但实际硬件中,写操作更接近组合过程:

  • 地址稳定
  • 数据稳定
  • 写使能有效
  • 经过写访问时间后完成写入

如果写使能先到,而地址或数据还没稳定,就会产生竞争问题

寄存器堆中可能出现:

\[ Rw,\ busW \quad 和 \quad RegWrite \]

之间的竞争

存储器中可能出现:

\[ Adr,\ Din \quad 和 \quad MemWrite \]

之间的竞争

多周期可以把“准备地址和数据”和“写使能有效”拆到不同周期

也就是:

  1. \(N\) 个周期先让地址和数据稳定
  2. \(N+1\) 个周期再让写使能有效
  3. 写使能无效前,地址和数据不改变

这个细节很重要,否则机器可能出现很难复现的错误

竞争问题和状态拆分

特别强调,实际寄存器组和存储器的写操作不能简单理解成“时钟边沿自动写入”

如果 RegWrite 先有效,而 RwbusW 还在变化,就可能写错寄存器或写入错误数据。

因此有些设计会把“准备写地址和写数据”和“真正打开写使能”分成连续两个状态。

例如 R 型指令:

  • 执行状态:先让目的寄存器选择信号和 ALU 输出稳定,但 RegWrite=0
  • 写回状态:保持目的寄存器选择和 ALU 输出稳定,再令 RegWrite=1

这解释了为什么有些状态看起来“重复做同一个 ALU 运算”。

目的不是重新计算,而是为了让写端口数据稳定。

多周期数据通路控制信号

1167X889/12.png

多周期数据通路比单周期多了一些控制信号:

信号含义
PCWrite无条件写 PC
PCWriteCond条件写 PC,常用于分支
PCSrc选择 PC 的新值来源
IRWrite是否写指令寄存器
IorD存储器地址来自 PC 还是数据地址
MemWrite是否写存储器
RegDst写寄存器目的选择
RegWrite是否写寄存器堆
MemtoReg写回数据来自内存还是 ALU
ALUSrcAALU 第一个操作数选择
ALUSrcBALU 第二个操作数选择
ALUOpALU 操作类型
ExtOp立即数符号扩展或 0 扩展

其中 ALUSrcB 通常不只是 1 位,因为 ALU 第二操作数可能来自:

  • 寄存器 B
  • 常数 4
  • 扩展立即数
  • 扩展立即数左移 2 位

PCSrc 也可能是多位,因为 PC 可能来自:

  • ALU 计算出的 PC+4
  • 分支目标地址
  • 跳转目标地址
  • 异常处理入口地址

常见控制信号分组

为了便于理解,可以按控制对象分组

PC 相关:

  • PCWr:无条件更新 PC
  • PCWrCond:结合 Zero 条件更新 PC
  • PCSrc:选择写入 PC 的来源

存储器相关:

  • IorD:选择地址来自 PC 还是 ALU 计算出的数据地址
  • MemWr:写存储器
  • IRWr:把存储器输出写入 IR

寄存器堆相关:

  • RegDst:写寄存器编号选 rt 还是 rd
  • RegWr:写寄存器堆
  • MemtoReg:写回数据来自存储器输出还是 ALU 输出

ALU 相关:

  • ALUSelA:ALU A 输入来自 PC 还是寄存器 A
  • ALUSelB:ALU B 输入来自常数、寄存器 B、立即数等
  • ALUOp:ALU 运算类型
  • ExtOp:立即数扩展方式

分支目标相关:

  • BrWrTargetWr:把投机计算出的分支目标保存起来

这些控制信号在每个状态都有固定取值

取指周期

所有指令的第一个周期相同

取指周期完成: \[ IR \leftarrow M[PC] \]

\[ PC \leftarrow PC+4 \]

控制信号为:

  • IorD=0,存储器地址来自 PC
  • IRWrite=1,把读出的指令写入 IR
  • ALUSrcA=0,ALU 第一个操作数来自 PC
  • ALUSrcB=01,ALU 第二操作数为 4
  • ALUOp=Add
  • PCSrc=0
  • PCWrite=1
  • MemWrite=0

取指结束后,PC 已经变成下一条顺序指令地址

但当前指令保存在 IR 中,所以后续周期不会因为 PC 改变而丢失当前指令

取指状态的 RTL

取指状态可以写成: \[ IR\leftarrow M[PC] \]

\[ PC\leftarrow PC+4 \]

这两个操作在同一个状态内并行完成

这里 ALU 被用来计算 PC+4,存储器被用来读指令

因此本状态中:

  • ALU A 输入选 PC
  • ALU B 输入选 4
  • 存储器地址选 PC
  • IRWr=1
  • PCWr=1

取指结束后,PC 已经指向顺序下一条指令,而 IR 保存当前指令

译码和读寄存器周期

第二个周期也基本是公共的

完成:

\[ busA \leftarrow RegFile[rs] \]

\[ busB \leftarrow RegFile[rt] \]

并根据 opfunc 字段译码

ALU 在这个周期本来可能空闲,所以可以顺便投机计算分支目标地址: \[ Target \leftarrow PC+SignExt(imm16)\times 4 \]

注意在上个周期 PC 已更新,此时的 PC 已经是 PC+4,所以这个式子实际就是: \[ Target=(PC+4)+SignExt(imm16)\times 4 \]

控制信号为:

  • ALUSrcA=0
  • ALUSrcB=10 或选择扩展立即数左移 2 位
  • ALUOp=Add
  • ExtOp=1
  • BrWrite=1TargetWrite=1
  • 其他写使能关闭

这样 beq 后续只需比较两个寄存器是否相等,不必再花一个周期计算分支目标。

译码状态的 RTL

译码状态至少完成: \[ A\leftarrow R[rs] \]

\[ B\leftarrow R[rt] \]

同时投机计算: \[ Target\leftarrow PC+SignExt(imm16)\times 4 \]

因为取指后 PC 已经是 PC+4,所以这里的 PC 其实就是分支公式里的 PC+4

这个投机计算对非分支指令没有副作用

如果当前指令不是分支,Target 的值不会被使用

这体现了硬件设计中一种常见思想:空闲部件可以提前计算可能有用的结果

lw 指令

lw rt, imm(rs) 的目标是: \[ R[rt]\leftarrow M[R[rs]+SignExt(imm16)] \]

执行阶段如下:

S0:取指

\[ IR \leftarrow M[PC],\quad PC\leftarrow PC+4 \]

S1:译码和读寄存器

读出 rsrt,并计算可能的分支目标

S2:计算存储器地址

\[ Addr \leftarrow R[rs]+SignExt(imm16) \]

控制:

  • ALUSrcA=1
  • ALUSrcB=10 或选择扩展立即数
  • ALUOp=Add

S3:读存储器

\[ MDR \leftarrow M[Addr] \]

控制:

  • IorD=1
  • MemWrite=0

S4:写寄存器

\[ R[rt]\leftarrow MDR \]

控制:

  • RegDst=0
  • MemtoReg=1
  • RegWrite=1

所以 lw 通常需要 5 个周期

lw 的状态序列

可以把 lw 写成: \[ IFetch \rightarrow RFetch/ID \rightarrow MemAdr \rightarrow MemFetch \rightarrow lwFinish \]

各状态功能:

状态功能
IFetch取指,PC+4
RFetch/ID译码,读寄存器,计算分支目标
MemAdr计算数据地址
MemFetch从数据存储器读数据
lwFinish写回 rt

MemAdr 中保证地址计算结果稳定

MemFetch 中用这个地址访问存储器

lwFinish 中才打开寄存器堆写使能

sw 指令

sw rt, imm(rs) 的目标是: \[ M[R[rs]+SignExt(imm16)]\leftarrow R[rt] \]

前面三个周期和 lw 相同:

  1. 取指
  2. 译码和读寄存器
  3. 计算地址

最后一个周期写存储器: \[ M[Addr]\leftarrow R[rt] \]

控制:

  • IorD=1
  • MemWrite=1

sw 不需要写回寄存器,所以通常 4 个周期完成

sw 的状态序列

sw 状态序列为: \[ IFetch \rightarrow RFetch/ID \rightarrow MemAdr \rightarrow swFinish \]

lw 共用 MemAdr 状态

区别在 MemAdr 之后:

  • 如果是 lw,进入 MemFetch
  • 如果是 sw,进入 swFinish

swFinish 中,必须保持地址和写入数据稳定,然后令 MemWr=1

写入数据来自前面保存的寄存器 B,也就是原来的 R[rt]

R 型指令

add rd, rs, rt 为例: \[ R[rd]\leftarrow R[rs]+R[rt] \]

流程:

  1. 取指
  2. 译码和读寄存器
  3. 执行 ALU 运算
  4. 写回寄存器

第三周期: \[ ALUOut \leftarrow R[rs]\ op\ R[rt] \]

控制:

  • ALUSrcA=1
  • ALUSrcB=00
  • ALUOp=Rtype
  • RegDst=1
  • RegWrite=0

第四周期: \[ R[rd]\leftarrow ALUOut \]

控制:

  • RegDst=1
  • MemtoReg=0
  • RegWrite=1

有些课件图中没有显式 ALUOut 寄存器,而是在写回周期保持 ALU 输入和操作不变,让 ALU 输出保持稳定,再打开 RegWrite

这是为了解决写地址、写数据和写使能之间的竞争

R 型状态序列

R 型指令状态序列为: \[ IFetch \rightarrow RFetch/ID \rightarrow RExec \rightarrow RFinish \]

RExec 执行: \[ ALUOut\leftarrow A\ op\ B \]

RFinish 执行: \[ R[rd]\leftarrow ALUOut \]

如果不设置 ALUOut,也可以在 RFinish 中保持 ALU 输入和 ALU 控制信号不变,使 ALU 输出继续稳定

但从概念上看,把 ALU 结果保存到 ALUOut 更容易理解

R 型指令可能产生溢出异常

如果是 addsub 这类需要溢出判断的指令,RExec 后要检查 ALU 的 Overflow

若溢出,则不能进入正常写回状态,而应进入异常处理状态

beq 指令

beq rs, rt, label\[ if\ R[rs]=R[rt]\ then\ PC\leftarrow Target \]

前两个周期:

  1. 取指并 PC+4
  2. 译码、读寄存器,并投机计算 Target

第三周期:

\[ R[rs]-R[rt] \]

如果 ALU 输出为 0,则 Zero=1,分支成立

控制:

  • ALUSrcA=1
  • ALUSrcB=00
  • ALUOp=Sub
  • PCSrc=1
  • PCWriteCond=1

PC 是否写入由: \[ PCWriteCond \land Zero \]

共同决定。

因此 beq 通常 3 个周期完成

beq 的状态序列

beq 状态序列为: \[ IFetch \rightarrow RFetch/ID \rightarrow BrFinish \]

RFetch/ID 中已经投机算好分支目标

BrFinish 中只需要:

  1. 用 ALU 做 \(A-B\)
  2. 根据 Zero 判断是否相等
  3. 如果相等,把 Target 写入 PC

可写为: \[ if\ Zero=1,\quad PC\leftarrow Target \]

对应控制是: \[ PCWrite=PCWrCond\land Zero \]

如果分支不成立,PC 保持取指阶段已得到的顺序地址

addiori

addi\[ R[rt]\leftarrow R[rs]+SignExt(imm16) \]

ori\[ R[rt]\leftarrow R[rs]\ or\ ZeroExt(imm16) \]

二者流程类似:

  1. 取指
  2. 译码和读寄存器
  3. ALU 执行立即数运算
  4. 写回 rt

addi 使用符号扩展和加法

ori 使用 0 扩展和或运算

写回周期控制:

  • RegDst=0
  • MemtoReg=0
  • RegWrite=1

立即数指令状态

立即数运算指令通常有两个执行状态:

\[ IFetch \rightarrow RFetch/ID \rightarrow ImmExec \rightarrow ImmFinish \]

ImmExec\[ ALUOut\leftarrow A\ op\ Ext(imm16) \]

ImmFinish\[ R[rt]\leftarrow ALUOut \]

ori 的扩展是 0 扩展,ALU 做或运算

addiu 的扩展是符号扩展,ALU 做加法,而且不判断溢出

j 指令

j label 只修改 PC: \[ PC \leftarrow (PC+4)[31:28]\ ||\ target[25:0]\ ||\ 00 \]

流程:

  1. 取指
  2. 译码
  3. 写 PC

第三周期:

  • PCSrc=10
  • PCWrite=1

寄存器堆和存储器的写使能都为 0

j 的状态

j 指令在译码后只需要一个跳转完成状态: \[ PC\leftarrow PC[31:28]\ ||\ target[25:0]\ ||\ 00 \]

这里的 PC 已经是 PC+4

跳转状态中,寄存器堆和存储器都不能写

否则会把指令字段误解释成寄存器编号或地址,造成破坏

控制器 FSM

单周期控制器可以只根据当前指令 op 产生一组控制信号,而多周期不行

这是因为同一条指令在不同周期要输出不同控制信号

所以多周期控制器通常设计成有限状态机 FSM

采用 Moore 型 FSM:

输出只依赖当前状态,不直接依赖输入

下一状态由当前状态和操作码共同决定

可以理解为: \[ NextState=f(CurrentState, Opcode) \]

\[ ControlSignals=g(CurrentState) \]

状态:

状态含义
S0取指令
S1译码/读寄存器
S2lw/sw 地址计算
S3lw 读存储器
S4lw 写寄存器
S5sw 写存储器
S6R 型执行
S7R 型写回
S8beq 分支完成
S9addi 执行
S10addi 写回
S11j 跳转

状态转移大致为:

  • S0 -> S1
  • S1 -> S2,如果是 lwsw
  • S1 -> S6,如果是 R 型
  • S1 -> S8,如果是 beq
  • S1 -> S9,如果是 addi
  • S1 -> S11,如果是 j
  • S2 -> S3,如果是 lw
  • S2 -> S5,如果是 sw
  • S3 -> S4
  • S4/S5/S7/S8/S10/S11 -> S0

每条指令结束后都回到取指状态

1194X838/13.png

状态转换表

FSM 可以用状态转换表实现

表中每一行说明:

  • 当前状态
  • 当前指令操作码
  • 下一状态

典型规则:

  • 所有结束状态的下一状态都是 IFetch
  • IFetch 的下一状态总是 RFetch/ID
  • RFetch/ID 根据 op 分派到不同执行状态
  • 中间状态通常无条件转到下一个固定状态
当前状态条件下一状态
IFetch任意RFetch/ID
RFetch/IDop=lw/swMemAdr
RFetch/IDop=R-typeRExec
RFetch/IDop=beqBrFinish
RFetch/IDop=ori/addiuImmExec
RFetch/IDop=jJFinish
MemAdrop=lwMemFetch
MemAdrop=swswFinish
MemFetch任意lwFinish
lwFinish任意IFetch

状态编码后,下一状态和控制信号都是组合逻辑输出

所以硬连线多周期控制器由两部分构成:

  • 状态寄存器
  • 组合逻辑控制单元

每个时钟边沿,状态寄存器更新为下一状态

ALU 译码器

ALU 控制仍然可以分成主控制和局部控制

一个典型 ALU 译码表:

ALUOpFunctALUControl指令
00xAddlwswaddi
01xSubbeq
10100000Addadd
10100010Subsub
10100100Andand
10100101Oror
10101010SLTslt

主控制器只负责说明“这类指令需要 ALU 做什么大类操作”,具体 R 型操作再由 funct 字段决定

ALUOp 和状态的关系

多周期中,ALU 的用途随状态变化,而不只随指令变化

例如:

  • IFetch 中,ALU 一定做 PC+4
  • RFetch/ID 中,ALU 可做分支目标地址计算
  • MemAdr 中,ALU 做基址加偏移
  • RExec 中,ALU 操作由 R 型 func 决定
  • BrFinish 中,ALU 做减法比较

因此同一条指令在不同状态下可能需要不同 ALU 控制

这也是多周期控制器必须看“状态”的原因

964X703/14.png

微程序控制器

多周期控制器可以用硬连线 FSM 实现,也可以用微程序实现

硬连线控制器:

  • 速度快
  • 适合 MIPS 这类简单规整的指令系统
  • 但复杂指令系统中逻辑会很庞大

微程序控制器的思想是:

用一段存放在控制存储器中的“微程序”来产生控制信号

概念关系如下:

  • 一条机器指令对应一个微程序
  • 一个微程序由多条微指令组成
  • 一条微指令包含多个微命令
  • 一个微命令就是一个控制信号或控制信号组合

控制存储器通常记为 CS

微程序控制器中有:

  • 微程序计数器 μPC
  • 微指令寄存器 μIR
  • 控制存储器 CS
  • 下地址形成逻辑

每个时钟取出一条微指令,微指令译码后产生控制信号

水平型和垂直型微指令

水平型微指令:

  • 一条微指令包含很多微命令
  • 并行性高
  • 微程序短
  • 但微指令很长,编码空间利用率低

垂直型微指令:

  • 一条微指令只控制一两个微操作
  • 微指令短
  • 编写类似机器指令
  • 但微程序长,速度慢

不译法是一种最直接的水平微指令编码:

一位对应一个控制信号

优点是快,不需要译码

缺点是微指令很长,很多位经常为 0

字段直接编码法会把互斥的微命令放到同一字段编码,把相容微命令放到不同字段,从而压缩微指令长度

微地址控制

微程序执行也需要决定下一条微指令地址

常见方式:

  • 增量法:默认执行下一条微指令,类似 \(\mu PC\leftarrow \mu PC+1\)
  • 断定法:微指令中显式给出下条微地址

实际控制中还要处理条件转移。

例如,根据 op 字段选择某条机器指令对应微程序的入口地址;根据 ZeroOverflow 等条件码选择不同微指令

微程序控制器的优点是结构规整、易修改,适合复杂指令系统

缺点是每条微指令要从控制存储器中读取,速度通常不如硬连线控制器

所以 RISC 处理器多用硬连线控制器,而复杂 CISC 处理器常把简单指令硬连线实现,把复杂指令交给微程序控制

异常和中断

处理器执行程序时可能遇到特殊情况,使当前程序暂时停止,转到专门的处理程序

这类事件分为:

  • 异常:CPU 内部发生,如溢出、非法指令、缺页、除数为 0
  • 中断:CPU 外部发生,如外设请求、定时器、DMA 完成
705X374/15.png

检测到异常或中断后,处理器通常要:

  1. 关中断,防止新的中断破坏现场
  2. 保存断点 PC
  3. 保存程序状态
  4. 记录异常原因
  5. 跳转到异常处理程序

MIPS 通常采用软件识别异常原因

相关寄存器:

  • EPC:保存异常返回地址
  • Cause:记录异常原因

MIPS 异常处理入口地址常见为: \[ 0x80000180 \]

为了支持异常,多周期数据通路中需要增加:

  • EPC 寄存器
  • Cause 寄存器
  • EPCWrite
  • CauseWrite
  • IntCause
  • PC 选择器中新增加异常入口地址一路

比如未定义指令异常和溢出异常可以分别进入不同状态:

  • 未定义指令:Cause=0
  • 溢出:Cause=1

异常状态中通常完成:

\[ EPC \leftarrow PC-4 \]

\[ PC \leftarrow 0x80000180 \]

其中 PC-4 是为了保存发生异常的那条指令地址,因为取指周期中 PC 已经提前加 4

中断和异常的区别在于:中断是外部随机事件,通常只能在一条指令结束后响应;异常可能在指令执行过程中被检测到

不能在一条指令执行到一半时随便响应中断,因为处理器一般无法回到“半条指令”的状态继续执行

异常断点

异常处理能否正确,关键是断点保存是否正确

所谓断点,就是异常处理结束后应该返回执行的位置

对故障类异常,比如缺页,处理后通常要重新执行发生异常的指令

因此 EPC 应保存发生异常的那条指令地址

在多周期处理中,取指周期已经执行了: \[ PC\leftarrow PC+4 \]

所以如果后续状态发现当前指令异常,常常需要保存: \[ EPC\leftarrow PC-4 \]

对于自陷或系统调用一类异常,处理后可能返回下一条指令,此时断点可能保存当前 PC

断点到底保存 PC 还是 PC-4,取决于异常类型和发生时机

多周期异常状态

在 FSM 中,可以为异常增加专门状态

例如:

  • 未定义指令异常状态
  • 溢出异常状态
  • 缺页异常状态
1253X933/16.png

异常状态通常完成:

  1. 设置 Cause
  2. 计算并保存断点到 EPC
  3. 把异常处理入口写入 PC
  4. 关中断

对未定义指令,可在译码状态检测

对溢出,可在 R 型执行状态或立即数加法执行状态检测

对缺页,可在访存相关状态检测

异常检测点不同,保存的断点和需要清除的副作用也不同

这也是后面流水线处理异常更复杂的原因

小结

多周期处理器把指令执行拆成多个较短周期

通过牺牲了 CPI=1,换来了:

  • 更短的时钟周期
  • 不同指令不同周期数
  • 功能部件复用
  • 更低硬件成本
  • 更自然的控制状态划分

单周期适合理解完整数据通路,多周期适合理解控制器和时序

再往后就是流水线:把这些阶段重叠起来,让多条指令同时处在不同阶段中执行

$ discussion
# Comments
waline