当前位置: 首页 > news >正文

嵌入式软件开发--回调函数

文章目录

  • 一、回调
    • 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、回调函数的核心作用

  1. 异步事件处理:中断、外设状态变化等异步事件发生时,通过回调函数快速响应(避免轮询浪费资源)。
  2. 模块解耦底层驱动无需知道上层业务逻辑,只需调用注册的回调函数(如UART驱动收到数据后,调用应用层的处理函数)
  3. 代码复用:同一驱动可通过注册不同回调函数,适配不同业务场景(如同一定时器可分别用于采样和报警)。

1.2、回调函数的使用场景

  • 中断服务程序(ISR)中:中断发生后,在ISR中调用回调函数处理具体业务(ISR只做简单判断,复杂逻辑放回调)。
  • 外设驱动中:如UART接收完成、SPI传输结束、ADC转换完成时触发回调。
  • 定时器中:定时时间到达后,调用回调函数执行周期性任务(如数据采集)。
  • 状态机中:状态切换时触发回调(如设备从“待机”到“运行”时通知应用层)。

1.3、回调函数的实现步骤

  1. 定义回调函数类型:用typedef声明函数指针类型,明确参数和返回值。
  2. 注册回调函数:提供注册接口,将应用层的函数地址保存到底层驱动的全局变量中。
  3. 触发回调函数:在事件发生时(如中断、超时),底层驱动调用保存的函数地址。

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、代码说明

  1. 回调类型定义UART_RxCallback_t 是一个函数指针类型,规定了回调函数必须接收uint8_t *(数据)和uint16_t(长度)参数。

  2. 注册机制UART_RegisterRxCallback 函数将应用层实现的OnUARTDataReceived函数地址保存到驱动层的全局变量g_rxCallback中,完成“底层”与“上层”的绑定。

  3. 触发时机:当UART收到数据并检测到结束符\n时,中断服务函数USART1_IRQHandler会调用g_rxCallback,即应用层的OnUARTDataReceived函数,实现“数据到达后自动处理”。

  4. 解耦优势:UART驱动层(uart_driver.c)无需知道应用层如何处理数据,只需负责“接收数据并触发回调”;应用层(app.c)只需实现处理逻辑,无需关心UART硬件细节。

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

  1. 避免耗时操作:若回调函数在中断中触发(如示例中的ISR),不可包含延时、打印(printf可能阻塞)等耗时操作,否则会影响中断响应速度。

  2. 线程安全:多任务环境下(如RTOS),需通过互斥锁保护回调函数中访问的全局变量,避免数据竞争。

  3. 空指针检查:调用回调函数前必须判断是否为NULL(如if (g_rxCallback != NULL)),否则可能导致程序崩溃。

  4. 参数有效性:回调函数中需检查输入参数(如缓冲区地址、长度)的有效性,避免越界访问。

通过回调函数,嵌入式软件可以实现模块化设计,让底层驱动更通用,上层业务更灵活,尤其适合处理复杂的异步事件场景。

二、回调函数–代码复用

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个场景:

  1. 周期性采集温湿度(每100ms一次);
  2. 按键长按检测(持续按下2秒后触发);
  3. 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) {// 主循环空闲}
}

复用逻辑分析

  1. 驱动层复用timer_driver.c 仅实现定时器的基础功能(定时、触发回调),与业务无关,可在所有需要“定时触发”的场景中直接使用,无需修改一行代码。
  2. 应用层灵活适配
    • 想采集温湿度?注册 TempHumi_CollectCallback 即可;
    • 想检测按键长按?注册 Key_LongPressCallback 即可;
    • 想让LED闪烁?注册 Led_BlinkCallback 即可。
  3. 扩展成本低:若新增“定时上报数据到云端”的场景,只需新增一个 Cloud_UploadCallback 函数并注册,驱动层依然无需改动。

核心优势

  • 减少重复开发:避免为每个场景编写一套独立的定时器驱动(如temp_timer.ckey_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) {// 其他业务逻辑...}
}
http://www.dtcms.com/a/333513.html

相关文章:

  • 大肠杆菌重组蛋白表达致命痛点:包涵体 / 低表达 / 可溶性差?高效解决方案全解析!
  • JVM核心原理与实战优化指南
  • c++程序示例:多线程下的实例计数器
  • Nginx反向代理与缓存实现
  • 企业级Java项目和大模型结合场景(智能客服系统:电商、金融、政务、企业)
  • 正确维护邵氏硬度计的使用寿命至关重要
  • 【办公类110-01】20250813 园园通新生分班(python+uibot)
  • 量化线性层(42)
  • JavaScript 逻辑运算符与实战案例:从原理到落地
  • JavaScript 中 call、apply 和 bind 方法的区别与使用
  • 技术解读 | 搭建NL2SQL系统需要大模型么?
  • 【Git】Git-fork开发模式
  • 从0开始学习Java+AI知识点总结-15.后端web基础(Maven基础)
  • ARM Cortex-M7 Thread Mode与Handler Mode
  • Android ViewPager2+Fragment viewModelScope问题
  • 在 Vue2 中使用 pdf.js + pdf-lib 实现 PDF 预览、手写签名、文字批注与高保真导出
  • Java零基础笔记18(Java编程核心:Java网络编程—数据通信方案)
  • leetcode 刷题1
  • SysGetVariableString函数
  • 【python实用小脚本-187】Python一键批量改PDF文字:拖进来秒出新文件——再也不用Acrobat来回导
  • 详解 k 近邻(KNN)算法:原理、实践与调优 —— 以鸢尾花分类为例
  • JUC LongAdder并发计数器设计
  • 指针操作:从到*的深度指南
  • JavaWeb开发_Day13
  • Cortex-Debug和openocd之间的关系?如何协同工作?
  • 《人形机器人的觉醒:技术革命与碳基未来》——触觉反馈系统:电子皮肤的概念、种类、原理及在机器中的应用
  • 攻防世界—fakebook(两种方法)
  • docker重启或系统重启后harbor自动启动
  • 深入理解C++正则表达式:从基础到实践
  • ReasonRank:从关键词匹配到逻辑推理,排序准确性大幅超越传统方法