回调函数详解 —— 以 STM32 平台为例

一、什么是回调函数

回调函数(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 的任务通知、信号量也常配合回调使用
  • 应用层开发中,按键、定时器、事件分发等通用框架都依赖回调机制

掌握回调函数的使用,是编写高质量、可维护的嵌入式代码的关键一步。

相关推荐
ccbw161 小时前
CH592 蓝牙 MCU 集成方案与低功耗设计要点
单片机·嵌入式硬件
I'm a winner2 小时前
《AI 赋能嵌入式开发:从 0 到全栈工程师》模块1|第3课时
人工智能·嵌入式硬件
数字新视界2 小时前
2026机房动环监控选型指南:场景规模需求、市场发展占有率和竞争梯队报告解析
大数据·运维·网络·人工智能·嵌入式硬件·机房动环监控·机房环境安全
程序员_小兵2 小时前
PCIe Switch Fabric 技术解析
服务器·c语言·嵌入式硬件·php·fabric
一直C2 小时前
【嵌入式 ARM 驱动开发】PWM 脉宽调制原理与 GT9147 电容触控屏驱动开发详解
嵌入式硬件·lcd·嵌入式·arm汇编·imx6ull·cortex-a7
尼喃2 小时前
USB接口保护怎么设计?OVP芯片、TVS、ESD器件选型指南
嵌入式硬件
LCG元3 小时前
STM32F103 I2C 驱动 DS3231 高精度 RTC 实战:BCD 码解码陷阱、闹钟状态机整定与日误差实测
stm32·嵌入式硬件·实时音视频
cu1433 小时前
细谈GM8775C的具体功能和应用
c语言·c++·人工智能·嵌入式硬件
漫漫长远3 小时前
低功耗UPF介绍(一)
笔记·嵌入式硬件·学习·fpga开发