公众号:嵌入式不难
本文仅供参考学习,如有错误之处,欢迎留言指正。
一、源码呈现
-
os_cpu_a.s文件
开始运行操作系统部分,开始操作系统首先会调用OSStart(&err)->OSStartHighRdy()而OSStartHighRdy()是由下面的汇编组成。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 OSStartHang B OSStartHang ; Should never get here
内核调度部分
OS_CPU_PendSVHandler CPSID I ; Prevent interruption during context switch MRS R0, PSP ; PSP is process stack pointer 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} LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP; LDR R1, [R1] STR R0, [R1] ; R0 is SP of process being switched out ; At this point, entire context of process has been saved OS_CPU_PendSVHandler_nosave PUSH {R14} ; Save LR exc_return value LDR R0, =OSTaskSwHook ; OSTaskSwHook(); BLX R0 POP {R14} LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy; LDR R1, =OSPrioHighRdy LDRB R2, [R1] STRB R2, [R0] LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr; LDR R1, =OSTCBHighRdyPtr LDR R2, [R1] STR R2, [R0] LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr; LDM R0, {R4-R11} ; Restore r4-11 from new process stack ADDS R0, R0, #0x20 MSR PSP, R0 ; Load PSP with new process SP ORR LR, LR, #0x04 ; Ensure exception return uses process stack CPSIE I BX LR ; Exception return will restore remaining context
-
os_cpu_c.c文件
创建任务初始化时需要调用的一个函数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); }
二、疑问呈现
- 怎么实现调度的?
- 为什么内核文件并没有看到跳转到跳转至某个程序或代码处,然而就切换到程序或某一处去执行了?
- CM3内核在运行UCOS的时候产生中断,那么产生中断后使用的是MSP还是PSP,如果使用的是MSP。那么在中断结束的情况下,怎么将SP指针再次设置成PSP呢?
- 剩余疑问我会在下述解析中再陈述。
三、解析呈现
1.基础知识
- 根据CM3与CM4权威指南第八章深入了解异常处理:
- R0 ~ R3,R12,R14(LR),以及PSR被称作“调用者保存寄存器”
- R4 ~ R11为“被调用者保存寄存器”
- 具有浮点单元的
- S0 ~ S15为 “调用者保存寄存器”
- S16 ~ S31为“被调用者保存寄存器”
- 一般来说:R0~R3作为输入参数,R0用作返回结果,若返回值为64位,则R1也用于返回结果
- 异常机制需要在异常入口处自动保存R0 ~ R3,R12,R14(LR),以及PSR,并在异常退出时将他们恢复,这些都要由硬件处理器控制,对于具有浮点单元的M4处理器。则异常机制需要保存S0 ~ S15。
所以由以上我们可知:如果在多个函数或者中断中有使用到R4 ~ R11和S16 ~ S31这些寄存器。那么在异常处理的时候还应该加上对这些寄存器的保存,防止在异常或函数调用退出时这些寄存器的值改变导致函数功能的改变。这里我们会想到为什么不保存SP呢?这个也很重要啊,关系到堆栈,答案会在下方揭晓。
- 根据CM3与CM4权威指南第四章架构可知特殊寄存器
由此上图可以知道CONTROL寄存器直接掌控着到底是使用MSP还是PSP,然后我们知道在UCOS运行的时候使用的PSP栈,而进入中断的时候自己切换成MSP,但是要从PSP过渡到MSP必然意味着要改变CONTROL寄存器的值,另外,之前说的异常处理机制需要自动压栈R0 ~ R3,R12,R14(LR),以及PSR或者R0 ~ R3,R12,R14(LR),以及PSR和S0 ~ S15,这里压栈是压入PSP还是MSP呢?然后等到异常处理完毕后,又是怎么切换到MSP的呢?然后我又继续翻阅权威指南。答案就在下一个知识点揭晓。
- 根据CM3与CM4权威指南第八章深入了解异常处理:
- 处理器进入异常处理或者中断服务(ISR)时,链接寄存器(LR)的值会被更新为EXC_RETURN数值。当使用BX,POP或者存储加载指令(LDR或LDM)被加载到程序寄存器时,该数值用于触发异常返回机制。具体定义见下图。
- 处理器进入异常处理或者中断服务(ISR)时,链接寄存器(LR)的值会被更新为EXC_RETURN数值。当使用BX,POP或者存储加载指令(LDR或LDM)被加载到程序寄存器时,该数值用于触发异常返回机制。具体定义见下图。
一看这个图就明白了一点,ARM果然聪明,已经知道我们会遇到什么问题了,所以在进入中断的时候先帮我们把上一次使用的情况给记录下来,你上一次到底使用的是MSP还是PSP,处于线程模式还是处理模式,我先记着,等你处理完了我告诉PC,PC一看是个EXC_RETURN值,PC就知道不是正常的执行指令,会先去解决一下情况,把PSP还是MSP决定好,再回来处理后续事情。这也解答了为什么压栈不压入SP了,但是细细一想,似乎上一问题还有一个小问题没有解决啊,我是知道了你会把PSP和MSP分的清清楚楚,但是你保存R0 ~ R3,R12,R14(LR),以及PSR和S0 ~ S15时压栈是压到哪儿去了呢?又是咋个区分的呢?继续阅读权威指南,下一图会解答这个疑问。
1.线程模式也在使用MSP的情况下(就是我们平时用的前后台)
分析:第一个中断来临的时候,LR = 0xFFFF FFF9,分析一下后四位,9 = 1001,对照上面的表表示返回线程返回主栈(MSP),第二个中断来的时候 LR = 0xFFFF FFF1,分析一下后四位,1 = 0001,返回处理模式,返回主栈(MSP),我就有点纳闷了,都返回主栈还可以理解,为什么要把线程模式和处理模式要分的这个开呢,原因就在于处理模式都是特权级的,而线程模式有特权级和用户级之分,这两者的区别在于用户级不能使某些指令,一些情况下不能访问某些内存区域。具体见权威指南。
2.线程模式使用PSP的情况下(UCOS使用的情况)
分析:第一个中断来临的时候,LR = 0xFFFF FFFD,分析一下后四位,9 = 1101,对照上面的表表示返回线程返回进程栈(PSP),第二个中断来的时候 LR = 0xFFFF FFF1,分析一下后四位,1 = 0001,返回处理模式,返回主栈(MSP)。
3.出栈操作
分析:在每次出栈操作结束后,处理器还会检查xPSR数值的第九位,并且压栈时插入了额外的空间则会将其去除。
总结:原来在运行时产生中断了我不管你现在是PSP还是MSP,我先压入栈再说,等到我压栈完成了,我才来记录一下我刚才压栈时压的PSP或MSP栈,把这个记录先保存到LR寄存器,压栈完成后,进入中断服务程序,开始使用MSP主栈,我也不管你之前用的是什么栈,现在开始全部使用MSP,这个时候里面有需要用到栈的情况,全部压入MSP,等到我中断函数执行完了,我就把刚才LR中的值放到PC中去,PC一看,特殊数据啊,所以就从LR记录的信息判断是从PSP还是MSP出栈,一下子就找到了应该出栈的位置了。中断嵌套也是一样的道理。
2.源码分析
-
os_cpu_a.s文件
开始运行操作系统部分,开始操作系统首先会调用OSStart(&err); ->OSStartHighRdy();而OSStartHighRdy();是由下面的汇编组成。OSStartHighRdy ;将 NVIC_SYSPRI14 的值读入 R0 LDR R0, =NVIC_SYSPRI14 ;R0=NVIC_SYSPRI14 ;Set the PendSV exception priority ;将 NVIC_PENDSV_PRI 的值读入 R1 LDR R1, =NVIC_PENDSV_PRI ;R1=NVIC_PENDSV_PRI ;将 R1 中的字节数据 写入 R0地址的存储器中 STRB R1, [R0] ;*NVIC_SYSPRI14 = NVIC_PENDSV_PRI ;功能1:以上的目的为设置PendSV异常优先级最低 ;将0写入R0并更新APSR寄存器 MOVS R0, #0 ;R0=0 ;Set the PSP to 0 for initial context switch call ;将R0中的值写入PSP MSR PSP, R0 ;PSP=0 ;功能2:以上为设置PSP初始为0,告诉OS这是第一次运行系统 ;将 OS_CPU_ExceptStkBase 的值读入R0 LDR R0, =OS_CPU_ExceptStkBase ;R0=&OS_CPU_ExceptStkBase ;Initialize the MSP to the OS_CPU_ExceptStkBase ;将OS_CPU_ExceptStkBase地址存储的值读入R1 LDR R1, [R0] ;R1=*(&OS_CPU_ExceptStkBase)-->R1=OS_CPU_ExceptStkBase ;将R1中的值写入MSP MSR MSP, R1 ;MSP=OS_CPU_ExceptStkBase ;功能3:以上为设置主堆栈指针 ;将 NVIC_INT_CTRL 的值 读入 R0 LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch) ;将 NVIC_PENDSVSET 的值 读入 R1 LDR R1, =NVIC_PENDSVSET ;将 R1 中的字数据写入 R0 地址的存储器中 STR R1, [R0] ;功能4:以上为触发PendSV异常 ;使能中断(清除PRIMASK)和_enable_irq()相同 CPSIE I ; Enable interrupts at processor level OSStartHang B OSStartHang ; Should never get here
内核调度部分
OS_CPU_PendSVHandler ;禁止中断(设置PRIMASK),NIM和HardFault不受影响和_disable_irq()相同 CPSID I ; Prevent interruption during context switch ;R0=PSP 将PSP内容复制到R0 MRS R0, PSP ; PSP is process stack pointer ;CBZ比较为0则跳转,将 R0 与 0 比较,若为真则跳转至 OS_CPU_PendSVHandler_nosave 地址处代码执行 CBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first time ;R0=R0-0x20=PSP-0x20 ;此运算更新 APSR 寄存器。这一步的目的是什么呢 SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack ;将 R4至R11 的寄存器的值保存至 R0 指向的地址中 STM R0, {R4-R11} ;这两步大家可能会有疑问,为什么R0要在PSP的基础上减0x20才开始保存呢,原因就在于堆栈是 ;由上向下生长的,但是STM存储的方式却是由下向上生长,那么疑问又来了,既然存储方式不一 ;样,那我使用PUSH指令不好吗,值得注意的是现在是在中断中,我们保存信息需要保存到PSP, ;现在使用POP指令肯定是保存在MSP中的,达不到效果 ;R1=&OSTCBCurPtr ,将 OSTCBCurPtr 的值读入 R1 中 LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP; ;R1=*(&OSTCBCurPtr)=OSTCBCurPtr 将 R1 所指向的存储器的值读入 R1 中 LDR R1, [R1] ;*OSTCBCurPtr=R0=PSP-0x20 ;将 R0 的值按字写入 R1 为地址的存储器中 STR R0, [R1] ; R0 is SP of process being switched out ; At this point, entire context of process has been saved OS_CPU_PendSVHandler_nosave ;将 R14 压入栈内 PUSH {R14} ; Save LR exc_return value ;R0=&OSTaskSwHook ;将 OSTaskSwHook 读入 R0 LDR R0, =OSTaskSwHook ; OSTaskSwHook(); ;程序跳转到 R0 地址值处执行 BLX R0 ;出栈,恢复上一次入栈的R14值,并存入 R14 POP {R14} ;R0=&OSPrioCur ;将 OSPrioCur 按字读入 R0 LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy; ;R1=&OSPrioHighRdy ;将 OSPrioHighRdy 按字读入 R1 LDR R1, =OSPrioHighRdy ;R2=*(&OSPrioHighRdy)=OSPrioHighRdy ;将存储器地址为 R1 的值按字节读入 R2 LDRB R2, [R1] ;*(&OSPrioCur)=OSPrioCur=OSPrioHighRdy ;将 R2 寄存器的值按字节写入 R0 为地址的存储器中 STRB R2, [R0] ;R0=&OSTCBCurPtr ;将 OSTCBCurPtr 按字读入 R0 LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr; ;R1=&OSTCBHighRdyPtr ;将 OSTCBHighRdyPtr 按字读入 R1 LDR R1, =OSTCBHighRdyPtr ;R2=*(&OSTCBHighRdyPtr)=OSTCBHighRdyPtr ;将存储器地址为 R1 的值按字读入 R2 LDR R2, [R1] ;*(&OSTCBCurPtr)=OSTCBCurPtr=OSTCBHighRdyPtr ;将 R2 寄存器的值按字写入 R0 为地址的存储器中 STR R2, [R0] ;R0=*(OSTCBHighRdyPtr) ;将 存储器地址为 R2 的字数据读入 R0 LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr; ;把 R0 指向的地址段 按字连续 读出来 赋值到R4~R11 (8字) LDM R0, {R4-R11} ;R4~R11保存至OSTCBHighRdyPtr->StkPtr地址开头的8个字中 ; Restore r4-11 from new process stack ;R0 = R0 + 0x20 = *(OSTCBHighRdyPtr) + 0x20; ADDS R0, R0, #0x20 ;PSP = *(OSTCBHighRdyPtr) + 0x20 ;将 R0 寄存器的内容 写入 PSP MSR PSP, R0 ; Load PSP with new process SP ;确保异常返回使用进程堆栈 ORR 按位或 LR = LR | 0x04 ; ORR LR, LR, #0x04 ; Ensure exception return uses process stack ;使能中断(清除PRIMASK)和_enable_irq()相同 CPSIE I ;跳转至 存放于 LR中的地址去执行代码 BX LR ; Exception return will restore remaining context
-
os_cpu_c.c文件
创建任务初始化时需要调用的一个函数//这个函数的最大的一个作用就是我事先把一些寄存器内该存的一些值保存在数组里面 //等待后面需要用就直接取出去 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); }
开始分析:分析分为两部来走
第一步先明确任务创建到任务开始运行是怎么运转的?
第二步再明确任务和任务之间又是怎么切换的
1.创建到任务开始运行分析:
- 一:分析创建任务到任务开始运行的过程,首先需要明确,创建完任务之后就会调用OSStart(&err)函数;而调用OSStart(&err)函数之前,肯定是使用MSP栈的,调用OSStart(&err)函数就会调用到OSStartHighRdy()函数,而这个函数使用汇编写的,在开始运行操作系统部分,根据上面的代码解析实现了四个功能如下
- 设置PendSV异常优先级最低
- 设置PSP初始为0,告诉OS这是第一次运行系统
- 改变主堆栈指针(MSP),假设之前的是old_MSP,现在的是new_MSP
- 触发PendSV异常
- 二:等到 OSStartHighRdy()函数执行完毕后,由于触发了PendSV异常,所以应该立马响应PendSV异常执行异常服务函数,而PendSV中断服务函数就是上面汇编写的内核调度部分OS_CPU_PendSVHandler函数,简单分析一下汇编代码步骤如下
- 第一步:由之前的基础知识可以知道,现在进入的是中断服务函数,那么进入之前肯定有保存R0 ~ R3,R12,R14(LR),以及PSR和S0 ~ S15这一系列值,但是这些值被保存到什么地方去了呢,虽然知道是主堆栈指针,但是到底是之前的old_MSP,还是改变之后的new_MSP,由于new_MSP是在未进入中断的时候产生的,所以这些值是被压入了new_MSP栈,另一方面,由于进入中断前使用的是MSP,所以在保存完这些值后LR的值会被更新为0xFFFF FFF9,告诉系统中断完了乖乖回来主堆栈
- 第二步:进入中断的第一步就是禁止中断,防止被其他高优先级中断打断内核部分代码
- 第三步:读出PSP的值,在运行过程中PSP的值是不可能出现为0的情况,但是在开始OS的时候有设置PSP初值为0,目的是告诉OS为第一次运行系统
- 第四步:第一次调度时,判断出PSP的值为0,所以知晓这是第一次执行系统,会直接转到OS_CPU_PendSVHandler_nosave代码处开始执行
- 第五步:首先将 R14 的值压入栈内,之所以要压栈R14,是因为接下来会进入OSTaskSwHook函数,为什么需要调用这个函数,目前具体作用还不知道,有待以后考证,而调用函数就意味着会改变LR的值,所以这里需要将LR的值保存起来,等到函数调用完毕时再将之前的LR读回LR,所以在函数执行完毕后,POP {R14}可将调用函数之前的LR返回至LR中。
- 第六步:将OSPrioHighRdy赋值给OSPrioCur
- 第七步:将OSTCBHighRdyPtr赋值给OSTCBCurPtr
- 第八步:把*(OSTCBHighRdyPtr)即OSTCBHighRdyPtr->StkPtr保存的R4 ~ R11寄存器的值按字连续读出来赋值到R4 ~ R11。
- 第九步:也是重中之重的一步,本来第一步是把LR的值搞成0xFFFF FFF9的,告诉中断完了回到主栈去,结果来了一句ORR LR, LR, #0x04,把LR与LR或一下再赋值给LR,0xFFFF FFF9 | 0x04 = 0xFFFF FFFD,现在LR变成0xFFFF FFFD了,由上面的基础可以知道0xFFFF FFFD表示进入中断之前使用的进程堆栈了,但是明明之前使用的new_MSP啊,OS就是在这个时候悄悄的偷梁换柱了,然后再执行BX LR跳转的时候。MCU以为进入中断之前使用的是进程堆栈,所以现在退出中断就要从进程堆栈中出栈R0 ~ R3,R12,R14(LR),以及PSR和S0 ~ S15这一系列值。
- 第十步:在出栈这些值之前,不禁会产生疑问,现在去哪儿出栈,怎么确定出栈的值呢?然后UCOS早早的就做好这一步了,在上述代码分析创建任务初始化时需要调用的一个函数部分可以看到数组里面已经规规矩矩的把这些寄存器的值赋值好初值了。然后出栈完毕后任务就切换完成了。
2.任务和任务之间切换分析:
- 切换内核与上述一致,唯一的不同在PSP不为0了,所以会先把此刻R4~R11的值先保存至PSP内,然后再切换成新的PSP假设为new_PSP,然后再将new_PSP内保存的R4 ~ R11的值先复原到寄存器上,然后再开始新的任务。