当前位置:   article > 正文

STM32下的uCOS底层调度深度分析_stm32二级指针

stm32二级指针
[size=10.5000pt]第一次发帖,终于明白了实时系统的调度,写了一下分享给大家。绝对原创。
大家用嵌入式系统都知道,可以运行多任务,那系统究竟是怎么从一个任务切换到另一个任务的呢。
[size=10.5000pt]这里以 uCOS 为例,以 STM32 为硬件平台。分析 uCOS 底层的任务调度。其他硬件平台的任务切换有待研究,不过应该类似。
[size=10.5000pt]STM32 采用 Cortex-M3 内核, ARM 公司在设计时考虑到了运行实时系统的要求。所以这里有一些重要的特性,给内核提供了很大的便利。
[size=10.5000pt]现在讨论的任务切换使用了 PendSV ,和堆栈。
[size=10.5000pt]首先是 PendSV
[size=10.5000pt]可悬挂请求,可以说是个软件中断,在进入 PendSV 中断处理程序时,处理器硬件也会做保护现场的操作。
[size=10.5000pt]它的使用是可以延迟任务调度,一般系统会在定时中断(即系统心跳)做任务调度。
[size=10.5000pt]不过这时可能出现心跳中断发生时在执行一个中断,此时又不能执行调度。那只能等下一个心跳。这时其实已经让任务的实时性下降了。而如果一个中断和心跳是同频率的的话,则系统调度永远不会发生。
[size=10.5000pt]有了 PendSV 之后就可以在心跳来到时通过设置 ICSR 悬起中断等到其他中断结束时便会自动执行 PendSV 中断处理程序完成任务切换。这里会把 PendSV 优先级设为最低。
[size=10.5000pt]********************************** 引用 *****************************************


file:///C:\Users\wql\AppData\Local\Temp\ksohtml\wps_clip_image-15739.png
[size=10.5000pt]上图摘至 Cortex-M3 权威指南---重点参考书目,如果要移植操作系统。
[size=10.5000pt]Cortex-M3 的堆栈有两个 MSP PSP MSP 用于 handle 模式, PSP 用于线程模式。不过这些都是有操作系统的情况下,平时写的前后台程序没有使用 PSP 。这两个堆栈指针同一时刻只有一个可以看到( banked )。同一时刻只有一个可以看到其实是指 PUSH POP 操作的对象只能是一个。
[size=10.5000pt]比如入栈
[size=10.5000pt]PUSH {R0}
[size=10.5000pt]出栈
[size=10.5000pt]POP{R0}
[size=10.5000pt]这里 PUSH POP 的操作对象其实是 SP R13 ),而 SP 可以映射到 MSP 也可以映射到 PSP
[size=10.5000pt]所以具体的 PUSH{R0} 可能是对 MSP 操作的也可以是对 PSP 操作的,可以通过控制寄存器 CONTROL 操作
[size=10.5000pt]********************************** 引用 *****************************************

file:///C:\Users\wql\AppData\Local\Temp\ksohtml\wps_clip_image-14627.png
[size=10.5000pt]上图摘至 Cortex-M3 权威指南。
[size=10.5000pt]而平时的前后台程序程序一运行就是在特权级下的而且 SP 映射 MSP 的。
[size=10.5000pt]下面看 uCOS 调度函数
[size=10.5000pt]void  OS_Sched (void)
[size=10.5000pt]{
[size=10.5000pt]#if OS_CRITICAL_METHOD == 3                           
[size=10.5000pt]    OS_CPU_SR  cpu_sr = 0;
[size=10.5000pt]#endif

[size=10.5000pt]    OS_ENTER_CRITICAL();
[size=10.5000pt]if (OSIntNesting == 0) {                           
[size=10.5000pt]        if (OSLockNesting == 0) {                     
[size=10.5000pt]            OS_SchedNew();
[size=10.5000pt]            if (OSPrioHighRdy != OSPrioCur) {         
[size=10.5000pt]                OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
[size=10.5000pt]#if OS_TASK_PROFILE_EN > 0
[size=10.5000pt]                OSTCBHighRdy->OSTCBCtxSwCtr++;         
[size=10.5000pt]#endif
[size=10.5000pt]                OSCtxSwCtr++;                                         [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]OS_TASK_SW();                        
[size=10.5000pt]            }
[size=10.5000pt]        }
[size=10.5000pt]    }
[size=10.5000pt]    OS_EXIT_CRITICAL();
[size=10.5000pt]}
[size=10.5000pt]上面注释部分删掉了,可以看的更清晰。
[size=10.5000pt]前面部分是通过就绪表查找就绪态的最高优先级的任务。
[size=10.5000pt]计算后会得到
[size=10.5000pt]OSPrioHighRdy
[size=10.5000pt]最高优先级就绪态任务的优先级
[size=10.5000pt]OSTCBHighRdy
[size=10.5000pt]最高优先级就绪态任务的[size=10.5000pt]TCB 控制块
[size=10.5000pt]#define  OS_TASK_SW()         OSCtxSw()
[size=10.5000pt]然后执行[size=10.5000pt]OS_TASK_SW()[size=10.5000pt]其实就是[size=10.5000pt]OSCtxSw()
[size=10.5000pt]OSCtxSw
[size=10.5000pt]    LDR     R0, =NVIC_INT_CTRL  ; Trigger the PendSV exception (causes context switch)
[size=10.5000pt]    LDR     R1, =NVIC_PENDSVSET
[size=10.5000pt]    STR     R1, [R0]
[size=10.5000pt]BX      LR
[size=10.5000pt]这里可以找到
[size=10.5000pt]NVIC_INT_CTRL   EQU     0xE000ED04
[size=10.5000pt]NVIC_PENDSVSET  EQU    0x10000000
[size=10.5000pt]其实[size=10.5000pt]0xE000ED04[size=10.5000pt]就是[size=10.5000pt]ICSR 的地址。我把上面汇编用类似 C 的伪代码翻译一下。因为是伪代码所以请大家不要在意语法。
[size=10.5000pt]伪代码中把 Ri 当做 32 位整型变量
[size=10.5000pt]LDR     R0, =NVIC_INT_CTRL[size=10.5000pt]R0 = [size=10.5000pt]0xE000ED04
[size=10.5000pt]LDR     R1, =NVIC_PENDSVSET[size=10.5000pt]R1 = [size=10.5000pt]0x10000000
[size=10.5000pt]STR     R1, [R0][size=10.5000pt]*U32*R0 = R1   
[size=10.5000pt]BX      LR[size=10.5000pt]return
[size=10.5000pt]其实就是把 ICSR PENDSVSET 位置 1 ,之后机会触发 PendSV 的中断处理程序。
[size=10.5000pt]在中断向量表中找到 PendSV 的中断处理程序


file:///C:\Users\wql\AppData\Local\Temp\ksohtml\wps_clip_image-4224.png
[size=10.5000pt]就是[size=10.5000pt]OS_CPU_PendSVHandler
[size=10.5000pt]先看下 OS_CPU_PendSVHandler 上面的注释
[size=10.5000pt]4) Since PendSV is set to lowest priority in the system (by OSStartHighRdy() above), we
[size=10.5000pt];              know that it will only be run when no other exception or interrupt is active, and
[size=10.5000pt];            therefore safe to assume that context being switched out was using the process stack (PSP).
[size=10.5000pt]前面三条要慢慢分析下。
[size=10.5000pt]看第四条。可以看到[size=10.5000pt]PendSV 设置成最低优先级,所以 PendSV 发生时不会再中断上下文中。
[size=10.5000pt]最后这句[size=10.5000pt]therefore safe to assume that context being switched out was using the process stack (PSP).[size=10.5000pt]在[size=10.5000pt]OS_CPU_PendSVHandler 中断返回时切换到 PSP
[size=10.5000pt]压轴的来了。
[size=10.5000pt]一边贴源代码一边分析。
[size=10.5000pt]CPSID   I[size=10.5000pt]关中断
[size=10.5000pt]MRS     R0, PSP[size=10.5000pt]R0 = PSP(注意这里用的是PSP
[size=10.5000pt]CBZ [size=10.5000pt]  [size=10.5000pt]R0, OS_CPU_PendSVHandler_nosave[size=10.5000pt]IfR0 == 0
[size=10.5000pt]{
[size=10.5000pt]     [size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt]();
[size=10.5000pt]}
[size=10.5000pt]这里如果 R0 等于 0 表示是第一次调度,就会执行[size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt]()
[size=10.5000pt]因为之前都是用 MSP 所以第一次调度 PSP 等于 0
[size=10.5000pt]这里先看下[size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt]( )
[size=10.5000pt]PUSH    {R14}[size=10.5000pt]简单的入栈,不过注意这里是用MSP因为在handle模式必须是MSP
[size=10.5000pt]LDR     R0, =OSTaskSwHook[size=10.5000pt]OSTaskSwHook[size=10.5000pt]这里是一个函数名
[size=10.5000pt]即这里[size=10.5000pt]R0等于[size=10.5000pt]OSTaskSwHook[size=10.5000pt]的地址
[size=10.5000pt]OSTaskSwHook[size=10.5000pt]内其实是空的由用户改写,先不管它。
[size=10.5000pt]BLX     R0[size=10.5000pt]OSTaskSwHook[size=10.5000pt]()
[size=10.5000pt]POP     {R14}[size=10.5000pt]出栈。这个R14LR
[size=10.5000pt]这里打断说下 LR ,在程序中执行 BL BLX 跳转指令时会保存当前 PC+4 LR 就是 R14
[size=10.5000pt]在子函数中要返回时调用 BX LR 就可以返回到调用它的地方。
[size=10.5000pt]BX  LR
[size=10.5000pt]和 BX   R14 效果是一样的。
[size=10.5000pt]不过在中断处理中就不一样了这时 LR 并不是跳转时的 PC+4 。因为中断的返回和函数返回是不一样的。中断返回要恢复现场。比如 51 中中断返回是 RETI 函数返回是 RET
[size=10.5000pt]下面一段引用 Cortex-M3 权威指南
[size=10.5000pt]在 Cortex-M3 在进入异常服务程序后,将自动更新LR的值为特殊的EXC_RETURN。这是一个高28位全为1的值,只有[3:0]的值有特殊含义,如表9.3所示。当异常服务例程把这个值送往PC时,就会启动处理器的中断返回序列。因为LR的值是由CM3自动设置的,所以只要没有特殊需求,就不要改动它。
file:///C:\Users\wql\AppData\Local\Temp\ksohtml\wps_clip_image-20252.png

[size=10.5000pt]**********************************引用*****************************************


[size=10.5000pt]继续分析:
[size=10.5000pt]LDR     R0, =[size=10.5000pt] [size=10.5000pt]OSPrioCur[size=10.5000pt]R0 = &OSPrioCur
[size=10.5000pt]LDR     R1, =[size=10.5000pt] [size=10.5000pt]OSPrioHighRdy[size=10.5000pt]R1 = &[size=10.5000pt]OSPrioHighRdy
[size=10.5000pt]LDRB    R2, [R1][size=10.5000pt]R2 = *(U16*)R1
[size=10.5000pt]STRB    R2, [R0][size=10.5000pt] *(U16*)R1 = R2
[size=10.5000pt] 上面 4 句就是 OSPrioCur =[size=10.5000pt]OSPrioHighRdy
[size=10.5000pt]看到这里不要吐槽汇编。原因就是,想想这里为什么用汇编。
[size=10.5000pt]LDR     R0, =OSTCBCur[size=10.5000pt] R0 = &[size=10.5000pt]OSTCBCur
[size=10.5000pt]LDR     R1, =OSTCBHighRdy[size=10.5000pt] R1 = &[size=10.5000pt]OSTCBHighRdy
[size=10.5000pt]LDR     R2, [R1][size=10.5000pt] R2 = *([size=10.5000pt]U32[size=10.5000pt]*)R1
[size=10.5000pt]STR     R2, [R0][size=10.5000pt] *([size=10.5000pt]uU32[size=10.5000pt]*)R0 = R2
[size=10.5000pt]效果和前面一样不过列出类要帮助下面分析。这两个数据都是[size=10.5000pt]OS_TCB[size=10.5000pt]类型,原型只截取最前面
[size=10.5000pt]typedef struct os_tcb {
[size=10.5000pt]OS_STK          *OSTCBStkPtr;
[size=10.5000pt]......................
[size=10.5000pt]}[size=10.5000pt] OS_TCB;
[size=10.5000pt]这个[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]就是指向任务堆栈的栈顶指针。它是个二级指针。
[size=10.5000pt]把它放在结构体最前面就是方便汇编操作。因为这样[size=10.5000pt]OSTCBCur[size=10.5000pt] 的地址和[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]的地址就一样了。
[size=10.5000pt]LDR     R0, [R2][size=10.5000pt]注意上面[size=10.5000pt]R2的值
[size=10.5000pt]R2 = &([size=10.5000pt]OSTCBHighRdy[size=10.5000pt]->[size=10.5000pt]OSTCBStkPtr[size=10.5000pt])
[size=10.5000pt]R0 = [size=10.5000pt]OSTCBStkPtr
[size=10.5000pt]LDM     R0, {R4-R11}[size=10.5000pt]for(i=0; i<8; i++)
[size=10.5000pt]R(i+4) = *(OSTCBStkPtr+i)
[size=10.5000pt]其实就是恢复R4-R11的值
[size=10.5000pt]这里只保存R4-R11是因为其他重要的寄存器中断发生时硬件会保存
[size=10.5000pt]ADDS    R0, R0, #0x20[size=10.5000pt]R0 = R0 + 0x20
[size=10.5000pt]MSR     PSP, R0[size=10.5000pt]PSP = R0
[size=10.5000pt]ORR     LR, LR, #0x04[size=10.5000pt]LR |= 0x04切换到线程模式
[size=10.5000pt]这时SP就映射到了PSP
[size=10.5000pt]之后返回线程模式在执行PUSH,POP等就使用PSP了。
[size=10.5000pt]CPSIE   I[size=10.5000pt]开中断
[size=10.5000pt]BX      LR[size=10.5000pt]中断返回(注意是中断返回)

[size=10.5000pt]重要的地方 ********************************************************************

[size=10.5000pt]注意这里是中断返回和跳转时不一样的。它不是直接跳转到指定位置,而是启动内核中断返回序列。由内核硬件恢复。这里关键地方来了。内核自动恢复的包括 PC 。而内核是从堆栈中恢复的 PC ,而前面执行了
[size=10.5000pt]LDR     R0, [R2]
[size=10.5000pt]LDM     R0, {R4-R11}
[size=10.5000pt]ADDS    R0, R0, #0x20
[size=10.5000pt]MSR     PSP, R0
[size=10.5000pt]PSP 被你改成了一个高优先级的任务的控制块。
[size=10.5000pt]也就是说你进来中断时内核帮你保存了 PC 等等数据,按理说中断返回时内核会恢复到原来的位置。也就是 PC 还等于中断前的位置,可是你改动了 PSP 中断返回时把这时的 PSP 当做原来的堆栈。这个 PC 已经不是进来中断时的 PC 了,等你返回时内核会跳到从你的[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]恢复出来的[size=10.5000pt]PC ,这就实现了切换。 PC 进中断指向 A 任务的代码,出中断就变成指向 B 任务的代码。
[size=10.5000pt]说了这么多还有一点代码,之后在总结一下就清晰了。最后一点应该不用我分析了吧。
[size=10.5000pt]实际上[size=10.5000pt]OS_CPU_PendSVHandler[size=10.5000pt]代码是:
[size=10.5000pt]CPSID   I                                                
[size=10.5000pt]MRS     R0, PSP                                             
[size=10.5000pt]CBZ     R0, OS_CPU_PendSVHandler_nosave                     
[size=10.5000pt]SUBS    R0, R0, #0x20                                          
[size=10.5000pt]STM     R0, {R4-R11}
[size=10.5000pt]LDR     R1, =OSTCBCur                                       
[size=10.5000pt]LDR     R1, [R1]
[size=10.5000pt]STR     R0, [R1]
[size=10.5000pt]OS_CPU_PendSVHandler_nosave
[size=10.5000pt]PUSH    {R14}                                               
[size=10.5000pt]LDR     R0, =OSTaskSwHook   
[size=10.5000pt]..........................
[size=10.5000pt]...........................[size=10.5000pt]                              
[size=10.5000pt]BX      LR
[size=10.5000pt]END
[size=10.5000pt]流程是先判断 PSP 等与 0
[size=10.5000pt]等于 0 就执行[size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt](第一次调度)
[size=10.5000pt]执行[size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt]后注意就直接中断返回了,不会执行下面的代码了。

[size=10.5000pt]如果不等于 0 就把当前任务的 R4-R11 保存起来
[size=10.5000pt]其实这之前内核已经把
[size=10.5000pt]xPSP, PC, LR, R12, R3 ,R2, R1, R0, 保存起来了。此时
[size=10.5000pt]OSTCBCur[size=10.5000pt]->[size=10.5000pt]OSTCBStkPtr 等顶端是
[size=10.5000pt]xPSP PC LR R12 R3 R2 R1 R0 R4 R5 R6 R7 R8 R9 R10 R11
[size=10.5000pt]而[size=10.5000pt]OSTCBCur[size=10.5000pt]->[size=10.5000pt]OSTCBStkPtr 指向 R11
[size=10.5000pt]接着把[size=10.5000pt]OSTCBCur[size=10.5000pt] = [size=10.5000pt]OSTCBHighRdy
[size=10.5000pt]恢复之前上次保存的[size=10.5000pt]R4-R11
[size=10.5000pt]LDM     R0, {R4-R11}
[size=10.5000pt]ADDS    R0, R0, #0x20
[size=10.5000pt]切换堆栈到[size=10.5000pt]OSTCBHighRdy[size=10.5000pt]的堆栈,执行中断返回内核从[size=10.5000pt]OSTCBHighRdy[size=10.5000pt]-[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]中读取[size=10.5000pt]PC 完成跳转。
[size=10.5000pt]在之前我有一个问题,我想任务切换不就是保存通用寄存器吗,而通用寄存器就那么多,至少是固定数量的,比如 15 个,建立任务时给它 15 个深度堆栈不就够了吗。
[size=10.5000pt]这时才知道每个任务都是用自己的[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]当做堆栈使用。
[size=10.5000pt]也就是你在任务里执行[size=10.5000pt]PUSH 的时候其实用的是自己[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]里的资源。那程序里的复杂运算就要借助堆栈实现,比如一个数组 U32 P[30] 这就耗掉 30*4 字节,任务里调用函数,那么在你调用的函数用的也是你的[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]里的资源。如果[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]不够就会串到别的地方,就会跑飞了。
[size=10.5000pt]写了这么多,不知道说清楚了没有。对于想深刻理解嵌入式内核的朋友,有帮助吗。
编辑了好多次把纯文本去掉了一保存怎么还有乱七八糟的东西。我在WPS下编辑拷贝过来的。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/406363
推荐阅读
相关标签
  

闽ICP备14008679号