实时操作系统μC/OS-II在51单片机上的移植
扫描二维码
随时随地手机看文章
μC/OS-II是一种公开源代码、结构小巧、具有可剥夺实时内核的嵌入式开发系统,代码简短、条理清晰、实时性及安全性能很高,绝大部分代码用C编写,现已被移植到多种处理器的构架中。随着51单片机片内资源的日益丰富,在51单片机上移植μC/OS-II已成为可能,植入系统后,由系统来管理软件与硬件资源,简化应用程序的设计,并且使应用系统功能更加完善。因此在51单片机上移植μC/OS-II具有十分重要的意义。
1 μC/OS实时操作系统概述
μC/OS-II实时操作系统是一种可移植、可固化、可裁剪即可剥夺型的多任务实时内核,适用于各种微处理器和微控制器。μC/OS-II主要包括任务调度、时间管理、内存管理、事件管理(信号量、邮箱、消息队列)4大部分。它的移植与4个文件相关:汇编文件(OS_CPU_A.A SM)、处理器相关C文件(OS_CPU.H、OS_CPU_C.C)和配置文件(OS_CFG.H)。有64个优先级,系统占用8个,用户可创建56任务,不支持时间片轮转。
它的基本思路就是“近似地每时每刻总是让优先级最高的就绪任务处于运行状态”。为了保证这一点,它在调用系统函数、中断结束、定时中断结束时总是执行调度算法。原作者通过事先计算好数据,简化了运算量,通过精心设计就绪表结构,使得延时可预知。任务的切换是通过模拟一次中断实现的。
2 任务调度的实现原理
任务调度是μC/OS-II的重要部分,和具体的微处理器关系紧密。必须移植的5个函数有4个都和任务有关。任务调度就是保存当前任务的寄存器和PC指针(即当前任务的断点),然后把将要执行的任务的寄存器值返回给寄存器并把PC指向将要执行任务的断点。这些的实现要借助于堆栈和中断,为了简便起见,先看函数调用时堆栈的使用情况。在函数调用时,堆栈的一个重要功能就是保存被调函数的断点地址。若有4个函数,Fun1调用Fun2,Fun2调用Fun3,Fun3调用Fun4,Fun4为叶子程序(无子程序调用)。
假设现在从Fun1一直运行到Fun4,此时堆栈结构如图1所示,中间的ADD_A到ADD_D为堆栈中的数据,左边的SP到SP-7为堆栈指针,右边的Fun1到Fun4为对应的调用函数。运行Fun4时,此时SP与SP-1所存的值为ADD_D,而ADD_D为Fun3中子函数Fun4的下一行的地址,即Fun3中3-2行的地址,以此类推,ADD_C为2-2行地址,ADD_B 图1函数运行及堆栈结构图为1-2行地址。
当函数A调用函数B时,进入函数B时就会把函数A的断点地址压栈,而当函数B运行结束时则把堆栈中函数A的断点地址弹出到PC指针,程序接着从函数A的断点开始运行。如果在函数B中更改SP及SP-1中的数据,则函数B运行结束时就不会再返回函数A中,而返回到SP及SP-1更改后的数据所代表的地址。
以上是函数调用时的基本情况,如果是中断则堆栈不仅保存断点地址还会自动保存寄存器的值。任务调度就是靠中断来实现,中断中所保存的断点地址就是任务的断点地址,当本任务要再次执行时就把断点地址赋给PC就可以接着任务被中断时地址顺序执行。
3 头文件移植
与移植相关的4个文件中有2个头文件,这2个头文件的移植比较简单,可以参考其它的移植程序。其中OS_CPU.H中主要是数据类型的定义、堆栈生长方向的定义、开关中断的定义以及函数级任务切换的宏定义。OS_CFG.H中主要是任务数、优先级数、事件数、每秒中断节拍数以及各种系统函数的使能定义。
4 汇编与C文件的移植
在要移植的汇编与C的两个文件中有14个函数,其中9个是接口函数,可根据实际需要来决定,有5个是必须写的。这5个函数分别是:OS_CPU_C.C文件中的OSTaskStkInit()和OS_CPU_A.ASM文件中的OSStartHighRdy()、OSCtxSw()、OSINTCtxSw()与OSTICkISR()。下面就这5个函数来做具体分析。
4.1 任务堆栈初始化函数OSTaskStkInit()
此函数是在任务创建函数OSTaskCreat()或OSTaskCreatExt()中调用的。因为系统为每个任务申请了一个数组作为栈,当一个任务运行时,就把堆栈指针指向本任务的栈,任务堆栈初始化函数就是在任务创建时将要创建任务的堆栈进行初始化。但C51的堆栈指针SP是8位的,只能在片内RAM的256个字节内寻址。因其寻址空间有限且SP唯一,不能像DSP或ARM那样为每一段程序或每一种模式定义堆栈,需小心管理堆栈空间。为了适应上述情况,需要换一种思路,不是让SP去指向各任务堆栈空间,而是把各任务堆栈空间的内容复制到系统栈中。至于堆栈数组空间要有多大以及堆栈数组空间里放些什么内容,可以借鉴keil中中断函数的压栈情况,当中断函数不指定寄存器组时,编译器一般将PC、ACC、B、DPTR、PSW、R0~R7寄存器入栈,其中PC和DPTR是双字节的,其它都是单字节的,一共15个字节,所以把堆栈数组设计成至少15个字节的,以保证任务所用的寄存器都在堆栈数组中包含着。因为每个数组里放的是寄存器的值,在此就把这每个任务的堆栈数组叫做寄存器数组,暂且把寄存器数组设计成15个字节,依次存放PC、ACC、B、DPTR、PSW、R0~R7。
函数OSTaskStkInit()传递4个参数,第1个参数task是所创建任务的起始地址,这个参数须保存到PC在寄存器数组的对应位置,第2个参数ppdata是所创建任务的参数,C51规则中用R1~R3来传递参数指针,这个参数须存放到R1~R3在寄存器数组中的对应位置。第3个参数ptos是栈底指针,从当前地址开始初始化堆栈指针,第4个参数opt是附加参数,一般不用。
4.2 运行等待任务中优先级最高任务函数OSStartHighRdy()
此函数在启动操作系统函数OSStart()的最后一行调用,且此函数不返回,经过此函数后μC/OS接管系统。OSStartHighRdy()不是去调用用户任务函数,而是让PC指针指向任务函数首地址。且任务函数的传递参数只有一个,若此参数正确,则可保证任务函数运行正确。在调用OSStartHighRdy()之前OSStart()已经把最高优先级任务的任务表准备好了,只要把最高优先级任务表的数据恢复到堆栈中,再执行返回指令即可,以上最关键的是如何让其返回到最高优先级任务中而不是返回到被调函数中。
当函数OSStart()调用函数OSStartHighRdy()时,断点地址入栈;当OSStartHighRdy()执行完之后,返回断点。在OSStartHighRdy()中把SP及SP-1的值改为最高优先级任务的地址,这样OSStartHighRdy()就会返回到最高优先级任务中去运行。
4.3 任务级的任务切换函数OSCtxSw()
此函数是保存当前任务的状态,然后运行处于就绪态中的最高优先级任务。前面介绍过不是更改SP去指向寄存器数组,而是把寄存器数组的数复制到堆栈中。先看下一般的情况,在用户任务MyTask(void*ppdtat)中调用TimeDly(),TimeDly()中调用OSSched(),在OSSched()中有一个宏OS_TASK_SW(),这个宏的目的是让程序进人函数OSCtxSw()。参看图1,就如Fun4为OSCtxSw(),Fun3为OSSched(),Fun2为TimeDly(),Fun1为MyTask()。ADD_D存的是OSSched()的断点,ADD_C为TimeDly()的断点,ADD_B为MyTask()的断点。如果进行任务切换,应该把高优先级任务的地址值赋给ADD_B(即SP-4与SP-5)。
以上考虑的是最简单的情况,当任务比较复杂时,可能更改了ACC、PSW、DPTR或R0~R7的值,在进入高优先任务时,寄存器并不是此任务的寄存器值,运行的结果可能不正确。
在上述情况下如何保证CPU寄存器的值正确,要分两个阶段。第一个阶段是把CPU寄存器值保存到要挂起任务的寄存器数组中,当刚进入OSCtxSw()时,CPU寄存器的值是要挂起任务的寄存器值,所以一开始就要锁定CPU寄存器的值。如果OS_TASK_SW()定义为中断的话,在进入OSCtxSw()时,CPU寄存器的值被自动压栈;如果把OS_TASK_SW()定义为函数时,在进入函数时使用内嵌汇编的方法把CPU寄存器入栈。这时堆栈中又压入了13个字节,就如在图1的ADD_D上又压入了13个字节的数据,然后从堆栈中把值取出来放到相应任务的寄存器数组中。第二个阶段是把将要执行任务的寄存器数组的值复制到堆栈中。此时PC指针在堆栈中对应的位置是SP-17与SP-18,SP到SP-12的13个字节对应ACC、B、DPTR、PSW、R0~R7。
4.4 中断级的任务切换函数OSINTCtxSw()
此函数和上一个函数基本思想一致,都要保存当前任务的状态,运行处于就绪态中的优先级最高的任务。二者的不同在于,上个函数的堆栈中SP-17与SP-18是PC值的位置,SP到SP-12是13个寄存器的位置。当中断来时,在中断中调用函数OSIntExit(),函数OSIntExit()调用函数OSIntCtxSw(),在OSIntCtxSw()中实现任务切换。在进入函数OSIntExit()之前寄存器的值已经入栈,所以运行到本函数时堆栈中SP-17与SP-18是PC值的位置。SP-4到SP-16是13个寄存器的位置。在图1上,上个函数的13个寄存器的值被压入ADD_D上面的13个字节中,而本函数是在ADD_B于ADD_C之间压入的这13个寄存器。
4.5周期节拍中断函数OSTICkISR()
这个函数是给系统提供一个节拍,一般每秒10~100次。如果节拍频率太高,μC/OS系统会占用大量硬件资源;如果太低,任务间的切换又会很慢。
此函数首先要保证产生一个周期性的中断,可以使用硬件定时器,也可以从交流电中获得50/60Hz的时钟频率。这个函数至少要做3件事:1)进入中断时,把中断嵌套层数计数器加1,说明又进入一次中断,也可以直接调用OSIntEnter()函数;2)调用时钟节拍函数OSTimeTick(),告知系统又经过了一个节拍;3)调用OSIntExit()函数,说明要退出中断了,此函数会自动处理。
5 结束语
文中阐述了在堆栈空间有限的51单片机上运行μC/OS-II系统的移植过程,利用系统栈SP作为数据交换的枢纽。在实际应用中,如果用系统栈来移植,只需根据文中的基本思想进行适当的改写,即可运行于其他处理器上。如果处理器的堆栈指针寻址空间足够大,也可以为每个任务开辟一个栈,通过改变堆栈指针指向不同任务的栈空间,来实现任务调度。
通过在51单片机上的运行,可以看出μC/OS-II也能在堆栈空间比较少的CPU上运行。