——STM32MP157 M4核移植复盘,为什么原生HalStartToRun不用SVC也能完成PendSV调度
系列:LiteOS‑M移植(Windows+GCC+Makefile,无Keil)
前置阅读:《LiteOS‑M PendSV阻塞修复:从绕开调度器到SVC+PendSV标准启动》
0 前言
上一篇博客移植过程遇到故障:绕开LOS_Start()直接调用任务会造成调度卡死,当时实现了SVC+异常返回的启动方案。当时形成的认知是Cortex‑M启动首个任务必须依靠SVC异常返回退出Reset上下文。
后续深入研读原生工程汇编代码后发现一个值得探讨的问题:LiteOS‑M原生HalStartToRun并没有使用SVC,同样可以正常完成任务创建、PendSV抢占调度。
本篇做原理辨析:SVC究竟是启动首任务的硬件硬性标准,还是面向通用场景的高兼容性可选实现。结合汇编代码、CPU Thread/Handler模式、三种启动路径,理清不同RTOS做出不同设计选择的底层原因。

图注:图① 方案A/B/C三种首任务启动方案总览对比
1 三条启动路径总览
| PendSV优先级 | 未配置 | SVC Handler内配置 | HalStartToRun汇编配置 |
| CONTROL/PSP | 完全未初始化 | EXC_RETURN硬件自动切换 | 汇编手动msr CONTROL + msr psp |
| 硬件栈帧恢复 | 缺失 | 异常返回硬件自动恢复 | ldmfd手动加载硬件帧内容 |
| 中断使能 | 未正确打开 | SVC Handler打开 | cpsie i汇编指令打开 |
| 进入任务方式 | C语言直接call | bx lr异常返回 | bx r6普通跳转 |
| 是否需要SVC | 不需要 | 需要 | 不需要 |
方案A:直接调用任务(绕开LOS_Start)——失败
LOS_KernelInit();
led_task(); // 直接调用,跳过LOS_Start()
这是第⑤篇的临时应急手段,HalStartToRun完全没有执行,5项关键运行条件全部缺失:
即便调用LOS_TaskDelay()触发PendSV挂起,也会触发HardFault或者系统卡死。
本质:只是普通函数调用,没有构建RTOS任务运行环境。
方案B:SVC触发异常返回(上篇的改造方案)——业界主流通用实现
FreeRTOS、RT‑Thread等主流RTOS均采用该套思路。该方案优势是不假设调用方CPU处于哪种运行模式,Handler、Thread上下文均可正常工作。
上篇移植故障场景中,启动代码意外运行在Handler模式。Handler模式硬件限制:直接修改CONTROL寄存器SPSEL位不会生效。
采用SVC完整流程:
这套方案兼容性很强,无论调用方来自Handler或者Thread模式都可以正常工作。上篇移植正是利用该特性解决了上下文异常带来的调度故障。
SVC方案不是错误补丁,是商用RTOS优先选择的高鲁棒性实现;只是在本工程原生Thread上下文场景下,不属于必需。

图注:图② 方案B:SVC+异常返回完整执行流程
方案C:原生路径 LOS_Start → HalStartSchedule → HalStartToRun → bx r6 OsTaskEntry(LiteOS‑M,无需SVC)
完整调用链:
Reset_Handler → main()【Thread模式】→ LOS_Start() → HalStartSchedule() → HalStartToRun() → bx r6 跳OsTaskEntry
⚠硬件隐式前置条件:LOS_Start函数内部没有任何代码做模式判断与断言拦截,能够正常切换PSP的前提是调用方已经处于Thread模式。
因为ARMv7‑M硬件行为:Handler模式下写CONTROL寄存器SPSEL位会被硬件静默忽略。如果从Handler上下文调用LOS_Start,不会触发报错,只是堆栈切换失效,继续跑在MSP主栈。
HalStartToRun汇编关键逻辑节选:
HalStartToRun:
ldr r4, =OS_NVIC_SYSPRI2 @① 设置PendSV、SysTick优先级,两者均配置为0xF0最低优先级
ldr r5, =OS_NVIC_PENDSV_PRI
str r5, [r4]
mov r0, #2
msr CONTROL, r0 @② Thread模式下,SPSEL=1,切换使用PSP
……
ldmfd r12!, {R0‑R7} @③ 从任务栈弹出硬件帧内容(汇编落点R0‑R7)
msr psp, r12 @④ 设置PSP为任务栈顶
vpush {s0}; vpop {s0} @⑤ 激活FPCA标志,异常自动保存FPU寄存器
cpsie i @⑥ 开启全局中断
bx r6 @⑦ 普通跳转进入任务入口 OsTaskEntry
核心洞察:Cortex‑M Thread模式,不需要异常返回就可以直接使用PSP。
这里手动恢复 R0‑R3/R12/LR/PC;xPSR仅读到R7寄存器,并不会写回真实xPSR状态寄存器。
Thumb模式依靠bx r6跳转目标地址bit0位保证,和异常返回硬件自动恢复完整xPSR存在本质区别。
SVC异常返回达成的效果:切Thread模式 + 切换PSP + 硬件完整恢复硬件栈帧。
原生HalStartToRun把上述动作拆解为普通汇编指令完成:
✨手动方案与异常返回的本质差别:
异常返回会真实写回xPSR并硬件强制把CPU切回Thread模式;
手动方案依赖已经处于Thread模式 + bx跳转地址的Thumb bit0位来等价实现,因此可以省去SVC一圈异常进出。
运行效果与SVC异常返回完全等价:任务运行在Thread模式+PSP任务栈,SysTick、PendSV可正常抢占。
后续调度闭环:任务内部调用LOS_TaskDelay → 设置PENDSVSET触发PendSV异常 → HalPendSV保存上下文到PSP,加载下一个任务,依靠bx lr(EXC_RETURN)异常返回切换新任务。
注意:任务之间PendSV上下文切换,必须依靠异常返回机制,这一点两种方案完全一致。

图注:图③ 方案C LiteOS‑M原生首任务启动执行流程
3 勘误回顾
📌勘误回顾(对应上篇博文补充)
上篇采用的SVC启动方案是业界通用可靠实现,兼容性强。当启动代码运行在Handler模式时,SVC异常返回是必要手段。
在本工程标准原生流程中,main运行在Thread模式,HalStartToRun直接操作寄存器即可完成全部初始化,SVC不是必需。两种实现都符合Cortex‑M架构规范,区别来自硬件前置条件,内核本身没有做运行时模式校验。
4 核心结论总结
- 运行在Handler模式:msr CONTROL修改SPSEL会被硬件忽略,必须借助SVC触发异常返回完成堆栈与模式切换;
- 运行在Thread模式:直接配置CONTROL、PSP,手动加载栈帧,普通bx跳转即可启动任务,无需SVC。
5 延伸思考:为什么 FreeRTOS、RT‑Thread 普遍采用SVC启动首任务?
这也是 FreeRTOS、RT‑Thread 等所有主流 RTOS 在 Cortex‑M 上启动第一个任务时,不约而同采用 SVC(或等价机制)的根本原因。

图注:图④ 不同RTOS首任务启动方案选型逻辑对比
主流RTOS不做严苛的前置假设:不保证调度启动函数一定在main的Thread模式下调用,允许从异常Handler上下文启动内核。
- 如果从Handler模式启动内核,直接修改CONTROL.SPSEL会被硬件忽略,LiteOS‑M方案C直接失效;
- SVC方案可以同时兼容Handler、Thread两种上下文,无论从何处调用,都能可靠完成模式切换、栈帧恢复,鲁棒性更高。
而 LiteOS‑M 的原生实现做了简化取舍,依赖系统从main(Thread)进入调度器的标准启动流程,在该前提之下,就可以省略SVC,直接操作寄存器完成首任务启动。
小结
- 方案B(SVC):通用无上下文假设 → FreeRTOS / RT‑Thread 主流选型;
- 方案C(直接寄存器操作):轻量化,依赖调用方处于Thread模式 → LiteOS‑M原生实现。
两种实现均符合ARMv7‑M架构规范,只是产品定位与设计取舍不一样,不存在绝对优劣。
系列文章索引




