看到一个读书笔记,和大家分享学习学习~


在ARM公司的RISC指令集Cortex-M3系列MCU中,异常与中断的实现与处理非常简单。对于串口接收中断函数如下:
void USART1_IRQHandler(void){
  •   uint8_t ret = 0;
  •   if (USART_GetIntStatus(USART1, USART_INT_RXDNE) != RESET)  {
  •     uart_rx.data[uart_rx.length] = USART_ReceiveData(USART1);
  •     uart_rx.length++;
  •   }
  • }
  • 复制代码
    进入中断函数后,检查一下是哪个中断与状态,当是接收数据中断后,在读取接收数据寄存器后,MCU硬件自动清除中断标识,并等待进入下一个中断的到来。而对于Systick滴哒定时器的中断函数则更是简单:
    void SysTick_Handler(void)
  • {
  •     timeline_ms++;
  • }
  • 复制代码
    systick中断函数触发后,MCU硬件则自己将计数器归位,每次自动执行,不需要软件来干预定时器。简单的不能再简单了。作为对比,RISC-V的MCU中断函数的实现则复杂的多的多,以GD32VF103为例:
    #Save caller registers
  • .macro SAVE_CONTEXT
  •   addi sp, sp, -20*REGBYTES
  •   STORE x1, 0*REGBYTES(sp)
  •     STORE x4, 1*REGBYTES(sp)
  •     STORE x5, 2*REGBYTES(sp)
  •     STORE x6, 3*REGBYTES(sp)
  •     STORE x7, 4*REGBYTES(sp)
  •     STORE x10, 5*REGBYTES(sp)
  •     STORE x11, 6*REGBYTES(sp)
  •     STORE x12, 7*REGBYTES(sp)
  •     STORE x13, 8*REGBYTES(sp)
  •     STORE x14, 9*REGBYTES(sp)
  •     STORE x15, 10*REGBYTES(sp)
  •         STORE x16, 11*REGBYTES(sp)
  •     STORE x17, 12*REGBYTES(sp)
  •     STORE x28, 13*REGBYTES(sp)
  •     STORE x29, 14*REGBYTES(sp)
  •     STORE x30, 15*REGBYTES(sp)
  •     STORE x31, 16*REGBYTES(sp)
  • #restore caller registers
  • .macro RESTORE_CONTEXT
  •   LOAD x1, 0*REGBYTES(sp)
  •     LOAD x4, 1*REGBYTES(sp)
  •     LOAD x5, 2*REGBYTES(sp)
  •     LOAD x6, 3*REGBYTES(sp)
  •     LOAD x7, 4*REGBYTES(sp)
  •     LOAD x10, 5*REGBYTES(sp)
  •     LOAD x11, 6*REGBYTES(sp)
  •     LOAD x12, 7*REGBYTES(sp)
  •     LOAD x13, 8*REGBYTES(sp)
  •     LOAD x14, 9*REGBYTES(sp)
  •     LOAD x15, 10*REGBYTES(sp)
  •         LOAD x16, 11*REGBYTES(sp)
  •     LOAD x17, 12*REGBYTES(sp)
  •     LOAD x28, 13*REGBYTES(sp)
  •     LOAD x29, 14*REGBYTES(sp)
  •     LOAD x30, 15*REGBYTES(sp)
  •     LOAD x31, 16*REGBYTES(sp)
  • 复制代码
    不仅需要有检查中断标识的代码,而且还需要保存与恢复MCU系统堆栈寄存器的现场。
    当年在试用RISC-V的MCU时,我曾公开表态RISC-V指令集这中断的处理代码——需要软件开发人员手动保存现场。但当时只知其然,并不知道所以然。在阅读了《RISC-V体系结构编程与实践 》书中第八章和第九章后,总算是明白了其中的缘由。下面就跟随我的读书笔记,一起来了解吧!
    ARM公司的RISC指令集是以应用为主,经过多年的经验总结,MCU中的中断控制器实现复杂,实现用户软件开发方便——典型的商用客户需求角度考虑嘛。而RISC-V是新生,秉承“大道至简”的方针——其实在本文中,可以理解为方便硬件实现。换句话说,RISC-V的设计思路是方便硬件或逻辑设计人员上手。当然要苦了我们这些做软件开发的人员了。
    我们在来看看RISC-V在发生异常时都分了几步?每步谁来参与了?又都做了什么?
    当异常发生时,我们把处理过程分解为3部分:
    1. 异常发生时 CPU做的事情

    • 保存当前 PC 值到mepc 寄存器中
    • 把异常的类型更新到mcause 寄存器
    • 把发生异常时的虚拟地址更新到 mtval 存器中
    • 保存异常发生前的中断状态,即把异常发生前的 MIE 字段保存到 mstatus 寄存器的MPE 字段中
    • 保存异常发生前的处理器模式(如U 模式、S 模式等),即把异常发生前的处理器模式保存到mstatus 寄存器的MPP 字段中
    • 关闭本地中断,即设置mstatus 寄存器中的MIE字段为0
    • 设置处理器模式为M模式
    • 跳转到异常向量表,即把 mtvec 寄存器的值设置到PC 寄存器中

    2. 操作系统需要做的事情

    操作系练需要做的事情是从异常向量表开始的。RISC-V 支持两种异常向量表处理模式。一种是直接跳转模式,另一种是异常向量模式。下面是操作系统需要做的事情。


    • 保在异常发生时的上下文,上下文包括所有通用存器的值和部分 M模式下的寄存器的值。上下文需要保存到栈里。


    • 查询 mcause 寄存器中的异常以及中断编号,跳转到合适的异常处理程序中
    • 异常或者中断处理完成之后,恢复保存在栈里的上下文
    • 执行MRET指令,返回异常现场

    3. 异常返回


    • 恢复设置 MIE 字段。把 mstatus 寄存器中的 MPIE 字段设置到 mstatus 寄存器的MIE段,恢复触发异常前的中断使能状态,通常这相当于使能了本地中断
    • 将处理器模式设置成之前保存到 MPP字段的处理器模式
    • 把mepc寄存器保存的值设置到PC寄存器中,即返回异常触发的现场

    RISC-V指令集的中断设计其实是设计的非常简单,只是大量的工作交由软件来做了而已。RISC-V中的中断与异常还是有很多可说的,比如异常与中断的区别?中断控制器如何实现中断嵌套?等等。限于篇幅,不再继续了。更多更权威的解读,大家直接下载RISC-V的技术文档——反正字也不多~~