【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱: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编写,难度不大的。
