嵌入式软件开发--回调函数
文章目录
- 一、回调
- 1.1、回调函数的核心作用
- 1.2、回调函数的使用场景
- 1.3、回调函数的实现步骤
- 1.4、示例代码(以STM32 UART接收为例)
- 1.5、代码说明
- 1.6、使用回调函数的注意事项
- 二、回调函数--代码复用
- 场景说明
- 代码实现
- 1. 通用定时器驱动(`timer_driver.c`)
- 2. 复用驱动实现3个场景(应用层)
- 复用逻辑分析
- 核心优势
- 三、回调函数--串口通信使用实例
在嵌入式软件开发中,回调函数是一种非常重要的编程机制,尤其适合用于处理异步事件(如中断、定时器超时、数据接收等)。它通过将函数作为参数传递给其他函数,实现“事件发生时自动执行指定操作”的效果,能有效解耦模块间的依赖关系。
使用总结如下
//定义回调类型
typedef void (*UART_Callback)(void);
//定义回调变量
static UART_Callback g_callback=NULL;
//定义注册函数
void Regist_Callback(UART_Callback callback)
{g_callback = callback;
}
//中断函数内部
void Interupt()
{g_callback();
}//定义实际处理函数
void Led_shift()
{}//注册函数
void main_virtual()
{Regist_Callback(Led_shift);
}
一、回调
1.1、回调函数的核心作用
- 异步事件处理:中断、外设状态变化等异步事件发生时,通过回调函数快速响应(避免轮询浪费资源)。
- 模块解耦:
底层驱动无需知道上层业务逻辑,只需调用注册的回调函数(如UART驱动收到数据后,调用应用层的处理函数)
。 - 代码复用:同一驱动可通过注册不同回调函数,适配不同业务场景(如同一定时器可分别用于采样和报警)。
1.2、回调函数的使用场景
- 中断服务程序(ISR)中:中断发生后,在ISR中调用回调函数处理具体业务(ISR只做简单判断,复杂逻辑放回调)。
- 外设驱动中:如UART接收完成、SPI传输结束、ADC转换完成时触发回调。
- 定时器中:定时时间到达后,调用回调函数执行周期性任务(如数据采集)。
- 状态机中:状态切换时触发回调(如设备从“待机”到“运行”时通知应用层)。
1.3、回调函数的实现步骤
- 定义回调函数类型:用
typedef
声明函数指针类型,明确参数和返回值。 - 注册回调函数:提供注册接口,将应用层的函数地址保存到底层驱动的全局变量中。
- 触发回调函数:在事件发生时(如中断、超时),底层驱动调用保存的函数地址。
1.4、示例代码(以STM32 UART接收为例)
以下示例展示如何在UART驱动中使用回调函数,实现“收到数据后自动通知应用层处理”。
app.c
#include "uart_driver.h"
#include <stdio.h>// 4. 应用层实现回调函数:处理接收到的数据
void OnUARTDataReceived(uint8_t *data, uint16_t len) {// 业务逻辑:例如打印接收的数据printf("收到数据:");for (uint16_t i = 0; i < len; i++) {printf("%c", data[i]);}printf("\r\n");// 其他处理:如解析命令、控制设备等
}// 应用层初始化
void App_Init() {// 初始化UART(波特率115200)UART_Init(115200);// 注册回调函数:将应用层的OnUARTDataReceived与UART驱动绑定UART_RegisterRxCallback(OnUARTDataReceived);
}int main() {// 系统初始化HAL_Init();App_Init();// 主循环(其他业务逻辑)while (1) {// 执行其他任务...}
}
uart_driver.c
#include "uart_driver.h"
#include "stm32f1xx_hal.h" // 假设使用STM32 HAL库// 全局变量:保存注册的回调函数(初始为NULL)
static UART_RxCallback_t g_rxCallback = NULL;// UART句柄(具体硬件相关)
UART_HandleTypeDef huart1;// 2. 实现注册接口:将应用层的回调函数地址保存到全局变量
void UART_RegisterRxCallback(UART_RxCallback_t callback) {g_rxCallback = callback;
}// UART初始化:配置硬件参数(波特率、数据位等)
void UART_Init(uint32_t baudrate) {huart1.Instance = USART1;huart1.Init.BaudRate = baudrate;huart1.Init.WordLength = UART_WORDLENGTH_8B;huart1.Init.StopBits = UART_STOPBITS_1;huart1.Init.Parity = UART_PARITY_NONE;huart1.Init.Mode = UART_MODE_TX_RX;HAL_UART_Init(&huart1); // 初始化硬件// 使能接收中断(数据到来时触发)HAL_UART_Receive_IT(&huart1, NULL, 0);
}// 3. 中断服务函数中触发回调(硬件中断发生时调用)
void USART1_IRQHandler(void) {static uint8_t rx_buf[128]; // 接收缓冲区static uint16_t rx_len = 0;uint8_t data;// 判断是否为接收中断if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET) {data = (uint8_t)(huart1.Instance->DR & 0x00FF); // 读取接收数据// 简单帧协议:以'\n'作为结束符if (data == '\n') {// 若已注册回调函数,则调用(将数据传递给应用层)if (g_rxCallback != NULL) {g_rxCallback(rx_buf, rx_len); // 触发回调}rx_len = 0; // 重置缓冲区} else {rx_buf[rx_len++] = data; // 数据存入缓冲区}__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_RXNE); // 清除中断标志}
}
uart_driver.h
#ifndef UART_DRIVER_H
#define UART_DRIVER_H#include <stdint.h>// 1. 定义回调函数类型:参数为接收缓冲区和长度,无返回值
typedef void (*UART_RxCallback_t)(uint8_t *data, uint16_t len);// 2. 注册回调函数的接口
void UART_RegisterRxCallback(UART_RxCallback_t callback);// UART初始化函数(配置波特率等)
void UART_Init(uint32_t baudrate);#endif
1.5、代码说明
-
回调类型定义:
UART_RxCallback_t
是一个函数指针类型,规定了回调函数必须接收uint8_t *
(数据)和uint16_t
(长度)参数。 -
注册机制:
UART_RegisterRxCallback
函数将应用层实现的OnUARTDataReceived
函数地址保存到驱动层的全局变量g_rxCallback
中,完成“底层”与“上层”的绑定。 -
触发时机:当UART收到数据并检测到结束符
\n
时,中断服务函数USART1_IRQHandler
会调用g_rxCallback
,即应用层的OnUARTDataReceived
函数,实现“数据到达后自动处理”。 -
解耦优势:UART驱动层(
uart_driver.c
)无需知道应用层如何处理数据,只需负责“接收数据并触发回调”;应用层(app.c
)只需实现处理逻辑,无需关心UART硬件细节。
1.6、使用回调函数的注意事项
-
避免耗时操作:若回调函数在中断中触发(如示例中的ISR),不可包含延时、打印(
printf
可能阻塞)等耗时操作,否则会影响中断响应速度。 -
线程安全:多任务环境下(如RTOS),需通过互斥锁保护回调函数中访问的全局变量,避免数据竞争。
-
空指针检查:调用回调函数前必须判断是否为
NULL
(如if (g_rxCallback != NULL)
),否则可能导致程序崩溃。 -
参数有效性:回调函数中需检查输入参数(如缓冲区地址、长度)的有效性,避免越界访问。
通过回调函数,嵌入式软件可以实现模块化设计,让底层驱动更通用,上层业务更灵活,尤其适合处理复杂的异步事件场景。
二、回调函数–代码复用
1、定义回调函数类型:
typedef void (*Timer_Callback_t)(void);`
2、定义回调函数类型变量:
`static Timer_Callback_t g_timerCallback = NULL;`
3、定义注册回调函数,供应用层绑定业务逻辑
void Timer_RegisterCallback(Timer_Callback_t callback) {g_timerCallback = callback;
}
4、将②中的回调函数类型变量放入中断中,发生中断就调用这个函数变量
void TIM2_IRQHandler(void) {if (/* 定时器溢出标志置位 */) {// 清除中断标志// 若注册了回调函数,则触发if (g_timerCallback != NULL) {g_timerCallback(); // 调用应用层注册的函数}}
}
5、编写与回调函数类型一致的对应类型的实际处理函数
// --------------------------
// 场景3:每500ms切换LED状态
// --------------------------static uint8_t ledState = 0;void Led_BlinkCallback(void) {ledState = !ledState; // 翻转状态Led_SetState(ledState); // 控制LED亮/灭
}
6、程序运行初始化时将⑤中函数注册到回调函数里:
Timer_RegisterCallback(Led_BlinkCallback);
7、后续想更改其他功能,只需要重新注册别的函数即可
// app.c
#include "timer_driver.h"
#include "sensor.h" // 温湿度传感器
#include "key.h" // 按键驱动
#include "led.h" // LED驱动功能一
Timer_RegisterCallback(TempHumi_CollectCallback);
功能二
Timer_RegisterCallback(Key_LongPressCallback);// --------------------------
// 场景1:每100ms采集温湿度
// --------------------------
void TempHumi_CollectCallback(void) {float temp, humi;Sensor_Read(&temp, &humi); // 读取传感器printf("温度:%.1f℃,湿度:%.1f%%\n", temp, humi);
}// --------------------------
// 场景2:按键长按2秒检测
// --------------------------
static uint32_t keyPressCount = 0; // 记录按键按下的周期数void Key_LongPressCallback(void) {if (Key_IsPressed()) { // 检测按键是否仍按下keyPressCount++;if (keyPressCount >= 20) { // 20 * 100ms = 2000msprintf("按键长按触发!\n");keyPressCount = 0; // 重置计数}} else {keyPressCount = 0; // 按键释放,重置计数}
}
···············································································
···············································································
···············································································
使用步骤
// 定义回调函数类型:定时结束后调用,无参数无返回值
typedef void (*Timer_Callback_t)(void);// 全局变量:保存注册的回调函数
static Timer_Callback_t g_timerCallback = NULL;// 注册回调函数:供应用层绑定业务逻辑
void Timer_RegisterCallback(Timer_Callback_t callback) {g_timerCallback = callback;
}
回调函数的代码复用体现在:同一套底层驱动代码,通过注册不同的回调函数,可适配完全不同的业务场景,无需修改驱动本身。以下以“定时器驱动”为例,展示如何通过回调函数实现代码复用。
场景说明
假设有一个通用定时器驱动(timer_driver.c
),功能是“定时N毫秒后触发事件”。通过注册不同的回调函数,该驱动可被复用在3个场景:
- 周期性采集温湿度(每100ms一次);
- 按键长按检测(持续按下2秒后触发);
- LED闪烁(每500ms切换一次状态)。
代码实现
1. 通用定时器驱动(timer_driver.c
)
驱动层仅负责“定时”和“触发回调”,不包含任何业务逻辑,可被所有场景复用。
// timer_driver.h
#include <stdint.h>// 定义回调函数类型:定时结束后调用,无参数无返回值
typedef void (*Timer_Callback_t)(void);// 全局变量:保存注册的回调函数
static Timer_Callback_t g_timerCallback = NULL;// 注册回调函数:供应用层绑定业务逻辑
void Timer_RegisterCallback(Timer_Callback_t callback) {g_timerCallback = callback;
}// 初始化定时器:设置定时周期(ms)
void Timer_Init(uint32_t period_ms) {// 硬件配置(以STM32定时器为例):// 1. 使能定时器时钟、配置分频和自动重装载值,实现定时period_ms// 2. 使能定时器更新中断
}// 定时器中断服务函数(硬件触发后调用)
void TIM2_IRQHandler(void) {if (/* 定时器溢出标志置位 */) {// 清除中断标志// 若注册了回调函数,则触发if (g_timerCallback != NULL) {g_timerCallback(); // 调用应用层注册的函数}}
}
2. 复用驱动实现3个场景(应用层)
无需修改驱动代码,仅通过注册不同回调函数,实现3种业务逻辑。
// app.c
#include "timer_driver.h"
#include "sensor.h" // 温湿度传感器
#include "key.h" // 按键驱动
#include "led.h" // LED驱动// --------------------------
// 场景1:每100ms采集温湿度
// --------------------------
void TempHumi_CollectCallback(void) {float temp, humi;Sensor_Read(&temp, &humi); // 读取传感器printf("温度:%.1f℃,湿度:%.1f%%\n", temp, humi);
}// --------------------------
// 场景2:按键长按2秒检测
// --------------------------
static uint32_t keyPressCount = 0; // 记录按键按下的周期数void Key_LongPressCallback(void) {if (Key_IsPressed()) { // 检测按键是否仍按下keyPressCount++;if (keyPressCount >= 20) { // 20 * 100ms = 2000msprintf("按键长按触发!\n");keyPressCount = 0; // 重置计数}} else {keyPressCount = 0; // 按键释放,重置计数}
}// --------------------------
// 场景3:每500ms切换LED状态
// --------------------------
static uint8_t ledState = 0;void Led_BlinkCallback(void) {ledState = !ledState; // 翻转状态Led_SetState(ledState); // 控制LED亮/灭
}// 初始化:根据需求注册不同回调
int main() {// 初始化定时器(通用驱动,无需修改)Timer_Init(100); // 定时周期100ms// 场景1:注册温湿度采集回调// Timer_RegisterCallback(TempHumi_CollectCallback);// 场景2:注册按键长按检测回调// Timer_RegisterCallback(Key_LongPressCallback);// 场景3:注册LED闪烁回调Timer_RegisterCallback(Led_BlinkCallback);while (1) {// 主循环空闲}
}
复用逻辑分析
- 驱动层复用:
timer_driver.c
仅实现定时器的基础功能(定时、触发回调),与业务无关,可在所有需要“定时触发”的场景中直接使用,无需修改一行代码。 - 应用层灵活适配:
- 想采集温湿度?注册
TempHumi_CollectCallback
即可; - 想检测按键长按?注册
Key_LongPressCallback
即可; - 想让LED闪烁?注册
Led_BlinkCallback
即可。
- 想采集温湿度?注册
- 扩展成本低:若新增“定时上报数据到云端”的场景,只需新增一个
Cloud_UploadCallback
函数并注册,驱动层依然无需改动。
核心优势
- 减少重复开发:避免为每个场景编写一套独立的定时器驱动(如
temp_timer.c
、key_timer.c
)。 - 降低维护成本:若定时器硬件需要升级(如更换芯片),只需修改
timer_driver.c
一次,所有依赖它的场景自动适配。 - 模块解耦:驱动层与应用层通过回调函数松耦合,驱动开发者和应用开发者可并行工作(驱动开发者无需知道业务,应用开发者无需关心硬件)。
这种复用方式在嵌入式开发中极为常见,例如RTOS的任务调度、外设的中断处理等,均通过回调函数实现“一套核心逻辑,多场景适配”。
三、回调函数–串口通信使用实例
uart_driver.h
#ifndef UART_DRIVER_H
#define UART_DRIVER_H#include <stdint.h>
#include <stdbool.h>// 定义串口接收回调函数类型
// 参数:data-接收的数据缓冲区,len-数据长度,is_complete-是否接收完成(如遇到结束符)
typedef void (*UART_RxCallback_t)(const uint8_t *data, uint16_t len, bool is_complete);// 定义串口发送完成回调函数类型
typedef void (*UART_TxCallback_t)(void);// 串口初始化函数
void UART_Init(uint32_t baud_rate);// 注册接收回调函数
void UART_RegisterRxCallback(UART_RxCallback_t callback);// 注册发送完成回调函数
void UART_RegisterTxCallback(UART_TxCallback_t callback);// 串口发送函数
void UART_SendData(const uint8_t *data, uint16_t len);#endif
uart_driver.c
#include "uart_driver.h"
#include "stm32f1xx_hal.h" // 以STM32 HAL库为例// 硬件句柄
UART_HandleTypeDef huart1;// 接收缓冲区
#define RX_BUF_SIZE 128
static uint8_t rx_buf[RX_BUF_SIZE];
static uint16_t rx_len = 0;// 回调函数指针
static UART_RxCallback_t rx_callback = NULL;
static UART_TxCallback_t tx_callback = NULL;// 串口初始化(配置硬件参数)
void UART_Init(uint32_t baud_rate) {huart1.Instance = USART1;huart1.Init.BaudRate = baud_rate;huart1.Init.WordLength = UART_WORDLENGTH_8B;huart1.Init.StopBits = UART_STOPBITS_1;huart1.Init.Parity = UART_PARITY_NONE;huart1.Init.Mode = UART_MODE_TX_RX;huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;HAL_UART_Init(&huart1);// 使能接收中断(接收到1个字节就触发中断)HAL_UART_Receive_IT(&huart1, &rx_buf[rx_len], 1);
}// 注册接收回调函数
void UART_RegisterRxCallback(UART_RxCallback_t callback) {rx_callback = callback;
}// 注册发送完成回调函数
void UART_RegisterTxCallback(UART_TxCallback_t callback) {tx_callback = callback;
}// 串口发送数据
void UART_SendData(const uint8_t *data, uint16_t len) {HAL_UART_Transmit_IT(&huart1, data, len); // 非阻塞发送
}// HAL库接收完成回调(每收到1个字节触发)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {if (huart == &huart1) {rx_len++;// 判断是否接收完成(这里以换行符'\n'作为结束标志)bool is_complete = (rx_buf[rx_len - 1] == '\n');// 若未接收完成且缓冲区未满,继续接收下一个字节if (!is_complete && rx_len < RX_BUF_SIZE) {HAL_UART_Receive_IT(&huart1, &rx_buf[rx_len], 1);}// 触发应用层回调(无论是否完成,都通知应用层)if (rx_callback != NULL) {rx_callback(rx_buf, rx_len, is_complete);}// 若接收完成或缓冲区满,重置接收状态if (is_complete || rx_len >= RX_BUF_SIZE) {rx_len = 0;HAL_UART_Receive_IT(&huart1, &rx_buf[rx_len], 1); // 重新开始接收}}
}// HAL库发送完成回调
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {if (huart == &huart1 && tx_callback != NULL) {tx_callback(); // 通知应用层发送完成}
}// 串口中断服务程序(由硬件自动调用)
void USART1_IRQHandler(void) {HAL_UART_IRQHandler(&huart1); // 交给HAL库处理
}
app.c
#include "uart_driver.h"
#include <stdio.h>
#include <string.h>// 接收回调函数:处理接收到的串口数据
void OnUARTReceived(const uint8_t *data, uint16_t len, bool is_complete) {if (is_complete) {// 接收完成(收到换行符),解析命令if (strstr((char*)data, "GET_TEMP") != NULL) {// 若收到"GET_TEMP"命令,返回温度数据uint8_t resp[] = "TEMP: 25.5C\n";UART_SendData(resp, sizeof(resp)-1);} else if (strstr((char*)data, "GET_HUMI") != NULL) {// 若收到"GET_HUMI"命令,返回湿度数据uint8_t resp[] = "HUMI: 60%\n";UART_SendData(resp, sizeof(resp)-1);} else {// 未知命令uint8_t resp[] = "UNKNOWN CMD\n";UART_SendData(resp, sizeof(resp)-1);}} else {// 接收未完成(可用于实时显示输入过程)printf("正在接收:%.*s\r", len, data);}
}// 发送完成回调函数:通知发送状态
void OnUARTSent(void) {printf("数据发送完成\n");
}int main(void) {// 初始化硬件HAL_Init();// 初始化串口(波特率115200)UART_Init(115200);// 注册回调函数UART_RegisterRxCallback(OnUARTReceived);UART_RegisterTxCallback(OnUARTSent);// 主循环while (1) {// 其他业务逻辑...}
}