本篇目标:把“一个函数”变成“一个任务”。原理篇03搞清了异常处理机制,本章用它实现两件事——创建任务(伪造现场)和切换任务(PendSV),对应 stp_rtos 的
stp_hw_stack_init 与 PendSV_Handler。一、先想清楚:任务切换到底在切什么
CPU 执行代码靠寄存器:PC 指向下一条指令,SP 指向当前栈,R0~R12 是运算现场。所谓“任务”,本质就是一套独立的寄存器现场 + 一段独立的栈。
所以切换任务只有三步:
- 把当前任务的寄存器现场存进它的栈;
- 取出下一个任务的栈指针;
- 从下一个任务的栈里恢复寄存器现场。
谁来执行?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:六、小结
一张图串起全流程:
stp_hw_stack_init伪造现场 → 栈里摆好 PC/R0/xPSR;
stp_thread_init把栈指针、入口、优先级装进 TCB;
stp_system_scheduler_start找到最高优先级线程,触发 PendSV;
PendSV_Handler保存上文、恢复下文、异常返回。
核心一句话:任务切换 = 换栈 + 换寄存器现场,PendSV 是执行这个动作的载体。

