基于实时内核的彩色电视机控制软件
曹晨
康佳集团股份有限公司彩电开发中心
摘 要:本文首先分析了一种源码公开的嵌入式实时内核 — μC/OS-II的结构和工作原理,然后将其移植到Micronas公司的SDA55xx单片机上并在此基础上设计了彩色电视机控制软件,最后根据需要对内核进行了裁剪和改进。该设计具有易于维护、性能可靠等特点,对今后的彩色电视机软件开发具有一定参考意义。
关键词:实时内核 μC/OS-II 彩色电视机 嵌入式软件
引言
随着嵌入式处理器性能的不断提高和嵌入式软件的日益复杂化,传统的前/后台软件架构已越来越不能适应需要,使用实时操作系统(RTOS)正在成为一种趋势。彩色电视机是一种典型的嵌入式系统,但目前国内的电视机几乎都没有使用RTOS,这一方面是因为现有的商业内核大多价格昂贵,另一方面也因为可通用于各种不同档次处理器的内核不多,不过由于μC/OS-II等新兴内核的出现和快速发展,上述问题已得到较好的解决。μC/OS-II具有成本低廉、通用性好、可靠性高等优点,本文对如何在μC/OS-II基础上设计彩色电视机控制软件进行了研究。
1 μC/OS-II内核分析
μC/OS-II内核的基本功能是进行任务管理,在某一时刻任务总是处于下列五种状态之一:休眠态(Dormant)指任务驻留在程序空间之中,还没有交由内核管理;就绪态(Ready)指任务已准备好运行,但因为优先级较低等原因暂时还不能运行;运行态(Running)指任务获得了处理器的使用权,正在运行中;等待态(Waiting)指任务正在等待延时结束或某个事件的发生;中断态(Interrupted)指正在运行的任务被中断而转去执行中断服务程序。μC/OS-II是抢占式内核,如果中断服务程序使更高优先级的任务就绪,则当中断服务程序执行完毕后该高优先级任务将开始运行,而当前任务则进入等待态。特别地,优先级最低的空闲(Idle)任务默认总是处于就绪态,如果所有的用户任务都进入等待态后系统将运行空闲任务。
每个任务的属性是通过任务控制块(TCB)来描述的。任务控制块是一个结构体数据类型,当任务建立时,相应的任务控制块数据被初始化;当任务被剥夺CPU使用权时,内核用任务控制块来保存该任务的状态;当任务重新获得CPU使用权时,任务控制块能确保该任务从之前挂起的位置恢复执行。
μC/OS-II的任务调度机制确保总是让处于就绪态且优先级最高的任务先运行。任务的就绪标志保存在就绪表(Ready List)中,就绪表由两个变量OSRdyGrp和OSRdyTbl[]共同组成,前者是一个字节变量,后者是一个字节数组,数组大小由总任务数决定,最大值为8。所有任务按优先级从高到低每8个分为一组,每组对应于OSRdyGrp中的1位,当某个组中有任务进入就绪态时,内核将OSRdyGrp中的相应位置1,同时OSRdyTbl[]中对应该任务的位也置1。确定当前处于就绪态且优先级最高的任务时并不需要从OSRdyTbl[0]开始逐位扫描就绪表,只要以OSRdyGrp 和OSRdyTbl[]的值为下标查询优先级判定表OSUnMapTbl[256]即可,具体方法可参考任务调度代码。
任务间的通信借助信号量、互斥信号量、消息邮箱、消息队列、事件标志组等数据结构完成,这些数据结构包括两部分:待传递的值和正在等待这些值的任务列表;如果有多个任务同时等待一个事件,则其中优先级最高的任务将得到该事件并进入就绪态。
2 μC/OS-II内核移植
要顺利移植μC/OS-II,目标处理器必须满足以下几个条件:
• 使用的C编译器能产生可重入代码;
• 处理器支持中断,并且能产生定时中断(通常为10~100Hz之间);
• 可以在C语言代码中关闭或打开中断;
• 处理器支持一定容量的硬件堆栈;
• 处理器有将堆栈指针等CPU寄存器值读出并存储到堆栈或内存中的指令。
Micronas SDA55xx单片机内含一个增强型8051内核并采用Keil C51开发工具,完全能够满足上述条件。由于μC/OS-II源代码中多次将“data”和“pdata”作为函数的形参使用,而这两个词都是Keil C51中的关键字,故为了避免冲突将其替换为“dat”和“pdat”。另外Keil C51中默认所有函数都不可重入,为了支持重入功能应在系统启动时初始化重入栈,方法是打开Keil安装路径下LIB子目录中的STARTUP.A51文件,将其中的XBPSTACK EQU 0改为XBPSTACK EQU 1,然后将STARTUP.A51与其他项目文件一起编译、链接;相应地,在所有C函数的定义后也要加入reentrant关键字来通知编译器该函数可重入。
整个移植过程主要包括对三个源文件的修改,现介绍如下:
2.1 OS_CPU.H
该文件必须被包含在所有C源文件的头部,它定义了如下一些内容:
⑴ 与编译器相关的数据类型
例如:typedef unsigned char INT8U; // 无符号8位整型
定义这些数据类型的好处是,将来无论把源程序移植到哪种处理器上都不必一一更改其中用到的数据类型,只需在OS_CPU.H中改一次即可。
⑵ 关、开中断的宏定义
进入临界段代码前要关中断,执行完临界段代码后要开中断,关、开中断的操作分别由宏定义OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()完成。这里采用了μC/OS-II提供的第三种方法来实现这两个宏,即关闭中断前先用一个局部变量来保存当前中断状态,退出中断时再将中断状态恢复,代码如下:
#define OS_ENTER_CRITICAL() (cpu_sr = IE, EA = 0)
#define OS_EXIT_CRITICAL() (IE = cpu_sr)
使用这种方法时要在每个需要关、开中断的函数里都定义局部变量cpu_sr,变量类型为OS_CPU_SR。
⑶ 堆栈的生长方向
SDA55xx的硬件堆栈生长方向为由低到高,但由于Keil C51对重入函数的特殊处理,每个任务堆栈实际均为仿真堆栈,这些仿真堆栈的生长方向为由高到低,故定义OS_STK_GROWTH如下:
#define OS_STK_GROWTH 1
⑷ 任务切换宏定义
因为SDA55xx没有软中断或陷阱指令,故将任务切换宏OS_TASK_SW()直接定义为汇编语言函数OSCtxSw(),代码如下:
#define OS_TASK_SW() OSCtxSw()
注意这里只是一般的函数调用,所以OSCtxSw()函数应以RET指令返回,而不要用RETI。
2.2 OS_CPU_A.ASM
该文件定义了3个任务切换处理函数和1个时钟节拍中断服务函数,由于任务切换是实时内核的最基本功能,故重点介绍。
SDA55xx的硬件堆栈指针SP只有8位,寻址范围限于IRAM的256字节空间,这样小的堆栈显然无法运行多任务;为解决此问题,Keil C51提供了一种仿真堆栈机制,即在XRAM内保留一定的内存做为仿真堆栈空间,通过编译器在IRAM中定义的16位指针?C_XBP来访问仿真堆栈,这样就能使堆栈空间扩展到最大64kB。使用仿真堆栈虽然效率较低,但在一般的应用中应可接受。
每个任务都要有自己的仿真堆栈,但处理器正常工作时仍离不开硬件堆栈,因为基本的堆栈操作指令PUSH、POP只对硬件堆栈有效。任务堆栈里具体应保存的内容与处理器和编译器紧密相关,经过研究后设计堆栈结构如下:
图1 SDA55xx堆栈示意图
图1右半部分为硬件堆栈,生长方向由低到高,起始位置记作Stack,栈指针为SP;左半部分为任务堆栈,生长方向由高到低,栈指针为?C_XBP。图中列出了任务切换时要保存的全部寄存器及其相对位置,任务堆栈相比硬件堆栈多保存了SP值。确定堆栈结构后实现任务切换就不是很难了,下面用示意性代码介绍了各汇编函数的主要内容。
⑴ OSStartHighRdy()函数
该函数在启动多任务环境时被OSStart()调用,使得系统中第一个任务开始运行。调用该函数时必须至少已经建立了一个任务,由于此时还没有任务在运行,故不涉及保存寄存器的问题。OSStartHighRdy()的示意代码如下:
void OSStartHighRdy(void)
{
OSTaskSwHook();
OSRunning = TRUE;
?C_XBP = OSTCBHighRdy -> OSTCBStkPtr;
从新任务堆栈中复制所有寄存器值到硬件堆栈;
从硬件堆栈中恢复所有处理器寄存器;
执行RET指令;
}
⑵ OSCtxSw()函数
该函数用来实现任务级的任务切换功能。如果当前任务调用μC/OS-II的系统服务使高优先级的任务进入就绪态,则内核将通过OSSched()函数调用OS_TASK_SW()宏,也就是调用OSCtxSw()函数。OSCtxSw()的示意代码如下:
void OSCtwSw(void)
{
将所有处理器寄存器保存到硬件堆栈中;
从硬件堆栈中复制所有寄存器到当前任务堆栈;
OSTCBCur -> OSTCBStkPt = ?C_XBP;
OSTaskSwHook();
OSTCBCur = OSTCBHighRdy;
OSPrioCur = OSPrioHighRdy;
?C_XBP = OSTCBHighRdy -> OSTCBStkPtr;
从新任务堆栈中复制所有寄存器值到硬件堆栈;
从硬件堆栈中恢复所有处理器寄存器;
执行RET指令;
}
⑶ OSIntCtxSw()函数
该函数用来实现中断级的任务切换功能。如果中断服务程序使高优先级的任务进入就绪态,则中断服务程序结束前会通过OSIntExit()调用该函数。与OSCtxSw()相比,OSIntCtxSw()只是少了保存当前寄存器的过程,并且最后应执行RETI指令。OSIntCtxSw()的示意代码如下:
void OSIntCtxSw(void)
{
OSTaskSwHook();
OSTCBCur = OSTCBHighRdy;
OSPrioCur = OSPrioHighRdy;
?C_XBP = OSTCBHighRdy -> OSTCBStkPtr;
从新任务堆栈中复制所有寄存器值到硬件堆栈;
从硬件堆栈中恢复所有处理器寄存器;
执行RETI指令;
}
⑷ OSTickISR()函数
该函数用来进行时钟节拍中断服务。必须在开始多任务环境后再启动时钟节拍中断,一般应在第一个运行的任务中进行这项工作。时钟节拍中断使用了SDA55xx的16位硬件定时器Timer0,该定时器的值在每个指令周期中加1,当增加到溢出时产生时钟节拍中断。因为一个指令周期包括12个系统时钟周期,故Timer0初始值的计算公式为:
T0 = 0xFFFF – (fosc * 1000000) / (12 * OS_TICKS_PER_SEC)
其中T0是Timer0初始值,fosc是系统时钟频率,OS_TICKS_PER_SEC是在OS_CFG.H文件中定义的时钟节拍频率。
注意应在每次时钟节拍中断发生后将Timer0的值重新设为初始值,以保持固定的时钟节拍中断频率。OSTickISR()的示意代码如下:
void OSTickISR(void)
{
将所有处理器寄存器保存到硬件堆栈中;
从硬件堆栈中复制所有寄存器到当前任务堆栈;
OSIntEnter();
if(OSIntNesting == 1)
OSTCBCur -> OSTCBStkPtr = ?C_XBP;
OSTimeTick();
OSIntExit();
从当前任务堆栈中复制所有寄存器值到硬件堆栈;
从硬件堆栈中恢复所有处理器寄存器;
执行RETI指令;
}
2.3 OS_CPU_C.C
该文件定义了OSTaskStkInit()函数以及一些对外接口(Hook)函数,但只有前者是必要的。OSTaskStkInit()在建立任务时将任务堆栈初始化成刚刚发生中断的样子,堆栈结构已在上文中介绍过,唯一要注意的是应在堆栈底部保存任务的入口地址,以便通过RET指令启动任务的执行。
μC/OS-II允许在建立任务的同时传递给任务一个指针参数,由于使用该指针并非必要而且会增加堆栈的复杂性、占用系统资源,故决定不使用该指针,相应要把OSTaskStkInit()、OSTaskCreate()和OSTaskCreateExt()中的第一个参数void (*task)(void *pd)改成void (*task)(void)。OSTaskStkInit()的代码设计如下:
OS_STK *OSTaskStkInit(void (*task)(void) reentrant, void *pdat, OS_STK *ptos, INT16U opt) reentrant
{
OS_STK *stk;
pdat = pdat; // pdat not used
opt = opt; // opt not used
stk = ptos; // task stack pointer
stk -= sizeof(INT16U);
*(INT16U*)stk = (INT16U)task; // entry address of the task
// push registers into hardware stack
*--stk = ''A''; // ACC
*--stk = ''B''; // B
*--stk = ''H''; // DPH
*--stk = ''L''; // DPL
*--stk = PSW; // PSW
*--stk = 0; // R0
*--stk = 1; // R1
*--stk = 2; // R2
*--stk = 3; // R3
*--stk = 4; // R4
*--stk = 5; // R5
*--stk = 6; // R6
*--stk = 7; // R7
*--stk = 0x80; // IE
// calculate the SP
*--stk = (OS_STK)Stack-1 // initial address of hardware stack
+1 // IE
+8 // R0-R7
+5 // PSW, DPL, DPH, B, ACC
+sizeof(INT16U); // entry address of the task
return ((OS_STK *)stk);
}
3 彩色电视机软件设计
任务分解是在实时内核上开发应用软件的重要环节,彩色电视机硬件的模块化较强,这也为任务分解提供了一个天然依据。综合考虑系统的各种控制要求后将软件划分为如下任务:
• 电源管理 — 处理软件开、关机请求;
• 命令管理 — 处理按下本机键或遥控键的事件;
• 存储管理 — 处理读写EEPROM数据的请求;
• 屏显管理 — 处理用户菜单等OSD内容的显示请求;
• 节目管理 — 处理节目搜索、节目切换等请求;
• 扫描管理 — 处理切换系统扫描参数的请求;
• 声音管理 — 处理音量、高低音等参数的调节请求;
• 图像管理 — 处理亮度、对比度等参数的调节请求。
这些任务从上到下优先级依次降低,确定优先级的依据是相应功能的重要性。由于任务数越多额外占用的系统资源也越多,故某些功能没有用任务来实现,例如内核已经提供了OSTimeTickHook()函数,就没有必要专门设置一个定时处理任务了。上述任务都是非周期性或者说事件驱动的异步任务,其一般形式为:
void Task(void)
{
for ( ; ; )
{
等待异步数据;
处理输入;
处理输出;
}
}
4 软件改进
μC/OS-II作为一个通用内核提供了较为丰富的系统服务,但实际应用中一般不会用到所有这些服务,多余的服务将造成系统资源浪费,为此μC/OS-II提供了一个配置文件OS_CFG.H来解决该问题。OS_CFG.H中提供了大量编译开关来控制所有的可选服务,对于那些需要的服务就把相应的编译开关设为1、不需要的就设为0,经过裁剪后内核代码可以只占用几kB的ROM空间。
此外移植内核时曾编写了时钟节拍中断服务程序OSTickISR(),每次进入该中断服务程序时要先把有关寄存器值压入硬件堆栈、然后复制到任务堆栈中,退出中断服务程序时则要先从任务堆栈里复制数据到硬件堆栈、然后恢复到寄存器中。由于时钟节拍中断是定期发生的,如果每次都进行这样的数据保存、恢复工作效率很低,但是考虑到退出中断前调用的OSIntExit()函数有可能进行任务切换,所以上述工作又不可避免。经过研究发现,只要把可能的任务切换动作从OSIntExit()函数移到中断服务程序中就能较好的解决该问题,为此要对内核代码做少量修改:首先定义一个布尔型全局变量OSIntCtxSwReq,并规定该变量值为真时表示需要进行任务切换;然后把OSIntExit()函数中对OSIntCtxSw()的调用删去,同时将OSIntCtxSwReq的值置为真;最后在OSTickISR()中判断OSIntCtxSwReq的值,如果为真就依次进行寄存器数据保存、重置OSIntCtxSwReq为假、调用OSIntCtxSw()等工作来完成中断级任务切换。
5 结束语
本文在μC/OS-II嵌入式实时内核的基础上实现了一套彩色电视机控制软件,具体包括了内核移植、任务设计和软件改进等内容。在彩色电视机控制软件设计中引入实时内核是一次有益的尝试,既增强了软件的实时性、可靠性和可移植性,也提高了软件开发效率,另外对今后从模拟电视转向数字电视软件开发也有积极意义。
[参考文献]
[1] Jean J. Labrosse. MicroC/OS-II: The Real-Time Kernel, 2nd Edition. CMP Books, 2002
[2] 陈明计, 周立功, 等. 嵌入式实时操作系统Small RTOS51原理及应用. 北京航空航天大学出版社, 2004
[3] 徐爱钧, 彭秀华. 单片机高级语言C51 Windows环境编程与应用. 电子工业出版社, 2001
[4] www.keil.com
[5] www.micronas.com
[作者介绍]
曹晨:康佳集团股份有限公司彩电开发中心