ZigBee是目前比较流行的一种低功耗无线组网技术,主要用于智能家居控制以及智能工业生产。ZigBee大的特点就是低功耗、自组网。
说到ZigBee就不得不提IEEE802.15和ZigBee联盟,他们公共制定了ZigBee协议栈的标准。组网过程就是基于ZigBee协议栈,协议栈完成了绝大部分的工作,留给没У木褪怯τ贸绦蚪涌凇P檎痪拖褚桓霾僮飨低骋谎没е恍枰ㄖ朴τ贸绦蚓涂梢允褂谩�
先来看一下ZigBee协议栈架构,操作系统下面的可以当做BootLoader,操作系统上面的可以看做应用程序。对于用户来说,只要了解操作系统,会定制task,那么就可以使用协议栈了。
接下来我们以TI公司的ZigBee协议栈为标准,了解一下osal操作系统机制,以方便后续定制task。
Osal源于一种简单的操作系统思想---轮询。在ZigBee协议栈中,OSAL负责调度各个任务的运行,如果有事件发生了,则会调用相应的事件处理函数进行处理。那么,事件和任务的事件处理函数是如何联系起来的呢?
ZigBee中采用的方法是:建立一个事件表,保存各个任务的对应的事件,建立另一个函数表,保存各个任务的事件处理函数的地址,然后将这两张表建立某种对应关系,当某一事件发生时则查找函数表找到对应的事件处理函数即可。
在ZigBee协议栈中,有三个变量至关重要。
● tasksCnt—该变量保存了任务的总个数。
该变量的声明为:uint8 tasksCnt,其中uint8的定义为:typedef unsigned char uint8
● tasksEvents—这是一个指针,指向了事件表的首地址。
该变量的声明为:uint16 *tasksEvents,其中uint16的定义为:typedef unsigned short uint16
● tasksArr—这是一个数组,数组的每一项都是一个函数指针,指向了事件处理函数。
该数组的声明为:const pTaskEventHandlerFn tasksArr[],其中pTaskEventHandlerFn的定义为:typedef unsigned short (*pTaskEventHandlerFn)( unsigned char task_id, unsigned short event ),这是定义了一个函数指针。tasksArr数组的每一项都是一个函数指针,指向了事件处理函数。
事件表和函数表的关系如下图
我们现在来总结下OSAL的工作原理:通过tasksEvents指针访问事件表的每一项,如果有事件发生,则查找函数表找到事件处理函数进行处理,处理完后,继续访问事件表,查看是否有事件发生,无限循环。OSAL就是一种基于事件驱动的轮询式操作系统。事件驱动是指发生事件后采取相应的事件处理方法,轮询指的是不断地查看是否有事件发生。
下面从代码中看一下osal运行机制。在Zmain文件夹下有个Zmain.c文件,打开该文件可以
找到main()函数,这就是整个协议栈的入口点。main()函数原型如下:
int main( void )
{
// Turn off interrupts
osal_int_disable( INTS_ALL );
// Initialization for board related stuff such as LEDs
HAL_BOARD_INIT();
// Make sure supply voltage is high enough to run
zmain_vdd_check();
// Initialize board I/O
InitBoard( OB_COLD );
// Initialze HAL drivers
HalDriverInit();
// Initialize NV System
osal_nv_init( NULL );
// Initialize the MAC
ZMacInit();
// Determine the extended address
zmain_ext_addr();
// Initialize basic NV items
zgInit();
#ifndef NONWK
// Since the AF isn't a task, call it's initialization routine
afInit();
#endif
// Initialize the operating system
osal_init_system();
// Allow interrupts
osal_int_enable( INTS_ALL );
// Final board initialization
InitBoard( OB_READY );
// Display information about this device
zmain_dev_info();
/* Display the device info on the LCD */
#ifdef LCD_SUPPORTED
zmain_lcd_init();
#endif
#ifdef WDT_IN_PM1
/* If WDT is used, this is a good place to enable it. */
WatchDogEnable( WDTIMX );
#endif
osal_start_system(); // No Return from here
return 0; // Shouldn't get here.
} // main()
在osal_start_system()函数之前的函数都是对板载硬件以及协议栈进行的初始化,直到调用osal_start_system()函数,整个ZigBee协议栈才算是真正地运行起来了。硬件驱动不需要多看,我们跳转到osal_start_system(),查看一下它的原型
void osal_start_system( void )
{
#if !defined ( ZBIT ) && !defined ( UBIT )
for(;;) // Forever Loop
#endif
{
uint8 idx = 0;
osalTimeUpdate();
// This replaces MT_SerialPoll() and osal_check_timer().
Hal_ProcessPoll();
do {
// Task is highest priority that is ready.
if (tasksEvents[idx])
{
break;
}
} while (++idx < tasksCnt);
if (idx < tasksCnt)
{
uint16 events;
halIntState_t intState;
HAL_ENTER_CRITICAL_SECTION(intState);
events = tasksEvents[idx];
// Clear the Events for this task.
tasksEvents[idx] = 0;
HAL_EXIT_CRITICAL_SECTION(intState);
events = (tasksArr[idx])( idx, events );
HAL_ENTER_CRITICAL_SECTION(intState);
// Add back unprocessed events to the current task.
tasksEvents[idx] |= events;
HAL_EXIT_CRITICAL_SECTION(intState);
}
#if defined( POWER_SAVING )
// Complete pass through all task events with no activity?
else
{
// Put the processor/system into sleep
osal_pwrmgr_powerconserve();
}
#endif
}
}
首先看到,真个osal是在一个for循环中执行的,这就意味正后面的事件一直在不停的重复,也是osal的基础。
第6行,定义了一个变量idx,用来在事件表中索引。
第7-9行,更新系统时钟,同时查看硬件方面是否有事件发生,如串口是否收到数据、是否有按键按下等信息,这部分内容在此可以暂时不予考虑。
第10-16行,使用do-while循环查看事件表是否有事件发生。分析一下这个循环,如果有事件发生,那么就跳出循环,去后面的代码处理事件。如果没有事件,那么久继续扫描表中的下一项。当然真个循环的次数不得多于tasksCnt,因为它记录着事件的总数。
第18~35行是事件的处理过程,23和27行规定了一个临界区。24行取出事件,保存在events变量。26行则将表中的事件清零,因为事件已经被取出将要处理。27行根据id找到对应的函数表,执行处理函数,而且拿到返回值(后面解释返回值)。31和34行又是一个临界区,33行又将事件重新赋值。
上面了解了osal的基本原理,就是两张表的查询和对应。那么其中又有一个events成了重点,为什么处理完事件时候又返回一个events(24行),而且还把events放回到事件表(33行)。
ZigBee协议栈使用一个unsigned short型的变量,因为unsigned short类型占两个字节,即16个二进制位,因此,可以使用每个二进制位表示一个事件,我们来看下协议栈定义的系统事件SYS_EVENT_MSG,十六进制:0x8000,二进制:0b1000000000000000。它用的就是高位来表示该事件:
可以看出在一个任务中多只能有16个事件,因为events是一个16位数据。
在系统初始化时,所有任务的事件初始化为0,因此,第10行通过tasksEvents[idx]是否为0来判断是否有事件发生,如果有事件发生了,则跳出循环。
29行执行完事件处理函数后,需要将未处理的事件返回,也就是说事件处理函数的返回值保存了未处理的事件,将该事件在写入事件表中,以便于下次进行处理。看一下下面的处理函数
uint16 SampleApp_ProcessEvent( uint8 task_id, uint16 events )
{
if ( events & SAMPLEAPP_SEND_PERIODIC_MSG_EVT )
{
// Send the periodic message
SampleApp_SendPeriodicMessage();
// Setup to send message again in normal period (+ a little jitter)
osal_start_timerEx( SampleApp_TaskID, SAMPLEAPP_SEND_PERIODIC_MSG_EVT,
(SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT + (osal_rand() & 0x00FF)) );
// return unprocessed events
return (events ^ SAMPLEAPP_SEND_PERIODIC_MSG_EVT);
}
// Discard unknown events
return 0;
}
前面已经说到,事件用一个二进制位1来表示,那么一个“与”操作就可以判断出来到底有没有事件。后return采用“异或”,这样可以将已经处理的事件清除掉。不清楚的小伙伴可以举一个实际的例子,仔细计算一番之后你会发现的确是这种写法。。。
在ZigBee协议栈中,用户可以定义自己的事件,但是,协议栈同时也给出了几个已经定义好的事件,由协议栈定义的事件成为系统强制事件(Mandatory Events),SYS_EVENT_MSG就是其中的一个事件,SYS_EVENT_MSG的定义如下:
那SAMPLEAPP_SEND_PERIODIC_MSG_EVT事件 是不是就是用户自己定义的事件了?是的!我们来看下它的定义:
提到事件,我们就不得不提到消息。事件是驱动任务去执行某些操作的条件,当系统中产生了一个事件,OSAL将这个事件传递给相应的任务后,任务才能执行一个相应的操作(调用事件处理函数去处理)。
通常某些事件发生时,又伴随着一些附加信息的产生,例如:从天线接收到数据后,会产生AF_INCOMING_MSG_CMD消息,但是任务的事件处理函数在处理这个事件的时候,还需要得到收到的数据。
因此,这就需要将事件和数据封装成一个消息,将消息发送到消息队列,然后在事件处理函数中就可以使用osal_msg_receive,从消息队列中得到该消息。如下代码可以获得指向从消息队列中得到消息的指针。
在使用ZigBee协议栈进行应用程序开发时,如何在应用程序中添加一个新任务呢?
打开OSAL_SampleApp.c文件,可以找到数组tasksArr[]和函数osalInitTasks()。tasksArr[]数组里存放了所有的事件处理函数的地址;osalInitTasks()是OSAL的任务初始化函数,所有任务的初始化工作都在这里边完成,并且自动给每个任务分配一个ID。
因此,要添加新任务,只需要编写两个函数:
● 新任务的初始化函数。
● 新任务的事件处理函数。
将事件处理函数的地址加入tasksArr[]数组,如下代码所示。
将新任务的初始化函数添加在osalInitTasks()函数的后,如下代码所示。
我们需要注意的是:
tasksArr[]数组里各事件处理函数的排列顺序要与osalInitTasks函数中调用各任务初始化函数的顺序保持一致,只有这样才能保证当任务有事件发生时会调用每个任务对应的事件处理函数。为了保存osalInitTasks()函数所分配的任务ID,需要给每一个任务定义一个全局变量。如在SampleApp.c文件中定义了一个全局变量SampleApp_TaskID,并且在osalInitTasks()函数中进行了赋值。
我们现在总结下OSAL的运行机理:
● 通过不断地查询事件表来判断每个任务中是否有事件发生,如果有事件发生,则查找函数表找到对应的事件处理函数对事件进行处理。
● 事件表使用数组来实现,数组的每一项对应一个任务的事件,每一位表示一个事件;函数表使用函数指针数组来实现,数组的每一项是一个函数指针,指向了事件处理函数。