P7-Design-Document
0.顶层设计概述
P7要求为实现MIPS微系统,需要为P6实现的流水线CPU添加异常中断功能,并封装为CPU模块、实现系统桥Bridge、计时器Timer1,Timer2等模块,最终形成MIPS微系统,如下图所示。
- 绿色虚线表示已经实现
- 紫色虚线表示新增部分
- 红色虚线为改变后的DM接口

P7需要实现的任务如下列表
| 任务 | 解释 |
|---|---|
| 计时器 | 课程组提供代码 |
| 系统桥 | 为CPU提供统一的访问外设的接口,自行实现 |
| 协处理器CP0 | 设置CPU的异常处理功能,反馈CPU的异常信息,自行实现 |
| 内部检测异常与流水 | CPU检测内部指令执行错误 |
| 外部中断响应 | CPU需要具有响应外部中断信号的能力 |
| 异常处理指令 | 异常处理程序中,会有一些特殊的指令需要实现 |
| 单周期CPU的封装 | 让CPU从外部看上去是单周期CPU |
| 异常处理程序 | 利用MARS编写简单的异常处理程序进行测试 |
施工步骤:
更改流水线各级使之可以产生异常
添加CP0处理异常
添加Bridge与两个外设
异常与中断
- 异常:内部异常 如F级取指异常、D级计算溢出等
- 中断:来自外部设备,Timer0,Timer1,Interrupt
- 来自外部设备的中断比内部异常优先级更高
一.功能部件设计
0.新增指令的实现思路
P7中增加四条指令
- mtc0
- mfc0
- eret
- syscall
mtc0 :写入CP0中寄存器(12/14)
对于mtc0和mfc0指令 : 读取的CP0寄存器地址均为rd域,由于本实现中采用了集中式译码,故增加数据通路,将原指令的rd域流水下去,作为CP0寄存器地址, CP0_addr
指令格式 :
010000 || 00100 || rt || rd || 00000000000
mtc0 rt,rd
MCU :
- CP0_WE_D
- T_rs_use = T_rt_use = 3(这里rt的真实使用时间是3,但是并不会对暂停/转发造成影响,Tuse >= Tnew成立,可以通过转发解决)
- T_new = 0
写入时应当注意 :Cause寄存器(13)不允许写入,EPC允许写入,SR寄存器部分字段允许写入,其他不允许写入的字段要保持为0
1 2 3 4 5 6 7if(A2 == 12) begin `IM <= D_in[15:10]; `EXL <= D_in[1]; `IE <= D_in[0]; end else if (A2 == 14) EPC <= D_in;
mfc0 : 读取CP0中寄存器的值(12/13/14)
在M级CP0输出结果与DE输出结果之间加MUX
指令格式 : 010000 || 00000 || rt || rd || 00000000000
mfc0 rt,rd
MCU :
- RegWrite
- T_rs_use = T_rt_use = 3
- T_new = 3
eret : 从中断/异常处理中返回
MCU中判断后进行流水,D_eret,在M级进行使用跳出异常处理
eret是错误最易发生的一个指令,对于eret的要求有:
跳转到CP0中EPC寄存器存储的受害PC
不执行eret后延迟槽中的指令
不执行延迟槽中的指令我的实现方式(比较优雅 较为推荐)为 :在D级识别到eret指令后,在F级直接插入nop,同时npc中选择EPC端口
1 2 3 4 5 6\\ D_MCU_eret : D_eret assign D_eret = op_eret; \\ F-D reg 当F级出现取指异常或D级识别到eret指令时 传递指令nop assign F_instr_new = (F_AdEL) ? 32'b0 : (D_eret) : F_instr; \\ D_npc assign npc = (D_eret) ? EPC : ...同时还有两种可能的实现方式 :
在D级识别到eret指令后,清D级延迟槽,但是此时应当注意清空延迟槽的条件为(clr && !stall),即判断当前非暂停,举一个简单的例子就可以知道,例如D:eret E:mtc0
**一定要注意信号之间的优先级!**下面给出D级流水线寄存器的代码(我采用集中式译码,好臃肿,更加推荐分布式译码(bushi))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37always@(posedge clk)begin if(reset )begin instr <= 32'b0; pc <= 32'b0; pc8 <= 32'b0; ExcCode <= 5'b0; BD <= 1'b0; end else if (req) begin instr <= 32'b0; pc <= 32'h0000_4180; pc8 <= 32'h0000_4188; ExcCode <= 5'b0; BD <= 1'b0; end else if (clr && en) begin instr <= 32'b0; pc <= 32'b0; pc8 <= 32'b0; ExcCode <= 5'b0; BD <= 1'b0; end else if (en) begin instr <= F_instr; pc <= F_pc; pc8 <= F_pc8; ExcCode <= F_ExcCode; BD <= F_BD; end else begin //stall instr <= instr; pc <= pc; pc8 <= pc8; ExcCode <= ExcCode; BD <= BD; end end在F级输入pc取指时进行特判(这种写法我没有通过,报错处理延迟槽中断后没有跳回跳转指令),不过多叙述
syscall : 系统调用异常
- 在D级MCU中识别出来并判断为异常即可
0.5关于流水线寄存器/PC选择中的信号优先级问题
2023秋计算机组成教程中课程组对于信号的优先级规定如下
reset > req > stall
在流水线寄存器和IFU中pc选择时需要对信号优先级进行判断,推荐使用if_else_if_else语句实现对优先级的判断,非常重要!
流水线寄存器,以信号最复杂的E级为例
| |
IFU中PC的选择
| |
1.CP0
0.数据通路
- **宏观PC:**为了将我们的流水线CPU封装为单周期CPU(至少在外界看来是这个样子),提出宏观PC的概念,在宏观PC之前的地址对应的指令均已经完成,在宏观PC之后的地址对应的指令还未完成,依据这一特点我们知道应当将CP0(Coprocessor0)放置在M级流水线。

1.需要处理的异常
0.异常优先级
- 一条指令发生多个异常,考虑最先发生的异常(F>D>E>M)
- 多条指令发生异常,也有限考虑最先发生的异常(M>E>D>F),即将M级异常传入CP0
1.F级异常:
- PC地址没有字对齐(AdEL)
- PC地址超过0x3000 ~ 0x6ffc(AdEL)
- 还需要判断D级不是eret指令,因为eret后“延迟槽”中指令不被执行,即使在F级发生异常也不应被处理/进入异常处理程序。
| |
注意F级发生取指错误后要流水空指令nop递交到CP0
| |
1.D级异常:
未知的指令码(RI)
从MCU中添加输出信号
invalid_D,标记当前指令是否为无效指令,注意在MCU中识别nop指令1assign invalid_D = !(cal_R | cal_I | store | load | branch | md | mt | mf | op_jal | op_jr | op_mfc0 | op_mtc0 | op_syscall | op_eret | op_nop | op_syscall);
syccall 系统调用异常
1 2 3 4assign D_ExcCode_fixed = (D_ExcCode) ? D_ExcCode : (invalid_D) ? RI : (D_syscall) ? SYSCALL : NONE;
2.E级异常:
addi、add、sub计算溢出(Ov): 在MCU中添加输出信号
isAriOv_D表示是不是需要判断溢出的运算指令1assign isAriOv_D = op_add | op_addi | op_sub;load类指令计算地址时加法溢出(AdEL)
store类指令计算地址时加法溢出(AdES)
| |
补位法溢出判断
依据add,addi,sub原始的指令RTL中对溢出的判断编写.
temp = (GPR[rs]31 || GPR[rs]) + (GPR[rt]31 || GPR[rt])
if temp32 ≠ temp31 then
SignalException(IntegerOverflow)
else
GPR[rd] ← temp31..0
endif
以add为例翻译为对应的verilog代码如下
| |
3.M级异常:
lw取数地址未 4 字节对齐(AdEL)
lh取数地址未与 2 字节对齐(AdEL)
lh、lb取 Timer 寄存器的值(AdEL)
load型指令取数地址超出 DM、Timer0、Timer1、中断发生器的范围(AdEL)
sw存数地址未 4 字节对齐(AdES)
sh存数地址未 2 字节对齐(AdES)
sh、sb存 Timer 寄存器的值(AdES)
store型指令向计时器的 Count 寄存器存值(AdES)

store型指令存数地址超出 DM、Timer0、Timer1、中断发生器的范围(AdES)
| |
2.端口定义列表
| 名称 | 方向 | 位宽 | 描述 | 产生来源和机制 |
|---|---|---|---|---|
| clk | I | 1 | 时钟信号 | |
| reset | I | 1 | 同步复位信号 | |
| A1 | I | 5 | 读CP0寄存器编号 | 指令mfc0 |
| A2 | I | 5 | 写CP0寄存器编号 | 指令mtc0 |
| D_in | I | 32 | 写入CP0寄存器的数据 | 指令mtc0 |
| M_pc | I | 32 | M级PC:发生中断/异常时的PC | |
| M_ExcCode | I | 5 | 中断/异常的类型 | 异常功能部件 |
| BD_in | I | 1 | 分支延迟槽指令标志 | |
| HWInt | I | 6 | 6个外部设备的中断 | 外部设备 |
| WE | I | 1 | CP0寄存器写使能 | 指令mtc0 |
| EXTClr | I | 1 | SR的EXL位置零 | eret_M控制信号输入 |
| req | O | 1 | 异常/中断请求 | CP0模块确认响应异常/中断 |
| EPC_out | O | 32 | EPC寄存器输出 | |
| D_out | O | 32 | CP0寄存器输出数据 | 指令mfc0 |
3.内部寄存器列表
将SR,Cause,EPC都实现为32位!
| 寄存器 | 编号 | 功能 |
|---|---|---|
| SR | 12 | 配置异常的功能。 |
| Cause | 13 | 记录异常发生的原因和情况。 |
| EPC | 14 | 记录异常处理结束后需要返回的 PC。 |
| 寄存器 | 功能域 | 位域 | 解释 |
|---|---|---|---|
| SR(State Register) | IM(Interrupt Mask) | 15:10 | 分别对应六个外部中断,相应位置 1 表示允许中断,置 0 表示禁止中断。这是一个被动的功能,只能通过 mtc0 这个指令修改,通过修改这个功能域,我们可以屏蔽一些中断。 |
| SR(State Register) | EXL(Exception Level) | 1 | 任何异常发生时置位,这会强制进入核心态(也就是进入异常处理程序)并禁止中断。 |
| SR(State Register) | IE(Interrupt Enable) | 0 | 全局中断使能,该位置 1 表示允许中断,置 0 表示禁止中断。 |
| Cause | BD(Branch Delay) | 31 | 当该位置 1 的时候,EPC 指向当前指令的前一条指令(一定为跳转),否则指向当前指令。 |
| Cause | IP(Interrupt Pending) | 15:10 | 为 6 位待决的中断位,分别对应 6 个外部中断,相应位置 1 表示有中断,置 0 表示无中断,将会每个周期被修改一次,修改的内容来自计时器和外部中断。 |
| Cause | ExcCode | 6:2 | 异常编码,记录当前发生的是什么异常。 |
| EPC | - | - | 记录异常处理结束后需要返回的 PC。 |
4.设计思路
在CP0中进行处理的指令有mfc0,mtc0
- mfc0 (读)
- SR
- Cause
- EPC
- mtc0 (写/Cause only read)
- SR
- EPC
在CP0中定义三个32位寄存器,并使用宏定义定义寄存器的功能字段
1.产生异常/中断请求
使用位缩减运算符 | : result = a[0] | a[1] | ….
注:外部中断比内部异常优先级更高,即有有中断先处理中断,后处理异常
| |
2.EPC处理
对于EPC要考虑延迟槽的问题
若产生异常的为延迟槽中的指令,则跳回到跳转指令,即M_pc - 4
wire [31:2] tmpEPC = (req) ? ((M_BD) ? M_pc[31:2] -1 : M_pc[31:2]) : EPC; assign EPC_out = {tmpEPC,2'b00}; // 4 byte align注:对PC进行字对齐处理,后补2’b00
需要注意的是EPC保存当前指令PC的条件是req,即发生了异常时才会对EPC进行更新,这样就保证了EPC中的值在进入异常处理程序之后不会发生改变,识别到eret指令之后跳回到异常指令。
3.对于延迟槽指令的判断
延迟槽即跳转指令的下一条指令,所以标记延迟槽指令只需要识别出他的上一条指令是跳转指令(NPCOp_D != 3’b000),并在F级跟随该指令进行流水到M级进行判断即可(无论跳转指令是否跳转)。
| |
2.nop和req在流水线寄存器中的问题
nop
如果沿用P6/P5中暂停时在E级插入nop的写法,这个nop指令的pc和bd信号都为0。此时M级宏观PC会显示错误的值,并且如果此时发生了中断,CP0中存入错误的EPC值。正确的做法是在暂停时让pc和bd继续流水
req
发生异常时,需要跳转到异常响应代码并清空流水线内还没有执行完的指令。直接将pc清为0会导致第一条处理异常的代码到达M级之前,宏观PC都是0,故req信号来时需要将pc置为异常代码地址32'h0000_4180
| |
3.NPC
1.端口定义列表
| 名称 | 方向 | 位宽 | 描述 |
|---|---|---|---|
| req | I | 1 | 中断请求 |
| D_eret | I | 1 | D级是否为eret指令 |
| b_result | I | 1 | B类跳转指令是否满足跳转条件 |
| NPCOp | I | 3 | 地址选择 |
| F_pc | I | 32 | F级PC输入 |
| D_pc | I | 32 | D级PC输入 |
| b_offset | I | 32 | B类跳转指令偏移 |
| j_address | I | 26 | J类跳转指令偏移 |
| reg_address | I | 32 | 寄存器中保存的地址 |
| npc | O | 32 | 输出下一PC |
2.PC选择逻辑
- 若发生req中断,跳转到异常处理程序地址 : 32’h0000_4180
- 若执行eret指令,eret将保存在CP0的地址写入PC,从而实现从处理异常程序中跳回到主程序 (同时保证了不会指令eret延迟槽中的指令)
- 在D级识别到eret,NPC选择EPC输入到F级
| |
4.Bridge
1.端口定义列表
| 名称 | 方向 | 位宽 | 描述 |
|---|---|---|---|
| Addr_in | I | 32 | 写入/读取外设的地址 |
| WD_in | I | 32 | 写入外设单元的数据 |
| byteen | I | 4 | 写入外设单元的使能 |
| DM_RD | I | 32 | DM读取值的输入 |
| T1_RD | I | 32 | Timer1读取值的输入 |
| T0_RD | I | 32 | Timer0读取值的输入 |
| Addr_out | O | 32 | 写入/读取外设的地址 |
| WD_out | O | 32 | 写入外设单元的数据 |
| RD_out | O | 32 | 外设单元的读取值输出 |
| DM_WE | O | 4 | DM写入使能 |
| T1_WE | O | 1 | Timer1写入使能 |
| T0_WE | O | 1 | Timer0写入使能 |
2.实现代码(对地址的判断)
需要注意的是我们写入的外设中,DM支持按字节访问,即byteen写入使能信号4位表示,写入Timer是按字写入的,一位写入使能信号。
观察官方Timer端口定义:
- input [31:2] Addr
- input [31:0] Din
说明Timer中寄存器为按字写入
**Timer按字写入:byteen = 4’b1111(sw指令) 使用位缩减运算符变为一位 : &byteen = 1’b1 **
| |
5.Timer
课程组已经提供实现好的代码,两种模式的有限状态机,具体分析见思考题
端口定义列表
| 名称 | 方向 | 位宽 | 描述 |
|---|---|---|---|
| clk | I | 1 | 时钟信号 |
| reset | I | 1 | 同步复位信号 |
| Addr | I | 30 | 写入寄存器地址 |
| Din | I | 32 | 写入数据 |
| Dout | O | 32 | 读取数据 |
| IRQ | O | 1 | 定时/定周期产生的中断信号 |
6.乘除槽处理
- 2023秋季计算机组成课程对于乘除槽的规定:
mult在 E 级启动了乘法运算,流水到 M 级时产生了中断,此时无需停止乘法计算,其它乘除法指令同理。mthi在 E 级修改了 HI 寄存器,流水到 M 级时产生了中断,此时无需恢复 HI 寄存器的值,mtlo同理。mult在 E 级,受害指令在 M 级,此时还未改变 MDU 状态,不应开始乘法计算,其它乘除法指令同理。mthi在 E 级,受害指令在 M 级,此时还未改变 MDU 状态,不应修改 HI 寄存器的值,mtlo同理。
即发生异常时若已经启动了乘除槽就不管他,如果还没启动,就不允许启动,只需要在启动乘除槽的条件中加入!req
| |
二.测试方案
| |
三.思考题
- 请查阅相关资料,说明鼠标和键盘的输入信号是如何被 CPU 知晓的?
- 鼠标和键盘产生中断信号,进入中断处理程序,在中断处理程序中,鼠标和键盘输入信号
- 请思考为什么我们的 CPU 处理中断异常必须是已经指定好的地址?如果你的 CPU 支持用户自定义入口地址,即处理中断异常的程序由用户提供,其还能提供我们所希望的功能吗?如果可以,请说明这样可能会出现什么问题?否则举例说明。(假设用户提供的中断处理程序合法)
- 若自定义入口地址,则很多软件将会不兼容,在程序员视角设计软件的时候,中断处理的入口地址是不重要的,也就是说这是软件和硬件之间的协议。
- 为何与外设通信需要 Bridge?
- 外设的种类繁多,我们通过bridge并且约定某段内存地址对应于某个外设,这样我们就只需要通过访存去实现与外设的联系,指令集会比较的简洁。添加外设时,外设也只需要体现在入口地址的不同而不需要改变CPU的内部结构,让CPU访问外设只需通过地址,这样体现了”高内聚,低耦合”的原则
- 请阅读官方提供的定时器源代码,阐述两种中断模式的异同,并分别针对每一种模式绘制状态移图
模式0 : 定时中断
模式1 : 周期性中断
画图如下

- 倘若中断信号流入的时候,在检测宏观 PC 的一级如果是一条空泡(你的 CPU 该级所有信息均为空)指令,此时会发生什么问题?在此例基础上请思考:在 P7 中,清空流水线产生的空泡指令应该保留原指令的哪些信息?
- 写入EPC会出错,延迟槽标记信号BD也会出错。
- 如果是中断或者异常而清空流水线,应该保持原有的PC值,以保证宏观PC的正确。
- 如果是阻塞而清空流水线,应该要保持原有的PC并且保持原有的BD标志信号。
- 为什么 jalr 指令为什么不能写成 jalr $31, $31?
这种操作具有二义性,不知道先跳转还是先链接
指令集要求 rs 和 rd 不得相等,因为此类指令在重新执行时不具有相同的效果。执行此类指令的结果是不可预测的。此限制允许异常处理程序在分支延迟槽中发生异常时通过重新执行分支来恢复执行。
