用 FreeRTOS 搭建 Event-Driven 應用框架

[導讀] 大家好,我是逸珺。

今天來分享一下,之前項目中使用 FreeRTOS 搭建的 Event-Driven 事件驅動框架。

什麼是 Event-Driven?

Event-DrivenEvent 在計算機編程方法中,是一種廣爲使用的編程範式。比如 Windows 中的鼠標、鍵盤輸入,就被 Windows 操作系統管理成了外部輸入事件,由操作系統向不同的應用分發這些輸入事件,再由用戶應用程序完成相應的動作 Action。在 GUI 編程中,這是一種主要的編程範式。

其基本結構可以用下面這張圖來描述:

爲什麼推崇事件驅動?

常規的做法是程序按照固有的順序執行,這樣的編程方式,靈活性比較差。一旦需求稍有變動,可能就需要比較大的修改。在現代編程方法論中,軟件的複雜度越來越大,傳統過程方法不能滿足複雜軟件的需求,可維護性很差。用戶與軟件的交互體驗也很差。

要回答爲什麼要推崇事件驅動範式,先來看看其特點:

通過上面簡要的總結其特徵,再來看看爲什麼這個範式比較好:

用 FreeRTOS 搭事件驅動框架

FreeRTOS 的 Queue 提供了任務到任務、任務到中斷、中斷到任務、中斷到任務間的通訊機制。關於 FreeRTOS 隊列本身應如何使用的細節,這裏不作展開。

假定 Task0 需要處理這樣一些事件,可以定義如下枚舉:

代碼如下:

typedef enum  {
    TASK0_EVENT_0,
    TASK0_EVENT_1,
    TASK0_EVENT_2
    .....
} Task0EventType;
typedef struct Task0Event_t {
    Task0EventType  type;
    union {
       float para1;
       int  para2;
       bool on;
       struct {
         xxx;
       }xxx;
    } params;    
} Task0Event;

定義一個聯合 params 放在 Task0Event 內,可以使事件發送附加信息的能力,使用 union 則可以考慮到不同的事件發送方需要傳送的附加信息不一樣的需求,比如有的中斷需要發送開關量信息,有的甚至可能是一條報文或者很多信息。

將 Task0 的任務循環寫成下面這樣的形式:

xQueueHandle  task0_queue;
//假定每10毫秒循環一次
#define TASK0_INTERVAL_MS           10 

void task0_main(void)
{   
   Task0Event event;
   if(xQueueReceive(task0_queue,&event,(TASK0_INTERVAL_MS/portTICK_RATE_MS))==pdTRUE) 
   {
       prv_event_process(&event);
   }   
   /*其他處理*/
   .....
}

static void prv_event_process( Task0Event* event)
{
   switch( event->type )
   {
      case TASK0_EVENT_0:
         .....
         break;
         
      case TASK0_EVENT_1:
         .....
         break;
      
      case TASK0_EVENT_2:
     .....
         break;
         
      default:
        .....
        break;
   }
}

這樣就寫好了事件處理端了,只需要分析出與該任務有哪些外設或其他任務會對該任務發送事件,就可以很好的寫出事件發送相關的代碼了。

對於事件處理的函數,如果不用 switch-case 語句,定義一個這樣的事件回調函數表也是可以的,一定要討論哪種好,哪種不好,我覺得意義不是很大,看個人喜歡吧:

//函數指針這裏舉個簡單的例子,實際使用的時候,可能需要加參數,返回值等
typedef void (*Event_Handler)( Task0Event *event );
typedef struct EventProcessor_t
{
    Task0Event     event;
    Event_Handler  handler;
} EventProcessor;

EventProcessor task0_event_table[] = {
  {TASK0_EVENT_0,event0_handler},
  {TASK0_EVENT_1,event1_handler},
  {TASK0_EVENT_2,event2_handler},
  ......
}

void task0_main(void)
{   
   Task0Event event;
   if (xQueueReceive(task0_queue,&event, (TASK0_INTERVAL_MS/portTICK_RATE_MS)) == pdTRUE) 
   {
       task0_event_table[event.type].handler(&event);
   }
   
   /*其他處理*/
   .....
}

用一張圖來描述這個思路,就是這樣的:

中斷中發送

比如是一箇中斷需要對該任務發送事件 0,就可以在該中斷函數內如下發送事件:

void xxx_ISR(void)
{
    ....
    Task0Event event;
    event.type = TASK0_EVENT_0;
    portBASE_TYPE woken = pdFALSE;
    xQueueSendFromISR(task0_queue, &event, &woken);
}

對參數 pxHigherPriorityTaskWoken,做個簡要說明:

單個隊列可能會阻塞一個或多個任務,就是該事件可以被多個任務處理。調用這三個函數:

這三個函數使等待該事件的任務離開阻塞態。如果調用 API 函數導致任務離開阻塞狀態,並且未阻塞任務的優先級等於或高於當前正在執行的任務(被中斷的任務),那麼在 API 內部函數會將 *pxHigherPriorityTaskWoken 設置爲真。如果這些函數將此值設置爲 pdTRUE,則應在退出中斷之前執行上下文切換。這將確保中斷直接返回到最高優先級的就緒狀態任務。

這三個函數的原型爲:

BaseType_t xQueueSendFromISR( QueueHandle_t xQueue,  
                 const void *pvItemToQueue,  
                 BaseType_t *pxHigherPriorityTaskWoken ); 
 
BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue,  
                    const void *pvItemToQueue,  
                    BaseType_t *pxHigherPriorityTaskWoken ); 
 
BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue,  
                     const void *pvItemToQueue,  
                     BaseType_t *pxHigherPriorityTaskWoken );

這三個函數的作用基本類似,都是在中斷中可以使用的發送事件到隊列的 API:

任務中發送

如任務間需要協作,比如需要向 task0 發送事件 1,可以這樣寫:

void xxx_f(void)
{
   ....
   Task0Event event;
   event.type = TASK0_EVENT_1;
   xQueueSend(task0_queue, &event, portMAX_DELAY);
   ...
}

可被使用的 API 有這樣三個:

BaseType_t xQueueSend( QueueHandle_t xQueue,  
             const void * pvItemToQueue,  
             TickType_t xTicksToWait ); 
 
BaseType_t xQueueSendToFront( QueueHandle_t xQueue,  
                 const void * pvItemToQueue,  
                 TickType_t xTicksToWait ); 
 
BaseType_t xQueueSendToBack( QueueHandle_t xQueue,  
                const void * pvItemToQueue,  
                TickType_t xTicksToWait );

這三個函數的作用類似,區別與前面中斷版本類似,就不贅述了。

總結一下:

利用 FreeRTOS 搭建這樣一個事件驅動應用框架,可以很容易開發,後期維護也很方便。需要加個功能或修改功能,很容易擴展,這樣一種編程範式在其他的 RTOS 中也可以使用,只不過不同的 RTOS 提供的 API 會有差異,方法是相通的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YEUYii9es5wealwddykJeg