基於TI樣例快速開發Zstack協議棧應用層
本文基於TI GenericApp樣例 梳理樣例應用層函數,學習協議棧和相關編程思路。
知識儲備
- C語言基礎(包括回調函數、靜態變量、指針、結構體以及一些很基礎的概念)
- 初步程序設計思想(過程設計,模塊化設計,代碼複用,自頂向下)
- Zstack,OSAL的基本運作機制(輪詢機制等概念,有個流程圖)
- 如果對後面文章哪一處有疑問,建議問問神奇的海螺
本文關注要點
- 協議棧與單片機裸機編程,在編程上的區別
- 應用層的應用是如何在協議棧和osal中工作的
- GenericApp中,TI樣例函數之間的關係
- C語言編程通用技巧
協議棧與單片機裸機編程(有初步程序設計概念的可以直接跳過)
平時無論是最原始的51亦或是其他單片機,學習之初是 瞭解硬件底層寄存器+在IDE中直接操作相關寄存器 來完成基礎功能,比如點個燈,點個流水燈,LCD俄羅斯方塊等。基本流程就是自己定一個主函數,利用一些編程技巧就可以實現一定的功能。而Zstack協議棧,則是一下跨越到了系統層級的編程,跨度很大,如果不找到一個合理的切入口去解構協議棧,那建議放棄治療。
說到切入口,根據協議的層級區分(zigbee網絡結構),我們能夠輕易介入的肯定是最上層的應用層。像Zstack一樣的成熟協議棧,會做兩件事方便使用者快速開發應用層
提供底層封裝好的,多樣的函數和宏定義(比如串口讀寫函數、按鍵函數等)。這裏體現到一個模塊化設計和代碼複用的思想,以往裸機編程反覆編寫的針對寄存器和其餘底層配置的操作和一系列操作的組合功能,被封裝成宏參數和函數,分佈在底層C文件中等待調用。
這樣做一來 方便閱讀和編寫,二來 提高代碼複用 的效率(劃重點!!)。在應用層看來,就直接變成了,調用多個功能函數+邏輯來進一步組合完成目標,同時也無需關心底層的實現,無線的傳輸等功能了。其實不僅是這個協議棧,這樣逐步模塊化,積累的過程,在任何上規模的程序中都是必須的。(STM32 之所以方便,就是因爲公司提供了完備的功能函數)提供基礎樣例,方便用戶極快的瞭解應用層相關函數的使用,同時也作爲開發模板。在GenericApp中,基礎的功能是兩個模塊完成組網綁定,互傳字符串hello world並顯示在LCD上。
由此可見,編程觀念要轉換,從事無鉅細的底層功能實現中解放出來,善於利用現成函數,完成更大規模的邏輯。關注點從操作寄存器—>利用函數(學過STM32或者熟悉編程的很容易遷移概念)。同樣,在利用底層函數的同時,我們也可以進一步利用編程技巧和思想,封裝我們自己編寫的功能,鬆耦合緊內聚,提高複用率。
針對於Zstack來說,要初步改造或者添加內容,就必須瞭解添加改造部分與原有功能之間的聯繫。以添加應用爲例,需要了解應用如何加入osal進行工作,如何調用底層來實現應用。樣例給出了規範的高可擴展性的寫法。
OSAL基礎工作流程(以應用層爲核心主線,其餘流程作用請善用搜索引擎,嫌繁瑣可以後跳到最後流程總結)
下圖爲Zstack 2.5.1爲例子介紹工程文件內容(其餘版本流程並無差別)
- 接下來選取的函數都是與創建應用緊密相關的函數,或者是直接需要改動的,或者是跟流程相關的。
- 重點關注目錄ZMain 和App
OSAL系統,從ZMain目錄下的ZMain.c 文件開始執行
/********************************************************************* * @fn main * @brief First function called after startup. * @return don't care */ int main( void ) { ... // Initialize the operating system osal_init_system(); ... osal_start_system(); // No Return from here return 0; // Shouldn't get here. } // main()
- 這裏截取了和應用層相關的兩個函數,一個是osal初始化的函數 osal_init_system(),另一個是系統正常運作函數,start函數就是日常死循環,裏面有詳細的輪詢代碼。
根據OSAL的工作流程,初始化是分配註冊ID順序和初始化應用或者系統其他任務,字面意思。
初始化的重要性不言而喻,類似應用中需要調用的IO或者寄存器配置初始化全在裏面
- 這裏截取了和應用層相關的兩個函數,一個是osal初始化的函數 osal_init_system(),另一個是系統正常運作函數,start函數就是日常死循環,裏面有詳細的輪詢代碼。
進入 osal_init_system() 瞄一眼,看到init_task函數(需要用戶改動)這個inittask函數很重要!!(敲黑板)
/*********************************************************************
* @fn osal_init_system
*
* @brief
*
* This function initializes the "task" system by creating the
* tasks defined in the task table (OSAL_Tasks.h).
*
* @param void
*
* @return SUCCESS
*/
uint8 osal_init_system( void )
{
...
// Initialize the system tasks.
osalInitTasks();
...
return ( SUCCESS );
}
爲什麼說這個task函數重要的很?請看!(函數位置在樣例的App目錄OSAL_GenericApp.c)
/********************************************************************* * @fn osalInitTasks * * @brief This function invokes the initialization function for each task. * * @param void * * @return none */ 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 );//前面的所有任務都是默認不需要用戶改動的,這一條纔是用戶添加的自定任務,有多少就在後面繼續++一個一個初始化 }
- 從代碼結構上來看,這個就是單純靠taskID這個變量不斷++來 確定各個任務被輪詢的順序(優先級),回看OSAL的運行機制,這裏的流程用代碼很清晰的展現了出來。
- init裏添加任務後,在上方 const 定義的tasksArr函數域裏 按順序 加上任務在被輪詢時 具體執行功能的函數 命名習慣是***_ProcessEvent。有關係統初始部分,到此結束。
- 這個初始化可以和別的C文件打包在一起,分開來單獨放一頁只是爲了結構簡明。
打開樣例,TI已經幫我們設計好了一個模板(大法好),這個模板就如同他的名字一樣,很通用,也是我們編程最重要的一環。
- 除去前面必要的頭文件引入、結構體、變量、函數聲明,前面提過的 init函數 就在第一個,ProcessEvent函數 在第二個,整個C文件,整個應用也就是圍繞這兩個函數在寫。
- init內容就是按部就班的初始化無線電參數、IO、中斷、串口、等,其中,如果你對於zigbee網絡沒有其他要求的話(改用組播或者其他組網方式),可以直接使用默認的綁定方式,初始化條件已經寫好。其餘個性化初始,就自己寫。建議使用內部集成的函數,比如關於硬件部分的延時在HAL目錄下,串口的HAL和MT都有,其中MT的串口是對HAL串口設置的進一步封裝(可以多任務多串口安排優先級等高級操作),系統相關函數就在OSAL目錄下找,比如 osal_memcpy 等常用操作。
- 舉個例子,在樣例init基礎上直接添加如下代碼,進行串口的初始化(HAL串口),位置建議在#if這類定義的前面,其中這個 halUARTCfg_t 是定義在haluart頭文件的結構體。
halUARTCfg_t uartConfig; //init uart uartConfig.configured = TRUE; // 2x30 don't care - see uart driver. uartConfig.baudRate = HAL_UART_BR_115200; uartConfig.flowControl = FALSE; uartConfig.flowControlThreshold = 64; // 2x30 don't care - see uart driver. uartConfig.rx.maxBufSize = 128; // 2x30 don't care - see uart driver. uartConfig.tx.maxBufSize = 128; // 2x30 don't care - see uart driver. uartConfig.idleTimeout = 6; // 2x30 don't care - see uart driver. uartConfig.intEnable = TRUE; // 2x30 don't care - see uart driver. uartConfig.callBackFunc = rxCB; //重點配置,rxCB是回調函數 HalUARTOpen (0, &uartConfig);
- 接下來會詳細分析ProcessEvents 函數,這個函數是整個應用任務的核心,它決定了當輪詢到應用頭上併發生了些什麼的時候,所做的操作內容。這裏TI的這個函數給的結構非常清楚。很值得參考其寫法。其中用了很多看起來很麻煩的變量,其實都和真正的編寫程序不相關。
- 這個函數大體意思是,用if判斷事件events有無發生,進而使用switch判斷事件類型,用switch來對每一種事件有一個反饋
- 我們在這個階段只需關注其中給定的
- 按鍵事件 KEY_CHANGE
- 無線接收事件 AF_INCOMING_MSG_CMD
- 無線發送事件(通常由 AF_DataRequest 函數來觸發),其中無線發送可以由 AF函數所在的任意函數來觸發。
- 我們需要按鍵發生什麼就到case下 GenericApp_HandleKeys 函數中去填寫相關邏輯,同理,無線接收到數據要做處理,就去 GenericApp_MessageMSGCB 中搞點事情。
- 無線發送大體可分定時發送,突發事件發送(事件回調),某些特殊動作發送,如果要在運行中定時發送 就在if GENERICAPP_SEND_MSG_EVT 處進行設置,突發和特殊動作,就在相應的動作函數中添加AF發送函數,在函數中填寫相關信息,就可實現發送無線消息。
- 如果想自己添加其他不需要switch 和if 判斷事件類型也要執行的動作。。。。。直接寫在函數體裏面就好啦(‘・ω・’)
/********************************************************************* * @fn GenericApp_ProcessEvent * * @brief Generic Application Task event processor. This function * is called to process all events for the task. Events * include timers, messages and any other user defined events. * * @param task_id - The OSAL assigned task ID. * @param events - events to process. This is a bit map and can * contain more than one event. * * @return none */ 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 KEY_CHANGE: GenericApp_HandleKeys( ((keyChange_t *)MSGpkt)->state, ((keyChange_t *)MSGpkt)->keys ); break; .... case AF_INCOMING_MSG_CMD: GenericApp_MessageMSGCB( MSGpkt ); break; ... default: break; } ... } // return unprocessed events return (events ^ SYS_EVENT_MSG); } // Send a message out - This event is generated by a timer // (setup in GenericApp_Init()). if ( events & GENERICAPP_SEND_MSG_EVT ) { // Send "the" message GenericApp_SendTheMessage(); // Setup to send message again osal_start_timerEx( GenericApp_TaskID, GENERICAPP_SEND_MSG_EVT, GENERICAPP_SEND_MSG_TIMEOUT ); // return unprocessed events return (events ^ GENERICAPP_SEND_MSG_EVT); } ... // Discard unknown events return 0; }
- 流程總結下來異常簡單(OSAL是一個簡單的實時操作系統):主函數入口int main() –> osal_init_system() 處初始化用戶自定義任務,其中的 osalInitTasks() 需要用戶安排的明明白白。–> osal_start_system() 開始輪詢死循環,輪詢到我們的應用了,執行的操作和反饋內容需要我們自己寫(大佬不按模板來也可以)。
- 整體是很簡單的一根筋–>死循環模型。
自頂向下設計流程
- 這是一個從需求出發的設計思路。目的是把握應用的大體功能和編程實踐的具體步驟順序。
- 思考的第一步是提出利用zigbee網絡來達成的某種目的
- 接下去思考實現目的所要使用的功能(無線收發、串口通訊、按鍵等)
- 安排各種功能的邏輯交互
- 簡單的,可以根據模板和需求功能開始具體編程,來實現相應簡單功能。
自底向上豐富功能
- 在具體編程階段,需要多種多樣的功能函數來滿足奇奇怪怪的需求和巧妙的技巧
- 瞭解更多的關於協議棧的功能函數有利於快速編程,快速實現功能,節約代碼量,進一步熟練需要實踐經驗
- 簡單應用部分常用HAL、MT、OSAL目錄中規定的函數和C語言特色功能函數,數量不少,需要熟練掌握搜索引擎
- 這部分自底向上是專精於zigbee和特定芯片的過程