用 FreeRTOS 搭建 Event-Driven 應用框架
[導讀] 大家好,我是逸珺。
今天來分享一下,之前項目中使用 FreeRTOS 搭建的 Event-Driven 事件驅動框架。
什麼是 Event-Driven?
Event-DrivenEvent 在計算機編程方法中,是一種廣爲使用的編程範式。比如 Windows 中的鼠標、鍵盤輸入,就被 Windows 操作系統管理成了外部輸入事件,由操作系統向不同的應用分發這些輸入事件,再由用戶應用程序完成相應的動作 Action。在 GUI 編程中,這是一種主要的編程範式。
其基本結構可以用下面這張圖來描述:
-
事件生產者:對系統產生各種事件,併發送事件給系統
-
事件分發:將外部輸入的事件進行分發管理
-
事件隊列:事件分發後,對應的的事件處理者,有可能有多個事件,因此需要按先後次序依次排隊處理,所以就有事件隊列管理
-
事件消費者:負責處理由事件生產者發送給它的對應事件,產生響應。事件消費者一般有一個循環程序,一直偵聽事件隊列,如果接收到事件,則調用相應的處理函數。
爲什麼推崇事件驅動?
常規的做法是程序按照固有的順序執行,這樣的編程方式,靈活性比較差。一旦需求稍有變動,可能就需要比較大的修改。在現代編程方法論中,軟件的複雜度越來越大,傳統過程方法不能滿足複雜軟件的需求,可維護性很差。用戶與軟件的交互體驗也很差。
要回答爲什麼要推崇事件驅動範式,先來看看其特點:
-
多播通信:事件生產者產生的事件可以將事件發送給多個消費者,也就是事件接收端,因此具備很強的靈活性
-
實時傳輸:事件可以被事件分發者實時的傳輸給事件接收端。這在嵌入式應用中尤爲明顯
-
異步通信:事件發佈端不需要等待事件處理端處理前一個事件,發的管發,處理的管處理,這也是一種解耦設計的體現。
-
細粒度通信:事件生產者,可以持續發送細粒度事件,而不需要將一系列事件與其業務邏輯關聯,不需要聚合處理。
通過上面簡要的總結其特徵,再來看看爲什麼這個範式比較好:
-
敏捷性:敏捷性是指應對系統外部需求的快速變化的響應能力。在事件驅動編程範式中,功能域是鬆散耦合的。這可確保發生在一個組件上的更改不會影響系統中的其他組件。因此,事件驅動編程範式提供的敏捷程度很高。
-
易於部署:在事件驅動編程範式中,組件是鬆散耦合的。這在嵌入式 Linux 多應用程序組成的系統比較常見,在單片機中體現不出來。
-
可測試性:事件驅動編程範式中單元測試難度適中,因爲它需要特殊的測試客戶端和測試工具來生成測試所需的事件。需要考慮其他因素,例如跨功能域的交互順序。事件的組合和交互的順序在系統行爲中起着關鍵作用,需要成爲測試的關鍵考慮因素。
-
性能:事件驅動編程範式能夠並行執行異步操作。這帶來更好性能,而不管消息排隊和出隊所涉及的時間延遲如何。
-
可擴展性:由於組件的高度解耦特性,事件驅動編程範式提供了高度的可擴展性。
-
易於開發:由於該模式的異步性質,使用該模式的開發難度較低。
用 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,做個簡要說明:
單個隊列可能會阻塞一個或多個任務,就是該事件可以被多個任務處理。調用這三個函數:
-
xQueueSendFromISR()
-
xQueueSendToFrontFromISR()
-
xQueueSendToBackFromISR()
這三個函數使等待該事件的任務離開阻塞態。如果調用 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:
-
xQueueSendFromISR 或 xQueueSendToBackFromISR
將發送事件至隊尾;
-
xQueueSendToFrontFromISR 發送至對首。
任務中發送
如任務間需要協作,比如需要向 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