网站Logo 网友马大帅的博客

riscv freertos 中断处理分析

ughostx
13
2026-03-25

在riscv中异常分为exception和中断。其中exception一般就是指同步异常,指的是core内部触发的各种异常信号,会立即被core响应。中断一般就是指的是异步中断,信号来源基本为外部硬件。通常中断信号会先送给中断控制器(CLIC),再由clic发送给core内部。

本篇主要分析在freertos内核中的riscv框架是如何处理中断的。

中断模式

在RISC-V架构中,中断处理主要有两种模式:直接中断模式向量中断模式。这两种模式通过CSR寄存器 mtvec(机器模式陷阱向量基地址寄存器)进行配置。

模式名称

配置方式 (mtvec.MODE)

工作原理

优点

缺点

适用场景

直接中断模式

设置为 0

所有中断和异常都跳转到同一个固定入口地址mtvec.BASE)。软件需读取mcause等CSR寄存器来判断中断源。

实现简单,代码空间占用少,灵活性高。

响应速度较慢,需要额外的指令进行分支判断。

通用场景、对中断延迟不敏感的系统、教学或简单内核。

向量中断模式

设置为 1

根据中断原因(mcause值)跳转到不同的入口地址。入口地址 = mtvec.BASE + 中断号 * 偏移量(通常为4或8字节)。

响应速度快,硬件自动完成跳转,减少了软件判断开销。

需要硬件支持,占用更多的代码存储空间。

对实时性要求高的嵌入式系统、高性能应用。

中断委托

通过 mideleg (中断委托) 和 medeleg (异常委托) 寄存器配置。

将特定的中断或异常从机器模式(M) 委托到监管者模式(S)用户模式(U) 处理。

提升系统安全性和灵活性,使低特权级操作系统能直接处理部分中断。

增加了特权级切换和管理的复杂性。

运行类Unix操作系统、需要实现完整特权级隔离的系统。

补充说明:

  1. mtvec寄存器:机器模式陷阱向量基地址寄存器,是配置中断模式的核心。

  2. 中断控制器:实际的优先级和中断使能/屏蔽通常由平台级中断控制器(如PLIC)或核心本地中断器(CLINT)管理,架构本身不定义硬件优先级。

  3. 默认模式直接中断模式是RISC-V的默认和必需实现模式,而向量模式是可选的扩展。

处理流程

入口设置

把 mtvec 设成 vectored mode,并传入向量表地址,向量表一般定义在 vector.S中,里面按照中断号mapping了中断入口地址。每4字节一个入口,相当于一条普通指令长度,故一般软件都会在这里放j指令跳转到真正的中断处理程序里面。

中断触发

  • 硬件产生 machine external interrupt

  • CPU 自动写 mcause/mepc/mstatus,清除 MIE,跳到 mtvec + 4 * cause入口地址,在freertos中一般会用内核提供好的中断处理函数freertos_risc_v_interrupt_handler

freertos通用内核中断处理函数freertos_risc_v_interrupt_handler

/*-----------------------------------------------------------*/

.section .text.freertos_risc_v_interrupt_handler
freertos_risc_v_interrupt_handler:
    portcontextSAVE_INTERRUPT_CONTEXT
    call freertos_risc_v_application_interrupt_handler
    portcontextRESTORE_CONTEXT

这里可以看出就三步:(1)保存上下文(2)处理中断(3)恢复上下文

上下文保存

上下文保存在portContext.h的 portcontextSAVE_CONTEXT_INTERNAL 宏

它做的事是:

  • 在当前任务栈上为上下文开辟 portCONTEXT_SIZE

  • 保存通用寄存器 x1, x5-x31

  • 保存 xCriticalNesting

  • 按需保存 FPU 上下文

    • 只在 mstatus.FS == dirty 时保存

  • 按需保存 VPU 上下文

  • 保存 mstatus

  • 保存芯片扩展寄存器

  • 最后把当前 sp 写回 pxCurrentTCB->pxTopOfStack

这里有几个关键点:

  • sp 本身不作为普通寄存器存一份,而是“当前上下文帧的基址”,最后直接写入 TCB

  • gp/tp 没有保存,这是 FreeRTOS 这份 RISC-V port 的设计假设

  • 对中断和异常,mepc 的处理不同

对“异步中断”路径,portcontextSAVE_INTERRUPT_CONTEXT 在保存完通用现场后会:

  • mcausea0

  • mepca1

  • 把“未修改的 mepc”存到任务栈

  • 然后把 sp 切到 xISRStackTop

这意味着:

  • 被打断任务的完整现场在“任务自己的栈”里

  • C 级 ISR 运行在独立 ISR 栈上

  • 后面是否切换任务,只取决于 pxCurrentTCB 最终指向谁

   .macro portcontextSAVE_CONTEXT_INTERNAL
addi sp, sp, -portCONTEXT_SIZE
store_x x1,  2  * portWORD_SIZE( sp )
store_x x5,  3  * portWORD_SIZE( sp )
store_x x6,  4  * portWORD_SIZE( sp )
store_x x7,  5  * portWORD_SIZE( sp )
store_x x8,  6  * portWORD_SIZE( sp )
store_x x9,  7  * portWORD_SIZE( sp )
store_x x10, 8  * portWORD_SIZE( sp )
store_x x11, 9  * portWORD_SIZE( sp )
store_x x12, 10 * portWORD_SIZE( sp )
store_x x13, 11 * portWORD_SIZE( sp )
store_x x14, 12 * portWORD_SIZE( sp )
store_x x15, 13 * portWORD_SIZE( sp )
#ifndef __riscv_32e
    store_x x16, 14 * portWORD_SIZE( sp )
    store_x x17, 15 * portWORD_SIZE( sp )
    store_x x18, 16 * portWORD_SIZE( sp )
    store_x x19, 17 * portWORD_SIZE( sp )
    store_x x20, 18 * portWORD_SIZE( sp )
    store_x x21, 19 * portWORD_SIZE( sp )
    store_x x22, 20 * portWORD_SIZE( sp )
    store_x x23, 21 * portWORD_SIZE( sp )
    store_x x24, 22 * portWORD_SIZE( sp )
    store_x x25, 23 * portWORD_SIZE( sp )
    store_x x26, 24 * portWORD_SIZE( sp )
    store_x x27, 25 * portWORD_SIZE( sp )
    store_x x28, 26 * portWORD_SIZE( sp )
    store_x x29, 27 * portWORD_SIZE( sp )
    store_x x30, 28 * portWORD_SIZE( sp )
    store_x x31, 29 * portWORD_SIZE( sp )
#endif /* ifndef __riscv_32e */

load_x t0, xCriticalNesting                                   /* Load the value of xCriticalNesting into t0. */
store_x t0, portCRITICAL_NESTING_OFFSET * portWORD_SIZE( sp ) /* Store the critical nesting value to the stack. */

#if( configENABLE_FPU == 1 )
    csrr t0, mstatus
    srl t1, t0, MSTATUS_FS_OFFSET
    andi t1, t1, 3
    addi t2, x0, 3
    bne t1, t2, 1f /* If FPU status is not dirty, do not save FPU registers. */

    portcontexSAVE_FPU_CONTEXT
1:
#endif

#if( configENABLE_VPU == 1 )
    csrr t0, mstatus
    srl t1, t0, MSTATUS_VS_OFFSET
    andi t1, t1, 3
    addi t2, x0, 3
    bne t1, t2, 2f /* If VPU status is not dirty, do not save VPU registers. */

    portcontexSAVE_VPU_CONTEXT
2:
#endif

csrr t0, mstatus
store_x t0, 1 * portWORD_SIZE( sp )

portasmSAVE_ADDITIONAL_REGISTERS /* Defined in freertos_risc_v_chip_specific_extensions.h to save any registers unique to the RISC-V implementation. */

#if( configENABLE_FPU == 1 )
    /* Mark the FPU as clean, if it was dirty and we saved FPU registers. */
    srl t1, t0, MSTATUS_FS_OFFSET
    andi t1, t1, 3
    addi t2, x0, 3
    bne t1, t2, 3f

    li t1, ~MSTATUS_FS_MASK
    and t0, t0, t1
    li t1, MSTATUS_FS_CLEAN
    or t0, t0, t1
    csrw mstatus, t0
3:
#endif

#if( configENABLE_VPU == 1 )
    /* Mark the VPU as clean, if it was dirty and we saved VPU registers. */
    srl t1, t0, MSTATUS_VS_OFFSET
    andi t1, t1, 3
    addi t2, x0, 3
    bne t1, t2, 4f

    li t1, ~MSTATUS_VS_MASK
    and t0, t0, t1
    li t1, MSTATUS_VS_CLEAN
    or t0, t0, t1
    csrw mstatus, t0
4:
#endif

load_x t0, pxCurrentTCB          /* Load pxCurrentTCB. */
store_x sp, 0 ( t0 )             /* Write sp to first TCB member. */

   .endm
/*-----------------------------------------------------------*/
   .macro portcontextSAVE_INTERRUPT_CONTEXT
portcontextSAVE_CONTEXT_INTERNAL
csrr a0, mcause
csrr a1, mepc
store_x a1, 0 ( sp )    /* Asynchronous interrupt so save unmodified exception return address. */
load_x sp, xISRStackTop /* Switch to ISR stack. */
   .endm
/*-----------------------------------------------------------*/

上下文恢复

上下文保存在portContext.h的 portcontextRESTORE_CONTEXT 宏

它的顺序是:

  • pxCurrentTCB 取出要运行任务的栈顶

  • 从栈 [0] 恢复 mepc

  • 恢复芯片扩展寄存器

  • 恢复 mstatus

  • 如果 FPU/VPU 状态是 dirty,就恢复其上下文

  • 恢复 xCriticalNesting

  • 恢复 x1, x5-x31

  • addi sp, sp, portCONTEXT_SIZE

  • mret

最核心的一句是:恢复时重新从 pxCurrentTCBsp
所以只要中断处理期间调用了 vTaskSwitchContext() 并修改了 pxCurrentTCBmret 回去的就已经不是原任务,而是新任务。

  .macro portcontextRESTORE_CONTEXT
load_x t1, pxCurrentTCB /* Load pxCurrentTCB. */
load_x sp, 0 ( t1 )     /* Read sp from first TCB member. */

/* Load mepc with the address of the instruction in the task to run next. */
load_x t0, 0 ( sp )
csrw mepc, t0

/* Defined in freertos_risc_v_chip_specific_extensions.h to restore any registers unique to the RISC-V implementation. */
portasmRESTORE_ADDITIONAL_REGISTERS

/* Restore mstatus register. It is important to use t3 (and not t0) here as t3
 * is not clobbered by portcontextRESTORE_VPU_CONTEXT and
 * portcontextRESTORE_FPU_CONTEXT. */
load_x t3, 1 * portWORD_SIZE( sp )
csrw mstatus, t3

#if( configENABLE_VPU == 1 )
    srl t1, t3, MSTATUS_VS_OFFSET
    andi t1, t1, 3
    addi t2, x0, 3
    bne t1, t2, 5f /* If VPU status is not dirty, do not restore VPU registers. */

    portcontextRESTORE_VPU_CONTEXT
5:
#endif /* ifdef portasmSTORE_VPU_CONTEXT */

#if( configENABLE_FPU == 1 )
    srl t1, t3, MSTATUS_FS_OFFSET
    andi t1, t1, 3
    addi t2, x0, 3
    bne t1, t2, 6f /* If FPU status is not dirty, do not restore FPU registers. */

    portcontextRESTORE_FPU_CONTEXT
6:
#endif /* ifdef portasmSTORE_FPU_CONTEXT */

load_x t0, portCRITICAL_NESTING_OFFSET * portWORD_SIZE( sp ) /* Obtain xCriticalNesting value for this task from task's stack. */
load_x t1, pxCriticalNesting                                 /* Load the address of xCriticalNesting into t1. */
store_x t0, 0 ( t1 )                                         /* Restore the critical nesting value for this task. */

load_x x1,  2  * portWORD_SIZE( sp )
load_x x5,  3  * portWORD_SIZE( sp )
load_x x6,  4  * portWORD_SIZE( sp )
load_x x7,  5  * portWORD_SIZE( sp )
load_x x8,  6  * portWORD_SIZE( sp )
load_x x9,  7  * portWORD_SIZE( sp )
load_x x10, 8  * portWORD_SIZE( sp )
load_x x11, 9  * portWORD_SIZE( sp )
load_x x12, 10 * portWORD_SIZE( sp )
load_x x13, 11 * portWORD_SIZE( sp )
load_x x14, 12 * portWORD_SIZE( sp )
load_x x15, 13 * portWORD_SIZE( sp )
#ifndef __riscv_32e
    load_x x16, 14 * portWORD_SIZE( sp )
    load_x x17, 15 * portWORD_SIZE( sp )
    load_x x18, 16 * portWORD_SIZE( sp )
    load_x x19, 17 * portWORD_SIZE( sp )
    load_x x20, 18 * portWORD_SIZE( sp )
    load_x x21, 19 * portWORD_SIZE( sp )
    load_x x22, 20 * portWORD_SIZE( sp )
    load_x x23, 21 * portWORD_SIZE( sp )
    load_x x24, 22 * portWORD_SIZE( sp )
    load_x x25, 23 * portWORD_SIZE( sp )
    load_x x26, 24 * portWORD_SIZE( sp )
    load_x x27, 25 * portWORD_SIZE( sp )
    load_x x28, 26 * portWORD_SIZE( sp )
    load_x x29, 27 * portWORD_SIZE( sp )
    load_x x30, 28 * portWORD_SIZE( sp )
    load_x x31, 29 * portWORD_SIZE( sp )
#endif /* ifndef __riscv_32e */
addi sp, sp, portCONTEXT_SIZE

mret
   .endm
/*-----------------------------------------------------------*/

动物装饰