一篇學會回調函數
函數指針
學習回調函數,其實就是函數指針的應用,關于函數指針在之前的文章《??指針與函數??》中有詳細的講解,這里不再展開詳解,重新貼一下之前文章中函數指針的示例代碼:
#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);
}
運行結果如下:
為什么需要回調函數
這里先說一下軟件分層的問題,軟件分層的一般原則是:上層可以直接調用下層的函數,下層則不能直接調用上層的函數。這句話說來簡單,在現實中,下層常常要反過來調用上層的函數。
比如你在拷貝文件時,在界面層調用一個拷貝文件函數。界面層是上層,拷貝文件函數是下層,上層調用下層,理所當然。但是如果你想在拷貝文件時還要更新進度條,問題就來了。
一方面,只有拷貝文件函數才知道拷貝的進度,但它不能去更新界面的進度條。另外一方面,界面知道如何去更新進度條,但它又不知道拷貝的進度。怎么辦?
常見的做法,就是界面設置一個回調函數給拷貝文件函數,拷貝文件函數在適當的時候調用這個回調函數來通知界面更新狀態。
上面主要說的一個大型軟件分層理念,作為嵌入式開發程序員,特別是單片機的開發中,由于和硬件結合緊密且需要快速響應,軟件結構大部分是面向過程開發的,回調函數使用頻率并不高。但在軟件中使用回調函數,可以讓軟件更加模塊化。
上圖形象展示了回調函數的作用,上面說到了軟件分層,在嵌入式代碼中我們一般將和硬件交互的代碼稱為硬件層,業務邏輯代碼稱為應用層代碼,對于優秀的的嵌入式代碼,一般要求硬件層和應用層代碼分開。
一般的回調函數代碼結構如下:
typedef void (*ReceiveFarmDataFun)();
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;
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庫。
后記
讀到這里的同學可能覺得這完全是“脫褲子放屁”啊,這屬于“炫技”啊,沒什么用啊。誠然在很多應用中,特別是一些單片機項目中,代碼量不大,使用類似receive_flg全局變量控制,代碼結構也清晰啊。
并且項目不需封裝庫給客戶,一個單片機軟件開發工程師可以吃透整個項目的代碼,根本不需要這樣的“騷操作”。
關于回調函數,我的態度是:回調函數可以使我們的代碼更高效且更易于維護,降低耦合。明智地使用它們很重要,否則過度使用回調(函數指針)會使代碼難以進行排查和調試。