目录
[0 相关内容](#0 相关内容)
[1 主栈、中断栈、RTOS任务栈 与 sp、mscratch 寄存器](#1 主栈、中断栈、RTOS任务栈 与 sp、mscratch 寄存器)
[1.1 三个栈的概念与物理存在](#1.1 三个栈的概念与物理存在)
[1.1.1 主栈(Main Stack / Boot Stack)](#1.1.1 主栈(Main Stack / Boot Stack))
[1.1.2 中断栈(Interrupt Stack)](#1.1.2 中断栈(Interrupt Stack))
[1.1.3 RTOS 任务栈(Task Stack)](#1.1.3 RTOS 任务栈(Task Stack))
[1.2 sp 寄存器的角色与 mscratch 的可选性](#1.2 sp 寄存器的角色与 mscratch 的可选性)
[1.2.1 sp(栈指针)](#1.2.1 sp(栈指针))
[1.2.2 mscratch(机器模式暂存寄存器)](#1.2.2 mscratch(机器模式暂存寄存器))
[1.3 简析FreeRTOS 中中断栈的两种配置方式(源码解析在下文)](#1.3 简析FreeRTOS 中中断栈的两种配置方式(源码解析在下文))
[模式 A:独立中断栈(静态数组)](#模式 A:独立中断栈(静态数组))
[模式 B:复用主栈(链接脚本)](#模式 B:复用主栈(链接脚本))
[1.4 简析FreeRTOS 异常入口中的中断栈切换流程(源码解析在下文)](#1.4 简析FreeRTOS 异常入口中的中断栈切换流程(源码解析在下文))
[1.5 为什么 FreeRTOS 没有使用 mscratch?](#1.5 为什么 FreeRTOS 没有使用 mscratch?)
[1.6 中断栈的核心意义总结](#1.6 中断栈的核心意义总结)
[1.7 RISC-V 的设计哲学体现](#1.7 RISC-V 的设计哲学体现)
[2 FreeRTOS在RISC-V架构下的中断与异常处理流程](#2 FreeRTOS在RISC-V架构下的中断与异常处理流程)
[3 RISC-V架构下的 FreeRTOS异常进入 & 上下文存档与恢复 源码解析](#3 RISC-V架构下的 FreeRTOS异常进入 & 上下文存档与恢复 源码解析)
[3.1 FreeRTOS定义中断栈源码](#3.1 FreeRTOS定义中断栈源码)
[3.1.1 源码](#3.1.1 源码)
[3.1.2 再谈中断栈](#3.1.2 再谈中断栈)
[1. 独立中断栈(静态数组方式)](#1. 独立中断栈(静态数组方式))
[2. 复用主栈(链接脚本方式)](#2. 复用主栈(链接脚本方式))
[3.2 FreeRTOS异常(trap)入口源码](#3.2 FreeRTOS异常(trap)入口源码)
[3.2.1 函数调用关系](#3.2.1 函数调用关系)
[3.2.2 源码](#3.2.2 源码)
[3.3 FreeRTOS任务上下文存档源码](#3.3 FreeRTOS任务上下文存档源码)
[3.4 异常例外处理](#3.4 异常例外处理)
[3.5 中断处理](#3.5 中断处理)
[3.6 FreeRTOS任务上下文恢复](#3.6 FreeRTOS任务上下文恢复)
0 相关内容
【外部链接】RISC-V 端口 |FreeRTOS/FreeRTOS-Kernel |DeepWiki
【外部链接】如何理解RISC-V中的hart? - 知乎
【外部链接】链接脚本(.ld文件)语法详解:链接脚本(.ld文件)语法详解-CSDN博客
【外部链接】特殊寄存器与异常详解:RISC-V 学习篇之特权架构下的中断异常处理_riscv mtvec-CSDN博客
【外部链接】freertos任务调度机制深度分析(以RISC-V架构为例)_risc-v freertos-CSDN博客
1 主栈、中断栈、RTOS任务栈 与 sp、mscratch 寄存器
1.1 三个栈的概念与物理存在
在 RISC-V + RTOS 系统中,通常涉及三种逻辑上的栈,但它们的物理内存块数量可能少于三个:
1.1.1 主栈(Main Stack / Boot Stack)
-
定义 :系统上电后,C 运行时环境和
main()函数使用的栈。它在链接脚本(.ld文件)中定义,通常是一个名为.stack的段,有固定大小和起始地址。 -
物理存在 :一块连续的内存区域,位于 RAM 中。
-
角色演变:
-
启动阶段 :作为主栈,供初始化代码和
main()使用。 -
调度器启动后 :如果未定义独立中断栈,这块区域会被复用为中断栈;如果定义了独立中断栈,这块区域可能被闲置或另作他用(如空闲任务栈)。
-
1.1.2 中断栈(Interrupt Stack)
-
定义:专门用于执行中断服务程序(ISR)的栈,存在的意义是防止 ISR 使用任务栈导致溢出或数据破坏。
-
物理存在:有两种可能性:
-
独立中断栈 :通过
configISR_STACK_SIZE_WORDS宏定义,静态分配一个全局数组xISRStack[],位于.bss段或数据段。 -
复用主栈 :不定义上述宏,通过链接脚本将主栈的栈顶地址赋给
__freertos_irq_stack_top,FreeRTOS 直接使用主栈作为中断栈。
-
-
关键点 :中断栈的物理内存块要么是独立的一块 ,要么就是主栈本身,不存在第三个独立的物理栈。
1.1.3 RTOS 任务栈(Task Stack)
-
定义:每个 RTOS 任务私有的栈,用于保存任务局部变量、函数调用上下文等。
-
物理存在 :每个任务对应一块独立的内存区域,可以是在创建任务时从堆中动态分配的,也可以是静态全局数组。它们位于堆区或
.bss段,与主栈/中断栈是完全独立的内存块。 -
数量:N 个任务就有 N 块任务栈。
小结 :物理上,系统至少有 1 + N 块栈内存(主栈/中断栈 + N 个任务栈)。如果独立中断栈被启用,则是 2 + N 块。
注:在Cortex m3/m4架构中,存在MSP与PSP,启动时使用MSP,使用主栈,任务线程中使用PSP,使用任务栈,进入中断时,使用MSP,主栈,也就是说,此时使用的中断栈就是主栈。
1.2 sp 寄存器的角色与 mscratch 的可选性
1.2.1 sp(栈指针)
-
是 RISC-V 的通用寄存器
x2,硬件上只有一个(与 Cortex M3/M4 有 MSP + PSP 两个 sp 寄存器不同)。 -
它总是指向当前正在使用的栈的顶部。在系统运行的不同阶段,
sp会指向不同的栈区域:-
启动时 → 主栈
-
任务运行时 → 当前任务的任务栈
-
中断处理时(在切换后) → 中断栈
-
1.2.2 mscratch(机器模式暂存寄存器)
-
是一个可选的 CSR 寄存器,并非所有 RISC-V 实现都必须包含。
-
常见的用法是在中断入口处通过
csrrw sp, mscratch, sp指令原子性交换sp和mscratch的值,从而实现快速栈切换。但FreeRTOS 的官方移植并没有采用这种方案,而是采用显式的全局变量加载。
mscratch 是 RISC-V 架构中的一个控制和状态寄存器(Control and Status Register),用于保存机器模式下的临时数据或上下文相关的信息。它的作用是提供一个通用的、临时的存储位置,供软件使用。
具体而言,mscratch 寄存器通常用于以下情况:
1.上下文切换:当处理器从一个上下文切换到另一个上下文时,可以将当前的 mscratch 寄存器的值保存到保存的上下文中。在切换到新的上下文后,可以将先前保存的 mscratch 寄存器的值恢复,以便继续使用其中的数据。
2.异步事件处理:当处理器在处理中断或异常时,可能需要保存一些临时数据,以便在恢复正常执行后继续使用。mscratch 寄存器提供了一个方便的位置来存储这些临时数据,以避免污染其他重要的寄存器。
3.调试和跟踪:在调试和跟踪应用程序时,mscratch 寄存器可以用于存储调试器或跟踪工具的临时数据,例如断点信息、调试状态等。
需要注意的是,mscratch 寄存器的使用是由软件决定的,它没有特定的预定义用途。软件可以根据需要将 mscratch 寄存器用于临时存储和处理数据。然而,由于 mscratch 寄存器的值可能会被上下文切换或其他操作修改,因此软件在使用 mscratch 寄存器时应注意保存和恢复其中的数据。
总结:mscratch 寄存器是 RISC-V 架构中的一个控制和状态寄存器,用于保存机器模式下的临时数据或上下文相关的信息。它可以用于上下文切换、异步事件处理、调试和跟踪等情况,提供一个通用的临时存储位置供软件使用。
1.3 简析FreeRTOS 中中断栈的两种配置方式(源码解析在下文)
从 FreeRTOS 源码(特别是 port.c 和 portasm.S)可以看出,中断栈的定义有两种模式,由宏 configISR_STACK_SIZE_WORDS 控制:
模式 A:独立中断栈(静态数组)
#ifdef configISR_STACK_SIZE_WORDS
static __attribute__((aligned(16))) StackType_t xISRStack[ configISR_STACK_SIZE_WORDS ] = { 0 };
const StackType_t xISRStackTop = ( StackType_t ) &( xISRStack[ configISR_STACK_SIZE_WORDS & ~portBYTE_ALIGNMENT_MASK ] );
#define portISR_STACK_FILL_BYTE 0xee
#endif
-
此时中断栈是一块独立的内存,与主栈无关。
-
xISRStackTop指向这块独立栈的栈顶。
模式 B:复用主栈(链接脚本)
#else
extern const uint32_t __freertos_irq_stack_top[];
const StackType_t xISRStackTop = ( StackType_t ) __freertos_irq_stack_top;
#endif
-
此时中断栈就是主栈本身,
xISRStackTop等于主栈的栈顶地址(由链接脚本提供)。 -
这种方式节省内存,符合 RISC-V 简洁的设计理念。
无论哪种模式 ,最终都会得到一个全局常量 xISRStackTop,它保存了中断栈的栈顶地址。
1.4 简析FreeRTOS 异常入口中的中断栈切换流程(源码解析在下文)
以下文中的 freertos_risc_v_trap_handler 代码为例,我们逐段分析中断栈的切换时机和方式:
freertos_risc_v_trap_handler:
portcontextSAVE_CONTEXT_INTERNAL // (1) 在任务栈上保存当前任务的全部寄存器
csrr a0, mcause
csrr a1, mepc
bge a0, x0, synchronous_exception
asynchronous_interrupt:
store_x a1, 0( sp ) // (2) 将 mepc 暂存到任务栈(当前 sp 指向任务栈)
load_x sp, xISRStackTop // (3) 关键:将 sp 切换到中断栈顶!
j handle_interrupt
-
步骤 (1) :
portcontextSAVE_CONTEXT_INTERNAL会在当前任务栈上分配空间,保存所有通用寄存器、mstatus、临界区嵌套计数等。此时sp仍然指向任务栈(但已下移指向保存的上下文底部)。 -
步骤 (2) :将
mepc存入当前任务栈(只是临时存放,后续不会再用)。 -
步骤 (3) :
load_x sp, xISRStackTop将sp设置为中断栈顶。从此,后续的代码(包括handle_interrupt及其调用的 C 函数)全部运行在中断栈上。
同步异常分支也有同样的切换过程。
处理完毕后,跳转到 processed_source:
processed_source:
portcontextRESTORE_CONTEXT // (4) 恢复任务上下文
注意 :portcontextRESTORE_CONTEXT 宏并不依赖当前的 sp 值!它的第一件事就是从当前任务的 TCB 中加载保存的栈指针到 sp(这个指针就是在步骤 (1) 之后存入 TCB 的)。因此,尽管此时 sp 指向中断栈,恢复宏一开始就会将其覆盖为正确的任务栈指针,然后才从栈上弹出寄存器。这个设计巧妙避免了在恢复前需要手动切换回任务栈的步骤。
1.5 为什么 FreeRTOS 没有使用 mscratch?
我们之前讨论过一种常见的 RISC-V 中断栈切换方式:利用 mscratch 寄存器和 csrrw sp, mscratch, sp 指令,在中断入口处自动交换栈指针。但 FreeRTOS 的移植选择了显式加载全局变量的方案,原因可能包括:
-
可移植性 :并非所有 RISC-V 实现都包含
mscratch寄存器(特别是低功耗微控制器),使用全局变量不依赖特定 CSR,可在任何 RISC-V 核上运行。 -
代码清晰:显式切换逻辑简单,容易理解和调试。
-
灵活性 :允许用户在两种中断栈配置间自由选择(独立数组或复用主栈),而
mscratch方法通常假设中断栈地址已预先写入mscratch,且一般只适用于复用主栈的场景。
结论 :FreeRTOS 采用了一种更通用、更直观的方式来实现中断栈隔离,虽然没有利用 mscratch 的硬件加速,但保证了代码的可移植性和配置灵活性。
1.6 中断栈的核心意义总结
综合以上分析,我们可以对中断栈得出以下结论:
-
中断栈是一个逻辑概念,其物理内存可以是独立的,也可以是复用的主栈。两种方式各有利弊,FreeRTOS 通过配置宏让用户按需选择。
-
中断栈的切换发生在保存任务上下文之后,这样 ISR 可以安全地使用独立的栈空间,不会污染任务栈。
-
切换机制不依赖于特定硬件,FreeRTOS 使用显式的全局变量加载,简单可靠。
-
恢复上下文时无需手动切回中断栈 ,因为恢复宏直接从 TCB 加载任务栈指针,自动覆盖
sp。 -
mscratch寄存器不是必需的,虽然某些实现用它加速栈切换,但 FreeRTOS 选择了更通用的方案。
1.7 RISC-V 的设计哲学体现
RISC-V 提供基础的硬件机制(如 sp 寄存器、异常自动保存 mepc/mcause、mret 指令),但将栈管理的具体策略留给软件自由实现。FreeRTOS 的这个移植正是这种哲学的体现:它没有强求使用 mscratch,而是用普通 load 指令完成栈切换,同时通过配置选项让用户决定中断栈的物理来源。这种灵活性使得 RISC-V 既能适应极简的裸机系统,也能支持功能丰富的 RTOS。
2 FreeRTOS在RISC-V架构下的中断与异常处理流程
RISC-V 端口实现了一个统一的陷阱处理程序,根据 mcause寄存器值向相应的处理程序发送调度。

3 RISC-V架构下的 FreeRTOS异常进入 & 上下文存档与恢复 源码解析
【外部链接】freertos任务调度机制深度分析(以RISC-V架构为例)_risc-v freertos-CSDN博客
【外部链接】freertos任务切换的现场保存、恢复(任务栈空间)深度分析(以RISC-V架构为例)_模拟freertos 切换任务返回现场-CSDN博客
【外部链接】freeRTOS异常处理函数分析(以RISC-V架构进行分析)_freertos 异常管理-CSDN博客
3.1 FreeRTOS定义中断栈源码
3.1.1 源码
/* The stack used by interrupt service routines. Set configISR_STACK_SIZE_WORDS
* to use a statically allocated array as the interrupt stack. Alternative leave
* configISR_STACK_SIZE_WORDS undefined and update the linker script so that a
* linker variable names __freertos_irq_stack_top has the same value as the top
* of the stack used by main. Using the linker script method will repurpose the
* stack that was used by main before the scheduler was started for use as the
* interrupt stack after the scheduler has started. */
/* 中断服务程序使用的栈。
* 如果定义了 configISR_STACK_SIZE_WORDS,则使用一个静态分配的数组作为中断栈。
* 另一种方式是不定义 configISR_STACK_SIZE_WORDS,并修改链接脚本,
* 使链接器变量 __freertos_irq_stack_top 的值与 main 函数使用的栈顶相同。
* 采用链接脚本的方法将复用调度器启动前 main 函数使用的栈,作为调度器启动后的中断栈。
*/
#ifdef configISR_STACK_SIZE_WORDS
/* 静态分配的中断栈数组,按16字节对齐 */
static __attribute__( ( aligned( 16 ) ) ) StackType_t xISRStack[ configISR_STACK_SIZE_WORDS ] = { 0 };
/* 计算中断栈的栈顶指针(考虑对齐掩码,确保地址对齐) */
const StackType_t xISRStackTop = ( StackType_t ) &( xISRStack[ configISR_STACK_SIZE_WORDS & ~portBYTE_ALIGNMENT_MASK ] );
/* 不要使用 0xa5 作为栈填充字节,因为内核用这个值填充任务栈,
* 因为这样会在 ISR 栈中也会合法地出现很多 0xa5。 */
/* 使用 0xee 作为 ISR 栈的填充字节 */
#define portISR_STACK_FILL_BYTE 0xee
#else
/* 如果不定义 configISR_STACK_SIZE_WORDS,则引用链接脚本中定义的符号,
* 该符号表示主栈的栈顶地址。 */
extern const uint32_t __freertos_irq_stack_top[];
/* 将链接脚本中的栈顶地址赋给 xISRStackTop,即复用主栈作为中断栈 */
const StackType_t xISRStackTop = ( StackType_t ) __freertos_irq_stack_top;
#endif
3.1.2 再谈中断栈
这段代码精准地总结了中断栈本质问题,它清晰地展示了 FreeRTOS 为 RISC-V 提供的两种中断栈配置策略,这两种策略正好对应了我们之前讨论的两种不同视角:
1. 独立中断栈(静态数组方式)
-
实现 :当定义了
configISR_STACK_SIZE_WORDS,FreeRTOS 会静态分配一个全局数组xISRStack作为独立的中断栈。 -
特点:
-
中断栈与主栈(即启动时使用的栈)是两块完全不同的物理内存区域。
-
中断栈的大小由用户通过宏直接指定,位置由编译器决定(通常位于
.bss段)。 -
这种方式下,中断栈和主栈确实是"两码事"------它们从物理到逻辑都是独立的。
-
-
优点:隔离性强,主栈可以被完全回收用作其他用途(如空闲任务栈),且中断栈大小可精确控制,不受主栈大小影响。
-
适用场景:对安全性要求较高,或希望精确控制内存占用的系统。
2. 复用主栈(链接脚本方式)
-
实现 :不定义
configISR_STACK_SIZE_WORDS,而是依赖链接脚本提供一个符号__freertos_irq_stack_top,该符号指向主栈的栈顶。FreeRTOS 将直接使用这个地址作为中断栈的栈顶。 -
特点:
-
中断栈与主栈是同一块物理内存区域 ,只是角色发生了转换:启动阶段作为主栈供
main使用,调度器启动后作为中断栈。 -
这是典型的"一栈两用"。
-
-
优点 :节省内存(无需额外分配中断栈),且符合 RISC-V 简单设计------只需一个主栈,通过 一个静态全局变量(即xISRStackTop)/
mscratch 寄存器保存中断栈栈指针,在中断时切换过去。 -
适用场景:内存受限的嵌入式系统,或希望最小化 RAM 占用的场景。
这段代码与之前讨论的关联
-
印证了"中断栈有时独立,有时复用主栈"的说法。
-
对齐和填充的细节 :代码中特意提到对齐和填充字节(
0xee),说明中断栈也需要像普通栈一样进行调试保护(如栈溢出检测),这进一步强调了它在系统中的重要地位。
总结
这段代码以极其简洁的方式,展示了 RTOS 如何灵活处理中断栈的两种经典设计。它既满足了希望节省内存的简单系统(复用主栈),也照顾了需要强隔离的安全系统(独立中断栈)。这正是 RISC-V 和 FreeRTOS 这类轻量级 RTOS 的设计哲学:提供机制而非策略,让开发者根据实际需求选择。
3.2 FreeRTOS异常(trap)入口源码
相关文章:【外部链接】RISC-V 端口 |FreeRTOS/FreeRTOS-Kernel |DeepWiki
3.2.1 函数调用关系
freertos_risc_v_trap_handler //异常处理函数入口
portcontextSAVE_CONTEXT_INTERNAL //保存任务切换上下文
asynchronous_interrupt //异步异常:时钟中断
handle_interrupt //异步异常:时钟中断
portUPDATE_MTIMER_COMPARE_REGISTER//更新产生时钟中断的时间
xTaskIncrementTick//决定是否切换任务
processed_source//中断返回,恢复中断现场
vTaskSwitchContext//切换上下文
processed_source//中断返回,恢复任务上下文
synchronous_exception //同步异常:ecall调用
handle_exception
vTaskSwitchContext//切换任务
processed_source//中断返回,恢恢复任务上下文
3.2.2 源码
.section .text.freertos_risc_v_trap_handler
.align 8
freertos_risc_v_trap_handler: //异常处理入口函数
portcontextSAVE_CONTEXT_INTERNAL //保存任务上下文
csrr a0, mcause //读取mcause寄存
csrr a1, mepc //读取发生异常时候的地址
//注意这里把mcause中读出来的数当做符号数,高位1表示负数
//异常异常时,mcause寄存器的高位是1(负数);同步异常,mcause寄存的高位是0
bge a0, x0, synchronous_exception
//异步异常:时钟中断
asynchronous_interrupt:
store_x a1, 0( sp ) //保存发生异常返回的地址
load_x sp, xISRStackTop //切换到中断特有的栈空间, xISRStackTop记录的栈顶
j handle_interrupt
//同步异常:ecall调用
synchronous_exception:
addi a1, a1, 4 //mepc的值+4,这里只处理ecall调用,所以默认+4。并不是所有的同步异常都要+4
store_x a1, 0( sp ) //保存发生异常返回的地址
load_x sp, xISRStackTop //切换到中断特有的栈空间, xISRStackTop记录的栈顶
j handle_exception
handle_interrupt:
#if( portasmHAS_MTIME != 0 )
test_if_mtimer: /* If there is a CLINT then the mtimer is used to generate the tick interrupt. */
addi t0, x0, 1
slli t0, t0, __riscv_xlen - 1 /* LSB is already set, shift into MSB. Shift 31 on 32-bit or 63 on 64-bit cores. */
addi t1, t0, 7 /* 0x8000[]0007 == machine timer interrupt. */
bne a0, t1, application_interrupt_handler
portUPDATE_MTIMER_COMPARE_REGISTER //更新产生时钟中断的时间
call xTaskIncrementTick //决定是否切换任务
beqz a0, processed_source /* Don't switch context if incrementing tick didn't unblock a task. */
call vTaskSwitchContext //切换上下文
j processed_source
#endif /* portasmHAS_MTIME */
application_interrupt_handler:
call freertos_risc_v_application_interrupt_handler
j processed_source
//任务主动切换,调用ecall进入中断
handle_exception:
/* a0 contains mcause. */
li t0, 11 /* 11表示是M模式的系统调用 */
bne a0, t0, application_exception_handler /* 不是ecall调用,则执行application_exception_handler */
call vTaskSwitchContext //切换任务
j processed_source
application_exception_handler:
call freertos_risc_v_application_exception_handler //死循环
j processed_source /* No other exceptions handled yet. */
processed_source:
portcontextRESTORE_CONTEXT //中断返回,恢复任务上下文
3.3 FreeRTOS任务上下文存档源码
.macro portcontextSAVE_CONTEXT_INTERNAL
/* 1. 在栈上分配空间,用于保存所有需要保存的寄存器。
portCONTEXT_SIZE 是整个上下文的字节大小,由 FreeRTOS 根据寄存器数量等计算得出。
将栈指针 sp 减去该大小,相当于在栈上开辟了一块新的栈帧。 */
addi sp, sp, -portCONTEXT_SIZE
/* 2. 保存通用寄存器(整数寄存器)到栈上。
这些寄存器包括调用者保存的寄存器(如 ra, t0-t6, a0-a7 等)以及被调用者保存的寄存器(s0-s11)。
注意:这里保存的寄存器顺序必须与后续恢复时完全一致。
store_x 是一个宏,用于存储一个寄存器值到内存(可能是 sw 或 sd,取决于 RV32/RV64)。 */
store_x x1, 2 * portWORD_SIZE( sp ) /* 保存返回地址 ra (x1) */
store_x x5, 3 * portWORD_SIZE( sp ) /* 保存临时寄存器 t0 (x5) */
store_x x6, 4 * portWORD_SIZE( sp ) /* t1 (x6) */
store_x x7, 5 * portWORD_SIZE( sp ) /* t2 (x7) */
store_x x8, 6 * portWORD_SIZE( sp ) /* s0/fp (x8) */
store_x x9, 7 * portWORD_SIZE( sp ) /* s1 (x9) */
store_x x10, 8 * portWORD_SIZE( sp ) /* a0 (x10) */
store_x x11, 9 * portWORD_SIZE( sp ) /* a1 (x11) */
store_x x12, 10 * portWORD_SIZE( sp ) /* a2 (x12) */
store_x x13, 11 * portWORD_SIZE( sp ) /* a3 (x13) */
store_x x14, 12 * portWORD_SIZE( sp ) /* a4 (x14) */
store_x x15, 13 * portWORD_SIZE( sp ) /* a5 (x15) */
#ifndef __riscv_32e
/* 如果不是 RV32E(嵌入式,只有16个寄存器),则保存其余寄存器。
这些是剩余的临时寄存器和保存寄存器。 */
store_x x16, 14 * portWORD_SIZE( sp ) /* a6 (x16) */
store_x x17, 15 * portWORD_SIZE( sp ) /* a7 (x17) */
store_x x18, 16 * portWORD_SIZE( sp ) /* s2 (x18) */
store_x x19, 17 * portWORD_SIZE( sp ) /* s3 (x19) */
store_x x20, 18 * portWORD_SIZE( sp ) /* s4 (x20) */
store_x x21, 19 * portWORD_SIZE( sp ) /* s5 (x21) */
store_x x22, 20 * portWORD_SIZE( sp ) /* s6 (x22) */
store_x x23, 21 * portWORD_SIZE( sp ) /* s7 (x23) */
store_x x24, 22 * portWORD_SIZE( sp ) /* s8 (x24) */
store_x x25, 23 * portWORD_SIZE( sp ) /* s9 (x25) */
store_x x26, 24 * portWORD_SIZE( sp ) /* s10 (x26) */
store_x x27, 25 * portWORD_SIZE( sp ) /* s11 (x27) */
store_x x28, 26 * portWORD_SIZE( sp ) /* t3 (x28) */
store_x x29, 27 * portWORD_SIZE( sp ) /* t4 (x29) */
store_x x30, 28 * portWORD_SIZE( sp ) /* t5 (x30) */
store_x x31, 29 * portWORD_SIZE( sp ) /* t6 (x31) */
#endif /* ifndef __riscv_32e */
/* 3. 保存临界区嵌套计数器(xCriticalNesting)。
xCriticalNesting 是 FreeRTOS 用于管理临界区嵌套的全局变量。
将其值加载到 t0,然后保存到栈上的特定偏移(portCRITICAL_NESTING_OFFSET 定义的位置)。 */
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. */
/* 4. 可选:保存浮点单元(FPU)上下文,如果使能了 FPU 并且 FPU 状态为"脏"(即已被使用)。
根据 RISC-V 的 mstatus 寄存器中的 FS 域判断。FS 域为 3(dirty)时表示 FPU 已被使用,需要保存其状态。 */
#if( configENABLE_FPU == 1 )
csrr t0, mstatus /* 读取 mstatus 寄存器 */
srl t1, t0, MSTATUS_FS_OFFSET /* 将 FS 域移到最低位 */
andi t1, t1, 3 /* 提取 FS 域的值(2位) */
addi t2, x0, 3 /* t2 = 3,表示 dirty 状态 */
bne t1, t2, 1f /* 如果不等于 dirty,则跳过保存 FPU 寄存器 */
portcontexSAVE_FPU_CONTEXT /* 调用保存 FPU 上下文的宏(保存浮点寄存器等) */
1:
#endif
/* 5. 可选:保存向量处理单元(VPU)上下文,类似 FPU,但用于向量扩展。 */
#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
portcontexSAVE_VPU_CONTEXT
2:
#endif
/* 6. 调用芯片特定的保存附加寄存器的宏。
某些 RISC-V 实现可能有额外的状态(如自定义寄存器、DSP 扩展等),
可以通过这个宏扩展,由用户或芯片移植层提供。 */
portasmSAVE_ADDITIONAL_REGISTERS /* Defined in freertos_risc_v_chip_specific_extensions.h to save any registers unique to the RISC-V implementation. */
/* 7. 保存 mstatus 寄存器到栈上。
因为 mstatus 可能在后续处理中被修改,需要保存其原始值以便恢复。 */
csrr t0, mstatus
store_x t0, 1 * portWORD_SIZE( sp )
/* 8. 如果保存了 FPU 上下文,将 mstatus 中的 FS 域标记为"干净"(clean),表示 FPU 状态已被保存。
这样在恢复时不会再次保存,或者允许其他任务使用 FPU。 */
#if( configENABLE_FPU == 1 )
/* Mark the FPU as clean, if it was dirty and we saved FPU registers. */
srl t1, t0, MSTATUS_FS_OFFSET /* 再次检查 FS 域 */
andi t1, t1, 3
addi t2, x0, 3
bne t1, t2, 3f /* 如果之前不是 dirty,则跳过 */
li t1, ~MSTATUS_FS_MASK /* 清除 FS 域 */
and t0, t0, t1
li t1, MSTATUS_FS_CLEAN /* 设置为 clean (通常为 2) */
or t0, t0, t1
csrw mstatus, t0 /* 写回 mstatus */
3:
#endif
/* 9. 类似地,如果保存了 VPU 上下文,将 VS 域标记为干净。 */
#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
/* 10. 将当前任务的栈指针 sp 保存到任务控制块(TCB)的第一个成员中。
FreeRTOS 中,TCB 的第一个成员通常是指向当前栈顶的指针(pxTopOfStack)。
load_x t0, pxCurrentTCB 加载当前任务控制块的地址到 t0,
store_x sp, 0 (t0) 将 sp 的值存储到 TCB 的开头(即 pxTopOfStack 字段)。 */
load_x t0, pxCurrentTCB /* Load pxCurrentTCB. */
store_x sp, 0 ( t0 ) /* Write sp to first TCB member. */
.endm
3.4 异常例外处理
.macro portcontextSAVE_EXCEPTION_CONTEXT
portcontextSAVE_CONTEXT_INTERNAL
csrr a0, mcause
csrr a1, mepc
addi a1, a1, 4 /* Synchronous so update exception return address to the instruction after the instruction that generated the exception. */
store_x a1, 0 ( sp ) /* Save updated exception return address. */
load_x sp, xISRStackTop /* Switch to ISR stack. */
.endm
3.5 中断处理
.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
3.6 FreeRTOS任务上下文恢复
.macro portcontextRESTORE_CONTEXT
/* 1. 从当前任务控制块(TCB)中恢复栈指针 sp。
pxCurrentTCB 指向当前要运行的任务的 TCB。
TCB 的第一个成员通常存储了该任务被换出时的栈指针(即保存上下文的栈顶)。 */
load_x t1, pxCurrentTCB /* 将 pxCurrentTCB 的地址加载到 t1 */
load_x sp, 0 ( t1 ) /* 从 TCB 的第一个字段读取该任务的栈指针到 sp */
/* 2. 恢复 mepc 寄存器,即任务被中断或异常发生时需要返回的地址。
在保存上下文中,mepc 被保存在栈的偏移 0 处(见保存宏)。 */
load_x t0, 0 ( sp ) /* 从栈顶加载保存的 mepc 值到 t0 */
csrw mepc, t0 /* 将 t0 写入 mepc 寄存器 */
/* 3. 恢复 mstatus 寄存器,包含全局中断使能、FPU/VPU 状态等。
保存时,mstatus 被保存在栈的偏移 1 * portWORD_SIZE 处。 */
load_x t0, 1 * portWORD_SIZE( sp ) /* 从栈上加载保存的 mstatus 值到 t0 */
csrw mstatus, t0 /* 将 t0 写入 mstatus 寄存器 */
/* 4. 恢复芯片特定的附加寄存器(如果有)。
这是可选的扩展点,由芯片移植层提供,用于恢复自定义寄存器。 */
portasmRESTORE_ADDITIONAL_REGISTERS
/* 5. 如果使能了 VPU(向量处理单元),并且 mstatus.VS 域为 dirty(3),则恢复 VPU 上下文。
保存宏中,如果 VPU 脏则保存了 VPU 寄存器,并在保存后将 VS 域标记为 clean。
这里检查 VS 域,如果是 dirty,说明 VPU 寄存器需要恢复(可能被中断处理修改了)。 */
#if( configENABLE_VPU == 1 )
csrr t0, mstatus /* 读取当前 mstatus 值(但刚恢复的 mstatus 可能已包含 VPU 状态?) */
srl t1, t0, MSTATUS_VS_OFFSET /* 将 VS 域移到低位 */
andi t1, t1, 3 /* 提取 VS 域值 */
addi t2, x0, 3 /* t2 = 3,dirty 状态值 */
bne t1, t2, 5f /* 如果不是 dirty,则跳过 VPU 恢复 */
portcontextRESTORE_VPU_CONTEXT /* 调用恢复 VPU 上下文的宏 */
5:
#endif
/* 6. 如果使能了 FPU(浮点单元),类似地恢复 FPU 上下文。 */
#if( configENABLE_FPU == 1 )
csrr t0, mstatus
srl t1, t0, MSTATUS_FS_OFFSET
andi t1, t1, 3
addi t2, x0, 3
bne t1, t2, 6f
portcontextRESTORE_FPU_CONTEXT
6:
#endif
/* 7. 恢复临界区嵌套计数器 xCriticalNesting。
保存时,该值被存放在栈的特定偏移 portCRITICAL_NESTING_OFFSET 处。
需要将其写回全局变量 xCriticalNesting。 */
load_x t0, portCRITICAL_NESTING_OFFSET * portWORD_SIZE( sp ) /* 从栈加载保存的临界区嵌套值 */
load_x t1, pxCriticalNesting /* 加载 xCriticalNesting 的地址到 t1 */
store_x t0, 0 ( t1 ) /* 将值存回全局变量 */
/* 8. 恢复所有通用寄存器(除 x0、x2(sp)、x3(gp)、x4(tp)外)。
保存宏保存了 x1, x5-x31,以及如果 RV32E 则更少。
恢复顺序必须与保存顺序一致。 */
load_x x1, 2 * portWORD_SIZE( sp ) /* 恢复 ra */
load_x x5, 3 * portWORD_SIZE( sp ) /* 恢复 t0 */
load_x x6, 4 * portWORD_SIZE( sp ) /* 恢复 t1 */
load_x x7, 5 * portWORD_SIZE( sp ) /* 恢复 t2 */
load_x x8, 6 * portWORD_SIZE( sp ) /* 恢复 s0/fp */
load_x x9, 7 * portWORD_SIZE( sp ) /* 恢复 s1 */
load_x x10, 8 * portWORD_SIZE( sp ) /* 恢复 a0 */
load_x x11, 9 * portWORD_SIZE( sp ) /* 恢复 a1 */
load_x x12, 10 * portWORD_SIZE( sp ) /* 恢复 a2 */
load_x x13, 11 * portWORD_SIZE( sp ) /* 恢复 a3 */
load_x x14, 12 * portWORD_SIZE( sp ) /* 恢复 a4 */
load_x x15, 13 * portWORD_SIZE( sp ) /* 恢复 a5 */
#ifndef __riscv_32e
/* 如果不是 RV32E(即完整 32 个寄存器),则恢复剩余的寄存器。 */
load_x x16, 14 * portWORD_SIZE( sp ) /* 恢复 a6 */
load_x x17, 15 * portWORD_SIZE( sp ) /* 恢复 a7 */
load_x x18, 16 * portWORD_SIZE( sp ) /* 恢复 s2 */
load_x x19, 17 * portWORD_SIZE( sp ) /* 恢复 s3 */
load_x x20, 18 * portWORD_SIZE( sp ) /* 恢复 s4 */
load_x x21, 19 * portWORD_SIZE( sp ) /* 恢复 s5 */
load_x x22, 20 * portWORD_SIZE( sp ) /* 恢复 s6 */
load_x x23, 21 * portWORD_SIZE( sp ) /* 恢复 s7 */
load_x x24, 22 * portWORD_SIZE( sp ) /* 恢复 s8 */
load_x x25, 23 * portWORD_SIZE( sp ) /* 恢复 s9 */
load_x x26, 24 * portWORD_SIZE( sp ) /* 恢复 s10 */
load_x x27, 25 * portWORD_SIZE( sp ) /* 恢复 s11 */
load_x x28, 26 * portWORD_SIZE( sp ) /* 恢复 t3 */
load_x x29, 27 * portWORD_SIZE( sp ) /* 恢复 t4 */
load_x x30, 28 * portWORD_SIZE( sp ) /* 恢复 t5 */
load_x x31, 29 * portWORD_SIZE( sp ) /* 恢复 t6 */
#endif /* ifndef __riscv_32e */
/* 9. 调整栈指针,释放之前保存上下文时分配的栈空间。
portCONTEXT_SIZE 是保存上下文时分配的字节数。 */
addi sp, sp, portCONTEXT_SIZE
/* 10. 执行 mret 指令,从机器模式异常返回。
这将恢复 PC 为 mepc 中的值,并恢复 mstatus 中的 MIE 位(即重新使能中断)。 */
mret
.endm
该宏执行的任务上下文恢复操作,与之前分析的保存宏完全对称。它从当前任务的 TCB 中获取栈指针,然后按照保存顺序逆序恢复所有寄存器、CSR 以及系统状态,最后通过 mret 指令让 CPU 回到任务被中断或异常前的执行点。注意恢复过程中对 FPU/VPU 的检查与保存时一致,确保状态一致性。