ZigBee是目前比較流行的一種低功耗無線組網技術,主要用于智能家居控制以及智能工業生產。ZigBee大的特點就是低功耗、自組網。
本文引用地址://www.einuk.cn/emb/Column/7541.html
說到ZigBee就不得不提IEEE802.15和ZigBee聯盟,他們公共制定了ZigBee協議棧的標準。組網過程就是基于ZigBee協議棧,協議棧完成了絕大部分的工作,留給用戶的就是應用程序接口。協議棧就像一個操作系統一樣,用戶只需要定制應用程序就可以使用。
先來看一下ZigBee協議棧架構,操作系統下面的可以當做BootLoader,操作系統上面的可以看做應用程序。對于用戶來說,只要了解操作系統,會定制task,那么就可以使用協議棧了。
接下來我們以TI公司的ZigBee協議棧為標準,了解一下osal操作系統機制,以方便后續定制task。
Osal源于一種簡單的操作系統思想---輪詢。在ZigBee協議棧中,OSAL負責調度各個任務的運行,如果有事件發生了,則會調用相應的事件處理函數進行處理。那么,事件和任務的事件處理函數是如何聯系起來的呢?
ZigBee中采用的方法是:建立一個事件表,保存各個任務的對應的事件,建立另一個函數表,保存各個任務的事件處理函數的地址,然后將這兩張表建立某種對應關系,當某一事件發生時則查找函數表找到對應的事件處理函數即可。
在ZigBee協議棧中,有三個變量至關重要。
● tasksCnt—該變量保存了任務的總個數。
該變量的聲明為:uint8 tasksCnt,其中uint8的定義為:typedef unsigned char uint8
● tasksEvents—這是一個指針,指向了事件表的首地址。
該變量的聲明為:uint16 *tasksEvents,其中uint16的定義為:typedef unsigned short uint16
● tasksArr—這是一個數組,數組的每一項都是一個函數指針,指向了事件處理函數。
該數組的聲明為:const pTaskEventHandlerFn tasksArr[],其中pTaskEventHandlerFn的定義為:typedef unsigned short (*pTaskEventHandlerFn)( unsigned char task_id, unsigned short event ),這是定義了一個函數指針。tasksArr數組的每一項都是一個函數指針,指向了事件處理函數。
事件表和函數表的關系如下圖
我們現在來總結下OSAL的工作原理:通過tasksEvents指針訪問事件表的每一項,如果有事件發生,則查找函數表找到事件處理函數進行處理,處理完后,繼續訪問事件表,查看是否有事件發生,無限循環。OSAL就是一種基于事件驅動的輪詢式操作系統。事件驅動是指發生事件后采取相應的事件處理方法,輪詢指的是不斷地查看是否有事件發生。
下面從代碼中看一下osal運行機制。在Zmain文件夾下有個Zmain.c文件,打開該文件可以
找到main()函數,這就是整個協議棧的入口點。main()函數原型如下:
int main( void )
{
// Turn off interrupts
osal_int_disable( INTS_ALL );
// Initialization for board related stuff such as LEDs
HAL_BOARD_INIT();
// Make sure supply voltage is high enough to run
zmain_vdd_check();
// Initialize board I/O
InitBoard( OB_COLD );
// Initialze HAL drivers
HalDriverInit();
// Initialize NV System
osal_nv_init( NULL );
// Initialize the MAC
ZMacInit();
// Determine the extended address
zmain_ext_addr();
// Initialize basic NV items
zgInit();
#ifndef NONWK
// Since the AF isn't a task, call it's initialization routine
afInit();
#endif
// Initialize the operating system
osal_init_system();
// Allow interrupts
osal_int_enable( INTS_ALL );
// Final board initialization
InitBoard( OB_READY );
// Display information about this device
zmain_dev_info();
/* Display the device info on the LCD */
#ifdef LCD_SUPPORTED
zmain_lcd_init();
#endif
#ifdef WDT_IN_PM1
/* If WDT is used, this is a good place to enable it. */
WatchDogEnable( WDTIMX );
#endif
osal_start_system(); // No Return from here
return 0; // Shouldn't get here.
} // main()
在osal_start_system()函數之前的函數都是對板載硬件以及協議棧進行的初始化,直到調用osal_start_system()函數,整個ZigBee協議棧才算是真正地運行起來了。硬件驅動不需要多看,我們跳轉到osal_start_system(),查看一下它的原型
void osal_start_system( void )
{
#if !defined ( ZBIT ) && !defined ( UBIT )
for(;;) // Forever Loop
#endif
{
uint8 idx = 0;
osalTimeUpdate();
// This replaces MT_SerialPoll() and osal_check_timer().
Hal_ProcessPoll();
do {
// Task is highest priority that is ready.
if (tasksEvents[idx])
{
break;
}
} while (++idx < tasksCnt);
if (idx < tasksCnt)
{
uint16 events;
halIntState_t intState;
HAL_ENTER_CRITICAL_SECTION(intState);
events = tasksEvents[idx];
// Clear the Events for this task.
tasksEvents[idx] = 0;
HAL_EXIT_CRITICAL_SECTION(intState);
events = (tasksArr[idx])( idx, events );
HAL_ENTER_CRITICAL_SECTION(intState);
// Add back unprocessed events to the current task.
tasksEvents[idx] |= events;
HAL_EXIT_CRITICAL_SECTION(intState);
}
#if defined( POWER_SAVING )
// Complete pass through all task events with no activity?
else
{
// Put the processor/system into sleep
osal_pwrmgr_powerconserve();
}
#endif
}
}
首先看到,真個osal是在一個for循環中執行的,這就意味正后面的事件一直在不停的重復,也是osal的基礎。
第6行,定義了一個變量idx,用來在事件表中索引。
第7-9行,更新系統時鐘,同時查看硬件方面是否有事件發生,如串口是否收到數據、是否有按鍵按下等信息,這部分內容在此可以暫時不予考慮。
第10-16行,使用do-while循環查看事件表是否有事件發生。分析一下這個循環,如果有事件發生,那么就跳出循環,去后面的代碼處理事件。如果沒有事件,那么久繼續掃描表中的下一項。當然真個循環的次數不得多于tasksCnt,因為它記錄著事件的總數。
第18~35行是事件的處理過程,23和27行規定了一個臨界區。24行取出事件,保存在events變量。26行則將表中的事件清零,因為事件已經被取出將要處理。27行根據id找到對應的函數表,執行處理函數,而且拿到返回值(后面解釋返回值)。31和34行又是一個臨界區,33行又將事件重新賦值。
上面了解了osal的基本原理,就是兩張表的查詢和對應。那么其中又有一個events成了重點,為什么處理完事件時候又返回一個events(24行),而且還把events放回到事件表(33行)。
ZigBee協議棧使用一個unsigned short型的變量,因為unsigned short類型占兩個字節,即16個二進制位,因此,可以使用每個二進制位表示一個事件,我們來看下協議棧定義的系統事件SYS_EVENT_MSG,十六進制:0x8000,二進制:0b1000000000000000。它用的就是高位來表示該事件:
可以看出在一個任務中多只能有16個事件,因為events是一個16位數據。
在系統初始化時,所有任務的事件初始化為0,因此,第10行通過tasksEvents[idx]是否為0來判斷是否有事件發生,如果有事件發生了,則跳出循環。
29行執行完事件處理函數后,需要將未處理的事件返回,也就是說事件處理函數的返回值保存了未處理的事件,將該事件在寫入事件表中,以便于下次進行處理。看一下下面的處理函數
uint16 SampleApp_ProcessEvent( uint8 task_id, uint16 events )
{
if ( events & SAMPLEAPP_SEND_PERIODIC_MSG_EVT )
{
// Send the periodic message
SampleApp_SendPeriodicMessage();
// Setup to send message again in normal period (+ a little jitter)
osal_start_timerEx( SampleApp_TaskID, SAMPLEAPP_SEND_PERIODIC_MSG_EVT,
(SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT + (osal_rand() & 0x00FF)) );
// return unprocessed events
return (events ^ SAMPLEAPP_SEND_PERIODIC_MSG_EVT);
}
// Discard unknown events
return 0;
}
前面已經說到,事件用一個二進制位1來表示,那么一個“與”操作就可以判斷出來到底有沒有事件。后return采用“異或”,這樣可以將已經處理的事件清除掉。不清楚的小伙伴可以舉一個實際的例子,仔細計算一番之后你會發現的確是這種寫法。。。
在ZigBee協議棧中,用戶可以定義自己的事件,但是,協議棧同時也給出了幾個已經定義好的事件,由協議棧定義的事件成為系統強制事件(Mandatory Events),SYS_EVENT_MSG就是其中的一個事件,SYS_EVENT_MSG的定義如下:
那SAMPLEAPP_SEND_PERIODIC_MSG_EVT事件 是不是就是用戶自己定義的事件了?是的!我們來看下它的定義:
提到事件,我們就不得不提到消息。事件是驅動任務去執行某些操作的條件,當系統中產生了一個事件,OSAL將這個事件傳遞給相應的任務后,任務才能執行一個相應的操作(調用事件處理函數去處理)。
通常某些事件發生時,又伴隨著一些附加信息的產生,例如:從天線接收到數據后,會產生AF_INCOMING_MSG_CMD消息,但是任務的事件處理函數在處理這個事件的時候,還需要得到收到的數據。
因此,這就需要將事件和數據封裝成一個消息,將消息發送到消息隊列,然后在事件處理函數中就可以使用osal_msg_receive,從消息隊列中得到該消息。如下代碼可以獲得指向從消息隊列中得到消息的指針。
在使用ZigBee協議棧進行應用程序開發時,如何在應用程序中添加一個新任務呢?
打開OSAL_SampleApp.c文件,可以找到數組tasksArr[]和函數osalInitTasks()。tasksArr[]數組里存放了所有的事件處理函數的地址;osalInitTasks()是OSAL的任務初始化函數,所有任務的初始化工作都在這里邊完成,并且自動給每個任務分配一個ID。
因此,要添加新任務,只需要編寫兩個函數:
● 新任務的初始化函數。
● 新任務的事件處理函數。
將事件處理函數的地址加入tasksArr[]數組,如下代碼所示。
將新任務的初始化函數添加在osalInitTasks()函數的后,如下代碼所示。
我們需要注意的是:
tasksArr[]數組里各事件處理函數的排列順序要與osalInitTasks函數中調用各任務初始化函數的順序保持一致,只有這樣才能保證當任務有事件發生時會調用每個任務對應的事件處理函數。為了保存osalInitTasks()函數所分配的任務ID,需要給每一個任務定義一個全局變量。如在SampleApp.c文件中定義了一個全局變量SampleApp_TaskID,并且在osalInitTasks()函數中進行了賦值。
我們現在總結下OSAL的運行機理:
● 通過不斷地查詢事件表來判斷每個任務中是否有事件發生,如果有事件發生,則查找函數表找到對應的事件處理函數對事件進行處理。
● 事件表使用數組來實現,數組的每一項對應一個任務的事件,每一位表示一個事件;函數表使用函數指針數組來實現,數組的每一項是一個函數指針,指向了事件處理函數。