• cpu设计和实现(异常和中断)


    【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】

            异常和中断几乎是cpu最重要的特性。而异常和中断,本质上其实是一回事。很多熟悉mips的朋友,应该都听过这么一个词,那就是精确异常,那什么是精确异常呢?其实意思是说,cpu在某一个阶段发生了异常之后,并不急于马上处理,而是等到了mem访存阶段来统一处理,因为说不定在运行过程中还会出现其他异常。

            发生异常的阶段很多,但是wb写回阶段是肯定不会发生异常的。所以,在mem阶段统一处理中断和异常是比较合适的。一般来说,中断的优先级高一点。如果有中断,先处理中断;没有中断,有异常的话,就先处理异常;如果这些都没有,cpu就正常执行好了。

            那什么情况下会发生异常呢?其实除了wb,其他每一个阶段都有可能发生异常。取指失败、译码不正确、执行阶段发生除0、加载内存数据失败等等,这些都可能发生异常的。但是,发生异常之后,cpu不是立刻就处理的,而是跟着流水线一步一步往前走,到了访存阶段才统一处理。

            假设目前译码阶段发生了异常,那么这个异常只是记录下来。它会被先被送到执行阶段,再被送到访存阶段,在访存阶段的时候,异常才会得到真正的处理。有同学也许会问,如果有多个异常怎么办呢?那就看谁的异常先送到访存阶段,先送到访存的异常肯定是最新受到处理的,哪怕它不是第一时间出现的那个异常。

    1、异常传递

    1)译码阶段的异常传递

    1. //exceptiontype的低8bit留给外部中断,第9bit表示是否是syscall指令
    2. //10bit表示是否是无效指令,第11bit表示是否是trap指令
    3. assign excepttype_o = {19'b0,excepttype_is_eret,2'b0,
    4. instvalid, excepttype_is_syscall,8'b0};
    5. //assign excepttye_is_trapinst = 1'b0;
    6. assign current_inst_address_o = pc_i;

    2)执行阶段的异常传递

    1. assign excepttype_o = {excepttype_i[31:12],ovassert,trapassert,excepttype_i[9:8],8'h00};
    2. assign is_in_delayslot_o = is_in_delayslot_i;
    3. assign current_inst_address_o = current_inst_address_i;

    3)mem阶段的异常输入和整理输出

    1. always @ (*) begin
    2. if(rst == `RstEnable) begin
    3. excepttype_o <= `ZeroWord;
    4. end else begin
    5. excepttype_o <= `ZeroWord;
    6. if(current_inst_address_i != `ZeroWord) begin
    7. if(((cp0_cause[15:8] & (cp0_status[15:8])) != 8'h00) && (cp0_status[1] == 1'b0) &&
    8. (cp0_status[0] == 1'b1)) begin
    9. excepttype_o <= 32'h00000001; //interrupt
    10. end else if(excepttype_i[8] == 1'b1) begin
    11. excepttype_o <= 32'h00000008; //syscall
    12. end else if(excepttype_i[9] == 1'b1) begin
    13. excepttype_o <= 32'h0000000a; //inst_invalid
    14. end else if(excepttype_i[10] ==1'b1) begin
    15. excepttype_o <= 32'h0000000d; //trap
    16. end else if(excepttype_i[11] == 1'b1) begin //ov
    17. excepttype_o <= 32'h0000000c;
    18. end else if(excepttype_i[12] == 1'b1) begin //返回指令
    19. excepttype_o <= 32'h0000000e;
    20. end
    21. end
    22. end
    23. end

    2、异常的统一处理,文件为ctrl.v

    1. `include "defines.v"
    2. module ctrl(
    3. input wire rst,
    4. input wire[31:0] excepttype_i,
    5. input wire[`RegBus] cp0_epc_i,
    6. input wire stallreq_from_id,
    7. //来自执行阶段的暂停请求
    8. input wire stallreq_from_ex,
    9. output reg[`RegBus] new_pc,
    10. output reg flush,
    11. output reg[5:0] stall
    12. );
    13. always @ (*) begin
    14. if(rst == `RstEnable) begin
    15. stall <= 6'b000000;
    16. flush <= 1'b0;
    17. new_pc <= `ZeroWord;
    18. end else if(excepttype_i != `ZeroWord) begin
    19. flush <= 1'b1;
    20. stall <= 6'b000000;
    21. case (excepttype_i)
    22. 32'h00000001: begin //interrupt
    23. new_pc <= 32'h00000020;
    24. end
    25. 32'h00000008: begin //syscall
    26. new_pc <= 32'h00000040;
    27. end
    28. 32'h0000000a: begin //inst_invalid
    29. new_pc <= 32'h00000040;
    30. end
    31. 32'h0000000d: begin //trap
    32. new_pc <= 32'h00000040;
    33. end
    34. 32'h0000000c: begin //ov
    35. new_pc <= 32'h00000040;
    36. end
    37. 32'h0000000e: begin //eret
    38. new_pc <= cp0_epc_i;
    39. end
    40. default : begin
    41. end
    42. endcase
    43. end else if(stallreq_from_ex == `Stop) begin
    44. stall <= 6'b001111;
    45. flush <= 1'b0;
    46. end else if(stallreq_from_id == `Stop) begin
    47. stall <= 6'b000111;
    48. flush <= 1'b0;
    49. end else begin
    50. stall <= 6'b000000;
    51. flush <= 1'b0;
    52. new_pc <= `ZeroWord;
    53. end //if
    54. end //always
    55. endmodule

            从软件的角度来说,异常处理和函数调用很像。都是pc跳到另外一个地址,开始执行新的操作。等处理完了,再返回来继续进行原来的操作。但是,和函数调用不同的地方,异常处理需要flush掉原来的流水线,这是从软件的角度所看不到的差异

    3、cp0寄存器处理

    1. case (excepttype_i)
    2. 32'h00000001: begin
    3. if(is_in_delayslot_i == `InDelaySlot ) begin
    4. epc_o <= current_inst_addr_i - 4 ;
    5. cause_o[31] <= 1'b1;
    6. end else begin
    7. epc_o <= current_inst_addr_i;
    8. cause_o[31] <= 1'b0;
    9. end
    10. status_o[1] <= 1'b1;
    11. cause_o[6:2] <= 5'b00000;
    12. end

            获得了excepttype_i之后,就可以在clock上升沿的时候记录返回地址、中断原因,同时关闭中断开关了。这里有一个小细节需要注意下,如果当前mem阶段中正在执行的指令是延迟槽里面的指令,那还需要对pc进行-4的操作,不然pc地址就飞掉了。

    4、异常返回

            在mips下面,异常返回的地址是eret。按照道理,这个时候应该返回到之前被中断的程序继续执行。那用什么方法处理比较好呢?一个比较简单的方法,就是把eret看成是和syscall一样的异常指令,等指令运行到mem阶段的时候,flush掉原来的流水线,恢复地址,打开中断即可。

    1. 32'h0000000e: begin //eret
    2. new_pc <= cp0_epc_i;
    3. end

            大家细看一下ctrl.v这段代码,也能明白eret是如何处理的。

    5、defines.v中需要修改的一处代码

    `define InstMemNum 128

            之前测试的汇编文件都比较短,但是在异常测试的case中,需要pc地址跳转。这个时候,编译器就会出现很多数值0的插入动作,故代码长度比原来要长一点。

    6、准备汇编文件

    1. .org 0x0
    2. .set noat
    3. .set noreorder
    4. .set nomacro
    5. .global _start
    6. _start:
    7. ori $1,$0,0x100 # $1 = 0x100
    8. jr $1
    9. nop
    10. .org 0x40
    11. ori $1,$0,0x8000 # $1 = 0x00008000
    12. ori $1,$0,0x9000 # $1 = 0x00009000
    13. mfc0 $1,$14,0x0 # $1 = 0x0000010c
    14. addi $1,$1,0x4 # $1 = 0x00000110
    15. mtc0 $1,$14,0x0
    16. eret
    17. nop
    18. .org 0x100
    19. ori $1,$0,0x1000 # $1 = 0x1000
    20. sw $1, 0x0100($0) # [0x100] = 0x00001000
    21. mthi $1 # HI = 0x00001000
    22. syscall
    23. lw $1, 0x0100($0) # $1 = 0x00001000
    24. mfhi $2 # $2 = 0x00001000
    25. _loop:
    26. j _loop
    27. nop

            汇编代码中的地址有三处,分别是0x0、0x40、0x100,中间没有汇编的地方,编译器会用0进行补全操作。

    7、翻译成二进制文件

    1. 34010100
    2. 00200008
    3. 00000000
    4. 00000000
    5. 00000000
    6. 00000000
    7. 00000000
    8. 00000000
    9. 00000000
    10. 00000000
    11. 00000000
    12. 00000000
    13. 00000000
    14. 00000000
    15. 00000000
    16. 00000000
    17. 34018000
    18. 34019000
    19. 40017000
    20. 20210004
    21. 40817000
    22. 42000018
    23. 00000000
    24. 00000000
    25. 00000000
    26. 00000000
    27. 00000000
    28. 00000000
    29. 00000000
    30. 00000000
    31. 00000000
    32. 00000000
    33. 00000000
    34. 00000000
    35. 00000000
    36. 00000000
    37. 00000000
    38. 00000000
    39. 00000000
    40. 00000000
    41. 00000000
    42. 00000000
    43. 00000000
    44. 00000000
    45. 00000000
    46. 00000000
    47. 00000000
    48. 00000000
    49. 00000000
    50. 00000000
    51. 00000000
    52. 00000000
    53. 00000000
    54. 00000000
    55. 00000000
    56. 00000000
    57. 00000000
    58. 00000000
    59. 00000000
    60. 00000000
    61. 00000000
    62. 00000000
    63. 00000000
    64. 00000000
    65. 34011000
    66. ac010100
    67. 00200011
    68. 0000000c
    69. 8c010100
    70. 00001010
    71. 08000046
    72. 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)准备中断测试的汇编代码

    1. .org 0x0
    2. .set noat
    3. .set noreorder
    4. .set nomacro
    5. .global _start
    6. _start:
    7. ori $1,$0,0x100 # $1 = 0x100
    8. jr $1
    9. nop
    10. .org 0x20
    11. addi $2,$2,0x1
    12. mfc0 $1,$11,0x0
    13. addi $1,$1,100
    14. mtc0 $1,$11,0x0
    15. eret
    16. nop
    17. .org 0x100
    18. ori $2,$0,0x0
    19. ori $1,$0,100
    20. mtc0 $1,$11,0x0
    21. lui $1,0x1000
    22. ori $1,$1,0x401
    23. mtc0 $1,$12,0x0
    24. _loop:
    25. j _loop
    26. nop

    2)翻译成二进制文件

    1. 34010100
    2. 00200008
    3. 00000000
    4. 00000000
    5. 00000000
    6. 00000000
    7. 00000000
    8. 00000000
    9. 20420001
    10. 40015800
    11. 20210064
    12. 40815800
    13. 42000018
    14. 00000000
    15. 00000000
    16. 00000000
    17. 00000000
    18. 00000000
    19. 00000000
    20. 00000000
    21. 00000000
    22. 00000000
    23. 00000000
    24. 00000000
    25. 00000000
    26. 00000000
    27. 00000000
    28. 00000000
    29. 00000000
    30. 00000000
    31. 00000000
    32. 00000000
    33. 00000000
    34. 00000000
    35. 00000000
    36. 00000000
    37. 00000000
    38. 00000000
    39. 00000000
    40. 00000000
    41. 00000000
    42. 00000000
    43. 00000000
    44. 00000000
    45. 00000000
    46. 00000000
    47. 00000000
    48. 00000000
    49. 00000000
    50. 00000000
    51. 00000000
    52. 00000000
    53. 00000000
    54. 00000000
    55. 00000000
    56. 00000000
    57. 00000000
    58. 00000000
    59. 00000000
    60. 00000000
    61. 00000000
    62. 00000000
    63. 00000000
    64. 00000000
    65. 34020000
    66. 34010064
    67. 40815800
    68. 3c011000
    69. 34210401
    70. 40816000
    71. 08000046
    72. 00000000

    3)利用iverilog和gtkwave分析

            结合汇编代码,我们发现这是一个内部compare和count寄存器产生的中断。主程序中,初始化compare寄存器,打开中断。所有一切做完之后,_loop循环。如果发生中断,就会跳到0x20处理中断。中断处理程序中继续设置compare寄存器,方便下次继续产生中断。等到中断处理结束,eret返回原来的程序继续执行。周而复始,就是这么一个处理过程。

             对于数字电路分析来说,中断观察pc寄存器和flush的数值即可达到此目的。 

     注:后面的话

            中断和异常是cpu的一个重要组成部分,建议可以反复看看、反复思考。一旦掌握了,后续收益很大,对于debug和性能分析都有很大的好处。

            至此,关于cpu的分析就结束了,也许有同学会说,还有总线、gpio、uart、flash这些外设可以聊一聊。个人看来,这些外设都是作为单一功能模块独立存在的,他们都是为了配合cpu而形成一个完整的mcu或者soc而存在的。只要掌握了cpu设计的精髓,一般的外设ip编写,难度不大的。

  • 相关阅读:
    使用Python对数据的操作转换
    设计一个支持百万用户的系统
    通用文档信息提取模型浅析
    直播背后的原理是?初识视频流协议 HLS 和 RTMP
    DDR3 NATIVE接口
    用预训练好的VGG16网络+ImageNet(用于图像分类的1000个类别标签)对图片类别做出预测
    UMI
    centos搭建docker镜像Harbor仓库的简明方法
    Java --- SpringMVC的RESTFul风格
    面试知识储备--打包工具篇(webpack和vite)
  • 原文地址:https://blog.csdn.net/feixiaoxing/article/details/128122334