UCOSIII之STM32上下文切换理解
UCOSIII之STM32上下文切换理解
程序上下文(context)
上下文(context),指的是什么呢,个人可以理解为一个任务或者线程控制的一些变量及CPU的寄存器状态,就是说任务被打断执行以后还可以还原回来。所以上下文就指的是两个操作,被打断任务状态的保存及就绪作务的还原。如果说一个任务的状态完全恢复就指的是,其CPU寄存器的值都被还原,以及其操作的内存及其他外设的状态是一致的,如果说其操作的内存被其他作务或线程改变,那这个就是多线程的一个资源共享及锁定的问题。
STM32 CPU寄存器
CORTEX-M3总共19个寄存器
R0-R15,MSP,PSP,特殊寄存器(三个状态寄存器合用一个32位的寄存器)
那接下来就看下UCOSIII中STM32是如何实现上下文切换的。
PENDSV中断
在发生任务切换时,是由任务激活PENDSV中断(cortex-m3专业用于OS的中断),在中断函数中进行上下文切换。同时这个PENDSV的中断优先级设置的是最低优先级。
在发生中断时,CORTEX-M3自动做了如下操作
1.入栈,把8个寄存器压入栈,R0~R3 R12 LR PC xPSR按一定的顺序入栈保存
自动入栈只是入了部分寄存器,并没将所有的寄存器入栈,应该是考虑到效率跟STACK的使用效率,不过只要在编译的时候,只用到入栈的几个寄存器就不影响使用了,如果要使用到其他寄存器,那么由编译器来负责PUSH与POP,就是C=>汇编时,编译器要做的事情。
2.取向量:即取得中断响应函数地址的PC
3.更新当前的寄存器,更新SP,LR,PC
CORTEX-M3在中断中使用的都是MSP,因此如果任务代码使用的PSP那么,SP内里会被更新为MSP,LR更新为EXC_RETRUN(系统自动生成),而不是函数的返回地址,不过中断返回时也会用到这个被更新的LR,PC改为之前取向量所得到的值。
具体的PENDSV中断操作
OS_CPU_PendSVHandler
;进入中断,此时已保存8个寄存器到SP,任务切换时是使用的PSP,因此自动保存至PSP
CPSID I ; Prevent interruption during context switch
;关中断
MRS R0, PSP ; PSP is process stack pointer
;PSP存入R0
CBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first time
;第一次开始使用的时候直接跳转,之后都不会跳转
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
STM R0, {R4-R11}
;保存R4~R11的寄存器至当前PSP
LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; R0 is SP of process being switched out
;更新PSP的地址为保存寄存器之后的值至当前OSTCBCurPtr
; At this point, entire context of process has been saved
OS_CPU_PendSVHandler_nosave
PUSH {R14} ; Save LR exc_return value
;保存R14(LR),防止调用函数时被覆盖
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
;LDR的伪指令
BLX R0
;调用OSTaskSwHook,钩子函数,当然这里去掉也是不影响系统正常使用,只是少了一个功能
;调用OSTaskSwHook函数时,肯定也会用到一些寄存器,只是这些寄存器的PUSH与POP都由OSTaskSwHook自动来完成
POP {R14}
;还原R14,用于中断返回
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
;更新当前READY的高优先级
LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R1, =OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0]
;更新当前READY的高优先级任务块指针
LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
;R2还是之前的OSTCBCurPtr(更新之后的),不过这里的使用跟OS_TCB的定义有关,OS_TCB的定义是把StkPtr放在最前面,因此这里可以直接把任务的栈地址,直接以这种方式调进来。
LDM R0, {R4-R11} ; Restore r4-11 from new process stack
;还原R4~R11
ADDS R0, R0, #0x20
;更新栈地址
MSR PSP, R0 ; Load PSP with new process SP
;更新PSP地址
ORR LR, LR, #0x04 ; Ensure exception return uses process stack
;将LR相应置位,保证返回时使用的是PSP,不过这个话,只对第一次的任务调用有效
;因为默认栈用的是MSP,而从PSP因中断切换为MSP是,中断返回时会自动跳转到PSP
CPSIE I
;开中断
BX LR ; Exception return will restore remaining context
;中断返回,返回根据当前的PSP来还原起始被PUSH的8个寄存器
END
下面是UCOS启动时的代码
手动将PSP置0,才会有上面OS_CPU_PendSVHandler_nosave跳转只发生一次的情形
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call
MSR PSP, R0
LDR R0, =OS_CPU_ExceptStkBase ; Initialize the MSP to the OS_CPU_ExceptStkBase
LDR R1, [R0]
MSR MSP, R1
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; Enable interrupts at processor level
任务栈初始化
任务栈初始化时,必须将相应的CPU寄存器预留相应的位置,这里面的一些值是起始的任务函数地址,及传入的函数,还有一些寄存器的初始化,只是必须要把栈的地址给空出来,不然任务第一恢复运行的时候,就不会正常工作。
CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK *p_stk_limit,
CPU_STK_SIZE stk_size,
OS_OPT opt)
{
CPU_STK *p_stk;
(void)opt; /* Prevent compiler warning */
p_stk = &p_stk_base[stk_size]; /* Load stack pointer */
/* Registers stacked as if auto-saved on exception */
*--p_stk = (CPU_STK)0x01000000u; /* xPSR */
*--p_stk = (CPU_STK)p_task; /* Entry Point */
*--p_stk = (CPU_STK)OS_TaskReturn; /* R14 (LR) */
*--p_stk = (CPU_STK)0x12121212u; /* R12 */
*--p_stk = (CPU_STK)0x03030303u; /* R3 */
*--p_stk = (CPU_STK)0x02020202u; /* R2 */
*--p_stk = (CPU_STK)p_stk_limit; /* R1 */
*--p_stk = (CPU_STK)p_arg; /* R0 : argument */
/* Remaining registers saved on process stack */
*--p_stk = (CPU_STK)0x11111111u; /* R11 */
*--p_stk = (CPU_STK)0x10101010u; /* R10 */
*--p_stk = (CPU_STK)0x09090909u; /* R9 */
*--p_stk = (CPU_STK)0x08080808u; /* R8 */
*--p_stk = (CPU_STK)0x07070707u; /* R7 */
*--p_stk = (CPU_STK)0x06060606u; /* R6 */
*--p_stk = (CPU_STK)0x05050505u; /* R5 */
*--p_stk = (CPU_STK)0x04040404u; /* R4 */
return (p_stk);
}
个别汇编指令备注
EXC_RETURN详解
这是为什么要使用ORR LR,LR,#0x04这个指令的原因,将EXC_RETURN的第2bit置1,使其返回线程模式,返回后使用PSP,而FREERTOS中并没有使用这个语句,是因为FREERTOS的第一个任务调用跟UCOS的实现方式是不一样,因此可以不使用这个置位操作
中断返回
一般使用BX LR这个指令来返回,但这种返回方式并不是唯一。
上面这个过程就实现了任务的上下文切换,不过关于的任务的调度是没有在PENDSV这个中断中实现的,先找到高优先级的任务再使用PENDSV来进行上下文切换。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律