LiteOS-M PendSV 阻塞修复:从「绕开调度器」到「SVC + PendSV 标准启动」(LiteOS-M 移植⑤·续篇)
LiteOS-M PendSV 阻塞修复:从「绕开调度器」到「SVC + PendSV 标准启动」(LiteOS-M 移植⑤·续篇)
系列定位:STM32MP157 M4 的 LiteOS-M 移植系列(Windows 纯 GCC + Makefile,不做 Keil)。
本篇是[第⑤篇「链接脚本与编译运行」]的续篇——第⑤篇结尾用「直接调用led_task()绕开LOS_Start()」的临时方案让 LED 先闪起来,本篇把它升级为SVC + PendSV 标准启动流程,真正恢复多任务调度。
0 一句话看懂:改了什么
第⑤篇留下一个尾巴:LOS_Start()启动不了调度器,只能绕开它。本篇找到根因并修复,核心变化就一句话:
| 对比项 | 修改前(第⑤篇临时方案) | 修改后(本篇正式方案) |
|---|---|---|
main.c | 直接调用led_task() | LOS_TaskCreate×2 +LOS_Start() |
| 任务数 | 1 个(忙等闪烁) | 2 个(LED0 + LED1) |
| 延时 | for(volatile i...)忙等 | LOS_TaskDelay(500/200) |
HalStartToRun | bx r6(普通跳转) | cpsie i+svc 0(触发系统调用) |
| SVC 处理 | SVC_Handler = Default_Handler(空) | 新增HalSVCHandler(bx lr异常返回) |
LOS_TaskDelay | ❌ 不可用(依赖 PendSV) | ✅ 正常工作 |
| 调度器 | ❌ 未启动,单任务裸跑 | ✅ 启动,多任务轮流调度 |
| TaskContext 布局 | 68 字节(FPU 宏丢失) | 204 字节(显式定义 FPU 宏) |
读完全篇你会理解:为什么第⑤篇要绕开LOS_Start,以及怎样用一次 SVC 异常返回把调度器真正拉起来——外加一个第⑤篇没提到的 FPU 布局坑。
1 修改前的现象:任务创建成功,但从未被调度
第⑤篇的临时方案之所以「绕开」,是因为标准写法根本跑不起来:
LOS_TaskCreate(&task_id,&task_init);// 创建任务 → 成功LOS_Start();// 启动调度器 → 卡死任务创建成功,但从未被调度。系统掉进HalSysExit死循环(LOS_IntLock(); while(1){}),LED 不闪。
当时的临时对策(第⑤篇修复 3):
if(LOS_KernelInit()!=LOS_OK){while(1);}led_task();/* 直接调用,绕开 LOS_Start → HalStartToRun → bx r6 路径 */任务能跑,但只是跑在main的调用栈上,没有经过调度器——LOS_TaskDelay不能用(依赖 PendSV 上下文切换),只能用忙等循环替代。这不是 RTOS 该有的样子。
2 根因:Cortex-M 异常优先级陷阱
2.1 异常优先级层级
Cortex-M 用「数值越小优先级越高」:
| 异常 | 优先级 | 说明 |
|---|---|---|
| Reset | -3(固定) | 最高,不可改 |
| NMI | -2(固定) | 不可屏蔽 |
| HardFault | -1(固定) | 硬件故障 |
| SVC | 0(可编程,复位默认) | 系统调用 |
| SysTick | 0xF0(LiteOS 设为最低) | 系统节拍 |
| PendSV | 0xF0(LiteOS 设为最低) | 上下文切换 |
2.2bx r6为什么不工作
看 LiteOS-M 原始的HalStartToRun(los_dispatch.S):
HalStartToRun: ldr r4, =OS_NVIC_SYSPRI2 @ SHPR3:设置 PendSV/SysTick 优先级 ldr r5, =OS_NVIC_PENDSV_PRI str r5, [r4] mov r0, #2 @ CONTROL.SPSEL = 1(切到 PSP) msr CONTROL, r0 ... ldmfd r12!, {R0-R7} @ 手动恢复寄存器 msr psp, r12 @ 设置 PSP cpsie i @ 开中断 bx r6 @ ← 普通跳转,不是异常返回!关键在这一行bx r6:
bx r6是普通跳转,只是把 PC 改到任务入口,不会执行「异常返回」动作。main()是从Reset_Handler里bl main调进来的,整条调用链(Reset → main → LOS_Start → HalStartToRun)都在Reset 异常上下文里执行。- 因为始终没有异常返回,CPU 一直停留在 Reset 异常上下文,异常优先级被压死在-3。
- PendSV 的优先级是0xF0(数值 15),远低于 -3,永远无法抢占,调度器的上下文切换永远不会发生。
一句话:只要 Reset 异常保持活跃(没做异常返回),PendSV 就永远排队等不到执行机会。
另一个细节:
msr CONTROL, #2在 Handler 模式下写SPSEL是无效的(SPSEL 只能在 Thread 模式生效),这也侧面印证了「原实现没真正退出 Handler 模式」。
3 修改后:SVC + PendSV 标准启动流程
这是 Cortex-M 上所有 RTOS 启动第一个任务的标准做法(FreeRTOS 同款)。第⑤篇 5.2.9 结尾列的「后续恢复步骤」三步,本篇逐一落地:
HalStartToRun不再手动恢复寄存器 + 普通跳转,而是设置好优先级后,直接svc 0触发系统调用。HalSVCHandler(SVC 异常处理函数)里,把第一个任务的上下文从 PSP 弹出来,然后bx lr(EXC_RETURN = 0xFFFFFFFD)做真正的异常返回。- CPU 由此退出 Reset 上下文、进入Thread 模式(优先级 0),PendSV 随之解锁。
整个时序如下:
EXC_RETURN 说明
EXC_RETURN是异常返回时放在LR里的特殊值,高 28 位全为 1,低 4 位编码返回目标:
| EXC_RETURN | 含义 |
|---|---|
| 0xFFFFFFF1 | 返回 Handler 模式 + MSP |
| 0xFFFFFFF9 | 返回 Thread 模式 + MSP |
| 0xFFFFFFFD | 返回 Thread 模式 + PSP(基本帧,无 FPU 硬件栈操作) |
| 0xFFFFFFED | 返回 Thread 模式 + PSP(扩展帧,含 FPU) |
这里选择0xFFFFFFFD:因为我们在HalSVCHandler里已经手动恢复了 FPU 寄存器(D8-D15),不需要硬件在异常返回时再处理 FPU 帧,所以用 basic frame 即可。
4 代码改动:逐文件「改前 → 改后」
4.1los_dispatch.S:HalStartToRun重写
改前(bx r6普通跳转):
ldmfd r12!, {R0-R7} msr psp, r12 cpsie i bx r6 @ ← 普通跳转,卡在 Reset 上下文改后(svc 0触发系统调用):
HalStartToRun: ldr r4, =OS_NVIC_SYSPRI2 @ SHPR3 (0xE000ED20) ldr r5, =OS_NVIC_PENDSV_PRI @ 0xF0F00000 str r5, [r4] cpsie i @ 开中断 svc 0 @ 触发 SVCall b . @ 安全网:不可达4.2los_dispatch.S:新增HalSVCHandler
改前:SVC 向量指向HalExcSvcCall(异常处理,不是任务启动)。
改后:新增专门的 SVC 处理函数:
HalSVCHandler: ldr r0, =g_losTask ldr r0, [r0] @ r0 = g_losTask.runTask ldr r0, [r0] @ r0 = runTask->stackPointer ldr r1, =OS_FPU_CPACR @ 判断 FPU 是否使能 ldr r1, [r1] and r1, r1, #OS_FPU_CPACR_ENABLE cmp r1, #OS_FPU_CPACR_ENABLE bne .LSVC_RestoreCore vldmia r0!, {d8-d15} @ FPU:恢复 S16-S31 (64 字节) .LSVC_RestoreCore: ldmia r0!, {r4-r11} @ 恢复 R4-R11 (32 字节) adds r0, r0, #4 @ 跳过 uwPriMask msr psp, r0 @ PSP ← 硬件帧起始 mov r1, #2 msr CONTROL, r1 @ SPSEL=1, FPCA=0 isb mvn lr, #2 @ lr = ~2 = 0xFFFFFFFD bx lr @ 异常返回 → 第一个任务启动!为什么用
mvn lr, #2?因为0xFFFFFFFD = ~0x00000002,MVN用小立即数即可编码,比ldr lr, =0xFFFFFFFD更省一次字面量池访问。
4.3 向量表路由:SVC 指向新 handler
/* los_interrupt.c:g_hwiForm 里的 SVC 向量 */g_hwiForm[SVCall_IRQn+OS_SYS_VECTOR_CNT]=HalSVCHandler;// 改前:HalExcSvcCall/* startup_stm32mp15xx.s:弱别名兜底 */ .weak SVC_Handler .thumb_set SVC_Handler,HalSVCHandler // 改前:Default_Handler同时注释掉stm32mp1xx_it.c里的 C 版SVC_Handler(与 PendSV、SysTick 同处理)。
4.4main.c:从「绕开」到「标准多任务」
改前(第⑤篇临时方案):
staticvoidled_task(void)/* 单任务 + 忙等 */{while(1){LED0(0);LED1(1);for(volatileUINT32 i=0;i<4000000;i++){__NOP();}LED0(1);LED1(0);for(volatileUINT32 i=0;i<4000000;i++){__NOP();}}}intmain(void){...if(LOS_KernelInit()!=LOS_OK){while(1);}led_task();/* 直接调用,绕开 LOS_Start */while(1);}改后(标准多任务):
staticVOID*led0_task(UINT32 arg)/* 红灯:500ms 亮灭,优先级 2 */{(void)arg;while(1){LED0(0);LOS_TaskDelay(500);LED0(1);LOS_TaskDelay(500);}returnNULL;}staticVOID*led1_task(UINT32 arg)/* 绿灯:200ms 亮灭,优先级 3 */{(void)arg;while(1){LED1(0);LOS_TaskDelay(200);LED1(1);LOS_TaskDelay(200);}returnNULL;}intmain(void){...if(LOS_KernelInit()!=LOS_OK){while(1);}TSK_INIT_PARAM_S task_init={0};task_init.pfnTaskEntry=(TSK_ENTRY_FUNC)led0_task;task_init.usTaskPrio=2;task_init.uwStackSize=0x400;if(LOS_TaskCreate(&g_led0_task_id,&task_init)!=LOS_OK){while(1);}task_init.pfnTaskEntry=(TSK_ENTRY_FUNC)led1_task;task_init.usTaskPrio=3;if(LOS_TaskCreate(&g_led1_task_id,&task_init)!=LOS_OK){while(1);}LOS_Start();/* 启动调度器,永不返回 */while(1);}变化:从「1 个忙等任务」变成「2 个
LOS_TaskDelay任务」。两个任务不同优先级、不同周期(500ms vs 200ms),本身就是「调度器真的在切换」的最直观证据。
5 额外的坑:FPU 布局不一致 → UsageFault(第⑤篇没遇到)
做完上面的改动,烧录 + GDB 实测,又暴露了一个更隐蔽的坑——这是第⑤篇临时方案(不经过HalStartToRun的bx r6路径)没有触发的。
5.1 现象
断点HalSVCHandler命中时一切正常(xpsr低 9 位 = 11 = SVC),但单步跨过bx lr后,CPU 没进任务,而是掉进了HalExcUsageFault(IPSR=6)。
5.2 根因:编译时布局 ≠ 运行时状态
关键在于TaskContext 结构体的布局由编译时宏决定,而上下文切换汇编用运行时寄存器检测,两者在 FPU 上不一致:
- 编译时:LiteOS 内核 arch 层的头文件(
los_arch_context.h、los_arch_interrupt.h)只 include 了los_config.h/los_compiler.h,没有 include CMSIS 的core_cm4.h。因此__FPU_PRESENT、__FPU_USED在内核编译单元里是未定义的,TaskContext被编译成68 字节的非 FPU 布局。证据:
$ objdump -d build/m4_liteos.elf | grep -A 5 "<HalTskStackInit>:" 1000a308: 3b44 subs r3, #68 @ sizeof(TaskContext) = 68 字节(非 FPU)运行时:HAL 层的
SystemInit()通过stm32mp1xx_hal.h引入core_cm4.h,__FPU_USED=1,于是使能了 CPACR(FPU)。而汇编HalSVCHandler/HalPendSV在运行时检测 CPACR=0xF00000,走了FPU 路径(vldmia {d8-d15},多偏移 64 字节)。错位:
HalSVCHandler按 FPU 布局算出PSP = context + 100,但TaskContext实际只有 68 字节、硬件帧在context + 36。PSP 偏了 64 字节,指向栈外 → 异常返回弹出垃圾 PC/xPSR →UsageFault。
5.3 修复
在los_config.h的 include 之后显式定义这两个宏:
#ifndef__FPU_PRESENT#define__FPU_PRESENT1#endif#ifndef__FPU_USED#define__FPU_USED1#endif为什么要显式定义?因为 Makefile 用了
-mfloat-abi=hard -mfpu=fpv4-sp-d16,FPU 本就是真实使用的,TaskContext本就应该用 204 字节的 FPU 布局。而内核 arch 层缺 CMSIS 头导致宏丢失,才让布局悄悄缩水成 68 字节。
5.4 修复后的验证
$ objdump -d build/m4_liteos.elf | grep -A 5 "<HalTskStackInit>:" 1000a308: 3bcc subs r3, #204 @ sizeof(TaskContext) = 204 字节(FPU 布局)✓6 验证结果
| 验证项 | 结果 |
|---|---|
| 编译链接 | 0 error |
反汇编HalSVCHandler | 末尾mvn lr, #2+bx lr(EXC_RETURN=0xFFFFFFFD),确认异常返回 |
| GDB 硬件帧 | 8 字与HalTskStackInit初始化一字不差 |
GDBsi跨bx lr | 进入OsTaskEntry,不再掉 UsageFault |
HalPendSV断点 | 反复命中,多任务调度打通 |
| LED 最终效果 | LED0(红)500ms、LED1(绿)200ms 各自独立闪烁 |
对比第⑤篇:当时 LED 是忙等循环驱动的单任务闪烁,节奏由
for循环次数决定;现在换成LOS_TaskDelay,500ms/200ms 是准时的调度延时,且两个任务真正在轮流切换。
(详细的编译、烧录、GDB 调试步骤见工程README.md。)
7 总结:临时方案 vs 正式方案的差距
| 对比项 | 第⑤篇临时方案 | 本篇正式方案 |
|---|---|---|
| 跳转方式 | bx r6普通跳转 | svc 0+bx lr异常返回 |
| 结束状态 | Reset 上下文 (pri -3) | Thread 模式 (pri 0) |
| PendSV | 永久阻塞 | 正常触发 |
| 调度器 | 未启动 | 正常工作 |
LOS_TaskDelay | ❌ 忙等替代 | ✅ 正常工作 |
| 任务数 | 1 个 | 2 个独立切换 |
| FPU 布局 | (未走该路径,未暴露) | 修复 68→204 字节 |
核心教训:
在 Cortex-M 上启动第一个任务,必须通过一次异常返回来退出 Reset 异常上下文,否则 PendSV(以及所有可编程优先级的异常)都会被优先级 -3 的 Reset 压住。SVC 是为此而生的——同步异常、优先级默认最高(0),可先于任何 pending IRQ 被响应,安全完成「从 Reset 上下文到 Thread 模式」的切换。
上下文结构布局(编译时宏)与上下文切换汇编(运行时寄存器检测)必须严格一致。RTOS 里这两处最容易因「宏丢失」而悄悄错位,且只在运行时以 UsageFault/HardFault 的形式爆发。
这也是 FreeRTOS、RT-Thread 等所有主流 RTOS 在 Cortex-M 上启动第一个任务时,不约而同采用 SVC(或等价机制)的根本原因。
—源代码下载链接:https://download.csdn.net/download/xiao089412/93270656
系列目录
⑤ 链接脚本与编译运行(临时方案绕开调度器)
⑤·续篇本篇:SVC + PendSV 标准启动流程,恢复完整多任务调度欢迎评论区交流移植经验,把复杂的讲简单,持续更新中。
标签:
#STM32MP157#LiteOS-M#GCC#Makefile#RTOS#PendSV#SVC#Cortex-M#嵌入式