zigbee 中 OSAL 事件傳遞機制和消息傳遞機制


一、概述

OSAL (Operating System Abstraction Layer) ,翻譯為“操作系統抽象層”。 OSAL 就是一種支持多任務運行的系統資源分配機制。OSAL與標准的操作系統還是有很大的區別的。簡單而言, OSAL 實現了類似操作系統的某些功能,但並不能稱之為真正意義上的操作系統。

二、OSAL任務運行方式

我們以TI1.2.1的 BLE 協議棧中的 SimpleBLEPeripheral 為例,分析一下 OSAL 。其中有一個 simpleBLEPeripheral.c 文件,里面有2個比較重要的函數:SimpleBLEPeripheral_InitSimpleBLEPeripheral_ProcessEventSimpleBLEPeripheral_Init 是任務的初始化函數,而 SimpleBLEPeripheral_ProcessEvent 則負責處理傳遞給此任務的事件。

大概瀏覽一下 SimpleBLEPeripheral_ProcessEvent 這個函數,我們可以發現,此函數的主要功能是判斷由參數傳遞的事件類型,然后執行相應的事件處理函數。由此,可以推斷出 BLE 協議棧應用程序的運行機制如下圖所示:

img

當有一個事件發生的時候, OSAL 負責將此事件分配給能夠處理此事件的任務,然后此任務判斷事件的類型,調用相應的事件處理程序進行處理。
明白了這個問題,新的問題又擺在了我們的面前: OSAL 是如何傳遞事件給任務的。

三、OSAL的事件傳遞機制

在試圖弄清楚這個問題之前,我們需要弄清楚另外一個十分基礎而重要的問題。那就是如何向我們的應用程序中添加一個任務。

我們先來看看 simpleBLEPeripheral.c 是如何添加任務的。

我們打開 OSAL_SimpleBLEPeripheral.c 文件。這里我們可以找到一個很重要的數組tasksArr 和一個同樣很重要的函數 osalInitTasks

TaskArr 這個數組里存放了所有任務的事件處理函數的地址,在這里事件處理函數就代表了任務本身,也就是說事件處理函數標識了與其對應的任務。

osalInitTasks 是 OSAL 的任務初始化函數,所有任務的初始化工作都在這里面完成,並且自動給每個任務分配一個ID。

要添加新任務,我們需要編寫新任務的事件處理函數和初始化函數,然后將事件處理函數的地址加入此數組。然后在 osalInitTasks 中調用此任務的初始化函數。在此例中,我們此前提到過的SimpleBLEPeripheral_ProcessEvent 這個函數被添加到了數組的末尾,  SimpleBLEPeripheral_Init 這個函數在 osalInitTasks 中被調用。

值得注意的是,TaskArr 數組里各任務函數的排列順序要與 osalInitTasks 函數中調用各任務初始化函數的順序必須一致,只有這樣才能夠保證每個任務能夠通過初始化函數接收到正確的任務ID。

另外,為了保存任務初始化函數所接收的任務ID,我們需要給每一個任務定義一個全局變量來保存這個ID。在 SimpleBLEPeripheralSimpleBLEPeripheral.c 中定義了一個全局變量 SimpleBLEPeripheral_TaskID ;並且在 SimpleBLEPeripheral_Init 函數中進行了賦值

{
  SimpleBLEPeripheral_TaskID = task_id;
}

這條語句將分配給SimpleBLEPeripheral 的任務ID保存了下來。
到此,我們就給應用程序中完整的添加了一個任務。

我們回到 OSAL 如何將事件分配給任務這個問題上來, 在 OSAL_SimpleBLEPeripheral.c這個文件中,在定義 TaskArr 這個數組之后,又定義了兩個全局變量。
tasksCnt這個變量保存了當前的任務個數。
tasksEvents 是一個指向數組的指針,此數組保存了當前任務的狀態。在任務初始化函數中做了如下操作

{
  tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
  osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
}
/*osal_mem_alloc()為當前OSAL中的各任務分配存儲空間(實際上是一個任務數組),
  函數返回指向任務緩沖區的指針,因此tasksEvents指向該任務數組(任務隊列).注意
  tasksEvents和后面談到的tasksArr[]里的順序是一一對應的, tasksArr[ ]中的第i個
  事件處理函數對應於tasksEvents中的第i個任務的事件.*/

/*osal_memset()把開辟的內存全部設置為0;sizeof( uint16 )是2個字節,即一個任務的長度(任務函數同樣是uint16定義),乘以任務數量tasksCnt,即全部內存空間*/

我們可以看出所有任務的狀態都被初始化為0。代表了當前任務沒有需要響應的事件。

緊接着,我們來到了 main() 函數。此 SimpleBLEPeripheral_Main.c 文件中。略過許多對當前來說並非重要的語句,我們先來看 osal_init_system() 這個函數。在此函數中,osalInitTasks() 被調用,從而tasksEvents 中的所有內容被初始化為0。

之后,在main() 函數中,我們進入了osal_start_system() 函數,此函數為一個死循環,在這個循環中,完成了所有的事件分配。

首先我們來看這樣一段代碼:

{
  do
  {
    if (tasksEvents[idx])
    {
      break;
    }
  } while (++idx < tasksCnt);
}

tasksEvents 這個數組中的某個元素不為0,即代表此任務有事件需要相應,事件類型取決於這個元素的值。這個do-while 循環會選出當前優先級最高的需要響應的任務,

{
  events = (tasksArr[idx])( idx, events );
}

此語句調用 tasksArr數組里面相應的事件處理函數來響應事件。如果我們新添加的任務有了需要響應的事件,那么此任務的事件處理程序將會被調用。

就這樣,OSAL 就將需要響應的事件傳遞給了對應的任務處理函數進行處理。

附:詳解 events = (tasksArr[idx])( idx, events );
(tasksArr[idx])( idx, events )是一個函數指針數組。那么什么是函數指針數組呢?顧名思義,函數指針數組是一個數組,數組中存放的元素類型是函數的指針。表達式舉例:char(*p[])(int i) ;對於這個表達式我們從語法上解釋為,p是一個數組變量名,數組變量類型是char(*)(int i),存放元素的類型是:char(int i)函數的指針。
tasksArr[idx]就是一個函數指針數組,里面存儲的就是函數的指針。

const pTaskEventHandlerFn tasksArr[] =
{
  LL_ProcessEvent,                                                  // task 0
  Hal_ProcessEvent,                                                 // task 1
  HCI_ProcessEvent,                                                 // task 2

#if defined ( OSAL_CBTIMER_NUM_TASKS )
  OSAL_CBTIMER_PROCESS_EVENT( osal_CbTimerProcessEvent ),           // task 3
#endif
  L2CAP_ProcessEvent,                                               // task 4
  GAP_ProcessEvent,                                                 // task 5
  GATT_ProcessEvent,                                                // task 6
  SM_ProcessEvent,                                                  // task 7
  GAPRole_ProcessEvent,                                             // task 8
  GAPBondMgr_ProcessEvent,                                          // task 9
  GATTServApp_ProcessEvent,                                         // task 10
  SimpleBLEPeripheral_ProcessEvent                                  // task 11
};

假設idx=11;;那么events = (tasksArr[11])( 11, events );;也就是調用了SimpleBLEPeripheral_ProcessEvent這個函數,其中傳入的參數就是(11,events);也就是調用了events = (tasksArr[11])( 11, events );其實就是執行了SimpleBLEPeripheral_ProcessEvent(11,events);

四、事件的捕獲

不過接下來就有了更加深入的問題了,事件是如何被捕獲的?直觀一些來說就是,tasksEvents這個數組里的元素是什么時候被設定為非零數,來表示有事件需要處理的?為了詳細的說明這個過程,我將以SimpleBLEPeripheral這個例程中響應按鍵的過程來進行說明。其他的事件雖然稍有差別,卻是大同小異。

按鍵在我們的應用里面應該屬於硬件資源,所以OSAL理應為我們提供使用和管理這些硬件的服務。稍微留意一下我們之前說過的tasksArr這樣一個數組,它保存了所有任務的事件處理函數。我們從中發現了一個很重要的信息:Hal_ProcessEventHAL(Hardware Abstraction Layer)翻譯為“硬件抽象層”。許多人在這里經常把將 BLE 的硬件抽象層與物理層混為一談。在這里,我們應該將 BLE 的硬件抽象層與物理層區分開來。硬件抽象層所包含的范圍是我們當前硬件電路上面所有對於系統可用的設備資源。而物理層則是針對無線通信而言,它所包含的僅限於支持無線通訊的硬件設備。

通過這個重要的信息,我們可以得出這樣一個結論: OSAL 將硬件的管理也作為一個任務來處理。那么我們很自然的去尋找 Hal_ProcessEvent 這個事件處理函數,看看它究竟是如何管理硬件資源的。

“HAL\Commn\ hal_drivers.c” 這個文件中,我們找到了這個函數。我們直接分析與按鍵有關的一部分。

{
  if (events & HAL_KEY_EVENT)
  {
    #if (defined HAL_KEY) && (HAL_KEY == TRUE)
    /* Check for keys */
    HalKeyPoll();
    /* if interrupt disabled, do next polling */
    if (!Hal_KeyIntEnable)
    {
      osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT, 100);
    }
    #endif // HAL_KEY
    return events ^ HAL_KEY_EVENT;
  }
}

在事件處理函數接收到 HAL_KEY_EVENT 這樣一個事件后,首先執行HalKeyPoll()函數。由於這個例程的按鍵采用查詢的方法獲取,所以是禁止中斷的,於是表達式(!Hal_KeyIntEnable)的值為真。那么osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT, 100)得以執行。osal_start_timerEx 這是一個很常用的函數,它在這里的功能是經過100毫秒后,向 Hal_TaskID 這個ID所標示的任務(也就是其本身)發送一個 HAL_KEY_EVENT 事件。這樣以來,每經過100毫秒, Hal_ProcessEvent這個事件處理函數都會至少執行一次來處理 HAL_KEY_EVENT 事件。也就是說每隔100毫秒都會執行 HalKeyPoll() 函數。

那么我們來看看HalKeyPoll函數到底在搞什么鬼!
代碼中給的注釋為:

/* Check for keys */
HalKeyPoll();

於是我們推斷這個函數的作用是檢查當前的按鍵情況。進入函數一看,果不其然。雖然這個函數很長很復雜,不過憑借着非凡的聰明才智,我們還是十分清楚的明白了,經過一系列的if語句和賦值語句,在接近函數末尾的地方, keys變量(在函數起始位置定義的)獲得了當前按鍵的狀態。最后,有一個十分重要的函數調用。

(pHalKeyProcessFunction) (keys, HAL_KEY_STATE_NORMAL);

pHalKeyProcessFunction這個函數指針指向了哪個函數我們現在依然不清楚,但是為了我們有個清晰而不間斷的思路,我在這里先告訴大家。在這里調用的是

void OnBoard_KeyCallback ( uint8 keys, uint8 state )

這個函數。此函數在“OnBoard .c”文件中可以找到。在這個函數中,又調用了

void OnBoard_KeyCallback ( uint8 keys, uint8 state )

在這個函數中,按鍵的狀態信息被封裝到了一個消息結構體中(對於消息,我們稍后再說)。最后有一個極其重要的函數被調用了。

osal_msg_send( registeredKeysTaskID, (uint8 *)msgPtr );

與前面的pHalKeyProcessFunction相同,我先直接告訴大家registeredKeysTaskID所指示的任務正是我們需要響應按鍵的SimpleBLEPeripheral這個任務。

那么也就是說,在這里我們向SimpleBLEPeripheral發送了一個附帶按鍵信息的消息。在osal_msg_send函數中

osal_set_event( destination_task, SYS_EVENT_MSG );

被調用,它在這里的作用是設置destination_task這個任務的事件為SYS_EVENT_MSG。而這個destination_task正式由osal_msg_send這個函數通過參數傳遞而來的,它也指示的是SimpleBLEPeripheral這個任務。在osal_set_event這個函數中,有這樣一個語句:

{
  tasksEvents[task_id] |= event_flag;
}

至此,剛才所提到的問題得到了解決。我們再將這個過程整理一遍。

首先,OSAL專門建立了一個任務來對硬件資源進行管理,這個任務的事件處理函數是Hal_ProcessEvent 。在這個函數中通過調用osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT, 100);這個函數使得每隔100毫秒就會執行一次 HalKeyPoll()函數。HalKeyPoll()獲取當前按鍵的狀態,並且通過調用OnBoard_KeyCallback 函數向SimpleBLEPeripheral 任務發送一個按鍵消息,並且設置tasksEventsSimpleBLEPeripheral 所對應的值為非零。如此,當main函數里這樣一段代碼

{
  do
  {
    if (tasksEvents[idx])
    {
      break;
    }
  } while (++idx < tasksCnt);
}

執行了以后,SimpleBLEPeripheral 這個任務就會被挑選出來。然后通過

events = (tasksArr[idx])( idx, events );

這個函數調用其事件處理函數,完成事件的響應。
現在,我們回過頭來處理我們之前遺留下來的問題。

第一、pHalKeyProcessFunction 這個函數指針為何指向了OnBoard_KeyCallback函數。

HAL\Common\ hal_drivers.c這個文件中,我們找到了HalDriverInit 這個函數,在這個函數中,按鍵的初始化函數HalKeyInit 被調用。在HalKeyInit 中有這樣的語句:

{
  pHalKeyProcessFunction  = NULL;
}

這說明在初始化以后pHalKeyProcessFunction 並沒有指向任何一個函數。那pHalKeyProcessFunction 是什么時候被賦值的呢?

就在HalKeyInit的下方有一個這樣的函數HalKeyConfig 。其中有這樣一條語句:

pHalKeyProcessFunction = cback;

cbackHalKeyConfig 所傳進來的參數,所以,想要知道它所指向的函數,必須找到其調用的地方。經過簡單的搜索我們不難找出答案。在main函數中有這樣一個函數調用:InitBoard( OB_READY ); 此函數中做了如下調用:

{
  HalKeyConfig( OnboardKeyIntEnable, OnBoard_KeyCallback);
}

第二、registeredKeysTaskID 為什么標識了SimpleBLEPeripheral 這個任務?
由於 OSAL 是一個支持多任務的調度機制,所以在同一時間內將會有多個任務同時運行。但是從邏輯上來講,一個事件只能由一個任務來處理。按鍵事件也不例外。

那么如何向 OSAL 聲明處理按鍵事件的任務是SimpleBLEPeripheral 呢?
SimpleBLEPeripheral_InitSimpleBLEPeripheral 的任務初始化函數)中有這么一個語句:

{
  RegisterForKeys( SimpleBLEPeripheral_TaskID );
}

RegisterForKeys 函數向 OSAL 聲明按鍵事件將由 SimpleBLEPeripheral 任務來處理。在 RegisterForKeys 函數中:

{
  registeredKeysTaskID = task_id;
}

我想我不用再做多余的解釋了,聰明的您肯定可以理解。

五、消息隊列

首先我需要向大家解釋清楚消息與事件的聯系。事件是驅動任務去執行某些操作的條件,當系統產生了一個事件,將這個傳遞給相應的任務后,任務才能執行一個相應的操作。但是某些事件在它發生的同時,又伴隨着一些附加信息的產生。任務的事件處理函數在處理這個事件的時候,還需要參考其附加信息。最典型的一類便是按鍵消息,它同時產生了一個哪個按鍵被按下了附加信息。所以在OnBoard_SendKeys 這個函數中,不僅向SimpleBLEPeripheral 發送了事件,還通過調用osal_msg_send 函數向SimpleBLEPeripheral 發送了一個消息,這個消息記錄了這個事件的附加信息。在SimpleBLEPeripheral_ProcessEvent 中,通過

{
  MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( SimpleBLEPeripheral_TaskID );
}

獲取了這樣一個消息,然后再進一步處理。

OSAL 在后台維護了一個消息隊列,每一個消息都會被放到這個消息隊列中去,當任務接收到事件以后,從消息隊列中獲取屬於自己的消息,然后進行處理。

以上就是我就將 OSAL 這樣一個事件驅動的多任務的資源分配機制做了一個簡明扼要的介紹,希望對大家有所幫助。


免責聲明!

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



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