ESP-ADF外设子系统深度解析:esp_peripherals组件架构与核心设计(输入类外设之按键Button)
版本信息: ESP-ADF v2.7-65-gcf908721
简介
本文档详细分析ESP-ADF中的输入类外设实现机制,包括按键(button)、触摸(touch)和ADC按键(adc_button)等输入类外设的设计模式、接口规范、初始化流程和事件处理机制。ESP-ADF输入类外设基于统一的外设框架设计,通过事件驱动模型实现用户输入的检测和处理,为应用程序提供了灵活且易用的输入接口。
模块概述
功能定义
ESP-ADF输入类外设主要负责检测和处理用户的物理输入操作,将物理信号转换为应用程序可处理的事件。主要功能包括:
- 物理输入信号检测(按键按下/释放、触摸触发/释放、ADC电平变化等)
- 输入事件生成(短按、长按、触摸等事件)
- 事件过滤和防抖处理
- 向应用程序传递输入事件
架构位置
输入类外设是ESP-ADF外设子系统的重要组成部分,位于硬件驱动层和应用层之间:
核心特性
- 多种输入类型支持:支持GPIO按键、电容触摸、ADC按键等多种输入方式
- 统一事件模型:所有输入外设使用统一的事件模型和接口
- 丰富的事件类型:支持按下、释放、长按、长按释放等多种事件类型
- 防抖处理:内置输入信号防抖处理机制
- 可配置参数:支持灵活配置长按时间、触发阈值等参数
- 中断和轮询结合:结合中断和定时器轮询提高响应速度和可靠性
按键(Button)外设
按键外设概述
按键外设基于GPIO实现,支持多个按键同时使用,可以检测按下、释放、长按等多种按键事件。按键外设通过中断和定时器相结合的方式检测按键状态变化,并生成相应的事件。
按键外设实现分为两个层次:
-
外设层:负责将按键集成到ESP-ADF外设系统中,处理事件分发和生命周期管理。
- 头文件:
components/esp_peripherals/include/periph_button.h
- 实现文件:
components/esp_peripherals/periph_button.c
- 头文件:
-
底层驱动层:提供底层按键驱动,负责GPIO配置、中断处理和按键状态检测。
- 头文件:
components/esp_peripherals/lib/button/button.h
- 实现文件:
components/esp_peripherals/lib/button/button.c
- 头文件:
按键外设层次架构图
按键外设API和数据结构
外设层API
源文件:components/esp_peripherals/include/periph_button.h
和components/esp_peripherals/periph_button.c
公共API
// 按键外设初始化函数
esp_periph_handle_t periph_button_init(periph_button_cfg_t* but_cfg);// 按键配置结构体
typedef struct {uint64_t gpio_mask; // GPIO掩码,使用BIT(GPIO_NUM)表示,如GPIO_SEL_36int long_press_time_ms; // 长按时间阈值,默认为2000ms
} periph_button_cfg_t;// 按键事件类型
typedef enum {PERIPH_BUTTON_UNCHANGE = 0, // 无事件PERIPH_BUTTON_PRESSED, // 按键按下PERIPH_BUTTON_RELEASE, // 按键释放PERIPH_BUTTON_LONG_PRESSED, // 长按PERIPH_BUTTON_LONG_RELEASE, // 长按后释放
} periph_button_event_id_t;
内部数据结构
// 按键外设内部结构体 (定义在periph_button.c中)
typedef struct {esp_button_handle_t btn; // 底层按键驱动句柄uint64_t gpio_mask; // GPIO掩码int long_press_time_ms; // 长按时间阈值
} periph_button_t;// 按键结果结构体 (定义在底层按键驱动中)
typedef struct {uint64_t press_mask; // 按下的按键掩码uint64_t release_mask; // 释放的按键掩码uint64_t long_press_mask; // 长按的按键掩码uint64_t long_release_mask; // 长按后释放的按键掩码
} button_result_t;
底层按键驱动API
源文件:components/esp_peripherals/lib/button/button.h
和components/esp_peripherals/lib/button/button.c
公开API (button.h)
// 按键状态枚举
typedef enum {BTN_UNCHANGE = 0,BTN_PRESSED,BTN_RELEASE,BTN_LONG_PRESS,BTN_LONG_RELEASE,
} button_status_t;// 按键结果结构体
typedef struct {uint64_t press_mask;uint64_t release_mask;uint64_t long_press_mask;uint64_t long_release_mask;
} button_result_t;// 按键驱动句柄
typedef struct esp_button *esp_button_handle_t;// GPIO中断处理函数类型
typedef void (*gpio_intr_handler)(void *);// 按键配置结构体
typedef struct {int long_press_time_ms; // 长按时间阈值uint64_t gpio_mask; // GPIO掩码gpio_intr_handler button_intr_handler; // 中断处理函数void *intr_context; // 中断上下文
} button_config_t;// 默认长按时间
#define DEFAULT_LONG_PRESS_TIME_MS (2*1000)// 初始化按键驱动
esp_button_handle_t button_init(button_config_t *config);// 读取按键状态
bool button_read(esp_button_handle_t button, button_result_t *result);// 销毁按键驱动
esp_err_t button_destroy(esp_button_handle_t button);
内部实现 (button.c)
// 按键项目结构体(单个按键,内部实现)
typedef struct esp_button_item {int gpio_num; // GPIO编号long long last_press_tick; // 上次按下时间bool long_pressed; // 长按标志STAILQ_ENTRY(esp_button_item) entry; // 链表项
} esp_button_item_t;// 按键控制结构体(内部实现)
struct esp_button {int long_press_time_ms; // 长按时间阈值uint64_t gpio_mask; // GPIO掩码STAILQ_HEAD(esp_button_list, esp_button_item) btn_list; // 按键链表
};// 获取按键状态(内部函数)
static button_status_t button_get_state(esp_button_handle_t button, esp_button_item_t *btn_item);
按键外设配置选项
- gpio_mask: 指定使用的GPIO引脚,可以同时配置多个引脚
- long_press_time_ms: 长按时间阈值,默认为2000ms
底层驱动配置选项
- long_press_time_ms: 长按时间阈值,与外设层相同
- gpio_mask: 指定使用的GPIO引脚掩码,与外设层相同
- button_intr_handler: GPIO中断处理函数,当GPIO状态变化时调用
- intr_context: 中断处理函数的上下文参数,通常传入外设句柄
注意:外设层的配置选项会传递给底层驱动,并自动设置中断处理函数和上下文参数。应用程序只需要配置外设层的参数即可。
按键外设初始化流程
按键外设的初始化流程涉及两个层次:外设层(Peripheral Layer)和底层驱动(Driver Layer)。下面分别介绍这两个层次的初始化过程。
外设层初始化过程(periph_button.c)
外设层初始化主要通过periph_button_init
函数(位于periph_button.c
)完成,主要包括以下步骤:
- 创建外设句柄:调用
esp_periph_create
函数创建外设句柄 - 分配内部数据结构:分配
periph_button_t
结构体内存 - 设置配置参数:设置GPIO掩码和长按时间阈值
- 注册回调函数:设置初始化、运行和销毁回调函数
// 文件:components/esp_peripherals/periph_button.c
esp_periph_handle_t periph_button_init(periph_button_cfg_t *config)
{// 1. 创建外设句柄esp_periph_handle_t periph = esp_periph_create(PERIPH_ID_BUTTON, "periph_btn");AUDIO_MEM_CHECK(TAG, periph, return NULL);// 2. 分配内部数据结构periph_button_t *periph_btn = audio_calloc(1, sizeof(periph_button_t));AUDIO_MEM_CHECK(TAG, periph_btn, {audio_free(periph);return NULL;});// 3. 设置配置参数periph_btn->gpio_mask = config->gpio_mask;periph_btn->long_press_time_ms = config->long_press_time_ms;// 4. 注册回调函数esp_periph_set_data(periph, periph_btn);esp_periph_set_function(periph, _button_init, _button_run, _button_destroy);return periph;
}
当外设被添加到外设集合并启动时,会调用_button_init
函数(位于periph_button.c
),该函数负责初始化底层按键驱动并启动定时器:
// 文件:components/esp_peripherals/periph_button.c
static esp_err_t _button_init(esp_periph_handle_t self)
{// 验证按键外设VALIDATE_BTN(self, ESP_FAIL);// 获取按键外设数据periph_button_t *periph_btn = esp_periph_get_data(self);// 准备底层驱动配置button_config_t btn_config = {.gpio_mask = periph_btn->gpio_mask,.long_press_time_ms = periph_btn->long_press_time_ms,.button_intr_handler = button_intr_handler,.intr_context = self,};// 调用底层驱动初始化函数periph_btn->btn = button_init(&btn_config);// 启动定时器用于按键状态检测esp_periph_start_timer(self, 50/portTICK_RATE_MS, button_timer_handler);return ESP_OK;
}
底层驱动初始化过程(button.c)
底层按键驱动初始化通过button_init
函数(位于button.c
)完成,主要包括以下步骤:
- 分配按键驱动结构体:分配
esp_button
结构体内存 - 设置按键参数:设置GPIO掩码和长按时间阈值
- 配置GPIO:设置GPIO为输入模式,启用上拉电阻,设置中断类型
- 初始化按键列表:初始化按键项目链表
- 为每个GPIO创建按键项:遍历GPIO掩码,为每个启用的GPIO创建按键项
- 配置中断处理:如果提供了中断处理函数,则为每个GPIO配置中断
// 文件:components/esp_peripherals/lib/button/button.c
esp_button_handle_t button_init(button_config_t *config)
{// 1. 分配按键驱动结构体esp_button_handle_t btn = audio_calloc(1, sizeof(struct esp_button));AUDIO_MEM_CHECK(TAG, btn, return NULL);// 验证GPIO掩码if (config->gpio_mask <= 0) {ESP_LOGE(TAG, "required at least 1 gpio");return NULL;}// 2. 设置按键参数btn->gpio_mask = config->gpio_mask;btn->long_press_time_ms = config->long_press_time_ms;if (btn->long_press_time_ms == 0) {btn->long_press_time_ms = DEFAULT_LONG_PRESS_TIME_MS;}// 3. 配置GPIOgpio_config_t gpiocfg = {.pin_bit_mask = btn->gpio_mask,.mode = GPIO_MODE_INPUT,.pull_up_en = GPIO_PULLUP_ENABLE,.pull_down_en = GPIO_PULLDOWN_DISABLE,.intr_type = GPIO_INTR_ANYEDGE,};gpio_config(&gpiocfg);uint64_t gpio_mask = btn->gpio_mask;int gpio_num = 0;// 4. 初始化按键列表STAILQ_INIT(&btn->btn_list);// 5. 为每个GPIO创建按键项while (gpio_mask) {if (gpio_mask & 0x01) {ESP_LOGD(TAG, "Mask = %llx, current_mask = %llx, idx=%d", btn->gpio_mask, gpio_mask, gpio_num);// 分配按键项内存esp_button_item_t *new_btn = audio_calloc(1, sizeof(esp_button_item_t));AUDIO_MEM_CHECK(TAG, new_btn, {button_destroy(btn);return NULL;});new_btn->gpio_num = gpio_num;// 6. 配置中断处理if (config->button_intr_handler) {gpio_set_intr_type(gpio_num, GPIO_INTR_ANYEDGE);gpio_isr_handler_add(gpio_num, config->button_intr_handler, config->intr_context);gpio_intr_enable(gpio_num);}// 将按键项添加到列表STAILQ_INSERT_TAIL(&btn->btn_list, new_btn, entry);}gpio_mask >>= 1;gpio_num ++;}return btn;
}
按键外设完整初始化时序图
下图展示了按键外设从应用程序调用到底层驱动完成初始化的完整流程:
按键外设销毁流程
按键外设的销毁流程同样涉及两个层次:外设层(Peripheral Layer)和底层驱动(Driver Layer)。下面分别介绍这两个层次的销毁过程。
外设层销毁过程(periph_button.c)
外设层销毁主要通过_button_destroy
函数(位于periph_button.c
)完成,主要包括以下步骤:
- 获取外设数据:获取按键外设的内部数据结构
- 停止定时器:停止按键状态检测定时器
- 释放底层资源:调用底层驱动的销毁函数释放资源
- 释放内部数据结构:释放按键外设的内部数据结构
// 文件:components/esp_peripherals/periph_button.c
static esp_err_t _button_destroy(esp_periph_handle_t self)
{// 1. 获取按键外设数据periph_button_t *periph_btn = esp_periph_get_data(self);// 2. 停止定时器esp_periph_stop_timer(self);// 3. 释放底层按键驱动资源button_destroy(periph_btn->btn);// 4. 释放按键外设数据结构audio_free(periph_btn);return ESP_OK;
}
底层驱动销毁过程(button.c)
底层按键驱动销毁通过button_destroy
函数(位于button.c
)完成,主要包括以下步骤:
- 参数检查:检查按键驱动句柄是否有效
- 释放按键项资源:遍历按键列表,释放每个按键项的资源
- 释放驱动结构体:释放按键驱动结构体内存
// 文件:components/esp_peripherals/lib/button/button.c
esp_err_t button_destroy(esp_button_handle_t button)
{// 1. 参数检查AUDIO_NULL_CHECK(TAG, button, return ESP_FAIL);// 2. 释放按键项资源esp_button_item_t *item, *tmp;STAILQ_FOREACH_SAFE(item, &button->btn_list, entry, tmp) {if (item) {STAILQ_REMOVE(&button->btn_list, item, esp_button_item, entry);audio_free(item);}}// 3. 释放驱动结构体audio_free(button);return ESP_OK;
}
按键外设完整销毁时序图
下图展示了按键外设从应用程序调用到底层驱动完成销毁的完整流程:
按键检测算法
按键检测算法结合了中断和定时器轮询机制,涉及外设层(Peripheral Layer)和底层驱动(Driver Layer)两个层次。下面分别介绍这两个层次的实现。
外设层检测实现(periph_button.c)
外设层通过中断和定时器触发按键状态检测,并将按键事件分发到ESP-ADF的事件系统中:
- 中断处理:按键状态变化时触发GPIO中断,发送命令到外设任务
- 定时器处理:定期检查按键状态,防止中断丢失
- 状态读取:调用底层驱动读取按键状态
- 事件分发:将按键事件分发到ESP-ADF的事件系统
// 文件:components/esp_peripherals/periph_button.c
static void IRAM_ATTR button_intr_handler(void* param)
{// 从中断上下文发送命令到外设任务esp_periph_handle_t periph = (esp_periph_handle_t)param;esp_periph_send_cmd_from_isr(periph, 0, NULL, 0);
}static void button_timer_handler(xTimerHandle tmr)
{// 从定时器上下文发送命令到外设任务esp_periph_handle_t periph = (esp_periph_handle_t) pvTimerGetTimerID(tmr);esp_periph_send_cmd_from_isr(periph, 0, NULL, 0);
}static esp_err_t _button_run(esp_periph_handle_t self, audio_event_iface_msg_t *msg)
{// 读取按键状态并生成事件button_result_t result;periph_button_t *periph_btn = esp_periph_get_data(self);// 调用底层驱动读取按键状态if (button_read(periph_btn->btn, &result)) {ESP_LOGD(TAG, "Button event, press_mask %llx, release_mask: %llx, long_press_mask: %llx, long_release_mask: %llx",result.press_mask, result.release_mask, result.long_press_mask, result.long_release_mask);// 发送各类按键事件到ESP-ADF事件系统button_send_event(self, PERIPH_BUTTON_PRESSED, result.press_mask);button_send_event(self, PERIPH_BUTTON_RELEASE, result.release_mask);button_send_event(self, PERIPH_BUTTON_LONG_PRESSED, result.long_press_mask);button_send_event(self, PERIPH_BUTTON_LONG_RELEASE, result.long_release_mask);}return ESP_OK;
}
底层驱动检测实现(button.c)
底层按键驱动实现在button.c
中,负责具体的按键状态检测和事件生成:
- 状态判断:根据GPIO电平和按下时长判断按键状态
- 事件生成:生成按下、释放、长按、长按释放事件
- 结果汇总:将多个按键的状态汇总到结果结构体中
// 文件:components/esp_peripherals/lib/button/button.c
/*** @brief 获取按键当前状态* * 该函数是按键状态检测的核心,通过检测GPIO电平和按键按下时长,判断按键的当前状态。* 按键状态机如下:* 1. 初始状态:按键未按下,last_press_tick = 0* 2. 按下状态:检测到低电平且之前未按下,记录按下时间,返回BTN_PRESSED* 3. 短按释放:检测到高电平且按下时间小于长按阈值,返回BTN_RELEASE* 4. 长按状态:按下持续时间超过长按阈值,返回BTN_LONG_PRESS(只触发一次)* 5. 长按释放:长按后检测到高电平,返回BTN_LONG_RELEASE* * @param button 按键驱动句柄,包含长按时间阈值等配置* @param btn_item 单个按键项,包含GPIO编号和按下时间记录* @return button_status_t 返回按键状态(无变化/按下/释放/长按/长按释放)*/
static button_status_t button_get_state(esp_button_handle_t button, esp_button_item_t *btn_item)
{// 获取当前GPIO电平int level = gpio_get_level(btn_item->gpio_num);int active_level = 0; // 按下为低电平(按键接地时为有效)int deactive_level = 1; // 释放为高电平(通过上拉电阻保持高电平)// 情况1:按键刚被按下(从未按下状态变为按下状态)// 条件:last_press_tick为0(表示之前未按下)且当前为低电平(按下状态)if (btn_item->last_press_tick == 0 && level == active_level) {btn_item->last_press_tick = tick_get(); // 记录当前按下时间戳btn_item->long_pressed = false; // 重置长按标志return BTN_PRESSED; // 返回按下事件}// 情况2:长按后释放(按下时间超过阈值后释放)// 条件:当前为高电平(释放状态)且之前有按下记录且按下时间超过长按阈值if (level == deactive_level && btn_item->last_press_tick && tick_get() - btn_item->last_press_tick > button->long_press_time_ms) {btn_item->last_press_tick = 0; // 清除按下时间记录btn_item->long_pressed = false; // 重置长按标志return BTN_LONG_RELEASE; // 返回长按释放事件}// 情况3:短按释放(按下时间未超过阈值就释放)// 条件:当前为高电平(释放状态)且之前有按下记录if (level == deactive_level && btn_item->last_press_tick) {btn_item->last_press_tick = 0; // 清除按下时间记录btn_item->long_pressed = false; // 重置长按标志return BTN_RELEASE; // 返回释放事件}// 情况4:长按事件(按下持续时间超过阈值)// 条件:长按标志为false(确保长按事件只触发一次)且当前为低电平(按下状态)// 且按下时间超过长按阈值if (btn_item->long_pressed == false && level == active_level && tick_get() - btn_item->last_press_tick > button->long_press_time_ms) {btn_item->long_pressed = true; // 设置长按标志,防止重复触发return BTN_LONG_PRESS; // 返回长按事件}// 情况5:无状态变化(继续保持之前的状态)return BTN_UNCHANGE;
}/*** @brief 读取所有按键的状态并汇总结果* * 该函数遍历所有注册的按键,获取每个按键的当前状态,并将状态信息汇总到结果结构体中。* 每种按键状态(按下、释放、长按、长按释放)都使用一个位掩码来表示,掩码中的每一位* 对应一个GPIO编号。例如,如果GPIO 5按下,则press_mask的第5位会被设置为1。* * 工作流程:* 1. 清空结果结构体,准备接收新的状态信息* 2. 遍历按键链表中的每个按键项* 3. 调用button_get_state获取单个按键的状态* 4. 根据返回的状态,设置对应掩码中的相应位* 5. 如果至少有一个按键状态发生变化,返回true* * @param button 按键驱动句柄,包含按键链表和配置信息* @param result 输出参数,用于存储汇总的按键状态信息* @return bool 如果有任何按键状态发生变化返回true,否则返回false*/
bool button_read(esp_button_handle_t button, button_result_t *result)
{esp_button_item_t *btn_item; // 当前处理的按键项button_status_t btn_status; // 按键状态bool changed = false; // 状态变化标志// 步骤1: 清空结果结构体,确保没有旧状态信息memset(result, 0, sizeof(button_result_t));uint64_t tmp; // 临时变量,用于构建掩码位// 步骤2: 遍历所有按键项STAILQ_FOREACH(btn_item, &button->btn_list, entry) {// 步骤3: 获取当前按键状态btn_status = button_get_state(button, btn_item);// 步骤4: 根据状态设置对应的掩码位switch (btn_status) {case BTN_UNCHANGE:// 无状态变化,不需要设置掩码break;case BTN_PRESSED:// 按键按下状态changed = true; // 标记有状态变化tmp = 0x01; // 准备掩码位tmp <<= btn_item->gpio_num; // 将位移动到对应GPIO位置result->press_mask |= tmp; // 设置按下掩码的对应位break;case BTN_RELEASE:// 按键释放状态(短按后释放)changed = true;tmp = 0x01;tmp <<= btn_item->gpio_num;result->release_mask |= tmp; // 设置释放掩码的对应位break;case BTN_LONG_RELEASE:// 长按后释放状态changed = true;tmp = 0x01;tmp <<= btn_item->gpio_num;result->long_release_mask |= tmp; // 设置长按释放掩码的对应位break;case BTN_LONG_PRESS:// 长按状态changed = true;tmp = 0x01;tmp <<= btn_item->gpio_num;result->long_press_mask |= tmp; // 设置长按掩码的对应位break;}}// 步骤5: 返回是否有状态变化// 如果任何按键的状态发生了变化,changed会被设置为truereturn changed;
}
按键状态转换时序图
下图展示了按键状态的转换过程,包括按下、释放、长按和长按释放的完整状态流转:
按键状态检测时间轴图
下图展示了按键状态随时间变化的过程,包括不同按键操作的时间轴:
按键检测流程时序图
下图展示了按键检测的完整流程,包括中断触发、定时器轮询和状态判断:
按键事件处理
按键外设产生以下事件类型:
- PERIPH_BUTTON_PRESSED:按键按下事件
- PERIPH_BUTTON_RELEASE:按键释放事件
- PERIPH_BUTTON_LONG_PRESSED:按键长按事件
- PERIPH_BUTTON_LONG_RELEASE:长按后释放事件
事件数据为按键对应的GPIO编号。
按键外设使用示例
#include "esp_peripherals.h"
#include "periph_button.h"void app_main()
{// 初始化外设管理器esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG();esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg);// 配置按键periph_button_cfg_t btn_cfg = {.gpio_mask = GPIO_SEL_36 | GPIO_SEL_39, // 使用GPIO36和GPIO39作为按键.long_press_time_ms = 2000, // 长按阈值2秒};// 初始化按键外设并添加到外设集合esp_periph_handle_t button_handle = periph_button_init(&btn_cfg);esp_periph_start(button_handle);esp_periph_set_add_periph(set, button_handle);// 注册事件回调esp_periph_set_register_callback(set, button_event_callback, NULL);// 主循环while (1) {vTaskDelay(1000 / portTICK_RATE_MS);}
}// 按键事件回调函数
static esp_err_t button_event_callback(audio_event_iface_msg_t *event, void *context)
{switch (event->source_type) {case PERIPH_ID_BUTTON:if (event->cmd == PERIPH_BUTTON_PRESSED) {printf("Button %d pressed\n", (int)event->data);} else if (event->cmd == PERIPH_BUTTON_RELEASE) {printf("Button %d released\n", (int)event->data);} else if (event->cmd == PERIPH_BUTTON_LONG_PRESSED) {printf("Button %d long pressed\n", (int)event->data);} else if (event->cmd == PERIPH_BUTTON_LONG_RELEASE) {printf("Button %d long released\n", (int)event->data);}break;}return ESP_OK;
}