手动触发上下文切换
像信号量等阻塞的,当满足条件不阻塞了,要调用这个,让高优先级的任务抢占。
#if (configUSE_PREEMPTION == 0)
/* 如果使用协作式调度(cooperative scheduling),
* 那么就算有更高优先级的任务被唤醒,也不应该立即触发一次调度(yield)。
*/
#define queueYIELD_IF_USING_PREEMPTION()
#else
/* 如果使用抢占式调度(preemptive scheduling),
* 一旦有更高优先级任务就绪,就要触发一次上下文切换。
*/
#define queueYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()
#endif
#ifndef portYIELD_WITHIN_API
#define portYIELD_WITHIN_API portYIELD
#endif#define portYIELD() \
{ \
/* 触发PendSV中断进行上下文切换 */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__dsb(portSY_FULL_READ_WRITE); /* 数据同步 */ \
__isb(portSY_FULL_READ_WRITE); /* 指令同步 */ \
}Cortex-M 内核寄存器
Cortex-M 内核一共 16 个通用寄存器(R0–R15)
R0–R3:函数调用参数 / 临时变量
R4–R11:被调用者保存寄存器(callee-saved),任务切换时必须手动保存/恢复
R12:临时
R13 (SP):堆栈指针,两种情况共用该寄存器:
MSP (Main Stack Pointer) → 内核、异常模式用
PSP (Process Stack Pointer) → 用户任务用(RTOS 把任务栈挂在 PSP 上)
R14 (LR):链接寄存器(返回地址)
R15 (PC):程序计数器
xPSR:程序状态寄存器(标志位、执行状态)
重要的特殊寄存器:
BASEPRI:优先级屏蔽寄存器 → 限制某个阈值以下的中断进入(FreeRTOS 用它做“关中断”)。
CONTROL:决定当前用 MSP 还是 PSP。
汇编语言
mrs
ldr
dsb
isb
str
stmdb
bl
mov
ldmia
bx
任务切换原理
__asm void xPortPendSVHandler(void)
{
extern uxCriticalNesting; // 临界区嵌套深度
extern pxCurrentTCB; // 当前任务
extern vTaskSwitchContext; // 选择下一个任务函数
PRESERVE8 // ARM 8字节对齐,符合 ABI 规范
//=========================== 保存当前任务 ==========================
mrs r0, psp // 将当前任务的堆栈指针读入r0
isb // 指令同步
ldr r3,= pxCurrentTCB // TCB地址赋值给r3
ldr r2, [r3] // 根据TCB地址拿到任务堆栈指针
stmdb r0!, {r4-r11} // 将寄存器r4-r11的值压入栈中
str r0, [r2] // 将新的堆栈指针保存到当前任务TCB中,这也是TCB中第一个成员必须为pxTopOfStack堆栈指针的原因,放其它地方无法实现
stmdb sp!, {r3, r14} // 将r3中的TCB地址和r14中的返回地址压入主堆栈
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0 // 关中断,和 vPortRaiseBASEPRI 函数实现原理一样
dsb // 数据同步
isb // 指令同步
//=========================== 加载新任务 ==========================
bl vTaskSwitchContext // 调用vTaskSwitchContext函数,这个函数用于获取下一个要运行的任务并更新pxCurrentTCB
mov r0, #0
msr basepri, r0 // r0清零并开中断,和 vPortSetBASEPRI(0) 函数实现原理一样
ldmia sp!, {r3, r14} // 从主堆栈中弹出当前TCB地址到r3和返回地址到r14中
ldr r1, [r3] // TCB地址赋值给r1
ldr r0, [r1] // 根据TCB地址拿到任务堆栈指针
ldmia r0!, {r4-r11} // 恢复寄存器r4-r11的值
msr psp, r0 // 将新的栈顶赋值给任务栈指针
isb // 指令同步
bx r14 // 跳转到这个任务的返回地址,继续执行
nop // 空指令,延时一个指令周期
}切换任务时不能被其它中断打断,但是pendSV优先级不是最高的吗?
系统异常(SVCall、PendSV、SysTick)都可以配置优先级。
FreeRTOS 把 PendSV 设置为最低优先级(也就是说,只有在没有别的中断在跑时,它才会跑)。
👉 所以 PendSV 并不是“最高优先级”,而是“最低优先级”。这样才能保证:
先处理硬件中断(UART、SPI、定时器…)。
等都处理完了,再切换任务,不会打断关键的中断处理。
为什么切换时还要关中断(BASEPRI)?
问题的核心是:
PendSV 自己不会被抢占(因为它最低),但在 PendSV 执行过程中,别的高优先级中断依然可能进来。
如果此时中断里调用了 RTOS API(比如信号量、任务切换相关),就会操作 pxCurrentTCB、任务链表等关键数据结构。
那么这些数据结构可能在 保存旧任务 / 恢复新任务 之间被破坏,导致上下文恢复错误,系统崩溃。