【轉】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呢?這個問題有待日后解決。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM