ucosii/iii实时嵌入式操作系统任务切换与中断管理深入解析

学习嵌入式实时操作系统ucos的比较好的三本书:嵌入式实时操作系统ucosii原理及应用(任哲)、嵌入式实时操作系统ucosiii(宫辉等译)、ucosiii内核实现与应用开发实战指南(刘火良)。第一本书先看,结合ucosii源码,可以完整理解整个ucosii系统(约三万两千行代码),然后用后面两本书交叉学习ucosiii系统即可(估计五六万行吧,反正比ucosii代码量多了很多,所以推荐先学ucosii,再学ucosiii)。
对于单片机,比如arm(stm32),51等单片机,跑裸机程序,都是有且仅有一个堆栈(就是栈)(当然有些单片机的另外还有个专门的中断堆栈,发生中断了,跳转到中断服务函数运行就是在这个栈里,运行完了自动跳回原来的系统栈继续被打断的函数运行),也叫系统栈(一定是在单片机内部ram里的,这样访问速度才快),c语言程序都是运行在这个栈里。现在单片机跑了操作系统比如ucosiii了,这个系统栈仍然存在,比如一开始的main函数就是在这里运行的,除此之外,每个任务还各自有一个堆栈。arm单片机由于内存ram够多(51单片机内部ram不够,所以一般都是在内部ram利用公共的系统堆栈,其他任务各自的堆栈放外部ram里(读写速度相对慢一些),然后任务切换时候就跟内部ram的公共系统栈复制数据即可,具体可参考上述第一本书的225页,所以51单片机的任务切换函数反而编写更繁琐),所以每一个任务堆栈也是创建在内部ram里。把CPU的堆栈指针sp(指向的是堆栈的栈顶,即入栈出栈的地方,所以这个地址是动态加减的,可以看我这篇文章,C语言函数调用时候内存中栈的动态变化详细分析)指向哪个堆栈栈顶,那么此时CPU运行的函数代码的过程都是在这个堆栈里完成的(比如局部变量,过程临时地址等的存储),这个堆栈加上CPU内部的寄存器就能实现整个程序的运行了。所以CPU实现任务切换的原理就是:当前任务A正在运行,那么CPU的堆栈指针肯定就是指向任务A的任务堆栈的栈顶,且CPU里面的寄存器的暂存值是记录的当前这个任务中运行的函数信息(比如子函数返回值,进位,状态字等),那么想要切换到任务B(即任务A里主动调用了OSCtxSw()函数,也叫任务级任务切换函数),先把任务A的状态给保存入A的堆栈里(CPU中的所有寄存器的值,任务A的堆栈的栈顶地址保存给任务A的任务控制块TCB里的成员StkPtr),然后只需要把CPU的堆栈指针寄存器指向任务B的堆栈的栈顶(是通过把CPU的堆栈寄存器赋值为任务B的控制块的成员StkPtr即可),同时把B的堆栈(上一次被中断时候保存的CPU内左右寄存器的值,包括指令寄存器pc的值)中先前保存的CPU的寄存器的值推入现在CPU中(pc寄存器的值得用汇编RET指令才能推入CPU),这样就完全实现了任务B的继续运行,即无缝切换。
接着继续说,此时CPU正在运行任务A,如果此时任务A发生了中断,那么进入中断服务函数前,还是把此断点信息压入A的堆栈里(CPU中所有的寄存器自动的全部保存进入A的堆栈里,包括堆栈指针寄存器里的值赋值给了OSTCBCurPtr->StkPtr,注意:这里的堆栈地址在中断函数的内容之前,也就说明下次切换回来直接是从被中断打断的点继续运行的,而不是中断函数内部(即与中断函数没有任何关系了),为什么这里进入中断可以自动保存断点呢呢,因为中断服务程序是得自己用汇编定制写前面的操作部分的,然后才到用户服务C程序部分,参考上述说的第二本书的116页,如下图所示:
ucosii/iii实时嵌入式操作系统任务切换与中断管理深入解析
文章图片

【ucosii/iii实时嵌入式操作系统任务切换与中断管理深入解析】
ucosii/iii实时嵌入式操作系统任务切换与中断管理深入解析
文章图片

可以看到进入中断函数的自动保存部分也是自己由汇编代码自己编写的,然后才到c语言写的中断处理部分,最后调用OSIntExit函数,这个函数里面会进行一次任务切换,使用的是下面说的OSIntCtxSw()函数,即中断级任务切换函数,这不正是在中断里使用的嘛,这就是这个中断级切换名字的由来,很明显它不需要做前面的寄存器保存工作,只需要把任务B的堆栈内容推入CPU即可,后面会继续说。从上面的中断函数编写模板可知,每个中断函数进入的时候都要用OSIntEnter()(就是执行OSIntNestingCtr++)函数和快要退出时候写上OSIntExit()函数,如果在OSIntExit函数里发生的任务调度确实有更高优先级任务运行了,那么就时通过OSIntCtxSw()函数发生的任务切换 ,里面不会进行保存了,而是直接推入待运行任务B的堆栈内容进入CPU即可,当再次切换回任务A的时候,就是直接从A上次被中断打断的点继续执行下去,因为从前面可以知道上次保存的断点信息时进入中断函数那儿的,而不是这个中断函数发生任务切换这儿的,所以中断函数最后那两句推入cpu寄存器和return语句就作废了。如果OSIntExit()函数里面没有发生任务切换(比如里面待切换任务跟原任务一样),那么就马上退出中断函数了,执行后面那句推入cpu寄存器的所有值)(发生中断转入中断函数的过程跟在函数中主动调用了一个子函数过程都是一样的),而不是系统堆栈(系统堆栈就是最开始的main函数跑了一下,后面一直不会跑了),此时中断函数会继续在A的堆栈里运行(就像调用了子函数)(也叫做中断函数的栈帧,而不是另外的一个堆栈,正如上面所说的,大部分单片机都没有另外的中断堆栈),如果是想在这个中断函数里做任务切换(比如系统定时器时间片到了定时器中断函数里进行任务切换),那么很明显跟上面的任务内主动切换不一样,这里不再需要保存任务A的断点信息(比如CPU寄存器因为已经被发生中断自动保存了),正如前面已经解释了,直接推入待运行任务B的信息即可,这样就完成了在中断里的任务切换,这也是任务级任务切换函数和中断级切换函数的不同点和存在的原因。
还有一点,中断函数还可以像裸机时候一样用,但是里面得关闭中断使能(防止更高优先级中断来了发生中断嵌套后在更高优先级里发生任务切换,由上面分析可知,这样就有问题),而且这个中断函数里只执行少量不耗时的动作而已就结束了。这样可以简单快速的响应一些外部不耗时事件,也叫做无需内核参与的中断服务程序。如果需要响应快速的响应外部耗时事件中断,那还是得中断程序任务化(因为中断执行的动作尽量少才不影响整个系统的实时性),即在此中断里发消息给一个最高优先级的任务,然后发生中断级任务切换,然后让耗时事件在那个最高优先级任务里处理即可。

    推荐阅读