CO-P7-设计文档
2024计算机组成原理P7设计文档
P7_MIPS微系统
预备知识
异常程序处理
先要明白一点,Mars不会帮你处理中断异常,Mars遇到异常指令或中断信号会跳至中断异常处理程序(0x4180之后),如何处理中断异常完全由0x4180之后的代码决定。
先看课程组给出的示例程序
1 | # 程序首先从这里运行 |
代码解释
.text: Subsequent items (instructions) stored in Text segment at next available address.随后的条目(指令)存储在文本段的下一个可用地址中。普通的用户模式下的代码段,简单来说就是存指令的地方。
.ktext: Subsequent items stored in Kernel Text segment at next available address.随后的条目存储在内核文本段的下一个可用地址中。即内核模式下的代码段,内核模式下运行,具有最高权限,可以直接访问硬件和受保护的资源。对于p7来说就是存放异常处理程序的地方。
异常包括很多种类型,中断和异常也不同,不同的异常或中断处理方式不一样,因此我们需要记录下异常或中断的信息,然后用特定方式处理异常,这就是CP0(协处理器)的作用。
进入中断异常处理程序后,我们先从CP0中获取异常或中断信息,然后跳转至不同的地方处理异常中断,一般最后都会跳转到eret,返回主程序。
CPU与外设的交互
CPU的作用就是对外界输入数据做出对应的处理后返回结果。
外设
对于p7,我们有三个外设需要处理。这三个外设的作用就是时不时向cpu发出一个中断信号。
- 计时器(Timer):计算机系统中的计时部件,可以按照配置定时地产生时钟中断。
- 存储器(Memory):计算机系统中的存储部件,用于存储指令和数据。我们在 P6 的时候已经接触过了。
- 中断发生器(InterruptGenerator):抽象的计算机系统外设,会随机的产生外部中断信号,产生的中断信号在 CPU 响应前会持续置高。
内存
我们可以认为内存也是一种外设。
我们可以认为指令、数据、异常处理程序、外设寄存器等都是储存在同一内存中,只不过存储的位置不同,有着严格的界限。
条目 | 地址或地址范围 | 备注 |
---|---|---|
数据存储器 | 0x0000_0000∼0x0000_2FFF | |
指令存储器 | *** 0x0000_3000∼0x0000_6FFF*** | |
PC 初始值 | 0x0000_3000 | |
异常处理程序入口地址 | 0x0000_4180 | |
计时器 0 寄存器地址 | 0x0000_7F00∼0x0000_7F0B | 计时器 0 的 3 个寄存器 |
计时器 1 寄存器地址 | 0x0000_7F10∼0x0000_7F1B | 计时器 1 的 3 个寄存器 |
中断发生器响应地址 | 0x0000_7F20∼0x0000_7F23 |
宏观PC
对于流水线寄存器,每一流水级的PC都不同。我们希望CPU实现的功能应该是单周期的,引入多周期只是为了提高效率。如果我们把CPU封装起来,不关注内部的复杂过程,在外部看来CPU应该只有一个PC,即CPU一个周期仅执行一条指令。为此我们需要引入宏观PC的概念。
- 我们将 CP0 放在 M 级(大家都放在M级,方便对拍),因此也以 M 级为界线,规定 M 级的 PC 就是宏观 PC。
- 所谓“宏观”指令,表示该指令之前的所有指令序列对 CPU 的更新已完成,该指令及其之后的指令序列对 CPU 的更新未完成。放在M级显然满足这个定义。
Tasks
Task-1: 建立与外界的联系
Timer
- Timer内部有三个寄存器: 0号寄存器ctrl、1号寄存器preset、2号寄存器count。虽然说是寄存器,但我们可以认为其实内存的一部分。
0号寄存器ctrl | 1号寄存器preset | 2号寄存器count | |
---|---|---|---|
TC1 | 0x0000_7F00∼0x0000_7F03 | 0x0000_7F04∼0x0000_7F07 | 0x0000_7F08∼0x0000_7F0b |
TC2 | 0x0000_7F10∼0x0000_7F13 | 0x0000_7F14∼0x0000_7F17 | 0x0000_7F18∼0x0000_7F1b |
我们可以通过sw指令向对应Timer寄存器中存值以开启计时并调整倒计时。
1
2
3
4
5
6
7
8
9
10
11# MODE 0 for timer1
ori $t1 $0 9 #设置计时使能和中断屏蔽
sw $t1 0x7F00($0)
ori $t1 $0 1 #设置计时数
sw $t1 0x7F04($0)
# MODE 1 for timer1
ori $t1 $0 11 #设置计时使能和中断屏蔽
sw $t1 0x7F00($0)
ori $t1 $0 10 #设置计时数
sw $t1 0x7F04($0)
Task-2: 记录异常并流水
- where CP0: M级。
- 延迟槽指令: 如果一个处于延迟槽的指令发生了异常,我们从异常处理程序中返回时需要回到它的上一个指令(跳转指令)。因此我们需要增加判断一个信号用于判断指令是否在延迟槽中,并向后流水传至CP0。(branch、jump、jal、jr在ID级,IF级的指令即处于延迟槽中)。
1 | assign BD_IF = ((branch_ID === 1'b1) || (jump_ID === 1'b1))? 1'b1: |
- 内部异常和外部中断:同时发生时处理外部中断,当内部无异常时设为Int(外部中断)。
- 一条指令多个异常 & 多个指令异常:优先处理最早可以探查到的异常。
异常与中断码 | 助记符与名称 | 指令与指令类型 | 描述 | |
---|---|---|---|---|
0 | Int (外部中断) |
所有指令 | 中断请求,来源于计时器与外部中断。 | |
4 | AdEL (取指异常) |
所有指令 | PC 地址未字对齐。 | 1 |
4 | AdEL (取指异常) |
所有指令 | PC 地址超过 0x3000 ~ 0x6ffc 。 |
1 |
4 | AdEL (取数异常) |
lw |
取数地址未与 4 字节对齐。 | 1 |
4 | AdEL (取数异常) |
lh |
取数地址未与 2 字节对齐。 | 1 |
4 | AdEL (取数异常) |
lh , lb |
取 Timer 寄存器的值。 | |
4 | AdEL (取数异常) |
load 型指令 | 计算地址时加法溢出。 | 1 |
4 | AdEL (取数异常) |
load 型指令 | 取数地址超出 DM、Timer0、Timer1、中断发生器的范围。 | |
5 | AdES (存数异常) |
sw |
存数地址未 4 字节对齐。 | 1 |
5 | AdES (存数异常) |
sh |
存数地址未 2 字节对齐。 | 1 |
5 | AdES (存数异常) |
sh , sb |
存 Timer 寄存器的值。 | |
5 | AdES (存数异常) |
store 型指令 | 计算地址加法溢出。 | 1 |
5 | AdES (存数异常) |
store 型指令 | 向计时器的 Count 寄存器存值。 | |
5 | AdES (存数异常) |
store 型指令 | 存数地址超出 DM、Timer0、Timer1、中断发生器的范围。 | |
8 | Syscall (系统调用) |
syscall |
系统调用。 | |
10 | RI (未知指令) |
- | 未知的指令码。 | |
12 | Ov (溢出异常) |
add , addi , sub |
算术溢出。 | 1 |
IF级
- PC 地址未字对齐
AdEL
- PC 地址超过 0x3000 ~ 0x6ffc
AdEL
1 | // in PC module |
ID级
- 未知的指令码
RI
- syscall 对于syscall指令我们只需发出一个异常信号即可
syscall
如果有if嵌套,注意内部if对应的else也需要判断为未知指令。
1 | // in controller module |
EX级
- 算术溢出
OV
1 | wire [32:0] A_ext = {A[31],A}; |
MA级
- load类指令
- lw取数地址未与 4 字节对齐
AdEL
- lh、lb取数地址未与 2 字节对齐
AdEL
- lh、lb取 Timer 寄存器的值:寄存器的值为32位,取半字或比特是非法行为非法
AdEL
- 计算地址时加法溢出
AdEL
- 取数地址超出 DM、Timer0、Timer1、中断发生器的范围
AdEL
- lw取数地址未与 4 字节对齐
- store类指令
- sw存数地址未 4 字节对齐
AdES
- sh、sb存数地址未 2 字节对齐
AdES
- sh、sb存 Timer 寄存器的值 :寄存器的值为32位,存半字或比特是非法行为非法
AdES
- 计算地址加法溢出
AdES
- 向计时器的 Count 寄存器存值 :计时器的count寄存器是只读寄存器(read-only)
AdES
- 存数地址超出 DM、Timer0、Timer1、中断发生器的范围
AdES
- sw存数地址未 4 字节对齐
1 | //load EXC |
Task-3: 加入协处理器CP0
- 所有寄存器均为32bits
- 处理中断和异常信号,判断是否发出req信号,发出req信号时所有流水寄存器清空(部分信号需要特殊处理)
- eret指令需要清空EXL部分
- 0001_0000
寄存器 | 寄存器编号 | 功能域 | 位域 | 解释 |
---|---|---|---|---|
SR(State Register) | 12 | IM(Interrupt Mask) | 15:10 | 分别对应六个外部中断,相应位置 1 表示允许中断,置 0 表示禁止中断。这是一个被动的功能,只能通过 mtc0 这个指令修改,通过修改这个功能域,我们可以屏蔽一些中断。 |
SR(State Register) | 12 | EXL(Exception Level) | 1 | 任何异常发生时置位,这会强制进入核心态(也就是进入异常处理程序)并禁止中断。 |
SR(State Register) | 12 | IE(Interrupt Enable) | 0 | 全局中断使能,该位置 1 表示允许中断,置 0 表示禁止中断。 |
Cause | 13 | BD(Branch Delay) | 31 | 当该位置 1 的时候,EPC 指向当前指令的前一条指令(一定为跳转),否则指向当前指令。 |
Cause | 13 | IP(Interrupt Pending) | 15:10 | 为 6 位待决的中断位,分别对应 6 个外部中断,相应位置 1 表示有中断,置 0 表示无中断,将会每个周期被修改一次,修改的内容来自计时器和外部中断。 |
Cause | 13 | ExcCode | 6:2 | 异常编码,记录当前发生的是什么异常。 |
EPC | 14 | - | - | 记录异常处理结束后需要返回的 PC。 |
端口 | 方向 | 位数 | 解释 |
---|---|---|---|
clk | IN | 1 | 时钟信号。 |
reset | IN | 1 | 复位信号。 |
en | IN | 1 | 写使能信号。 |
CP0Add | IN | 5 | 读取/写入目标寄存器地址。 |
CP0In | IN | 32 | 写入寄存器数据。 |
CP0Out | OUT | 32 | 读出寄存器数据。 |
VPC | IN | 32 | 当前异常中断发生时PC的值。 |
BDIn | IN | 1 | 当前异常中断发生指令是否是延迟槽内指令。 |
ExcCodeIn | IN | 5 | 当前发生异常类型。 |
HWInt | IN | 6 | 外部产生的中断信号。 |
EXLClr | IN | 1 | 是否结束异常中断处理(复位 EXL)。 |
EPCOut | OUT | 32 | EPC 的值。 |
Req | OUT | 1 | 进入处理程序请求。 |
1 |
|
Task-4: 加指令
在 P6 基础上新增了 mfc0, mtc0, eret, syscall
四条新指令
目前p7要求实现如下指令:(最好把jump指令也实现了,方便处理异常)
1 | nop, add, sub, and, or, slt, sltu, lui |
MFC0
把CP0中的rd号寄存器的值存入grf的rt寄存器
- 需要简单处理下转发,类似lw的转发
MTC0
将grf的rt号寄存器的值存入CP0的rd号寄存器
ERET
从错误处理程序中返回到主程序
- 在ID级时设置next_pc为CP0的EXC
- 在ID级时清空延迟槽,确保后续指令不会被处理
- 到达MA级时,需要清空CP0的EXL部分,表示当前不在处于核心状态(异常处理程序)
- 在ID级时,如果EX或MA级是MTC0且目标寄存器为14号(EXC)则阻塞,确保返回地址正确。
syscall
在controller里发出一个异常信号(异常码为8)即可
流水寄存器行为
为什么要修改寄存器的清空行为?
我们在阻塞的时候会在EX级产生一个bubble,可以看作nop不发生任何作用,但实际上我们要把CPU封装为单周期CPU,级两条相邻的指令之间不会莫名其妙多出来一个nop指令。
试想,如果不修改清空逻辑的情况下,ID级是一个延迟槽内的指令,正常来说它到达MA级时如果发生了异常或中断,我们的EPC是需要写入PC-4的,因为我们从异常处理程序中返回时需要返回到延迟槽指令的前一条指令(即跳转指令)。而如果延迟槽内的指令在ID级时发生了阻塞产生了一个’nop’,nop流水到MA级时发生了异常或中断我们该如何记录EPC?这个’nop’在外部看来是不应该存在的,其没有对应的PC,其对应的PC应该是延迟槽指令的PC,其也应该被标记为延迟槽内指令,这样从异常处理程序中返回时才能返回到正确的地址。
按照从上至下的优先级依次处理
信号 | PC流水寄存器行为 | BD流水寄存器行为 | 其他流水寄存器行为 |
---|---|---|---|
reset |
复位至 0x00003000 |
复位至 0 |
清空 |
req |
设置为 0x00004180 |
设置为 0 |
清空 |
ID_EXLClr (仅对F级到D级流水寄存器起作用,且此时stall信号为0) |
设置为当前周期的 EPC 的值 |
不发生变化 | 清空 |
flush (仅指对D级到E级流水寄存器起作用的阻塞信号) |
不发生变化 | 不发生变化 | 清空 |
PC
1 | always @(posedge clk) begin |
IF_ID Reg
clear_bd表示清空延迟槽,对于P7仅针对eret指令
1 | always @(posedge clk) begin |
ID_EX Reg
1 | always @(posedge clk) begin |
EX_MA Reg
1 | always @(posedge clk) begin |
MA_WB Reg
1 | always @(posedge clk) begin |
我的Bug
以下是我在通过课下弱测和中测后发现的bug:
- 不涉及计算操作时ALUOP默认设为了`ADD,导致不是ALU类指令时出现了溢出情况。
- 当MA级异常时,如果EX级为mthi或mtlo时仍会向HI或LO中写入值。
- 当PC错误或遇到未知指令时没有向后传递nop。
- mtc0位于MA级时,如果当出现中断时,仍会向CP0中的寄存器写入值。
- EXCCode优先级问题。load或store指令会触发EX级的算术overflow(异常码12)异常,在MA级也会除法overflow(异常码4或5)的异常,按照
优先处理先探查到的错误
的原则我们会选择EX级的算术overflow,导致存入CP0的异常码错误。
测试方案
- 手动构造样例确保新指令的功能正确性,尽可能覆盖大部分异常情况。
- 随机生成指令强测确保转发和阻塞的正确性。对CO-Killer略加修改,生成符合要求的随机数据。
- 限制读写寄存器的范围,能够尽可能多的造成数据冒险,再加上大量随机数据,基本可以覆盖全部转发和阻塞情况。
- 利用自己写的python脚本填充nop至0x4180。
- 定时器产生周期性中断信号,测试timer的中断是否正确被处理。
- 修改testbench产生中断,测试中断发生器发出的中断是否正确被处理。
1 | # 填充nop |
思考题
- 请查阅相关资料,说明鼠标和键盘的输入信号是如何被 CPU 知晓的?
鼠标和键盘产生中断信号,进入中断处理区的对应位置,将输入信号从鼠标和键盘中读入寄存器。
- 请思考为什么我们的 CPU 处理中断异常必须是已经指定好的地址?如果你的 CPU 支持用户自定义入口地址,即处理中断异常的程序由用户提供,其还能提供我们所希望的功能吗?如果可以,请说明这样可能会出现什么问题?否则举例说明。(假设用户提供的中断处理程序合法)
- 指定好的中断处理入口地址由硬件设计决定,能确保所有中断和异常按照既定逻辑处理。如果允许用户自定义入口地址,用户可能会直接跳过关键的上下文保存步骤,导致程序状态紊乱
- 指定的中断地址通常位于受保护的系统区域,由操作系统或固件控制。用户故意定义有害的中断处理程序,例如窃取敏感信息或修改关键数据。
- 为何与外设通信需要 Bridge?
- CPU与外设数据吞吐量存在差异:Bridge充当缓冲区和速率匹配器,协调高速的处理器与低速外设之间的数据交换。
- 地址映射差异:Bridge可以对地址进行解码,将处理器的访问请求转化为外设能够理解的操作。
- 请阅读官方提供的定时器源代码,阐述两种中断模式的异同,并针对每一种模式绘制状态移图。
MODE 0
定时产生中断信号
MODE 1
周期性产生中断信号
- 倘若中断信号流入的时候,在检测宏观 PC 的一级如果是一条空泡(你的 CPU 该级所有信息均为空)指令,此时会发生什么问题?在此例基础上请思考:在 P7 中,清空流水线产生的空泡指令应该保留原指令的哪些信息?
如果此时有一个中断或异常信号,则CP0记录下的PC为0x0000,在从异常处理程序返回时会发生错误。
应该保留PC值,由于在Req信号有效时,通过将流水寄存器的PC值不进行复位,使得达到精确异常的效果。使得回到EPC时依然是精确的。
- 为什么
jalr
指令为什么不能写成jalr $31, $31
?
如果
jalr $31 $31
的延迟槽内发生异常或需要响应中断。那么$31
寄存器的值已经被jalr
改变,但是处理异常结束后,会再次执行jalr
指令,这就会跳转到不正确的PC
地址。
- [P7 选做] 请详细描述你的测试方案及测试数据构造策略。
见上文
课程组官方TestBench
1 |
|
CO-P7-设计文档