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

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

上图表示用户应用程序代码和硬件驱动程序之间的交互。硬件驱动程序是一个独立的可重用驱动程序,它不了解上面的层(在本例中为用户应用程序)。硬件驱动程序提供 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.

END

来源:嵌入式应用研究院

版权归原作者所有,如有侵权,请联系删除。

推荐阅读
国产MCU厂商,一网打尽!
外资芯片大厂又裁员:研发一个不留!
国产FPGA开发板上手体验:不足百元,冲击高端

→点关注,不迷路←

嵌入式ARM 关注这个时代最火的嵌入式ARM,你想知道的都在这里。
评论
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 134浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 79浏览
  • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
    Industio_触觉智能 2025-01-06 10:43 87浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 85浏览
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 132浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
    GIRtina 2025-01-06 11:10 106浏览
  • PLC组态方式主要有三种,每种都有其独特的特点和适用场景。下面来简单说说: 1. 硬件组态   定义:硬件组态指的是选择适合的PLC型号、I/O模块、通信模块等硬件组件,并按照实际需求进行连接和配置。    灵活性:这种方式允许用户根据项目需求自由搭配硬件组件,具有较高的灵活性。    成本:可能需要额外的硬件购买成本,适用于对系统性能和扩展性有较高要求的场合。 2. 软件组态   定义:软件组态主要是通过PLC
    丙丁先生 2025-01-06 09:23 85浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 85浏览
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 178浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 57浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 149浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦