【转】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呢?这个问题有待日后解决。

原文地址:https://www.cnblogs.com/yelin/p/6054611.html