的使用 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、其实函数都已经暴露出来了,可以直接调用的,但是不太符合面向对象思想,没有框架性;

物联沃分享整理
物联沃-IOTWORD物联网 » 的使用 STM32实战指南:如何正确使用回调函数

发表评论