应用笔记 | 浅谈STM32库里的回调函数
有人对STM32固件库里的回调函数有些好奇甚至纠结,这里简单地介绍下,以供参考。其实从用法及功能上讲他们并没有什么特别的,跟其他函数一样,也是实现特定功能的代码段。一般来讲,所谓回调函数,泛指基于事件触发而被调用执行的函数,简单点说,就是条件满足了就调用的函数,往往会跟函数指针结合起来通过函数指针实现调用。
经常会有人基于类似下面的代码介绍回调函数:
在上面代码中,那四个有关加减乘除的函数可以看成回调函数,具体何时被调用,根据函数Compute(float a,float b,float(*Action)(float a,float b))里的函数指针的赋值情况来定,被赋予哪个回调函数的地址就调用哪个回调函数。当然,使用函数指针并不是回调函数的核心特征,因事件驱动而被调用才是其核心特征。
生活中我们有时会对某人说,回头再谈、回头再聊。潜台词往往就是等时机成熟了、条件满足了再来具体交涉。这里就充满着浓浓的回调意味。
回调函数可以理解为事件响应函数或者说事件驱动函数。即使相同的事件、基于不同的场景可能会有不同应对处理,从软件代码角度讲就对应不同的回调函数代码。
我们不妨看个生活中的例子。生活中有人中了六合彩了,针对这一事件,中奖人可能有下面诸多举动之一【这里简化下,多选一】。但这件事发生在不同人身上,右边的选择很可能不尽一样。换言之,中奖了,到底会选择右边哪一项还得结合具体的人来定。
图1 中六合彩的可能后续行为
我们再切换到STM32的嵌入式开发中来,以UART接收完成事件为例。针对这一事件,不同的应用场景的应对处理往往也是五花八门、五彩缤纷。
图2 UART接收完成后可能后续动作
显然,特定的应用场景对应着特定的回调函数,一般来讲,没法简单地仅仅基于事件就拟定一段既能适用于各种场景而又富有针对性的代码。
结合上面的描述,稍微小结下。回调函数除了具有基于事件的触发而被调用执行的特征外,还具有相同事件因应不同应用场景可能需要不同的回调函数之特征,即基于特定应用场景的回调函数其内容具有特定性。
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,则该外设事件相关回调函数采用指针注册方式。
整体上讲,STM32外设库里的API函数大体由三部分组成,分别是:
初始化函数
启动型执行函数
回调函数 【弱定义函数或回调函数指针,最终靠用户具体完成编写】
这样的安排,让整个工程代码结构比较清晰,可以让人快速了解库结构,同时现存的API函数大大减少开发工作量,预留的回调函数接口一方面给开发者提供了便利,另一方面让用户基于不同应用场景自由组织代码而又不破坏整个软件架构。
对于回调函数,可以由哪些事件触发呢?大致分三类,分别是外设初始化操作、外设处理完成【中断】事件、外设出错【中断】事件。我们关注最多是 外设 处理完成中断事件 相关的回调函数。
图8 回调函数触发事件的分类
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开发者。
关注STM32
▽ 点击“阅读原文”,可下载原文档