一篇學(xué)會(huì)回調(diào)函數(shù)
函數(shù)指針
學(xué)習(xí)回調(diào)函數(shù),其實(shí)就是函數(shù)指針的應(yīng)用,關(guān)于函數(shù)指針在之前的文章《??指針與函數(shù)??》中有詳細(xì)的講解,這里不再展開(kāi)詳解,重新貼一下之前文章中函數(shù)指針的示例代碼:
#include <stdio.h>
void MyFun1(int x);
void MyFun2(int x);
void MyFun3(int x);
typedef void (*FunType)(int); /* ②. 定義一個(gè)函數(shù)指針類(lèi)型FunType,與①函數(shù)類(lèi)型一致 */
void CallMyFun(FunType fp, int x);
int main(int argc, char *argv[])
{
CallMyFun(MyFun1, 10); /* ⑤. 通過(guò)CallMyFun函數(shù)分別調(diào)用三個(gè)不同的函數(shù) */
CallMyFun(MyFun2, 20);
CallMyFun(MyFun3, 30);
}
void CallMyFun(FunType fp, int x) /* ③. 參數(shù)fp的類(lèi)型是FunType。*/
{
fp(x); /* ④. 通過(guò)fp的指針執(zhí)行傳遞進(jìn)來(lái)的函數(shù),注意fp所指的函數(shù)是有一個(gè)參數(shù)的。 */
}
void MyFun1(int x) /* ①. 這是個(gè)有一個(gè)參數(shù)的函數(shù),以下兩個(gè)函數(shù)也相同。 */
{
printf("MyFun1:%d\n", x);
}
void MyFun2(int x)
{
printf("MyFun2:%d\n", x);
}
void MyFun3(int x)
{
printf("MyFun3:%d\n", x);
}
運(yùn)行結(jié)果如下:
為什么需要回調(diào)函數(shù)
這里先說(shuō)一下軟件分層的問(wèn)題,軟件分層的一般原則是:上層可以直接調(diào)用下層的函數(shù),下層則不能直接調(diào)用上層的函數(shù)。這句話說(shuō)來(lái)簡(jiǎn)單,在現(xiàn)實(shí)中,下層常常要反過(guò)來(lái)調(diào)用上層的函數(shù)。
比如你在拷貝文件時(shí),在界面層調(diào)用一個(gè)拷貝文件函數(shù)。界面層是上層,拷貝文件函數(shù)是下層,上層調(diào)用下層,理所當(dāng)然。但是如果你想在拷貝文件時(shí)還要更新進(jìn)度條,問(wèn)題就來(lái)了。
一方面,只有拷貝文件函數(shù)才知道拷貝的進(jìn)度,但它不能去更新界面的進(jìn)度條。另外一方面,界面知道如何去更新進(jìn)度條,但它又不知道拷貝的進(jìn)度。怎么辦?
常見(jiàn)的做法,就是界面設(shè)置一個(gè)回調(diào)函數(shù)給拷貝文件函數(shù),拷貝文件函數(shù)在適當(dāng)?shù)臅r(shí)候調(diào)用這個(gè)回調(diào)函數(shù)來(lái)通知界面更新?tīng)顟B(tài)。
上面主要說(shuō)的一個(gè)大型軟件分層理念,作為嵌入式開(kāi)發(fā)程序員,特別是單片機(jī)的開(kāi)發(fā)中,由于和硬件結(jié)合緊密且需要快速響應(yīng),軟件結(jié)構(gòu)大部分是面向過(guò)程開(kāi)發(fā)的,回調(diào)函數(shù)使用頻率并不高。但在軟件中使用回調(diào)函數(shù),可以讓軟件更加模塊化。
上圖形象展示了回調(diào)函數(shù)的作用,上面說(shuō)到了軟件分層,在嵌入式代碼中我們一般將和硬件交互的代碼稱(chēng)為硬件層,業(yè)務(wù)邏輯代碼稱(chēng)為應(yīng)用層代碼,對(duì)于優(yōu)秀的的嵌入式代碼,一般要求硬件層和應(yīng)用層代碼分開(kāi)。
一般的回調(diào)函數(shù)代碼結(jié)構(gòu)如下:
typedef void (*ReceiveFarmDataFun)();
static CallbackReceive_t HandlerCompleted;
/*用來(lái)注冊(cè)回調(diào)函數(shù)的功能函數(shù)*/
void CallbackRegister (CallbackFunc_t callback_func) {
HandlerCompleted = callback_func;
}
串口應(yīng)用
在嵌入式應(yīng)用中,串口通信是很經(jīng)典且常用的外設(shè),舉一個(gè)簡(jiǎn)單的栗子,接收的串口數(shù)據(jù)幀頭是@,幀尾是*。中間數(shù)據(jù)不可能出現(xiàn)@和*。那么一般情況下代碼如下編寫(xiě)。
/*串口中斷函數(shù)*/
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);
}
}
/*應(yīng)用層代碼,簡(jiǎn)單化->在main函數(shù)*/
void main()
{
/*省略其他代碼*/
while(1)
{
if(receive_flg == 1)//通過(guò)檢查receive_data判斷是否接收到函數(shù)
{
/*通過(guò)receive_data數(shù)組處理數(shù)據(jù)*/
receive_flg = 0;
}
}
}
這樣實(shí)現(xiàn)功能是沒(méi)有問(wèn)題的,在我接觸到很多的項(xiàng)目中的確是類(lèi)似的架構(gòu),但是它的移植性較差。
還有一種情況,那就是如果你接到需求把硬件層封裝給客戶(hù)使用,不讓客戶(hù)看到源碼,封裝成庫(kù),起到"保護(hù)通訊協(xié)議"的目的,那么你要告訴客戶(hù),需要判斷receive_flg變量,然后讀取receive_data數(shù)組的內(nèi)容???
不得不說(shuō),你這樣干是可以的,但是大部分公司不會(huì)這樣干的。這時(shí)候可以使用回調(diào)函數(shù)來(lái)解決這個(gè)問(wèn)題。
/*開(kāi)放給客戶(hù)的頭文件*/
/* Includes ------------------------------------------------------------------*/
#include <stdio.h>
typedef void (*ReceiveFarmDataFun)(uint8_t *buff,uint32_t bufferlen);
extern void CallbackRegister (CallbackFunc_t callback_func);
/*封裝的函數(shù)*/
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);
}
}
那么客戶(hù)拿到的有用信息如下:
typedef void (*ReceiveFarmDataFun)(uint8_t *buff,uint32_t bufferlen);
extern void CallbackRegister (CallbackFunc_t callback_func);
客戶(hù)可以寫(xiě)如下代碼:
void uartdatadeal(uint8_t *buff,uint32_t bufferlen)
{
/*buff指針存儲(chǔ)了串口數(shù)據(jù),bufferlen存儲(chǔ)數(shù)據(jù)長(zhǎng)度*/
/*客戶(hù)的應(yīng)用層代碼*/
}
void main()
{
/*省略其他代碼*/
CallbackRegister (uartdatadeal);
while(1)
{
}
}
這樣的話,就可以解決上述問(wèn)題,客戶(hù)只要注冊(cè)一下串口接收的函數(shù),當(dāng)接收到有效數(shù)據(jù)后,就可以跳轉(zhuǎn)到用戶(hù)的代碼,而你可以將自己的硬件層封裝起來(lái)。
看到這里可能有嵌入式大佬意識(shí)到某些問(wèn)題了,這樣寫(xiě)代碼,數(shù)據(jù)處理的函數(shù)就等于在中斷里了,這是不合理的啊。
是的,是有這個(gè)問(wèn)題,所以給客戶(hù)的庫(kù)文件必須說(shuō)明這一點(diǎn),讓客戶(hù)自行選擇,客戶(hù)不想在中斷中執(zhí)行,可以再按照我們一開(kāi)始的邏輯寫(xiě)啊,如下:
void uartdatadeal(uint8_t *buff,uint32_t bufferlen)
{
/*buff指針存儲(chǔ)了串口數(shù)據(jù),bufferlen存儲(chǔ)數(shù)據(jù)長(zhǎng)度*/
receive_flg = 1;
}
void main()
{
/*省略其他代碼*/
CallbackRegister (uartdatadeal);
while(1)
{
if(receive_flg == 1)
{
/*處理數(shù)據(jù)*/
receive_flg = 0;
}
}
}
事實(shí)上,芯片/模塊廠家寫(xiě)SDK經(jīng)常這樣做,一些大型的開(kāi)源庫(kù)也會(huì)這樣用,典型的如lwip庫(kù)。
后記
讀到這里的同學(xué)可能覺(jué)得這完全是“脫褲子放屁”啊,這屬于“炫技”啊,沒(méi)什么用啊。誠(chéng)然在很多應(yīng)用中,特別是一些單片機(jī)項(xiàng)目中,代碼量不大,使用類(lèi)似receive_flg全局變量控制,代碼結(jié)構(gòu)也清晰啊。
并且項(xiàng)目不需封裝庫(kù)給客戶(hù),一個(gè)單片機(jī)軟件開(kāi)發(fā)工程師可以吃透整個(gè)項(xiàng)目的代碼,根本不需要這樣的“騷操作”。
關(guān)于回調(diào)函數(shù),我的態(tài)度是:回調(diào)函數(shù)可以使我們的代碼更高效且更易于維護(hù),降低耦合。明智地使用它們很重要,否則過(guò)度使用回調(diào)(函數(shù)指針)會(huì)使代碼難以進(jìn)行排查和調(diào)試。