在长期的嵌入式开发实践中,对中断机制的理解始终停留在表面层次,特别当开发者长期局限于纯软件抽象层面时,对中断机制的理解极易陷入"知其然而不知其所以然"的困境,这种认知的局限更为明显;随着工作需要不断深入底层技术,对硬件机制的了解逐渐加深,并积累了大量的学习笔记。借此机会,我将这些零散的知识进行系统化梳理,既是对自身知识的复盘,也希望能为相关领域的开发者提供些许帮助和参考。关于RISC-V中断机制的分析,本文将从硬件实现原理和软件应用以下两个方面来展开介绍:
RISC-V CLINT、PLIC及芯来ECLIC中断机制分析 —— RISC-V中断机制(一)
ECLIC中断流程及实际应用 —— RISC-V中断机制(二)
背景
中断(Interrupt)机制,即处理器内核在顺序执行程序指令流的过程中突然被别的请求打断而中止执行当前的程序,转而去处理别的事情,待其处理完了别的事情,然后重新回到之前程序中断的点继续执行之前的程序指令流。
中断相关的基本知识要点:
- 打断处理器执行的“别的请求”便称之为中断请求(Interrupt Request),“别的请求”的来源便称之为中断源(Interrupt Source),中断源通常来自于内核外部(称之为外部中断源),也可以来自于内核内部(成为内部中断源)。
- 处理器转而去处理的“别的事情”便称之为中断服务程序(Interrupt Service Routine,ISR)。
- 中断处理是一种正常的机制,而非一种错误情形。处理器收到中断请求之后,需要保存当前程序的现场,简称为“保存现场”。等到处理完中断服务程序后,处理器需要恢复之前的现场,从而继续执行之前被打断的程序,简称为“恢复现场”。
- 可能存在多个中断源同时向处理器发起请求的情形,需要对这些中断源进行仲裁,从而选择哪个中断源被优先处理。此种情况称为“中断仲裁”,同时可以给不同的中断分配级别和优先级以便于仲裁,因此中断存在着“中断级别”和“中断优先级”的概念。
芯来N级别处理器内核实现了一个“改进型内核中断控制器(Enhanced Core Local Interrupt Controller,ECLIC)”,可用于多个中断源的管理。N级别处理器内核中的所有类型(除了调试中断之外)的中断都由ECLIC统一进行管理。
1 寄存器
有详细介绍说过ECLIC相关的寄存器,下面介绍中断处理流程使用的CSR寄存器:用来保存控制信息。
1.1 硬件自动填写的寄存器
mepc(Machine Exception Program Counter)
保存发生异常或中断时的PC值。
如果中断处理需要恢复到异常指令后一条指令进行执行,就需要正确判断将 pc 寄存器加上多少字节。mcause(Machine Cause Register)
记录中断是否是硬件中断,以及具体的中断原因,如:- Interrupt Bit(最高位):1 表示中断,0 表示异常。
- Exception Code(低 31 位):具体原因编码(如 0x0B 表示外部中断)。
异常编码表:
mtval(Machine Trap Value Register)
存储与异常相关的附加信息(如非法地址、非法指令编码)。- 非法地址异常:mtval 记录访问的非法地址。
- 非法指令异常:mtval 存储非法指令的二进制编码。
- 对于中断(非异常),mtval 通常无意义,可能保留为 0
1.2 指示硬件处理中断的寄存器
- mtvec(Machine Trap Vector Base Address Register)
设置机器模式(M-mode)下中断和异常的入口地址基址。存储了一个基址 BASE 和模式 MODE:- MODE 为 0 表示 直接模式,即遇到中断便跳转至 同一入口地址(mtvec.BASE)。
- MODE 为 1 表示 向量模式,中断跳转到 mtvec.BASE + 4 * cause,异常仍使用统一入口。
以上是官方中断入口,芯来的eclic是基于clic,这里是有改动的,正常情况下mtvec是作为异常处理入口的,中断入口是由mtvt2定义。芯来mtvec定义如下:
mstatus(Machine Status Register)
控制全局中断使能及特权模式切换;如:- MIE(Machine Interrupt Enable):全局中断开关。若为 0,所有中断被屏蔽。
- MPP(Machine Previous Privilege):记录中断前的特权模式(如 M/S/U-mode),用于 mret 返回。
mie(Machine Interrupt Enable Register)
用来控制具体类型中断的使能,如:- MEIE(Machine External Interrupt Enable):外部中断(如 PLIC/ECLIC 中断)使能。
- MTIE(Machine Timer Interrupt Enable):定时器中断使能。
- MSIE(Machine Software Interrupt Enable):软件中断使能。
mip(Machine Interrupt Pending Register)
和 mie 相对应,标记中断的挂起状态(Pending Bits)- MEIP(Machine External Interrupt Pending):外部中断挂起位。
- MTIP(Machine Timer Interrupt Pending):定时器中断挂起位。
- MSIP(Machine Software Interrupt Pending):软件中断挂起位。
mtvt
寄存器保存中断向量表的基址(在CLIC模式下),基址至少对齐64字节边界mtvt2
用于指示所有ECLIC中断共享的公共基处理程序的入口地址
mscratch
寄存器的用处会在实现线程时起到作用,在中断处理开始时,将当前线程的上下文指针保存到 mscratch,再从 mscratch 加载中断栈指针;感兴趣可以自行学习下。msubm
芯来自定义的CSR msubm寄存器保存当前机器子模式和当前陷阱之前的机器子模式
- jalmnxti
芯来自定义的CSR jalmnxti,用来减少中断延迟并加速中断尾部链接。
jalmnxti包括mnxti(Next Interrupt Handler Address and Interrupt-Enable CSR)的所有功能,此外还包括启用中断、处理下一个中断、跳转到下一个中断入口地址以及跳转到中断处理程序。(感兴趣可以自行继续深入学习下)
实际通过修改jalmnxti和ra的地址的值,在中断嵌套和咬尾时,可以节省保存上下文(CSR和通用寄存器)的开销,
1.4 临时寄存器
- pushmepc
- pushmsubm
- pushmcause
芯来自定义了通过CSR指令csrrwi将msubm、mepc、mcause的值存储在以SP为基址的内存空间中,该指令将CSR寄存器的值存储在SP+1*4地址中。
2 ECLIC中断处理流程
2.1 整体流程(主要以非向量中断为例)
当一个hart发生中断时,整个中断流程需要软硬协作完成,下图是eclic中断处理,包括:进入以及推出全部流程流程
硬件接收到中断信号,硬件自动更新CSR寄存器
- 更新内核退出中断时的返回地址,存储在mepc(1、该地址就是中断打断的PC值(在中断结束之后,回到被停止执行的程序点), 2、mepc软件可以显示修改)
- ①、mcause.EXCCODE存放中断ID,以便软件查询;②、如果当前中断抢占了低优先级中断,mcause.MPIL将更新为minstatus.MIL的值,处理中断后,将使用mcause.MPIL的值来恢复mintcause.MIL的值;③、如果是向量模式中断,mcause.minhv的值将更新为1,在完成从中断向量表中取出存储的目标地址,然后再跳转到目标地址中去后,mcause.minhv域的值清除为0
- ①、mstatus.MPIE更新为mstatus.MIE的值,mstatus.MIE置0(屏蔽所有中断);②、处理器的当前特权模式(Privilege Mode)切换到机器模式(Machine Mode)mstatus.MPP从特权模式将切换到机器模式;
- ①、msubm.PTYP域的值被更新为中断发生前的Machine Sub-Mode(msubm.TYP域的值)②、msubm.TYP域的值则被更新为“中断处理模式”(反映当前的模式已经是“中断处理模式”)
- 更新内核退出中断时的返回地址,存储在mepc(1、该地址就是中断打断的PC值(在中断结束之后,回到被停止执行的程序点), 2、mepc软件可以显示修改)
跳转到共享中断入口地址,保存中断上下文(非向量中断)(寄存器clicintattr[i]的shv域决定中断是向量中断还是非向量中断)
- 跳入到mtvt2.CMMON-CODE-ENTRY(mtvt2.MTVT2EN = 1);
- 将一些通用寄存器(ra/tp/t0-t6/a0-a7)(保存中断上下文)保存到堆栈中;
- 将CSR mepc、mcause、msubm保存到堆栈中,确保后续的抢占中断可以被正确处理;
- 如果被配置成为向量处理模式,则该中断被处理器内核响应后,处理器直接跳入该中断的向量入口(Vector Table Entry)存储的目标地址(ISR地址)
- 如果被配置成为非向量处理模式,则该中断被处理器内核响应后,处理器直接跳入所有中断共享的入口地址
执行Nuclei自定义指令“
csrrw ra,CSR_JALMNXTI,ra
”,如果没有待处理的中断,则该指令将被视为空操作;否则进入下面步骤- 直接跳入该中断的向量入口(Vector Table Entry)存储的目标地址,即该中断源的中断服务程序**(Interrupt Service Routine,ISR)**中去
- 硬件置位全局中断使能位mstatus.MIE,此时可以接受新中断,以形成中断嵌套。
- 把当前PC(csrrw ra,CSR_JALMNXTI,ra)写入返回地址ra寄存器,达到JAL(Jump and Link)的效果,即:在执行完中断handle之后,将再次执行该指令“
csrrw ra,CSR_JALMNXTI,ra
”,进而重新判断是否有还未处理的中断(pending),进而形成中断咬尾
从中断服务函数中返回后,软件来恢复中断上下文
- mstatus.MIE置0,屏蔽所有中断,确保操作的原子性
- 从堆栈中恢复中断前
CSR寄存器(msubm、mepc、mcause)
以及通用寄存器(ra/tp/t0-t6/a0-a7/sp)
的值
软件执行mret退出中断处理程序,硬件将自动更新CSR寄存器
- ①、mcause.MPIL的值恢复minstatus.MIL的值,minstatus.MIL的值会被恢复到中断前的原始值;②、使用mcause.MPIE的值恢复minstatus.MIE的值,minstatus.MIE的值会被恢复到触发中断前的值;③、mcause.MPP特权模式从中断模式退出,恢复为中断前的模式,(同样也会更新mstatus.MPIE和mcause.MPP的值,见最后NOTE)。
- 硬件将处理器Machine Sub-Mode的值恢复为msubm.PTYP域的值
- 跳转到mepc定义的PC,继续执行之前被中止的程序流。
NOTE:
mstatus.MPIE域和mstatus.MPP域的值与mcause.MPIE域和mcause.MPP域的值是镜像关系,即,在正常情况下,mstatus.MPIE域的值与mcause.MPIE域的值总是完全一样,mstatus.MPP域的值与mcause.MPP域的值总是完全一样。
2.2 向量中断与非向量中断
ECLIC的每个中断源均可以设置成向量或者非向量处理(通过寄存器clicintattr[i]的shv域
),向量处理模式和非向量处理模式二者有较大的差别
2.1章节主要是以非向量中断为例介绍的中断处理流程,这里把前面的一些和向量中断不同的点再总结下,方便和向量中断做对比
2.2.1 非向量中断处理模式
1、非向量处理模式,则该中断被处理器内核响应后,处理器会直接跳入到所有非向量中断共享的入口地址,该入口地址可以通过软件进行设置
如果配置CSR寄存器
mtvt2的最低位为0
(上电复位默认值),则所有非向量中断共享的入口地址由CSR寄存器mtvec的值(忽略最低2位的值)指定。由于mtvec寄存器的值也指定异常的入口地址,因此,意味着在这种情况下,异常和所有非向量中断共享入口地址。如果配置CSR寄存器
mtvt2的最低位为1
(芯来SDK的bootloader里配置为1了,将异常和非向量中断入口分开,不用判断是中断还是异常了,提升效率),则所有非向量中断共享的入口地址由CSR寄存器mtvt2的值(忽略最低2位的值)指定。
2、如2.1章节步骤二描述,进入所有非向量中断共享的入口地址之后,处理器会开始执行一段共有的软件代码
- 首先保存CSR寄存器
mepc、mcause、msubm
入堆栈。保存这几个CSR寄存器是为了保证后续的中断嵌套能够功能正确,因为新的中断响应会重新覆盖mepc、mcause、msubm的值,因此需要将它们先保存入堆栈 - 保存若干通用寄存器(处理器的上下文)入堆栈
- 然后执行一条特殊的指令“
csrrw ra, CSR_JALMNXTI, ra
”。如果没有中断在等待(Pending),则该指令相当于是个Nop指令不做任何操作;如果有中断在等待(Pending),执行该指令后处理器会:- 直接跳入该中断的向量入口(Vector Table Entry)存储的目标地址,即该中断源的中断服务程序(Interrupt Service Routine,ISR)中去。
- 在跳入中断服务程序的同时,硬件也会同时打开中断的全局使能,即,设置mstatus寄存器的MIE域为1。打开中断全局使能后,新的中断便可以被响应,从而达到中断嵌套的效果。
- 在跳入中断服务程序的同时,“csrrw ra, CSR_JALMNXTI, ra”指令还会达到JAL(Jump and Link)的效果,硬件同时更新Link寄存器的值为该指令的PC自身作为函数调用的返回地址。因此,从中断服务程序函数返回后会回到该“csrrw ra, CSR_JALMNXTI, ra”指令重新执行,重新判断是否还有中断在等待(Pending),从而达到中断咬尾的效果。
- 在中断服务程序的结尾处同样需要添加对应的恢复上下文出栈操作。并且在CSR寄存器mepc、mcause、msubm出堆栈之前,需要将中断全局使能再次关闭,以保证mepc、mcause、msubm恢复操作的原子性(不被新的中断所打断)。
2.2.2 向量中断的处理
1、如果被配置成为向量处理模式,则该中断被处理器内核响应后,处理器会**直接跳入该中断的向量入口(Vector Table Entry)**存储的目标地址,即该中断源的中断服务程序(Interrupt Service Routine,ISR)
2、向量处理模式具有如下特点:
向量处理模式时处理器会直接跳到中断服务程序,并没有进行上下文的保存,因此,中断响应延迟非常之短,从中断源拉高到处理器开始执行中断服务程序中的第一条指令,基本上只需要硬件进行查表和跳转的时间开销,理想情况下约6个时钟周期。
对于向量处理模式的中断服务程序函数,一定要使用特殊的
__attribute__((interrupt))
来修饰中断服务程序函数。向量处理模式时,由于在跳入中断服务程序之前,处理器并没有进行上下文的保存,因此,理论上中断服务程序函数本身不能够进行子函数的调用。
如果不小心调用了其他子函数,只要使用了
__attribute__((interrupt))
来修饰中断复位函数,编译器就会自动插入一段代码进行上下文的保存。(实际使用中不推荐调用子函数)处理器在响应向量中断后,mstatus寄存器中的MIE域将会被硬件自动更新成为0(中断被全局关闭,从而无法响应新的中断)。因此向量处理模式默认是不支持中断嵌套的,为了达到向量处理模式且又能够中断嵌套的效果,可以在中断服务例程里依次加入以下操作来实现中断嵌套效果。
- 保存CSR寄存器mepc、mcause、msubm入堆栈。
- 重新打开中断的全局使能(mstatus.MIE置1)
- 执行中断程序内容
- 关闭中断全局使能,恢复上下文出栈操作
3、对于向量处理模式的中断而言,由于在跳入中断服务程序之前,处理器并没有进行上下文的保存,因此进行“中断咬尾”的意义不大,因此,向量处理模式的中断,没有“中断咬尾”处理能力。(除非在向量中断复位函数里进行中断上下文以及返回地址的处理,但这样不如一开始就注册为非向量中断)
2.2.3 二者区别
这里就简单总结对比下:
对比项 | 向量中断(Vectored Interrupt) | 非向量中断(Non-Vectored Interrupt) |
---|---|---|
入口地址 | 每个中断有独立的入口地址(由向量表定义)。 | 所有中断共享统一入口地址。 |
开销 | 低(硬件自动跳转IRQ,无需软件处理中断上下文)。 | 高(需要从统一入口进入,需要软件处理中断上下文)。 |
中断嵌套 | 支持(需要在中断服务例程中手动处理上下文来支持) | 支持 |
中断咬尾 | 不支持 | 支持 |
IRQ定义 | void __INTERRUPT isr_uart() { ... } |
void isr_uart() { ... } |
应用场景 | 需要快速响应的高优先级外设 | 低优先级或非实时外设 |
NOTE:
非向量模式的中断处理函数,中断函数前面一定不要加__INTERRUPT
这个关键字描述,否则编译器会用mret 指令,导致中断提前返回了中断前的代码,而不是返回到common_entry 这里
而向量中断需要使用__attribute__((interrupt))
来修饰,让编译器来处理该服务例程
2.3 中断抢占和中断咬尾
实际上通过前面的前面的介绍,对于中断抢占和中断咬尾,基本上已经能了解个七七八八了,这里再简单总结下,并举例说明下。
2.3.1 基本概念
1、中断嵌套
处理器内核正在处理某个中断的过程中,可能有一个级别更高的新中断请求到来,处理器可以中止当前的中断服务程序,转而开始响应新的中断,并执行其“中断服务程序”,如此便形成了中断嵌套(即前一个中断还没响应完,又开始响应新的中断),并且嵌套的层次可以有很多层。
2、中断咬尾
处理器内核正在处理某个中断的过程中,可能有新中断请求到来,但是**“新中断的级别”低于或者等于“当前正在处理的中断级别”**,因此,新中断不能够打断当前正在处理的中断(因此不会形成嵌套)
2.3.2 中断嵌套处理流程
1、非向量中断
假设中断源30、31、32这三个中断源先后到来,且“中断源32的级别” > “中断源31的级别”> “中断源30的级别”,那么后来的中断便会打断之前正在处理的中断形成中断嵌套
2、向量中断
假设中断源30、31、32这三个中断源先后到来,且“中断源32的级别” > “中断源31的级别”> “中断源30的级别”,那么后来的中断便会打断之前正在处理的中断形成中断嵌套。
向量中断不同点是,1、中断入口不一致(中断的服务函数地址),2、中断服务函数里软件实现中断上下文处理
2.3.3 中断咬尾处理流程
假设中断源30、29、28这三个中断源先后到来,且“中断源30的级别” >= “中断源29的级别”>= “中断源28的级别”,那么后来的中断不会打断之前正在处理的中断(不会形成中断嵌套),但是会被置于等待(Pending)状态。当中断源30完成处理后,将会直接开始中断源29的中断处理,省掉中间的“恢复上下文”和“保存上下文”过程。
2.3.4 总结
非向量中断总是能够支持中断嵌套和中断咬尾的;
向量中断则是可以通过在中断服务程序中通过软件处理来支持中断嵌套,但不支持中断咬尾。
另外,并未给出向量中断和非向量中断相互嵌套的例子,不过大家可以自行分析下。
3 实际应用
这里结合芯来开源SDK中demo_eclic代码及qemu(RISC-V汇编学习(四)—— RISCV QEMU平台搭建(基于芯来平台))来演示下,向量中断与非向量中断以及中断的嵌套。
中断注册、入口初始化及共享中断入口流程代码,自行参考芯来开源sdk中的驱动及BootLoader学习(参考下面源码链接)。
3.1、源码分析
芯来提供了一个软件中断和timer中断的示例,源码点击链接即可查看:
Gitee: https://gitee.com/Nuclei-Software/nuclei-sdk/blob/master/application/baremetal/demo_eclic/demo_eclic.c
Github: https://github.com/Nuclei-Software/nuclei-sdk/blob/master/application/baremetal/demo_eclic/demo_eclic.c.
源码执行结果,展示部分打印:
-------------------
[IN TIMER INTERRUPT]timer interrupt hit 0 times
[IN TIMER INTERRUPT]trigger software interrupt
[IN TIMER INTERRUPT]software interrupt will run when timer interrupt finished
[IN TIMER INTERRUPT]timer interrupt end
[IN SOFTWARE INTERRUPT]software interrupt hit 0 times
[IN SOFTWARE INTERRUPT]software interrupt end
-------------------
[IN TIMER INTERRUPT]timer interrupt hit 1 times
[IN TIMER INTERRUPT]trigger software interrupt
[IN TIMER INTERRUPT]software interrupt will run when timer interrupt finished
[IN TIMER INTERRUPT]timer interrupt end
[IN SOFTWARE INTERRUPT]software interrupt hit 1 times
[IN SOFTWARE INTERRUPT]software interrupt end
分析
- 程序中分别注册了:
- 1、timer中断:高优先级、非向量中断
- 2、软件中断:低优先级、向量中断
timer_intlevel = HIGHER_INTLEVEL;
swirq_intlevel = LOWER_INTLEVEL;
// initialize timer
setup_timer();
// initialize software interrupt as vector interrupt
returnCode = ECLIC_Register_IRQ(SysTimerSW_IRQn, ECLIC_VECTOR_INTERRUPT,
ECLIC_LEVEL_TRIGGER, swirq_intlevel, 0, eclic_msip_handler);
// inital timer interrupt as non-vector interrupt
returnCode = ECLIC_Register_IRQ(SysTimer_IRQn, ECLIC_NON_VECTOR_INTERRUPT,
ECLIC_LEVEL_TRIGGER, timer_intlevel, 0, eclic_mtip_handler);
- 实现:
通过设置core内部的MTIMERCMP寄存器,来触发timer中断,之后在timer中断服务例程里配置MSIP来触发软件中断
根据打印可以发现程序这里实现了中断咬尾效果,即:
1、低优先级中断无法打断高优先级中断
2、每次都会先触发非向量中断(timer中断),然后再触发向量中断(soft中断)
3、在高优先级的非向量中断退出后再次执行"csrrw ra, CSR_JALMNXTI, ra
",此时还有低优先级的中断在pending中,就会继续执行低优先级的软件中断,从而形成中断咬尾
另外,这里把软件中断配置成非向量中断(记得去掉IRQ的__INTERRUPT
修饰)效果也是一样的,
但是把timer中断换成向量中断(不支持中断咬尾)是不行的。(第一个触发的中断是非向量中断可以实现中断咬尾)
3.2 中断嵌套实现
我们在芯来提供的demo_eclic基础上来做些修改:
只需要打开#define SWIRQ_INTLEVEL_HIGHER 1
,就会从新定义优先级: soft中断>timer中断
执行代码,部分打印如下:
-------------------
[IN TIMER INTERRUPT]timer interrupt hit 0 times
[IN TIMER INTERRUPT]trigger software interrupt
[IN TIMER INTERRUPT]software interrupt will run during timer interrupt
[IN SOFTWARE INTERRUPT]software interrupt hit 0 times
[IN SOFTWARE INTERRUPT]software interrupt end
[IN TIMER INTERRUPT]timer interrupt end
-------------------
[IN TIMER INTERRUPT]timer interrupt hit 1 times
[IN TIMER INTERRUPT]trigger software interrupt
[IN TIMER INTERRUPT]software interrupt will run during timer interrupt
[IN SOFTWARE INTERRUPT]software interrupt hit 1 times
[IN SOFTWARE INTERRUPT]software interrupt end
[IN TIMER INTERRUPT]timer interrupt end
可以看到在低优先级timer中断执行时,将被高优先级soft中断打断,从而形成中断嵌套。
这里把软件中断配置成非向量中断(记得去掉IRQ的__INTERRUPT
修饰)效果也是一样的,
后者把timer中断换成向量中断也是可行的(1、IRQ加上的__INTERRUPT
修饰 2、IRQ里开始加上SAVE_IRQ_CSR_CONTEXT();
,结束加上RESTORE_IRQ_CSR_CONTEXT();
)。
以上就是芯来eclic中断相关的内容,如有纰漏,还请各位看官大佬予以指出
参考:
芯来科技N级别指令集架构