以ZStask协议栈点播工程进行说明,打开“CC2530DB\GenericApp.eww”工程,在Workspace 下拉框选项中可以看到 3 个子工程,分别是协调器、路由节点和终端节点工程,通过选择不同的工程,就会选择不同的源文件、编译选项
根据前面介绍的ZStack 协议栈内容,可得知,zigbee 无线通信中一般含有 3 类节点类型: 协调器(负责建立 zigbee 网络、信息收发)、终端节点(信息采集、接收控制)和路由节点(在终端节点的基础上增加了一个路由转发的功能)。根据这 3 种节点类型的功能可以得知 zigbee 无线通信中所有节点的工作流程。为了更容易理解ZStack 协议栈的工作原理,在介绍 ZStack 协议的工作原理之前,先简单介绍协调器、终端节点和路由节点这 3 种类型的节点在 zigbee 无线通信中的流程图
通过流程图以及对比终端节点、路由节点和协调器节点工程的源码可知,3 种类型的节点工程的大部分源码是相同的,而只有在执行用户任务事件时稍有不同。下面根据流程图来解析一下 ZStack 协议栈的工作流程,解析 ZStack 协议栈最简单、直接的方法就是从工程的入口main 函数开始解析
在工程的ZMain目录下有一个ZMain.c 文件,该文件中的main函数就是整个协议栈的入口处, 源代码解析如下
int main( void )
{
// 关闭所有中断
osal_int_disable( INTS_ALL );
// 硬件初始化(系统时钟、LED)
HAL_BOARD_INIT();
// 检查系统电源
zmain_vdd_check();
// 初始化板子I/O(关中断、系统弱电压复位处理)
InitBoard( OB_COLD );
// 初始化硬件层驱动(ADC、DMA、LED、UART等驱动)
HalDriverInit();
// 初始化NV存储区(主要存储节点组网的网络信息且掉电不会丢失)
osal_nv_init( NULL );
// 初始化MAC层
ZMacInit();
// 将节点的扩展地址写入NV存储区
zmain_ext_addr();
#if defined ZCL_KEY_ESTABLISH
// 初始化验证信息
zmain_cert_init();
#endif
// 初始化基本的NV条目
zgInit();
#ifndef NONWK
// 应用层初始化
afInit();
#endif
// 初始化操作系统
osal_init_system();
// 开中断
osal_int_enable( INTS_ALL );
// 最后一次板子初始化
InitBoard( OB_READY );
// 显示设备信息
zmain_dev_info();
/* 在LCD上显示设备信息 */
#ifdef LCD_SUPPORTED
zmain_lcd_init();
#endif
#ifdef WDT_IN_PM1
/* 使 能 看 门 狗 */
WatchDogEnable( WDTIMX );
#endif
osal_start_system(); // 启动操作系统
return 0; // 永远不会执行到这里
} // main()
ZStack 协议栈是一个多任务轮询的简单操作系统,其实在上述main.c 文件中除了一些基本的初始化功能之外,要想理解 ZStack 协议栈的工作原理,最关键的就是要理解main函数中的osal_init_system()
和osal_start_system()
方法。其中 osal_init_system()
函数源码展开如下
uint8 osal_init_system( void )
{
// 初始化内存分配系统
osal_mem_init();
// 初始化消息队列
osal_qHead = NULL;
// 初始化定时器
osalTimerInit();
// 初始化电源管理系统
osal_pwrmgr_init();
// 初始化系统任务.
osalInitTasks();
// 内存释放
osal_mem_kick();
return ( SUCCESS );
}
在osal_init_system()函数中初始化了 ZStack 系统的核心功能,包括内存分配初始化、电源管理初始化、任务初始化和内存释放等功能。而对于开发人员来讲最重要的还是理解其中的系统任务初始化函数 osalInitTasks(),再来展开该函数就可以发现该函数初始化了 9 个系统任务,并为每个任务赋予了任务标识符 taskID
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 );
}
通过将上述各层任务的初始化函数展开之后,可以发现 macTaskInit()、nwk_init()、APS_Init() 任务的初始化函数源码是闭源的,这是因为TI公司将这些关键源码进行了封装成库,开发人员无法查看其中的源码。展开剩余的 Hal_Init()、MT_TaskInit()和 GenericApp_Init()等任务初始化函数之后就可以发现这些任务初始化函数的作用就是将各层任务信息进行注册,并调用 osal_set_event( uint8 task_id, uint16 event_flag )函数将各任务的事件添加到任务事件数组tasksEvents[]中。下面以GenericApp_Init()函数为例来进行解析:
void GenericApp_Init( uint8 task_id )
{
GenericApp_TaskID = task_id; //记录GenericApp任务的任务标识符
GenericApp_NwkState = DEV_INIT;
GenericApp_TransID = 0;
// 定义目标设备节点的信息(如果有需要往目标节点发送数据的话)
GenericApp_DstAddr.addrMode = (afAddrMode_t)AddrNotPresent;
GenericApp_DstAddr.endPoint = 0;
GenericApp_DstAddr.addr.shortAddr = 0;
// 节点描述信息赋值
GenericApp_epDesc.endPoint = GENERICAPP_ENDPOINT;
GenericApp_epDesc.task_id = &GenericApp_TaskID;
GenericApp_epDesc.simpleDesc
= (SimpleDescriptionFormat_t *)&GenericApp_SimpleDesc;
GenericApp_epDesc.latencyReq = noLatencyReqs;
// 在AF应用层注册节点描述信息
afRegister( &GenericApp_epDesc );
// 注册按键事件
RegisterForKeys( GenericApp_TaskID );
// 刷新LCD显示
#if defined ( LCD_SUPPORTED )
HalLcdWriteString( "GenericApp", HAL_LCD_LINE_1 );
#endif
// 从ZDApp注册回调事件
ZDO_RegisterForZDOMsg( GenericApp_TaskID, End_Device_Bind_rsp );
ZDO_RegisterForZDOMsg( GenericApp_TaskID, Match_Desc_rsp );
#if defined( IAR_ARMCM3_LM )
// 注册RTOS任务
RTOS_RegisterApp( task_id, GENERICAPP_RTOS_MSG_EVT );
#endif
}
上述代码的讲述了用户任务的初始化及节点设备的注册,在 ZigBee协议栈 – Zstack协议栈(Zstack2.5.1a) 中提到OSAL系统中有9种默认任务,它们存储在taskArr
这个函数指针数组中:
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
};
这样每个任务就都有其对应的任务标识符(taskID ) 和对应的事件(event)。任务初始化的源码解析结束之后,再来解析启动系统的 osal_start_system()函数,在该函数中就实现了轮询各个任务,并执行各任务的处理函数,在任务处理函数中则处理了事件发生后所做的操作。
将 osal_start_system()函数展开之后,就可以发现系统启动之后进入了 1 个死循环,并循环调用 osal_run_system()函数,解析过程如下:
void osal_start_system( void )
{
#if !defined ( ZBIT ) && !defined ( UBIT )
for(;;) // 死循环
#endif
{
osal_run_system(); // 运行系统
}
}
展开 osal_run_system()
函数之后,就可以发现该函数的主要作用就是先对任务事件数组进行遍历,遍历过程从优先级别高的任务开始遍历,遍历过程中会判断该任务是否有未执行完的事件,如果该任务有未执行完的事件,则跳出while循环,然后调用 (tasksArr[idx])( idx, events )
进入该任务的事件处理函数;如果在遍历中该任务的已经执行完毕即没有事件,则继续循环检查下一个任务。当系统中所有的任务都执行结束后,系统就会自动进入睡眠模式以节约资源。源代码解析如下:
void osal_run_system( void )
{
uint8 idx = 0;
osalTimeUpdate(); // 系统时间更新
Hal_ProcessPoll(); // 硬件抽象层处理轮询(如UART、TIMER等)
do {
if (tasksEvents[idx]) // 判断任务中是否有事件
{
break;
}
} while (++idx < tasksCnt);
if (idx < tasksCnt) // 执行任务,优先执行任务标识符低的任务
{
uint16 events;
halIntState_t intState;
HAL_ENTER_CRITICAL_SECTION(intState);
events = tasksEvents[idx];
tasksEvents[idx] = 0; // 清除任务事件
HAL_EXIT_CRITICAL_SECTION(intState);
activeTaskID = idx;
events = (tasksArr[idx])( idx, events ); // 执行任务事件,并返回该任务未完成的事件
activeTaskID = TASK_NO_TASK;
HAL_ENTER_CRITICAL_SECTION(intState);
tasksEvents[idx] |= events; // 将未处理的任务事件添加到任务事件数组中,以便下次继续执行
HAL_EXIT_CRITICAL_SECTION(intState);
}
#if defined( POWER_SAVING )
else
{
osal_pwrmgr_powerconserve(); // 任务执行完成自动将MCU进入睡眠状态
}
#endif
/* Yield in case cooperative scheduling is being used. */
#if defined (configUSE_PREEMPTION) && (configUSE_PREEMPTION == 0)
{
osal_task_yield();
}
#endif
}
通过上述代码可知,关键的源码就在 events = (tasksArr[idx])( idx, events )中,tasksArr 数组存储了各层任务的事件处理函数,通过查看 tasksArr 数组的定义就可以知道,系统定义了如下 9 个处理函数:
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
};
在上面的 9 个任务处理函数中只有 Hal_ProcessEvent、MT_ProcessEvent、ZDApp_event_loop和 GenericApp_ProcessEvent
可以查看其函数实现的源码,其余均被官方公司封装成库。系统调用(tasksArr[idx])(idx, events) 之 后 其 实 就 是 调 用 Hal_ProcessEvent(idx, events) 、MT_ProcessEvent( idx, events )、ZDApp_event_loop( idx, events )和 GenericApp_ProcessEvent( idx, events )等任务事件处理函数。文中以 GenericApp_ProcessEvent( idx, events )任务事件处理函数为例进行源码解析,解析如下:
uint16 GenericApp_ProcessEvent( uint8 task_id, uint16 events )
{
afIncomingMSGPacket_t *MSGpkt;
afDataConfirm_t *afDataConfirm;
// 数据确认消息字段
byte sentEP;
ZStatus_t sentStatus;
byte sentTransID; // 该变量需要匹配发送值
(void)task_id;
if ( events & SYS_EVENT_MSG ) // 系统消息事件,当节点接收到消息之后自动触发该事件
{
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( GenericApp_TaskID );
while ( MSGpkt ) // 判断消息是否为空
{
switch ( MSGpkt->hdr.event )// 消息过滤
{
case ZDO_CB_MSG:
GenericApp_ProcessZDOMsgs( (zdoIncomingMsg_t *)MSGpkt );
break;
case KEY_CHANGE:
GenericApp_HandleKeys( ((keyChange_t *)MSGpkt)->state, ((keyChange_t *)MSGpkt)->keys );
break;
case AF_DATA_CONFIRM_CMD:
// This message is received as a confirmation of a data packet sent.
// The status is of ZStatus_t type [defined in ZComDef.h]
// The message fields are defined in AF.h
afDataConfirm = (afDataConfirm_t *)MSGpkt;
sentEP = afDataConfirm->endpoint;
sentStatus = afDataConfirm->hdr.status;
sentTransID = afDataConfirm->transID;
(void)sentEP;
(void)sentTransID;
// Action taken when confirmation is received.
if ( sentStatus != ZSuccess )
{
// The data wasn't delivered -- Do something
}
break;
// 用户任务中zigbee无线接收的数据在此处处理
case AF_INCOMING_MSG_CMD:
GenericApp_MessageMSGCB( MSGpkt );
break;
case ZDO_STATE_CHANGE:
GenericApp_NwkState = (devStates_t)(MSGpkt->hdr.status);
if ( (GenericApp_NwkState == DEV_ZB_COORD)
|| (GenericApp_NwkState == DEV_ROUTER)
|| (GenericApp_NwkState == DEV_END_DEVICE) )
{
// 开启消息定时发送
osal_start_timerEx( GenericApp_TaskID,
GENERICAPP_SEND_MSG_EVT,
GENERICAPP_SEND_MSG_TIMEOUT );
}
break;
default:
break;
}
// Release the memory
osal_msg_deallocate( (uint8 *)MSGpkt );
// Next
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( GenericApp_TaskID );
}
// return unprocessed events
return (events ^ SYS_EVENT_MSG);
}
// 发送消息,该事件由定时器触发
// (setup in GenericApp_Init()).
if ( events & GENERICAPP_SEND_MSG_EVT )
{
// 发送节点数据
GenericApp_SendTheMessage();
// 开启下一次定时发送
osal_start_timerEx( GenericApp_TaskID,
GENERICAPP_SEND_MSG_EVT,
GENERICAPP_SEND_MSG_TIMEOUT );
// 返回未处理事件
return (events ^ GENERICAPP_SEND_MSG_EVT);
}
#if defined( IAR_ARMCM3_LM )
// Receive a message from the RTOS queue
if ( events & GENERICAPP_RTOS_MSG_EVT )
{
// Process message from RTOS queue
GenericApp_ProcessRtosMessage();
// return unprocessed events
return (events ^ GENERICAPP_RTOS_MSG_EVT);
}
#endif
// Discard unknown events
return 0;
}
在上述源码中可得知, GenericApp任务事件处理函数中处理了 SYS_EVENT_MSG 事件、GENERICAPP_SEND_MSG_EVT事件、GENERICAPP_RTOS_MSG_EVT事件。开发人员只要理解并掌握好事件的处理过程就可以了。
通过上文的源代码解析,基本介绍完了 ZStack 协议栈中任务调度与任务事件处理的一些关系, 对于任务的执行其实可以这样理解:在一个大循环里面一直调用各任务的任务事件处理函数。
综合协议栈的工作流程以及多任务之间的调度关系,就可以知道 ZStack 协议栈的工作流程,下所示是 ZStack 协议栈的工作流程图
继续解析 ZStack 协议栈的重要组成部分,即 zigbee 无线数据包的接收、发送处理过程
前面解析内容中提到 GenericApp 事件处理过程有接收的消息数据处理,如下所示:
uint16 GenericApp_ProcessEvent( uint8 task_id, uint16 events )
{
......
if ( events & SYS_EVENT_MSG )
{
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( GenericApp_TaskID );
while ( MSGpkt )
{
switch ( MSGpkt->hdr.event )
{
......
case AF_INCOMING_MSG_CMD:
GenericApp_MessageMSGCB( MSGpkt );
break;
......
default:
break;
}
}
}
return 0;
}
在上述代码中 pMSGpkt 结构体存储了节点接收到的无线数据包,事件处理过程中将数据包的内容直接赋值给了GenericApp_MessageMSGCB 函数的参数,跳转到这个函数的调用过程,可以发现GenericApp_MessageMSGCB( afIncomingMSGPacket_t *pkt )函数接收到数据之后,使用switch语句判断数据包的clusterId,项目中只用到了GENERICAPP_CLUSTERID,也就是接收到节点数据后是在这个case下面进行处理,该函数需要开发人员完成数据处理的代码编写。针对协议栈点播工程的设计,协调器节点可以将接收到的数据包通过串口传给上位机,下面是协调器中数据包的处理源码解析:
static void GenericApp_MessageMSGCB( afIncomingMSGPacket_t *pkt )
{
switch ( pkt->clusterId )
{
case GENERICAPP_CLUSTERID:
// 通过串口0打印节点数据包内容
uartSendString(pkt->cmd.Data, pkt->cmd.DataLength);
#if defined( LCD_SUPPORTED )
HalLcdWriteScreen( (char*)pkt->cmd.Data, "rcvd" );
#elif defined( WIN32 )
WPRINTSTR( pkt->cmd.Data );
#endif
break;
}
}
ZStack 协议栈中数据包的发送只要调用GenericApp_SendTheMessage()函数即可,下面是该函数的原型:
static void GenericApp_SendTheMessage( void )
{
char theMessageData[] = "Hello World";
if ( AF_DataRequest( &GenericApp_DstAddr, &GenericApp_epDesc,
GENERICAPP_CLUSTERID,
(byte)osal_strlen( theMessageData ) + 1,
(byte *)&theMessageData,
&GenericApp_TransID,
AF_DISCV_ROUTE, AF_DEFAULT_RADIUS ) == afStatus_SUCCESS )
{
// Successfully requested to be sent.
}
else
{
// Error occurred in request to send.
}
}
从上面源码不难看出,协议栈数据包发送实质上是调用AF_DataRequest()函数来实现,AF_DataRequest()函数的参数包括了目标节点的地址信息GenericApp_DstAddr,数据发送节点信息GenericApp_epDesc,数据传输编号GENERICAPP_CLUSTERID以及数据长度、数据指针等参数。
在AF_DataRequest()函数成功发出数据后会返回afStatus_SUCCESS,通过判断数据是否发送成功也可以在发送数据后加一个串口信息打印,上面就是该函数的解析过程,如果要发送 zigbee 数据包,只要按照该函数的参数说明进行调用即可