深入剖析 xPortPendSVHandler:FreeRTOS 任务切换的核心引擎
·
一、背景知识回顾
在 Cortex-M 内核中,有**双堆栈**机制:
- MSP (主堆栈指针):用于中断服务函数和内核代码。
- PSP (进程堆栈指针):用于任务(线程)代码。
当任务运行时,使用的堆栈指针是 PSP。发生中断时,硬件会自动将部分寄存器压入当前使用的堆栈(即 PSP 指向的任务栈),然后切换堆栈指针到 MSP,进入中断服务函数。
PendSV 是一个可悬挂的中断,通常由 SysTick 或任务主动触发,专门用于任务切换。在 PendSV 中断服务函数中,我们需要:
- 保存当前任务(上文)的剩余寄存器(硬件已自动保存了部分寄存器)。
- 更新当前任务的控制块(TCB)中的栈顶指针。
- 选择下一个要运行的任务(调用
vTaskSwitchContext())。 - 恢复下一个任务(下文)的上下文(包括手动保存的寄存器和硬件自动恢复的寄存器)。
二、举例场景
假设系统中有两个任务:
- 任务 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 指向的栈),这些寄存器包括:
xPSR、PC、LR(R14)、R12、R3、R2、R1、R0。硬件压栈后,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 会被覆盖,所以需要保护。
- r3 保存了
- 使用
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 中断的返回地址(异常返回时使用)。
- r3 重新获得了
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 中断服务函数完成了三个主要步骤:
- 保存上文:将当前任务的 r4-r11 手动压入其栈,并将新的栈顶指针保存到 TCB。
- 选择下文:关中断保护全局数据,调用
vTaskSwitchContext更新pxCurrentTCB,然后开中断。 - 恢复下文:从新任务的 TCB 中获取栈顶指针,恢复 r4-r11,更新 PSP,最后异常返回,硬件自动恢复剩余寄存器。
通过这个机制,FreeRTOS 利用 Cortex-M 的 PendSV 实现了高效、可嵌套的任务切换,同时通过 BASEPRI 精细控制中断,保证了硬实时性。
更多推荐



所有评论(0)