(异常不和中断一起出现时指代总称异常)
任务清单
顶层模块篇
本次顶层模块,最终实例化四个部件:cpu、bridge、timer1、timer2,外界(tb)中实现DM、IFU和外界中断(外设)。你可能会对如何连接这些东西感到困惑,我只能说,反复看,反复感受。
新增元件篇
CP0
功能:识别异常/中断,并根据异常/中断记录下种类和EPC(受害者PC)
建议放到M级,这么做的人比较多,方便对拍
注:
- 退出异常的条件是发生eret指令
- 哪怕你采用分布式译码,BD信号和EPC也是需要流水的,这是因为延迟槽异常要求把EPC指向延迟槽所属的跳转指令,而分布式译码并直接连接M段控制信号在阻塞时发生延迟槽异常就会出现问题(同时,对于放到M级的CP0,宏观PC(就是M段PC也会出问题,因为阻塞插入的nop在不处理的情况下PC是0)。这些问题如何解决,信号如何流水见后。
- reset 与 EXLClr 的操作是不一样的
if(reset)begin
SR<=0;
Cause<=0;
EPC<=0;
end
else
if(EXLClr==1'b1)
begin
SR[1]<=1'b0;
end
else
begin
if(en)begin
//tackle with mtc0
end
else;
if(Req)begin
//tackle with req
end
else;
Cause[15:10] <= HWInt;
end
end
- 每个周期都需要更新存储中断信号,哪怕当前不能发生中断。
- addr、CP0In和CP0Out是用于处理mfc0和mtc0指令的
桥
功能:连接cpu和DM、timer1、timer2,将正确的数据传回cpu、传给timer1、timer2和外部部件。实现起来并不困难。
计时器
给了现成的代码,重点是理解其功能模式和如何工作。在顶层电路中,实例化举例如下:
TCDout即对应的中断信号,传递给桥,由桥将外部中断信号、两个timer中断信号拼成完整的中断信号,并传递回cpu,传递给CP0供其比较允许发生的中断类型和是否能发生中断,最终决定是否中断。
已有元件修改篇(因人而异)
识别新异常
最终采取如上的异常识别顺序。
F段识别取指异常,D段识别未知指令,E段识别算数溢出、存数/取数地址计算溢出,M段处理取数/存数地址不对齐等各种异常。中断直接提交给CP0特殊处理。
其他异常都好理解,M段的异常可能有些不好理解,因为我的实现过于丑陋,贴一段学长的代码。他是将溢出信号流水了,我觉得太麻烦,所以将溢出放在E段处理。
wire ErrAlign = ((DMType == `DM_w) && (|addr[1:0])) ||
(((DMType == `DM_h) || (DMType == `DM_hu)) && (addr[0]));
wire ErrOutOfRange = !( ((addr >= `StartAddrDM) && (addr <= `EndAddrDM)) ||
((addr >= `StartAddrTC1) && (addr <= `EndAddrTC1)) ||
((addr >= `StartAddrTC2) && (addr <= `EndAddrTC2)));
wire ErrTimer = (DMType != `DM_w) && (addr >= `StartAddrTC1);
wire ErrSaveTimer = (addr >= 32'h0000_7f08 && addr <= 32'h0000_7f0b) ||
(addr >= 32'h0000_7f18 && addr <= 32'h0000_7f1b); // cannot access count of timer
assign excAdEL = load && (ErrAlign || ErrOutOfRange || ErrTimer || excOvDM);
assign excAdES = store && (ErrAlign || ErrOutOfRange || ErrTimer || ErrSaveTimer || excOvDM);
在ALU中,溢出的判断方法是(举例加法,减法同理):
需要注意:
- 对于同一条指令,越靠近F段的异常,优先级越高。表现为第三条
- 对于不同的指令,越靠近M段的异常,优先级越高。表现为第四条
- 为实现以上效果,每一段的异常是需要流水的,举例如下(E段):
也就是说,如果流水的异常信号已经不为0,即在之前的段中已经发生了异常,那么已经发生的异常优先级更高,否则就判断新的异常。
空泡问题
当发生异常时,需要清空寄存器,修改PC,并通过CP0确保不会接受新的异常。但是该过程的“清空”与reset是不一样的,为了保证宏观PC的正确输出,当因异常而清空寄存器时,需要修改PC的值为异常进入程序的PC(这也是为什么要流水PC和BD)。
还有一个问题,因阻塞而插入的nop指令,其对应的PC值是0,因此在因阻塞而清空寄存器时,注意PC需要更新为输入的PC(这么做意味着nop的PC等同于下一条指令的PC,想一想为什么可以这么做)。举例D/E寄存器清空如下(每个寄存器的实现都不一样,不能照搬):
if(CLR==1'b1||reset==1'b1||req)//D->E register
begin
InstrTemp<=32'h00000000;
PCPlus4Temp<= stall ? PCPlus4D : (req ? 32'h0000_4184 : 0);
if(stall==1'b1)
BD_Temp<=BD_D;
else
BD_Temp<=0;
GPR_rsTemp<=0;
GPR_rtTemp<=0;
ImmTemp<=0;
excTemp<=0;
end
实现新指令
冲突处理单元
自行更新冲突控制单元的内容,根据你采用谨慎转发法(AT法)和懒惰转发法,实现方法不一样。我提供谨慎转发法的一个思路:
注意!以下讨论中省略不提mfc0/mtc0与mflo/mfhi/mtlo/mthi的rs、rt、rd区别!但两种指令目标寄存器地址差别很大,需要着重注意!!!!!
首先,在冲突处理中mfc0和mflo很相似,Tnew取值可以等同。而mfc0并不会用到32个通用寄存器的值,所以把Tuse设为inf(我的处理中是设成2’b11)即可。
而mtc0,虽然会写寄存器,但是写的是CP0里的寄存器,一般情况下并不会因为这个引发冲突。可以把这个指令和mtlo做类比,来设置它的Tuse。试想,如果mtc0指令后紧跟一条mfc0,会怎么样呢?答案显然,因为两个都是在M段才发挥真正的作用,因此无需做任何处理,只需要把mfc0的Tnew设置到M段以后才得到正确结果即可。
然后就是eret。在什么情况下eret会需要阻塞呢?在用mtc0写EPC的值时。(我的设计中NPC在D段)然而eret在D段发挥作用,mtc0在M段发挥作用,这种情况下就需要阻塞到mtc0完成功能以后。为了方便,我只写了相对应的阻塞,即新设置一Tuse_EPC,只有eret指令设为2,其他指令设为3,然后在阻塞判断时,新增对应的逻辑。举例如下:
stall_CP0=((Tuse_EPC==2'b10)&&(TnewE==2'b10)&&(A3E==14))||
((Tuse_EPC==2'b10)&&(TnewM==2'b01)&&(A3M==14));
Stall=Stall_RT|Stall_RS|stall_HILO|stall_CP0;//暂停处理完毕
我这种做法难以避免会出现误阻塞的情况,但是因此导致的额外周期可以忽略不计。
mfc0/mtc0
这两条指令的实现过程和乘除模块的mf/mt
很像,完全可以照抄,但是需要注意的是,mfc0
写入的是rt寄存器,而mfhi
和mflo
写入的是rd
寄存器,mtc0
将rt
的数据写进去,但是mthi
和mtlo
是将rs
的数据写进去
eret
需要注意两点:
- 该指令代表退出异常状态,需要注意
- 该指令无延迟槽。我的解决办法是通过根据D段控制信号控制F/D寄存器输入来人为插入一个nop指令,抵消掉延迟槽,该方法不会受阻塞影响,可靠。
细节
1、
异常发生时,乘除槽除了reset以外的行为要被冻结
2、
如何编写mars程序?
分成两段编写,分别是正常的测试程序和异常处理程序。测试程序就是正常的测试,试探能否正确识别异常和中断,异常处理程序不用教程弄得那么麻烦,就简单粗暴的
取EPC->取BD->根据BD将EPC加4或加8->放回EPC->响应中断(用教程给的sb指令)->eret退出异常
然后分别导出十六进制代码,并将测试程序代码补全到(从1开始数)1121行,用00000000
补全,然后再把异常处理程序粘贴上去,就可以了。而在Mars里,异常处理程序粘贴到测试程序文件尾部,并在前面加上.ktext 0x4180
即可
注意!mars可能有部分行为是错误的!和同学对拍更保险!
比如,mars的计时器存在很大问题,因为mars内置cpu采用单周期实现,和我们的五段全速转发流水线cpu有很大区别。
3、
芙宁娜世界第一可爱