ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/8/17 0:12:20 拓冰建站 浏览量
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)
HalStartToRunbx r6(普通跳转)cpsie i+svc 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 不闪。

当时的临时对策(第⑤篇修复 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(固定)硬件故障
SVC0(可编程,复位默认)系统调用
SysTick0xF0(LiteOS 设为最低)系统节拍
PendSV0xF0(LiteOS 设为最低)上下文切换

2.2bx r6为什么不工作

看 LiteOS-M 原始的HalStartToRunlos_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

  1. bx r6是普通跳转,只是把 PC 改到任务入口,不会执行「异常返回」动作。
  2. main()是从Reset_Handlerbl main调进来的,整条调用链(Reset → main → LOS_Start → HalStartToRun)都在Reset 异常上下文里执行。
  3. 因为始终没有异常返回,CPU 一直停留在 Reset 异常上下文,异常优先级被压死在-3
  4. PendSV 的优先级是0xF0(数值 15),远低于 -3,永远无法抢占,调度器的上下文切换永远不会发生。

一句话:只要 Reset 异常保持活跃(没做异常返回),PendSV 就永远排队等不到执行机会。

另一个细节:msr CONTROL, #2在 Handler 模式下写SPSEL无效的(SPSEL 只能在 Thread 模式生效),这也侧面印证了「原实现没真正退出 Handler 模式」。


3 修改后:SVC + PendSV 标准启动流程

这是 Cortex-M 上所有 RTOS 启动第一个任务的标准做法(FreeRTOS 同款)。第⑤篇 5.2.9 结尾列的「后续恢复步骤」三步,本篇逐一落地:

  1. HalStartToRun不再手动恢复寄存器 + 普通跳转,而是设置好优先级后,直接svc 0触发系统调用。
  2. HalSVCHandler(SVC 异常处理函数)里,把第一个任务的上下文从 PSP 弹出来,然后bx lrEXC_RETURN = 0xFFFFFFFD)做真正的异常返回
  3. 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.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 @ SPSEL=1, FPCA=0 isb mvn lr, #2 @ lr = ~2 = 0xFFFFFFFD bx lr @ 异常返回 → 第一个任务启动!

为什么用mvn lr, #2?因为0xFFFFFFFD = ~0x00000002MVN用小立即数即可编码,比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 实测,又暴露了一个更隐蔽的坑——这是第⑤篇临时方案(不经过HalStartToRunbx r6路径)没有触发的。

5.1 现象

断点HalSVCHandler命中时一切正常(xpsr低 9 位 = 11 = SVC),但单步跨过bx lr后,CPU 没进任务,而是掉进了HalExcUsageFault(IPSR=6)。

5.2 根因:编译时布局 ≠ 运行时状态

关键在于TaskContext 结构体的布局由编译时宏决定,而上下文切换汇编用运行时寄存器检测,两者在 FPU 上不一致:

  1. 编译时:LiteOS 内核 arch 层的头文件(los_arch_context.hlos_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)
  1. 运行时:HAL 层的SystemInit()通过stm32mp1xx_hal.h引入core_cm4.h__FPU_USED=1,于是使能了 CPACR(FPU)。而汇编HalSVCHandler/HalPendSV在运行时检测 CPACR=0xF00000,走了FPU 路径vldmia {d8-d15},多偏移 64 字节)。

  2. 错位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初始化一字不差
GDBsibx lr进入OsTaskEntry,不再掉 UsageFault
HalPendSV断点反复命中,多任务调度打通
LED 最终效果LED0(红)500ms、LED1(绿)200ms 各自独立闪烁

对比第⑤篇:当时 LED 是忙等循环驱动的单任务闪烁,节奏由for循环次数决定;现在换成LOS_TaskDelay500ms/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 字节

核心教训

  1. 在 Cortex-M 上启动第一个任务,必须通过一次异常返回来退出 Reset 异常上下文,否则 PendSV(以及所有可编程优先级的异常)都会被优先级 -3 的 Reset 压住。SVC 是为此而生的——同步异常、优先级默认最高(0),可先于任何 pending IRQ 被响应,安全完成「从 Reset 上下文到 Thread 模式」的切换。

  2. 上下文结构布局(编译时宏)与上下文切换汇编(运行时寄存器检测)必须严格一致。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#嵌入式