一、背景知识回顾

在 Cortex-M 内核中,有**双堆栈**机制:

  • MSP (主堆栈指针):用于中断服务函数和内核代码。
  • PSP (进程堆栈指针):用于任务(线程)代码。

当任务运行时,使用的堆栈指针是 PSP。发生中断时,硬件会自动将部分寄存器压入当前使用的堆栈(即 PSP 指向的任务栈),然后切换堆栈指针到 MSP,进入中断服务函数。

PendSV 是一个可悬挂的中断,通常由 SysTick 或任务主动触发,专门用于任务切换。在 PendSV 中断服务函数中,我们需要:

  1. 保存当前任务(上文)的剩余寄存器(硬件已自动保存了部分寄存器)。
  2. 更新当前任务的控制块(TCB)中的栈顶指针
  3. 选择下一个要运行的任务(调用 vTaskSwitchContext())。
  4. 恢复下一个任务(下文)的上下文(包括手动保存的寄存器和硬件自动恢复的寄存器)。

二、举例场景

假设系统中有两个任务:

  • 任务 A:当前正在运行,优先级较低。
  • 任务 B:优先级较高,已就绪。

当前 pxCurrentTCB 指向任务 A 的 TCB。某时刻 SysTick 中断触发,在中断服务中判断需要切换任务,于是悬起 PendSV。当所有更高优先级的中断处理完后,PendSV 中断服务函数 xPortPendSVHandler 被执行,完成从任务 A 切换到任务 B。


三、逐行解析 xPortPendSVHandler

代码清单(复制方便对照)

__asm void xPortPendSVHandler( void ) 
{ 
    extern pxCurrentTCB;          // (1)
    extern vTaskSwitchContext;    // (2)

    PRESERVE8                     // (3)

    mrs r0, psp                   // (4)
    isb

    ldr r3, =pxCurrentTCB         // (5)
    ldr r2, [r3]                  // (6)

    stmdb r0!, {r4-r11}           // (7)
    str r0, [r2]                  // (8)

    stmdb sp!, {r3, r14}          // (9)
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY  // (10)
    msr basepri, r0               // (11)
    dsb
    isb
    bl vTaskSwitchContext         // (12)
    mov r0, #0                    // (13)
    msr basepri, r0               // (14)
    ldmia sp!, {r3, r14}          // (15)

    ldr r1, [r3]                  // (16)
    ldr r0, [r1]                  // (17)
    ldmia r0!, {r4-r11}           // (18)
    msr psp, r0                   // (19)
    isb
    bx r14                        // (20)
    nop
}

1. 声明外部变量和函数(1-2)

extern pxCurrentTCB;          // (1)
extern vTaskSwitchContext;    // (2)
  • pxCurrentTCB 是全局指针,指向当前正在运行(或即将运行)的任务的 TCB。在任务切换过程中,它会被更新。
  • vTaskSwitchContext 是函数,用于选择下一个要运行的任务,它会修改 pxCurrentTCB 的值。

2. 保持栈对齐(3)

PRESERVE8                     // (3)
  • 这是一个汇编指示符,告诉编译器当前函数需要保持 8 字节对齐。在 Cortex-M 中,某些指令(如双字加载/存储)要求栈地址是 8 字节对齐,否则可能引发异常。

3. 获取当前任务的 PSP 并保存剩余寄存器(4-8)

(4) 读取 PSP 到 r0
mrs r0, psp                   // (4)
isb
  • 进入 PendSV 时,硬件已经自动将当前任务的部分寄存器压入该任务的栈中(即 PSP 指向的栈),这些寄存器包括:xPSRPCLR(R14)、R12R3R2R1R0。硬件压栈后,PSP 会自动递减指向新的栈顶。
  • 指令 mrs r0, psp 将当前 PSP 的值读到通用寄存器 r0 中,以便后续操作。
  • isb 是指令同步隔离,确保前面的操作完成。

此时内存与寄存器的状态(以任务 A 为例):

  • 假设任务 A 的栈起始地址为 0x20001000,之前已使用一部分。硬件自动压栈后,PSP 指向了栈顶,比如 0x20000FE8(压入了 8 个寄存器,每个 4 字节,共 32 字节)。
  • r0 现在保存着 0x20000FE8,即当前任务 A 的栈顶指针。
(5-6) 加载 pxCurrentTCB 的地址和值
ldr r3, =pxCurrentTCB         // (5)
ldr r2, [r3]                  // (6)
  • ldr r3, =pxCurrentTCB:将 pxCurrentTCB 变量的地址加载到 r3。注意 = 表示取地址。
  • ldr r2, [r3]:从该地址加载内容,即 pxCurrentTCB 本身的值(一个指向 TCB 的指针)。此时 r2 指向当前任务(任务 A)的 TCB 结构体。
(7-8) 手动保存 R4-R11 并更新 TCB 中的栈顶指针
stmdb r0!, {r4-r11}           // (7)
str r0, [r2]                  // (8)
  • stmdb r0!, {r4-r11}:以 r0 为基址,先递减再存储(DB = Decrease Before)。由于 r0 当前指向任务 A 栈的当前栈顶(硬件压栈后),stmdb 会先将 r0 减去 32 字节(因为存储 8 个寄存器),然后将 r4-r11 依次存入新的栈位置,最后更新 r0 指向新的栈顶。
    • 这 8 个寄存器是任务中需要手动保存的,因为硬件只自动保存了 r0-r3、r12、LR、PC、xPSR。
    • 存储后,任务 A 的所有上下文(硬件自动 + 手动)都完整保存在它的栈中。
  • str r0, [r2]:将更新后的 r0(即任务 A 新的栈顶指针)写入 r2 指向的地址。由于 r2 指向 pxCurrentTCB,而 TCB 的第一个成员通常是 pxTopOfStack,因此这条指令将任务 A 最新的栈顶指针保存到 TCB 中。至此,当前任务(上文)的保存工作完成。

图示说明

硬件自动保存后:
PSP 指向 0x20000FE8
栈内容:[ xPSR, PC, LR, R12, R3, R2, R1, R0 ]  (从高地址到低地址)

手动保存 R4-R11 后(stmdb):
r0 先减到 0x20000FC8,然后存储 R4-R11
新栈顶指针 = 0x20000FC8
任务 A 的 TCB.pxTopOfStack 更新为 0x20000FC8

4. 临时保护寄存器并关中断,选择下一个任务(9-15)

(9) 将 r3 和 r14 压入 MSP 栈
stmdb sp!, {r3, r14}          // (9)
  • 当前中断使用的是 MSP,sp 就是 MSP。将 r3 和 r14 压入 MSP 栈。
    • r3 保存了 pxCurrentTCB 的地址,后面还需要用它来获取更新后的 TCB。
    • r14 是链接寄存器,保存了 PendSV 中断的返回地址。如果接下来调用其他函数,r14 会被覆盖,所以需要保护。
  • 使用 stmdb sp!, {...} 将这两个寄存器压入主栈。
(10-11) 关中断(通过 BASEPRI)
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY  // (10)
msr basepri, r0               // (11)
dsb
isb
  • mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY:将宏定义的值(例如 191)加载到 r0。
  • msr basepri, r0:写入 BASEPRI 寄存器,屏蔽所有优先级数值 ≥ 191 的中断(即逻辑优先级较低的中断)。这样,那些优先级高于此阈值的中断仍可响应,而调用 FreeRTOS API 的中断会被屏蔽,保护内核数据结构。
  • dsb(数据同步隔离)和 isb(指令同步隔离)确保写操作完成。
(12) 调用 vTaskSwitchContext
bl vTaskSwitchContext         // (12)
  • 调用函数 vTaskSwitchContext,它会查找优先级最高的就绪任务,并更新 pxCurrentTCB 指向该任务的 TCB。
  • 在我们的例子中,任务 B 优先级更高,因此 pxCurrentTCB 被更新为指向任务 B 的 TCB。
  • 函数返回后,pxCurrentTCB 已经改变。
(13-14) 恢复中断
mov r0, #0                    // (13)
msr basepri, r0               // (14)
  • 将 r0 清零,然后写入 BASEPRI,恢复所有中断(BASEPRI=0 表示不屏蔽任何中断)。提前开中断,减少中断延迟,因为后续只是恢复上下文,不涉及共享数据。
(15) 恢复之前保护的 r3 和 r14
ldmia sp!, {r3, r14}          // (15)
  • ldmia sp!, {r3, r14} 从 MSP 栈中弹出之前压入的 r3 和 r14,恢复它们的值。
    • r3 重新获得了 pxCurrentTCB地址
    • r14 恢复了 PendSV 中断的返回地址(异常返回时使用)。

5. 恢复新任务的上下文(16-20)

(16-17) 获取新任务的 TCB 和栈顶指针
ldr r1, [r3]                  // (16)
ldr r0, [r1]                  // (17)
  • ldr r1, [r3]:r3 存的是 pxCurrentTCB 的地址,[r3] 就是 pxCurrentTCB 的值(即新任务 B 的 TCB 地址),加载到 r1。
  • ldr r0, [r1]:从 TCB 中加载第一个成员,即 pxTopOfStack(栈顶指针)到 r0。此时 r0 指向新任务 B 的栈顶,该栈顶之前保存了任务 B 的所有寄存器(包括手动保存的 r4-r11)。
(18) 恢复新任务的 R4-R11
ldmia r0!, {r4-r11}           // (18)
  • ldmia r0!, {r4-r11}:从 r0 指向的地址加载 r4-r11,同时 r0 递增。注意这是从任务 B 的栈中恢复手动保存的寄存器。恢复后,r0 指向了任务 B 栈中硬件自动保存的寄存器区域的起始位置(即之前硬件压栈时保存的 xPSR, PC, LR 等)。
(19) 更新 PSP 为新任务的栈顶
msr psp, r0                   // (19)
isb
  • 将 r0(指向任务 B 栈中硬件自动保存区域的起始位置)写入 PSP。这样,当 PendSV 中断返回时,硬件会自动从 PSP 指向的位置弹出剩余寄存器(xPSR, PC, LR, R12, R3, R2, R1, R0),完成上下文的完全恢复。
(20) 异常返回
bx r14                        // (20)
  • r14 中保存的是异常返回的专用值(0xFFFFFFED 等),bx r14 会触发异常返回机制。硬件会根据 PSP 自动出栈剩余的寄存器,并切换回线程模式(使用 PSP)。此时 CPU 将恢复执行新任务(任务 B)的代码。

四、总结

整个 PendSV 中断服务函数完成了三个主要步骤:

  1. 保存上文:将当前任务的 r4-r11 手动压入其栈,并将新的栈顶指针保存到 TCB。
  2. 选择下文:关中断保护全局数据,调用 vTaskSwitchContext 更新 pxCurrentTCB,然后开中断。
  3. 恢复下文:从新任务的 TCB 中获取栈顶指针,恢复 r4-r11,更新 PSP,最后异常返回,硬件自动恢复剩余寄存器。

通过这个机制,FreeRTOS 利用 Cortex-M 的 PendSV 实现了高效、可嵌套的任务切换,同时通过 BASEPRI 精细控制中断,保证了硬实时性。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐