[導(dǎo)讀] 大家好,我是逸珺。
今天來分享一下,之前項(xiàng)目中使用FreeRTOS搭建的Event-Driven事件驅(qū)動(dòng)框架。
什么是Event-Driven?Event-DrivenEvent在計(jì)算機(jī)編程方法中,是一種廣為使用的編程范式。比如Windows中的鼠標(biāo)、鍵盤輸入,就被Windows操作系統(tǒng)管理成了外部輸入事件,由操作系統(tǒng)向不同的應(yīng)用分發(fā)這些輸入事件,再由用戶應(yīng)用程序完成相應(yīng)的動(dòng)作Action。在GUI編程中,這是一種主要的編程范式。
其基本結(jié)構(gòu)可以用下面這張圖來描述:
事件生產(chǎn)者:對(duì)系統(tǒng)產(chǎn)生各種事件,并發(fā)送事件給系統(tǒng)
事件分發(fā):將外部輸入的事件進(jìn)行分發(fā)管理
事件隊(duì)列:事件分發(fā)后,對(duì)應(yīng)的的事件處理者,有可能有多個(gè)事件,因此需要按先后次序依次排隊(duì)處理,所以就有事件隊(duì)列管理
事件消費(fèi)者:負(fù)責(zé)處理由事件生產(chǎn)者發(fā)送給它的對(duì)應(yīng)事件,產(chǎn)生響應(yīng)。事件消費(fèi)者一般有一個(gè)循環(huán)程序,一直偵聽事件隊(duì)列,如果接收到事件,則調(diào)用相應(yīng)的處理函數(shù)。
為什么推崇事件驅(qū)動(dòng)?常規(guī)的做法是程序按照固有的順序執(zhí)行,這樣的編程方式,靈活性比較差。一旦需求稍有變動(dòng),可能就需要比較大的修改。在現(xiàn)代編程方法論中,軟件的復(fù)雜度越來越大,傳統(tǒng)過程方法不能滿足復(fù)雜軟件的需求,可維護(hù)性很差。用戶與軟件的交互體驗(yàn)也很差。
要回答為什么要推崇事件驅(qū)動(dòng)范式,先來看看其特點(diǎn):
多播通信:事件生產(chǎn)者產(chǎn)生的事件可以將事件發(fā)送給多個(gè)消費(fèi)者,也就是事件接收端,因此具備很強(qiáng)的靈活性
實(shí)時(shí)傳輸:事件可以被事件分發(fā)者實(shí)時(shí)的傳輸給事件接收端。這在嵌入式應(yīng)用中尤為明顯
異步通信:事件發(fā)布端不需要等待事件處理端處理前一個(gè)事件,發(fā)的管發(fā),處理的管處理,這也是一種解耦設(shè)計(jì)的體現(xiàn)。
細(xì)粒度通信:事件生產(chǎn)者,可以持續(xù)發(fā)送細(xì)粒度事件,而不需要將一系列事件與其業(yè)務(wù)邏輯關(guān)聯(lián),不需要聚合處理。
通過上面簡(jiǎn)要的總結(jié)其特征,再來看看為什么這個(gè)范式比較好:
敏捷性:敏捷性是指應(yīng)對(duì)系統(tǒng)外部需求的快速變化的響應(yīng)能力。在事件驅(qū)動(dòng)編程范式中,功能域是松散耦合的。這可確保發(fā)生在一個(gè)組件上的更改不會(huì)影響系統(tǒng)中的其他組件。因此,事件驅(qū)動(dòng)編程范式提供的敏捷程度很高。
易于部署:在事件驅(qū)動(dòng)編程范式中,組件是松散耦合的。這在嵌入式Linux多應(yīng)用程序組成的系統(tǒng)比較常見,在單片機(jī)中體現(xiàn)不出來。
可測(cè)試性:事件驅(qū)動(dòng)編程范式中單元測(cè)試難度適中,因?yàn)樗枰厥獾臏y(cè)試客戶端和測(cè)試工具來生成測(cè)試所需的事件。需要考慮其他因素,例如跨功能域的交互順序。事件的組合和交互的順序在系統(tǒng)行為中起著關(guān)鍵作用,需要成為測(cè)試的關(guān)鍵考慮因素。
性能:事件驅(qū)動(dòng)編程范式能夠并行執(zhí)行異步操作。這帶來更好性能,而不管消息排隊(duì)和出隊(duì)所涉及的時(shí)間延遲如何。
可擴(kuò)展性:由于組件的高度解耦特性,事件驅(qū)動(dòng)編程范式提供了高度的可擴(kuò)展性。
易于開發(fā):由于該模式的異步性質(zhì),使用該模式的開發(fā)難度較低。
用FreeRTOS搭事件驅(qū)動(dòng)框架FreeRTOS的Queue提供了任務(wù)到任務(wù)、任務(wù)到中斷、中斷到任務(wù)、中斷到任務(wù)間的通訊機(jī)制。關(guān)于FreeRTOS隊(duì)列本身應(yīng)如何使用的細(xì)節(jié),這里不作展開。
假定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;
定義一個(gè)聯(lián)合params放在Task0Event內(nèi),可以使事件發(fā)送附加信息的能力,使用union則可以考慮到不同的事件發(fā)送方需要傳送的附加信息不一樣的需求,比如有的中斷需要發(fā)送開關(guān)量信息,有的甚至可能是一條報(bào)文或者很多信息。
將Task0的任務(wù)循環(huán)寫成下面這樣的形式:
xQueueHandle task0_queue;
//假定每10毫秒循環(huán)一次
#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;
}
}
這樣就寫好了事件處理端了,只需要分析出與該任務(wù)有哪些外設(shè)或其他任務(wù)會(huì)對(duì)該任務(wù)發(fā)送事件,就可以很好的寫出事件發(fā)送相關(guān)的代碼了。
對(duì)于事件處理的函數(shù),如果不用switch-case語句,定義一個(gè)這樣的事件回調(diào)函數(shù)表也是可以的,一定要討論哪種好,哪種不好,我覺得意義不是很大,看個(gè)人喜歡吧:
//函數(shù)指針這里舉個(gè)簡(jiǎn)單的例子,實(shí)際使用的時(shí)候,可能需要加參數(shù),返回值等
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();
}
/*其他處理*/
.....
}
用一張圖來描述這個(gè)思路,就是這樣的:
中斷中發(fā)送
比如是一個(gè)中斷需要對(duì)該任務(wù)發(fā)送事件0,就可以在該中斷函數(shù)內(nèi)如下發(fā)送事件:
void xxx_ISR(void)
{
....
Task0Event event;
event.type = TASK0_EVENT_0;
portBASE_TYPE woken = pdFALSE;
xQueueSendFromISR(task0_queue, &event, &woken);
}
對(duì)參數(shù)pxHigherPriorityTaskWoken,做個(gè)簡(jiǎn)要說明:
單個(gè)隊(duì)列可能會(huì)阻塞一個(gè)或多個(gè)任務(wù),就是該事件可以被多個(gè)任務(wù)處理。調(diào)用這三個(gè)函數(shù):
xQueueSendFromISR()
xQueueSendToFrontFromISR()
xQueueSendToBackFromISR()
這三個(gè)函數(shù)使等待該事件的任務(wù)離開阻塞態(tài)。如果調(diào)用API函數(shù)導(dǎo)致任務(wù)離開阻塞狀態(tài),并且未阻塞任務(wù)的優(yōu)先級(jí)等于或高于當(dāng)前正在執(zhí)行的任務(wù)(被中斷的任務(wù)),那么在API內(nèi)部函數(shù)會(huì)將 *pxHigherPriorityTaskWoken設(shè)置為真。如果這些函數(shù)將此值設(shè)置為 pdTRUE,則應(yīng)在退出中斷之前執(zhí)行上下文切換。這將確保中斷直接返回到最高優(yōu)先級(jí)的就緒狀態(tài)任務(wù)。
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 );
這三個(gè)函數(shù)的作用基本類似,都是在中斷中可以使用的發(fā)送事件到隊(duì)列的API:
xQueueSendFromISR或xQueueSendToBackFromISR
將發(fā)送事件至隊(duì)尾;
xQueueSendToFrontFromISR發(fā)送至對(duì)首。
void xxx_f(void)
{
....
Task0Event event;
event.type = TASK0_EVENT_1;
xQueueSend(task0_queue, &event, portMAX_DELAY);
...
}
可被使用的API有這樣三個(gè):
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 );
這三個(gè)函數(shù)的作用類似,區(qū)別與前面中斷版本類似,就不贅述了。
總結(jié)一下:利用FreeRTOS搭建這樣一個(gè)事件驅(qū)動(dòng)應(yīng)用框架,可以很容易開發(fā),后期維護(hù)也很方便。需要加個(gè)功能或修改功能,很容易擴(kuò)展,這樣一種編程范式在其他的RTOS中也可以使用,只不過不同的RTOS提供的API會(huì)有差異,方法是相通的。