浅谈 STM32 库里的回调函数

1、回调函数

有人对 STM32 固件库里的回调函数有些好奇甚至纠结,这里简单介绍下。其实从用法及功能上讲他们并没有什么特别的,跟其它函数一样,也是实现特定功能的代码段。一般来讲,所谓回调函数,泛指基于事件触发而被调用执行的函数,简单点说,就是条件满足了就调用的函数,往往会跟函数指针结合起来通过函数指针实现调用。

经常会有人基于类似下面的代码介绍回调函数:

 /******************************/
 float Compute_Result[4]; //Store the computing result
 float Compute(float a,float b, float (*Action)(float a,float b));
 float Compute_Add(float a,float b); //callback function
 float Compute_Minus(float a,float b); //callback function
 float Compute_Multiply(float a,float b); //callback function
 float Compute_Divide(float a,float b); //callback function

float Compute_Add(float a,float b)
{return a+b;}
float Compute_Minus(float a,float b)
{return a-b;}
float Compute_Multiply(float a,float b)
{return a*b;}
float Compute_Divide(float a,float b)
{return a/b;}

float Compute(float a,float b, float (*Action)(float a,float b))
{
 return Action( a, b);
}
 float a = 100.5, b = 2.3;
 Compute_Result[0] = Compute( a, b, Compute_Add);
 Compute_Result[1] = Compute( a, b, Compute_Minus);
 Compute_Result[2] = Compute( a, b, Compute_Multiply);
 Compute_Result[3] = Compute( a, b, Compute_Divide);
/******************************/

在上面代码中,那四个有关加减乘除的函数可以看成回调函数,具体何时被调用,根据函数Compute(float a,float b, float (*Action)(float a ,float b ))里的函数指针的赋值情况来定,被赋予哪个回调函数的地址就调用哪个回调函数。当然,使用函数指针并不是回调函数的核心特征,因事件驱动而被调用才是其核心特征。

生活中我们有时会对某人说,回头再说、回头再聊。这里的潜台词往往就是等时机成熟了、条件满足了再来具体交涉。这里就充满着浓浓的回调意味。

回调函数可以理解成事件响应函数或者说事件驱动函数。即使相同的事件、基于不同的场景可能会有不同应对处理,从软件代码角度讲就对应不同的回调函数代码。

我们不妨看个生活中的例子。生活中有人中了六合彩了,针对这一事件,中奖人可能有下面诸多举动之一【这里简化下,多选一。】。但这件事发生在不同人身上,右边的选择很可能不尽一样。换言之,中奖了,到底会选择右边哪一项还得结合具体的人来定。

图 1、中六合彩的可能后续行为

在这里插入图片描述
我们再切换到 STM32 的嵌入式开发中来,以 UART 接收完成事件为例。针对这一事件,不同的应用场景的应对处理往往也是五花八门、五彩缤纷。

图 2、UART 接收完成后可能后续动作
在这里插入图片描述

显然,特定的应用场景对应着特定的回调函数,一般来讲,没法简单地仅仅基于事件就炮制一段处理代码,尤其具有针对性的代码。

结合上面的描述,稍微小结下。回调函数除了具有基于事件的触发而被调用执行的特征外,还具有相同事件因应不同应用场景可能需要不同的回调函数之特征,即基于特定应用场景的回调
函数其内容具有特定性。

2、STM32 固件库里的回调函数

说到这里,我们具体结合 STM32 外设固件库里回调函数来聊聊。

首先,作为一个函数库,里面不存在现存的完整的回调函数。前面的介绍已为此做铺垫,因为回调函数需要结合具体场景而拟定,作为函数库根本做不到这一点,它没法事先知晓发生某个事件时不同的应用会需要采取怎样的操作。

其次,STM32 库函数的确采用了回调机制,并基于可能的各种事件为 STM32 开发者预留了只有函数定义而无具体内容的空回调函数,或者是只定义了一些基于各类事件的函数指针,具体的回调函数需我们用户完成并将函数地址赋给相应的函数指针而被调用。简单点说,库给我们预留了众多回调函数接口。

STM32 固件库里的回调函数采用了两种调用方式:

第一种就是 legacy 方式,传统的回调方式,库以 weak 方式定义了各种空的回调函数,像下面这些。STM32 库里都给我们准备好了。【下面是有关 UART 部分事件的弱回调函数体,内容为空】

图 3、UART 传输事件相关弱回调函数定义
在这里插入图片描述

具体开发时,我们根据事件和应用场景基于类似上面的 weak 函数进行重写,重写时拿掉weak,库里预留的弱定义函数不用动它。比方像下面这些都是最终的用户回调函数。

图 4、UART 传输事件相关的用户回调函数
在这里插入图片描述
另外一种就是指针方式,或称注册方式。即函数库里先基于各类事件定义好了各种回调函数指针,具体的回调函数由用户基于不同事件和应用需求撰写,然后将函数地址赋给函数指针,这个动作我们称之为回调函数进行注册,之后回调函数就可以通过函数指针而被适时调用。

比方下面是 UART 外设里定义的一些函数指针:【星号所指的是与 UART 传输完成事件有关的回调函数所用的指针】

图 5、UART 传输事件相关的回调函数指针
在这里插入图片描述
当我们将回调函数写好后,将函数地址赋给函数指针即可在相应事件发生时被调用。比方类似下面的操作代码。红星标所指代码就是在做回调函数的注册。

图 6、UART 传输完成事件用户回调函数及注册
在这里插入图片描述

给函数指针赋地址可以直接赋地址或通过调用库函数 xxx_RegisterCallback 完成【见上图星标代码】。

这种指针方式需要我们对 C 语言中的结构体、函数指针有较好的了解,库只是给我们提供了相应的函数指针,具体的用户回调函数由用户根据需要来编写,将其地址赋给相应的函数指针以供调用。

而前面介绍的传统型回调函数,库则帮我们把可能涉及到的回调函数全部以弱定义的方式都准备好了,我们按需针对性选用,去掉 weak 填空重写。使用起来相对更直观些,无需我们对函数指针有太多了解。

目前 STM32 库回调机制中,作为用户到底使用上面的哪种回调方式呢?在每个系列的固件库的配置头文件中有针对各个外设事件回调函数使用方式的选择,比方以 STM32F4 系列为例,这里有个 stm32f4xx_hal_conf.h 的头文件,我们可以看到基于各个外设事件回调函数使用方式选择的宏。

图 7、回调函数调用方式的选择配置
在这里插入图片描述
若我们不对该头文件的相应外设事件的回调函数调用方式的宏定义做调整,则默认传统回调方式,即 legacy 方式,非指针方式。若将相应的宏值改为 1,则该外设事件相关回调函数采用指针注册方式。

3、STM32 库函数里的回调机制及触发事件

整体上讲,STM32 外设库里的 API 函数由三部分组成,分别是:

初始化函数
启动型执行函数
回调函数【弱定义函数或回调函数指针,最终得靠用户具体完成编写】

这样的安排,让整个工程代码结构比较清晰,可以让人快速了解库结构,同时现存的 API 函数大大减少开发工作量,预留的回调函数接口一方面给开发者提供了便利,另一方面让用户基于不同应用场景自由组织代码而又不破坏整个软件架构。

对于回调函数,可以由哪些事件触发呢?大致分三类,分别是外设初始化操作 、外设处理完成【中断】事件、外设出错【中断】 事件。我们关注最多是外设处理完成中断事件相关的回调函数。

图 8、回调函数触发事件的分类
在这里插入图片描述

4、关于 STM32 HAL 库里的回调函数的几个常见问题

4.1、STM32 库函数里的回调函数是什么,有何用?

回调函数终究乃用户所编写,是用户基于特定事件和应用需求而编写的功能模块,与其它函
数并无本质区别。形式上讲,STM32 库预先为用户做了回调函数的弱定义或基于事件的函数指
针的定义。它基于特定条件发生后被调用执行而被冠以回调称号。
严格来讲,库函数里还没有完整的回调函数,只有基于各类事件的弱定义的不具备实际功能
的空回调函数,或者是针对各类事件定义了各种用于调用回调函数的函数指针。我们的程序监测
相应条件或事件往往是有的放矢,当相应事件出现时我们需要做相应的处理,这正是回调函数要
实现的功能,也是其功用所在。

4.2、STM32 工程里的回调函数与中断函数有什么区别?

STM32 里的回调函数的确多数时候跟中断事件及中断服务程序息息相关,往往在中断服务
程序中基于特定事件调用相应的用户回调函数,我们完全可以将用户回调函数看成中断函数的一
个调用模块或延伸。
一个中断服务程序里可以因不同事件而调用不同的回调函数,即一个中断服务程序里可以包
含多个不同的回调函数。比方,我们在定时器中断服务程序里可以涉及多个事件及相应的用户回
调函数,定时器中断服务程序可能涉及更新事件、不同通道的比较事件或捕获事件,相应的用户
回调函数往往因应用场景而异。
当然,回调函数的调用还可以是中断事件以外的其它事件触发调用,比方可以基于初始化操
作来调用相应初始化回调函数。在库里对某个外设的初始化可能有些默认操作,但这个默认操作
很难是放之四海而皆准的操作,这时我们就得根据实际应用针对性编写初始化代码,即初始化回
调函数。

4.3、STM32 库函数里的回调函数是否可以不用?

STM32 库函数里的回调机制是库设计者为了便于软件框架清晰、减少开发者工作量等因素事先准备的函数声明及接口,用户使用时只需根据具体应用编写相关函数体。当然,你如果不想理睬这些回调函数声明及定义也是可以的,你根据具体应用自行组织代码完全可行。

4.4、STM32 库函数里似乎存在着类似半成品的库回调函数?

STM32 库函数里的确准备了一些包含用户回调函数的由库定义的回调函数,是库设计者基于各类特定事件而准备的回调函数,它会针对特定事件做一些基本而必要的操作,比方状态的检查、标志检测及清除,但它没有办法彻底写完整,因为它无法知道该事件发生后用户的真实需求是什么,该如何操作,所以它终究需要调用真正的用户回调函数。这样做的目的还是为了给开发者减少开发工作量、以及减少出错等。

我们不妨具体看个实例。下面的回调函数采样的指针注册方式,我们看看 UART 的 DMA 传输完成中断里传输完成的回调函数的调用过程。

首先,在 UART 的 DMA 启动函数 HAL_UART_Transmit_DMA()里有这样一部分内容:

图 9、外设启动运行代码中库回调函数的赋值
在这里插入图片描述
库里就 DMA 传输事件准备了几个回调函数【传输完成、半完成、出错】,即上图中红线标示出来的。其实这几个回调函数还不算真正的用户回调函数,是库定义的并会做一些在它看来用户必定需要完成的一些操作,它事先帮助完成,之后才调用最终的用户回调函数。我们以传输完成事件为例来看看,上图星号所标的函数。

图 10、库回调函数进一步调用用户回调函数
在这里插入图片描述
在这个库定义的 UART_DMATransmitCplt()函数里,它对 DMA 的传输模式做了判断,如果是 Normal 模式,就将 UART 的传输数据长度设置为 0,禁止 DMA 后续传输功能,使能UART 传输完成中断的使能。然后才来调用用户回调函数【上图中箭头所指】。如果 DMA 工作在循环模式,代码进到 UART_DMATransmitCplt()函数后就直接调用最终的用户回调函数。

也就说这些库定义的回调函数在用户回调函数的基础上做了些必要操作,用户回调函数可以看成这类库回调函数的子集。

4.5、基于 STM32 库来组织用户回调函数要注意什么?

前面提过了,用户回调函数主要基于初始化事件或中断事件而组织代码。那些中断事件的回调函数的调用都是在中断服务程序里发生的。所以,我们在编写回调函数时要结合具体情况灵活地组织代码。要考虑中断优先级、具体事件响应的实时性等。具体点说,我们在组织回调函数时,要考虑是不是一定要一股脑地全写在中断服务程序里,会不会影响别的中断响应。对于有些不紧急而又耗时的事件响应代码,可以考虑只在回调函数里设置相应标志,真正的处理代码放到主循环去完成。

还有一点,STM32 库里主动给我们准备了弱定义回调函数或基于各个事件的回调函数指针,尽管很丰富了,但也未必能包罗万象,必要时我们可能还得根据具体情况来额外组织些类似回调函数的事件响应代码。

关于 STM32 HAL 库里的回调函数就简单介绍到这里,希望能帮到一些 STM32 开发者。


本文档参考ST官方的《【应用笔记】LAT1271+待机模式被意外唤醒之原因分析》文档。
参考下载地址:https://download.csdn.net/download/u014319604/89072672

  • 61
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值