基于MC68HC908GP32的μC/OS-II移植
扫描二维码
随时随地手机看文章
在前几讲中,介绍了μC/OS-II的概念、工作机制,还介绍了μC/OS-II在Intel 80X86CPU上的移植,相信读者通信学习,已经对移植的过程和步骤有了一定的了解。由于RAM容量的限制,移植就存在一定的困难;但对于有些8位的MCU,将μC/OS-II移植到MOTOROLA MC68H908GP32(以下简称GP32)上。
一、在GP32上移植μC/OS-11的主要问题
GP32为韩国Gamepark株式会社自行开发的掌上游戏机,于2001年10月开始发售。主机使用3.5英寸TFT-LCD液晶,解决了STN-LCD画面暗的缺点(GBA存在这样的问题)同时也解决了画面残象的问题,玩动作游戏画面更流畅。主机参数
名称- GP32,LCD- 反射型TFT彩色液晶,画面尺寸- 3.5英寸,解象度- 320 x 240,表示能力- 65536色,CPU- 32bit RISC-CPU,ROM- 512Kbyte,SDRAM- 8MB,声音- 16Bit Stereo Sound MIDI 4声道PCM,通信- RF Network支援,使用电池- 2节AA电池,电池寿命- 12小时,外部连接端子- USB port,MEDIA- SMC(Smart Card Media)
在第(4)讲中,介绍过要移植μC/OS-11,目标处理必须满足以下要求:
(1)处理器的C编译器能产生可重入代码;
(2)用C语言就可以打开和关闭中断;
(3)处理器支持中断,并且能产生定时中断(通常在10~100Hz之间);
(4)处理器支持足够的RAM,保存全局变量和作为多任务环境下的任务堆栈。
(5)处理器有将堆栈指针和其他CPU寄存器读出和存储到堆栈或内存中的指令。
编译后的μC/OS-II的内核大约有6~10KB;如果只保留最核心的代码,则最小可压缩到2KB。RAM的占用与系统中的任务数有关,任务堆栈要占用大量的RAM空间,堆栈的大小取决于任务的局部变量、缓冲区大小及可能的中断嵌套的层数。而像MOTOROLA6805系列的8位MCU,由于RAM资源太小且堆栈指针是固定的,不能满足上面的第(4)条和第(5)条要求,所以μC/OS-II不能在这类处理器上运行。
μC/OS-II 是一种基于优先级的抢占式多任务实时操作系统,包含了实时内核、任务管理、时间管理、任务间通信同步(信号量,邮箱,消息 队列)和内存管理等功能。它可以使各个任务独立工作,互不干涉,很容易实现准时而且无误执行,使实时应用程序的设计和扩展变得容易,使应用程序的设计过程大为减化。
μC /OS-II是一个完整的、可移植、可固化、可裁剪的占先式实时多任务内核。μC/OS-II绝大部分的代码是用ANSI的C语言编写的,包含一小部分汇编代码,使之可供不同架构的微处理器使用。至今,从8位到64位,μC/OS-II已在超过40种不同架构上的微处理器上运行。μC/OS-II已经在世界范围内得到广泛应用,包括很多领域, 如手机、路由器、集线器、不间断电源、飞行器、医疗设备及工业控制上。实际上,μC/OS-II已经通过了非常严格的测试,并且得到了美国航空管 理局(Federal Aviation Administration)的认证,可以用在飞行器上。这说明μC/OS-II是稳定可靠的,可用于与人性命攸关的安全紧要(safety critical)系统。除此以外,μ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上移植μ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_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_TASK_CHANGE_PRIO_EN 0 /*不包括任务优先级动态改变代码*/
#define OS_TASK_CREATE_EN1 /*包括任务创建函数代码*/
#define OS_TASK_CREATE_EXT_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 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。判断堆栈增长方向
说明:今天看到alloca.c中find_stack_direction函数,用来判断堆栈的增长方向,记录下来。
#define ADDRESS_FUNCTION(arg) &(arg)
/* Define STACK_DIRECTION if you know the direction of stack
growth for your system; otherwise it will be automatically
deduced at run-time.
STACK_DIRECTION > 0 => grows toward higher addresses
STACK_DIRECTION < 0 => grows toward lower addresses
STACK_DIRECTION = 0 => direction of growth unknown */
#ifndef STACK_DIRECTION
#define STACK_DIRECTION 0 /* Direction unknown. */
#endif
#if STACK_DIRECTION != 0
#define STACK_DIR STACK_DIRECTION /* Known at compile-time. */
#else /* STACK_DIRECTION == 0; need run-time code. */
static int stack_dir; /* 1 or -1 once known. */
#define STACK_DIR stack_dir
static void
find_stack_direction ()
{
static char *addr = NULL; /* Address of first `dummy', once known. */
auto char dummy; /* To get stack address. */
if (addr == NULL)
{ /* Initial entry. */
addr = ADDRESS_FUNCTION (dummy);
find_stack_direction (); /* Recurse once. */
}
else
{
/* Second entry. */
if (ADDRESS_FUNCTION (dummy) > addr)
stack_dir = 1; /* Stack grew upward. */
else
stack_dir = -1; /* Stack grew downward. */
}
}
#endif /* STACK_DIRECTION == 0 */
(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中断返回。需要说明的是,由于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()。如果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 //中断返回,切换任务
}}
(3)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()将返回调用者OSTickISR(),最后OSTickISR()返回被中断的任务。
4.OS_CPU08.C文件
μC/OS-II的移植需要用户在OS_CPU08.C中定义6个函数:
OSTaskStkInit()
OSTaskCreateHook()
OSTaskDelHook()
OSTaskSwHook()
OSTaskStatHook()
OSTimeTickHook()
OSTaskStkInit()函数由任务创建函数OSTaskCreate()或OSTaskCreateExt()调用,用来初始化任务的堆栈。初始状态的堆栈模拟发生一次中断后的堆栈结构,按照中断后的进栈次序预留各个寄存器存储空间;而中断返回地址指向任务代码的起始地址。当调用OSTaskCreate()或OSTaskCreateExt()创建一个新任务时,需要传递的参数是:任务代码的起始地址、参数指针(pdata)、任务堆栈顶端的地址、任务的优先级。堆栈初始化工作结束后,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<hidef.h>
#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字节。
综上所述,GP32的512字节RAM可分为8个64字节的RAM块。如果运行4个任务,能留给应用程序的RAM也只剩下128字节了。如果在GP32上运行μC/OS-II,且不多于8个任务,则任务调度表可以再简化,不需要调度64个任务,只调度8个任务就可以了。