——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 三条启动路径总览
方案A:直接调用任务(绕开LOS_Start) | 方案B:SVC触发异常返回(上篇改造方案,FreeRTOS/RT‑Thread主流) | 方案C:原生路径LOS_Start → HalStartToRun(LiteOS‑M当前工程) | |
|---|---|---|---|
| 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项关键运行条件全部缺失:
- PendSV优先级没有设置,保持默认值
CONTROL.SPSEL=0,持续使用MSP主栈,没有切换PSP任务栈- PSP寄存器未初始化,PendSV上下文切换读取垃圾值
TaskContext任务栈硬件帧未做恢复弹出- 中断状态不可控,SysTick、PendSV无法抢占
即便调用LOS_TaskDelay()触发PendSV挂起,也会触发HardFault或者系统卡死。
本质:只是普通函数调用,没有构建RTOS任务运行环境。
方案B:SVC触发异常返回(上篇的改造方案)——业界主流通用实现
FreeRTOS、RT‑Thread等主流RTOS均采用该套思路。该方案优势是不假设调用方CPU处于哪种运行模式,Handler、Thread上下文均可正常工作。
上篇移植故障场景中,启动代码意外运行在Handler模式。Handler模式硬件限制:直接修改CONTROL寄存器SPSEL位不会生效。
采用SVC完整流程:
- 执行
svc #0触发SVC异常,CPU进入SVC Handler; - Handler内部准备
EXC_RETURN = 0xFFFFFFFD; bx lr执行异常返回,硬件自动完成三件事:切Thread模式、切换PSP、硬件完整恢复R0‑R3/R12/LR/PC/xPSR硬件栈帧。
这套方案兼容性很强,无论调用方来自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把上述动作拆解为普通汇编指令完成:
- Thread模式直接写
CONTROL.SPSEL=1切换PSP; msr psp手动设置任务栈指针;ldmfd手动加载硬件帧内容;cpsie i开中断,普通bx跳转进入任务。
✨手动方案与异常返回的本质差别:
异常返回会真实写回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 核心结论总结
- SVC不是Cortex‑M硬件强制的必选操作,是否选用取决于调用
HalStartToRun时刻CPU所处模式(IPSR寄存器)- 运行在Handler模式:
msr CONTROL修改SPSEL会被硬件忽略,必须借助SVC触发异常返回完成堆栈与模式切换; - 运行在Thread模式:直接配置
CONTROL、PSP,手动加载栈帧,普通bx跳转即可启动任务,无需SVC。
- 运行在Handler模式:
- 方案B(SVC)为商用RTOS主流选择,兼容任意调用上下文;LiteOS‑M方案C属于轻量化实现,依赖「调用方处于Thread模式」这个硬件前置条件。
- 首任务启动不强制异常返回;任务之间PendSV上下文切换必须依靠异常返回。
- FPU相关编译宏会改变
TaskContext结构体尺寸,C语言结构体必须和汇编硬编码栈偏移严格对齐,否则会出现栈错位触发UsageFault异常。
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架构规范,只是产品定位与设计取舍不一样,不存在绝对优劣。
github 源码下载地址:https://gitee.com/tstcoder/stm32mp157-liteos-m/tree/v3.0-native-pendsv
系列文章索引
- LiteOS‑M移植①:环境搭建、编译链接脚本
- LiteOS‑M移植⑤:链接脚本与基础运行
- LiteOS‑M PendSV阻塞修复:从绕开调度器到SVC+PendSV标准启动
- 本篇:再辨析:启动第一个任务到底需不需要SVC?