一、什么是回调函数
回调函数(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);
#endif
step2:按键驱动层(框架代码)
// 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 的任务通知、信号量也常配合回调使用
- 应用层开发中,按键、定时器、事件分发等通用框架都依赖回调机制
掌握回调函数的使用,是编写高质量、可维护的嵌入式代码的关键一步。