1、函式指標
學習回呼函式,其實就是函式指標的應用,關于函式指標在之前的文章《指標與函式》中有詳細的講解,這里不再展開詳解,重新貼一下之前文章中函式指標的示例代碼
#include <stdio.h> void MyFun1(int x); void MyFun2(int x); void MyFun3(int x); typedef void (*FunType)(int); /* ②. 定義一個函式指標型別FunType,與①函式型別一致 */ void CallMyFun(FunType fp, int x); int main(int argc, char *argv[]) { CallMyFun(MyFun1, 10); /* ⑤. 通過CallMyFun函式分別呼叫三個不同的函式 */ CallMyFun(MyFun2, 20); CallMyFun(MyFun3, 30); } void CallMyFun(FunType fp, int x) /* ③. 引數fp的型別是FunType,*/ { fp(x); /* ④. 通過fp的指標執行傳遞進來的函式,注意fp所指的函式是有一個引數的, */ } void MyFun1(int x) /* ①. 這是個有一個引數的函式,以下兩個函式也相同, */ { printf("MyFun1:%d\n", x); } void MyFun2(int x) { printf("MyFun2:%d\n", x); } void MyFun3(int x) { printf("MyFun3:%d\n", x); }
運行結果如下

建議看懂這個示例代碼再往下看本篇文章,如果對示例代碼有疑問,請移步之前的文章《指標與函式》,
2、為什么需要回呼函式
這里先說一下軟體分層的問題,軟體分層的一般原則是:上層可以直接呼叫下層的函式,下層則不能直接呼叫上層的函式,這句話說來簡單,在現實中,下層常常要反過來呼叫上層的函式,
比如你在拷貝檔案時,在界面層呼叫一個拷貝檔案函式,界面層是上層,拷貝檔案函式是下層,上層呼叫下層,理所當然,但是如果你想在拷貝檔案時還要更新進度條,問題就來了,
一方面,只有拷貝檔案函式才知道拷貝的進度,但它不能去更新界面的進度條,另外一方面,界面知道如何去更新進度條,但它又不知道拷貝的進度,怎么辦?
常見的做法,就是界面設定一個回呼函式給拷貝檔案函式,拷貝檔案函式在適當的時候呼叫這個回呼函式來通知界面更新狀態,
上面主要說的一個大型軟體分層理念,作為嵌入式開發程式員,特別是單片機的開發中,由于和硬體結合緊密且需要快速回應,軟體結構大部分是面向程序開發的,回呼函式使用頻率并不高,但在軟體中使用回呼函式,可以讓軟體更加模塊化,

上圖形象展示了回呼函式的作用,上面說到了軟體分層,在嵌入式代碼中我們一般將和硬體互動的代碼稱為硬體層,業務邏輯代碼稱為應用層代碼,對于優秀的的嵌入式代碼,一般要求硬體層和應用層代碼分開,
一般的回呼函式代碼結構如下
typedef void (*ReceiveFarmDataFun)(); static CallbackReceive_t HandlerCompleted; /*用來注冊回呼函式的功能函式*/ void CallbackRegister (CallbackFunc_t callback_func) { HandlerCompleted = callback_func; }
3、串口應用
在嵌入式應用中,串口通信是很經典且常用的外設,舉一個簡單的栗子,接收的串口資料幀頭是@,幀尾是*,中間資料不可能出現@和*,那么一般情況下代碼如下撰寫,
/*串口中斷函式*/ uint8_t receive_flg = 0; uint8_t receive_data[100]; uint8_t USART1_data = 0; uint8_t USART1_data_len = 0; uint8_t USART1_receive_sta = 0; void USART1_IRQHandler(void) { uint8_t data_tmp; if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE)) { data_tmp = USART_ReceiveData(USART1); if((data_tmp == '*')&&(USART1_receive_sta == 1)) { receive_flg = 1; USART1_receive_sta = 0; receive_data[USART1_data_len++] = data_tmp; } if(receive_flg == 0){ if(data_tmp == '@') { USART1_receive_sta = 1; USART1_data_len = 0; } if(USART1_receive_sta) receive_data[USART1_data_len++] = data_tmp; if(USART1_data_len > (100-1)) { receive_flg = 0; USART1_receive_sta = 0; } } USART_ClearFlag(USART1, USART_FLAG_RXNE); } } /*應用層代碼,簡單化->在main函式*/ void main() { /*省略其他代碼*/ while(1) { if(receive_flg == 1)//通過檢查receive_data判斷是否接收到函式 { /*通過receive_data陣列處理資料*/ receive_flg = 0; } } }
這樣實作功能是沒有問題的,在我接觸到很多的專案中的確是類似的架構,但是它的移植性較差,
還有一種情況,那就是如果你接到需求把硬體層封裝給客戶使用,不讓客戶看到原始碼,封裝成庫,起到"保護通訊協議"的目的,那么你要告訴客戶,需要判斷receive_flg變數,然后讀取receive_data陣列的內容???
不得不說,你這樣干是可以的,但是大部分公司不會這樣干的,這時候可以使用回呼函式來解決這個問題,
/*開放給客戶的頭檔案*/ /* Includes ------------------------------------------------------------------*/ #include <stdio.h> typedef void (*ReceiveFarmDataFun)(uint8_t *buff,uint32_t bufferlen); extern void CallbackRegister (CallbackFunc_t callback_func); /*封裝的函式*/ static CallbackReceive_t HandlerCompleted; void CallbackRegister (CallbackFunc_t callback_func) { HandlerCompleted = callback_func; } uint8_t receive_flg = 0; uint8_t receive_data[100]; uint8_t USART1_data = 0; uint8_t USART1_data_len = 0; uint8_t USART1_receive_sta = 0; void USART1_IRQHandler(void) { uint8_t data_tmp; if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE)) { data_tmp = USART_ReceiveData(USART1); if((data_tmp == '*')&&(USART1_receive_sta == 1)) { receive_flg = 1; USART1_receive_sta = 0; HandlerCompleted(receive_data,USART1_data_len); } if(receive_flg == 0){ if(data_tmp == '@') { USART1_receive_sta = 1; USART1_data_len = 0; } if(USART1_receive_sta) receive_data[USART1_data_len++] = data_tmp; if(USART1_data_len > (100-1)) { receive_flg = 0; USART1_receive_sta = 0; } } USART_ClearFlag(USART1, USART_FLAG_RXNE); } }
那么客戶拿到的有用資訊如下
typedef void (*ReceiveFarmDataFun)(uint8_t *buff,uint32_t bufferlen); extern void CallbackRegister (CallbackFunc_t callback_func);
客戶可以寫如下代碼
void uartdatadeal(uint8_t *buff,uint32_t bufferlen) { /*buff指標存盤了串口資料,bufferlen存盤資料長度*/ /*客戶的應用層代碼*/ } void main() { /*省略其他代碼*/ CallbackRegister (uartdatadeal); while(1) { } }
這樣的話,就可以解決上述問題,客戶只要注冊一下串口接收的函式,當接收到有效資料后,就可以跳轉到用戶的代碼,而你可以將自己的硬體層封裝起來,
看到這里可能有嵌入式大佬意識到某些問題了,這樣寫代碼,資料處理的函式就等于在中斷里了,這是不合理的啊,
是的,是有這個問題,所以給客戶的庫檔案必須說明這一點,讓客戶自行選擇,客戶不想在中斷中執行,可以再按照我們一開始的邏輯寫啊,如下
void uartdatadeal(uint8_t *buff,uint32_t bufferlen) { /*buff指標存盤了串口資料,bufferlen存盤資料長度*/ receive_flg = 1; } void main() { /*省略其他代碼*/ CallbackRegister (uartdatadeal); while(1) { if(receive_flg == 1) { /*處理資料*/ receive_flg = 0; } } }
事實上,芯片/模塊廠家寫SDK經常這樣做,一些大型的開源庫也會這樣用,典型的如lwip庫,
4、后記
讀到這里的同學可能覺得這完全是“脫褲子放屁”啊,這屬于“炫技”啊,沒什么用啊,誠然在很多應用中,特別是一些單片機專案中,代碼量不大,使用類似receive_flg全域變數控制,代碼結構也清晰啊,
并且專案不需封裝庫給客戶,一個單片機軟體開發工程師可以吃透整個專案的代碼,根本不需要這樣的“騷操作”,
關于回呼函式,我的態度是:回呼函式可以使我們的代碼更高效且更易于維護,降低耦合,明智地使用它們很重要,否則過度使用回呼(函式指標)會使代碼難以進行排查和除錯,
點擊查看本文所在的專輯:C語言進階專輯
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/458334.html
標籤:其他
上一篇:回呼函式
下一篇:2022-A rch安裝(詳細)
