一、什么是回调函数
回调函数(Callback Function)就是一个通过函数指针传递的函数。调用方不直接调用目标函数,而是将目标函数的地址(即函数指针)作为参数传递给另一个函数(回调注册函数),由该函数在合适的时机"回调"执行传入的函数。
通俗理解:
你打电话咨询问题,客服说"我稍后给您回电",然后留了你的电话号码。客服处理完后,主动打回你留下的号码——这就是回调。电话号码就是"回调地址",回拨这个动作就是"回调"。
核心概念
| 概念 | 说明 |
|---|---|
| 回调函数 | 被传入、被调用的目标函数 |
| 回调注册 | 将函数指针传递给回调管理函数的过程 |
| 触发时机 | 由回调管理函数在内部特定条件满足时自动调用 |
| 函数指针 | 存储函数入口地址的指针变量,是回调的载体 |
二、回调函数的作用
回调函数在嵌入式开发和软件架构中有着广泛用途,核心作用如下:
1. 事件驱动与解耦
回调函数实现了调用方与被调用方的解耦。调用方不需要知道谁实现了功能,只需要在合适时机触发回调即可;实现方也不需要知道谁在调用,只需要注册函数即可。
典型场景:按键事件、定时器超时、串口接收完成、ADC转换完成等。
2. 代码复用与扩展性
通过回调机制,通用的框架代码可以处理通用流程,而将具体业务逻辑交给回调函数。新增功能只需注册新的回调,无需修改框架代码——符合开闭原则(对扩展开放,对修改封闭)。
3. 异步处理
在异步操作中,调用方发起请求后可立即执行其他任务,当操作完成时系统自动回调注册的函数通知结果,避免阻塞等待。
4. 状态机与流程控制
在复杂的状态机中,不同状态下执行不同的回调函数,使状态切换逻辑清晰、易于维护。
三、C 语言中的函数指针基础
回调函数的底层载体是函数指针。先回顾函数指针的声明与使用:
// 函数指针的声明 // 格式:返回类型 (*指针名)(参数列表) int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int main(void) { // 声明函数指针 int (*op)(int, int); op = add; // 指向 add 函数 printf("2+3=%d\n", op(2, 3)); // 输出 5 op = sub; // 指向 sub 函数 printf("10-3=%d\n", op(10, 3)); // 输出 7 return 0; }使用 typedef 简化函数指针声明
// 定义函数指针类型别名 typedef int (*MathOp)(int, int); MathOp op = add; // 更清晰的写法四、STM32 平台回调函数实战
4.1 场景一:GPIO 按键中断回调
在 STM32 中,外部中断(EXTI)触发后需要执行具体业务逻辑。通过回调函数机制,可以将不同按键的处理逻辑解耦。
step1:定义回调函数类型
// key_callback.h #ifndef KEY_CALLBACK_H #define KEY_CALLBACK_H // 定义按键回调函数类型 typedef void (*KeyCallback_t)(uint16_t key_id); #endifstep2:按键驱动层(框架代码)
// key_driver.c #include "stm32f4xx.h" #include "key_callback.h" #define MAX_KEYS 4 static KeyCallback_t key_callbacks[MAX_KEYS] = {NULL}; // 注册回调函数 void Key_RegisterCallback(uint16_t key_id, KeyCallback_t callback) { if (key_id < MAX_KEYS) { key_callbacks[key_id] = callback; } } // EXTI 中断服务函数(以 GPIO0 为例) void EXTI0_IRQHandler(void) { if (EXTI->PR & EXTI_PR_PR0) { // 清除中断标志 EXTI->PR = EXTI_PR_PR0; // 消抖 HAL_Delay(20); if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_SET) { // 回调注册的处理函数 if (key_callbacks[0] != NULL) { key_callbacks[0](0); // 回调:传入按键ID } } } } // 按键扫描主循环 void Key_Scan(void) { for (uint8_t i = 0; i < MAX_KEYS; i++) { if (key_callbacks[i] != NULL) { uint16_t key_id = Read_Key_Hardware(i); if (key_id != 0xFFFF) { key_callbacks[i](key_id); // 触发回调 } } } }step3:应用层(业务逻辑)
// main.c #include "key_callback.h" // 按键1回调:LED翻转 void Key1_Callback(uint16_t key_id) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5); printf("Key1 pressed, LED toggled\r\n"); } // 按键2回调:串口发送数据 void Key2_Callback(uint16_t key_id) { uint8_t msg[] = "Key2 triggered!\r\n"; HAL_UART_Transmit(&huart1, msg, sizeof(msg) - 1, 100); printf("Key2 pressed, message sent\r\n"); } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); // 注册回调函数 Key_RegisterCallback(0, Key1_Callback); Key_RegisterCallback(1, Key2_Callback); printf("System initialized, callbacks registered.\r\n"); while (1) { Key_Scan(); // 主循环中扫描按键 // 其他业务逻辑... } }4.2 场景二:ADC 转换完成回调(HAL 库风格)
STM32 HAL 库内部已经使用了回调机制。当 ADC 转换完成时,HAL 库会自动调用用户需要重写的回调函数。
// 用户只需重写此函数,HAL库在ADC转换完成后自动调用 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { uint32_t adc_value = HAL_ADC_GetValue(hadc); float voltage = (adc_value * 3.3f) / 4096.0f; char buf[32]; sprintf(buf, "ADC Voltage: %.2f V\r\n", voltage); HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100); } }HAL 库内部的调用流程:
ADC 硬件转换完成 -> 产生中断 -> ADC_IRQHandler() [HAL库源码] -> HAL_ADC_IRQHandler() [HAL库源码] -> ... 数据处理 ... -> HAL_ADC_ConvCpltCallback() [用户实现,自动被调用]用户不需要在中断服务函数中写任何代码,只需重写回调函数即可。
4.3 场景三:通用任务/定时器回调
实现一个通用的定时器框架,支持多个定时器各自注册不同的回调函数:
// timer_manager.c #include "stm32f4xx.h" #include <stdbool.h> #include <string.h> #define MAX_TIMERS 8 // 定时器回调函数类型 typedef void (*TimerCallback_t)(void *param); // 定时器控制块 typedef struct { uint32_t period_ms; // 周期(ms) uint32_t elapsed_ms; // 已流逝时间 bool enabled; // 使能标志 bool one_shot; // 单次触发标志 TimerCallback_t callback; // 回调函数指针 void *param; // 回调参数 } Timer_t; static Timer_t timers[MAX_TIMERS]; static uint32_t timer_tick_ms = 0; // 系统滴答定时器中断(1ms 一次) void SysTick_Handler(void) { HAL_IncTick(); timer_tick_ms++; } // 创建定时器 uint8_t Timer_Create(TimerCallback_t callback, uint32_t period_ms, bool one_shot, void *param) { for (uint8_t i = 0; i < MAX_TIMERS; i++) { if (timers[i].callback == NULL) { timers[i].callback = callback; timers[i].period_ms = period_ms; timers[i].enabled = true; timers[i].one_shot = one_shot; timers[i].param = param; timers[i].elapsed_ms = 0; return i; // 返回定时器ID } } return 0xFF; // 无可用槽位 } // 删除定时器 void Timer_Delete(uint8_t id) { if (id < MAX_TIMERS) { timers[id].callback = NULL; timers[id].enabled = false; } } // 使能/禁用定时器 void Timer_Enable(uint8_t id) { if (id < MAX_TIMERS) timers[id].enabled = true; } void Timer_Disable(uint8_t id) { if (id < MAX_TIMERS) timers[id].enabled = false; } // 主循环中调用,检测并触发超时 void Timer_Update(void) { for (uint8_t i = 0; i < MAX_TIMERS; i++) { if (timers[i].callback != NULL && timers[i].enabled) { if (timer_tick_ms - timers[i].elapsed_ms >= timers[i].period_ms) { timers[i].elapsed_ms = timer_tick_ms; timers[i].callback(timers[i].param); // 触发回调 if (timers[i].one_shot) { timers[i].callback = NULL; // 单次触发,禁用 } } } } } // ========== 应用层示例 ========== // 回调函数1:LED闪烁 void LED_Blink_Callback(void *param) { uint16_t *pin = (uint16_t *)param; HAL_GPIO_TogglePin(GPIOB, *pin); } // 回调函数2:串口定时上报 void UART_Report_Callback(void *param) { char buf[64]; sprintf(buf, "System Tick: %lu\r\n", (unsigned long)timer_tick_ms); HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100); } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); uint16_t led_pin = GPIO_PIN_5; // 创建定时器:LED每500ms翻转 uint8_t led_timer = Timer_Create(LED_Blink_Callback, 500, false, &led_pin); // 创建定时器:串口每1000ms上报 uint8_t uart_timer = Timer_Create(UART_Report_Callback, 1000, false, NULL); while (1) { Timer_Update(); // 在main循环中更新定时器 // 其他业务... } }4.4 场景四:消息队列/事件分发回调
实现一个简易的事件分发系统,不同事件触发不同回调:
// event_dispatcher.h #ifndef EVENT_DISPATCHER_H #define EVENT_DISPATCHER_H #define MAX_EVENTS 16 typedef void (*EventCallback_t)(uint32_t event_id, void *data, uint16_t len); typedef struct { uint32_t event_id; EventCallback_t callback; } EventReg_t; // 注册事件回调 void Event_Register(uint32_t event_id, EventCallback_t callback); // 分发事件 void Event_Dispatch(uint32_t event_id, void *data, uint16_t len); // 初始化 void Event_Init(void); #endif// event_dispatcher.c #include "event_dispatcher.h" #include <string.h> static EventReg_t events[MAX_EVENTS]; void Event_Init(void) { memset(events, 0, sizeof(events)); } void Event_Register(uint32_t event_id, EventCallback_t callback) { for (uint8_t i = 0; i < MAX_EVENTS; i++) { if (events[i].callback == NULL) { events[i].event_id = event_id; events[i].callback = callback; return; } } } void Event_Dispatch(uint32_t event_id, void *data, uint16_t len) { for (uint8_t i = 0; i < MAX_EVENTS; i++) { if (events[i].callback != NULL && events[i].event_id == event_id) { events[i].callback(event_id, data, len); } } }// 应用层 #include "event_dispatcher.h" #define EVT_BUTTON_PRESS 0x01 #define EVT_TIMER_TIMEOUT 0x02 #define EVT_UART_RECEIVE 0x03 void Button_Event_Handler(uint32_t event_id, void *data, uint16_t len) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5); printf("Button event handled\r\n"); } void Timer_Event_Handler(uint32_t event_id, void *data, uint16_t len) { printf("Timer event handled\r\n"); } int main(void) { Event_Init(); // ... 初始化硬件 ... // 注册事件回调 Event_Register(EVT_BUTTON_PRESS, Button_Event_Handler); Event_Register(EVT_TIMER_TIMEOUT, Timer_Event_Handler); while (1) { // 当按键按下时,分发事件 if (Is_Button_Pressed()) { Event_Dispatch(EVT_BUTTON_PRESS, NULL, 0); } // 当串口收到数据时 if (HAL_UART_GetState(&huart1) & HAL_UART_STATE_RXRDY) { uint8_t rx_data[10]; uint16_t rx_len = HAL_UART_Receive(&huart1, rx_data, 10, 100); Event_Dispatch(EVT_UART_RECEIVE, rx_data, rx_len); } } }五、回调函数 vs 直接函数调用对比
| 对比维度 | 直接调用 | 回调函数 |
|---|---|---|
| 耦合度 | 高(调用方必须知道被调函数) | 低(通过指针间接调用) |
| 扩展性 | 差(新增功能需改源码) | 好(新增回调即可) |
| 可读性 | 直观 | 需理解调用链 |
| 调试难度 | 低 | 较高(间接调用) |
| 适用场景 | 简单、固定的流程 | 事件驱动、框架/插件架构 |
六、回调函数的注意事项
1. 空指针检查
调用回调前务必检查函数指针是否为 NULL,否则会导致 HardFault:
if (callback != NULL) { callback(param); }2. 中断中的回调
在中断服务函数中调用回调时:
- 回调函数应尽量简短,避免耗时操作
- 不要在中断回调中调用阻塞式 API(如
HAL_Delay、printf) - 复杂处理应通过设置标志位,在主循环中处理
3. 重入问题
如果回调函数可能被中断再次进入,需注意保护共享资源:
void Timer_Callback(void *param) { __disable_irq(); // 关全局中断 shared_var++; __enable_irq(); // 开全局中断 // 执行其他非阻塞操作... }4. 回调函数签名一致性
所有注册到同一类型的回调函数,参数类型和返回类型必须一致,否则会导致未定义行为。
5. 避免回调链过深
多层回调嵌套会导致调用栈溢出,应扁平化设计,避免"回调地狱"。
七、总结
回调函数是嵌入式开发中最重要的设计模式之一,其核心价值在于解耦和扩展性。在 STM32 平台中:
- HAL 库内部大量使用回调(ADC、UART、SPI、定时器、EXTI 等)
- FreeRTOS/RT-Thread等 RTOS 的任务通知、信号量也常配合回调使用
- 应用层开发中,按键、定时器、事件分发等通用框架都依赖回调机制
掌握回调函数的使用,是编写高质量、可维护的嵌入式代码的关键一步。