? ? 在我們平時開發STM32或者其它單片機時,我們經常都會用到原廠提供的固件庫函數,固件庫函數中有非常多回調函數。那么什么是回調函數呢?回調函數是作為參數傳遞給另一個函數的函數。接受回調作為參數的函數預計會在某個時間點執行它。回調機制允許下層軟件層調用上層軟件層定義的函數。
????上圖表示用戶應用程序代碼和硬件驅動程序之間的交互。硬件驅動程序是一個獨立的可重用驅動程序,它不了解上面的層(在本例中為用戶應用程序)。硬件驅動程序提供 API 函數,允許用戶應用程序將函數注冊為回調。然后,此回調函數由硬件驅動程序作為執行的一部分進行調用。如果不使用回調,就會被編碼為直接調用。這將使硬件驅動程序特定于特定的高級軟件級別,并降低其可重用性。回調機制的另一個好處是,在程序執行期間可以動態更改被調用的回調函數。
????不同的編程語言有不同的實現回調的方式。在本文中,我們將重點介紹C編程語言,因為它是用于嵌入式軟件開發的最流行的語言。C語言中的回調是使用函數指針實現的。函數指針就像普通指針一樣,但它不是指向變量的地址,而是指向函數的地址。在程序運行期間,可以設置相同的函數指針指向不同的函數。在下面的代碼中,我們可以看到如何使用函數指針將函數作為參數傳遞給函數。該函數將函數指針和兩個整數值作為參數和。將執行的算術運算取決于將傳遞給函數指針參數的函數。
uint16_t?cal_sum(uint8_t?a,?uint8_t?b)?{
????return?a?+?b;
}
uint16_t?cal_mul(uint8_t?a,?uint8_t?b)?{
????return?a?*?b;
}
uint16_t?cal_op?(uint16_t?(*callback_func)(uint8_t,?uint8_t),uint8_t?a,?uint8_t?b)?{???
????return?callback_func(a,b);
}
void?main()?{
????cal_op(cal_mul,4,10);?
????cal_op(cal_sum,9,5);?
}
????回調可用于多種情況,并廣泛用于嵌入式固件開發。它們提供了更大的代碼靈活性,并允許我們開發可由最終用戶進行微調而無需更改代碼的驅動程序。
????在我們的代碼中具有回調功能所需的元素是:
????接下來介紹使用回調函數的簡單流程。首先聲明一個函數指針,用于訪問回調函數我們可以簡單地將函數指針聲明為:
uint8_t?(*p_CallbackFunc)(void);
????但是對于更清晰的代碼,最好定義一個函數指針類型:
typedef?uint8_t?(*CallbackFunc_t)?(void);?
????定義回調函數——重要的是要注意回調函數只是一個函數。由于它的使用方式(通過函數指針訪問),我們將其稱為回調。所以這一步只是我們之前聲明的指針將指向的函數的定義。
uint8_t?Handler_Event(void)?{
/*?code?of?the?function?*/
}
????注冊回調函數——這是為函數指針分配地址的操作。在我們的例子中,地址應該是回調函數的地址。可以有一個專門的函數來注冊回調函數,如下所示:
static?CallbackFunc_t?HandlerCompleted;
/*用來注冊回調函數的功能函數*/
void?CallbackRegister?(CallbackFunc_t?callback_func)?{
?????HandlerCompleted?=?callback_func;
}
/*?注冊Handler_Event作為回調*/
CallbackRegister(Handler_Event);
????在這個例子中,我們展示了如何使用回調來處理事件。下面的示例代碼是基于較低級別物理通信接口(例如 UART、SPI、I2C 等)構建的數據通信協議棧。通信協議棧實現了兩種不同類型的幀——標準通信幀和增強型通信幀。有兩種不同的函數用于處理接收到的字節事件。在初始化函數中,函數指針被分配了應該使用的函數的地址用于處理事件。這是注冊回調函數的操作。
/*指向回調函數的函數指針*/
uint8_t?(?*Receive_Byte)?(?void?);
/*
?*?簡化的初始化函數
?*?這里函數指針被分配了一個函數的地址(注冊回調函數)
?*/
void?Comm_Init(?uint8_t?op_mode)?{
????????switch?(?op_mode?)?{
????????case?STD_FRAME:???????????
????????????Receive_Byte?????=?StdRxFSM;
????????????break;
????????case?ENHANCED_FRAME:??
????????????Receive_Byte?????=?EnhancedRxFSM;
????????????break;
????????default:
????????????Receive_Byte?????=?EnhancedRxFSM;
????????}
}
/*?這些是在通信棧中實現的函數(回調)
*?它們不會在任何地方直接調用,而是使用函數指針來訪問它們?*/
uint8_t??StdRxFSM(void)?{
????//在這里完成處理工作
}
uint8_t??EnhancedRxFSM(void)?{
????//在這里完成處理工作
}
????當從物理通信接口(例如 UART)接收到新字節(事件)時,用戶應用程序代碼會調用我們示例中的回調函數。
extern?uint8_t?(*Receive_Byte)(?void?);
void?receive_new_byte()?
{
???Receive_Byte();?
}
????這個例子展示了我們如何創建一個寄存器來存儲回調函數。它是使用數據類型元素的數組實現的。數據類型是具有成員和成員的結構。用于為寄存器中的每個回調函數分配一個標識(唯一編號)。函數指針被分配與唯一關聯的回調函數的地址。以下實現的是添加和刪除回調的功能:
#define?FUNC_REGISTER_SIZE?255
#define?FUNC_ID_MAX?127
//函數指針類型
typedef??uint8_t?(*callback_func)?(?uint8_t?*?p_data,?uint16_t?len?);
typedef?struct?
{
????uint8_t???????????function_id;
????callback_func?p_callback_func;
}?function_register_t;
//一組函數處理程序,每個處理程序都有一個id
static?function_register_t?func_register[FUNC_REGISTER_SIZE];
//注冊函數回調
uint8_t?RegisterCallback?(uint8_t?function_id,?callback_func?p_callback_func?)?{
????uint8_t????status;
????if?((0?????{
????????//向寄存器添加函數
????????if?(?p_callback_func?!=?NULL?)?{?
????????????for?(int?i?=?0;?i?????????????????if?((?func_register[i].p_callback_func?==?NULL?)?||
????????????????????(?func_register[i].p_callback_func?==?p_callback_func?))?{
????????????????????func_register[i].function_id?=?function_id;
????????????????????func_register[i].p_callback_func?=?p_callback_func;
????????????????????break;
????????????????}
????????????}
?????if?(i?!=?FUNC_REGISTER_SIZE)?{
????????status?=?SUCESSFULL;
?????}
?????else?{
????????status?=?FAILURE;
?????}
????????}
????????else?{?
????????????//從寄存器中刪除
????????????for?(?i?=?0;?i?????????????????if?(?func_register[i].function_id?==?function_id?)?{
????????????????????func_register[i].function_id?=?0;
????????????????????func_register[i].p_callback_func?=?NULL;
????????????????????break;
????????????????}
????????????}
????????????status?=?SUCESSFULL;
????????}
????}
????else?{
????????status?=?FAILURE;?/*?Invalid?argument?*/
????}
????return?status;
}
????在下面的代碼中,我們可以看到一個函數示例,該函數可用于根據函數 id 調用回調。
//具有特定函數代碼的回調函數如何被調用的示例
uint8_t?execute_callback(uint8_t?FuncCode,?uint8_t?*?p_data_buf,?uint16_t?len)?
{??
????uint8_t?status;
????status?=?FAILURE;
????for(?i?=?0;?i?????????/*?No?more?callbacks?registered,?exit.?*/
????????if(?func_register[i].function_id?==?0?){
????????????break;
????????}
????????else?if(?func_register[i].function_id?==?FuncCode)?{
????????????status?=?func_register[i].p_callback_func(?p_data_buf,?len?);
????????????break;
????????}
?????}
?????return?status;
}
????我們可以編寫不使用回調的程序,但是通過將它們添加到我們的工具庫中,它們可以使我們的代碼更高效且更易于維護。明智地使用它們很重要,否則過度使用回調(函數指針)會使代碼難以進行排查和調試。另一件需要考慮的事情是使用函數指針可能會阻止編譯器執行的一些優化(例如函數內聯)。
5、文獻引用
本文來源網絡,免費傳達知識,版權歸原作者所有。如涉及作品版權問題,請聯系我進行刪除。
由于微信公眾號近期改變了推送規則,為了防止找不到,可以星標置頂,這樣每次推送的文章才會出現在您的訂閱列表里。
猜你喜歡:
柔性數組在實際項目中的應用?
干貨 | protobuf-c之嵌入式平臺使用
C語言、嵌入式重點知識:回調函數
實用 | 10分鐘教你搭建一個嵌入式web服務器
在公眾號聊天界面回復1024,可獲取嵌入式資源;回復?m?,可查看文章匯總