在嵌入式实时操作系统FreeRTOS中,任务切换是核心功能之一。作为一款轻量级RTOS,FreeRTOS通过PendSV异常来实现高效的任务上下文切换。这种设计源于ARM Cortex-M架构的特性,它允许将上下文切换这种非实时关键操作延迟处理,从而减少对实时性要求高的中断响应的影响。
任务切换的本质是将当前运行任务的执行状态(即上下文)保存到任务栈中,然后从待运行任务的任务栈中恢复其执行状态。这个过程需要精确处理CPU寄存器的保存与恢复,确保任务再次被调度时能够从上次中断的位置继续执行,就像从未被中断过一样。
c复制__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
函数声明为__asm表示这是一个纯汇编函数。PRESERVE8指令确保栈按照8字节对齐,这是ARM架构的ABI要求。三个外部变量声明:
uxCriticalNesting:临界区嵌套计数器pxCurrentTCB:指向当前任务控制块的指针vTaskSwitchContext:任务切换的C函数armasm复制mrs r0, psp ; 获取进程栈指针(当前任务的栈)
isb ; 指令同步屏障
ldr r3, =pxCurrentTCB ; 获取当前任务控制块指针
ldr r2, [ r3 ] ; r2 = pxCurrentTCB
这里使用mrs指令将进程栈指针(PSP)读取到r0寄存器。PSP是任务模式下的栈指针,与处理模式使用的MSP不同。isb确保指令同步,防止流水线乱序执行带来的问题。
armasm复制tst r14, #0x10 ; 检查EXC_RETURN bit 4
it eq ; 如果bit4=0,说明使用了FPU
vstmdbeq r0!, {s16-s31} ; 保存FPU高寄存器s16-s31
ARM Cortex-M4及以上内核支持FPU,需要额外保存FPU寄存器。通过检查EXC_RETURN的bit4可以判断任务是否使用了FPU。如果使用了FPU,则需要保存s16-s31寄存器(s0-s15由硬件自动保存)。
注意:FPU寄存器保存采用"高寄存器优先"策略,因为大多数应用不会使用全部32个FPU寄存器,这样可以节省栈空间和切换时间。
armasm复制stmdb r0!, {r4-r11, r14} ; 保存CPU寄存器 r4-r11, r14(EXC_RETURN)
str r0, [ r2 ] ; 更新TCB中的栈顶指针
这里保存r4-r11和r14寄存器到任务栈中,采用"递减存储"(stmdb)方式。r14保存的是EXC_RETURN值,与进入异常时硬件自动保存的LR不同。保存完成后,更新TCB中的栈顶指针。
armasm复制stmdb sp!, {r0, r3} ; 临时保存 r0, r3
mov r0, # configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0 ; 提升中断优先级(屏蔽某些中断)
dsb ; 数据同步屏障
isb ; 指令同步屏障
bl vTaskSwitchContext ; 调用C函数选择下一个任务
mov r0, # 0
msr basepri, r0 ; 恢复中断屏蔽
ldmia sp!, {r0, r3} ; 恢复 r0, r3
这段代码是任务切换的核心:
vTaskSwitchContext选择下一个要运行的任务重要技巧:BASEPRI的使用确保了任务切换的原子性,防止在切换过程中被低优先级中断打断,这是保证系统稳定性的关键。
armasm复制ldr r1, [ r3 ] ; r1 = 新的pxCurrentTCB
ldr r0, [ r1 ] ; r0 = 新任务的栈顶指针
ldmia r0!, {r4-r11, r14} ; 恢复CPU寄存器
tst r14, # 0x10 ; 检查是否需要恢复FPU上下文
it eq
vldmiaeq r0!, {s16-s31} ; 恢复FPU高寄存器
msr psp, r0 ; 更新进程栈指针
isb
新任务上下文恢复是保存过程的逆操作:
armasm复制#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
nop
#endif
#endif
bx r14 ; 异常返回
最后通过bx r14指令从异常返回。这里包含了一个针对XMC4000系列芯片的特定勘误处理。EXC_RETURN值决定了返回后使用的栈指针(MSP/PSP)和处理器模式。
ARM Cortex-M采用双栈机制:
在任务切换时,硬件自动将xPSR、PC、LR、R12、R3-R0压入任务栈(PSP),软件需要手动保存R4-R11和FPU寄存器。这种分工既保证了关键寄存器的及时保存,又减少了不必要的保存/恢复开销。
EXC_RETURN是异常返回时的特殊值,存储在LR寄存器中,其位域含义如下:
| 位 | 含义 |
|---|---|
| 31:4 | 保留(全1) |
| 3 | 0=返回后使用PSP,1=使用MSP |
| 2 | 保留(0) |
| 1 | 0=返回Thumb状态(必须为0) |
| 0 | 0=返回Handler模式,1=返回Thread模式 |
对于任务切换,我们需要返回Thread模式并使用PSP,因此EXC_RETURN值通常为0xFFFFFFFD。
FPU寄存器保存策略对性能影响很大。FreeRTOS采用惰性保存策略:
这种优化可以减少不使用FPU的任务的切换开销。
任务切换是检测栈溢出的理想时机。可以在保存上下文后检查栈指针是否越界:
armasm复制; 假设每个TCB中有pxEndOfStack成员指向栈底
ldr r1, [r2, #4] ; 获取栈底地址
cmp r0, r1 ; 比较当前栈指针与栈底
bcc stack_overflow ; 如果小于则溢出
正确配置PendSV和SysTick的中断优先级至关重要:
c复制// 正确的中断优先级设置示例
NVIC_SetPriority(PendSV_IRQn, (1 << __NVIC_PRIO_BITS) - 1);
NVIC_SetPriority(SysTick_IRQn, (1 << __NVIC_PRIO_BITS) - 1);
栈指针错误:
EXC_RETURN值错误:
FPU上下文不一致:
利用Cortex-M的FPU状态寄存器:
OS-aware调试:
PSP/MSP监视:
使用DWT(Data Watchpoint and Trace)单元测量上下文切换时间:
c复制// 初始化DWT周期计数器
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
// 在切换前后读取CYCCNT计算周期数
uint32_t start = DWT->CYCCNT;
// 触发任务切换
uint32_t end = DWT->CYCCNT;
uint32_t cycles = end - start;
典型Cortex-M3/M4的上下文切换时间在100-300周期之间,具体取决于是否使用FPU。