概述
【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】
异常和中断几乎是cpu最重要的特性。而异常和中断,本质上其实是一回事。很多熟悉mips的朋友,应该都听过这么一个词,那就是精确异常,那什么是精确异常呢?其实意思是说,cpu在某一个阶段发生了异常之后,并不急于马上处理,而是等到了mem访存阶段来统一处理,因为说不定在运行过程中还会出现其他异常。
发生异常的阶段很多,但是wb写回阶段是肯定不会发生异常的。所以,在mem阶段统一处理中断和异常是比较合适的。一般来说,中断的优先级高一点。如果有中断,先处理中断;没有中断,有异常的话,就先处理异常;如果这些都没有,cpu就正常执行好了。
那什么情况下会发生异常呢?其实除了wb,其他每一个阶段都有可能发生异常。取指失败、译码不正确、执行阶段发生除0、加载内存数据失败等等,这些都可能发生异常的。但是,发生异常之后,cpu不是立刻就处理的,而是跟着流水线一步一步往前走,到了访存阶段才统一处理。
假设目前译码阶段发生了异常,那么这个异常只是记录下来。它会被先被送到执行阶段,再被送到访存阶段,在访存阶段的时候,异常才会得到真正的处理。有同学也许会问,如果有多个异常怎么办呢?那就看谁的异常先送到访存阶段,先送到访存的异常肯定是最新受到处理的,哪怕它不是第一时间出现的那个异常。
1、异常传递
1)译码阶段的异常传递
//exceptiontype的低8bit留给外部中断,第9bit表示是否是syscall指令
//第10bit表示是否是无效指令,第11bit表示是否是trap指令
assign excepttype_o = {19'b0,excepttype_is_eret,2'b0,
instvalid, excepttype_is_syscall,8'b0};
//assign excepttye_is_trapinst = 1'b0;
assign current_inst_address_o = pc_i;
2)执行阶段的异常传递
assign excepttype_o = {excepttype_i[31:12],ovassert,trapassert,excepttype_i[9:8],8'h00};
assign is_in_delayslot_o = is_in_delayslot_i;
assign current_inst_address_o = current_inst_address_i;
3)mem阶段的异常输入和整理输出
always @ (*) begin
if(rst == `RstEnable) begin
excepttype_o <= `ZeroWord;
end else begin
excepttype_o <= `ZeroWord;
if(current_inst_address_i != `ZeroWord) begin
if(((cp0_cause[15:8] & (cp0_status[15:8])) != 8'h00) && (cp0_status[1] == 1'b0) &&
(cp0_status[0] == 1'b1)) begin
excepttype_o <= 32'h00000001; //interrupt
end else if(excepttype_i[8] == 1'b1) begin
excepttype_o <= 32'h00000008; //syscall
end else if(excepttype_i[9] == 1'b1) begin
excepttype_o <= 32'h0000000a; //inst_invalid
end else if(excepttype_i[10] ==1'b1) begin
excepttype_o <= 32'h0000000d; //trap
end else if(excepttype_i[11] == 1'b1) begin //ov
excepttype_o <= 32'h0000000c;
end else if(excepttype_i[12] == 1'b1) begin //返回指令
excepttype_o <= 32'h0000000e;
end
end
end
end
2、异常的统一处理,文件为ctrl.v
`include "defines.v"
module ctrl(
input wire rst,
input wire[31:0] excepttype_i,
input wire[`RegBus] cp0_epc_i,
input wire stallreq_from_id,
//来自执行阶段的暂停请求
input wire stallreq_from_ex,
output reg[`RegBus] new_pc,
output reg flush,
output reg[5:0] stall
);
always @ (*) begin
if(rst == `RstEnable) begin
stall <= 6'b000000;
flush <= 1'b0;
new_pc <= `ZeroWord;
end else if(excepttype_i != `ZeroWord) begin
flush <= 1'b1;
stall <= 6'b000000;
case (excepttype_i)
32'h00000001: begin //interrupt
new_pc <= 32'h00000020;
end
32'h00000008: begin //syscall
new_pc <= 32'h00000040;
end
32'h0000000a: begin //inst_invalid
new_pc <= 32'h00000040;
end
32'h0000000d: begin //trap
new_pc <= 32'h00000040;
end
32'h0000000c: begin //ov
new_pc <= 32'h00000040;
end
32'h0000000e: begin //eret
new_pc <= cp0_epc_i;
end
default : begin
end
endcase
end else if(stallreq_from_ex == `Stop) begin
stall <= 6'b001111;
flush <= 1'b0;
end else if(stallreq_from_id == `Stop) begin
stall <= 6'b000111;
flush <= 1'b0;
end else begin
stall <= 6'b000000;
flush <= 1'b0;
new_pc <= `ZeroWord;
end //if
end //always
endmodule
从软件的角度来说,异常处理和函数调用很像。都是pc跳到另外一个地址,开始执行新的操作。等处理完了,再返回来继续进行原来的操作。但是,和函数调用不同的地方,异常处理需要flush掉原来的流水线,这是从软件的角度所看不到的差异。
3、cp0寄存器处理
case (excepttype_i)
32'h00000001: begin
if(is_in_delayslot_i == `InDelaySlot ) begin
epc_o <= current_inst_addr_i - 4 ;
cause_o[31] <= 1'b1;
end else begin
epc_o <= current_inst_addr_i;
cause_o[31] <= 1'b0;
end
status_o[1] <= 1'b1;
cause_o[6:2] <= 5'b00000;
end
获得了excepttype_i之后,就可以在clock上升沿的时候记录返回地址、中断原因,同时关闭中断开关了。这里有一个小细节需要注意下,如果当前mem阶段中正在执行的指令是延迟槽里面的指令,那还需要对pc进行-4的操作,不然pc地址就飞掉了。
4、异常返回
在mips下面,异常返回的地址是eret。按照道理,这个时候应该返回到之前被中断的程序继续执行。那用什么方法处理比较好呢?一个比较简单的方法,就是把eret看成是和syscall一样的异常指令,等指令运行到mem阶段的时候,flush掉原来的流水线,恢复地址,打开中断即可。
32'h0000000e: begin //eret
new_pc <= cp0_epc_i;
end
大家细看一下ctrl.v这段代码,也能明白eret是如何处理的。
5、defines.v中需要修改的一处代码
`define InstMemNum 128
之前测试的汇编文件都比较短,但是在异常测试的case中,需要pc地址跳转。这个时候,编译器就会出现很多数值0的插入动作,故代码长度比原来要长一点。
6、准备汇编文件
.org 0x0
.set noat
.set noreorder
.set nomacro
.global _start
_start:
ori $1,$0,0x100 # $1 = 0x100
jr $1
nop
.org 0x40
ori $1,$0,0x8000 # $1 = 0x00008000
ori $1,$0,0x9000 # $1 = 0x00009000
mfc0 $1,$14,0x0 # $1 = 0x0000010c
addi $1,$1,0x4 # $1 = 0x00000110
mtc0 $1,$14,0x0
eret
nop
.org 0x100
ori $1,$0,0x1000 # $1 = 0x1000
sw $1, 0x0100($0) # [0x100] = 0x00001000
mthi $1 # HI = 0x00001000
syscall
lw $1, 0x0100($0) # $1 = 0x00001000
mfhi $2 # $2 = 0x00001000
_loop:
j _loop
nop
汇编代码中的地址有三处,分别是0x0、0x40、0x100,中间没有汇编的地方,编译器会用0进行补全操作。
7、翻译成二进制文件
34010100
00200008
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
34018000
34019000
40017000
20210004
40817000
42000018
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
34011000
ac010100
00200011
0000000c
8c010100
00001010
08000046
00000000
8、利用iverilog和gtkwave进行波形分析
测试的时候可以重点观察一下pc寄存器和flush信号。pc寄存器主要记录了取指的顺序,而flush表示了cpu当前正在发生异常,需要进行流水下清空,下一步pc就要跳转了。首先查看pc为0,接着跳到0x100,结合汇编来看,这一切都算正常。等到390ns的时候,发现出现了flush清空操作。
因为异常只有mem阶段才会处理,而pc地址可能已经提前走了三步。当前pc是0x118,因为每条指令的长度是4,所以0x118 - 0x4*3 = 0x10C。这个时候看0x10c处的指令是什么即可。对着汇编文件看了下,原来是syscall,那么这个时候发生异常被执行也就不奇怪了。
继续往后,可以观察下一次flush是什么时候被触发的。
看了一下 pc地址,数值为0x60。根据我们的经验,触发异常的指令地址是0x60-0x4 * 3 = 0x54。这个时候,对着汇编查看一下0x54对应的汇编指令是什么,原来是eret,也就是中断返回。所以这个时候,相当于再次借助于exception机制对流水线做了一次flush操作。
并且,我们还惊奇的发现,中断后继续执行的pc地址是0x110,这就是之前0x10c后面一条指令的地址。而0x10c就是发生异常执行syscall的地址。这样一来,所有的汇编代码、波形图就全部对上了。
9、中断测试
1)准备中断测试的汇编代码
.org 0x0
.set noat
.set noreorder
.set nomacro
.global _start
_start:
ori $1,$0,0x100 # $1 = 0x100
jr $1
nop
.org 0x20
addi $2,$2,0x1
mfc0 $1,$11,0x0
addi $1,$1,100
mtc0 $1,$11,0x0
eret
nop
.org 0x100
ori $2,$0,0x0
ori $1,$0,100
mtc0 $1,$11,0x0
lui $1,0x1000
ori $1,$1,0x401
mtc0 $1,$12,0x0
_loop:
j _loop
nop
2)翻译成二进制文件
34010100
00200008
00000000
00000000
00000000
00000000
00000000
00000000
20420001
40015800
20210064
40815800
42000018
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
34020000
34010064
40815800
3c011000
34210401
40816000
08000046
00000000
3)利用iverilog和gtkwave分析
结合汇编代码,我们发现这是一个内部compare和count寄存器产生的中断。主程序中,初始化compare寄存器,打开中断。所有一切做完之后,_loop循环。如果发生中断,就会跳到0x20处理中断。中断处理程序中继续设置compare寄存器,方便下次继续产生中断。等到中断处理结束,eret返回原来的程序继续执行。周而复始,就是这么一个处理过程。
对于数字电路分析来说,中断观察pc寄存器和flush的数值即可达到此目的。
注:后面的话
中断和异常是cpu的一个重要组成部分,建议可以反复看看、反复思考。一旦掌握了,后续收益很大,对于debug和性能分析都有很大的好处。
至此,关于cpu的分析就结束了,也许有同学会说,还有总线、gpio、uart、flash这些外设可以聊一聊。个人看来,这些外设都是作为单一功能模块独立存在的,他们都是为了配合cpu而形成一个完整的mcu或者soc而存在的。只要掌握了cpu设计的精髓,一般的外设ip编写,难度不大的。
最后
以上就是缓慢白开水为你收集整理的cpu设计和实现(异常和中断)的全部内容,希望文章能够帮你解决cpu设计和实现(异常和中断)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复