#计组学习笔记3
单周期处理器中,每条指令都在一个时钟周期内完成
优点是控制简单,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:存储器数据寄存器,保存读出的数据A、B:保存寄存器堆读出的两个操作数ALUOut或Target:保存 ALU 计算出的地址或分支目标
重点强调 IR、BranchTarget/Target、PCWrite、IRWrite 等控制信号
核心区别是:
单周期中一条指令的数据流一次走完
多周期中数据流分阶段走,每个阶段结束后把必要结果暂存起来
状态
一个阶段就是一个状态
多周期处理器中,一条指令由多个状态完成
在指令译码前,所有指令经过的状态相同:
- 取指令
- 译码和读寄存器
译码后,根据 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 \]
之间的竞争
多周期可以把“准备地址和数据”和“写使能有效”拆到不同周期
也就是:
- 第 \(N\) 个周期先让地址和数据稳定
- 第 \(N+1\) 个周期再让写使能有效
- 写使能无效前,地址和数据不改变
这个细节很重要,否则机器可能出现很难复现的错误
竞争问题和状态拆分
特别强调,实际寄存器组和存储器的写操作不能简单理解成“时钟边沿自动写入”
如果 RegWrite 先有效,而 Rw 或 busW 还在变化,就可能写错寄存器或写入错误数据。
因此有些设计会把“准备写地址和写数据”和“真正打开写使能”分成连续两个状态。
例如 R 型指令:
- 执行状态:先让目的寄存器选择信号和 ALU 输出稳定,但
RegWrite=0 - 写回状态:保持目的寄存器选择和 ALU 输出稳定,再令
RegWrite=1
这解释了为什么有些状态看起来“重复做同一个 ALU 运算”。
目的不是重新计算,而是为了让写端口数据稳定。
多周期数据通路控制信号

多周期数据通路比单周期多了一些控制信号:
| 信号 | 含义 |
|---|---|
PCWrite | 无条件写 PC |
PCWriteCond | 条件写 PC,常用于分支 |
PCSrc | 选择 PC 的新值来源 |
IRWrite | 是否写指令寄存器 |
IorD | 存储器地址来自 PC 还是数据地址 |
MemWrite | 是否写存储器 |
RegDst | 写寄存器目的选择 |
RegWrite | 是否写寄存器堆 |
MemtoReg | 写回数据来自内存还是 ALU |
ALUSrcA | ALU 第一个操作数选择 |
ALUSrcB | ALU 第二个操作数选择 |
ALUOp | ALU 操作类型 |
ExtOp | 立即数符号扩展或 0 扩展 |
其中 ALUSrcB 通常不只是 1 位,因为 ALU 第二操作数可能来自:
- 寄存器
B - 常数 4
- 扩展立即数
- 扩展立即数左移 2 位
PCSrc 也可能是多位,因为 PC 可能来自:
- ALU 计算出的
PC+4 - 分支目标地址
- 跳转目标地址
- 异常处理入口地址
常见控制信号分组
为了便于理解,可以按控制对象分组
PC 相关:
PCWr:无条件更新 PCPCWrCond:结合Zero条件更新 PCPCSrc:选择写入 PC 的来源
存储器相关:
IorD:选择地址来自 PC 还是 ALU 计算出的数据地址MemWr:写存储器IRWr:把存储器输出写入 IR
寄存器堆相关:
RegDst:写寄存器编号选rt还是rdRegWr:写寄存器堆MemtoReg:写回数据来自存储器输出还是 ALU 输出
ALU 相关:
ALUSelA:ALU A 输入来自 PC 还是寄存器 AALUSelB:ALU B 输入来自常数、寄存器 B、立即数等ALUOp:ALU 运算类型ExtOp:立即数扩展方式
分支目标相关:
BrWr或TargetWr:把投机计算出的分支目标保存起来
这些控制信号在每个状态都有固定取值
取指周期
所有指令的第一个周期相同
取指周期完成: \[ IR \leftarrow M[PC] \]
\[ PC \leftarrow PC+4 \]
控制信号为:
IorD=0,存储器地址来自 PCIRWrite=1,把读出的指令写入 IRALUSrcA=0,ALU 第一个操作数来自 PCALUSrcB=01,ALU 第二操作数为 4ALUOp=AddPCSrc=0PCWrite=1MemWrite=0
取指结束后,PC 已经变成下一条顺序指令地址
但当前指令保存在 IR 中,所以后续周期不会因为 PC 改变而丢失当前指令
取指状态的 RTL
取指状态可以写成: \[ IR\leftarrow M[PC] \]
\[ PC\leftarrow PC+4 \]
这两个操作在同一个状态内并行完成
这里 ALU 被用来计算 PC+4,存储器被用来读指令
因此本状态中:
- ALU A 输入选 PC
- ALU B 输入选 4
- 存储器地址选 PC
IRWr=1PCWr=1
取指结束后,PC 已经指向顺序下一条指令,而 IR 保存当前指令
译码和读寄存器周期
第二个周期也基本是公共的
完成:
\[ busA \leftarrow RegFile[rs] \]
\[ busB \leftarrow RegFile[rt] \]
并根据 op 和 func 字段译码
ALU 在这个周期本来可能空闲,所以可以顺便投机计算分支目标地址: \[ Target \leftarrow PC+SignExt(imm16)\times 4 \]
注意在上个周期 PC 已更新,此时的 PC 已经是 PC+4,所以这个式子实际就是: \[ Target=(PC+4)+SignExt(imm16)\times 4 \]
控制信号为:
ALUSrcA=0ALUSrcB=10或选择扩展立即数左移 2 位ALUOp=AddExtOp=1BrWrite=1或TargetWrite=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:译码和读寄存器
读出 rs、rt,并计算可能的分支目标
S2:计算存储器地址
\[ Addr \leftarrow R[rs]+SignExt(imm16) \]
控制:
ALUSrcA=1ALUSrcB=10或选择扩展立即数ALUOp=Add
S3:读存储器
\[ MDR \leftarrow M[Addr] \]
控制:
IorD=1MemWrite=0
S4:写寄存器
\[ R[rt]\leftarrow MDR \]
控制:
RegDst=0MemtoReg=1RegWrite=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 相同:
- 取指
- 译码和读寄存器
- 计算地址
最后一个周期写存储器: \[ M[Addr]\leftarrow R[rt] \]
控制:
IorD=1MemWrite=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] \]
流程:
- 取指
- 译码和读寄存器
- 执行 ALU 运算
- 写回寄存器
第三周期: \[ ALUOut \leftarrow R[rs]\ op\ R[rt] \]
控制:
ALUSrcA=1ALUSrcB=00ALUOp=RtypeRegDst=1RegWrite=0
第四周期: \[ R[rd]\leftarrow ALUOut \]
控制:
RegDst=1MemtoReg=0RegWrite=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 型指令可能产生溢出异常
如果是 add 或 sub 这类需要溢出判断的指令,RExec 后要检查 ALU 的 Overflow
若溢出,则不能进入正常写回状态,而应进入异常处理状态
beq 指令
beq rs, rt, label: \[ if\ R[rs]=R[rt]\ then\ PC\leftarrow Target \]
前两个周期:
- 取指并
PC+4 - 译码、读寄存器,并投机计算
Target
第三周期:
\[ R[rs]-R[rt] \]
如果 ALU 输出为 0,则 Zero=1,分支成立
控制:
ALUSrcA=1ALUSrcB=00ALUOp=SubPCSrc=1PCWriteCond=1
PC 是否写入由: \[ PCWriteCond \land Zero \]
共同决定。
因此 beq 通常 3 个周期完成
beq 的状态序列
beq 状态序列为: \[ IFetch \rightarrow RFetch/ID \rightarrow BrFinish \]
RFetch/ID 中已经投机算好分支目标
BrFinish 中只需要:
- 用 ALU 做 \(A-B\)
- 根据
Zero判断是否相等 - 如果相等,把 Target 写入 PC
可写为: \[ if\ Zero=1,\quad PC\leftarrow Target \]
对应控制是: \[ PCWrite=PCWrCond\land Zero \]
如果分支不成立,PC 保持取指阶段已得到的顺序地址
addi 和 ori
addi: \[ R[rt]\leftarrow R[rs]+SignExt(imm16) \]
ori: \[ R[rt]\leftarrow R[rs]\ or\ ZeroExt(imm16) \]
二者流程类似:
- 取指
- 译码和读寄存器
- ALU 执行立即数运算
- 写回
rt
addi 使用符号扩展和加法
ori 使用 0 扩展和或运算
写回周期控制:
RegDst=0MemtoReg=0RegWrite=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 \]
流程:
- 取指
- 译码
- 写 PC
第三周期:
PCSrc=10PCWrite=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 | 译码/读寄存器 |
| S2 | lw/sw 地址计算 |
| S3 | lw 读存储器 |
| S4 | lw 写寄存器 |
| S5 | sw 写存储器 |
| S6 | R 型执行 |
| S7 | R 型写回 |
| S8 | beq 分支完成 |
| S9 | addi 执行 |
| S10 | addi 写回 |
| S11 | j 跳转 |
状态转移大致为:
S0 -> S1S1 -> S2,如果是lw或swS1 -> S6,如果是 R 型S1 -> S8,如果是beqS1 -> S9,如果是addiS1 -> S11,如果是jS2 -> S3,如果是lwS2 -> S5,如果是swS3 -> S4S4/S5/S7/S8/S10/S11 -> S0
每条指令结束后都回到取指状态

状态转换表
FSM 可以用状态转换表实现
表中每一行说明:
- 当前状态
- 当前指令操作码
- 下一状态
典型规则:
- 所有结束状态的下一状态都是
IFetch IFetch的下一状态总是RFetch/IDRFetch/ID根据op分派到不同执行状态- 中间状态通常无条件转到下一个固定状态
| 当前状态 | 条件 | 下一状态 |
|---|---|---|
IFetch | 任意 | RFetch/ID |
RFetch/ID | op=lw/sw | MemAdr |
RFetch/ID | op=R-type | RExec |
RFetch/ID | op=beq | BrFinish |
RFetch/ID | op=ori/addiu | ImmExec |
RFetch/ID | op=j | JFinish |
MemAdr | op=lw | MemFetch |
MemAdr | op=sw | swFinish |
MemFetch | 任意 | lwFinish |
lwFinish | 任意 | IFetch |
状态编码后,下一状态和控制信号都是组合逻辑输出
所以硬连线多周期控制器由两部分构成:
- 状态寄存器
- 组合逻辑控制单元
每个时钟边沿,状态寄存器更新为下一状态
ALU 译码器
ALU 控制仍然可以分成主控制和局部控制
一个典型 ALU 译码表:
| ALUOp | Funct | ALUControl | 指令 |
|---|---|---|---|
| 00 | x | Add | lw、sw、addi |
| 01 | x | Sub | beq |
| 10 | 100000 | Add | add |
| 10 | 100010 | Sub | sub |
| 10 | 100100 | And | and |
| 10 | 100101 | Or | or |
| 10 | 101010 | SLT | slt |
主控制器只负责说明“这类指令需要 ALU 做什么大类操作”,具体 R 型操作再由 funct 字段决定
ALUOp 和状态的关系
多周期中,ALU 的用途随状态变化,而不只随指令变化
例如:
IFetch中,ALU 一定做PC+4RFetch/ID中,ALU 可做分支目标地址计算MemAdr中,ALU 做基址加偏移RExec中,ALU 操作由 R 型func决定BrFinish中,ALU 做减法比较
因此同一条指令在不同状态下可能需要不同 ALU 控制
这也是多周期控制器必须看“状态”的原因

微程序控制器
多周期控制器可以用硬连线 FSM 实现,也可以用微程序实现
硬连线控制器:
- 速度快
- 适合 MIPS 这类简单规整的指令系统
- 但复杂指令系统中逻辑会很庞大
微程序控制器的思想是:
用一段存放在控制存储器中的“微程序”来产生控制信号
概念关系如下:
- 一条机器指令对应一个微程序
- 一个微程序由多条微指令组成
- 一条微指令包含多个微命令
- 一个微命令就是一个控制信号或控制信号组合
控制存储器通常记为 CS
微程序控制器中有:
- 微程序计数器
μPC - 微指令寄存器
μIR - 控制存储器
CS - 下地址形成逻辑
每个时钟取出一条微指令,微指令译码后产生控制信号
水平型和垂直型微指令
水平型微指令:
- 一条微指令包含很多微命令
- 并行性高
- 微程序短
- 但微指令很长,编码空间利用率低
垂直型微指令:
- 一条微指令只控制一两个微操作
- 微指令短
- 编写类似机器指令
- 但微程序长,速度慢
不译法是一种最直接的水平微指令编码:
一位对应一个控制信号
优点是快,不需要译码
缺点是微指令很长,很多位经常为 0
字段直接编码法会把互斥的微命令放到同一字段编码,把相容微命令放到不同字段,从而压缩微指令长度
微地址控制
微程序执行也需要决定下一条微指令地址
常见方式:
- 增量法:默认执行下一条微指令,类似 \(\mu PC\leftarrow \mu PC+1\)
- 断定法:微指令中显式给出下条微地址
实际控制中还要处理条件转移。
例如,根据 op 字段选择某条机器指令对应微程序的入口地址;根据 Zero、Overflow 等条件码选择不同微指令
微程序控制器的优点是结构规整、易修改,适合复杂指令系统
缺点是每条微指令要从控制存储器中读取,速度通常不如硬连线控制器
所以 RISC 处理器多用硬连线控制器,而复杂 CISC 处理器常把简单指令硬连线实现,把复杂指令交给微程序控制
异常和中断
处理器执行程序时可能遇到特殊情况,使当前程序暂时停止,转到专门的处理程序
这类事件分为:
- 异常:CPU 内部发生,如溢出、非法指令、缺页、除数为 0
- 中断:CPU 外部发生,如外设请求、定时器、DMA 完成

检测到异常或中断后,处理器通常要:
- 关中断,防止新的中断破坏现场
- 保存断点 PC
- 保存程序状态
- 记录异常原因
- 跳转到异常处理程序
MIPS 通常采用软件识别异常原因
相关寄存器:
EPC:保存异常返回地址Cause:记录异常原因
MIPS 异常处理入口地址常见为: \[ 0x80000180 \]
为了支持异常,多周期数据通路中需要增加:
EPC寄存器Cause寄存器EPCWriteCauseWriteIntCause- 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 中,可以为异常增加专门状态
例如:
- 未定义指令异常状态
- 溢出异常状态
- 缺页异常状态

异常状态通常完成:
- 设置
Cause - 计算并保存断点到
EPC - 把异常处理入口写入 PC
- 关中断
对未定义指令,可在译码状态检测
对溢出,可在 R 型执行状态或立即数加法执行状态检测
对缺页,可在访存相关状态检测
异常检测点不同,保存的断点和需要清除的副作用也不同
这也是后面流水线处理异常更复杂的原因
小结
多周期处理器把指令执行拆成多个较短周期
通过牺牲了 CPI=1,换来了:
- 更短的时钟周期
- 不同指令不同周期数
- 功能部件复用
- 更低硬件成本
- 更自然的控制状态划分
单周期适合理解完整数据通路,多周期适合理解控制器和时序
再往后就是流水线:把这些阶段重叠起来,让多条指令同时处在不同阶段中执行