说在前面
还得是设计文档。
每次一旦盯着理论看,又想看全又想做细,就会陷入P5当时的茫然
总是想一针见血地写出最有远见卓识的代码,当然会踌躇,更何况还未必能写出来。
但是一步一步走出来,也就走出来了。
设计文档
在对P7的整体内容有了把握之后,我们可以知道,本次P7主要要完成的任务是:
- 更改流水线各级使之可以产生异常
- 添加 CP0 与 异常处理
- 添加 Bridge 与两个外设(计时器)交互
ExcCode的产生与流水
根据教程的表格,我们可以得到:
异常与中断码 | 助记符与名称 | 指令与指令类型 | 描述 |
---|---|---|---|
0 | Int (外部中断) | 所有指令 | 中断请求,来源于计时器与外部中断。 |
4 | AdEL (取指异常) | 所有指令 | PC 地址未字对齐。 |
4 | AdEL (取指异常) | 所有指令 | PC 地址超过 0x3000 ~ 0x6ffc。 |
4 | AdEL (取数异常) | lw | 取数地址未与 4 字节对齐。 |
4 | AdEL (取数异常) | lh | 取数地址未与 2 字节对齐。 |
4 | AdEL (取数异常) | lh, lb | 取 Timer 寄存器的值。 |
4 | AdEL (取数异常) | load 型指令 | 计算地址时加法溢出。 |
4 | AdEL (取数异常) | load 型指令 | 取数地址超出 DM、Timer0、Timer1、中断发生器的范围。 |
5 | AdES (存数异常) | sw | 存数地址未 4 字节对齐。 |
5 | AdES (存数异常) | sh | 存数地址未 2 字节对齐。 |
5 | AdES (存数异常) | sh, sb | 存 Timer 寄存器的值。 |
5 | AdES (存数异常) | store 型指令 | 计算地址加法溢出。 |
5 | AdES (存数异常) | store 型指令 | 向计时器的 Count 寄存器存值。 |
5 | AdES (存数异常) | store 型指令 | 存数地址超出 DM、Timer0、Timer1、中断发生器的范围。 |
8 | Syscall (系统调用) | syscall | 系统调用。 |
10 | RI(未知指令) | 未知指令 | 未知的指令码。 |
12 | Ov(溢出异常) | add, addi, sub | 算术溢出。 |
根据教程的要求,我们容易知道,只需分析好每一个部位可能产生的异常,然后随着流水线流水即可。
同一条指令在某一个阶段不会产生多种异常。(这由异常的划分方式确定)
但是一个阶段可能存在多个异常。其处理顺序为先来后到。也就是处理最老的指令,执行到最后面的指令的新错误。
有人说,你这样做就是只处理最新的错误啊,怎么保证处理了最新的错误还能处理旧的错误呢?
这是因为处理异常的方法是把句子变成nop,并重新从受害指令下一条开始执行。
这样,未被处理的指令的错误就会重新展现出来,再次被处理。
我们决定,传入流水线的ExcCode,是要经过一个有优先级的多路选择器的。
为了区分,我们命名为
ExcCode_X(本阶段产生的) 和ExcCode_X_true(实际传入流水线的) 以及ExcCode_X_last(流水线传进来的)
接下来就来判断异常吧。
我打算根据流水级来进行分类处理。
注意:
Int表示外部中断,更表示此处无异常。因此ExcCode可以大胆赋0,具体是否中断还要看外部信号。
附一个可能用到的表格:
条目 | 地址或地址范围 | 备注 |
---|---|---|
数据存储器 | 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 |
我们使用以下宏定义:
|
|
ExcCode_F
我们发现只能发生:
AdEL: PC未字对齐
或PC超界
|
|
ExcCode_D
D级的核心工作是译码。
RI:未知指令
Syscall: 系统调用
RI与Syscall信号也由Ctrl译码时顺带产生。
|
|
|
|
ExcCode_E
AdEL: 计算load地址
时加法溢出
AdES:计算store地址
时加法溢出
Ov:add
,addi
,sub
的算数溢出
这些信号利用ALU模块得出。
|
|
大错特错!大错特错!!!
|
|
de出这个bug真费了不少功夫。
第一次de出来是意识到ALUOp是ADD,未必指令就是ADD,
还有可能是store与load指令!!
结果还没过。。
这次很随便一个数据居然de出来了
因为一条ori指令出现了Ov!!!
这让我感到极为震惊。我就看了看Ov的所有相关信号————
当我看到add,并发现它是一个32位数时……
太抽象了!!!!!
add原来是数据信号啊不是控制信号!!!
控制信号是ALUOp!!!!!
Update 12.1:
这也太抽象了!!!
感谢COKiller!!!
没想到能在这里de三次。。
第一次是因为sw犯了Ov
第二次是因为ori犯了Ov
第三次是因为div犯了Ov!!!!!!!
天打五雷轰。
multu,divu乃至于mfc0,mfhi都在ALUOp对应ADD!!!
所以我老实了。
我直接把Instr传到E级,由Instr指导is_ADD。
|
|
这样应该就没有问题了罢。。
|
|
ExcCode_M
AdEL:
- lw,lh字对齐
- lh,lb取Timer
- 超范围
AdES:
- sw,sh字对齐
- sh,sb写Timer
- 所有store写Count寄存器
- 超范围
|
|
好嘞笑死了,已经被自己蠢死了
应当改为
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
assign AdEL_M = (DMRd_M == `LH && MemAddr_M[0]) || (DMRd_M == `LW && MemAddr_M[1:0]) || (DMRd_M == `LH && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end) || (DMRd_M == `LH && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end) || (DMRd_M == `LB && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end) || (DMRd_M == `LB && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end) || (DMRd_M && (MemAddr_M < `DM_start || MemAddr_M > `DM_end) && (MemAddr_M < `T0_start || MemAddr_M > `T0_end) && (MemAddr_M < `T1_start || MemAddr_M > `T1_end) && (MemAddr_M < `Int_generator_start || MemAddr_M > `Int_generator_end)); assign AdES_M = (DMWr_M == `SH && MemAddr_M[0]) || (DMWr_M == `SW && MemAddr_M[1:0]) || (DMWr_M == `SH && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end) || (DMWr_M == `SH && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end) || (DMWr_M == `SB && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end) || (DMWr_M == `SB && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end) || (DMWr_M && MemAddr_M >= `T0_count_start && MemAddr_M <= `T0_count_end) || (DMWr_M && MemAddr_M >= `T1_count_start && MemAddr_M <= `T1_count_end) || (DMWr_M && (MemAddr_M < `DM_start || MemAddr_M > `DM_end) && (MemAddr_M < `T0_start || MemAddr_M > `T0_end) && (MemAddr_M < `T1_start || MemAddr_M > `T1_end) && (MemAddr_M < `Int_generator_start || MemAddr_M > `Int_generator_end));
不同的异常类型确实应该或起来,但是地址越界是一
种异常!!
必须同时满足在所有区域之外!内部表述应使用与
W级不产生异常。
至此,我们貌似写完了所有异常。
CP0及异常处理
我们一般将CP0放进M级
。
这意味着指令异常在M级进行处理。
这形成了单周期CPU的封装:
一般认为M级指令为正在执行的指令
。
一般认为PC_M即为宏观PC
。
一般认为W级处的指令是已完成的指令
。
一般认为F,D,E级的指令都是未开始执行的指令
。
一旦有这样单周期的认知,那么
出现异常就是指M级指令出现异常
;
受害PC
即为M级PC。
或者说,任何指令在某一流水级产生的异常,
都要等待该指令进入M级才可被处理。
CP0基础信息
课程组给出的接口表格:
端口 | 方向 | 位数 | 解释 |
---|---|---|---|
clk | I | 1 | 时钟信号 |
reset | I | 1 | 复位信号 |
en | I | 1 | 写使能信号 |
CP0Addr | I | 5 | 寄存器地址 |
CP0In | I | 32 | CP0 |
CP0Out | O | 32 | CP0 |
VPC | I | 32 | 受害PC |
BDIn | I | 1 | 是否是延迟槽指令 |
ExcCodeIn | I | 5 | 记录异常类型 |
HWInt | I | 6 | 输入中断信号 |
EXLClr | I | 1 | 用来复位 EXL |
EPCOut | O | 32 | EPC 的值 |
Req | O | 1 | 进入处理程序请求 |
课程组给出的关键寄存器信息:
寄存器 | 编号 | 功能 |
---|---|---|
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 | 任何异常发生时置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。 |
对于关键域,我们使用宏定义以方便书写:
|
|
对于CP0,它的主要功能即为
存储异常中断的相关信息,表征异常中断的开始与结束,
引导转向异常中断模块处理与退回原指令。
判断异常中断的发生
|
|
这里比较巧妙的是 (|(HWInt & `IM)) 这一语句
巧妙使用了按位与,先得到各个信号是否中断
且中断是否被允许
然后再把这6位或起来(不或也可以)
当然最后不能忘了全局使能与EXL限制
存储异常中断的相关信息
|
|
`ExcCode <= (Int_req) ? 5’b0 : ExcCodeIn;
这句话是表明优先级的,Int与Exc的区别,在CP0中就是靠Cause寄存器来展现。
如果同时发生Int与Exc,如何保证Int优先?
只需一个三目运算符,先判断中断。
EPC <= (BDIn) ? (VPC - 32’d4) : VPC;
这句话体现了BDIn的功能。
如果这句话是延迟槽语句,那么你应当保证跳转指令正常进行。如果你只重新执行延迟槽,跳转指令就不能实现。
那么此时我们就不采用直接重新执行受害指令的方法,而是采用执行受害指令前一句的分支跳转。
但是为什么可以选择执行受害指令上一句?
这样的做法无疑是执行了两遍这一指令。
但是正因为它是跳转指令,它并没有累加效应。
就连唯一有写功能的jal也只会写他对应的那一个值。
那就有人说,你这不是钻空子吗?万一添加个新跳转指令,让你给$ra写当前$ra的值加4,这不就完了吗?
新指令在W级,延迟槽在M级判出问题(或中断)。这样的话你完全来得及让第一次的写入无效,只需把W级写使能修改一下,把BDIn和req引出去,并说明这个时候不能写入。
诶诶,那又有人问了,你这么写不就说明你的写使能是可能最后突然改变的,那你之前要是执行过转发怎么办?
这就不得不说咱们的单周期思想了。在异常中断面前,M级以前的指令都相当于没有执行。转发给你啥都无所谓。
那就又有人问了,M级本身要是被转发了怎么办?
害,M级如果发生异常中断了,那就也需要重新执行,也相当于未执行指令。
好,一段小思考结束了。
写入关键寄存器
这一操作也应在CP0中完成
其实也只是添加了如下内容。
|
|
但是具体en,CP0Addr,CP0In怎么得到,那就是CP0外部的事了。
在这里我还是打算直接写好。因为这确实是一块很小的,而且与下一部分关系不大的内容。
核心就是添加mtc0,mfc0指令,放进流水线等等。
这个和mfhi,mthi的逻辑几乎完全相同。
先分析mtc0吧,只需要在M级给CP0的en接口接上Mtc0_M
写的地址是rd_M
写的内容是MemData_M
注意控制信号Tuse_rt = 2
mfc0要注意RFWr置1,Tnew = 2 写的地址是rt 写的内容是RegData
|
|
可以发现CP0Addr始终为rd,所以直接让该端口接rd_M。
整体来说是这个样子。
|
|
CPU进行异常处理
-
语句跳转 遇到异常时进入
Exception Handler
0x00004180
在异常处理程序结束时会执行指令eret
回到EPC -
语句清空 利用CP0生成的req信号对所有流水线寄存器进行清空。 (因为CP0在M级,那么此时W级的操作在本时钟周期已完成,由于同步复位,清空寄存器只会影响下一周期的内容,这样下一周期所有指令都已完成,不受干扰。)
(注意精确异常,认真阅读教程中关于乘除槽精确异常的讲述。 )
这里有很重要的一点在于
eret
没有延迟槽。因此执行完eret后,下一条指令应当是EPC。eret在D级,此时eret的下一条指令(物理层面)在F级
我们需要F级的指令是EPC而非eret的下一条指令
以下内容看乐子就行
我们采用一种奇妙的做法:
当eret在F级时,我们直接进行检测该指令是否是eret。
如果是,那么NPC为EPC。
1 2 3 4 5 6
wire Eret_F; assign Eret_F = (Instr_F == 32'd01000010000000000000000000011000); assign _npc = Eret_F ? EPC : PCSel_D ? npc : (pc_F + 32'd4);
解释一下之前的定义:
_npc为真正的NPC
PCSel_D是判断D级是否为跳转指令
npc是跳转指令算出来的npc
这里要注意EPC它并不是一个定值,他可能被mtc0改变!!
因此有一套转发与阻塞逻辑:
我现在需要一个正确的pc
|
|
这样的转发足够吗?
不够!必须阻塞!!
mtc0不被阻塞的前提是mtc0认为自己能在写CP0之前拿到对的值
所以他在没有到达CP0的时候的值都有可能不对
EPC的值是在mtc0到达W级的时候被传送到F级供eret使用
在mtc0在M级的时候,它的CP0In肯定也是对的,可以转发。
在mtc0在D,E级的时候,都可以考虑阻塞。
|
|
我们看似用的是D级的阻塞,但这样也能起到阻塞F级eret的作用
这一段的写法过于创新。我们不得不有把这种设计毁掉的打算。
Update 12.1:
我很抱歉。在没有de出真正的bug之前,我选择把这一种方法全部删去。采用了大家普遍使用的D级执行。
当然,在修改之后当时并没能解决任何问题。
这里要注意一点:
eret严格意义上在M级才能视为被执行。
因此EXLClr需要在Eret信号传到M级时才置1。
至于eret的清空延迟槽,就不再多说了,需要注意它的BD应为0。
在阻塞上我也是正常全力阻塞,转发也不想写了。。
逻辑就是eret在D级,mtc0在E,M级且要写的内容是EPC时直接大胆阻塞。
对于异常跳转,监测标志为req
req置1时,pc需变为0x00004180
|
|
req优先级需注意,低于reset,位于第二高的地位。
紧接着,我们解决req对寄存器的清空。
本来很简单的一件事情,由于阻塞而变得复杂。
阻塞,产生了空泡,但这个空泡不应是全空的。
这个空泡应当是上一条指令生命的延续。
尤其是PC信息与Bd信息。
对于阻塞型清空,应保持不变。
如D_E流水线寄存器中:
|
|
真的是这样吗??
Update 12.2 上机debug
空泡要存的信息究竟是谁?
是
被阻塞指令
的pc和跳转指令
的PCSel因此
1 2 3 4
pc_E <= reset ? 32'h00003000 : req ? 32'h00004180 : pc_D; ExcCode_E_last <= 0; PCSel_E <= (reset || req) ? 0 : PCSel_E;
两种继承方式! PCSel直接继承,pc流水继承。
其次我已做了改动,eret不再是PCSel指令了。
为什么要这么做呢?
eret他要起到修正PCSel的作用这确实,但是他还有很多自己的特别作用!
你不能对它进行额外的流水处理!
你总不能让一句nop拥有eret性质吧,我们EXLClr可是跟eret很有关系的。
外设及其交互
其实整体来说,外设的作用就只有信息存储与中断产生。
中断信号的导入
中断信号相对简单,只需要传递给HWInt接口。
Timer0 输出的中断信号接入 HWInt[0] (最低中断位),Timer1 输出的中断信号接入 HWInt[1],来自中断发生器的中断信号接入 HWInt[2]。
|
|
系统桥与信息存储
我们在此构建一个新模块Bridge
|
|
我们可以看出,它无非是
- 根据addr和byteen得出新的字节使能信号
- 对各方面的读取进行选择,得出真正的读入内容
这样,我们就可以顺利搭建顶层模块:
无非是两个Timer,还有一些信息处理与传输嘛,最后还是给CPU使用。
|
|
注意一下TC传的地址是[31:2]
连完顶层模块,P7好像就,结束了?
通过ISE把语法错误搞完,然后…
陷入无尽的debug之中了。
测试文档
P6既然过了,P7的无异常情况基本是不需要考虑的。
基本处理模式
测试首先从简单的异常开始,比如简单的Ov
认真观察波形图,分析一下进入异常处理程序后各寄存器的情况,以及是怎么跳转回去的,跳转回去各寄存器的情况又如何。
此时的异常处理程序还比较简单,比如可以只有4条语句:
取EPC,EPC+4,存EPC,eret
(这个还能顺便测测阻塞)
|
|
防止漏判
构造出所有引发异常的组合。
你只需要对着你那个表格,一种种地敲,一种种检验。
关键是看好ExcCode,这个最简单,但也往往是大量错误出现的地方。
防止误判
这个是比较难发现的,在使用COKiller之前,我碰运气确实找到了一些样例误判,但是用了COKiller我才发现div触发Ov没有被我发现。
我的建议就是多用评测机,多测点数据,认真看代码。
其他细节谬误
这些我觉得可以对着各种学长博客所强调的点先检查是否实现,再写测试程序来看写的是否正确。
例如空泡的信息继承,乘除槽的执行与否等等。
思考题
请查阅相关资料,说明鼠标和键盘的输入信号是如何被 CPU 知晓的?
鼠标和键盘等都属于外设,外设与CPU的时钟频率可谓是天差地别。中间必须要有一个接口,实现两者之间的信息交流。
这个接口往往有硬件的控制信号,更有软件的处理程序,能够使这些信号变为CPU接受的形式。
请思考为什么我们的 CPU 处理中断异常必须是已经指定好的地址?如果你的 CPU 支持用户自定义入口地址,即处理中断异常的程序由用户提供,其还能提供我们所希望的功能吗?如果可以,请说明这样可能会出现什么问题?否则举例说明。(假设用户提供的中断处理程序合法)
因为我们的异常处理程序是统一放在这里的,你要执行异常处理程序就是要到达这个pc。
不过用户自己实现处理异常中断应该也不会有大的问题,毕竟我们自己搭CPU测试的时候不就是自己写处理中断的程序嘛,写得很烂就是了。。
但是0x4180本身位置的控制一定是经过慎重考虑的,有其空间分配的优越性。
为何与外设通信需要 Bridge?
高内聚,低耦合。
外设可以添加,但是CPU不应为之改变。
这时候,系统桥就能起到分析处理双方信息的作用。
当我们添加新外设,只需改动Bridge来控制要读谁,要写谁的问题。
请阅读官方提供的定时器源代码,阐述两种中断模式的异同,并针对每一种模式绘制状态移图。
整个流程两种模式十分类似,差别在于是否具有周期性。
对于IDLE,ctrl[0]为0转为状态LOAD,并把IRQ置0,接下来,无条件进入计时阶段,如果中途ctrl[0]为1就终止,进入IDLE,否则就计数完成时进入INT,发送中断。
到了INT阶段,
if(`ctrl[2:1] == 2’b00),那么就一直中断,进入IDLE继续循环。
否则,IRQ置0,即只中断这一个周期,然后进入IDLE重新循环。
不过共同之处在于,ctrl[0]对中断的产生都有明显的控制作用。
倘若中断信号流入的时候,在检测宏观 PC 的一级如果是一条空泡(你的 CPU 该级所有信息均为空)指令,此时会发生什么问题?在此例基础上请思考:在 P7 中,清空流水线产生的空泡指令应该保留原指令的哪些信息?
会发生EPC为0的问题。这个问题在设计文档已经提到过,还是那句话:
空泡应当是上一条指令生命的延续。
我们处理的核心问题是EPC,所以说空泡要着重继承与之有关的PC值与Bd值。
空泡要存的信息究竟是谁?
是
被阻塞指令
的pc和跳转指令
的PCSel
为什么 jalr 指令为什么不能写成
jalr
$31
,$31
?
这也是我的设计文档中所提到的一个问题。
Bd为1而是用重复执行跳转指令的方法确实有潜在问题,正如jalr $31
, $31
的情况:
这是一个有累加效应的写指令。两次执行jalr的写效果是不同的。
解决方案文中也有讨论到:
异常中断在延迟槽发生,那么就屏蔽req时的写GRF,让此时W级的写指令无效。
下一次回到这句话的时候才是真正执行。
倘若延迟槽又出现问题,那就再让此时W级写使能无效即可。