的使用 STM32实战指南:如何正确使用回调函数
什么是回调函数?
回调函数是指:使用者自己定义一个函数,实现这个函数的程序内容,然后把这个函数(入口地址)作为参数传入别人(或系统)的函数中,由别人(或系统)的函数在运行时来调用的函数。函数是你实现的,但由别人(或系统)的函数在运行时通过参数传递的方式调用,这就是所谓的回调函数。
第一层含义,回调函数是通过指针来调用的函数。
当然,只是通过指针来调用,不一定就是回调函数,比如上一篇讲LED的文章中,就是通过结构体里面的指针来调用函数的,但这不是回调函数。
回调函数通常涉及到三层,应用层、中间层以及回调函数层,将回调函数作为中间层的形参,然后应用层通过调用中间层来调用回调函数。
示例:
Fun1() { Fun2(Fun3()); }
以上,Fun1是应用层函数,Fun2是中间层函数,Fun3就是回调函数。
意义何在?
像之前写LED那样直接调用不好吗?
两层的调用,有什么问题呢?
1、被调用层每增加一个函数,就要在结构体中增加一个对应的函数指针,增加了内存消耗(主要是变量占内存),大工程时尤其明显;
2、如果被调用层函数有调整,可能应用层就要进行对应的修改,耦合较高;
如果分为三层,通过回调函数,就能一定程度上解决以上问题。
貌似这里面有一个规律,只要有三层,就能解放最上层。
代码实现
代码实现,大体上就是在之前的两层之间,再加入虚拟层。
低层负责硬件部分,和硬件打交道的驱动部分;
应用层是业务逻辑相关的实现;
中间层就是其到一个衔接作用,用来减小应用层和驱动层之间的耦合,同时降低内存开销。
在上一篇HAL之GPIO中(STM32实战总结:HAL之GPIO_路溪非溪的博客-CSDN博客),led.c是和硬件相关的底层,状态机调用驱动层函数来实现业务功能,再往上的层次都是和业务相关了。
回调函数中,驱动层就是第三层,状态机层就是应用层,当前的调用方法是,应用层直接调用驱动层。
那么,为了实现回调函数,我们就需要增加一层虚拟中间层,应用层调用中间层,中间层再调用驱动层。
注意,这里如果仔细想想,至少有两种实现。
第一种
再创建两个文件作为中间层,叫做ledmiddle.c和ledmiddle.h层。
ledmiddle.h
#ifndef _LEDMIDDLE_H_ #define _LEDMIDDLE_H_ #include "stdint.h" //确定要实现的led功能 typedef struct { //点亮 void (*led_light_middle)(uint8_t); //熄灭 void (*led_extinguish_middle)(uint8_t); //转换亮灭 void (*led_switch_middle)(uint8_t); } led_funtcions_middle; //将结构体声明出去 extern led_funtcions_middle led_operater_middle; #endif
ledmiddle.c
#include "myapplication.h" static void LedLightMiddle(uint8_t lednum); static void LedExtinguishMiddle(uint8_t lednum); static void LedSwitchMiddle(uint8_t lednum); led_funtcions_middle led_operater_middle = { LedLightMiddle, LedExtinguishMiddle, LedSwitchMiddle }; static void LedLightMiddle(uint8_t lednum) { led_operater.led_light(lednum); } static void LedExtinguishMiddle(uint8_t lednum) { led_operater.led_extinguish(lednum); } static void LedSwitchMiddle(uint8_t lednum) { led_operater.led_switch(lednum); }
对于之前的led.c和led.h,内容完全不变,对于stamachie而言,只用将原本调用驱动层的代码,改成调用中间层。中间层里去直接调用驱动层的代码。
有没有看明白?
这样也是间接调用。
但是,这种间接调用改善了上面说的耦合以及减少内存开销的问题了吗?
仔细想想,不仅没有减少开销,而且使得问题更严重了。
1、耦合的问题并没有解决,之前的情况是,假如驱动层修改了一个代码的名称,那么应用层就要做相应的修改。现在是驱动层修改了一个代码的名称,那么中间层就要做相应的修改。如果中间层调整了函数名呢?应用层就得跟着改。使得耦合变复杂了。
2、关于内存占用,不仅没有减少内存开销,反而增加了两个文件,两个文件中,只要驱动层有几个函数指针,这里就要设置几个函数指针,使得内存占用翻倍了。
可见,这种间接调用是无意义的,而且是冗余的。也能得出一个结论:
回调函数不是在Fun2中直接调用Fun3。
第二种
到底回调函数是怎样的一种实现呢?
回调函数不是在Fun2中直接调用Fun3,而是必须将Fun3作为中间层的形参,中间层,实际上是个虚拟层,并不实现具体功能。
接下来进入主题:
考虑一下,如果F1要调用F2,并且F3是F2的形参,从而实现F1间接调用F3的话,那么,中间层就得有个函数,这个函数有个函数指针作为形参,并且,这个形参将来可以调用到对应的驱动层函数,那么,就得把要调用的函数所需要的形参也传进去。
有点抽象。直接代码走起。
1、首先,删除刚才创建的两个冗余文件ledmiddle.c和ledmiddle.h;
2、在led.h中定义一个结构体,该结构体只有一个函数,这个函数,传递驱动函数指针以及对应的参数;
#ifndef _LED_H_ #define _LED_H_ #include "stdint.h" //有三个LED灯,定义成枚举,并编号 typedef enum { LED1 = 1u, LED2, LED3 } led_status; //确定要实现的led功能 typedef struct { void (*ledMiddle)(uint8_t, void (*callback)(uint8_t)); /*因为不直接调用驱动层了,所以这些函数指针没用了 //点亮 void (*led_light)(uint8_t); //熄灭 void (*led_extinguish)(uint8_t); //转换亮灭 void (*led_switch)(uint8_t); */ } led_funtcion_middle; //将结构体声明出去 extern led_funtcion_middle led_operater_middle; //函数声明 void LedLight(uint8_t lednum); void LedExtinguish(uint8_t lednum); void LedSwitch(uint8_t lednum); #endif
3、初始化该结构体变量并在相应头文件中声明出去,让应用层调用。
同时,因为应用层调用时,需要用到驱动层的函数指针,所以将被调用的函数放开static,并在头文件里声明。
#include "myapplication.h" static void LedMiddle(uint8_t led, void (*callback)(uint8_t)); static void Led1Blink(void); led_funtcion_middle led_operater_middle = { LedMiddle }; static void LedMiddle(uint8_t led, void (*callback)(uint8_t)) { callback(led); } void LedLight(uint8_t lednum) { switch(lednum) { case LED1 : HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET); break; case LED2 : HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET); break; case LED3 : HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin, GPIO_PIN_SET); break; default : Led1Blink(); } } void LedExtinguish(uint8_t lednum) { switch(lednum) { case LED1 : HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET); break; case LED2 : HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET); break; case LED3 : HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin, GPIO_PIN_RESET); break; default : Led1Blink(); } } void LedSwitch(uint8_t lednum) { switch(lednum) { case LED1 : HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); break; case LED2 : HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); break; case LED3 : HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin); break; default : Led1Blink(); } } //如果输入的不是LED1/LED2/LED3则LED1闪烁 static void Led1Blink(void) { HAL_Delay(100); HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); }
4、在应用层上,直接调用中间函数,并将驱动层函数作为指针传入,来达到间接调用的目的。
#include "myapplication.h" static void Sta1Func(void); static void Sta2Func(void); static void Sta3Func(void); static void Sta4Func(void); static void Sta5Func(void); state_machine state_machiner = { STA1, Sta1Func, Sta2Func, Sta3Func, Sta4Func, Sta5Func, }; static void Sta1Func(void) { HAL_Delay(500); led_operater_middle.ledMiddle(LED1, LedExtinguish); led_operater_middle.ledMiddle(LED2, LedExtinguish); led_operater_middle.ledMiddle(LED3, LedExtinguish); state_machiner.stateLocation = STA2; } static void Sta2Func(void) { HAL_Delay(500); led_operater_middle.ledMiddle(LED1, LedLight); HAL_Delay(500); led_operater_middle.ledMiddle(LED1, LedExtinguish); state_machiner.stateLocation = STA3; } static void Sta3Func(void) { HAL_Delay(500); led_operater_middle.ledMiddle(LED2, LedLight); HAL_Delay(500); led_operater_middle.ledMiddle(LED2, LedExtinguish); state_machiner.stateLocation = STA4; } static void Sta4Func(void) { HAL_Delay(500); led_operater_middle.ledMiddle(LED3, LedLight); HAL_Delay(500); led_operater_middle.ledMiddle(LED3, LedExtinguish); state_machiner.stateLocation = STA5; } static void Sta5Func(void) { HAL_Delay(500); led_operater_middle.ledMiddle(LED1, LedLight); led_operater_middle.ledMiddle(LED2, LedLight); led_operater_middle.ledMiddle(LED3, LedLight); state_machiner.stateLocation = STA1; }
以上就完成了回调函数的实现。
不得不说,这种方式确实很巧妙,相对原来,只多了个结构体及其实现,第一点,减少了指针变量,也就相应减少了内存开销;第二点,并没有将驱动函数和应用层,甚至没有跟中间层产生过多联系,函数完全是独立的,需要谁的时候,只需要知道函数名称,直接传入指针即可。
不过,我也发现,一种类型的回调函数,就需要一个中间虚拟层函数。因为传入的是函数指针,如果函数指针的类型不一样,就需要不同的中间层函数。但不管怎么样,肯定不会比原来的有更多指针,而且,确实解决了耦合的问题。
补充:
1、中间函数不需要返回值,只是一个调用动作;
2、中间函数没有具体实现,只是在里面调用函数指针;
3、其实函数都已经暴露出来了,可以直接调用的,但是不太符合面向对象思想,没有框架性;