一般情况下,我们调用已经存在的函数叫做调用(call)
那么,已经存在的函数调用我们自定义的函数,就叫做回调(call back)

回调函数:嵌入式系统中的核心机制

一、基本概念与定义

回调函数是一种将函数作为参数传递给另一个函数,并在特定事件触发时由后者调用的编程机制。其本质是函数指针的使用

  • 调用(Call):我们调用已存在的函数
  • 回调(Call Back):已存在的函数调用我们自定义的函数

核心特点:将控制流程与具体实现分离,使程序能够响应外部事件或条件。

二、工作原理

回调函数的工作流程可概括为三个步骤:

  1. 定义:用户定义符合特定接口规范的函数
  2. 注册:将该函数的指针注册到需要回调的模块中
  3. 触发:当特定事件发生时,模块通过调用函数指针执行用户定义的逻辑

关键技术机制:

// 函数指针类型定义
typedef void (*CallbackFunc)(void);

// 回调注册
void register_callback(CallbackFunc func) {
    g_callback = func;  // 存储函数指针
}

// 事件触发执行
void event_trigger(void) {
    if (g_callback != NULL) {
        g_callback();  // 执行回调
    }
}

三、回调函数 vs 普通函数

特性 回调函数 普通函数
调用方式 通过函数指针隐式传递,由外部触发 由开发者显式调用,如func()
执行时机 由外部事件(中断、系统信号)驱动,无法直接预测 由开发者控制,执行时机确定
执行环境 可能在中断上下文中执行,需遵循"快进快出"原则 通常在主程序上下文中执行,可执行复杂操作
资源访问 需使用volatile关键字保护共享变量 无需特殊处理,可直接访问变量
设计目的 解耦调用者与被调用者,支持动态行为替换和异步响应 实现固定逻辑,执行特定功能

四、回调函数在嵌入式系统中的核心应用场景

1. 硬件中断处理(最典型应用)

实现方式

  • 通过bsp_StartHardTimer等函数注册回调
  • 硬件定时器配置为在特定事件(如计数值达到)时触发中断
  • 中断服务程序(ISR)中调用注册的回调函数

示例

void bsp_StartHardTimer(uint8_t _CC, uint32_t _uiTimeOut, void * _pCallBack) {
    // ...配置定时器...
    s_TIM_CallbackX = (void (*)(void))_pCallBack; // 注册回调
    TIMx->DIER |= TIM_IT_CCX; // 使能中断
}

void TIM_HARD_IRQHandler(void) {
    // ...检查中断标志...
    if (itstatus != RESET) {
        TIMx->DIER &= ~TIM_IT_CCX; // 禁用中断
        s_TIM_CallbackX(); // 调用回调函数
    }
}

优势

  • 解决轮询方式效率低下的问题
  • 实现微秒级精度的定时任务管理
  • 避免阻塞主程序执行

2. 事件驱动编程

实现方式

  • 通过中断或定时器触发事件
  • 事件发生时执行相应的回调函数

示例(按键处理):

// 注册回调
void key_register_callback(KeyCallback callback) {
    key_callback = callback;
}

// 中断服务程序
void EXTI0_IRQHandler(void) {
    if (key_callback != NULL) {
        key_callback(); // 调用注册的回调
    }
}

优势

  • 代码结构清晰,主循环简洁
  • 逻辑解耦,易于维护和扩展
  • 支持多事件处理,互不干扰

3. 资源监控与动态行为替换

实现方式

  • 在关键资源访问点注册回调
  • 在回调中执行监控逻辑
  • 通过函数指针实现运行时行为替换

优势

  • 提高代码灵活性和可维护性
  • 支持不同场景下动态调整处理逻辑

五、实际应用示例

1. 外部中断处理(按键/按钮)

传统写法(问题:代码混乱,难以维护)
// main.c
void main() {
    // 初始化按键
    GPIO_Init(KEY_PORT, KEY_PIN, INPUT_PULLUP);
    
    while(1) {
        if (GPIO_Read(KEY_PIN) == 0) {
            // 按键按下处理逻辑
            LED_Toggle();
            // 等待消抖
            delay_ms(20);
            // 按键松开处理
            while(GPIO_Read(KEY_PIN) == 0);
        }
        // 其他功能...
    }
}
回调函数写法(优势:解耦,逻辑清晰)
// key.h
typedef void (*KeyCallback)(void);  // 定义回调函数类型

void key_register_callback(KeyCallback callback);  // 注册回调函数

// key.c
static KeyCallback key_callback = NULL;

void key_init(void) {
    // 初始化按键硬件
    // ...
}

void key_register_callback(KeyCallback callback) {
    key_callback = callback;
}

// 中断服务程序(在ISR中)
void EXTI0_IRQHandler(void) {
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        // 消抖处理(实际应用中可能有更复杂的消抖)
        delay_ms(20);
        if (key_callback != NULL) {
            key_callback();  // 调用注册的回调函数
        }
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

// main.c
void key_pressed(void) {
    LED_Toggle();
    printf("按键被按下!\n");
}

int main() {
    key_init();
    key_register_callback(key_pressed);  // 注册回调
    
    while(1) {
        // 主循环中只需处理其他任务
        // 无需处理按键逻辑
    }
}

为什么用回调?

  • 按键处理逻辑被移到了key_pressed函数中,主循环变得干净
  • 如果需要改变按键功能,只需修改key_pressed函数,无需改动主循环
  • 多个按键可以注册不同的回调函数,互不干扰

2. 串口数据接收(避免轮询)

传统写法(问题:占用CPU,效率低)
// main.c
void main() {
    uart_init();
    
    while(1) {
        if (uart_data_available()) {
            uint8_t data = uart_read();
            // 处理数据
            process_data(data);
        }
        // 其他任务...
    }
}
回调函数写法(优势:高效,不阻塞主程序)
// uart.h
typedef void (*UartCallback)(uint8_t data);  // 数据接收回调

void uart_register_callback(UartCallback callback);  // 注册回调

// uart.c
static UartCallback uart_callback = NULL;

void uart_init(void) {
    // 初始化串口
    // ...
}

void uart_register_callback(UartCallback callback) {
    uart_callback = callback;
}

// 串口接收中断服务程序
void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        uint8_t data = USART_ReceiveData(USART1);
        if (uart_callback != NULL) {
            uart_callback(data);  // 调用回调函数处理数据
        }
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

// main.c
void process_uart_data(uint8_t data) {
    printf("Received: %c\n", data);
    // 处理数据的逻辑
}

int main() {
    uart_init();
    uart_register_callback(process_uart_data);
    
    while(1) {
        // 主循环中可以处理其他任务
        // 不再需要轮询串口
    }
}

为什么用回调?

  • 串口数据接收完全由中断驱动,主循环可以专注于其他任务
  • 不需要在主循环中轮询串口状态,节省CPU资源
  • 串口数据处理逻辑与串口初始化解耦,更易于维护

3. ADC数据采集(多通道)

传统写法(问题:代码冗长,难以扩展)
// main.c
void main() {
    adc_init();
    
    while(1) {
        uint16_t temp = adc_read(TEMP_CHANNEL);
        uint16_t light = adc_read(LIGHT_CHANNEL);
        
        // 处理温度数据
        if (temp > TEMP_THRESHOLD) {
            // 触发警报
        }
        
        // 处理光照数据
        if (light < LIGHT_THRESHOLD) {
            // 打开灯光
        }
        
        // 其他任务...
    }
}
回调函数写法(优势:可扩展性好)
// adc.h
typedef void (*AdcCallback)(uint8_t channel, uint16_t value);

void adc_register_callback(uint8_t channel, AdcCallback callback);

// adc.c
#define MAX_CHANNELS 4
static AdcCallback adc_callbacks[MAX_CHANNELS] = {NULL};

void adc_init(void) {
    // 初始化ADC
    // ...
}

void adc_register_callback(uint8_t channel, AdcCallback callback) {
    if (channel < MAX_CHANNELS) {
        adc_callbacks[channel] = callback;
    }
}

// ADC中断服务程序
void ADC_IRQHandler(void) {
    if (ADC_GetITStatus(ADC1, ADC_IT_EOC) != RESET) {
        uint16_t value = ADC_GetConversionValue(ADC1);
        uint8_t channel = ADC_GetCurrentConversionChannel(ADC1);
        
        if (adc_callbacks[channel] != NULL) {
            adc_callbacks[channel](channel, value);  // 调用对应通道的回调
        }
        
        ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);
    }
}

// main.c
void handle_temp(uint8_t channel, uint16_t value) {
    if (value > TEMP_THRESHOLD) {
        // 触发警报
    }
}

void handle_light(uint8_t channel, uint16_t value) {
    if (value < LIGHT_THRESHOLD) {
        // 打开灯光
    }
}

int main() {
    adc_init();
    adc_register_callback(TEMP_CHANNEL, handle_temp);
    adc_register_callback(LIGHT_CHANNEL, handle_light);
    
    while(1) {
        // 主循环中可以处理其他任务
        // 不需要关心ADC数据处理
    }
}

为什么用回调?

  • 每个ADC通道可以注册不同的处理逻辑
  • 添加新的ADC通道处理逻辑时,只需添加一个回调函数
  • 主循环保持简洁,专注于系统整体逻辑

4. 定时器事件处理(比软件定时器更精确)

传统软件定时器(问题:精度有限,占用CPU)
// main.c
void main() {
    timer_init();
    
    while(1) {
        if (timer_check(1)) {
            // 1秒事件
        }
        if (timer_check(10)) {
            // 10秒事件
        }
        // 其他任务...
    }
}
回调函数实现硬件定时器(优势:高精度,高效)
// timer.h
typedef void (*TimerCallback)(void);

void timer_start(uint32_t ms, TimerCallback callback);

// timer.c
static TimerCallback timer_callback = NULL;
static uint32_t timer_period = 0;

void timer_init(void) {
    // 初始化定时器
    // ...
}

void timer_start(uint32_t ms, TimerCallback callback) {
    timer_period = ms;
    timer_callback = callback;
    
    // 配置定时器
    // ...
}

// 定时器中断服务程序
void TIM2_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        if (timer_callback != NULL) {
            timer_callback();  // 调用回调函数
        }
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

// main.c
void one_second_event(void) {
    // 1秒事件处理
    LED_Toggle();
}

void five_second_event(void) {
    // 5秒事件处理
    printf("5秒过去了\n");
}

int main() {
    timer_init();
    timer_start(1000, one_second_event);
    // 5秒事件可以通过另一个定时器或软件计数实现
    // 但这里为了演示,我们使用同一个定时器
    // 实际应用中可以注册多个定时器
    
    while(1) {
        // 主循环
    }
}

为什么用回调?

  • 精度更高(硬件定时器比软件定时器更精确)
  • 事件处理逻辑与定时器初始化解耦
  • 一个定时器可以处理多个不同事件(通过不同回调)

5. 传感器数据就绪通知(I2C/SPI设备)

传统写法(问题:阻塞式,难以处理多个传感器)
// main.c
void main() {
    sensor_init();
    
    while(1) {
        if (sensor_data_ready()) {
            float temp = sensor_read_temp();
            float humidity = sensor_read_humidity();
            // 处理数据
        }
        // 其他任务...
    }
}
回调函数写法(优势:支持多个传感器,非阻塞)
// sensor.h
typedef void (*SensorCallback)(float value);

void sensor_register_callback(uint8_t sensor_id, SensorCallback callback);

// sensor.c
#define MAX_SENSORS 3
static SensorCallback sensor_callbacks[MAX_SENSORS] = {NULL};

void sensor_init(void) {
    // 初始化传感器
    // ...
}

void sensor_register_callback(uint8_t sensor_id, SensorCallback callback) {
    if (sensor_id < MAX_SENSORS) {
        sensor_callbacks[sensor_id] = callback;
    }
}

// I2C中断服务程序(假设传感器通过I2C通信)
void I2C1_EV_IRQHandler(void) {
    if (I2C_GetFlagStatus(I2C1, I2C_FLAG_RXNE) != RESET) {
        uint8_t data = I2C_ReceiveData(I2C1);
        // 解析数据,确定是哪个传感器
        uint8_t sensor_id = get_sensor_id();
        float value = parse_sensor_data(data);
        
        if (sensor_callbacks[sensor_id] != NULL) {
            sensor_callbacks[sensor_id](value);
        }
        
        // 清除中断标志
        I2C_ClearFlag(I2C1, I2C_FLAG_RXNE);
    }
}

// main.c
void handle_temp(float value) {
    printf("Temperature: %.2f°C\n", value);
}

void handle_humidity(float value) {
    printf("Humidity: %.2f%%\n", value);
}

int main() {
    sensor_init();
    sensor_register_callback(TEMP_SENSOR_ID, handle_temp);
    sensor_register_callback(HUMIDITY_SENSOR_ID, handle_humidity);
    
    while(1) {
        // 主循环
    }
}

为什么用回调?

  • 支持多个传感器同时工作,每个传感器有独立的处理逻辑
  • 传感器数据处理不阻塞主循环,系统响应更快
  • 传感器初始化和数据处理完全解耦

六、同步回调与异步回调的区分

在嵌入式单片机(尤其是裸机系统)中,你看到的这些例子绝大多数属于「同步回调」,但它们是在「异步事件(如中断)上下文中被调用」的。

这个看似矛盾的说法其实很准确,我们来详细拆解:


一、核心概念区分

✅ 同步回调(Synchronous Callback)
  • 定义:回调函数由调用者直接、立即、在同一线程/上下文中执行
  • 特点
    • 调用栈连续;
    • 执行完回调才返回;
    • 不涉及任务切换或延迟调度。
✅ 异步回调(Asynchronous Callback)
  • 定义:回调函数不会立即执行,而是在未来某个不确定的时间点(通常由操作系统/事件循环/硬件触发)被调用。
  • 特点
    • 调用与执行分离;
    • 通常需要事件队列、消息循环或 OS 调度支持;
    • 常见于带 RTOS、Linux 或 JavaScript 等环境。

二、分析你看到的例子

📌 例1:串口中断回调
void USART1_IRQHandler(void) {
    uint8_t data = USART_ReceiveData(USART1);
    if (uart_callback != NULL) {
        uart_callback(data);  // ← 这里直接调用!
    }
}
  • 这是同步回调uart_callback(data)直接函数调用,压栈、执行、返回,一气呵成。
  • 但它响应的是异步事件:串口数据到达是异步硬件事件(你无法预测何时来),由中断触发。
  • 执行上下文:在中断服务程序(ISR)上下文中同步执行。

✅ 结论:同步回调 + 异步触发事件


📌 例2:按键中断回调
void EXTI0_IRQHandler(void) {
    key_callback();  // ← 直接调用
}
  • 同样是在 ISR 中直接调用函数指针同步回调
  • 触发源(按键按下)是异步的人为事件

✅ 结论:同步回调 + 异步外部事件


三、为什么嵌入式裸机系统很少用“真正”的异步回调?

因为:

条件 裸机系统 RTOS/Linux
是否有任务调度器? ❌ 没有 ✅ 有
是否有事件循环? ❌ 通常没有 ✅ 有(如 select/poll/event loop)
回调能否“延迟执行”? ❌ 必须立刻执行或自己实现队列 ✅ 可放入消息队列稍后处理

所以在裸机开发中:

  • 所有回调都是“立即执行”的 → 同步回调
  • 但它们往往由异步事件(中断)触发 → 异步上下文中的同步调用

四、什么时候会出现“真正的异步回调”?

场景1:使用 RTOS(如 FreeRTOS)
// 在任务A中发起一个网络请求,并注册回调
http_get("http://example.com", on_response);

// 主任务继续运行
while(1) { ... }

// 回调函数(可能在另一个任务中被调用)
void on_response(char* data) {
    // 这个函数不是立刻执行!
    // 而是等网络数据回来后,由网络任务调用
}

→ 这才是真正的异步回调:调用和执行不在同一个时间点,甚至不在同一个任务。


场景2:自己实现事件队列(进阶裸机设计)
// 中断中不直接调回调,而是把事件放入队列
void USART1_IRQHandler(void) {
    uint8_t data = read_uart();
    event_queue_push(UART_EVENT, data);  // 只放事件,不调回调
}

// 主循环中处理事件并调用回调
while(1) {
    Event e = event_queue_pop();
    if (e.type == UART_EVENT) {
        uart_callback(e.data);  // ← 在主循环中调用
    }
}

这时候:

  • 注册回调发生在过去;
  • 回调执行发生在未来的主循环中;
  • 调用和执行解耦 → 更接近“异步回调”。

但严格来说,这仍是同步调用(因为在主循环中直接调),只是延迟执行了。


五、总结:一张表说清楚

场景 回调类型 触发方式 执行上下文 是否真正异步
裸机中断中直接调回调(你的例子) 同步回调 异步硬件事件(中断) ISR 上下文 ❌ 否
RTOS 中通过消息队列调回调 异步回调 任务间通信 接收任务上下文 ✅ 是
主循环轮询 + 函数指针调用 同步回调 同步轮询 主任务上下文 ❌ 否
自己实现事件队列后调用 同步回调(延迟执行) 队列消费 主循环上下文 ⚠️ 半异步

✅ 给你的建议

在学习阶段,不必过度纠结“同步/异步”的术语,关键是理解:

在嵌入式裸机开发中,回调函数通常是“在中断里被直接调用的函数指针”,用于将硬件事件与应用逻辑解耦。

这种模式虽然技术上是“同步调用”,但它实现了事件驱动编程的效果,这才是它的核心价值。

当你以后接触 RTOS 或 Linux 嵌入式开发时,才会遇到真正意义上的“异步回调”。

希望这个解释帮你理清了概念!如果还有疑问,欢迎继续讨论 😊

七、使用回调函数的注意事项

  1. 中断安全
    中断安全是回调函数使用的关键问题之一。在中断服务程序中执行回调函数时,需要确保回调函数不会阻塞中断或执行复杂的操作。

    • 回调函数应在中断服务程序中执行
    • 遵循"快进快出"原则,避免复杂操作
    • 在调用回调前禁用中断,防止嵌套中断
  2. 空指针检查
    函数指针的有效性验证同样重要。在实际应用中,应该添加空指针检查,避免在回调函数指针未初始化时调用导致的系统崩溃。

    if (key_callback != NULL) {
        key_callback(); // 必须检查指针有效性
    }
    
  3. 共享变量处理
    资源访问的同步问题也需要考虑。在多线程或中断环境中,回调函数可能同时访问共享资源,需要使用互斥锁或其他同步机制确保数据的一致性 。

    • 共享变量应使用volatile修饰
    • 避免在中断上下文中访问复杂数据结构
  4. 执行时间控制
    还需要注意回调函数的执行时间。在中断服务程序中执行的回调函数应该尽量简短,避免长时间占用CPU或阻塞其他中断的响应 。如果需要执行复杂操作,应该在回调函数中设置标志,然后在主程序中执行相应的逻辑。

    • 回调函数应尽量简短
    • 复杂操作应设置标志,由主循环处理

八、总结与建议

回调函数的核心价值:通过解耦调用者与被调用者,实现事件驱动编程,提高代码的灵活性、可维护性和系统效率。

在嵌入式系统中使用回调函数的建议

  1. 清晰定义接口:确保回调函数接口规范明确
  2. 添加空指针检查:避免未初始化回调导致系统崩溃
  3. 保持回调函数简洁:避免在中断上下文中执行复杂操作
  4. 考虑RTOS环境:在RTOS中,可将回调与任务调度结合,实现更灵活的异步处理
  5. 避免过度使用:并非所有场景都需要回调,选择最合适的编程模式

终极理解:回调函数不是"异步"的,而是"事件驱动"的。它通过将事件与处理逻辑解耦,使程序能够高效响应外部事件,而不是被动等待。在嵌入式系统中,这种机制是构建高效、可维护软件的关键。

生活类比:就像你去餐厅点餐,告诉服务员"菜好了叫我"(注册回调),然后你可以去做其他事情(主循环),等到菜做好了(事件触发),服务员会来叫你(执行回调)。你不用一直守在厨房门口等,效率高又省心。

九、回调函数的高级应用与优化

回调函数在嵌入式系统中还可以应用于更复杂的场景,如动态任务调度、事件队列管理和资源监控等 。

动态任务调度是回调函数的高级应用之一。通过将任务函数注册为回调函数,并设置不同的触发条件和优先级,可以实现一个灵活的任务调度系统。在安富莱定时器代码bsp_time代码中SysTick_ISR函数通过检查s.ucTimeOutFlags_uiDelayCount等变量,实现了对软件定时器的周期性检查和处理 。

void SysTick_ISR(void)
{
    static uint8_t s_count = 0;
    uint8_t i;
    if (s_uiDelayCount > 0)
    {
        if (--s_uiDelayCount == 0)
        {
            s_ucTimeOutFlag = 1;
        }
    }
    for (i = 0; i < TMR_COUNT; i++)
    {
        bsp_SoftTimerDec(&s_tTmr[i]); // 软件定时器减一操作
    }
    g_iRunTime++; // 全局运行时间每1ms增1
    bsp idling(); // 执行空闲函数
    if (++s_count >= 10)
    {
        s_count = 0;
        bsp idling10ms(); // 每隔10ms调用一次
    }
}

事件队列管理是另一个高级应用场景。通过将事件处理函数注册为回调函数,并将事件放入队列中,可以在主程序中顺序处理事件,避免中断服务程序的复杂性。在用户代码中,软件定时器的到期标志被设置为Flag字段,主程序可以通过bsp idling函数检查并处理这些标志,体现了事件队列管理的思想。

资源监控是回调函数在系统安全方面的应用。通过在关键资源访问点注册回调函数,可以在资源被访问时执行监控逻辑,防止非法访问或错误操作 。在用户代码中,g_iRunTime等全局变量被声明为__IO(即volatile),确保了在中断服务程序中访问的正确性,防止了编译器优化导致的访问错误 。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐