如何理解STM32HAL库的回调函数,一文搞懂

一起学嵌入式 2023-12-15 08:53

在标准库开发中很少遇到回调函数,但是在HAL库开发中经常要使用到回调函数,不知你是不是也有这样的疑惑,我们为什么需要回调函数这个概念呢?直接调用函数不就可以了?回调函数到底有什么作用?程序员到底该如何理解回调函数?

扫描关注一起学嵌入式,一起学习,一起成长

这篇文章就来为你解答这些问题,读完这篇文章后你的武器库将新增一件功能强大的利器

其实回调函数和普通函数没有本质的区别。首先让我们来看看普通的函数调用,假设我们在fun函数中调用函数fun1:

void fun(void ) 
{
   ...
   fun1();
   ...
}

想一想,你怎么知道可以调用func呢?哦,原来fun1是你自己定义的:

void fun1(void ) 
{
  blablabla;
}

这很简单吧,现在假设你编写的这段代码无比之牛逼,全世界的程序员都无比疯狂的想引入到自己的项目中,这时你会把A函数编写成一个库供全世界的码农使用。

但此时所有人都发现一个问题,那就是他们都想在fun函数中的某个特定点上执行一段自己的代码,作为这个库的创作者你可能会这样实现:

void fun() {
   ...   
   if (张三) 
   {
     func1();
   } else if (李四) 
   {
     func2();
   }
   ...
}

假设全世界有一千万码农,那你是不是要有一千万个if else。。。想想这样的代码就很刺激有没有!

更好的办法是什么呢?把函数也当变量!你可以这样定义fun函数:

void fun (fun1 f) {
   ...
   f();
   ...
}

任何一个码农在调用你的fun函数时传入一个函数变量,fun函数在合适的时机调用你传入的函数变量,从而节省了一千万行代码。为了让这个过程更加难懂一些,这个函数变量起了一个无比难懂的称呼:回调函数

现在你应该明白了回调函数是怎么一回事了吧,相比回调函数来说我更愿意将其看做函数变量。

什么?你说你还是不理解?那我下面这样说你明白了吗?

回调函数

一般解释:回调函数就是一个通过函数指针调用的函数。如果你把函数的地址传递给中间函数的形参,中间函数通过函数指针调用其所指向的函数时,我们就说这是回调函数。

通俗解释:函数Fun1调用函数Fun2,同时将函数Fun3作为形参传递给Fun2,此时,Fun1为应用层函数,Fun2为中间层函数,Fun3为回调函数,回调函数是一种说法而已。

fun1()
{
fun2(fun3);
}

意义何在:

  1. 利于代码结构,将代码分为应用层,中间层,硬件驱动层,彼此独立,方便程序的编辑,阅读,修改与移植

  2. 结构化编程时,结构体只需要定义中间函数,减小内存的开销

为什么不在Fun2里面直接调用Fun3

Fun1()
{
 Fun2()
 {
  Fun3_1();
  Fun3_2();
 }
}

通过函数指针的好处是,Fun2Fun3实现隔离,比如Fun3具有多个功能函数,增加或减少时,不需要修改Fun2的代码。

这会应该明白了吧?什么还是不太清楚,那下面就来详细说一说回调函数的吧!

一切要从这样的需求说起

假设你们公司要开发下一代国民App明日油条,一款主打解决国民早餐问题的App,为了加快开发进度,这款应用由A小组和B小组协同开发。

其中有一个核心模块由A小组开发然后供B小组调用,这个核心模块被封装成了一个函数,这个函数就叫make_youtiao()

如果make_youtiao()这个函数执行的很快并可以立即返回,那么B小组的同学只需要:

  1. 调用make_youtiao()
  2. 等待该函数执行完成
  3. 该函数执行完后继续后续流程

从程序执行的角度看这个过程是这样的:

  1. 保存当前被执行函数的上下文
  2. 开始执行make_youtiao()这个函数
  3. make_youtiao()执行完后,控制转回到调用函数中

如果世界上所有的函数都像make_youtiao()这么简单,那么程序员大概率就要失业了,还好程序的世界是复杂的,这样程序员才有了存在的价值。

现实并不容易

现实中make_youtiao()这个函数需要处理的数据非常庞大,假设有10000个,那么make_youtiao(10000)不会立刻返回,而是可能需要10分钟才执行完成并返回。

这时你该怎么办呢?想一想这个问题。

可能有的同学会问,和刚才一样直接调用不可以吗,这样多简单。

是的,这样做没有问题,但就像爱因斯坦说的那样“一切都应该尽可能简单,但是不能过于简单”。

想一想直接调用会有什么问题?

显然直接调用的话,那么调用线程会被阻塞暂停,在等待10分钟后才能继续运行。在这10分钟内该线程不会被操作系统分配CPU,也就是说该线程得不到任何推进。

这并不是一种高效的做法。

没有一个程序员想死盯着屏幕10分钟后才能得到结果。

那么有没有一种更加高效的做法呢?

我们知道了这种一直等待直到另一个任务完成的模式叫做同步

如果你是老板的话你会什么都不干一直盯着员工写代码吗?因此一种更好的做法是程序员在代码的时候老板该干啥干啥,程序员写完后自然会通知老板,这样老板和程序员都不需要相互等待,这种模式被称为异步

回到我们的主题,这里一种更好的方式是调用make_youtiao()这个函数后不再等待这个函数执行完成,而是直接返回继续后续流程,这样A小组的程序就可以和make_youtiao()这个函数同时进行了,就像这样:

在这种情况下,回调(callback)就必须出场了

为什么我们需要回调callback

有的同学可能还没有明白为什么在这种情况下需要回调,别着急,我们慢慢讲。假设我们明日油条App代码第一版是这样写的:

make_youtiao(10000);
sell();

可以看到这是最简单的写法,意思很简单,制作好油条后卖出去。

我们已经知道了由于make_youtiao(10000)这个函数10分钟才能返回,你不想一直死盯着屏幕10分钟等待结果,那么一种更好的方法是让make_youtiao()这个函数知道制作完油条后该干什么,即,更好的调用make_youtiao的方式是这样的:“制作10000个油条,炸好后卖出去”,因此调用make_youtiao就变出这样了:

make_youtiao(10000, sell);

看到了吧,现在make_youtiao这个函数多了一个参数,除了指定制作油条的数量外还可以指定制作好后该干什么,第二个被make_youtiao这个函数调用的函数就叫回调,callback

现在你应该看出来了吧,虽然sell函数是你定义的,但是这个函数却是被其它模块调用执行的,就像这样:

make_youtiao这个函数是怎么实现的呢,很简单:

//make_youtiao这个函数是怎么实现的呢,很简单:
void make_youtiao(int num, func call_back) 
{
    // 制作油条
    call_back(); //执行回调 
}

这样你就不用死盯着屏幕了,因为你把make_youtiao这个函数执行完后该做的任务交代给make_youtiao这个函数了,该函数制作完油条后知道该干些什么,这样就解放了你的程序。

有的同学可能还是有疑问,为什么编写make_youtiao这个小组不直接定义sell函数然后调用呢?

不要忘了明日油条这个App是由A小组和B小组同时开发的,A小组在编写make_youtiao时怎么知道B小组要怎么用这个模块,假设A小组真的自己定义sell函数就会这样写:

void make_youtiao(int num)
 
{
    real_make_youtiao(num);
    sell(); //执行回调 
}

同时A小组设计的模块非常好用,这时C小组也想用这个模块,然而C小组的需求是制作完油条后放到仓库而不是不是直接卖掉,要满足这一需求那么A小组该怎么写呢

void make_youtiao(int num) {
    real_make_youtiao(num);
    
    if (Team_B) {
       sell(); // 执行回调
    } else if (Team_D) {
       store(); // 放到仓库
    }
}

故事还没完,假设这时D小组又想使用呢,难道还要接着添加if else吗?这样的话A小组的同学只需要维护make_youtiao这个函数就能做到工作量饱满了,显然这是一种非常糟糕的设计。

所以你会看到,制作完油条后接下来该做什么不是实现make_youtiao的A小组该关心的事情,很明显只有调用make_youtiao这个函数的使用方才知道。

因此make_youtiao的A小组完全可以通过回调函数将接下来该干什么交给调用方实现,A小组的同学只需要针对回调函数这一抽象概念进行编程就好了,这样调用方在制作完油条后不管是卖掉、放到库存还是自己吃掉等等想做什么都可以,A小组的make_youtiao函数根本不用做任何改动,因为A小组是针对回调函数这一抽象概念来编程的。

以上就是回调函数的作用,当然这也是针对抽象而不是具体实现进行编程这一思想的威力所在。面向对象中的多态本质上就是让你用来针对抽象而不是针对实现来编程的

正式定义

在计算机科学中,回调函数是指一段以参数的形式传递给其它代码的可执行代码。这就是回调函数的定义了。

回调函数就是一个函数,和其它函数没有任何区别。

注意,回调函数是一种软件设计上的概念,和某个编程语言没有关系,几乎所有的编程语言都能实现回调函数。

对于一般的函数来说,我们自己编写的函数会在自己的程序内部调用,也就是说函数的编写方是我们自己,调用方也是我们自己。

但回调函数不是这样的,虽然函数编写方是我们自己,但是函数调用方不是我们,而是我们引用的其它模块,也就是第三方库,我们调用第三方库中的函数,并把回调函数传递给第三方库,第三方库中的函数调用我们编写的回调函数,如图所示:

定节点,比如我们举的例子油条制作完成、接收到网络数据、文件读取完成等之后该做什么,这些只有库的使用方才知道,因此第三方库的编写者无法针对具体的实现来写代码,而只能对外提供一个回调函数,库的使用方来实现该函数,第三方库在特定的节点调用该回调函数就可以了。

另一点值得注意的是,从图中我们可以看出回调函数和我们的主程序位于同一层中,我们只负责编写该回调函数,但并不是我们来调用的。

最后值得注意的一点就是回调函数被调用的时间节点,回调函数只在某些特定的节点被调用,就像上面说的油条制作完成、接收到网络数据、文件读取完成等,这些都是事件,也就是event,本质上我们编写的回调函数就是用来处理event的,因此从这个角度看回调函数不过就是event handler,因此回调函数天然适用于事件驱动编程event-driven

在HAL库中我们的中断服务函数里我们不需要清楚中断标志位。在标准库中,串口中断了以后,我们要先在中断中判断是否是接收中断,然后读出数据,顺便清除中断标志位,然后再是对数据的处理,这样如果我们在一个中断函数中写这么多代码,就会显得很混乱:

标准库写法

//中断服务程序
void TIM3_IRQHandler(void)
{
 if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) 
  {
  TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  
  LED1=!LED1;
  }
}

HAL库回调函数写法

//中断服务函数
void TIM3_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&TIM3_Handler);
}

//回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim==(&TIM3_Handler))
    {
        LED1=!LED1; 
    }
}

现在应该明白回调函数是什么了吧!


关注【一起学嵌入式】,回复加群进技术交流群。



觉得文章不错,点击“分享”、“”、“在看” 呗!

一起学嵌入式 公众号【一起学嵌入式】,RTOS、Linux编程、C/C++,以及经验分享、行业资讯、物联网等技术知
评论
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 59浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 54浏览
  • 国产光耦合器正以其创新性和多样性引领行业发展。凭借强大的研发能力,国内制造商推出了适应汽车、电信等领域独特需求的专业化光耦合器,为各行业的技术进步提供了重要支持。本文将重点探讨国产光耦合器的技术创新与产品多样性,以及它们在推动产业升级中的重要作用。国产光耦合器创新的作用满足现代需求的创新模式新设计正在满足不断变化的市场需求。例如,高速光耦合器满足了电信和数据处理系统中快速信号传输的需求。同时,栅极驱动光耦合器支持电动汽车(EV)和工业电机驱动器等大功率应用中的精确高效控制。先进材料和设计将碳化硅
    克里雅半导体科技 2024-11-29 16:18 157浏览
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 86浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 57浏览
  • 在电子技术快速发展的今天,KLV15002光耦固态继电器以高性能和强可靠性完美解决行业需求。该光继电器旨在提供无与伦比的电气隔离和无缝切换,是现代系统的终极选择。无论是在电信、工业自动化还是测试环境中,KLV15002光耦合器固态继电器都完美融合了效率和耐用性,可满足当今苛刻的应用需求。为什么选择KLV15002光耦合器固态继电器?不妥协的电压隔离从本质上讲,KLV15002优先考虑安全性。输入到输出隔离达到3750Vrms(后缀为V的型号为5000Vrms),确保即使在高压情况下,敏感的低功耗
    克里雅半导体科技 2024-11-29 16:15 119浏览
  • 国产光耦合器因其在电子系统中的重要作用而受到认可,可提供可靠的电气隔离并保护敏感电路免受高压干扰。然而,随着行业向5G和高频数据传输等高速应用迈进,对其性能和寿命的担忧已成为焦点。本文深入探讨了国产光耦合器在高频环境中面临的挑战,并探索了克服这些限制的创新方法。高频性能:一个持续关注的问题信号传输中的挑战国产光耦合器传统上利用LED和光电晶体管进行信号隔离。虽然这些组件对于标准应用有效,但在高频下面临挑战。随着工作频率的增加,信号延迟和数据保真度降低很常见,限制了它们在电信和高速计算等领域的有效
    腾恩科技-彭工 2024-11-29 16:11 106浏览
  • 光耦合器作为关键技术组件,在确保安全性、可靠性和效率方面发挥着不可或缺的作用。无论是混合动力和电动汽车(HEV),还是军事和航空航天系统,它们都以卓越的性能支持高要求的应用环境,成为现代复杂系统中的隐形功臣。在迈向更环保技术和先进系统的过程中,光耦合器的重要性愈加凸显。1.混合动力和电动汽车中的光耦合器电池管理:保护动力源在电动汽车中,电池管理系统(BMS)是最佳充电、放电和性能监控背后的大脑。光耦合器在这里充当守门人,将高压电池组与敏感的低压电路隔离开来。这不仅可以防止潜在的损坏,还可以提高乘
    腾恩科技-彭工 2024-11-29 16:12 117浏览
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 70浏览
  • 艾迈斯欧司朗全新“样片申请”小程序,逾160种LED、传感器、多芯片组合等产品样片一触即达。轻松3步完成申请,境内免费包邮到家!本期热荐性能显著提升的OSLON® Optimal,GF CSSRML.24ams OSRAM 基于最新芯片技术推出全新LED产品OSLON® Optimal系列,实现了显著的性能升级。该系列提供五种不同颜色的光源选项,包括Hyper Red(660 nm,PDN)、Red(640 nm)、Deep Blue(450 nm,PDN)、Far Red(730 nm)及Ho
    艾迈斯欧司朗 2024-11-29 16:55 155浏览
  • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
    youyeye 2024-11-30 14:30 63浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦