资讯动态

LiteOS-M PendSV 阻塞修复:从「绕开调度器」到「SVC + PendSV 标准启动」(LiteOS-M 移植⑤·续篇)

发布时间:2026/8/16 23:57:24 来源:尧图企业网站定制
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)HalStartToRunbx r6普通跳转cpsie isvc 0触发系统调用SVC 处理SVC_Handler Default_Handler空新增HalSVCHandlerbx 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 不闪。当时的临时对策第⑤篇修复 3if(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固定硬件故障SVC0可编程复位默认系统调用SysTick0xF0LiteOS 设为最低系统节拍PendSV0xF0LiteOS 设为最低上下文切换2.2bx r6为什么不工作看 LiteOS-M 原始的HalStartToRunlos_dispatch.SHalStartToRun: 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 r6bx 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触发系统调用。HalSVCHandlerSVC 异常处理函数里把第一个任务的上下文从 PSP 弹出来然后bx lrEXC_RETURN 0xFFFFFFFD做真正的异常返回。CPU 由此退出 Reset 上下文、进入Thread 模式优先级 0PendSV 随之解锁。整个时序如下EXC_RETURN 说明EXC_RETURN是异常返回时放在LR里的特殊值高 28 位全为 1低 4 位编码返回目标EXC_RETURN含义0xFFFFFFF1返回 Handler 模式 MSP0xFFFFFFF9返回 Thread 模式 MSP0xFFFFFFFD返回 Thread 模式 PSP基本帧无 FPU 硬件栈操作0xFFFFFFED返回 Thread 模式 PSP扩展帧含 FPU这里选择0xFFFFFFFD因为我们在HalSVCHandler里已经手动恢复了 FPU 寄存器D8-D15不需要硬件在异常返回时再处理 FPU 帧所以用 basic frame 即可。4 代码改动逐文件「改前 → 改后」4.1los_dispatch.SHalStartToRun重写改前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 SPSEL1, FPCA0 isb mvn lr, #2 lr ~2 0xFFFFFFFD bx lr 异常返回 → 第一个任务启动为什么用mvn lr, #2因为0xFFFFFFFD ~0x00000002MVN用小立即数即可编码比ldr lr, 0xFFFFFFFD更省一次字面量池访问。4.3 向量表路由SVC 指向新 handler/* los_interrupt.cg_hwiForm 里的 SVC 向量 */g_hwiForm[SVCall_IRQnOS_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 i0;i4000000;i){__NOP();}LED0(1);LED1(0);for(volatileUINT32 i0;i4000000;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.usTaskPrio2;task_init.uwStackSize0x400;if(LOS_TaskCreate(g_led0_task_id,task_init)!LOS_OK){while(1);}task_init.pfnTaskEntry(TSK_ENTRY_FUNC)led1_task;task_init.usTaskPrio3;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 没进任务而是掉进了HalExcUsageFaultIPSR6。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_USED1于是使能了 CPACRFPU。而汇编HalSVCHandler/HalPendSV在运行时检测 CPACR0xF00000走了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-abihard -mfpufpv4-sp-d16FPU 本就是真实使用的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, #2bx lrEXC_RETURN0xFFFFFFFD确认异常返回GDB 硬件帧8 字与HalTskStackInit初始化一字不差GDBsi跨bx lr进入OsTaskEntry不再掉 UsageFaultHalPendSV断点反复命中多任务调度打通LED 最终效果LED0红500ms、LED1绿200ms 各自独立闪烁对比第⑤篇当时 LED 是忙等循环驱动的单任务闪烁节奏由for循环次数决定现在换成LOS_TaskDelay500ms/200ms 是准时的调度延时且两个任务真正在轮流切换。详细的编译、烧录、GDB 调试步骤见工程README.md。7 总结临时方案 vs 正式方案的差距对比项第⑤篇临时方案本篇正式方案跳转方式bx r6普通跳转svc 0bx 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#嵌入式

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价