北航计算机组成P7
北航计组P7
前言
- 咳咳,更的越来越少了(),最近在致力于把测评机搭得花里胡哨,所以一直没有写博客,现在刚好有点闲所以写一下P7的博客,再不写课上的指令什么的都忘光了
课下搭建注意
- 欸为什么不写课下的搭建博客。第一,我觉得之前的学长已经写得很好了(反正看懂博客P7就成功一半了,看懂源码P7就成了hhh);第二,我好懒;第三,之前似乎没看到有人写测试的博客,所以我来填一点点坑,希望对以后的同学有用(话说2024以后计组好像要改革了,希望不要删掉P7
- 搭建博客
- 一些补充的搭建要点
- 说实话我没看明白为什么要加入eret,似乎返回的EPC并不会触发这个异常()
assign pc_error = (|PC[1: 0]) | (PC < 32'h0000_3000) | (PC > 32'h0000_6fff) | !D_eret;
- Req和stall的优先级反了,这样会出bug下面同样给出测出bug的代码,或者直接用我的测评机吧()
E_PC <= stall ? D_PC : (Req ? 32'h0000_4180 : 32'b0);
ori $1, $0, 0x4180
lw $31, 0($1) <- exception
beq $31, $0, jlabel <- interrupt
nop
jlabel:
nop- 如果按照学长先判断阻塞再判断异常的话,处理lw的异常时会意外触发外部中断,导致CP0模块的cause寄存器记录interrupt的被意外置位
- 话说课程组也说过reset位应该是最优先的,但是实际我们实现时都是将reset放在优先级最低的位置,不过还是过了,大概是因为使用reset没办法和mars对拍吗(烧烤ing)
- 尽量不要用课程组的Mars的计时器吧,毕竟Mars终究还是一个单周期CPU,和我们的时钟不一样,而且就算是和好兄弟们对拍也需要保证你们阻塞的思路是一致的,烦死了(悟空脸)
- 说实话我没看明白为什么要加入eret,似乎返回的EPC并不会触发这个异常()
课下测试
CP0
- 毕竟是P7新增的模块,自然和异常中断是强相关的。其中和异常处理程序只会使用
Cause
和EPC
两个寄存器Cause(ExcCode)
- 用于判断异常中断类型,一般在异常处理程序开头就读出,再通过掩码处理跳转不同异常的处理部分
Cause(BD)
- 这个位主要是为了中断设计的,假如在一条会发生跳转的跳转指令的延迟槽触发了中断异常,这个时候我们最后应该返回跳转指令而非其延迟槽
- 中断并不改变原有程序执行路径,只是暂时停止去做一些其他的事情,我们跳回延迟槽就默认这条跳转指令不会发生跳转,显然矛盾了,所以只能设置一个延迟槽位跳回跳转指令让跳转指令重新执行一遍
- 因为BD不是给异常用的,所以我们处理异常的时候反而需要特判BD位,防止跳错位置
Cause(IP)
- 这6个位记录了中断产生的原因,通过它们我们可以处理不同的中断,不论是计时器的中断还是外界给的中断
handler
- P7测试与以往最不同的地方大概就是多了一段异常处理程序。为了保证我们的程序可以正常运行下去,有一个适合自己生成器的异常处理程序是很重要的呀,我编写异常处理程序时主要关注以下几个点
- 对于PC的异常(单独从AdEL错误中分出来)
- 对于其余AdEL、AdES、RI、Ov处理
- 对于Interrupt处理
- 记得存储上下文和恢复上下文
- 生成器遵从一般汇编程序编写思路,不使用
$a0, $a1, $k0, $k1
,因为异常处理程序刚开始需要使用这些寄存器
- 对于PC错误异常
- 超出正常PC错误,从内存
0x2180
位置读出新的EPC(这个不是什么特殊位置,或者你可以专门用一个寄存器作为存储应该返回的地址,主要就是为了一个可控,保证测试代码可以正常执行下去) - PC未对齐,强行对齐,并将PC指向下一位置(
0x3003 -> 0x3000 -> 0x3004
)
- 超出正常PC错误,从内存
- 对于其他异常
- 直接跳过这条异常指令(记得判断延迟槽,如果是延迟槽需要跳两位,因为此时EPC指向了异常指令前面的跳转指令)
- 对于Interrupt
- 对于tb文件中给出的中断,我们直接响应
0x7f20
即可,tb文件接受到响应会取消中断 - 对于计时器触发的异常,我们需要写计时器的
CTRL
寄存器,赋0给使能位就是CTRL[0]
或者干脆直接全给0就好了
- 对于tb文件中给出的中断,我们直接响应
- 其他注意点
- 调用函数慎用
jal
,改用beq
或j
,因为有的时候我们在主程序中会使用31号寄存器做些什么,异常程序按理不应该改变除了$a0, $a1, $k0, $k1
的寄存器,或者请在保存上下文之后再使用jal
- 调用函数慎用
handler源码
# 主要处理PC异常 |
异常测试
- 忘了叠甲了,我说的异常是指内部指令出现了问题导致的异常,我们主要解决下面几个问题
- 是否能正确响应各种异常
- 把各种异常试一下就好了,我是按照我在handler的分类尝试的
- 注意不要一直异常,最好穿插一些正确的指令,否则万一这个CPU啥都判为异常呢()
- 是否能正确处理延迟槽(BD)
- 比较easy,放在jal指令后面就好了
- 是否能正确处理乘除槽
- 这个我采取的是在异常指令前后随机插入
multu
或者mtlo
什么的,异常处理程序中有专门读HI LO
寄存器的,我们就不需要额外验证了
- 这个我采取的是在异常指令前后随机插入
- 是否能正确处理异常与阻塞
- 这个主要和存取类指令有关,主要就存取类指令比较方便设计阻塞,例如后面放一条不会跳的beq指令,一劳永逸;前面就放一条load指令好了,尽可能地多阻塞一会,方便查出流水寄存器可能的错误
- 是否能正确响应各种异常
中断测试
- 嘿嘿,我的中断测试单独分开了,但是中断测试实际上也会涉及异常的处理
- 由于对于一个指令位置的中断有两种方案
- 将需要中断指令的PC加入tb文件中。优点:优雅、方便;缺点:对测评机不友好(),无法测试中断和阻塞之间的耦合关系
- 使用计时器中断,几条指令过后自动触发中断。优点:测试更加灵活;缺点:不方便对拍(虽然我已经尽力保证中间不出现阻塞了,难保有人实现从P5就与我不一致)
- 注意力各位,就算使用计时器中断也不要和Mars对拍,中断要和自己的好兄弟们对拍哦,或者上github找一个学长的改一改(感谢FlyingLandlord学长orz)
- 接下来测试数据主要解决下面几个问题
- 是否能正确响应各种中断
- 这个和上面异常一样,各种中断都试一下
- 是否能正确处理跳转(尤其是延迟槽)时中断
- 分别测试跳转指令时、跳转指令延迟槽内、刚从eret中返回时中断
- 是否能正确处理阻塞和中断
- 这里我利用了一下乘除槽,由于
div mflo
指令相隔很多周期仍然会阻塞,更加方便我们在中间发挥,尤其注意这个阻塞主要测试那条产生的nop,所以请务必保证传递正确与否会导致结果不同 - 例如在被阻塞的指令之前放一条会跳转的指令,这样BD位没实现好就会发生错误
- 这里我利用了一下乘除槽,由于
- 是否能正常处理异常和中断
- 这个其实可以可以融合在其他环节中,当然随便拉一条异常指令给它设置断点即可
- 是否能正常处理延迟槽
- 为了防止有人异常和中断分开了,按理来说异常时候没问题这里也没问题,不过为了保险
- 是否能正常处理乘除槽
- 和上面一条同理,不过中断的延迟槽更方便了,生成一条乘除指令随机在这条指令前、这条指令、这条指令后生成中断点
- 是否能正确响应各种中断
关于数据生成器
- 提示,没有加入P7新增指令的转发,不过异常处理程序已经很多变了吧(),各位还是自己测试一下转发什么的吧
- 因为很懒,所以我从P3~P7的数据生成器是一整个大型文件,因此很多函数需要传递各种参数限制生成的数据(),提供给大家,不过大概率很难调用吧,里面有几个外置的函数可供P7使用!DataMaker源码
dataMaker = DataMaker()
dataMaker.exc_test(500, True, "./test")
# exc_test(times, forbidden=True, _dir=".")
# times生成指令最大长度,不建议超过1000条因为这里有中断程序
# forbidden是否禁止RI测试,方便和Mars对拍
# _dir设置好文件夹,生成的数据会在这个文件夹下
dataMaker.int_test(500, _dir="./test", tb_dir="./tbs")
# int_test(times, noTimer=False, noInterrupt=False, _dir=".", tb_dir=".")
# times同上
# noTimer不使用计时器产生中断
# noInterrupt不使用tb产生中断
# _dir同上
# tb_dir如果需要tb的中断,生成的tb文件会在这个文件夹下
课上测试
- 这个课上是真的测试哦
- 首先大家手要快,拿到了之后立马提交前四个,否则会等得很煎熬,不过我19:00提交的,19:01前四道题就过了,过题最快的一集,看它转的时候有那么一点点小紧张,不过我相信我的测评机(如果没过我大概率也de不出来,已经想好用拉肚子作为逃跑的理由了)
- 最后一道题是一道抽象指令题,归为啥类都不合适哈
withdraw
withdraw
- 将最近一条sw的行为撤销
- 首先在CP0中添加四个额外的寄存器
DomainLo
、DomainHi
、ChangeAddr
、ChangeData
ChangeAddr
: 最近一次sw修改值的地址ChangeData
: 最近一次sw修改之前的值,用于恢复这个地址的值DomainLo
: 允许撤销sw指令地址下界,低于这个值报AdES
异常DomainHi
: 允许撤销sw指令地址上界,超过这个值报AdES
异常
- 注意实现这些值,寄存器编号忘了;
mfc0
可以获取它们的值,mtc0
只可以更改地址上下界两个寄存器,而且保证是字对齐 - 如果withdraw指令前面没有出现过sw,则忽略这条指令
- 如果withdraw发挥了作用,其效果相当于一条sw指令
- 一道相当繁琐的指令题
- 首先将四个寄存器加入CP0
- CP0中设置一个符号位flag,标识是否已经有sw指令了
- 在CP0中多加一层ExcCode的判断
//withdraw时判断是否有ExcCodePlus
assign newExcCode = (ExcCode != 5'b0) ? ExcCode :
(ExcCodePlus != 5'b0) ? ExcCodePlus : 5'b0; - 建议传入ByteEn,通过是否位
4'b1111
来判断是否为sw - 遇到withdraw指令判断逻辑较为复杂
//withdraw是外界传入的信号,标识withdraw指令
if (withdraw & flag) begin
if(Addr < DomainLo || Addr >= DomainHi) begin
ExcCodePlus = `EXC_AdES;
end
if(!Req) begin
outByteEn = 4'b1111;
outAddr = ChangeAddr;
outData = ChangeData;
end
end - 然后在M级把这些输出值接到DM即可
assign m_data_byteen = (withdraw) ? outByteEn :....
assign m_data_addr ...
assign m_data_wdata ... - 最后这条指令不需要转发和暂停,课程组还算优点人性,要我当助教我就这样(doge)
- 变异load指令
withdraw $rt, offset($base)
addr <- GPR[base] + offset
vaddr <- addr[31: 2]
temp1 <- Mem[vaddr]
temp2 <- Mme[CP0.ChangeAddr]
temp3 <- temp2[4:0]
if temp1 > CP0.ChangeData:
withdraw <- 这个withdraw是原题的withdraw
elif temp1 < CP0.ChangeData:
GPR[rt] <- temp1
else:
GPR[temp3 ^ rt] <- temp2- 写完我自己都笑了,希望明年不会有这么阴间的()
- 变异load指令
彩蛋
- 我的测评机有一个彩蛋噢
- 嘻嘻
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Trash Bin for Chi!
评论