ESP-IoT-Solution 按钮组件实战指南:GPIO / ADC / 矩阵按键的创建、事件回调与低功耗设计
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
本文以 ESP-IoT-Solution 开源仓库中的 按钮组件官方文档 为核心,结合 iot_button.c、iot_button.h 等源码,系统讲解 ESP32 平台上 GPIO 按键、ADC 分压按键与矩阵键盘的硬件设计差异、事件触发机制、回调注册与轮询查询两种使用方式、可配置参数,以及面向 Light Sleep 场景的低功耗按键模式。读完本文,你将能够根据实际 IO 资源和功耗需求,为你的 ESP32 项目选型并落地一套完整的按键检测方案。
组件概述与按键类型选型
ESP-IoT-Solution 的 Button 组件(仓库路径 components/button)是一个通用的按键驱动库,它同时支持GPIO 模式与ADC 模式,并且允许在同一时间创建两种不同类型的按键。除了这两种官方文档中重点介绍的类型外,从 button_matrix.h 还可以看到组件额外支持矩阵键盘(Matrix Keypad),且接口层 button_interface.h 允许用户自定义驱动接入任意按键硬件。
两种按键的硬件差异决定了它们的适用场景:
- GPIO 按键:每个按键独立占用一个 IO 口,按键之间互不影响,稳定性高。缺点是当按键数量增多时,会占用过多的 IO 资源,适合按键数量少、对稳定性要求高的场景。
- ADC 按键:一个 ADC 通道通过分压电阻网络可以共享多个按键,显著节省 IO 资源。缺点是无法同时按下多个按键(同一通道上只能识别单键),且随着使用时间增长,按键触点氧化会导致接触电阻增大,从而引起电压漂移,稳定性下降。适合 IO 资源紧张、按键需求较多的场景。
硬件设计注意事项
在绘制按键电路时,需要特别留意两点(见原文档的 note 说明):
- GPIO 按键的上下拉电阻:芯片内部的上下拉电阻默认会启用。但对于仅支持输入功能的 IO 引脚,其内部并不存在该电阻,外部电路必须自行连接上下拉电阻,否则按键电平将处于不确定状态。
- ADC 按键的电压范围:按键分压后送入 ADC 引脚的电压不能超过 ADC 的量程,否则无法正确区分各个按键对应的电压区间,甚至可能损坏引脚。
按键事件与触发条件
组件通过一个按键状态机(见 iot_button.c 中的button_handler())对按键进行扫描,并将按键行为抽象为一系列事件。下表列出所有事件及其触发条件:
| 事件 | 触发条件 |
|---|---|
| BUTTON_PRESS_DOWN | 按下瞬间 |
| BUTTON_PRESS_UP | 松开瞬间 |
| BUTTON_PRESS_REPEAT | 按下并松开 ≥ 2 次 |
| BUTTON_PRESS_REPEAT_DONE | 重复按压结束 |
| BUTTON_SINGLE_CLICK | 按下并松开一次(单击) |
| BUTTON_DOUBLE_CLICK | 按下并松开两次(双击) |
| BUTTON_MULTIPLE_CLICK | 按下并松开 N 次,达到指定次数时触发 |
| BUTTON_LONG_PRESS_START | 按住达到设定时长的那一刻 |
| BUTTON_LONG_PRESS_HOLD | 长按过程中持续触发 |
| BUTTON_LONG_PRESS_UP | 长按后松开 |
| BUTTON_PRESS_END | 当前一次按键检测流程结束 |
这些事件定义在 button_types.h 同目录下的头文件(枚举button_event_t)中,并在 iot_button.c 中维护了与之一一对应的字符串表,可通过iot_button_get_event_str()获取事件的字符串表示,方便日志输出与调试。
从状态机实现可以看到事件间的协作关系:按下触发BUTTON_PRESS_DOWN后进入长按判定窗口,若在long_press_ticks内未松开则触发BUTTON_LONG_PRESS_START,随后在BUTTON_LONG_PRESS_HOLD_SERIAL_TIME_MS间隔下周期性触发BUTTON_LONG_PRESS_HOLD;若在短按有效时间内松开,则根据累计按压次数(repeat计数)依次判定单击、双击或多次点击,最终以BUTTON_PRESS_END收尾并回到初始状态。
回调模式与轮询模式
每个按键同时支持回调(Call-back)与轮询(Pooling)两种事件获取方式:
- 回调模式:为按键的每个事件注册独立的回调函数,事件发生时由组件自动调用。该方式效率高、实时性好,事件不会丢失,适合对响应实时性有要求的场景。
- 轮询模式:在程序中周期性调用
iot_button_get_event()查询当前按键事件。该方式使用简单,适合任务简单的场合;但由于是定时查询,部分按键事件可能无法被及时捕获,存在漏事件的风险。
两种方式可以同时组合使用。需要特别注意的是,回调函数中不允许执行阻塞操作(如 TaskDelay 等),否则会阻塞按钮组件的定时扫描任务,影响后续所有按键的事件处理。
可配置参数(Kconfig)
组件的核心时序参数全部通过 Kconfig 配置(见 components/button/Kconfig),各项参数的含义、取值范围与默认值如下:
| 配置项 | 含义 | 取值范围 | 默认值 |
|---|---|---|---|
| BUTTON_PERIOD_TIME_MS | 扫描周期 | 2 ~ 500 ms | 5 ms |
| BUTTON_DEBOUNCE_TICKS | 消抖时间(以扫描周期为单位) | 1 ~ 7 | 2 |
| BUTTON_SHORT_PRESS_TIME_MS | 短按判定有效时间 | 50 ~ 800 ms | 180 ms |
| BUTTON_LONG_PRESS_TIME_MS | 长按判定有效时间 | 500 ~ 5000 ms | 1500 ms |
| ADC_BUTTON_MAX_CHANNEL | ADC 按键最大通道数 | 1 ~ 5 | 3 |
| ADC_BUTTON_MAX_BUTTON_PER_CHANNEL | 每个 ADC 通道的最大按键数 | 1 ~ 10 | 8 |
| ADC_BUTTON_SAMPLE_TIMES | 每次 ADC 扫描的采样次数 | 1 ~ 4 | 1 |
| BUTTON_LONG_PRESS_HOLD_SERIAL_TIME_MS | 长按期间回调的连续触发间隔 | 2 ~ 1000 ms | 20 ms |
从源码 iot_button.c 可以看到这些配置在编译期被换算为内部 tick 计数:
#define TICKS_INTERVAL CONFIG_BUTTON_PERIOD_TIME_MS #define DEBOUNCE_TICKS CONFIG_BUTTON_DEBOUNCE_TICKS //MAX 8 #define SHORT_TICKS (CONFIG_BUTTON_SHORT_PRESS_TIME_MS / TICKS_INTERVAL) #define LONG_TICKS (CONFIG_BUTTON_LONG_PRESS_TIME_MS / TICKS_INTERVAL) #define SERIAL_TICKS (CONFIG_BUTTON_LONG_PRESS_HOLD_SERIAL_TIME_MS / TICKS_INTERVAL) #define TOLERANCE (CONFIG_BUTTON_PERIOD_TIME_MS*4)因此BUTTON_DEBOUNCE_TICKS的实际时间等于"消抖 tick 数 × 扫描周期",例如默认 2 × 5 ms = 10 ms。所有按键共享同一个由esp_timer实现的周期定时器(首次创建按键时在iot_button_create()中初始化,见 iot_button.c),这也是后续低功耗模式需要"关闭 esp_timer"的原因。
实战:创建按键
创建 GPIO 按键
GPIO 按键是最基础、最稳定的按键类型,配置结构体button_gpio_config_t包含gpio_num(引脚号)与active_level(按下时的有效电平),可选enable_power_save(低功耗使能)与disable_pull(禁用内部上下拉):
// create gpio button const button_config_t btn_cfg = {0}; const button_gpio_config_t btn_gpio_cfg = { .gpio_num = 0, .active_level = 0, }; button_handle_t gpio_btn = NULL; esp_err_t ret = iot_button_new_gpio_device(&btn_cfg, &btn_gpio_cfg, &gpio_btn); if(NULL == gpio_btn) { ESP_LOGE(TAG, "Button create failed"); }其中button_config_t可单独指定long_press_time与short_press_time(为 0 时使用 Kconfig 默认值,见 button_types.h),active_level = 0表示按键按下时引脚被拉低(低电平有效)。
创建 ADC 按键
ADC 按键通过分压网络把多个按键映射到同一个 ADC 通道,每个按键对应一个电压区间。配置结构体button_adc_config_t中min/max即该按键对应的最小 / 最大电压值(单位 mV):
// create adc button const button_config_t btn_cfg = {0}; button_adc_config_t btn_adc_cfg = { .unit_id = ADC_UNIT_1, .adc_channel = 0, .button_index = 0, .min = 100, .max = 400, }; button_handle_t adc_btn = NULL; esp_err_t ret = iot_button_new_adc_device(&btn_cfg, &btn_adc_cfg, &adc_btn); if(NULL == adc_btn) { ESP_LOGE(TAG, "Button create failed"); }复用项目已有 ADC 实例:如果 ADC1 已经在项目其他模块中被使用,再创建 ADC 按键时不应重复初始化 ADC1,而应将已有的adc_oneshot_unit_handle_t句柄传入adc_handle字段(见 button_adc.h,该字段为 NULL 时组件才会在内部自行创建 ADC 单元):
adc_oneshot_unit_handle_t adc1_handle; adc_oneshot_unit_init_cfg_t init_config1 = { .unit_id = ADC_UNIT_1, }; //-------------ADC1 Init---------------// adc_oneshot_new_unit(&init_config1, &adc1_handle); const button_config_t btn_cfg = {0}; button_adc_config_t btn_adc_cfg = { .adc_handle = &adc1_handle, .unit_id = ADC_UNIT_1, .adc_channel = 0, .button_index = 0, .min = 100, .max = 400, }; button_handle_t adc_btn = NULL; esp_err_t ret = iot_button_new_adc_device(&btn_cfg, &btn_adc_cfg, &adc_btn); if(NULL == adc_btn) { ESP_LOGE(TAG, "Button create failed"); }创建矩阵键盘按键
当按键数量很多时,矩阵键盘是最省 IO 的方案:row_gpios配置为输出引脚,col_gpios配置为输入引脚,行列交叉处放置按键(见 button_matrix.h 中 3×3 布局示例)。同一列上的按键不能同时检测,但同一行上的按键可以并行检测。创建时需传入一个容量足够的button_handle_t数组,矩阵规模必须等于"行数 × 列数":
// create matrix keypad button const button_config_t btn_cfg = {0}; const button_matrix_config_t matrix_cfg = { .row_gpios = (int32_t[]){4, 5, 6, 7}, .col_gpios = (int32_t[]){3, 8, 16, 15}, .row_gpio_num = 4, .col_gpio_num = 4, }; button_handle_t matrix_button = NULL; esp_err_t ret = iot_button_new_matrix_device(&btn_cfg, &matrix_cfg, btns, &matrix_button); if(NULL == matrix_button) { ESP_LOGE(TAG, "Button create failed"); }注册事件回调函数
组件支持为多个事件分别注册回调函数,每个事件都可以拥有自己的回调。事件触发时,对应的回调会被依次调用。
先看一个最简单的单击回调示例:
static void button_single_click_cb(void *arg,void *usr_data) { ESP_LOGI(TAG, "BUTTON_SINGLE_CLICK"); } iot_button_register_cb(gpio_btn, BUTTON_SINGLE_CLICK, NULL, button_single_click_cb,NULL);再看一个涉及多个回调、且带事件参数的示例。这里通过button_event_args_t联合体(见 iot_button.h)为BUTTON_LONG_PRESS_START设置不同的长按触发时间,让同一事件在不同按压时长下触发不同的回调:
static void button_long_press_1_cb(void *arg,void *usr_data) { ESP_LOGI(TAG, "BUTTON_LONG_PRESS_START_1"); } static void button_long_press_2_cb(void *arg,void *usr_data) { ESP_LOGI(TAG, "BUTTON_LONG_PRESS_START_2"); } button_event_args_t args = { .long_press.press_time = 2000, }; iot_button_register_cb(gpio_btn, BUTTON_LONG_PRESS_START, &args, button_auto_check_cb_1, NULL); args.long_press.press_time = 5000; iot_button_register_cb(gpio_btn, BUTTON_LONG_PRESS_START, &args, button_long_press_2_cb, NULL);此处的关键机制是:
BUTTON_LONG_PRESS_START与BUTTON_LONG_PRESS_UP事件支持通过args.long_press.press_time设置专属的长按触发时间(ms)。BUTTON_MULTIPLE_CLICK事件支持通过args.multiple_clicks.clicks设置需要连续点击的次数。- 从源码 iot_button.c 可以看到,回调注册时组件会按
press_time/clicks大小对同一事件的多个回调做排序插入,并在状态机中通过时间匹配(误差不超过TOLERANCE)逐个触发符合条件的回调。注册长按类回调时,press_time必须大于短按判定时间,否则会返回ESP_ERR_INVALID_ARG。
此外,iot_button.h 还提供了iot_button_unregister_cb()(注销指定回调)、iot_button_count_cb()(统计某按键注册的回调总数)、iot_button_count_event_cb()(统计某事件注册的回调数)等管理接口。
动态修改按键默认参数
组件运行期间可以通过iot_button_set_param()动态修改按键的时序参数,无需重新编译。参数枚举button_param_t支持BUTTON_LONG_PRESS_TIME_MS与BUTTON_SHORT_PRESS_TIME_MS两项,对应源码中直接更新内部 tick 计数(见 iot_button.c):
iot_button_set_param(btn, BUTTON_LONG_PRESS_TIME_MS, 5000);查询当前事件(轮询模式)
轮询模式下,只需周期性调用iot_button_get_event()获取当前事件即可,可与 iot_button.c 中的实现配合理解:
button_event_t event; event = iot_button_get_event(button_handle);此外还有若干查询辅助接口:iot_button_get_repeat()返回连续按压次数(双击返回 2,三击返回 3),iot_button_get_pressed_time()返回从按下到松开的实际时间(ms),iot_button_get_long_press_hold_cnt()返回BUTTON_LONG_PRESS_HOLD已触发的次数,iot_button_get_key_level()返回当前按键电平。
低功耗模式(Light Sleep)
按键扫描依赖周期性的esp_timer回调,即使进入 Light Sleep 也会周期性唤醒 CPU,导致整体功耗居高不下。为此组件提供了低功耗模式:所有按键空闲时自动关闭 esp_timer,仅在按键被按下时通过 GPIO 中断唤醒 CPU 并恢复扫描。
启用条件与配置
启用低功耗模式有两个前提:
- 所有已创建的按键必须全部是 GPIO 类型,并且在
button_gpio_config_t中将enable_power_save置为true; - 存在其他类型的按键(如 ADC 按键)时,低功耗模式无法生效。
button_config_t btn_cfg = {0}; button_gpio_config_t gpio_cfg = { .gpio_num = button_num, .active_level = BUTTON_ACTIVE_LEVEL, .enable_power_save = true, }; button_handle_t btn; iot_button_new_gpio_device(&btn_cfg, &gpio_cfg, &btn);从源码看,启用低功耗后定时器不会在创建时启动(iot_button.c),而是在button_cb()中检测到所有按键均处于BUTTON_NONE_PRESS空闲态时停止 esp_timer 并调用驱动的enter_power_save(iot_button.c);当 GPIO 中断触发时,iot_button_power_save_wakeup_isr()(IRAM 中的 ISR,见 iot_button.c)负责重新启动周期定时器并禁用 GPIO 中断,完成唤醒接力。
注意:该特性只保证"按键被使用时才唤醒 CPU",并不保证 CPU 一定进入低功耗模式——实际功耗还取决于应用中其他外设与任务。
功耗对比
由于 GPIO 唤醒仅支持电平触发,CPU 只有在按键处于有效电平期间才会被唤醒。因此:
- 单次按压场景:低功耗模式下按下期间的瞬时平均电流高于未开启模式,平均电流取决于按键按下的持续时间;
- 长时间运行场景:由于大部分时间定时器处于关闭状态,低功耗模式在整体运行周期内显著节省功耗。例如在 4 秒内连续按压 3 次的对比中,低功耗模式的总体功耗优势更加明显(详见 button.rst 中配套的
button_three_press_4s.png与button_power_save_three_press_4s.png波形图)。
进入 Light Sleep 的两种时机
- 自动进入:按键关闭 esp_timer 后,设备将自动进入 Light Sleep;
- 用户控制进入:通过注册
enter_power_save_cb回调,在该回调被调用(即所有按键停止工作、esp_timer 已停止)后再手动控制设备进入 Light Sleep:
void btn_enter_power_save(void *usr_data) { ESP_LOGI(TAG, "Can enter power save now"); } button_power_save_config_t config = { .enter_power_save_cb = btn_enter_power_save, }; iot_button_register_power_save_cb(&config);button_power_save_config_t结构体(定义见 iot_button.h)还支持通过usr_data向回调传递用户数据。
启用 CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP 后的按键使用
当使能CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP宏时,GPIO 模块在 Light Sleep 期间会被断电,此时必须改用RTC/LP GPIO,并将唤醒源改为EXT1,否则按键无法唤醒设备。GPIO 类型、该宏与唤醒源的对应关系如下表:
| GPIO 类型 | CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP 已使能? | 唤醒源 |
|---|---|---|
| 数字引脚 | N | GPIO 电平触发 |
| 数字引脚 | Y | 无(不可用) |
| RTC/LP 引脚 | N | GPIO 电平触发 / EXT1 |
| RTC/LP 引脚 | Y | EXT1 |
需要特别说明的是:ESP32-C5 与 ESP32-C6 的 LP GPIO 同时支持 GPIO 电平唤醒和 EXT1 唤醒,使用这两款芯片时还需要额外使能gpio_hold_en。
停止与恢复按键
组件支持在任意时刻停止与恢复按键功能,适合需要临时屏蔽按键输入的场景。两个接口的底层操作分别是esp_timer_stop与esp_timer_start_periodic(见 iot_button.c):
// stop button iot_button_stop(); // resume button iot_button_resume();完整 API 速览
除上述用到的接口外,iot_button.h 还提供了删除按键iot_button_delete()、打印当前事件iot_button_print_event()等管理接口。组件的安装方式也很简单,在项目目录下使用 ESP-IDF 组件管理器添加依赖即可(参考 components/button/README.md):
idf.py add-dependency "espressif/button=*"更多 API 的详细参数与返回值说明,可参考原文档 docs/en/input_device/button.rst 末尾的 API Reference 章节,以及仓库中的配套示例(如 examples/get-started/button_power_save 演示低功耗按键在 Light Sleep 下的完整用法)。
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考