LiteOS-M PendSV 阻塞修复:从「绕开调度器」到「SVC + PendSV 标准启动」(LiteOS-M 移植⑤·续篇)
LiteOS-M PendSV 阻塞修复:SVC + PendSV 标准启动流程与 FPU 布局陷阱(LiteOS-M 移植⑤·续篇)
系列定位:STM32MP157 M4 的 LiteOS-M 移植系列(Windows 纯 GCC + Makefile,不做 Keil)。
本篇是[第⑤篇「链接脚本与编译运行」]的续篇——第⑤篇结尾用「直接调用led_task()绕开LOS_Start()」的临时方案让 LED 先闪起来,本篇把它升级为 SVC + PendSV 标准启动流程,真正恢复多任务调度。
摘要:本文解决第⑤篇遗留的调度器启动问题——
LOS_Start()卡死、任务从未被调度。核心修复是通过svc 0触发 SVC 异常,再以bx lr(EXC_RETURN=0xFFFFFFFD)真正退出 Reset 上下文,进入 Thread 模式解锁 PendSV。同时修复了 FPU 宏丢失导致的 TaskContext 布局错位(68→204 字节),最终实现双任务独立闪烁。
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.2 bx 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.1 los_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.2 los_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.4 main.c:从「绕开」到「标准多任务」
改前(第⑤篇临时方案):
static void led_task(void) /* 单任务 + 忙等 */
{
while (1) {
LED0(0); LED1(1);
for (volatile UINT32 i = 0; i < 4000000; i++) { __NOP(); }
LED0(1); LED1(0);
for (volatile UINT32 i = 0; i < 4000000; i++) { __NOP(); }
}
}
int main(void)
{
...
if (LOS_KernelInit() != LOS_OK) { while (1); }
led_task(); /* 直接调用,绕开 LOS_Start */
while (1);
}
改后(标准多任务):
static VOID *led0_task(UINT32 arg) /* 红灯:500ms 亮灭,优先级 2 */
{
(void)arg;
while (1) { LED0(0); LOS_TaskDelay(500); LED0(1); LOS_TaskDelay(500); }
return NULL;
}
static VOID *led1_task(UINT32 arg) /* 绿灯:200ms 亮灭,优先级 3 */
{
(void)arg;
while (1) { LED1(0); LOS_TaskDelay(200); LED1(1); LOS_TaskDelay(200); }
return NULL;
}
int main(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_PRESENT 1
#endif
#ifndef __FPU_USED
#define __FPU_USED 1
#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 初始化一字不差 |
GDB si 跨 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(或等价机制)的根本原因。
📌 重要后期勘误(代码复盘最终修正)
本文实现的 SVC + 异常返回启动方案,是 Cortex‑M 主流 RTOS 的标准通用启动方式,并非临时补救方案。该方案专门解决 HalStartToRun 在 Handler 异常模式执行 的场景:此时 CPU 滞留 Reset 异常上下文,普通 bx r6 跳转无法切换堆栈与运行模式,PendSV 会被高优先级异常永久阻塞,必须依靠 SVC 异常返回完成模式切换与栈帧恢复。
而 LiteOS‑M 原生默认流程(Reset_Handler → main (Thread 模式) → LOS_Start → HalStartToRun)全程运行在 Thread 模式下。在该合法前置条件下,CPU 可直接修改 CONTROL、PSP 寄存器,通过普通跳转即可正常拉起首个任务,无需 SVC 参与。
两种实现均符合 ARM 架构规范:
SVC 方案:通用性极强,兼容任意调用上下文,是 FreeRTOS、RT‑Thread 等商用 RTOS 的统一标准;
直接跳转方案:轻量化精简实现,依赖「必须在 Thread 模式启动调度器」的内核约束。
无论使用哪种启动方式,FPU 编译宏、C 语言 TaskContext 结构体大小、汇编栈偏移三者必须严格对齐,否则会出现栈布局错乱,稳定触发 UsageFault 异常。
—源代码下载链接:https://download.csdn.net/download/xiao089412/93270656
github 下载地址:https://gitee.com/tstcoder/stm32mp157-liteos-m/tree/v2.0-svc
系列目录
⑤ 链接脚本与编译运行(临时方案绕开调度器)
⑤·续篇 本篇:SVC + PendSV 标准启动流程,恢复完整多任务调度
欢迎评论区交流移植经验,把复杂的讲简单,持续更新中。
标签:
#STM32MP157#LiteOS-M#GCC#Makefile#RTOS#PendSV#SVC#Cortex-M#嵌入式
更多推荐



所有评论(0)