[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]其实就是把
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]这里如果
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]这里打断说下
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] 上面
4
句就是
OSPrioCur =[size=10.5000pt]OSPrioHighRdy
[size=10.5000pt]看到这里不要吐槽汇编。原因就是,想想这里为什么用汇编。
[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]重要的地方 ********************************************************************
[size=10.5000pt]注意这里是中断返回和跳转时不一样的。它不是直接跳转到指定位置,而是启动内核中断返回序列。由内核硬件恢复。这里关键地方来了。内核自动恢复的包括 PC 。而内核是从堆栈中恢复的 PC ,而前面执行了
[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]切换堆栈到[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下编辑拷贝过来的。
大家用嵌入式系统都知道,可以运行多任务,那系统究竟是怎么从一个任务切换到另一个任务的呢。
[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]在中断向量表中找到 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]If(R0 == 0) [size=10.5000pt]{ [size=10.5000pt] [size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt](); [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]出栈。这个R14是LR |
[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]看到这里不要吐槽汇编。原因就是,想想这里为什么用汇编。
[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]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]也就是说你进来中断时内核帮你保存了 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]在之前我有一个问题,我想任务切换不就是保存通用寄存器吗,而通用寄存器就那么多,至少是固定数量的,比如 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下编辑拷贝过来的。