Lazy loaded image
原理篇04-任务创建和切换(实践01)
Words 1609Read Time≈ 5 min
2026-9-2
🎯
本篇目标:把“一个函数”变成“一个任务”。原理篇03搞清了异常处理机制,本章用它实现两件事——创建任务(伪造现场)和切换任务(PendSV),对应 stp_rtos 的 stp_hw_stack_init 与 PendSV_Handler。

一、先想清楚:任务切换到底在切什么

CPU 执行代码靠寄存器:PC 指向下一条指令,SP 指向当前栈,R0~R12 是运算现场。所谓“任务”,本质就是一套独立的寄存器现场 + 一段独立的栈。
所以切换任务只有三步:
  1. 把当前任务的寄存器现场存进它的栈;
  1. 取出下一个任务的栈指针;
  1. 从下一个任务的栈里恢复寄存器现场。
谁来执行?PendSV 异常。原理篇03讲过:异常入口硬件自动压栈 R0~R3、R12、LR、PC、xPSR,异常返回硬件自动弹栈,剩下 R4~R11 由软件补上,正好凑齐 16 个寄存器。

二、创建任务:伪造现场

任务第一次运行前,栈里没有“真实现场”。我们要手工在栈里摆一套寄存器值,让 PendSV 第一次弹栈时把任务“弹”进入口函数——这就是伪造现场。

2.1 栈帧结构

stp_rtos 在 cpuport.c 里定义两层结构:
exception_stack_frame 对应硬件自动压栈的 8 个字,stack_frame 再加上软件保存的 R4~R11,两层合起来就是一次完整上下文切换的栈布局。

2.2 stp_hw_stack_init

三个关键值:
寄存器
填什么
为什么
PC
任务入口函数地址
异常返回时 PC 从这里弹出,CPU 从入口函数开始跑
R0
任务参数
入口函数 void entry(void *param) 的形参走 R0
xPSR
0x01000000
bit24 是 T 位(Thumb 状态),必须置 1,否则直接 HardFault
两个细节:
  • 对齐:栈顶先 8 字节对齐再减掉栈帧大小,满足 AAPCS 对齐要求;
  • 0xdeadbeef 填充:把栈填成已知值,栈溢出时一眼就能从内存里认出来,这是调试栈使用量的土办法。
注意 LR 填的是 texit,而 stp_thread_init 实际传 0——意思是任务函数不允许 return,一旦返回就会跳去地址 0 跑飞。

三、任务控制块:把“现场”组织起来

光有栈不够,还要知道“每个任务的栈顶指针存哪”。早期实现用全局数组 task_stacks[] 硬存,任务一多就乱。于是引入任务控制块(TCB):
上下文切换汇编里,stp_interrupt_to_thread 存的是 &thread->sp,切换时 LDR r1, [r1] 直接取到栈指针。所以 sp 必须能被稳定定位——stp_rtos 靠“切换时传的就是 sp 的地址”来保证,而不是 FreeRTOS 那样要求栈指针必须是 TCB 第一个成员。

四、上下文切换:PendSV_Handler 逐段拆

context_gcc.s 里的 PendSV_Handler 是核心,分四段。

4.1 关中断 + 检查标志

4.2 保存上文

4.3 恢复下文

4.4 异常返回

BX lr 之后硬件把剩下 8 个字弹出,PC 指向新任务入口,任务就跑起来了。

五、启动第一个任务

第一个任务没有“上文”可保存,走 stp_hw_context_switch_to,它做四件事:把 from_thread 置 0(PendSV 里据此跳过保存)、把切换标志置 1、设置 PendSV/SysTick 为最低优先级、触发 PendSV:
注意最后 CPSIE I 开中断后 PendSV 才会真正响应——但 PendSV 优先级最低,要等当前所有中断处理完才执行切换。
配合 stp_system_scheduler_start:

六、小结

一张图串起全流程:
  1. stp_hw_stack_init 伪造现场 → 栈里摆好 PC/R0/xPSR;
  1. stp_thread_init 把栈指针、入口、优先级装进 TCB;
  1. stp_system_scheduler_start 找到最高优先级线程,触发 PendSV;
  1. PendSV_Handler 保存上文、恢复下文、异常返回。
核心一句话:任务切换 = 换栈 + 换寄存器现场,PendSV 是执行这个动作的载体。
上一篇
C++ String 常用操作
下一篇
嵌入式工程师习题集

Comments
Loading...