【转】ZigBee终端入网方式深入分析

时间:2023-03-09 08:28:42
【转】ZigBee终端入网方式深入分析

前述

继之前对终端Direct Join的分析,发现很多东西还很模糊,存在很多问题。终于找到时间继续深入挖下去,这次应该比较完整地搞清了终端的入网机制,并纠正之前的几个认识偏差。

由于Z-Stack网络层并不开源,所以一些地方是靠的推测,很多地方的结论也没有实验验证,谨留给诸君参考和斧正。

ZigBee2007协议规范分析

先来看看ZigBee2007协议规范是怎样规定入网请求的:

The semantics of this primitive are as follows: 
NLME-JOIN.request 

ExtendedPANId, 
RejoinNetwork, 
ScanChannels, 
ScanDuration, 
CapabilityInformation, 
SecurityEnable 
}

The next higher layer of a device generates this primitive to request to: 
- Join a network using the MAC association procedure. 
- Join or rejoin a network using the orphaning procedure. 
- Join or rejoin a network using the NWK rejoin procedure. 
- Switch the operating channel for a device that is joined to a network.

就此原语的描述可以看出,前三种情况均为设备入网的方式,最后一个是为设备切换信道所用,暂不考虑。所以ZigBee设备入网有三种方式,我们分别称之为Join、Orphan Join、Rejoin。三种方式RejoinNetwork参数分别设置为0x00、0x01、0x02。

  1. Join入网过程。首先发起Network Discovery,返回所有应答的节点的信息。在发现的结果中找出符合要求(这里的要求是一些最基本的条件,详见ZigBee协议规范3.6.1.4.1.1节,下同)的父节点,向它发送入网请求。父节点分配16位网络地址。

  2. Rejoin入网过程。发起Network Discovery,在应答的节点中挑选出和自己的ExtendedPANID相同的节点,在这些节点中找出符合要求的父节点,发送入网请求,并且使用自己已拥有的16位网络地址(若没有,则随机生成一个)。

  3. Orphan Join过程。发起Orphan Scan,寻找邻居表中保存有本设备IEEE地址的父节点,在返回结果中找出符合要求的父节点,发送入网请求。父节点返回邻居表中保存的16位网络地址。

可以看出三种入网的过程都可以归纳为网络扫描+选择目标。三者的选择的筛选条件是递增的:任何节点—>指定PANID的节点—>邻居表中有自己信息的节点。

Z-Stack协议栈分析

版本号:ZStack-CC2530-2.5.1a

1. 第一步 扫描

下面是设备启动的函数ZDO_StartDevice,它是设备入网流程的入口,这个函数仅在ZDApp_event_loop事件轮询函数中发生ZDO_NETWORK_INIT事件的时候被调用,而ZDApp_NetworkInit函数就是用来延时发送ZDO_NETWORK_INIT事件的,所以ZDApp_NetworkInit函数也是设备入网过程的触发,这个函数下面将被用到。

这里我只把与终端启动的相关代码贴了出来:

/*********************************************************************
* @fn ZDO_StartDevice
*
* @brief This function starts a device in a network.
*
* @param logicalType - Device type to start
* startMode - indicates mode of device startup
*
* @return none
*/
void ZDO_StartDevice( byte logicalType, devStartModes_t startMode, byte beaconOrder, byte superframeOrder )
{
ZStatus_t ret;
ret = ZUnsupportedMode;
if ( (startMode == MODE_JOIN) || (startMode == MODE_REJOIN) )
{
devState = DEV_NWK_DISC;
ret = NLME_NetworkDiscoveryRequest( zgDefaultChannelList, zgDefaultStartingScanDuration );
}
else if ( startMode == MODE_RESUME ) //Orphan Join
{
devState = DEV_NWK_ORPHAN;
ret = NLME_OrphanJoinRequest( zgDefaultChannelList,
zgDefaultStartingScanDuration );
}
if ( ret != ZSuccess )
{
osal_start_timerEx(ZDAppTaskID, ZDO_NETWORK_INIT, NWK_RETRY_DELAY );
}
}

从上面可以看出,终端的入网第一步就是调用了这两个函数NLME_NetworkDiscoveryRequest、 
NLME_OrphanJoinRequest(放到第3步再看),而Join和Rejoin方式的这一部分是完全相同的。从TI的API手册中可以查到:

NLME_NetworkDiscoveryRequest()

此函数请求网络层寻找相邻路由器。这个函数应该在加入并执行网络扫描前调用。扫描确认结果将被返回到ZDO_NetworkDiscoveryConfirmCB()回调函数中。……

2. 扫描结果

在ZDO_NetworkDiscoveryConfirmCB()回调函数中发现,就做了一件事,就是向ZDApp_event_loop发送ZDO_NWK_DISC_CNF事件,直接找到ZDO_NWK_DISC_CNF事件的处理函数(为了方便分析,只留下了关键的函数名):

case ZDO_NWK_DISC_CNF:
if (devState != DEV_NWK_DISC)
break; if ( ZG_BUILD_JOINING_TYPE && ZG_DEVICE_JOINING_TYPE )
{
networkDesc_t *pChosenNwk;
if ( ( (pChosenNwk = ZDApp_NwkDescListProcessing()) != NULL ) &&
(zdoDiscCounter > NUM_DISC_ATTEMPTS) )
{
if ( devStartMode == MODE_JOIN )
{
devState = DEV_NWK_JOINING;
if ( NLME_JoinRequest( pChosenNwk->…… ) != ZSuccess )
{
ZDApp_NetworkInit(…… );
}
}
else if ( devStartMode == MODE_REJOIN )
{
devState = DEV_NWK_REJOIN; if ( _NIB.nwkDevAddress == INVALID_NODE_ADDR )
{
// Before trying to do rejoin,
// check if the device has a valid short address
// If not, generate a random short address for itself
} if ( _NIB.nwkPanId == INVALID_PAN_ID )
{
// Check if the device has a valid PanID,
// if not, set it to the discovered Pan
}
if ( NLME_ReJoinRequest( ……) != ZSuccess )
{
ZDApp_NetworkInit( …… );
}
}
}
else
{
if ( continueJoining )
{
zdoDiscCounter++;
ZDApp_NetworkInit( …… );
}
}
}
break;

通过简化了的代码可以看出,对于扫描结果的处理是这样一个流程:首先需进行至少NUM_DISC_ATTEMPTS次扫描,每次都调用ZDApp_NetworkInit进行重新扫描,如果找到了合格的父节点(pChosenNwk = ZDApp_NwkDescListProcessing()) != NULL),就依照MODE_JOIN或 MODE_REJOIN 分别调用NLME_JoinRequest或NLME_ReJoinRequest向目标父节点发送请求。由于后者的请求中要附带自己的PANID和ShortAddress,所以要事先检查和处理。

从这里可以看出,不管是Join还是Rejoin,如果找不到可用的父节点,将持续调用ZDApp_NetworkInit扫描网络,陷入死循环。

3. 加入父节点

着眼到NLME_JoinRequest和NLME_ReJoinRequest,以及前面的NLME_OrphanJoinRequest上,从TI的API手册中可以查到:

NLME_OrphanJoinRequest() 
此函数请求网络层孤立地连接到网络上。此函数是一个默示加入形式的扫描。此函数的结果(状态值)返回到ZDO_JoinConfirmCB()回调函数中。……

NLME_JoinRequest () 
此函数允许相邻的更高层请求设备将自己加入到一个网络中。此函数的结果(状态)返回到ZDO_JoinConfirmCB()回调函数中。……

NLME_ReJoinRequest () 
使用此函数重新加入一个设备已经加入过的网络。此函数的结果(状态)返回到ZDO_JoinConfirmCB()回调函数中。……

ZDO_JoinConfirmCB()一样只做了一件事,就是向ZDApp_event_loop发送事件ZDO_NWK_JOIN_IND。

下面是ZDO_NWK_JOIN_IND事件的处理函数ZDApp_ProcessNetworkJoin(已简化):

void ZDApp_ProcessNetworkJoin( void )
{
if ( (devState == DEV_NWK_JOINING) ||
((devState == DEV_NWK_ORPHAN) &&
(ZDO_Config_Node_Descriptor.LogicalType == NODETYPE_ROUTER)) )
{
// Result of a Join attempt by this device.
if ( nwkStatus == ZSuccess )
{
osal_set_event( ZDAppTaskID, ZDO_STATE_CHANGE_EVT );
if ( devState == DEV_NWK_JOINING )
{
ZDApp_AnnounceNewAddress();
}
devState = DEV_END_DEVICE;
}
else
{
if ( (devStartMode == MODE_RESUME) &&
(++retryCnt >= MAX_RESUME_RETRY) )
{
if ( _NIB.nwkPanId == 0xFFFF || _NIB.nwkPanId == INVALID_PAN_ID )
devStartMode = MODE_JOIN;
else
{
devStartMode = MODE_REJOIN;
_tmpRejoinState = true;
}
}
/******************************/
/*some process*/
/******************************/
zdoDiscCounter = 1;
ZDApp_NetworkInit( …… );
}
}
else if ( devState == DEV_NWK_ORPHAN || devState == DEV_NWK_REJOIN )
{
// results of an orphaning attempt by this device
if (nwkStatus == ZSuccess)
{
devState = DEV_END_DEVICE;
osal_set_event( ZDAppTaskID, ZDO_STATE_CHANGE_EVT );
ZDApp_AnnounceNewAddress();
}
else
{
if ( devStartMode == MODE_RESUME )
{
if ( ++retryCnt <= MAX_RESUME_RETRY )
{
if ( _NIB.nwkPanId == 0xFFFF || _NIB.nwkPanId == INVALID_PAN_ID )
devStartMode = MODE_JOIN;
else
{
devStartMode = MODE_REJOIN;
_tmpRejoinState = true;
}
}
// Do a normal join to the network after certain times of rejoin retries
else if( AIB_apsUseInsecureJoin == true )
{
devStartMode = MODE_JOIN;
}
} // Clear the neighbor Table and network discovery tables.
nwkNeighborInitTable();
NLME_NwkDiscTerm(); // setup a retry for later...
ZDApp_NetworkInit( …… );
}
}
}

至此终端就完成了入网的全部流程,如果被父节点接受,那么入网成功;如果失败,则重新开始入网流程。

4. 提出问题

可以看出,函数中没有对失败时的Join方式或Rejoin方式做任何的处理,毫无疑问,两种方式下都将无限重试直到入网成功。并没有实现所谓的:

// Do a normal join to the network after certain times of rejoin retries

那么分析Orphan Join,而根据源代码的逻辑,如果是路由器(NODETYPE_ROUTER)执行Orphan Join,那么当重试次数超过MAX_RESUME_RETRY时,将根据是否搜索到了父节点(_NIB.nwkPanId == 0xFFFF || _NIB.nwkPanId == INVALID_PAN_ID),将入网方式重置为Join方式或Rejoin方式。那么针对Rejoin方式和终端(NODETYPE_DEVICE)的Orphan Join方式呢,很令人费解:

if ( devStartMode == MODE_RESUME )
{
if ( ++retryCnt <= MAX_RESUME_RETRY )
{
if ( _NIB.nwkPanId == 0xFFFF || _NIB.nwkPanId == INVALID_PAN_ID )
devStartMode = MODE_JOIN;
else
{
devStartMode = MODE_REJOIN;
_tmpRejoinState = true;
}
}
// Do a normal join to the network after certain times of rejoin retries
else if( AIB_apsUseInsecureJoin == true )
{
devStartMode = MODE_JOIN;
}
}

不管怎样,失败的Orphan Join都将直接被置为Join或Rejoin,在这里条件 (++retryCnt <= MAX_RESUME_RETRY)好像总是成立的。那么有没有可能是其他地方对retryCnt进行了修改,搜索遍整个工程,除了这个函数中有对retryCnt的+操作外,只有两处地方对retryCnt进行了赋值,一处是定义时的初始化,一处是断网重连,执行Orphan Join前对retryCnt的清零。

所以,对于终端来说,都只能执行一次Orphan Join,与宏定义MAX_RESUME_RETRY毫无关系。

这到底是TI有意为之,还是逻辑的Bug呢?这个问题有待日后解决。