Thursday, November 15, 2007

uCOS研究]在MC68HC908GP32上移植μC/OS-II

在前几讲中,介绍了μC/OS-II的概念、工作机制,还介绍了μC/OS-II在Intel
80X86CPU上的移植,相信读者通信学习,已经对移植的过程和步骤有了一定的了解。ΜC/OS-II最初是为摩托罗拉68HC11系列单片机设计的。68HC11系列单片机有外部总线,可以外接RAM和ROM;而没有外部总线8位MCU。由于RAM容量的限制,移植就存在一定的困难;但对于有些8位的MCU,将μC/OS-II移植到MOTOROLA
MC68H908GP32(以下简称GP32)上。
一、在GP32上移植μC/OS-11的主要问题
在第(4)讲中,介绍过要移植μC/OS-11,目标处理必须满足以下要求:
(1)处理器的C编译器能产生可重入代码;
(2)用C语言就可以打开和关闭中断;
(3)处理器支持中断,并且能产生定时中断(通常在10~100Hz之间);
(4)处理器支持足够的RAM,保存全局变量和作为多任务环境下的任务堆栈。
(5)处理器有将堆栈指针和其他CPU寄存器读出和存储到堆栈或内存中的指令。
编译后的μC/OS-II的内核大约有6~10KB;如果只保留最核心的代码,则最小可压缩到2KB。RAM的占用与系统中的任务数有关,任务堆栈要占用大量的RAM空间,堆栈的大小取决于任务的局部变量、缓冲区大小及可能的中断嵌套的层数。所以,所要移植的系统中必须有足够的RAM资源。而像MOTOROLA6805系列的8位MCU,由于RAM资源太小且堆栈指针是固定的,不能满足上面的第(4)条和第(5)条要求,所以μC/OS-II不能在这类处理器上运行。
GP32是68HC08家庭的成员,具有512字节的片内RAM,32K字节的片内Flash,8MHz总线时钟。内部寄存器包括1个8位累加器A,1个16位索引寄存器X,1个16位堆栈指针寄存器SP,1个16位程序指针寄存器PC及1个8位标志寄存器CCR。与6805系列MCU相比,68HC08系列MCU的堆栈指针为16位,可以自由寻址。这就满足了移植条件(5),且可以使用C编译器生成代码。GP32的用户手册可以在摩托罗拉公司的主页上下载,http://www.motorola.com。
在GP32上移植μC/OS-II的主要困难还是RAM资源太少。为了移植成功,必须采取措施减少RAM的用量,包括限制系统中的任务数量、仔细修改μC/OS-II内核、去掉不使用的部分、限制任务的断嵌套层数、在任务中尽量减少使用局部变量等等。在采取了上述措施后,可将RAM用量减少到最低。但需要提醒的是,为了节省堆栈空间而采用的限制中断嵌套层数的方法将影响系统的实时性能。所以,在GP32上移植μC/OS-II更多的是一种演示,能更好地说明μC/OS-II内核的可裁剪性和灵活性。
在本讲中将介绍一个移植实例。为了减少RAM用量,在本例中只运行了2个用户任务。尽量减池一内核中不必要的模块(包括由箱、消息队列、内存管理等),去掉了任务挂起、唤醒和删除等扩展功能,但仍然支持任务的创建和管理,也保留了信号量模块来用来任务间的通讯。
二、工具和运行环境
要实现μC/OS-II向GP32的移植,需要一个面向MC68HC08的C编译器。笔者使用的是HIWARE公司的C编译器。移植过程同样适用于MC68HC08家庭的其他成员。
三、移植中所需修改的文件
首先是编写整个项目的公共头文件include.h,这个文件定义使用内核中的哪些模块。Include.h会被所有的C源程序引用。还要修改和CPU相关的三个文件,分别是头文件OS_CPU08.H、汇编代码文件OS_CPU08.ASM和C代码文件OS_CPU08.C。
1.include.h文件
include.h是主头文件,在所有后缀名为.C的文件的开始都包含include.h文件。文件中可以内核进行裁剪。在本例中定义如下:
#define OS_MAX_EVENTS 2 /*共用了2个信号量*/
#define OS_MAX_MEM_PART 0 /*不使用内存块功能*/
#define OS_MAX_QS 0 /*不使用消息队列功能*/
#define OS_MAX_TASKS 3 /*共有3个任务(包括空闲任务)*/
#define OS_LOWEST_PRIO 20 /*定义最低优先级20*/
#define OS_TASK_IDLE_STK_SIZE 60 /*定义空闲任务堆栈60字节*/
#define OS_TASK_STAT_EN 0 /*不使用统计任务*/
#define OS_MBOX_EN 0 /*不使用消息邮箱功能*/
#define OS_MEM_EN 0 /*不包括内存管理部分代码*/
#define OS_Q_EN 0 /*不包括消息队列部分代码*/
#define OS_SEM_EN 1 /*定义包括信息量相关代码*/
#define OS_TASK_CHANGE_PRIO_EN 0 /*不包括任务优先级动态改变代码*/
#define OS_TASK_CREATE_EN1 /*包括任务创建函数代码*/
#define OS_TASK_CREATE_EXT_EN 0 /*不包括带扩展功能的任务创建函数*/
#define OS_TASK_DEL_EN 0 /*不包括删除任务函数代码*/
#define OS_TASK_SUSPEND_EN 0 /*不包括任务挂起和唤醒函数代码*/
#define OS_TICKS_PER_SEC 10 /*定义每秒的时钟节拍数*/
对于不同类型的处理器,还需要改写include.h文件,增加自己的头文件,但必须加在文件末尾。在安装μC/OS-II的时候,附带了几个移植实例,例如,针对Intel
80X86的代码安装到IIL目录下。我们为GP32编写的移植实例可放在IIHC08下,这样,为GP32改写的include.h文件中应该加入下列语句:
#include "iiHC08_CPU08.ASM"
#include "iiHC08_CPU08.C"
#include "iiHC08_CPU08.H"
2.OS_CPU08.H文件
OS_CPU08.H文件中定义了硬件相关的基本信息:
/*数据类型*/
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U;
typedef signed char INT8S;
typedef unsigned short INT16U;
typedef signed short INT16S;
typedef unsigned long INT32U;
typedef signed long INT32S;
/*定义堆栈增长方向*/
#define OS_STK_GROWTH1 /*堆栈由高地址向低地址增长*/
/*定义堆栈单位*/
#define OS_STK INT8U
/*定义进入临界代码区开关中断宏*/
#define OS_ENTER_CRITICAL() asm sei
#define OS_EXIT_CRITICAL() asm cli
#define OS_TASK_SW() asm swi
(1)数据类型
由于不同的处理器有不同的字长,μC/OS-II的移植需要重新定义一系列的数据结构。具体字长还和使用的C编译器有关。在GP32中堆栈是按字节操作的,堆栈数据类型OS_STK声明为8位。μC/OS-II中所有任务的堆栈都必须用OS_STK声明。
(2)代码临界区
μC/OS-II在进入系统临界代码区之前要关闭中断,等到退出临界区后再打开,从而保护核心数据不被多任务环境下的其他任务或中断破坏。在GP32中,开关中断可以通过汇编指令CLI和SEI来实现。所以μC/OS-II中的宏OS_ENTER_CRITICAL()定义为指令SEI,OS_EXIT_CRITICAL()定义为指令CLI。
(3)堆栈增长方向
GP32的堆栈是由高地址向低地址方向增长的,所以常量OS_STK_GPOWTH必须设置为1。
(4)OS_TASK_SW()函数的定义
在μC/OS-II中,OS_TASK_SW()用来实现任务切换。就绪任务的堆栈初始化应该模拟一次中断发生后的样子,堆栈中应该按进栈次序设置好各个寄存器的内容。OS_TASK_SW()函数模拟一次中断过程,在中断返回的时候进行任务切换。GP32中可采用软中断指令SWI实现任务切换。中断服务程序的入口点必须指向汇编函数OSCtxSw()。
OS_TASK_SW()的定义:
#define OS_TASK_SW() asm swi
3.OS_CPU08.ASM文件
μC/OS-II的移植需要改写OS_CPU08.ASM中的4个函数:OSStartHighRdy()、OSCtxSw()、OSIntCtxSw()和OSTickISR()。
(1)OSStartHighRdy()函数
该函数由SStart()函数调用,功能是运行优先级最高的就绪任务。在调用OSStart()之前,必须先调用OSInit(),并且已经至少创建了一个任务。为了启动任务,OSStartHighRdy()首先找到当前就绪的优先级最高的任务(OSTCBHighRdy中保存有优先级最高任务的任务控制块-TCB的地址),并从任务的任务控制块(OS_TCB)中找到指向堆栈的指针,然后从堆栈中弹出全部寄存器的内容,运行RTE中断返回。由于任务创建时堆栈的结构就是按中断后的堆栈结构初始化的,执行RET指令后就切换到新任务(有关μC/OS-II的任务切换机制,请参考系列讲座的第2讲)。对于OSStartHighRdy的代码,我们采用在C中嵌入汇编的方法编写。需要说明的是,由于GP32中有512字节RAM,所以地址指针必须是16位的;而GP32中累加寄存器A为8位,所以用累加器A传递地址必须进行两次读入、输出操作。
Void OSStartHighRdy(void)
{asm
{
jsr OSTaskSwHook //调用用户定义接口函数
lda OSRunning //设置OSRunning变量,标志进入多任务模式
inca
sta OSRunning
ldx OSTCBHighRdy //取得最高优先级就绪任务TCB地址
stx OSTCBCur //保存到OSTCBCur中
pshx
ldx OSTCBHighRdy:1//保存地址的第二个字节
stx OSTCBCur:1
pulh
lda 0,X //载放就绪任务堆栈指针
psha
ldx 1,X //载入就绪任务堆栈指针第二个字节
pulh
txs
pulh //恢复索引寄存器内容
rti //中断返回,运行新任务
}}
(2)OSCtxSw()函数
OSCtxSw()是一个任务级的任务切换函数(在任务中调用,区别于在中断程序中调用的OSIntCtxSw())。在GP32上实现,可通过执行一条软中断指令SWI来实现任务切换。软中断向量指向OSCtxSw()。在μC/OS-II中,如果任务调用了某个函数,而该函数的执行结果可能造成系统任务新调度(例如试图唤醒一个优先级更高的任务),则在函数的末尾会调用OSSched();如果OSSched()将查找当前就绪的优先级最高的任务,若不是当前任务,则判断是否需要进行任务调度,并找到该任务控制块OS_TCB的地址,将该地址拷贝到变量OSTCBHighRdy中,然后通过宏OS_TASK_SW()执行软中断进行任务切换。在此过程中,变量OSTCBCur始终包含一个指向当前运行任务OS_TCB的指针。OSCtxSw()的汇编代码如下:
Void OSCtxSw(void)
{asm
{pshh //保存X寄存器
tsx
pshx
pshh
dx OSTCBCur //载入当前任务的TCB指针
pshx
ldx OSTCBCur:1 //载入TCB的第二个字节
pulh
pula
sta 0,x //保存当前堆栈指针
pula
sta 1,x
jsr OSTaskSwHook //调用用户定义的接口函数
lda OSPrioHighRdy //设置OSPrioCur=OSPrioHighRdy
sta OSPrioCur
pshx
ldx OSTCBHighRdy:1
stx OSTCBCur:1
pulh
lda 0,x //载入堆栈指针
psha
ldx,1,x
pulh
txs
pulh //恢复索引寄存器内容
rti //中断返回,切换任务
}}
(4)OSTickISR()函数
在μC/OS-II中,当调用OSStart()启动多任务环境后,时钟中断的使用是非常重要的。在时钟中断程序中负责处理所有与定时相关的工作,如任务的延时、等待操作等等。在时钟中断中将查询处于等待状态的任务,判断是否延时结束,否则将重新进行任务调度。
为GP32编写的函数OSTickISR()的代码如下:
void OSTickISR()void{
asm{
pshh
LDA T1SC
BCLR 7,T1SC //允许中断嵌套
}
OsintEnter(); /*标志进入中断*/
OSTimeTick(); /*调用时钟节拍函数*/
OSlntExit(); /*标志退出中断*/
Asm{
Pulh
Rti
}}
和μC/OS-II中的其他中断服务程序一样,OSTickISR()首先在被中断任务堆栈中保存CPU寄存器的值,然后调用OSIntEnter()。μC/OS-II要求在中断服务程序开头调用OSIntEnter(),其作用是将记录中断嵌套层数的全局变量OSIntNesting加1。如果不调用OSIntEnter(),直接将OSIntNesting加1也是允许的。随后,OSTickISR()调用OSTimeTick(),检查所有处于延时等待状态的任务,判断是否有延时结束就绪的任务。在OSTickISR()的最后调用OSIntExit(),如果在中断中(或其他嵌套的中断)有更高优先级的任务就绪,并且当前中断为中断嵌套的最后一层,OSIntExit()将进行任务调度。注意:如果进行了任务调度,OSIntExit()将不同志返回调用者,而是用新任务的堆栈中的寄存器数值恢复CPU现场,然后用IRET实现任务切换。如果当有中断不是中断嵌套的最后一层,或中断中没有改变任务的就绪状态,OSIntExit()将返回调用者OSTickISR(),最后OSTickISR()返回被中断的任务。
4.OS_CPU08.C文件
μC/OS-II的移植需要用户在OS_CPU08.C中定义6个函数:
OSTaskStkInit()
OSTaskCreateHook()
OSTaskDelHook()
OSTaskSwHook()
OSTaskStatHook()
OSTimeTickHook()
实际需要定义的只有OSTaskStkInit()函数,其他5个函数需要声明,但不一定有实际内容。这5个函数都是用户定义,所以OS_CPU08.C中只有定义,没有给出代码。如果用户需要使用这些函数,请将文件OS_CFG.H中的#define
constant OS_CPU_HOOKS_EN设为1,设为0表示不使用这些函数。
OSTaskStkInit()函数由任务创建函数OSTaskCreate()或OSTaskCreateExt()调用,用来初始化任务的堆栈。初始状态的堆栈模拟发生一次中断后的堆栈结构,按照中断后的进栈次序预留各个寄存器存储空间;而中断返回地址指向任务代码的起始地址。当调用OSTaskCreate()或OSTaskCreateExt()创建一个新任务时,需要传递的参数是:任务代码的起始地址、参数指针(pdata)、任务堆栈顶端的地址、任务的优先级。OSTaskCreateExt()还需要一些其他参数,但与OSTaskStkinit()没有关系。OSTaskStkInit()只需要以上提到的3个参数(task、pdata和ptos)。堆栈初始化工作结束后,OSTaskStkInit()返回新的堆栈栈顶指针,OSTaskCreate()OSTaskCreateExt()将指针保存在任务的OS_TCB中。
Void*OSTaskStklint(void(*task)(void*pd),void*pdata,void*ptos,INT16U
opt)
{
INT16U *stk;
stk=(INT16U*)ptos; /*保存堆栈指针*/
*--stk=(INT16U)(task); /保存程序计数器内容*/
*--stk=(INT16U)(0x00); /初始化X和A寄存器内容*/
--stk=(INT16U)(0x00); /*初始化CCR和H寄存器*/
return((void*)stk);
}
其余的几个函数:OSTaskCreateHook()、OSTaskDelHook()、OSTaskSwHook()、OSTaskStatHook和OSTimeTickHook()均由用户自定义。
四、制作用户自己的项目
在为内核编写了上述与硬件相关的代码以后,用户就可以为自己的项目编写实际的代码了。在本例中,用户任务共有两个。任务1在初始化时钟中断以后,就进入了一人死循环。在这个循环里,任务1一方面以1s(秒)为周期改变并行I/O口PORTA第0个引脚的输出电压,另一方面每隔4s便向任务2发送1个信号。而任务2则始终等待任务1发来的信号,一旦收到信号,便改变并行I/O口PORTA第1个引脚的输出电压。具体的代码如下:
/*****************************************
* EXE2.C
*******************************************/
#include
#include "includes.h"
Byte PORTA @0x0000; /*并口A地址$0000*/
Byte DDRA @0x0004; /*并口A方向寄存器地址$0004*/
Byte T1SC @0x0020; /*定时器控制寄存器地址$0020*/
Byte T1MODH@0x0023; /*定时器模式寄存器地址$0023*/
OS_EVENT *Semaphore;
#define TASK_STK_SIZE 64 /*任务堆栈大小64字节*/
INT8U Task1Stk[TASK_STK_SIZE]; /*定义任务1堆栈*/
INT8U Task2Stk[TASK_STK_SIZE]; /*定义任务2堆栈*/
Void Hardwareinit(void);
Void Task1(void*pdata)
{int count=0;
/*int count=0;
/*初始化定时器*/
asm{
LDA #0x50
STA T1SC
LDHX #0x0333 //设定定时器间隔100ms
STHX T1MODH
CLI
}
for(;;){
PORTA&=0xFE;
OSTimeDly(5); /*延时0.5s*/
PORTA|=0x01;
/*延时0.5s*/
DSTimeDly(5);
Count++;
If(count= =4){
OSSemPost(Semaphore);
Count=0;
}
}}
void Task2(void *pdata)
{
Byte err;
For(;;){
OSSemPend(Semaphore,0,&err);
PORTA&=0xFD:
OSSemPend(Semaphore,0,&err);
PORTA|=0x02;
}
}
void main(void){
Hardwarelnit(); /*完成硬件的初始化工作*/
Oslint(); /*初始化多任务环境*/
Semaphore=OSSemCreate(0);
OSTaskCreate(Task1,(void*)0,(void*)&Task1Stk
[TASK_STK_SIZE],10);
OSTaskCreate(Task2,void*)0,(void*)&Task2Stk
[TASK_STK_SIZE],9);
OSStart();
}
在主程序main()中,用户必须先调用OSInit(),然后创建各个任务和信号量等,最后调用OSStart(),以启动内核运行,开始正常的任务调度。
本例中尽量减小了对RAM的需求:假如中断嵌套层数不超过三层,所需事件只有一个,即只需要一个事件控制块;应用中对μC/OS-II提供的功能进行最大限度的裁剪,能不用的尽量不用。采用了上述措施后,μC/OS-II的RAM使用情况大致如下:μC/OC-II所使用的全局变量占用22字节,事件控制块占用12字节。此外,当系统初始化时,还需要最小30字节的系统堆栈用于初始化TCB,并传递参数。以上为μC/OS-II中系统所必需的RAM,计64字节。
假设给每个任务分配64个字节的堆栈空间,其中用于任务控制块17字节,允许三层中断嵌套要用18字节,任务切换时的栈结构要用8字节。由于程序是用C语言编写的,确切的子函数嵌套调用层数是不知道的。如果不使用本身需要缓冲区的C语言函数,如printf()等,任务中程序调用所产生的嵌套层数不超过10层,则64字节中还能分析给任务局部变量的空间只剩下1字节了。这是能分配任务的最小RAM空间了。
综上所述,GP32的512字节RAM可分为8个64字节的RAM块。如果运行4个任务,能留给应用程序的RAM也只剩下128字节了。如果在GP32上运行μC/OS-II,且不多于8个任务,则任务调度表可以再简化,不需要调度64个任务,只调度8个任务就可以了。