本人从事zigbee的研发工作已接有多年,在这几年的技术之路上收获了很多,也失去了很多。几年之后,离开了zigbee研发岗位,决定写点什么作为纪念,另外也希望给后来的学习zigbee的同盟们留下一点“砖块”。
不复杂的小系统一般设计成如图1所示的样子,这种系统一般称作为前后台系统或超循环系统。整个应用程序(整个工程)是一个无限循环,对于应用中的具体操作既是在这个无限循环中不停地调用各个相应的函数来实现,这部分可以叫做后台行为。后台也叫做任务级。前台也叫中断级。要求实时响应的操作一般由中断服务来保证。如果前后台程序需要修改,原先整体的循环会被打乱的凌乱不堪,循环的时序也会受到影响,从而也就不能保证修改后的程序继续能够正常无误工作。这样就给中大型程序的升级,应用的增加,工程的管理带来的不可避免的麻烦,最坏的情况就是一个程序的升级相当于一个工程的重新编写。
(原文件名:图片1.JPG)
引用图片
图 1
此时就需要引进操作系统了,有了操作系统后,整个工程可以被划分成许多小的模块(任务)互相协调合作共同完成整个项目,程序修改或者升级时只需要修改对应的任务即可完成,不必改动整个工程,这是笔者认为在使用操作系统时最显而易见的好处。另外,使用操作系统使得程序的各个功能模块化,利用各种调度方法(不同操作系统可能不同)实现整个工程,从而也使得大型的程序、杂乱无章的循环变的尽然有序,程序运行更加安全可靠。现在在嵌入式领域被广泛关注和认同的操作系统有uCOS,linux,windowsCE,UCLINUX,等等,据笔者了解(才疏学浅),应用于8位单片机并得到了实践证明的暂时听说了UCOS以及RTX51就是KEIL公司针对51开发的一个小型RTOS。RTOS只提供了库接口函数,对于学者和开发者并不开源,所以使用者和探讨者的人数相对于UCOS并不多,UCOS是一款开源的实时操作系统, UCOS一直受到学者和开发者的青睐,但是由于任务中加入ucos操作系统后编译所占用的code区以及XDATA区增大较多,不适合移植到小容量的单片机上使用。笔者有幸于2008年接触到TI公司应用与zigbee协议栈的一款非抢占操作系统,下面将其叫做LTOS(little TI OS or LiTie OS),由于它应用简单方便,,开发项目稳定可靠,便于理解和学习,使得操作系统初学者可以很容易的对操作系统整体有一个全面的了解,所以笔者决定将该操作系统移植出来,放入STC12C60S2单片机中使用。记录下该文档抛砖引玉,使更多人能更快地理解该操作系统并得到的该小型操作系统更好的发展和应用。
一:对TI操作系统初步分析
1.1任务、事件、消息
刚拿到TI-MAC1.2.1时被该程序搞蒙了,TI-MAC1.2.1的程序竟然是基于他们自己编写的OS操作系统运行的。说到LTOS操作系统,不得不说说他的任务、事件和消息机制。据笔者理解,任务就是程序编写员将预实现的功能分成不同的模块,这些模块之间分工明确并且相互合作,共同完成程序员预完成的某个项目的整个功能;事件是这些任务中要处理的某个小功能的口令,比如老师说张三你站起来或坐下,张三听到站起来就站起来,听到坐下就坐下,同样道理,某个任务得到处理器后,先判断自己的事件是什么,如果是URAT_Writer则任务知道是串口写;而如果是LED_STOP,则任务知道是小灯停;消息是任务之间相互通信的方式,任务之间的数据传输一是通过消息来实现,二是通过延时设置任务来完成。任务内部消息就是一个系统事件。
在进入LTOS系统前,先利用osal_init_system()等初始化程序将操作系统初始化,主要功能就是内存分配函数的初始化、定时器的初始化以及为任务的加入。任务初始化时将任务按预先设定分配了不同的优先级,LTOS系统按照赋值的优先级顺序从高到底不停的扫描这些任务,查看他们是否被设置了事件,如果该任务被设置了事件,则操作系统将马上进入这个该任务对应的pFnEventProcessor(处理任务函数)中执行该任务中的事件。
初始化和任务加入完成之后就开始进入任务调度函数osalNextActiveTask( void )。进入任务调度函数首先扫描定时器和串口,查看定时器和串口的变化,然后利用osalNextActiveTask()函数查看任务列表中是否有被设置了事件的任务。以下是该函数的原型:
osalTaskRec_t *osalNextActiveTask( void )
{
osalTaskRec_t *srchTask;
// Start at the beginning
srchTask = tasksHead;
// When found or not
while ( srchTask ) {
if (srchTask->events) {
//判断最高优先级中有无事件
return srchTask;
}
srchTask = srchTask->next;
}
return NULL;
}
进入该函数后,让srchTask指向任务列表的头(tasksHead),然后利用if(srch->events)查看改任务中是否有事件,如果没有事件则srchTask指向任务链表的下一个元素,继续以上的工作,一旦查到某个任务有事件就返回任务的任务ID。然后利用retEvents = (activeTask->pfnEventProcessor)( activeTask->taskID, events )函数进入改任务中执行事先编写的函数。值得注意的是任务在执行完成之后一定要记得将任务的事件清空,不然返回的retEvents会跟activeTask->events相或(activeTask->events |= retEvents),假如该任务的优先级最高,这样每当程序进入下一次的调度时总会进入该任务中(因为该任务的事件不曾清空),这样其余的任务即使有置位的事件也不会被执行。具体的分析将在下一章中利用实验详细讲解。
1.2加入自己的任务
上一节中讲到了操作系统的基本运行方式,运行中涉及的任务的初始化和运行,下面主要介绍如何加入自己的任务。
在TI-MAC1.2.1中全部的任务加入是利用osalAddTasks( void )在初始化时完成的,该函数属于应用层和OS之间的接口函数,而单个的任务加入就在osalAddTasks( void )函数中利用osalTaskAdd (Task_Init,Task_ProcessEvent, OSAL_TASK_PRIORITY)加入的,其中Task-Init(uint8 task_id)是任务的初始化函数,该函数中系统为任务分配特定的唯一的ID号;Task_ProcessEvent(uint8 task_id, uint16 events)是任务的执行函数,该函数中程序表达的就是要实现的功能;最后一个参数OSAL_TASK_PRIORITY是任务的优先级别。
下面以一个小的实验例子来说明自己的任务的加入:
该实验实现一个简单的功能——LED小灯的闪烁,首先是任务的初始化函数
void LED1Init(uint8 taskId)
{
LED1Id=taskId;
}
taskId是OS系统为该函数分配的任务ID,利用该初始化函数将taskId赋值给LED1Id。
接下来就是任务的执行函数的编写
uint16 LED2_ProcessEvent(uint8 taskId, uint16 events)
{
if(events&MSA_SEND_EVENT)
{
HalLedSet (HAL_LED_2, HAL_LED_MODE_ON);
delay(5000);
HalLedSet (HAL_LED_2, HAL_LED_MODE_OFF);
osal_start_timerEx(LED1Id,MSA_SEND_EVENT,100);
return (events ^ MSA_SEND_EVENT);
}
return 0;
}
最后就利用osalTaskAdd()函数将该任务加入到操作系统中去
void osalAddTasks( void )
{
/* HAL Drivers Task */
osalTaskAdd (Hal_Init, Hal_ProcessEvent, OSAL_TASK_PRIORITY_LOW);
osalTaskAdd(LED1Init,LED1_ProcessEvent,OSAL_TASK_PRIORITY_MED );
}
二:TI操作系统的运行方式
OS操作系统不是一个完整的操作系统,任务与任务之间也不能抢占,只是简单地利用定时器管理和任务事件设置来周而复始地调用任务,与其他的操作系统一样(ucos,linux)它同样需要定时器来确定一个系统时钟tick,在cc2430中是占用一个硬件定时器来定时。每次一个任务执行完后系统都会从高优先级到低优先级扫描任务是否被设置了事件,当有任务被设置事件时,就马上进入该任务中。OS操作系统的思想是:保证高优先级的任务有事件时最先得到处理器。在任务的优先级赋值时,MAC Task一般赋最高的优先级,这样是为了使得无线电的发射和接受提高到最重要的地位。
2.1一些主要的函数
OS操作系统的运行依靠许多重要的函数,下面介绍一些主要函数以及其中的参数。
1 byte osal_set_event(byte task_id,uint16 event_flag)
说明:该函数与任务的运行至关重要,它是为任务设置事件的函数,该函数被利用为任务设置事件标示符。
参数:task_id:欲设置事件的这个任务ID,一旦写入,将为该任务ID的任务设置事件。
event_falg:事件标示,该事件标示占2个字节,每个位指定一个事件,只能有一个系统事件,其余的事件位在接受任务中自行定义。
2 osal_start_timer(uint16 event_id,uint16 timeout_value)
说明:该函数启动一个计时器,timeout_value单位时间后为这个函数现在所处的任务设置event_id事件标示。
参数:event_id:同上。
Timeout_value:设置的时间毫秒数,当时间到是设置事件。
这个函数用的不多,为了精确地给出为哪个任务ID的任务设置事件,这个函数升级为osal_start_timerEx(byte taskID,uint16 event_id,uint16 timeout_value)
其中taskID就是所指出的预设置的事件的任务。也就是说,osal_start_timer()只能为自己所在的任务设置事件,而osal_start_timerEx()不仅可以为自己所在的任务设置事件,也可以为其余的任务设置事件。
3 byte *osal_msg_allocate( uint16 len )
说明:分配一个消息缓冲器,供任务之间利用osal_msg_send()传送消息。
参数:len:消息缓冲器的长度。
4 byte osal_msg_deallocate( byte *msg_ptr )
说明:当用消息接受完成之后,取消掉分配的消息缓冲器。
参数:*msg_ptr:指向预取消的消息缓冲器的指针。
5 byte osal_msg_send( byte destination_task, byte *msg_ptr )
说明:该函数用于源任务向目的任务发送命令,数据信息等,目的任务的标示符必须给出一个有效的系统任务ID,当消息发送成功后会给目的任务设置一个事件,该事件为系统事件——SYS_EVENT_MSG.
参数:destination_task:目的任务的任务标示
*msg_ptr:指向预发送的消息的指针
6 byte *osal_msg_receive( byte task_id )
说明:该函数用于一个任务去接收消息缓冲器中的消息,接收完成之后最好利用osal_msg_deallocate()取消消息缓冲器。
参数:task_id:接收者的任务标示号,这里要注意的就是task_id并不是发送消息的任务的任务ID而是接收任务的任务ID,比如说在MSA的任务标示为MSA_TaskId,在该任务中接收其余的任务发给它的消息就应该是osal_msg_receive(MSA_TaskId)。
理解了以上几个函数之后,基本上就可以实现一些小的任务的加入,任务的执行和消息的发送与接收了。
2.2小实验验证系统的运行方式
猜测:OS系统按照任务的优先级从高到底不停的扫描这些任务,查看他们是否被设置了事件,如果该任务被设置了事件,则操作系统将马上进入这个任务的pFnEventProcessor(处理任务函数)中执行程序员预先编制好的程序。高优先级的任务处理完成后必须取消该任务的事件,否则处理器一直进入该高优先级的任务中,不能正常执行低优先级的任务。
实验目的:验证以上猜测是否正确
实验器材:zigbee实验板一套 TI-MAC程序(或者使用移植出来的LTOS以及STC12C60S2实验板)
实验步骤:
1:设置两个任务,TASK_LED1和TASK_LED2,TASK_LED1的优先级低,TASK_LED2的优先级高。
void osalAddTasks( void )
{
/* HAL Drivers Task */
osalTaskAdd (Hal_Init, Hal_ProcessEvent, OSAL_TASK_PRIORITY_LOW);
/* MAC Task */
osalTaskAdd( LED1Init, LED1_ProcessEvent, OSAL_TASK_PRIORITY_MED );
/* Application Task */
osalTaskAdd( LED2Init, LED2_ProcessEvent, OSAL_TASK_PRIORITY_HIGH );
}
2:在任务TASK_LED1中为TASK_LED2设置开灯关灯事件,并且在TASK_LED2执行完任务后清除事件标志。在任务TASK_LED2中为TASK_LED1设置开灯关灯事件,并且TASK_LED1执行完成后清除事件标志(注意程序中标I和II的语句)。运行结果:两小灯交替闪烁。
uint16 LED2_ProcessEvent(uint8 taskId, uint16 events)
{
if(events&LED_START_EVENT)
{
HalLedSet (HAL_LED_2, HAL_LED_MODE_ON);
delay(5000);
HalLedSet (HAL_LED_2, HAL_LED_MODE_OFF);
osal_start_timerEx(LED1Id,MSA_SEND_EVENT,100);
return (events ^ MSA_SEND_EVENT); (I)
}
return 0; (I)
}
uint16 LED1_ProcessEvent(uint8 taskId, uint16 events)
{
if(events & LED_START_EVENT)
{
HalLedSet (HAL_LED_1, HAL_LED_MODE_ON);
delay(5000);
HalLedSet (HAL_LED_1, HAL_LED_MODE_OFF);
osal_start_timerEx(LED2Id,MSA_SEND_EVENT,100);
return (events ^ MSA_SEND_EVENT); (II)
}
return 0; (II)
}
3:TASK_LED1,TASK_LED2互相为对方设置开灯关灯事件,并且TASK_LED2执行完成后清除事件标志,而TASK_LED1运行完成后不清除(去掉I的语句)。运行结果:两小灯交替闪烁。
4:TASK_LED1,TASK_LED2互相为对方设置开灯关灯事件,并且TASK_LED1执行完成后清除事件标志,而TASK_LED2运行完成后不清除(去掉II的语句)。运行结果:LED1和LED2各闪烁一下,不再闪烁,处理器一直进入TASK_LED2中。
实验分析:在初始化时,通过osal_start_TimerEx(1, LED_START_EVENT,100)为任务TASK_LED1设置了LED_START_EVENT事件标示,于是程序扫描TASK_LED1时知道该任务中设置了事件,就进入任务TASK_LED1中,TASK_LED1为TASK_LED2设置了事件且运行完成后自己的事件标志清零了,当任务链表从头扫到尾时,扫到TASK_LED1中没事件而TASK_LED2中有事件,则进入TASK_LED2中,而TASK_LED2中为TASK_LED1设置了LED_START_EVENT事件,则TASK_LED2执行完成之后,任务链表从头扫到尾,扫到TASK_LED1中有事件,然后又进入TASK_LED1中,这样一直循环下去。
同过上面的分析不难想到第3步的实验结论是正确的,但对于第4步不好理解。其实第4步中的TASK_LED2虽然为TASK_LED1设置了事件但是自己的事件号没清除,又因为TASK_LED2的优先级高于TASK_LED1,故先扫描到TASK_LED2,于是进入TASK_LED2中,TASK_LED2执行完成之后任务链表从头扫描,先扫描到TASK_LED2中有事件又进入TASK_LED2中,这样一直在TASK_LED2中。
实验结论:猜测是正确的。
三:揭秘TI操作系统:
3.1:调度,非抢占,不需重入
调度是内核的主要职责之一,就是要决定该轮到哪个任务运行了。多数实时内核是基于优先级调度法的。每个任务根据其重要程度的不同被赋予一定的优先级。基于优先级的调度法指,CPU 总是让处在就绪态的优先级最高的任务先运行。然而,究竟何时让高优先级任务掌握CPU 的使用权,有两种不同的情况,这要看用的是什么类型的内核,是非抢占型的还是可抢占型内核。
3.1.1 非抢占型内核 (Non-Preemptive Kernel)
非抢占型内核也叫做不可剥夺型内核,不可剥夺型内核要求每个任务自我放弃CPU 的所有权。不可剥夺型调度法也称作合作型多任务,各个任务彼此合作共享一个CPU。异步事件还是由中断服务来处理。中断服务可以使一个高优先级的任务由挂起状态变为就绪状态。但中断服务以后控制权还是回到原来被中断了的那个任务,直到该任务主动放弃CPU 的使用权时,那个高优先级的任务才能获得CPU的使用权。
不可剥夺型内核的一个优点是响应中断快。在任务级,不可剥夺型内核允许使用不可重入函数。每个任务都可以调用不可重入性函数,而不必担心其它任务可能正在使用该函数从而造成数据的破坏。因为每个任务要运行到完成时才释放CPU 的控制权。
使用不可剥夺型内核时,任务级响应时间比前后台系统快得多。此时的任务级响应时间取决于最长的任务执行时间。不可剥夺型内核的另一个优点是,几乎不需要使用信号量保护共享数据。运行着的任务占有CPU,而不必担心被别的任务抢占。但这也不是绝对的,在某种情况下,信号量还是用得着的。处理共享I/O 设备时仍需要使用互斥型信号量。例如,在打印机的使用上,仍需要满足互斥条件。
图2 示意不可剥夺型内核的运行情况,任务在运行过程之中,[2(1)]中断来了,如果此时中断是开着的,CPU 由中断向量[2(2)]进入中断服务子程序,中断服务子程序做事件处理[2(3)],使一个有更高级的任务进入就绪态。中断服务完成以后,中断返回指令[2(4)], 使CPU 回到原来被中断的任务,接着执行该任务的代码[2(5)]直到该任务完成,调用一个内核服务函数以释放CPU 控制权,由内核将控制权交给那个优先级更高的、并已进入就绪态的任务[2(6)],这个优先级更高的任务才开始处理中断服务程序标识的事件[2(7)]。
(原文件名:图片2.jpg)
引用图片
不可剥夺型内核的最大缺陷在于其响应时间。高优先级的任务已经进入就绪态,但还不能运行,要等,也许要等很长时间,直到当前运行着的任务释放CPU。
3.1.2 可剥夺型内核
当系统响应时间很重要时,要使用可剥夺型内核。最高优先级的任务一旦就绪,总能得到CPU 的控制权。当一个运行着的任务使一个比它优先级高的任务进入了就绪态,当前任务的CPU 使用权就被剥夺了,或者说被挂起了,那个高优先级的任务立刻得到了CPU 的控制权。如果是中断服务子程序使一个高优先级的任务进入就绪态,中断完成时,中断了的任务被挂起,优先级高的那个任务开始运行。如图3 所示。
(原文件名:图片3.jpg)
引用图片
使用可剥夺型内核,最高优先级的任务什么时候可以执行,可以得到CPU 的控制权是可知的。
3.1.3 LTOS的调度分析
LTOS属于非抢占型操作系统,所以不必担心函数重入的问题,也不必担心临界区的保护问题,对于没有任何操作系统使用经验的人来说,学习并且分析LTOS操作系统,将使你在最短的时间里对操作系统有一个整体的理解。LTOS中调度函数 osal_start_system( void ) 函数分析
void osal_start_system( void )
{
UINT16 events;
UINT16 retEvents;
halIntState_t intState;
// Forever Loop
while(1)
{
/* This replaces MT_SerialPoll() and osal_check_timer() */
TaskActive = osalNextActiveTask();
TaskActive是一个全局变量,总是记录着此刻处于就绪态的任务,在LTOS中,所谓就绪态就是有事件等待处理的任务。osalNextActiveTask()该函数是用来寻找此刻有事件并且处于最高优先级的任务,该函数返回一个指针,指针指向最高优先级有事件任务。
if ( TaskActive )
如果TaskActive非空,即某个任务有事件,则进入处理函数中
{
HAL_ENTER_CRITICAL_SECTION(intState);
宏,保存此时的EA寄存器值,然后关闭中断
events = TaskActive->events;
取得TaskActive中的事件。
// Clear the Events for this task
TaskActive->events = 0;
清除TaskActive中的事件,为下一次的调度作准备。
HAL_EXIT_CRITICAL_SECTION(intState);
宏,还原中断值
if ( events != 0 )
此处,不少读者可能认为该判断可以不要。但是TI程序员写程序非常的谨慎,为了避免任何一个不可预知的错误,他们总是很小心。
{
// Call the task to process the event(s)
if ( TaskActive->pfnEventProcessor )
如果TaskActive中的处理函数非空。TaskActive->pfnEventProcessor是一个指向任务的指针,即指向在osal_add_Task()中加入的APPLICATION中设计的FUNCTION()函数,不能理解函数指针的读者可以想阅读一些关于指针的教程。
{
retEvents= (TaskActive->pfnEventProcessor)( TaskActive->taskID, events );
运行函数(TaskActive->pfnEventProcessor)( TaskActive->taskID, events ),即运行函数FUNCTION()
// Add back unprocessed events to the current task
HAL_ENTER_CRITICAL_SECTION(intState);
宏,保存此时的EA寄存器值,然后关闭中断
TaskActive->events |= retEvents;
将函数的返回值重新赋值给TaskActive->events,这就是为什么任务函数执行完之后,一定要将事件清空的原因。不清空高优先级的任务中事件,高优先级任务将一直运行,低优先级任务得不到运行。
HAL_EXIT_CRITICAL_SECTION(intState);
宏,还原中断值
}
}
}
// Complete pass through all task events with no activity?
}
}
该函数就是整个程序的总调度,他总是在不停地从高到低扫描每一个任务,当任务中有事件时,就进入到该任务中去执行事件。进入该调度函数后,程序就这样无限运行下去,不会退出该调度函数了,除非中断。知道了程序是如何被调度的,读者可能还是一头雾水,不用着急,下一步我们来介绍任务的加载以及任务中的事件是如何被加入进去的。
3.2:任务加载
void osal_add_Task(pTaskInitFn pfnInit,
pTaskEventHandlerFn pfnEventProcessor,
UINT8 taskPriority)
{
OsalTadkREC_t * TaskNew;
OsalTadkREC_t *TaskSech;
OsalTadkREC_t **TaskPTR;
TaskNew = osal_mem_alloc(sizeof( OsalTadkREC_t));
申请一块空间给新的任务
if(TaskNew)
{
TaskNew->pfnInit = pfnInit;
TaskNew->pfnEventProcessor = pfnEventProcessor;
TaskNew->taskID = Task_id++;
TaskNew->events = 0;
TaskNew->taskPriority = taskPriority;
TaskNew->next = (OsalTadkREC_t *)NULL;
新任务中赋初值
TaskPTR = &TaskHead;
TaskSech = TaskHead;
以下为将各个任务一一加入任务链表的操作方法,实现过程中使用了双重指针,掌握起来会有些困难,不过只要在好好揣摩之,也不难理解。
While(TaskSech)
{
if(TaskNew->taskPriority > TaskSech->taskPriority)
{
如果新任务的优先级高于现在的任务
TaskNew->next = TaskSech;
*TaskPTR = TaskNew;
直接让TaskNew放在TaskPTR指向的地址,TaskPTR先前指向的地址可能为前一个优先级高的任务,这里经过比较优先级后直接将TaskNew放入TaskPTR指向的地址,也就是让TaskNew与比他优先级低的任务交换了位置,让比新任务优先级的任务往后靠,而新任务放在了以前这个任务的地方。笔者在此是这样理解,如有错误,请大家指正。
return;
}
TaskPTR = &TaskSech->next;
TaskSech = TaskSech->next;
如果优先级不高于以前的任务,则继续往后找
}
*TaskPTR = TaskNew;
连表头没有任务时,直接让新任务放入连表头。新任务的优先级不高于以往任何一个任务时,则新任务放于链表尾。第一次见识链表还能这样子建立,这种写法相当精妙,让人不得不感叹老外做事之用心。
}
return;
}
3.3:事件设置函数
事件被设置只有一个函数,就是osal_set_event(),但是设置的方式有三种:
1直接调用该函数为某个任务设置事件
2 使用延时设置事件函数
3 使用消息,消息会被LTOS默认为是一个系统事件(system_event)
下面先分析一下osal_set_event函数
byte osal_set_event( byte task_id, UINT16 event_flag )
{
OsalTadkREC_t *srchTask;
halIntState_t intState;
srchTask = osalFindTask( task_id );
在任务链表中寻找任务号为task_id的节点,并让srchTash指向该地址
task_id是需要增加事件的任务号,event_flag为需要设定的事件号,事件号一般设置成掩码方式,比如0x0001,0x0020,0x0004等等,一个十六位的变量可以同时表示16的事件的置位与否,换句话来说,一个任务中事件数目不要超过15,读者可能会疑惑了,刚刚明明说是16怎么一下又变成了15,其实是这样的,0x8000这个掩码已经被默认为系统事件了,所以供我们使用的还有15个掩码。对于一些小型的工程,每个任务15个事件号足以应对。但是笔者在实际应用中发现有些任务确确实实需要多余15个的事件来完成整个任务,这个时候该怎么办呢?这也是有办法的,将所有要处理的事件分门别类,一类为系统事件,一类为其余事件。系统事件的触发是通过消息来完成的,任务自己给任务自己发送消息同样是可以的,这样系统事件就可以再利用消息中的变量来判断更多的事件标志从而达到更多事件的触发了。但是笔者在移植该操作系统时,考虑到“弱功能”单片机的资源有限,并没有将消息队列移植出来。这就需要读者在做项目规划的时候就将各个任务分配好,考虑项目是划分为3个任务还是4个任务或者更多。每个任务所做的事情不得超过十五个事件就可以了,或者使用全局变量定义标志位,同样可以达到扩展事件的目的。
if ( srchTask )
{
如果找到了合适的节点
// Hold off interrupts
HAL_ENTER_CRITICAL_SECTION(intState);
宏,保存此时的EA寄存器值,然后关闭中断
// Stuff the event bit(s)
srchTask->events |= event_flag;
将事件掩码赋给节点中的events元素,请注意:上节中所讲解的osalNextActiveTask()函数为什么能搜索到有事件的任务呢,就是因为这里给任务加了事件。
// Release interrupts
HAL_EXIT_CRITICAL_SECTION(intState);
宏,还原中断值
}
else
return ( INVALID_TASK );
return ( ZSUCCESS );
}
3.4:事件设置方式
在前面介绍了事件设置函数,下面介绍设置事件的另外两种方式。
3.4.1 延时为某个任务设置事件
byte osal_start_timerEx( byte taskID, UINT16 event_id, UINT16 timeout_value )
{
在该函数中taskID是预设置事件的任务号,evnet_id为预设置的事件,timeout_value为延时多少个时间刻度再设置事件,实际延时的时间为timeout_value*(1/系统频率)。比如系统频率为100,即10ms周期。那么设置timeout_value为100,实际时间也就是1s。
halIntState_t intState;
osalTimerRec_t *newTimer;
HAL_ENTER_CRITICAL_SECTION( intState ); // Hold off interrupts.
宏,保存此时的EA寄存器值,然后关闭中断
// Add timer
newTimer = osalAddTimer( taskID, event_id, timeout_value );
在此又要引入一个链表了------时间链表,时间链表的各个节点有三个主要元素:记录时间的变量,记录任务号的变量和记录事件的变量。osalAddTimer函数就是以某个任务的任务号作为主要信息来增加节点,osalAddTimer函数先判断这个任务在之前有无添加过时间节点并还没有被利用,如果有则直接修改该时间节点将最新延时时间放入,如果这个任务之前没有添加时间节点或时间节点已经被利用并删除,则重新申请空间并添加一个节点。添加完后返回该节点的地址。
if ( newTimer )
{
如果添加成功
// Does the timer need to be started?
if ( timerActive == FALSE )
{
timerActive为一个全局变量,用来判断定时器是否开启了,这里如果没有开启,则马上开启定时器,使得定时器开始运作。
osal_timer_activate( TRUE );
定时器运作函数,这个函数最终会被一个跟硬件定时器打交道的函数代替
}
}
HAL_EXIT_CRITICAL_SECTION( intState ); // Re-enable interrupts.
宏,还原中断值
return ( (newTimer != NULL) ? ZSUCCESS : NO_TIMER_AVAIL );
}
3.4.2 消息为某个任务添加事件,添加的事件为系统事件
byte osal_msg_send( byte destination_task, byte *msg_ptr )
{
destination_task为需要接收消息的任务,msg_prt为指向消息的指针。在使用消息发送函数之前,要先使用osal_msg_allocate(len))来动态申请一个空间存放该消息,用完该消息后同样得调用osal_msg_deallocate((uint8 *) pMsg)函数来释放动态申请的空间。
if ( msg_ptr == NULL )
return ( INVALID_MSG_POINTER );
消息为空的话直接返回错误指示信息
if ( osalFindTask( destination_task ) == NULL )
{
osal_msg_deallocate( msg_ptr );
return ( INVALID_TASK );
}
如果找不到需要接收消息的任务,说明该函数绝对被错误调用了,另外直接释放动态申请的空间
// Check the message header
if ( OSAL_MSG_NEXT( msg_ptr ) != NULL ||
OSAL_MSG_ID( msg_ptr ) != TASK_NO_TASK )
{
osal_msg_deallocate( msg_ptr );
return ( INVALID_MSG_POINTER );
}
如果msg_ptr的指针指向地址不正确,直接释放动态申请的空间。
OSAL_MSG_ID( msg_ptr ) = destination_task;
// queue message
osal_msg_enqueue( &osal_qHead, msg_ptr );
将信号放入消息队列中
// Signal the task that a message is waiting
osal_set_event( destination_task, SYS_EVENT_MSG );
设置为系统事件
return ( ZSUCCESS );
}
事实上考虑到小型单片机code与xdata,RAM都比较小这个事实,我并没有将消息以及消息队列这个功能移植过来。其实利用延时设置事件、直接设置事件以及15个事件号已经足以完成许多中小心的程序工程。再大点的工程可能小单片机也吃不消了,那个时候需要考虑的不是增加操作系统了 ,而是要考虑跟换单片机。
3.5:TICK,时间单位,硬件定时器
在介绍完任务和事件的加入之后,我们开始阐述一个隐蔽于LTOS操作系统身后但又十分重要的概念-----系统时钟。
LTOS需要用户提供周期性信号源,用于实现事件的定时判断事件延时时间到并将事件置位。节拍率应在每秒10 次到1000 次之间,或者说10 到1000Hz。时钟节拍率越高,系统的额外负荷就越重。时钟节拍的实际频率取决于用户应用程序的精度。TI在他们的zigbee协议栈TI-MAC1.2.1就是使用了1000HZ的时钟频率。在此提出一点异议请读者注意,笔者主要讲解的是将LTOS移植到“弱功能”单片机上使用,所以并不提倡使用太高精度的时钟频率,这样会给小型单片机单来太多的额外负担,建议使用100HZ的系统时钟。时钟节拍源可以是专门的硬件定时器,也可以是一些外部信号源。
对于osalTimerUpdate( UINT8 updateTime ) 函数分析
static void osalTimerUpdate( uint16 updateTime )
{
halIntState_t intState;
osalTimerRec_t *srchTimer;
osalTimerRec_t *prevTimer;
osalTimerRec_t *saveTimer;
HAL_ENTER_CRITICAL_SECTION( intState ); // Hold off interrupts.
宏,保存此时的EA寄存器值,然后关闭中断
// Update the system time
osal_systemClock += updateTime;
osal_systemClock 是一个全局变量,用于知道系统从启动到现在一直运行了多少个系统时钟,updataTime是时间走动刻度,一般取1。该函数在系统定时器的中断函数中调用。比如,系统的频率设置为100,那么也就是定时器的时间设置成10MS,每10MS进入一次定时器中断,然后执行该osalTimerUpdate()函数。
// Look for open timer slot
if ( timerHead != NULL )
{
对时间链表头的分析。头不为空就进入处理函数。那么时间链表头怎么样才不为空呢,也就是osalAddTimer()函数至少被调用了一次,换句话说,也就是只少有一个任务利用osal_start_timerEx()函数为其余任务或自己增加延时任务,时间链表中的节点的结构体中的timeout成员会被加入时间值来代表某个任务多久之后触发特定事件。这里听着可能有点绕口,不过慢慢往后看,就能明白。
// Add it to the end of the timer list
srchTimer = timerHead;
prevTimer = (void *)NULL;
// Look for open timer slot
while ( srchTimer )
{
// Decrease the correct amount of time
if (srchTimer->timeout <= updateTime)
如果时间链表中的某个节点结构体中timeout 成员变量值已经小于等于系统时间走动刻度,说明该节点的时间已到,也可以理解为到时候设置事件了。
srchTimer->timeout = 0;
直接将该小于等于系统时间走动刻度的变量清零。
else
srchTimer->timeout = srchTimer->timeout - updateTime;
如果timeout 成员变量值还比较大,说明他的时间还没有到,就必须减去系统时间走动刻度让他一步步减小,如同时光一点点流逝。
// When timeout, execute the task
if ( srchTimer->timeout == 0 )
{
时间已到的链表节点
osal_set_event( srchTimer->task_id, srchTimer->event_flag );
给指定任务号的任务设置事件。
// Take out of list
if ( prevTimer == NULL )
timerHead = srchTimer->next;
将该节点从链表中剔除
else
prevTimer->next = srchTimer->next;
// Next
saveTimer = srchTimer->next;
// Free memory
osal_mem_free( srchTimer );
由于时间链表中的节点空间是动态申请的,所以剔除完链表中的节点后,要将申请的空间释放。
srchTimer = saveTimer;
}
else
{
// Get next
prevTimer = srchTimer;
srchTimer = srchTimer->next;
向后继续寻找
}
}
}
HAL_EXIT_CRITICAL_SECTION( intState ); // Re-enable interrupts.
宏,还原中断值
}
任务的调度、事件的加入、系统时钟中的时间刻度的走动,这些智慧碰撞于一起最终使一个系统动态地运作了起来。
3.6:内存管理与分配
在前面讲解到的时间链表的建立、任务链表的建立都离不开内存的动态分配与释放,下面分析关于内存管理的几个函数。
在TI的zigbee程序TI-MAC1.2.1中,在XDATA定义了一个1024字节的数组,后续全部的动态空间申请与释放都是在这个数组中实现。
void osal_mem_init( void )
{
初始化函数,主要完成了内存分配数组的初始化
osalMemHdr_t *tmp;
// Setup a NULL block at the end of the heap for fast comparisons with zero.
tmp = (osalMemHdr_t *)theHeap + (MAXMEMHEAP / HDRSZ) - 1;
找到分配数组的尾部
*tmp = 0;
赋零,便于在使用该动态内存时判断是否到了尾部,也就是说判断是否还有空间能够分配
// Setup a small-block bucket.
tmp = (osalMemHdr_t *)theHeap;
找到分配数组的头
*tmp = SMALLBLKHEAP;
LTOS中将动态分配的空间划分为了两块,一个为小空间分配块ff1,专门用来负责分配给小于16个字节的空间分配。另一块为大空间分配块ff2,用于分配给大于16个字节的空间分配。将SMALLBLKHEAP这个常数赋值给头,SMALLBLKHEAP这个常数在TI-MAC1.2.1中设置为232,表示小空间分配块有232个字节长度,大空间分配块有(1024-232-2)个长度,为什么是1024-232-2而不是1024-232,读者读完下面的分析就能明白。如下图4
(原文件名:图片4.JPG)
引用图片
被分配的空间总是比实际要分配的空间多HDRSZ个字节,在8位单片机中,HDRSZ为2个字节,前一个字节用来表示这个分配空间被使用了没有,使用了就将该字节的最高位置一,没使用就清零。后一个字节表示该空间的长度。所以由于232个字节的长度的分配空间占用了两个字节的头,此时就必须1024-232-2。
// Setup the wilderness.
tmp = (osalMemHdr_t *)theHeap + (SMALLBLKHEAP / HDRSZ);
*tmp = ((MAXMEMHEAP / HDRSZ) * HDRSZ) - SMALLBLKHEAP - HDRSZ;
#if ( OSALMEM_GUARD )
ready = OSALMEM_READY;
#endif
// Setup a NULL block that is never freed so that the small-block bucket
// is never coalesced with the wilderness.
ff1 = tmp;
让ff1先指向大空间分配块,然后在该处给ff2分配一个0字节长度的空间,最后让ff1指向小空间分配块。这样就巧妙地将小空间分配块与大空间分配块分开了。值得注意的是,当小空间分配块ff1的空间被耗尽后也可以继续在大空间分配块ff2中分配。
ff2 = osal_mem_alloc( 0 );
ff1 = (osalMemHdr_t *)theHeap;
}
对于分配内存函数的分析:
void *osal_mem_alloc(UINT16 size )
{
osalMemHdr_t *prev;
osalMemHdr_t *hdr;
halIntState_t intState;
UINT16 tmp;
byte xdata coal = 0;
#if ( OSALMEM_GUARD )
// Try to protect against premature use by HAL / OSAL.
if ( ready != OSALMEM_READY )
{
osal_mem_init();
调用初始化函数,这里可能造成编译器的递归调用警告
}
#endif
size += HDRSZ;
预分配的长度加上标志头的长度,如前面所说,标志头的长度为两个字节,第一个字节表示该空间被使用没,第二个字节表示申请的长度
// Calculate required bytes to add to 'size' to align to halDataAlign_t.
if ( sizeof( halDataAlign_t ) == 2 )
{
size += (size & 0x01);
}
else if ( sizeof( halDataAlign_t ) != 1 )
{
const byte mod = size % sizeof( halDataAlign_t );
if ( mod != 0 )
{
size += (sizeof( halDataAlign_t ) - mod);
}
}
以上是对于size长度的一些计算,主要是针对不同位数的处理器,使size作一个靠齐处理。
HAL_ENTER_CRITICAL_SECTION( intState ); // Hold off interrupts.
宏,保存此时的EA寄存器值,然后关闭中断
// Smaller allocations are first attempted in the small-block bucket.
if ( size <= OSALMEM_SMALL_BLKSZ )
{
如果要分配的空间比较小,小于OSALMEM_SMALL_BLKSZ(16),就直接在小长度分配空间分配
hdr = ff1;
}
else
{
否则,说明要分配的空间比较大,在大长度分配空间分配
hdr = ff2;
}
tmp = *hdr;
将此处的空间头的值取出来赋给tmp,也就是上面介绍的头两个字节
do
{
if ( tmp & OSALMEM_IN_USE )
判断tmp的最高位是否被置一了,也就是判断这块空间是否被用了,如果被用了则继续往后寻找。
{
tmp ^= OSALMEM_IN_USE;
coal = 0;
当空间被动态分配和释放多次以后,就会形成一种每个动态分配内存机制都会碰到的问题----内存碎片,当程序要分配一个连续的大空间时,就需要将以前被分散的小空间联合起来,coal这个变量就是用在将几块连续打散的空间合并的判断变量,判断coal即可知道内存快是否连续。如下图5中,红色的块表示未使用的块,黄色的块表示已经使用的块,如果要在这个空间中分配大小为3的内存,就必须跨过这些黄色的块,使用最后一个连续的红块。而前面的红块成为了内存碎片。
(原文件名:图片5.JPG)
引用图片
}
else
{
if ( coal != 0 )
{
*prev += *hdr;
这里的一段程序主要是用来判断如何分配一块合适的内存给应用程序,当内存区没有内存碎片时,这时申请内存就直接到内存分配块分配,这个时候不用操心内存碎片以及内存合并的事,直截了当分配就是。一旦需要分配大的内存并且存在内存碎片时,就必须操心更多的事了。判断以下几种情况:
1是否有一块没使用的空间大于欲分配的空间,有则直接分配。
2若没有,则合并内存块,直到合并到连续的块并且空间足够大
3 若没有连续可加起来满足条件的块,则((void *)NULL).
if ( *prev >= size )
{
hdr = prev;
tmp = *hdr;
break;
}
}
else
{
if ( tmp >= size )
{
break;
}
coal = 1;
prev = hdr;
}
}
hdr = (osalMemHdr_t *)((byte *)hdr + tmp);
tmp = *hdr;
if ( tmp == 0 )
{
hdr = ((void *)NULL);
break;
}
} while ( 1 );
直到按照以上条件分配完成,退出循环
if ( hdr != ((void *)NULL))
{
下面的程序主要将被分配的空间与刚才分配的实际空间作一个差值,比较这个差值的大小。如果这个值很大,说明这块空间还有很多能够利用的余地,于是先将这个空间重新分割开以便接下来的继续分配使用,然后在已经分配的空间上打上已用的标记;如果这个值不大,说明空间利用的差不多了,还算合理了,就不分割,直接打上已用的标记。
tmp -= size;
// Determine whether the threshold for splitting is met.
if ( tmp >= OSALMEM_MIN_BLKSZ )
{
// Split the block before allocating it.
osalMemHdr_t *next = (osalMemHdr_t *)((byte *)hdr + size);
*next = tmp;
*hdr = (size | OSALMEM_IN_USE);
分割在此
}
else
{
*hdr |= OSALMEM_IN_USE;
}
hdr++;
}
HAL_EXIT_CRITICAL_SECTION( intState ); // Re-enable interrupts.
return (void *)hdr;
}
void osal_mem_free( void *ptr )
{
osalMemHdr_t *currHdr;
halIntState_t intState;
#if ( OSALMEM_GUARD )
// Try to protect against premature use by HAL / OSAL.
if ( ready != OSALMEM_READY )
{
osal_mem_init();
}
#endif
HAL_ENTER_CRITICAL_SECTION( intState ); // Hold off interrupts.
currHdr = (osalMemHdr_t *)ptr – 1;
*currHdr &= ~OSALMEM_IN_USE;
释放空间的函数相对于申请空间的函数要简单许多,直接将欲释放空间的头的被使用标志清除了即可
if ( ff1 > currHdr )
{
这里可能会让不少读者疑惑:ff1为数组的头,怎么也不可能指向比currHdr还靠后的的地址,怎么可能大于currHdr呢。其实这是有可能的,上面已经提到,当ff1的空间被分配完时,是可以通过osal_mem_kick( void )函数,让ff1指向ff2的地址继续分配的,这种情况下,ff1被移动到了后面去了,但是currHdr可能是ff1在数组头时分配的一块空间,这时必须让ff1前移来表示着ff1处继续有可用空间了。
ff1 = currHdr;
}
HAL_EXIT_CRITICAL_SECTION( intState ); // Re-enable interrupts.
}
五 内核移植:
笔者将LTOS移植出来了,放入STC12C60S2中运行起来,LTOS工程中建立了六个任务,每个任务负责点亮一个由P1口控制的小灯,产生跑马灯效果。移植工程见万能配置文件夹。下面介绍一下移植工程中需要修改的地方。
5.1:与芯片相关数据结构
Type.h 中
// Data
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
// Unsigned numbers
typedef unsigned char UINT8;
typedef unsigned char byte;
typedef unsigned short UINT16;
typedef unsigned short INT16U;
typedef unsigned long UINT32;
typedef unsigned long INT32U;
typedef unsigned char halDataAlign_t;
// Signed numbers
typedef signed char INT8;
typedef signed short INT16;
typedef signed long INT32;
5.2与芯片相关的中断开关:
Type.h 中
#define HAL_ENABLE_INTERRUPTS() st( EA = 1; )
#define HAL_DISABLE_INTERRUPTS() st( EA = 0; )
#define HAL_INTERRUPTS_ARE_ENABLED() (EA)
typedef unsigned char halIntState_t;
#define HAL_ENTER_CRITICAL_SECTION(x) st( x = EA; HAL_DISABLE_INTERRUPTS(); )
#define HAL_EXIT_CRITICAL_SECTION(x) st( EA = x; )
#define HAL_CRITICAL_STATEMENT(x) st( halIntState_t s; HAL_ENTER_CRITICAL_SECTION(s); x; HAL_EXIT_CRITICAL_SECTION(s); )
5.3:芯片定时器实现内核TICK
定时器的更改,本例子中利用STC12C60S2的PCA作为系统定时器
Timer6.c中
初始化程序:
void OSAL_TIMER_TICKINIT(void)
{
CMOD = 0X80;
CCON = 0X00;
CL = 0X00;
CH = 0X00;
IP |= 0x80;
IPH |= 0x80;
CCAP0L = channel0_10ms_l;
CCAP0H = channel0_10ms_h;
CCAPM0 = 0X49;
}
中断函数:
void timer_interrupt(void ) PCA_Routine
{
halIntState_t intState;
OSAL_TIMER_TICKSTOP();
HAL_ENTER_CRITICAL_SECTION( intState );
CCF0 = 0;
osal_update_timers( );
CCAP0L += channel0_10ms_l;
CCAP0H += channel0_10ms_h;
HAL_EXIT_CRITICAL_SECTION(intState);
OSAL_TIMER_TICKSTART();
}
5.4:内存中可分配单元的定义
本例子中分配STC12C60S2单片机的XDATA中768个字节的空间供系统调用,另外一些可用于其余的全局变量或作为缓冲
Osal_memory.c中
#define MAXMEMHEAP 768
#if defined( EXTERNAL_RAM )
static byte *theHeap = (byte *)EXT_RAM_BEG;
#else
static halDataAlign_t xdata _theHeap[ MAXMEMHEAP / sizeof( halDataAlign_t ) ];
static byte *theHeap = (byte *)_theHeap;
#endif
结束语:LTOS讲解以及移植过程全部完成了,由于本人接触操作系统的经验并不是特别丰富,所以以上讲解中难免不出现错误或有出入的地方,望各位经验丰富的研究人员和学者加以指正。
最后,谈一点点我个人对于技术研发这个职位的看法,做为技术人员,大家都觉得工资高,工作稳定,还能学到很多 的东西。是大部份走出校门或性格内向,或希望过平静生活的人的必然选择。其实,你们有没有问过自己,这条路到底走对了吗? 一个刚毕业的大学生,从事销售和从事技术两种不同的工作,可能工资的差距会达到数倍之远。对于初出校门的人来说,不无一种极端的诱惑力。刚毕业的年青人,当然会果断的选择技术之路。 两年后,我们再看看,由于经验的积累,做业务的积累了部份客户资源,做技术的积累了好的经验,在各自的领域内都大展开了手脚,收入也基本接近了。 再以后呢,技术之路越来越难走,毕竟做技术需要的大量的时间和精力,否则就跟不上现在时代的技术更新了,做业务的呢,客户群越来越大,经验越来越丰富,谁的收入会更高? 两种不同的职业,它们有着各自不同的特点,技术行业是个撑不死,饱不了的地方,而销售行业则是没有尽头的发展之路。 过了三十岁,大家会选择什么呢,结婚、生子,人生的一条老路, 到了三十岁,你还有自信面对繁重的工作吗?你有刚出社会的人的活力吗?你能和他们比工作时间,玩命地在老板面前表现吗?你能丢下妻儿出差一、两个月吗?
比之于我们的生活和人际关系及工作,那些从事售前和市场开发的朋友,却有比我们多的多的工作之外的时间,甚至他们工作的时间有的时候是和生活的时间是可以兼顾的,他们可以通过市场开发,认识各个行业的人士,可以认识各种各样的朋友,他们比我们坦率说更有发财和发展的机会,只要他们跟我们一样勤奋。(有一种勤奋的普通人,如果给换个地方,他马上会成为一个勤奋且出众的人。) 有人会说,我有了技术! 技术经验是什么?一些老的,过去了的东西,他代表着你所留恋的过去,你所放不下的那一部份,你会以经验来判别事物,选择工作方法。在新老技术交替的时间内,经验可以起到承前启后的作用,让你威风八面。可是,你还会用到多少十年以前的芯片呢? 大家所掌握的技术终会过时,脑子僵化的时候总会到来。那时,你何去何从?
当然,我并不是说技术就一无是处了,许多做技术的朋友都是贫苦出生,也有许多的朋友是不得以慢慢走上这条道路,也许有的朋友完全出于兴趣爱好,出于兴趣爱好的我先撇开不谈,因为爱好这个东西可以使你无欲无求而只求与它。我们都需要靠自己的双手去创造未来,我只是想说不要一辈子只靠技术活着,不要一辈子只会一点技术。其实做技术的人往往思维慎密,做事谨慎,并且由于以前有很相当长时间的技术积累,以至于很容易跟讨论技术的客户沟通起来,所以说做技术的人很应该多拿出一点时间和精力去培养别的才能(比如口才,销售,管理)。我只是奉劝那些学习技术的朋友,千万不要拿科举考试样的心态去学习技术,对技术的学习几近的痴迷,想掌握所有所有的技术,以让自己成为技术领域的权威和专家,但是我们的国家没有那么多的研究院去容下我们这么多的高手,等到年纪来了就开始感叹怀才不遇了。 技术仅仅是一个工具,是你在人生一个阶段生存的工具,你可以一辈子喜欢他,但最好不要一辈子靠它生存。摆弄电子是我的爱好,也是一个工具,我跟大家一样每天像毒瘾犯了似的偷偷跑到Ourdev论坛里瞄瞄有无新的帖子,也潜水很久,今天终于按捺不住写出一些东东,仅属于个人愚见,欢迎大家拍砖,。