回调函数的注册机制为什么会在嵌入式固件开发中应用如此广泛?

简介: 回调函数的注册机制为什么会在嵌入式固件开发中应用如此广泛?

   在我们平时开发STM32或者其它单片机时,我们经常都会用到原厂提供的固件库函数,固件库函数中有非常多回调函数。那么什么是回调函数呢?回调函数是作为参数传递给另一个函数的函数。接受回调作为参数的函数预计会在某个时间点执行它。回调机制允许下层软件层调用上层软件层定义的函数。

640.jpg

   上图表示用户应用程序代码和硬件驱动程序之间的交互。硬件驱动程序是一个独立的可重用驱动程序,它不了解上面的层(在本例中为用户应用程序)。硬件驱动程序提供 API 函数,允许用户应用程序将函数注册为回调。然后,此回调函数由硬件驱动程序作为执行的一部分进行调用。如果不使用回调,就会被编码为直接调用。这将使硬件驱动程序特定于特定的高级软件级别,并降低其可重用性。回调机制的另一个好处是,在程序执行期间可以动态更改被调用的回调函数。

1、C语言中的回调

   不同的编程语言有不同的实现回调的方式。在本文中,我们将重点介绍C编程语言,因为它是用于嵌入式软件开发的最流行的语言。C语言中的回调是使用函数指针实现的。函数指针就像普通指针一样,但它不是指向变量的地址,而是指向函数的地址。在程序运行期间,可以设置相同的函数指针指向不同的函数。下面的代码中,我们可以看到如何使用函数指针将函数作为参数传递给函数。该函数将函数指针和两个整数值作为参数和。将执行的算术运算取决于将传递给函数指针参数的函数。

uint16_t cal_sum(uint8_t a, uint8_t b) {
    return a + b;
}
uint16_t cal_mul(uint8_t a, uint8_t b) {
    return a * b;
}
uint16_t cal_op (uint16_t (*callback_func)(uint8_t, uint8_t),uint8_t a, uint8_t b) {   
    return callback_func(a,b);
}
void main() {
    cal_op(cal_mul,4,10); 
    cal_op(cal_sum,9,5); 
}

2、回调的实际使用

   回调可用于多种情况,并广泛用于嵌入式固件开发。它们提供了更大的代码灵活性,并允许我们开发可由最终用户进行微调而无需更改代码的驱动程序。


   在我们的代码中具有回调功能所需的元素是:


  • 将被调用的函数(回调函数)
  • 将用于访问回调函数的函数指针
  • 将调用回调函数的函数("调用函数")


   接下来介绍使用回调函数的简单流程。首先声明一个函数指针,用于访问回调函数我们可以简单地将函数指针声明为:

uint8_t (*p_CallbackFunc)(void);

   但是对于更清晰的代码,最好定义一个函数指针类型:

typedef uint8_t (*CallbackFunc_t) (void);

   定义回调函数——重要的是要注意回调函数只是一个函数。由于它的使用方式(通过函数指针访问),我们将其称为回调。所以这一步只是我们之前声明的指针将指向的函数的定义。

uint8_t Handler_Event(void) {
/* code of the function */
}

   注册回调函数——这是为函数指针分配地址的操作。在我们的例子中,地址应该是回调函数的地址。可以有一个专门的函数来注册回调函数,如下所示:

static CallbackFunc_t HandlerCompleted;
/*用来注册回调函数的功能函数*/
void CallbackRegister (CallbackFunc_t callback_func) {
     HandlerCompleted = callback_func;
}
/* 注册Handler_Event作为回调*/
CallbackRegister(Handler_Event);

3、代码应用案例

3.1、事件回调

   在这个例子中,我们展示了如何使用回调来处理事件。下面的示例代码是基于较低级别物理通信接口(例如 UART、SPI、I2C 等)构建的数据通信协议栈。通信协议栈实现了两种不同类型的帧——标准通信帧和增强型通信帧。有两种不同的函数用于处理接收到的字节事件。在初始化函数中,函数指针被分配了应该使用的函数的地址用于处理事件。这是注册回调函数的操作。

/*指向回调函数的函数指针*/
uint8_t ( *Receive_Byte) ( void );
/*
 * 简化的初始化函数
 * 这里函数指针被分配了一个函数的地址(注册回调函数)
 */
void Comm_Init( uint8_t op_mode) {
        switch ( op_mode ) {
        case STD_FRAME:           
            Receive_Byte     = StdRxFSM;
            break;
        case ENHANCED_FRAME:  
            Receive_Byte     = EnhancedRxFSM;
            break;
        default:
            Receive_Byte     = EnhancedRxFSM;
        }
}
/* 这些是在通信栈中实现的函数(回调)
* 它们不会在任何地方直接调用,而是使用函数指针来访问它们 */
uint8_t  StdRxFSM(void) {
    //在这里完成处理工作
}
uint8_t  EnhancedRxFSM(void) {
    //在这里完成处理工作
}

   当从物理通信接口(例如 UART)接收到新字节(事件)时,用户应用程序代码会调用我们示例中的回调函数。

extern uint8_t (*Receive_Byte)( void );
void receive_new_byte() 
{
   Receive_Byte(); 
}

3.2、寄存器中的多个回调

   这个例子展示了我们如何创建一个寄存器来存储回调函数。它是使用数据类型元素的数组实现的。数据类型是具有成员和成员的结构。用于为寄存器中的每个回调函数分配一个标识(唯一编号)。函数指针被分配与唯一关联的回调函数的地址。以下实现的是添加和删除回调的功能:

#define FUNC_REGISTER_SIZE 255
#define FUNC_ID_MAX 127
//函数指针类型
typedef  uint8_t (*callback_func) ( uint8_t * p_data, uint16_t len );
typedef struct 
{
    uint8_t           function_id;
    callback_func p_callback_func;
} function_register_t;
//一组函数处理程序,每个处理程序都有一个id
static function_register_t func_register[FUNC_REGISTER_SIZE];
//注册函数回调
uint8_t RegisterCallback (uint8_t function_id, callback_func p_callback_func ) {
    uint8_t    status;
    if ((0 < function_id) && (function_id <= FUNC_ID_MAX)) 
    {
        //向寄存器添加函数
        if ( p_callback_func != NULL ) { 
            for (int i = 0; i < FUNC_REGISTER_SIZE; i++ ) {
                if (( func_register[i].p_callback_func == NULL ) ||
                    ( func_register[i].p_callback_func == p_callback_func )) {
                    func_register[i].function_id = function_id;
                    func_register[i].p_callback_func = p_callback_func;
                    break;
                }
            }
     if (i != FUNC_REGISTER_SIZE) {
        status = SUCESSFULL;
     }
     else {
        status = FAILURE;
     }
        }
        else { 
            //从寄存器中删除
            for ( i = 0; i < FUNC_REGISTER_SIZE; i++ ) {
                if ( func_register[i].function_id == function_id ) {
                    func_register[i].function_id = 0;
                    func_register[i].p_callback_func = NULL;
                    break;
                }
            }
            status = SUCESSFULL;
        }
    }
    else {
        status = FAILURE; /* Invalid argument */
    }
    return status;
}

   在下面的代码中,我们可以看到一个函数示例,该函数可用于根据函数 id 调用回调。

//具有特定函数代码的回调函数如何被调用的示例
uint8_t execute_callback(uint8_t FuncCode, uint8_t * p_data_buf, uint16_t len) 
{  
    uint8_t status;
    status = FAILURE;
    for( i = 0; i < FUNC_REGISTER_SIZE; i++ ){
        /* No more callbacks registered, exit. */
        if( func_register[i].function_id == 0 ){
            break;
        }
        else if( func_register[i].function_id == FuncCode) {
            status = func_register[i].p_callback_func( p_data_buf, len );
            break;
        }
     }
     return status;
}

4、结论

   我们可以编写不使用回调的程序,但是通过将它们添加到我们的工具库中,它们可以使我们的代码更高效且更易于维护。明智地使用它们很重要,否则过度使用回调(函数指针)会使代码难以进行排查和调试。另一件需要考虑的事情是使用函数指针可能会阻止编译器执行的一些优化(例如函数内联)。

5、文献引用

   [1]王铬. 回调函数在软件设计中的应用[J]. 河南教育学院学报:自然科学版, 2003, 12(3):3.    [2]李建波, 陈榕福, & 王劲. (2020). Stm32cube mx串口中断回调函数的研究. 电子世界(5), 2.

往期精彩

分享GitHub上一些嵌入式相关的高星开源项目


开源:AliOS_Things_Developer_Kit开发板复活计划


手把手教你在STM32上实现OLED视频播放(很简单也很硬很肝!)


一些值得被定义为常用C语言头文件库的漂亮宏定义(值得收藏,以备使用参考)

目录
相关文章
|
5天前
|
开发工具 C语言 git
【嵌入式开源库】MultiTimer 的使用,一款可无限扩展的软件定时器
【嵌入式开源库】MultiTimer 的使用,一款可无限扩展的软件定时器
|
5天前
|
JSON 算法 应用服务中间件
嵌入式设备OTA升级的大致过程!
嵌入式设备OTA升级的大致过程!
40 0
|
5天前
|
Linux 网络安全 开发工具
嵌入式中利用VS Code 远程开发原理
嵌入式中利用VS Code 远程开发原理
33 0
|
5天前
|
监控 安全 API
7.2 Windows驱动开发:内核注册并监控对象回调
在笔者上一篇文章`《内核枚举进程与线程ObCall回调》`简单介绍了如何枚举系统中已经存在的`进程与线程`回调,本章`LyShark`将通过对象回调实现对进程线程的`句柄`监控,在内核中提供了`ObRegisterCallbacks`回调,使用这个内核`回调`函数,可注册一个`对象`回调,不过目前该函数`只能`监控进程与线程句柄操作,通过监控进程或线程句柄,可实现保护指定进程线程不被终止的目的。
34 0
7.2 Windows驱动开发:内核注册并监控对象回调
|
5天前
嵌入式中利用软件实现定时器的两种方法分析
嵌入式中利用软件实现定时器的两种方法分析
42 0
|
5月前
|
监控 安全 API
7.6 Windows驱动开发:内核监控FileObject文件回调
本篇文章与上一篇文章`《内核注册并监控对象回调》`所使用的方式是一样的都是使用`ObRegisterCallbacks`注册回调事件,只不过上一篇博文中`LyShark`将回调结构体`OB_OPERATION_REGISTRATION`中的`ObjectType`填充为了`PsProcessType`和`PsThreadType`格式从而实现监控进程与线程,本章我们需要将该结构填充为`IoFileObjectType`以此来实现对文件的监控,文件过滤驱动不仅仅可以用来监控文件的打开,还可以用它实现对文件的保护,一旦驱动加载则文件是不可被删除和改动的。
32 1
7.6 Windows驱动开发:内核监控FileObject文件回调
|
11月前
|
缓存 API C语言
|
存储 人工智能 JSON
HarmonyOS系统中内核实现智慧烟感控制的方法
大家好,今天主要和大家聊一聊,如何利用鸿蒙系统实现智慧烟感方法
161 0
HarmonyOS系统中内核实现智慧烟感控制的方法
|
网络协议 Android开发 数据安全/隐私保护
HarmonyOS系统中内核实现MQTT协议开发的方法
大家好,今天主要来聊一聊,如何使用鸿蒙开始实现MQTT协议开发的方法
274 1
HarmonyOS系统中内核实现MQTT协议开发的方法
|
存储 JSON 物联网
HarmonyOS系统中内核实现智慧物流控制的方法
大家好,今天主要和大家聊一聊,如何使用鸿蒙系统实现智能物流的开发.
177 0
HarmonyOS系统中内核实现智慧物流控制的方法