基于esp32的单按键多功能

单按键多功能

在平常项目中,经常会使用按键来进行一些人机交互。但只是经过判断按键连接引脚电平变化来判断单击操作,因此一个按键只能对应一个事件。为了扩展硬件的利用效率(榨干软件开发大脑),可以通过软件算法来实现判断按键的单击 双击 长按等操作。

分析的源码来源于乐鑫组件库,以及对组件介绍的官方文档。因此关于代码环境是基于esp-idf,在源码中关于按键的抽象定义有多种类型

typedef enum {
    BUTTON_TYPE_GPIO,	// 按键类型为GPIO
    BUTTON_TYPE_ADC,	// 按键类型为ADC
    BUTTON_TYPE_MATRIX,	// 按键类型为矩阵
    BUTTON_TYPE_CUSTOM	// 按键类型为用户自定义需要有
        				//(1.按键摁下后的电平 2.按键初始化函数 3.获取按键当前输入值函数 4.按键反初始化函数)
} button_type_t;
// 不管哪种类型 关于按键的本质是一样的 都是通过检测按键的电平变化 来判断按键按下

由于平常都是采用GPIO方式来使用按键,所以这里只对GPIO做介绍,对于别的类型可以自行查阅源码,只是底部硬件层采用方式不同,对应多状态算法实现都是一样的。

😉0.按键配置操作

按键功能的实现,本质上就是根据外连引脚的按键硬件环境检测对应的上升沿或者下降沿,也就是按键按下后的电平变化。因此需要将按键引脚初始化为输入模式,并且实现能够获取引脚输入值的功能。因为机械按键在摁下过程中会有抖动,所以还需要进行消抖。但整个过程所需要完成的功能就可以抽象为两步。

ⅰ.按键初始化为输入模式
/*
关于esp-idf环境,对于gpio的初始化有两种方式,一种是采用结构体进行所有属性配置,另一种是采用函数来配置对应属性
本例子采用函数的方式(源码中采用结构体方式)具体内容可看esp-idf官方文档有详细介绍
*/
//MY_BTN_PIN 为按键连接的引脚
gpio_set_direction(MY_BTN_PIN, GPIO_MODE_INPUT);// 设置为输入模式
// 根据按键硬件环境 调整为引脚为上拉还是下拉 如果按键悬空电平可以确定也可舍去
gpio_set_pull_mode(MY_BTN_PIN, GPIO_PULLUP_ONLY);
ⅱ.获取按键引脚输入值
// 通过调用
uint8_t button_gpio_get_key_level(void *gpio_num)
{
    return (uint8_t)gpio_get_level((uint32_t)gpio_num);
}

这两个操作,作为实现底层硬件配置的接口,上层软件会使用其来实现对硬件的操作。除此之外,还需要提供按键IO引脚和按键按下后的电平(在库中称为active_level)。

😁1. 关于该库的使用

由于此库的封装已经非常完善,对该库的使用代码量极其少且逻辑简单,而且采用回调函数完全解耦的方式,也能保证按键事件对应的用户的逻辑处理完全自由化。下面步骤源码皆源于该库源代码文件夹下examples的例程

🍖ⅰ.建个房子吧–设置按键参数

头文件所定义的button_config_t结构体,用来描述需要配置按键的硬件信息

typedef struct {
    button_type_t type;                               /**按键使用的类型 就是上文提到的gpio,adc,矩阵等**/
    
    // 关于long_press_time,short_press_time用户可以不配置,在初始化的时候会使用库的默认值
    uint16_t long_press_time;                         /**长按最短判断时间 如果按键保持按下超过此时间判定为长按事件 */
    uint16_t short_press_time;                        /**连按最长允许时间  相邻连按间隔超过此时间无效*/
    
    //这是一个联合体 具体内容要和type相对应,如果type设置为BUTTON_TYPE_GPIO 则该联合体需要根据button_gpio_config_t配置
    union {
        button_gpio_config_t gpio_button_config;      /** gpio按键配置 */
        button_adc_config_t adc_button_config;        /** adc按键配置 */
        button_matrix_config_t matrix_button_config; /** 矩阵按键配置 */
        button_custom_config_t custom_button_config;  /** 自定义按键配置 */
    }; 
} button_config_t;

由于本文是针对GPIO类型所分析,所以关于该联合体只介绍button_gpio_config_t结构体内容,别的内容若感兴趣可自行分析,只要搞懂按键原理(根据按键按下,找到变化量),分析起来并不是很困难。

typedef struct {
    int32_t gpio_num;              /** 按键引脚 */
    uint8_t active_level;          /** 当按键按下后 按键引脚输入电平值 只有0和1 分别对应低电平和高电平*/
} button_gpio_config_t;

分析完上面两个结构体后 再去看例程中关于按键初始化的内容就轻易多了

// 声明一个button_config_t变量
button_config_t btn_cfg = {
    .type = BUTTON_TYPE_GPIO,				// 该按键类型为GPIO
    // 这里给的例程并没有为long_press_time,short_press_time赋值,而是准备采用默认值
    .gpio_button_config = {
        .gpio_num = button_num,				// 按键引脚
        .active_level = BUTTON_ACTIVE_LEVEL,// 按键按下后 输入电平
    },
};

ok ok 有这些内容就已经完全够了,在多就装不下了,下面让我们干个大事,比如创造个按键句柄怎么样,我觉得以后肯定用的着。尤其当你突然灵机一哆嗦,发出疑问?咦~,如果我要使用两个按键怎么办。放心我们有句柄来帮忙分辨谁是谁,可以把句柄看作一个钥匙,关于指定按键的操作,比如给它事件绑定一个回调函数,或者修改一些参数,就可以类比于进入这个房间。也就是句柄=>钥匙,对按键操作=>要进入上锁的房间。所以我们要有句柄才能操作对应按键。

但是现在是一片废墟啊,哪有什么房间,我手里只有一个该房间的蓝图(btn_cfg 上面声明的按键配置变量)。所以我们就需要建造这个名叫按键的房间了,说到这里不得不提一嘴放在xxx书架上的函数了,只需简单一句话就可“万丈高楼平地起”。

// button_handle_t iot_button_create(const button_config_t *config); 函数原型
// 通过调用该函数 传入参数 你的“按键”房间就此诞生 作为户主 拿好你的“钥匙”btn 这可是该按键的唯一凭证
button_handle_t btn = iot_button_create(&btn_cfg);
🍗ⅱ.选个装修吧–设置回调函数

在上面,我们已经有了一个按键“房间”,但里面空空如也,你也不想一回到家就睡在地板上吧,你也不想饿了就啃承重墙吧。所以少年选好按键指定事件要执行的功能(就是按键单击要实现啥功能,按键双击会执行什么操作),让你的房间焕然一新吧。

直接看回调函数模板样式

// 这是一个函数指针 作为回调函数 在声明中必须严格按照该形式
typedef void (* button_cb_t)(void *button_handle, void *usr_data);

比如举个例子看看应该怎么写

static void button_event_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "Button event %s", button_event_table[(button_event_t)data]);
}

对比可得 所谓严格按照该形式就是要保证 1.函数返回值相同 2.函数参数相同。关于函数名不如取个暗恋女孩的缩写,因为念念不忘必有回响嘛。关于函数里面的内容,就交给聪明的你自由发挥了。在这里实现的功能就是打印出调用该回调函数的事件。这您能看出来吧

🥩ⅲ.合二为一吧–绑定回调函数

我们现在有了按键“房间”,有了回调函数“装修”。下面就动手合二为一吧!寻找我们的工具

esp_err_t iot_button_register_cb(button_handle_t btn_handle, button_event_t event, button_cb_t cb, void *usr_data)

button_handle_t btn_handle:我们的房间钥匙,装修工人不得进你房间啊。

button_event_t event:回调函数所绑定的事件,这个具体有什么事件没介绍过,不是现在,以后分析源码的时候还会逐个“讲解”,但可以让你看一眼其中一个BUTTON_SINGLE_CLICK 。考验英语的时候到了,说了不是现在,非要看。button是按钮的意思吧,single这个应该熟悉吧,是不是可以看做单身(你是单身嘛,你为什么单身,是喜欢吗)。也可以翻译成单次,click点击的意思,ok让我们连起来是不是单击

button_cb_t cb:刚刚才说过的回调函数

void *usr_data:可以往上翻翻 看看哪里出现了usr_data,是不是能得出结论,在注册的时候传入的usr_data,最后跑到了回调函数的参数data


ok了老铁们,下面实战看看应该怎么用吧

 esp_err_t err = iot_button_register_cb(btn, BUTTON_SINGLE_CLICK, button_event_cb, (void *)BUTTON_SINGLE_CLICK);

这分析起来不难了吧,不就是把button_event_cb回调函数与BUTTON_SINGLE_CLICK事件绑定一起了嘛,在绑定的时候传进去的用户数据是BUTTON_SINGLE_CLICK,可以结合例程源码分析,这就是多个事件绑定同一个回调函数,又可以执行不同效果的关键。其中一个事件也可以绑定多个回调函数,执行顺序和绑定顺序相关。

开始了嘛 不 已经结束了 看看例程是怎么写的吧(复制粘贴 最爽了)

const char *button_event_table[] = {
    "BUTTON_PRESS_DOWN",
    "BUTTON_PRESS_UP",
    "BUTTON_PRESS_REPEAT",
    "BUTTON_PRESS_REPEAT_DONE",
    "BUTTON_SINGLE_CLICK",
    "BUTTON_DOUBLE_CLICK",
    "BUTTON_MULTIPLE_CLICK",
    "BUTTON_LONG_PRESS_START",
    "BUTTON_LONG_PRESS_HOLD",
    "BUTTON_LONG_PRESS_UP",
};

static void button_event_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "Button event %s", button_event_table[(button_event_t)data]);
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause != ESP_SLEEP_WAKEUP_UNDEFINED)
    {
        ESP_LOGI(TAG, "Wake up from light sleep, reason %d", cause);
    }
}

void button_init(uint32_t button_num)
{
    button_config_t btn_cfg = {
        .type = BUTTON_TYPE_GPIO,
        .gpio_button_config = {
            .gpio_num = button_num,
            .active_level = BUTTON_ACTIVE_LEVEL,
        },
    };
    button_handle_t btn = iot_button_create(&btn_cfg);
    assert(btn);
    esp_err_t err = iot_button_register_cb(btn, BUTTON_PRESS_DOWN, button_event_cb, (void *)BUTTON_PRESS_DOWN);
    err |= iot_button_register_cb(btn, BUTTON_PRESS_UP, button_event_cb, (void *)BUTTON_PRESS_UP);
    err |= iot_button_register_cb(btn, BUTTON_PRESS_REPEAT, button_event_cb, (void *)BUTTON_PRESS_REPEAT);
    err |= iot_button_register_cb(btn, BUTTON_PRESS_REPEAT_DONE, button_event_cb, (void *)BUTTON_PRESS_REPEAT_DONE);
    err |= iot_button_register_cb(btn, BUTTON_SINGLE_CLICK, button_event_cb, (void *)BUTTON_SINGLE_CLICK);
    err |= iot_button_register_cb(btn, BUTTON_DOUBLE_CLICK, button_event_cb, (void *)BUTTON_DOUBLE_CLICK);
    err |= iot_button_register_cb(btn, BUTTON_LONG_PRESS_START, button_event_cb, (void *)BUTTON_LONG_PRESS_START);
    err |= iot_button_register_cb(btn, BUTTON_LONG_PRESS_HOLD, button_event_cb, (void *)BUTTON_LONG_PRESS_HOLD);
    err |= iot_button_register_cb(btn, BUTTON_LONG_PRESS_UP, button_event_cb, (void *)BUTTON_LONG_PRESS_UP);
    ESP_ERROR_CHECK(err);
}

我才不再分析一遍呐 φ(* ̄0 ̄) 看看执行效果得了

  1. 单击

    在这里插入图片描述

  2. 双击

    在这里插入图片描述

  3. 长按
    在这里插入图片描述

😭2.分析源码时间到

因为这是分析源码,就像阅读语文文章,我们只需管代码的功能和内容,无需去考虑为什么要这样写。

由于通过该库可以支持多个按键,每个按键都有其对应的配置变量button_config_t,但经由iot_button_create()函数最终会示例化一个设备变量button_dev_t,用来表示一个按键实体,存储着按键的物理信息和按键事件判断算法的参数。

/**
 * @brief 记录单个按键参数的结构体
 *
 */
typedef struct Button
{
    uint16_t             ticks;					// 用来记录滴答数 作为长按,连按的判定依据。具体实现在代码中体现
    uint16_t             long_press_ticks;     	// 通过和上面ticks比较 来判断是否为长按事件
    uint16_t             short_press_ticks;    	// 如果两次按键摁下时间间隔的ticks小于该项,则认为是连按
    uint16_t             long_press_hold_cnt;  	// 长按保持计数,判断为长按事件后,如果一直摁下就为保持状态
    //uint16_t             long_press_ticks_default; 源码中有些参数和代码块 分析不出来有什么目的 在运行中也没有使用
    uint8_t              repeat;				// 记录连按次数 如单击1次 双击2次
    uint8_t              state : 3;				// 记录状态机的状态 3表示位数 即3位二进制 最大值为8
    uint8_t              debounce_cnt : 3;		// 这是去抖变量 在去抖算法中会使用到
    uint8_t              active_level : 1;		// 按键按下后的电平
    uint8_t              button_level : 1;		// 按键最近稳定电平值
    uint8_t              enable_power_save : 1;	// 是否为低功耗模式
    button_event_t       event;					// 按键事件
    uint8_t(*hal_button_Level)(void *hardware_data);	// 获取按键当前电平值
    esp_err_t(*hal_button_deinit)(void *hardware_data);	// 按键反初始化 在删除按键会用到
    void *hardware_data;								// 硬件数据 在GPIO模式下为按键IO
    button_type_t        type;							// 按键类型
    // 这是一个指针数组 即数组每项为一个指针 可以理解为二维动态数组 用来存储不同事件绑定的多个回调函数
    button_cb_info_t *cb_info[BUTTON_EVENT_MAX];
    size_t               size[BUTTON_EVENT_MAX];     	// 因为一个事件可以有多个回调函数,此变量用来存储具体数量
    // int                  count[2];					分析不了在代码中此函数有什么用
    struct Button *next;								// 该结构体也是一个链表节点 这用来指向下一个按键
} button_dev_t;
🍘ⅰ.从iot_button_create开始

这个函数是整个库工作最开始的地方,执行完之后实现的功能有:1.根据配置对按键初始化,2.将按键注册在*”列车管理员“* 管理册中(就是一个链表),3.创建一个定时器任务,不断检测按键状态,并判断对应的事件(核心)。

iot_button_create()源码部分内容
button_handle_t iot_button_create(const button_config_t *config)
{
    // 打印库版本信息 并检测config是否合法
    ESP_LOGI(TAG, "IoT Button Version: %d.%d.%d", BUTTON_VER_MAJOR, BUTTON_VER_MINOR, BUTTON_VER_PATCH);
    BTN_CHECK(config, "Invalid button config", NULL);

    esp_err_t ret = ESP_OK;
    // 这个变量是 这个库真正要管理的结构体 里面有涉及按键的相关算法变量和按键信息
    // 要和 button_config_t 区分开 根据名字也可以很容易看出区别 config配置变量 dev设备变量
    // 注册完之后 返回的句柄也是此变量的变形 其实可以看做该变量的内存地址
    button_dev_t *btn = NULL;
    uint16_t long_press_time = 0;
    uint16_t short_press_time = 0;
    // 将设置的时间 转化为“滴答数” 比如举个例子 设置长按时间为1500ms 按键的定时中断周期为5ms 其滴答数就为1500ms/5ms=500
    // 如果用户没有手动设置 TIME_TO_TICKS会采用系统默认值
    long_press_time = TIME_TO_TICKS(config->long_press_time, LONG_TICKS);
    short_press_time = TIME_TO_TICKS(config->short_press_time, SHORT_TICKS);
    // 根据传进函数的配置中 按键类型信息进行选择 要执行的代码块(因为采用GPIO模式 这里只显示相关代码内容) 
    switch (config->type) 
    {
        case BUTTON_TYPE_GPIO: 
        {
            //获取传入参数 GPIO相关配置
            const button_gpio_config_t *cfg = &(config->gpio_button_config);
            //然后根据此配置对GPIO初始化 
            ret = button_gpio_init(cfg);
            BTN_CHECK(ESP_OK == ret, "gpio button init failed", NULL);
            //这个函数实现上面所说的功能之一 -- 将按键注册在列车管理员管理册中 稍后会介绍其内容
            btn = button_create_com(cfg->active_level, button_gpio_get_key_level, (void *)cfg->gpio_num, long_press_time, short_press_time);
            //对于这个按键算法有两种实现方式 一种是采用定时器 功耗较大 另一种采用中断 功耗较小
            //这个条件编译指令 可以根据你四级稍微吃力的英语可以看出是将按键配置成低功耗模式
#if CONFIG_GPIO_BUTTON_SUPPORT_POWER_SAVE
            if (cfg->enable_power_save) {
                    btn->enable_power_save = cfg->enable_power_save;
                    button_gpio_set_intr(cfg->gpio_num, cfg->active_level == 0 ? GPIO_INTR_LOW_LEVEL : 	GPIO_INTR_HIGH_LEVEL, button_power_save_isr_handler, (void *)cfg->gpio_num);
            	}
#endif
        } break;
     /*这个是源库中省略的内容 有关其他按键类型的初始化 包括 (adc 矩阵 自定义) 这里不做分析 其实可以复制过来凑字数*/       
    ...... 
    }
    BTN_CHECK(NULL != btn, "button create failed", NULL);
    btn->type = config->type;
    //这句话的意思是如果该按键不是低功耗模式 那就启动定时器 周期为5毫秒 关于这个定时器句柄可以提前告诉你这是个全局变量 
    //可以事先留意一下 你哥俩还会再见面 如果这是你喜欢的女孩 我现在告诉你 你俩还会再见面的 是不是幸福起来了
    if (!btn->enable_power_save) {
        esp_timer_start_periodic(g_button_timer_handle, TICKS_INTERVAL * 1000U);
        g_is_timer_running = true;
    }
    // 看是不是 把button_dev_t类型强转了 其中button_handle_t == void*   iot_button.h头文件可以看到
    return (button_handle_t)btn;
} 

小小总结一下 该函数实现功能有1.把config中的参数复制到btn中,2.对GPIO初始化,3.当个”甩手掌柜“把接下来的活外包给button_create_com()干,4.根据是否为低功耗模式 启动定时器或者配置中断

button_create_com()源码内容
/* 先看看参数
active_level 摁下按键的输入电平 
hal_get_key_state 这是一个函数指针 用来获取当前按键IO的输入电平
hardware_data 在GPIO模式中就是按键GPIO引脚
long_press_ticks 长按判断最短滴答数
short_press_ticks 连按判断最长滴答数
*/
static button_dev_t *button_create_com(uint8_t active_level, uint8_t (*hal_get_key_state)(void *hardware_data), void *hardware_data, uint16_t long_press_ticks, uint16_t short_press_ticks)
{
    BTN_CHECK(NULL != hal_get_key_state, "Function pointer is invalid", NULL);

    // 都是用来给btn赋值的内容 根据变量名字还是很容易看出来是干嘛的
    button_dev_t *btn = (button_dev_t *) calloc(1, sizeof(button_dev_t));
    BTN_CHECK(NULL != btn, "Button memory alloc failed", NULL);
    btn->hardware_data = hardware_data;
    btn->event = BUTTON_NONE_PRESS;
    btn->active_level = active_level;
    btn->hal_button_Level = hal_get_key_state;
    btn->button_level = !active_level;
    btn->long_press_ticks = long_press_ticks;
    // btn->long_press_ticks_default = btn->long_press_ticks;
    btn->short_press_ticks = short_press_ticks;

    // 这就是一个单链表 先注册的按键排到链表最后 
    // g_head_handle这是一个全局变量 表示链表的头指针
    btn->next = g_head_handle;
    g_head_handle = btn;

    // g_button_timer_handle是一个全局变量初始值为NULL(是不是上面说的你潜对象(潜在的对象)) if代码块内容是配置一个定时器
   	// 执行一次之后g_button_timer_handle就不为空 所以如果有第二个按键注册 就不会在创造一个定时器对象
    if (!g_button_timer_handle) {
        esp_timer_create_args_t button_timer = {0};
        button_timer.arg = NULL;
        // 这个回调函数表示定时器周期执行的函数 button_cb就是用来检测所有按键事件的‘罪魁祸首’
        button_timer.callback = button_cb;
        button_timer.dispatch_method = ESP_TIMER_TASK;
        button_timer.name = "button_timer";
        esp_timer_create(&button_timer, &g_button_timer_handle);
    }

    return btn;
}

这个函数没啥好总结的 逻辑挺简单的不是 直接转战看看button_cb内容

button_cb()源码部分内容
// 删掉了关于低功耗的代码
static void button_cb(void *args)
{
    button_dev_t *target;
    // 循环访问链表内容 并且执行button_handler函数 根据上面定时器分析内容可以知道 
    // 每5毫秒就会for循环检测所有 注册在列车管理员 管理册中的按钮
    for (target = g_head_handle; target; target = target->next) {
        button_handler(target);
    }
}
button_handler()源码内容

先看一下都有那些按键事件,可以对比上面例程执行效果一起食用。

typedef enum {
    BUTTON_PRESS_DOWN = 0,		//按键摁下,只要按键摁下就会执行,具体电平体现为按键正常电平->按下电平(active_level)
    BUTTON_PRESS_UP,			//按键抬起,只要按键抬起就会执行,具体电平体现为按键按下电平->正常电平(!active_level
    BUTTON_PRESS_REPEAT,		//按键连按事件 只要按键出现连按就会执行 即按键连按次数大于一
    BUTTON_PRESS_REPEAT_DONE,	//按键连按结束事件 顾名思义
    BUTTON_SINGLE_CLICK,		//按键单击
    BUTTON_DOUBLE_CLICK,		//按键双击
    BUTTON_MULTIPLE_CLICK,		//按键多击 按键次数大于2
    BUTTON_LONG_PRESS_START,	//按键长按开始
    BUTTON_LONG_PRESS_HOLD,		//按键一直保持长按状态
    BUTTON_LONG_PRESS_UP,		//按键长按松开
    BUTTON_EVENT_MAX,			//用来表示按键事件数量
    BUTTON_NONE_PRESS,			//无按键事件 初始化事件
} button_event_t;

具体事件触发流程可看下图

在这里插入图片描述

胆小勿入,此函数有几段看不懂的地方 不知道是用来干嘛的,况且在正常使用库的情况下,代码也并没使用到(能力有限,分析不出来) 。写的时候,与上段话差了半个小时,又回去看了一下代码,感觉由于用户在设置参数时,判断按键长按时间可能不同,然后对于长摁开始事件可能不会执行,然后用来调整对应参数。由于我们设置按键的时候,使用的都是默认值,所以就没有什么影响。这里还是对那些代码进行删改,具体变动会在下面代码注释中体现。

该函数以状态机的形式实现了对函数事件的判断,具体状态的切换可看下图

在这里插入图片描述

状态0为最基础状态,是按键的开始也是归宿,像李白的一技能,你可以往任何地方戳,但最终都会回到他的影子处。当位于状态0时,若按键并没有被按下就无任何动作,当按键被按下后,触发BUTTON_PRESS_DOWN事件并进入状态1。

状态1为按键分水岭,因为这像李信切换光形态还是暗形态的技能,在状态1中根据按键动作进入不同状态,比如是连击还是长按。若按键抬起触发BUTTON_PRESS_UP事件并进入状态2,作为连击嫌疑状态。若按键一直长按超过长按判断时间long_press_ticks,则会触发BUTTON_LONG_PRESS_START事件并进入状态4.

状态2为连按循环判断事件和连按终结按键,要与状态3一起食用。若在连按最长允许时间short_press_ticks之内按下进入状态3并且触发BUTTON_PRESS_DOWNBUTTON_PRESS_REPEAT事件,若超过最长允许时间还没有检测到按键按下则根据repeat参数来判断按键一共连按了几次有单击BUTTON_SINGLE_CLICK,双击BUTTON_DOUBLE_CLICK,多击BUTTON_MULTIPLE_CLICK,然后结束连按判断重回状态0。

状态3为状态2辅助状态,用来与状态2来记录连按次数 当按键松开后触发BUTTON_PRESS_UP事件并进入状态2,可以通过上图查看状态2和状态3之间有个小循环,每次循环都会是按键连按次数加1。如果若按键一直处于长按状态,则在释放后会直接进入状态0。类比于人工操作就是先单击又迅速长按。

状态4为长按保持判断状态,若按键始终处于长按状态就每隔一段时间触发一次BUTTON_LONG_PRESS_HOLD事件,可以看到上图状态4本身有个小循环。若按键释放,则触发BUTTON_LONG_PRESS_UP事件和BUTTON_PRESS_UP事件最终回到状态0。

总结一下状态切换顺序

若是双击操作:状态0->状态1->状态2->状态3->状态2->状态0

若是长按操作:状态0->状态1->状态4->状态0

//button_cb_info_t结构体 btn->cb_info的存储内容,如何绑定回调函数将在下节介绍,这里只需明白其参数的内容即可。
typedef struct
{
    button_cb_t cb;					// 与事件绑定的回调函数
    void *usr_data;					// 绑定回调函数时用户传入的参数
    button_event_data_t event_data;	// 这个是我上文所说的看不懂其在干嘛的代码会用到的内容
} button_cb_info_t;

// 结合本节刚开始对button_dev_t的介绍不难看懂其如何调用与事件绑定的回调函数的操作
#define CALL_EVENT_CB(ev)                                                   \
    if (btn->cb_info[ev]) {                                                 \
        for (int i = 0; i < btn->size[ev]; i++) {                           \
            btn->cb_info[ev][i].cb(btn, btn->cb_info[ev][i].usr_data);      \
        }                                                                   \
    }   
static void button_handler(button_dev_t *btn)
{
    // 获取当前按键IO电平
    uint8_t read_gpio_level = btn->hal_button_Level(btn->hardware_data);

    // 滴答计数,这个在状态0不会增加,而在别的状态定时器每进一次中断就会加1,定时器中断周期为5ms
    // 也就是在别的状态每隔5ms就会加1,就可以当作时间变量来比较是否达到长按、连按时间要求
    if ((btn->state) > 0) {
        btn->ticks++;
    }

    // 按键消抖过滤 对于按键抖动表现为一段时间内按钮IO电平来回振荡,不能处于一个稳定的值
    // btn->button_level为上次按键稳定值 DEBOUNCE_TICKS为设定的总消抖次数 btn->debounce_cnt为记录消抖次数的变量
    // 也就是需要该按键保持与上次按键稳定电平不同的值一段时间,才认为按键电平发生变化,若有一次出现同电平,则全部作废。
    // 这个按键消抖不仅针对按键按下也对按键释放进行消抖。因为两次操作IO电平都发生了变化,只是方向不一样。
    if (read_gpio_level != btn->button_level) {
        if (++(btn->debounce_cnt) >= DEBOUNCE_TICKS) {
            btn->button_level = read_gpio_level;
            btn->debounce_cnt = 0;
        }
    } else {
        btn->debounce_cnt = 0;
    }

    /** State machine */
    switch (btn->state) {
    case 0://状态0
        if (btn->button_level == btn->active_level) { 	// 如果当前按钮IO稳定电平为按键按下时的活跃电平 也就是按键按下
            btn->event = (uint8_t)BUTTON_PRESS_DOWN;  	// 就触发BUTTON_PRESS_DOWN事件回调函数
            CALL_EVENT_CB(BUTTON_PRESS_DOWN);			// 这个上面就有 要不你在看一看
            btn->ticks = 0;								// 清0滴答数 所以这个东西不是一直加的 它状态切换后会清0的
            btn->repeat = 1;							// 认为按键已经按下1次了 毕竟你确实按下了!
            btn->state = 1;								// 切换至状态1
        } else {
            btn->event = (uint8_t)BUTTON_NONE_PRESS;	// 若按键没按下 就无事发生
        }
        break;

    case 1://状态1 此时滴答数btn->ticks每隔5ms加1 因为button_handler 5ms调用一次 别的状态同理
        if (btn->button_level != btn->active_level) {		// 此时按键IO电平不与active_level相同,说明按键释放
            btn->event = (uint8_t)BUTTON_PRESS_UP;			// 就触发BUTTON_PRESS_UP事件回调函数
            CALL_EVENT_CB(BUTTON_PRESS_UP);
            btn->ticks = 0;									// 清0滴答数
            btn->state = 2;									// 切换至状态2
        } else if (btn->ticks > btn->long_press_ticks) { 	// 若按键一直按着 使滴答数超过长按判断时间 就进入长按事件
            btn->event = (uint8_t)BUTTON_LONG_PRESS_START;	// 这里我进行了修改 反正对于同一配置按键 效果是一样的
            CALL_EVENT_CB(BUTTON_LONG_PRESS_START);			// 源代码很长 如果感兴趣可以看下源码 有想法一定要告诉我!
            btn->state = 4;									// 切换至状态4
        }
        break;

    case 2://状态2
        if (btn->button_level == btn->active_level) {		// 按键又按下,则是个连击事件
            btn->event = (uint8_t)BUTTON_PRESS_DOWN;		
            CALL_EVENT_CB(BUTTON_PRESS_DOWN);				// 触发BUTTON_PRESS_DOWN事件回调函数
            btn->event = (uint8_t)BUTTON_PRESS_REPEAT;		
            btn->repeat++;									// 按键连按次数加1 毕竟你确实按下了
            CALL_EVENT_CB(BUTTON_PRESS_REPEAT);				// 触发BUTTON_PRESS_REPEAT事件回调函数
            btn->ticks = 0;									// 清0滴答数
            btn->state = 3;									// 切换至状态3
        } else if (btn->ticks > btn->short_press_ticks) {	// 若按键已经超过连按允许区间 则认为连按结束
            if (btn->repeat == 1) {							
                btn->event = (uint8_t)BUTTON_SINGLE_CLICK;	// 若连按次数为1 则触发BUTTON_SINGLE_CLICK事件
                CALL_EVENT_CB(BUTTON_SINGLE_CLICK);
            } else if (btn->repeat == 2) {
                btn->event = (uint8_t)BUTTON_DOUBLE_CLICK;
                CALL_EVENT_CB(BUTTON_DOUBLE_CLICK);			// 若连按次数为2 则触发BUTTON_DOUBLE_CLICK事件
            } else if ((btn->repeat >= 2){
                btn->event = (uint8_t)BUTTON_MULTIPLE_CLICK;
                CALL_EVENT_CB(BUTTON_MULTIPLE_CLICK);		// 若连按次数大于2 则触发BUTTON_MULTIPLE_CLICK事件
            }
           													// 这里也进行了修改 因为源代码判断不了超过2次连按情况	
                       										// 且源代码还是很长 和上面一样 别担心下面还有
            btn->event = (uint8_t)BUTTON_PRESS_REPEAT_DONE;
            CALL_EVENT_CB(BUTTON_PRESS_REPEAT_DONE); 		// 触发BUTTON_PRESS_REPEAT_DONE事件回调函数 连按结束
            btn->repeat = 0;								// 清0连按次数
            btn->state = 0;									// 切换至状态0
        }
        break;

    case 3://状态3
        if (btn->button_level != btn->active_level) {		// 按键又抬起
            btn->event = (uint8_t)BUTTON_PRESS_UP;
            CALL_EVENT_CB(BUTTON_PRESS_UP);					// 触发BUTTON_PRESS_UP事件回调函数
            if (btn->ticks < SHORT_TICKS) {					// 若按键在合适区间内释放 认为是一次正常按键按下操作
                btn->ticks = 0;								// 清0滴答数
                btn->state = 2; 							// 切换回状态2 形成连按循环 每次循环都是一次短按
            } else {										// 若非正常区间释放 表现为按下后释放又长按
                btn->state = 0;								// 非法操作 直接退回状态0
            }
        }
        break;

    case 4://状态4
        if (btn->button_level == btn->active_level) {		// 按键一直处于长按状态
            // long_press_hold_cnt表示这是第几次保持长按触发 每隔SERIAL_TICKS滴答数触发一次保持长按事件
            // btn->long_press_ticks 表示滴答数要达到多少次才认为是长按事件 况且由长按状态1切换到状态4 滴答数没有清0
            // 结合下面代码是不是好理解多了
            if (btn->ticks >= (btn->long_press_hold_cnt + 1) * SERIAL_TICKS + btn->long_press_ticks) {
                btn->event = (uint8_t)BUTTON_LONG_PRESS_HOLD;
                btn->long_press_hold_cnt++;					
                CALL_EVENT_CB(BUTTON_LONG_PRESS_HOLD);		// 触发BUTTON_LONG_PRESS_HOLD事件回调函数
                											// 这里也删除了很多代码 可以对比源代码 但功能是一样的
            }
        } else {											// 按键释放 表示长按结束
			btn->event = (uint8_t)BUTTON_LONG_PRESS_UP;
            CALL_EVENT_CB(BUTTON_LONG_PRESS_UP);			// 触发BUTTON_LONG_PRESS_UP事件回调函数
          	btn->event = (uint8_t)BUTTON_PRESS_UP;
            CALL_EVENT_CB(BUTTON_PRESS_UP);					// 触发BUTTON_PRESS_UP事件回调函数
            btn->state = 0; 								// 切换至状态0
            btn->long_press_hold_cnt = 0;					// 清0长按保持记录变量
        }
        break;
    }
}

ok! 这破代码终于分析完了,虽然与源代码有些出入,但你真的要信我,功能完全一致。如果有问题一定要告诉我!!我自己写完读着都费劲,希望看到这里的你别骂我写的是什么够使。。

🍙ⅱ.以iot_button_register_cb结束吧

通过对关于该库的使用那一节,可以知道该函数是用来绑定事件回调函数的,毕竟这个才是我们写了这么多,想实现的事情。我们成熟的按键,不仅有单击单一的功能,我们算法与时间做朋友,充分榨干了按键,有了双击,长按事件。那如何给这些事件绑定回调函数呐,请随我一同前往,看向代码的方向!

iot_button_register_cb内容

其实这个代码内容,如果你看懂了button_dev_t结构体内容,真的很清晰

esp_err_t iot_button_register_cb(button_handle_t btn_handle, button_event_t event, button_cb_t cb, void *usr_data)
{
    // 检测btn_handle是否为空
    BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", ESP_ERR_INVALID_ARG);
    button_dev_t *btn = (button_dev_t *)btn_handle;
    // 这个源代码是取消注释的 但是如果取消注释 绑定与BUTTON_MULTIPLE_CLICK有关的回调函数 就会出错
    // BTN_CHECK(event != BUTTON_MULTIPLE_CLICK, "event argument is invalid", ESP_ERR_INVALID_ARG);
    // 后面这一堆 不太清楚 它要干嘛 只在上文我所说 button_handler 状态机处理函数中使用 但已经被我删除了 如果不是跳着看
    // 你应该留意到了
    button_event_config_t event_cfg = {
        .event = event,
    };

    if ((event == BUTTON_LONG_PRESS_START || event == BUTTON_LONG_PRESS_UP) && !event_cfg.event_data.long_press.press_time)
    {
        event_cfg.event_data.long_press.press_time = btn->long_press_ticks_default * TICKS_INTERVAL;
    }
	// 直接下一个 看这个函数了 最后一个 也把与event_cfg有关的代码删除了一些
    return iot_button_register_event_cb(btn_handle, event_cfg, cb, usr_data);
}
iot_button_register_event_cb内容

已把与event_cfg有关的代码删除 但保证依旧可用 详请可查看源码

esp_err_t iot_button_register_event_cb(button_handle_t btn_handle, button_event_config_t event_cfg, button_cb_t cb, void *usr_data)
{
    BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", ESP_ERR_INVALID_ARG);
    button_dev_t *btn = (button_dev_t *)btn_handle;
    button_event_t event = event_cfg.event;
    // 判断输入事件 是否超过总事件
    BTN_CHECK(event < BUTTON_EVENT_MAX, "event is invalid", ESP_ERR_INVALID_ARG);

    // 如果这是该事件第一次绑定回调函数 存储数组初始值为NULL 则会为其分配内存
    if (!btn->cb_info[event])
    {
        // 分配内存
        btn->cb_info[event] = calloc(1, sizeof(button_cb_info_t));
        BTN_CHECK(NULL != btn->cb_info[event], "calloc cb_info failed", ESP_ERR_NO_MEM);
    }
    // 表示这不是该事件第一次绑定回调函数 需要对原来分配的内容空间大小进行修改扩充
    else
    {
         // 增大内存 但返回的指针内容保持不变
        button_cb_info_t *p = realloc(btn->cb_info[event], sizeof(button_cb_info_t) * (btn->size[event] + 1));
        BTN_CHECK(NULL != p, "realloc cb_info failed", ESP_ERR_NO_MEM);
        btn->cb_info[event] = p;
    }
	// 为新分配的cb_info赋值,这时侯就是注册回调函数成功了
    btn->cb_info[event][btn->size[event]].cb = cb;
    btn->cb_info[event][btn->size[event]].usr_data = usr_data;
    // 因为该事件新增了一个回调函数 对应的表示回调函数数量的变量 加1
    btn->size[event]++;
    return ESP_OK;
}
🍚 ⅲ 还有 你说你叫彩蛋…

关于该库的主要经常使用代码已经分析完了,如果还是模模糊糊的可以对比源码掌握button_dev_t结构体内容

本次彩蛋由该库别的函数冠名播出,他们虽不经常使用,但它在,我就跑不掉!

esp_err_t iot_button_delete(button_handle_t btn_handle):删除按键,里面代码可以对比iot_button_delete 无非是回收回调函数内存,从链表中释放btn_handle,如果是最后一个把定时器也释放了。其实可以自己分析一下源码,看看是怎么收尾的,反向对比学习当初iot_button_create是怎么实例化一个按键对象的。

esp_err_t iot_button_register_event_cb(button_handle_t btn_handle, button_event_config_t event_cfg, button_cb_t cb, void *usr_data):注销按钮事件回调函数

esp_err_t iot_button_unregister_event(button_handle_t btn_handle, button_event_config_t event_cfg, button_cb_t cb):注销与按钮事件相关的所有回调函数

size_t iot_button_count_cb(button_handle_t btn_handle):统计注册该按键的所有回调函数,这个源码真的简单,0基础也得会啊。就是操作button_dev_t结构体

size_t iot_button_count_event(button_handle_t btn_handle, button_event_t event):统计注册该按键事件的所有回调函数

button_event_t iot_button_get_event(button_handle_t btn_handle):获取当前按键事件,就是返回btn->event

uint8_t iot_button_get_repeat(button_handle_t btn_handle):获取按键连按次数值,就是返回btn->repeat

uint16_t iot_button_get_ticks_time(button_handle_t btn_handle):获取按键滴答时间,就是返回btn->tick和每滴答是多长时间的乘积

uint16_t iot_button_get_long_press_hold_cnt(button_handle_t btn_handle):获取长按保持事件触发次数 就是返回btn->long_press_hold_cnt

uint8_t iot_button_get_key_level(button_handle_t btn_handle):获取当前按键IO电平

esp_err_t iot_button_resume(void):恢复按键检测,恢复的是定时器

esp_err_t iot_button_stop(void):关闭按键检测,由于关闭的是定时器,所以是关闭所有的按键检测。

😃3.写一个自己的库

看到这里,对于该库的分析已经完全结束了。写这一节的内容也是为了不忘初心,因为当初也是想自己写一个单按键多功能的库,但能力有限,只好拾人牙慧,先看看成熟的一套代码是如何布局的,算法逻辑是如何设计的,所以就有了本节。虽然如此,可能整体写下来不过是此库的简略版而已。

算了没什么好看的,就是复制粘贴 加删除一些代码,说是我写的,有点臭不要脸,就写到这里吧。

2024年2月6日 18:13

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值