ZigBee协议栈
一、ZigBee协议栈的构成
类似计算机网络中的ISO模型,ZigBee协议栈的实现也采用了分层的思想。由底层到高层依次是:物理层、介质访问层、网络层和应用层(而应用层要可分为:应用程序支持子层、应用程序框架层和ZDO设备对象层)。具体见下图
物理层(PHY):负责将数据通过发射天线发送出去以及从天线接收数据
介质访问控制层(MAC):网络的发现、网络的形成,以及提供点对点通信的数据确认(Per-hop Acknowledments)。但不支持多跳(Multi-hop)、网型网络。
以上两层都是由IEEE802.15.4规范定义的。
网络层(NWK):对网型网络提供支持(如:全网内发送广播包、单播数据包选择路由等功能)。此外也提供网络安全策略,用户也可以自己选择所需安全策略。
应用层:
应用程序支持子层(APS):提供一些API函数。绑定表也存储在该层。还有该层中的设备对象ZDO运行在端口0的应用程序,提供一些网络管理的函数,主要对应用程序支持子层、网络层初始化和管理。
ZigBee协议栈的实现也采用了分层的思想,这种思想的好处在于上层实现的功能对于下层来说是不知道,上层可以调用下层提供的函数来实现某些功能。因此,各层之间的数据传递是通过服务接入点(Service Access Point简称:SAP)来实现的。而服务接入点主要两种类型:一种是 数据传输服务接入点,另一种是管理的服务接入点。建立工程时,也表现了这个分层思想,便于代码的编写和管理。(见下图)
而整个协议开始执行的位置和其他编程一样也是从main()开始的。
/********************************************************************* * @fn main * @brief First function called after startup. * @return don't care */ 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(); #if defined ZCL_KEY_ESTABLISH // Initialize the Certicom certificate information. zmain_cert_init(); #endif // 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 至此,ZigBee才真正运行期整个协议栈。//osal_start_system();相当于是一个小型操作系统的入口函数。除非板子掉电,否则进入后将没有返回,一直运行里边的代码return 0;// Shouldn't get here.} // main()
上面提到osal_start_system();相当于是一个小型操作系统的入口函数。除非板子掉电,否则进入后将没有返回,一直运行里边的代码。下面就得接受OSAL小型操作系统的原理。
二、ZigBee协议栈OSAL小型操作系统的介绍
ZigBee协议栈是对ZigBee协议的代码实现。因此,ZigBee协议栈是一个函数集,便于ZigBee协议所规定的功能的实现。这里有一个叫OSAL (操作系统抽象层,Operating System Abstraction),它是是一个小型实时操作系统。
OSAL提供的主要功能: 任务的注册、初始化、启动; 任务间的同步、互斥; 中断处理; 存储器分配和管理。
OSAL的工作原理:
通过不断地查询时间表来判断是否有事件的发生,如果有事件发生,则查找函数表找到对应的事件处理函数对事件进行处理
事件表使用数组实现,数组的每一项对应一个任务事件,每一位 表一个事件;函数表使用函数指针数组来实现。数组的每一项是一个函数指针指向事件处理函数。
1、操作系统(OS)基础知识补充:
资源(Resource):任何任务所占用的实体都可以称为资源。如:一个变量、数组、结构体
共享资源(Shared Resource):可以被两个任务使用的资源为共享资源。(要避免两个任务同时对共享资源的操作,每个任务访问资源时,必须独自资源)。
任务(Task):一个任务,就是一个线程。是一简单的执行过程。在任务执行过程中独占CPU。
多任务运行(Muti-task Running):CPU在某一时刻只能有一个任务运行。但CPU采用任务调度策略将多了任务进行调度。每个任务执行特定的时间,时间片到后,就进行任务的切换。每个任务的执行时间都很短,任务切换频繁。造成多个任务同时切换的“假象”
内核(Kernel):在多任务系统中,内核负责管理各个任务。主要:为每个任务分配CPU时间;任务的调度;负责任务间的通信;任务的切换。
互斥(Mutual Exclusion):多个任务间通信采用的是共享数据结构,在CC2530中,所有的任务都单一的地址空间之下。共享数据结构包括:全局变量、指针、缓冲区等。对共享数据的访问必须保证对共享数据结构的写操作具有唯一性。避免晶振和数据不同步。保护共享数据资源的方法有:关闭中断、使用测试并置位指令(T&S指令)、禁止任务的切换、使用信号量。关闭中断是最常用的。
消息队列(Message Queue):消息队列用于任务间传递消息,通常包含数据任务间同步的信息。(记得它的作用是在:任务间 哦)。通过内河提供的服务、任务或者中断服务程序将一条消息放入队列。然后,其他任务可以使用内核提供的服务从小心队列中获取属于自己的消息。通常传递的是指针,能降低开支。
2、OSAL(操作系统抽象层:Operating System Abstract Layer)的运行机理。
OSAL就是一种支持多任务运行的系统资源分配机制(主要包括:任务切换、提供内存管理功能)。而且 OSAL是一个基于事件驱动的轮询式的小型操作系统。OSAL提供了最底层的代码,我们开发只要在应用层进行程序开发就行啦。应用程序框架包含最多240个应用程序对象,每个应用程序对象运行在不同的端口上。端口的作用是区分不同的应用成寻对象。一个任务就是一个任务。
在上次实验2中,比较重要的两个函数是extern void GenericApp_Init(byte task_id);和extern UINT16 GenericApp_ProcessEvent(byte task_id,UINT16 evens);。
GenericApp_Init(byte task_id);是对任务的初始化,我觉得是给地事件id号比较合适,以唯一确定某一事件,就叫事件。而GenericApp_ProcessEvent(byte task_id,UINT16 evens);判断参数传递的事件的类型,并找到相应的时间处理函数并执行。因此,该函数可叫:任务事件处理函数。
那么事件和任务事件处理函数是怎样的唯一确定的呢?
建立一个事件表(即是一个数组)和一个任务事件处理函数表(也是一个数组),这样就能用数组中的下表idx唯一确定。具体结构:
//OSAL_Tasks.h文件中进行声明 typedef unsigned short (*pTaskEventHandlerFn)( unsigned char task_id, unsigned short event );//函数指针 extern const pTaskEventHandlerFn tasksArr[];//任务事件处理函数表(也是一个数组) extern const uint8 tasksCnt;//任务事件的个数 extern uint16 *tasksEvents;//事件表
//OSAL_GenericApp.c文件中进行引用 //任务事件处理函数表(也是一个数组) const pTaskEventHandlerFn tasksArr[] = { macEventLoop, nwk_event_loop, Hal_ProcessEvent, #if defined( MT_TASK ) MT_ProcessEvent, #endif APS_event_loop, #if defined ( ZIGBEE_FRAGMENTATION ) APSF_ProcessEvent, #endif ZDApp_event_loop, #if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT ) ZDNwkMgr_event_loop, #endif GenericApp_ProcessEvent }; const uint8 tasksCnt = sizeof( tasksArr ) // sizeof( tasksArr[0] );//任务事件的个数 uint16 *tasksEvents;//事件表OSAL具体工作原理:通过taskEvents指针访问事件表的每一项,如果有事件发生,则查找函数表找到事件函数处理函数并进行处理,处理完毕,继续访问事件表,查看是否有事件的发生,无限循环。因此,OSAL是一种基于事件驱动的轮询式操作系统。
系统启动后,首先是硬件各种的初始化,最后是调用osal_start_system( ).进入操作系统。具体代码:
/********************************************************************* * @fn osal_start_system * @brief * This function is the main loop function of the task system. It * will look through all task events and call the task_event_processor() * function for the task with the event. If there are no events (for * all tasks), this function puts the processor into Sleep. * This Function doesn't return. * @param void * @return none */ void osal_start_system( void ) { #if !defined ( ZBIT ) && !defined ( UBIT ) //此行代码略过 for(;;) // Forever Loop 死循环 #endif //此行代码略过 { uint8 idx = 0; osalTimeUpdate(); //更新系统时钟 Hal_ProcessPoll(); // This replaces MT_SerialPoll() and osal_check_timer(). 查看硬件方面是否有事件的发生。//依次tasksEvents[]中是否事件的发生,有则不再查询。 do { if (tasksEvents[idx]) // Task is highest priority that is ready. 查看 任务时间tasksExents表中下标为idx的元素是否被标记为有事件 { break; //下标为idx的元素有 事件存在,则不再查询,否则继续查询下一个idx } } while (++idx < tasksCnt); if (idx < tasksCnt) { uint16 events; halIntState_t intState;//定义intState共享变量,这个halIntState_t类型到底是怎样呢??? HAL_ENTER_CRITICAL_SECTION(intState); //关闭中断 events = tasksEvents[idx]; //保存任务事件tasksEvents[idx]到events tasksEvents[idx] = 0; // Clear the Events for this task. 对tasksEvents[idx]置为0 HAL_EXIT_CRITICAL_SECTION(intState); //宏用以恢复中断 events = (tasksArr[idx])( idx, events );//执行与tasksEvents[idx]任务事件对应的 任务事件处理函数表中tasksArr[idx],下面会用实验2中的调用GenericApp_processEvent()为例进行分析 HAL_ENTER_CRITICAL_SECTION(intState);//关闭中断 tasksEvents[idx] |= events; // Add back unprocessed events to the current task. 同一任务事件是可有多个事件(最多可有16个,因为 uint16 *tasksEvents中是uint16类型的,从uint16 events;等也可以看出)的,此句是把未处理的事件放回在任务taskEvents[idx]中,待下一次循环继续执行。 HAL_EXIT_CRITICAL_SECTION(intState);//宏用以恢复中断 } #if defined( POWER_SAVING ) else // Complete pass through all task events with no activity? { osal_pwrmgr_powerconserve(); // Put the processor/system into sleep } #endif } }任务事件处理函数中是怎样处理事件并返回未处理的事件
//消息处理函数 UINT16 GenericApp_ProcessEvent(byte task_id,UINT16 events) { afIncomingMSGPacket_t* MSGpkt;//MSGpkt用于指向接收消息结构体的指针 if(events&SYS_EVENT_MSG)//这里只处理SYS_EVENT_MSG一个事件,其他的事件都没有处理,下面是的消息队列中的一个消息或多个消息是附在这个事件上的,而不能把事件和消息混为一谈。区别见下面的分析 { MSGpkt=(afIncomingMSGPacket_t*)osal_msg_receive(GenericApp_TaskID);//osal_msg_receive()从消息队列上接收消息 while(MSGpkt) { switch(MSGpkt->hdr.event) //判断事件的类型 { case AF_INCOMING_MSG_CMD: //接受到新数据的消息的ID是AF_INCOMING_MSG_CMD,这个宏是在协议栈中定义好的值为0x1A //接受到的是无线数据包 GenericApp_MessageMSGCB(MSGpkt);//功能是完成对接受数据的处理 break; default: break; } osal_msg_deallocate((uint8 *)MSGpkt);//接收到的消息处理完后,释放消息所占的存储空间 MSGpkt=(afIncomingMSGPacket_t*)osal_msg_receive(GenericApp_TaskID); //处理完一个消息后,再从消息队列里接受消息,然后对其进行相应处理,直到所有消息处理完 } return (events ^ SYS_EVENT_MSG);//使用异或运算。 } return 0; }
事件和消息的区别:以上面的SYS_ENENT_MSG和AF_INCOMING_CMD为例,说说区别和内在联系。
上面谈到tasksEvents[idx]中因为是unit16型的变量,每个二进制位表一个事件,则可最多有16个事件。事件可以自己定义,协议栈也有已经定义好的事件即系统强制事件,如
SYS_EVENT_MSG就是其中的一个。定义如下:
Filename: comdef.h #define SYS_EVENT_MSG 0x8000 // A message is waiting event而SYS_EVENT_MSG是一个事件集,包括以下事件
#define AF_INCOMING_MSG_CMD 0x1A // Incoming MSG type message 表收到一个新的无线数据
#define ZDO_STATE_CHANGE 0xD1 // ZDO has changed the device's network state 当网络状态发生变化时
#define ZDO_CB_MSG 0xD3 // ZDO incoming message callback 指示每一个注册的ZDO响应消息
#define AF_DATA_CONFIRM_CMD 0xFD // Data confirmation 调用AF_DataRequest()发生数据时,有需要确认信息,该事件如此相关。
从上面看来,这个几个宏,从目前看我的理解是有偏差的。
3、OSAL消息队列
事件和消息的区别
事件:是驱动任务区执行某些操作的条件,当系统中产生一个事件,OSAL将这个时间传递给相应的任务后,任务才能执行一个相应的操作(调用时间处理函数去处理)
消息:事件和数据的封装就成了一个消息。(事件的发生有伴随这附加信息的产生,如天线接收到数据后,产生AF_INCOMING_MSG_CMD消息,但任务的时间处理这个事件时,还需要得到接收到的数据)。然后,将消息发送到消息队列中,然后,在时间处理函数中就可以使用osal_msg_receive,从消息队列总得到该消息。代码如下:
MSGpkt=(afIncomingMSGPacket_t*)osal_msg_receive(GenericApp_TaskID);//osal_msg_receive()从消息队列上接收消息OSAL里有一个消息对垒,每一个消息都会被放到这个消息队列中去,每当任务接收到事件后,可以从消息队列中获得属于自己的消息。然后调用消息对应的函数去处理即可,如下:
if(events&SYS_EVENT_MSG)//这里只处理SYS_EVENT_MSG一个事件,其他的事件都没有处理,下面是的消息队列中的一个消息或多个消息是附在这个事件上的,而不能把事件和消息混为一谈。 { MSGpkt=(afIncomingMSGPacket_t*)osal_msg_receive(GenericApp_TaskID);//osal_msg_receive()从消息队列上接收消息 while(MSGpkt) { switch(MSGpkt->hdr.event) //判断事件的类型 { case AF_INCOMING_MSG_CMD: //接受到新数据的消息的ID是AF_INCOMING_MSG_CMD,这个宏是在协议栈中定义好的值为0x1A //接受到的是无线数据包 GenericApp_MessageMSGCB(MSGpkt);//功能是完成对接受数据的处理
4、OSAL添加新任务。
上面已经知道OSAL的基本原理,从原理可知,要做一件事(比如实验2中的 点对点实验中的协调器中接收数据任务为例。)就得添加事件(新任务的初始化函数)和处理事件的函数(新任务的事件处理函数)。
tasksArr[]数组存放所有任务的处理函数的地址
// The order in this table must be identical to the task initialization calls below in osalInitTask. const pTaskEventHandlerFn tasksArr[] = { macEventLoop, nwk_event_loop, Hal_ProcessEvent, #if defined( MT_TASK ) MT_ProcessEvent, #endif APS_event_loop, #if defined ( ZIGBEE_FRAGMENTATION ) APSF_ProcessEvent, #endif ZDApp_event_loop, #if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT ) ZDNwkMgr_event_loop, #endif GenericApp_ProcessEvent };而oslInitTasks是OSAL的任务初始化函数。所有的任务的初始都是在这里面完成的。并自动给每一个任务分配一个ID。
/********************************************************************* * @fn osalInitTasks * * @brief This function invokes the initialization function for each task. * * @param void * * @return none *///OSAL_GenericApp.c void osalInitTasks( void ) { uint8 taskID = 0; tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt); osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt)); macTaskInit( taskID++ ); nwk_init( taskID++ ); Hal_Init( taskID++ ); #if defined( MT_TASK ) MT_TaskInit( taskID++ ); #endif APS_Init( taskID++ ); #if defined ( ZIGBEE_FRAGMENTATION ) APSF_Init( taskID++ ); #endif ZDApp_Init( taskID++ ); #if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT ) ZDNwkMgr_Init( taskID++ ); #endif GenericApp_Init( taskID ); }
//Coordinator.c(做协调器时,下载这个函数)void GenericApp_Init(byte task_id)//任务初始化函数 { GenericApp_TaskID =task_id; //初始化任务优先级(任务优先级有协议栈的操作系统OSAL分配) GenericApp_TransID =0; //发送数据包的序号初始化为0 //对节点描述符进行初始化 GenericApp_epDesc.endPoint =GENERICAPP_ENDPOINT; GenericApp_epDesc.task_id =&GenericApp_TaskID; GenericApp_epDesc.simpleDesc =(SimpleDescriptionFormat_t*)&GenericApp_SimpleDesc; GenericApp_epDesc.latencyReq =noLatencyReqs; afRegister(&GenericApp_epDesc);//afRegister()对节点的描述符进行注册。注册后,才能使用OSAL提供的系统服务。 }
//任务初始化函数(做中断节点时,下载这个函数) void GenericApp_Init(byte task_id) { GenericApp_TaskID = task_id;//初始化任务优先级 GenericApp_NwkState =DEV_INIT; //初始化为DEV_INIT,表节点没有连接到ZigBee网络 GenericApp_TransID =0; //发送数据包的序列号初始化为0 //对节点描述符进行初始化 GenericApp_epDesc.endPoint=GENERICAPP_ENDPOINT; GenericApp_epDesc.task_id =&GenericApp_TaskID; GenericApp_epDesc.simpleDesc=(SimpleDescriptionFormat_t*)&GenericApp_SimpleDesc; GenericApp_epDesc.latencyReq=noLatencyReqs; //afRegister()函数将节点描述符进行注册,注册后才可以使用OSAL提供的系统服务 afRegister(&GenericApp_epDesc); }保存自己的任务TaskID
GenericApp_TaskID =task_id; //初始化任务优先级(任务优先级有协议栈的操作系统OSAL分配)
利用自己的任务TaskID从消息队列中获取属于自己的消息
MSGpkt=(afIncomingMSGPacket_t*)osal_msg_receive(GenericApp_TaskID);//osal_msg_receive()从消息队列上接收消息
本文参考自:《ZigBee无线传感器网络设计与实现》 王小强等人编著化学工业出版社