版本信息: ESP-ADF v2.7-65-gcf908721
本文档详细分析ESP-ADF中的输入类外设实现机制,包括按键(button)、触摸(touch)和ADC按键(adc_button)等输入类外设的设计模式、接口规范、初始化流程和事件处理机制。ESP-ADF输入类外设基于统一的外设框架设计,通过事件驱动模型实现用户输入的检测和处理,为应用程序提供了灵活且易用的输入接口。
ESP-ADF输入类外设主要负责检测和处理用户的物理输入操作,将物理信号转换为应用程序可处理的事件。主要功能包括:
输入类外设是ESP-ADF外设子系统的重要组成部分,位于硬件驱动层和应用层之间:
ADC按键外设基于ESP32的ADC(模数转换器)模块实现,通过检测不同电压值来识别不同的按键。它支持多个按键同时使用,可以检测按键按下、释放、长按和长按释放等多种事件。ADC按键外设通过定时器定期采样ADC值,并根据阈值判断按键状态。
ADC按键外设的实现分为两个层次:外设层(Peripheral Layer)和底层驱动(Driver Layer)。
外设层提供了以下公共API,用于初始化和配置ADC按键外设:
// 文件:components/esp_peripherals/include/periph_adc_button.h
// 创建ADC按键外设句柄用于esp_peripherals
esp_periph_handle_t periph_adc_button_init(periph_adc_button_cfg_t *btn_cfg);
// ADC按键任务配置常量
#define ADC_BUTTON_STACK_SIZE 2500 // 任务栈大小
#define ADC_BUTTON_TASK_PRIORITY 10 // 任务优先级
#define ADC_BUTTON_TASK_CORE_ID 0 // 任务运行核心
// ADC按键默认配置宏
#define PERIPH_ADC_BUTTON_DEFAULT_CONFIG() { \
.task_cfg = { \
.task_stack = ADC_BUTTON_STACK_SIZE, \
.task_core = ADC_BUTTON_TASK_CORE_ID, \
.task_prio = ADC_BUTTON_TASK_PRIORITY,\
.ext_stack = false \
} \
}
// ADC默认通道配置宏
#define ADC_DEFAULT_ARR() { \
.adc_ch = ADC1_CHANNEL_3, \
.adc_level_step = NULL, \
.total_steps = 6, \
.press_judge_time = 3000, \
}
// ADC按键配置结构体
typedef struct {
adc_arr_t *arr; // ADC按键配置数组
int arr_size; // 配置数组大小
adc_btn_task_cfg_t task_cfg; // ADC按键任务配置
} periph_adc_button_cfg_t;
// ADC按键事件类型
typedef enum {
PERIPH_ADC_BUTTON_IDLE = 0, // 无事件
PERIPH_ADC_BUTTON_PRESSED, // 按键按下
PERIPH_ADC_BUTTON_RELEASE, // 按键释放
PERIPH_ADC_BUTTON_LONG_PRESSED, // 按键长按
PERIPH_ADC_BUTTON_LONG_RELEASE, // 按键长按后释放
} periph_adc_button_event_id_t;
ESP32 ADC1通道与GPIO对应关系表:
ESP32 ADC1通道与GPIO对应关系表
ADC1_CHANNEL_0 - GPIO36
ADC1_CHANNEL_1 - GPIO37
ADC1_CHANNEL_2 - GPIO38
ADC1_CHANNEL_3 - GPIO39
ADC1_CHANNEL_4 - GPIO32
ADC1_CHANNEL_5 - GPIO33
ADC1_CHANNEL_6 - GPIO34
ADC1_CHANNEL_7 - GPIO35
外设层内部使用以下数据结构:
// 文件:components/esp_peripherals/periph_adc_button.c
// ADC按键外设内部结构体
typedef struct {
int adc_channels; // ADC通道数量
adc_btn_list *list; // ADC按键列表
adc_btn_task_cfg_t task_cfg; // 任务配置
} periph_adc_btn_t;
底层驱动提供了以下API,用于直接操作ESP32的ADC模块:
// 文件:components/esp_peripherals/lib/adc_button/adc_button.h
// 按键ID枚举
typedef enum {
USER_KEY_ID0,
USER_KEY_ID1,
USER_KEY_ID2,
USER_KEY_ID3,
USER_KEY_ID4,
USER_KEY_ID5,
USER_KEY_ID6,
USER_KEY_MAX,
} user_key_id_num;
// 初始化ADC按键
void adc_btn_init(void *user_data, adc_button_callback cb, adc_btn_list *head, adc_btn_task_cfg_t *task_cfg);
// 创建ADC按键列表
adc_btn_list *adc_btn_create_list(adc_arr_t *adc_conf, int channels);
// 销毁ADC按键列表
esp_err_t adc_btn_destroy_list(adc_btn_list *head);
// 删除ADC按键任务
void adc_btn_delete_task(void);
// ADC按键状态枚举
typedef enum {
ADC_BTN_STATE_IDLE, // 空闲状态
ADC_BTN_STATE_ADC, // 检测状态
ADC_BTN_STATE_PRESSED, // 按下状态
ADC_BTN_STATE_RELEASE, // 释放状态
ADC_BTN_STATE_LONG_PRESSED, // 长按状态
ADC_BTN_STATE_LONG_RELEASE, // 长按释放状态
} adc_btn_state_t;
// ADC按键回调函数类型
typedef void (*adc_button_callback) (void *user_data, int adc, int id, adc_btn_state_t state);
// ADC按键配置结构体
typedef struct {
int adc_ch; // ADC通道
int *adc_level_step; // ADC电平步进值数组
int total_steps; // 总步数(按键数量)
int press_judge_time; // 长按判断时间(毫秒)
} adc_arr_t;
// 按键描述结构体
typedef struct {
int active_id; // 活动ID
int click_cnt; // 计时器计数
int long_click; // 长按标志
} btn_decription;
// ADC按键列表节点
typedef struct adc_btn {
adc_arr_t adc_info; // ADC信息
btn_decription *btn_dscp; // 按键描述数组
struct adc_btn *next; // 下一个节点
} adc_btn_list;
// ADC按键任务配置结构体
typedef struct {
int task_stack; // 任务栈大小
int task_prio; // 任务优先级
int task_core; // 任务运行的核心
bool ext_stack; // 是否使用外部栈
} adc_btn_task_cfg_t;
底层驱动内部使用以下数据结构:
// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
// ADC按键标签结构体
typedef struct {
adc_button_callback btn_callback; // 按键回调函数
adc_btn_list *head; // 列表头
void *user_data; // 用户数据
audio_thread_t audio_thread; // 音频线程
} adc_btn_tag_t;
ADC按键外设的初始化流程涉及两个层次:外设层(Peripheral Layer)和底层驱动(Driver Layer)。下面分别介绍这两个层次的初始化过程。
外设层初始化主要通过periph_adc_button_init
函数(位于periph_adc_button.c
)完成,主要包括以下步骤:
esp_periph_create
函数创建外设句柄periph_adc_btn_t
结构体内存adc_btn_create_list
创建ADC按键列表// 文件:components/esp_peripherals/periph_adc_button.c
esp_periph_handle_t periph_adc_button_init(periph_adc_button_cfg_t *config)
{
// 1. 创建外设句柄
esp_periph_handle_t periph = esp_periph_create(PERIPH_ID_ADC_BTN, "periph_adc_btn");
AUDIO_MEM_CHECK(TAG, periph, return NULL);
// 2. 分配ADC按键外设结构体内存
periph_adc_btn_t *periph_adc_btn = audio_calloc(1, sizeof(periph_adc_btn_t));
AUDIO_MEM_CHECK(TAG, periph_adc_btn, {
audio_free(periph);
return NULL;
});
// 3. 设置ADC通道数量
periph_adc_btn->adc_channels = config->arr_size;
// 4. 创建ADC按键列表
periph_adc_btn->list = adc_btn_create_list(config->arr, config->arr_size);
// 5. 复制任务配置
memcpy(&periph_adc_btn->task_cfg, &config->task_cfg, sizeof(adc_btn_task_cfg_t));
// 6. 内存检查
AUDIO_MEM_CHECK(TAG, periph_adc_btn->list, {
audio_free(periph);
audio_free(periph_adc_btn);
return NULL;
});
// 7. 设置外设数据和函数
esp_periph_set_data(periph, periph_adc_btn);
esp_periph_set_function(periph, _adc_button_init, NULL, _adc_button_destroy);
return periph;
}
当外设被添加到外设集合并启动时,会调用_adc_button_init
函数(位于periph_adc_button.c
),该函数负责初始化底层ADC按键驱动:
// 文件:components/esp_peripherals/periph_adc_button.c
static esp_err_t _adc_button_init(esp_periph_handle_t self)
{
// 1. 获取外设数据
periph_adc_btn_t *periph_adc_btn = esp_periph_get_data(self);
// 2. 调用底层驱动初始化函数
adc_btn_init((void *)self, btn_cb, periph_adc_btn->list, &periph_adc_btn->task_cfg);
return ESP_OK;
}
底层ADC按键驱动初始化通过adc_btn_init
函数(位于adc_button.c
)完成,主要包括以下步骤:
adc_btn_tag_t
结构体内存// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
void adc_btn_init(void *user_data, adc_button_callback cb, adc_btn_list *head, adc_btn_task_cfg_t *task_cfg)
{
// 1. 分配ADC按键标签结构体内存
adc_btn_tag_t *tag = audio_calloc(1, sizeof(adc_btn_tag_t));
if (NULL == tag) {
ESP_LOGE(TAG, "Memory allocation failed!");
return;
}
// 2. 设置用户数据、列表头和回调函数
tag->user_data = user_data;
tag->head = head;
tag->btn_callback = cb;
// 3. 创建事件组
g_event_bit = xEventGroupCreate();
// 4. 创建ADC按键任务
audio_thread_create(&tag->audio_thread,
"button_task", button_task,
(void *)tag,
task_cfg->task_stack,
task_cfg->task_prio,
task_cfg->ext_stack,
task_cfg->task_core);
}
在ADC按键检测任务中,会进行以下操作:
// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
static void button_task(void *parameters)
{
_task_flag = true;
adc_btn_tag_t *tag = (adc_btn_tag_t *)parameters;
adc_btn_list *head = tag->head;
adc_btn_list *find = head;
xEventGroupClearBits(g_event_bit, DESTROY_BIT);
// 1. 配置ADC宽度
#if CONFIG_IDF_TARGET_ESP32S2
adc1_config_width(ADC_WIDTH_BIT_13);
#else
adc1_config_width(ADC_WIDTH_BIT_12);
#endif
// 2. 初始化所有ADC通道和按键状态
while (find) {
adc_arr_t *info = &(find->adc_info);
reset_btn(find->btn_dscp, info->total_steps);
adc1_config_channel_atten(info->adc_ch, ADC_ATTEN_11db);
find = find->next;
}
// 3. 主循环:定期检测ADC值
while (_task_flag) {
find = head;
// 3.1 遍历所有ADC通道
while (find) {
adc_arr_t *info = &(find->adc_info);
btn_decription *btn_dscp = find->btn_dscp;
// 3.2 获取ADC电压值
int adc = get_adc_voltage(info->adc_ch);
// 3.3 查找活动按键ID
int active_id = ADC_BTN_INVALID_ACT_ID;
for (int i = 0; i < info->total_steps; i++) {
if (btn_dscp[i].active_id != ADC_BTN_INVALID_ID) {
active_id = i;
break;
}
}
// 3.4 获取按键状态
adc_btn_state_t st = get_adc_btn_state(adc, active_id, find);
// 3.5 如果状态不是空闲,调用回调函数
if (st != ADC_BTN_STATE_IDLE && tag->btn_callback) {
int btn_id = (active_id != ADC_BTN_INVALID_ACT_ID) ? active_id : get_button_id(find, adc);
tag->btn_callback(tag->user_data, info->adc_ch, btn_id, st);
}
find = find->next;
}
// 3.6 等待一段时间
vTaskDelay(ADC_BTN_DETECT_TIME_MS / portTICK_PERIOD_MS);
// 3.7 检查是否需要退出任务
if (xEventGroupGetBits(g_event_bit) & DESTROY_BIT) {
break;
}
}
// 4. 任务结束,释放资源
audio_free(tag);
vEventGroupDelete(g_event_bit);
audio_thread_cleanup(&tag->audio_thread);
_task_flag = false;
vTaskDelete(NULL);
}
下图展示了ADC按键检测任务(button_task
)的工作流程:
该时序图展示了ADC按键检测任务的完整生命周期,包括以下主要阶段:
初始化阶段:
检测循环阶段:
资源释放阶段:
这个时序图可以帮助理解ADC按键检测任务的工作原理,特别是按键状态检测和事件触发的流程。
ADC按键列表创建通过adc_btn_create_list
函数(位于adc_button.c
)完成,主要包括以下步骤:
// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
adc_btn_list *adc_btn_create_list(adc_arr_t *adc_conf, int channels)
{
// 1. 初始化列表头和节点指针
adc_btn_list *head = NULL;
adc_btn_list *node = NULL;
adc_btn_list *find = NULL;
// 2. 为每个通道创建节点
for (int i = 0; i < channels; i++) {
// 2.1 分配节点内存
node = (adc_btn_list *)audio_calloc(1, sizeof(adc_btn_list));
if (NULL == node) {
ESP_LOGE(TAG, "内存分配失败!");
return NULL;
}
// 2.2 复制ADC配置信息
adc_arr_t *info = &(node->adc_info);
memcpy(info, adc_conf + i, sizeof(adc_arr_t));
// 2.3 分配ADC电平步进值数组内存
info->adc_level_step = (int *)audio_calloc(1, (info->total_steps + 1) * sizeof(int));
if (NULL == info->adc_level_step) {
ESP_LOGE(TAG, "内存分配失败!");
audio_free(node);
return NULL;
}
// 2.4 设置ADC电平步进值
if (adc_conf[i].adc_level_step == NULL) {
// 使用默认步进值
memcpy(info->adc_level_step, default_step_level, USER_KEY_MAX * sizeof(int));
} else {
// 使用配置的步进值
memcpy(info->adc_level_step, adc_conf[i].adc_level_step,
(adc_conf[i].total_steps + 1) * sizeof(int));
}
// 2.5 分配按键描述数组内存
node->btn_dscp = (btn_decription *)audio_calloc(1,
sizeof(btn_decription) * (adc_conf[i].total_steps));
if (NULL == node->btn_dscp) {
ESP_LOGE(TAG, "内存分配失败!");
audio_free(info->adc_level_step);
audio_free(node);
}
// 2.6 将节点添加到链表中
node->next = NULL;
if (NULL == head) {
head = node;
find = head;
} else {
find->next = node;
find = node;
}
}
return head;
}
下图展示了ADC按键外设从应用程序调用到底层驱动完成初始化的完整流程:
ADC按键外设的销毁流程同样涉及两个层次:外设层(Peripheral Layer)和底层驱动(Driver Layer)。下面分别介绍这两个层次的销毁过程。
外设层销毁主要通过_adc_button_destroy
函数(位于periph_adc_button.c
)完成,主要包括以下步骤:
// 文件:components/esp_peripherals/periph_adc_button.c
static esp_err_t _adc_button_destroy(esp_periph_handle_t self)
{
// 1. 获取外设数据
periph_adc_btn_t *periph_adc_btn = esp_periph_get_data(self);
// 2. 删除ADC按键任务
adc_btn_delete_task();
// 3. 销毁ADC按键列表
adc_btn_destroy_list(periph_adc_btn->list);
// 4. 释放外设数据内存
audio_free(periph_adc_btn);
return ESP_OK;
}
底层ADC按键驱动销毁通过两个主要函数完成:adc_btn_delete_task
和adc_btn_destroy_list
。
adc_btn_delete_task
函数(位于adc_button.c
)负责停止ADC按键检测任务,主要包括以下步骤:
// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
void adc_btn_delete_task(void)
{
// 1. 设置任务标志为false,通知任务退出
if (_task_flag) {
_task_flag = false;
}
// 2. 等待任务退出并删除事件组
if (g_event_bit) {
xEventGroupWaitBits(g_event_bit, DESTROY_BIT, pdTRUE, pdFALSE, portMAX_DELAY);
vEventGroupDelete(g_event_bit);
g_event_bit = NULL;
}
}
adc_btn_destroy_list
函数(位于adc_button.c
)负责释放ADC按键列表的内存,主要包括以下步骤:
// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
esp_err_t adc_btn_destroy_list(adc_btn_list *head)
{
// 1. 检查列表头是否为空
if (head == NULL) {
ESP_LOGD(TAG, "The head of list is null");
return ESP_OK;
}
// 2. 遍历列表,释放每个节点的内存
adc_btn_list *find = head;
adc_btn_list *tmp = find;
while (find) {
adc_arr_t *info = &(find->adc_info);
tmp = find->next;
// 2.1 释放按键描述数组内存
audio_free(find->btn_dscp);
// 2.2 释放ADC电平步进值数组内存
audio_free(info->adc_level_step);
// 2.3 释放节点内存
audio_free(find);
// 2.4 移动到下一个节点
find = tmp;
}
return ESP_OK;
}
下图展示了ADC按键外设从应用程序调用到底层驱动完成销毁的完整流程:
这个时序图展示了ADC按键外设销毁的完整流程,包括以下关键步骤:
esp_periph_destroy
销毁外设_adc_button_destroy
函数_adc_button_destroy
函数调用底层驱动的adc_btn_delete_task
函数adc_btn_delete_task
函数设置任务标志为false,通知任务退出adc_btn_delete_task
函数等待DESTROY_BIT事件位并删除事件组_adc_button_destroy
函数调用adc_btn_destroy_list
函数销毁ADC按键列表adc_btn_destroy_list
函数释放所有节点的资源_adc_button_destroy
函数释放外设层的资源这个实现确保了所有资源都被正确释放,包括硬件资源(ADC通道)和软件资源(内存、任务、事件组)。
ADC按键检测算法结合了定时采样和状态机机制,涉及外设层(Peripheral Layer)和底层驱动(Driver Layer)两个层次。下面分别介绍这两个层次的实现。
外设层通过回调函数将底层驱动的按键状态转换为ESP-ADF的事件系统事件:
// 文件:components/esp_peripherals/periph_adc_button.c
static void btn_cb(void *user_data, int adc, int id, adc_btn_state_t state)
{
// 获取外设句柄
esp_periph_handle_t self = (esp_periph_handle_t)user_data;
periph_adc_button_event_id_t event_id = PERIPH_ADC_BUTTON_IDLE;
// 将底层驱动状态转换为外设事件
if (state == ADC_BTN_STATE_PRESSED) {
event_id = PERIPH_ADC_BUTTON_PRESSED;
} else if (state == ADC_BTN_STATE_LONG_PRESSED) {
event_id = PERIPH_ADC_BUTTON_LONG_PRESSED;
} else if (state == ADC_BTN_STATE_RELEASE) {
event_id = PERIPH_ADC_BUTTON_RELEASE;
} else if (state == ADC_BTN_STATE_LONG_RELEASE) {
event_id = PERIPH_ADC_BUTTON_LONG_RELEASE;
}
// 发送事件到外设框架
// 使用按键ID作为数据,ADC通道作为数据长度
esp_periph_send_event(self, event_id, (void *)id, adc);
}
底层ADC按键驱动实现在adc_button.c
中,负责具体的ADC值采样、按键状态检测和事件生成。核心功能由三个函数实现:get_adc_voltage
、get_button_id
和get_adc_btn_state
。
get_adc_voltage
函数负责采样ADC值并进行滤波处理,以获取稳定的ADC电压值:
// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
/**
* @brief 获取ADC通道的电压值
*
* 该函数对ADC通道进行多次采样,并通过排序滤波算法获取稳定的电压值。
* 采样过程如下:
* 1. 特性化ADC,设置参考电压和衰减
* 2. 对ADC通道进行多次采样
* 3. 对采样值进行排序
* 4. 去除最大值和最小值,计算平均值
*/
static int get_adc_voltage(int channel)
{
uint32_t data[ADC_SAMPLES_NUM] = { 0 };
uint32_t sum = 0;
int tmp = 0;
// 1. 特性化ADC
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
esp_adc_cal_characteristics_t characteristics;
#if CONFIG_IDF_TARGET_ESP32
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_11db, ADC_WIDTH_12Bit, V_REF, &characteristics);
#elif CONFIG_IDF_TARGET_ESP32S2
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_11db, ADC_WIDTH_BIT_13, 0, &characteristics);
#else
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_11db, ADC_WIDTH_12Bit, 0, &characteristics);
#endif
// 2. 采样多次ADC值
for (int i = 0; i < ADC_SAMPLES_NUM; ++i) {
esp_adc_cal_get_voltage(channel, &characteristics, &data[i]);
}
#endif
// 3. 对采样值进行排序(冒泡排序)
for (int j = 0; j < ADC_SAMPLES_NUM - 1; j++) {
for (int i = 0; i < ADC_SAMPLES_NUM - j - 1; i++) {
if (data[i] > data[i + 1]) {
tmp = data[i];
data[i] = data[i + 1];
data[i + 1] = tmp;
}
}
}
// 4. 去除最大值和最小值,计算平均值
for (int num = 1; num < ADC_SAMPLES_NUM - 1; num++)
sum += data[num];
return (sum / (ADC_SAMPLES_NUM - 2));
}
get_button_id
函数根据ADC值和预设的阈值判断当前按下的是哪个按键:
// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
/**
* @brief 根据ADC值获取按键ID
*
* 该函数遍历所有按键阈值,查找匹配的按键ID。
* 每个按键对应一个ADC电压范围,当ADC值落在某个范围内时,
* 返回对应的按键ID。
*
* @param node ADC按键列表节点
* @param adc ADC电压值
* @return 按键ID,如果没有匹配的按键,返回ADC_BTN_INVALID_ID
*/
static int get_button_id(adc_btn_list *node, int adc)
{
int m = ADC_BTN_INVALID_ID;
adc_arr_t *info = &(node->adc_info);
// 遍历所有按键阈值,查找匹配的按键ID
for (int i = 0; i < info->total_steps; i++) {
if ((adc > info->adc_level_step[i]) && (adc <= info->adc_level_step[i + 1])) {
m = i;
break;
}
}
return m;
}
get_adc_btn_state
函数是ADC按键状态检测的核心,负责判断按键的当前状态:
// 文件:components/esp_peripherals/lib/adc_button/adc_button.c
/**
* @brief 获取ADC按键当前状态
*
* 该函数是ADC按键状态检测的核心,通过检测ADC值和按键历史状态,
* 判断按键的当前状态。按键状态机如下:
* 1. 初始状态:未按下,active_id = -1
* 2. 按下状态:检测到按键ID有效,记录active_id,click_cnt增加
* 3. 短按释放:按键ID变为无效且click_cnt小于长按阈值,返回RELEASE
* 4. 长按状态:按键持续按下且click_cnt达到长按阈值,返回LONG_PRESSED
* 5. 长按释放:长按后按键ID变为无效,返回LONG_RELEASE
*
* @param adc_value 当前ADC值
* @param act_id 当前活动按键ID
* @param node ADC按键列表节点
* @return 按键状态
*/
static adc_btn_state_t get_adc_btn_state(int adc_value, int act_id, adc_btn_list *node)
{
adc_btn_state_t st = ADC_BTN_STATE_IDLE;
adc_arr_t *info = &(node->adc_info);
btn_decription *btn_dscp = node->btn_dscp;
// 1. 根据ADC值获取按键ID
int id = get_button_id(node, adc_value);
// 2. 如果ID无效(没有按键被按下)
if (id == ADC_BTN_INVALID_ID) {
// 2.1 如果之前也没有按键被按下,保持空闲状态
if (act_id == ADC_BTN_INVALID_ACT_ID) {
return ADC_BTN_STATE_IDLE;
}
// 2.2 如果点击计数太小,忽略这次点击
if (btn_dscp[act_id].click_cnt <= 1) {
return ADC_BTN_STATE_IDLE;
}
// 2.3 有之前的活动ID,但现在ID无效,需要发送释放事件
if (btn_dscp[act_id].click_cnt < (info->press_judge_time / ADC_BTN_DETECT_TIME_MS)) {
// 短按释放
st = ADC_BTN_STATE_RELEASE;
} else {
// 长按释放
st = ADC_BTN_STATE_LONG_RELEASE;
}
// 2.4 重置按键状态
btn_dscp[act_id].active_id = -1;
btn_dscp[act_id].long_click = 0;
btn_dscp[act_id].click_cnt = 0;
return st;
}
// 3. ID有效但活动ID无效(第一次检测到按键)
if (act_id == ADC_BTN_INVALID_ACT_ID) {
btn_dscp[id].active_id = id;
return ADC_BTN_STATE_IDLE;
}
// 4. ID和活动ID都有效,但不相等(按键变化)
if (id != act_id) {
// 4.1 重置旧按键状态
btn_dscp[act_id].active_id = -1;
btn_dscp[act_id].long_click = 0;
// 4.2 设置新按键活动ID
btn_dscp[id].active_id = id;
// 4.3 检查是否需要发送释放事件
if (btn_dscp[act_id].click_cnt < ADC_BTN_DETECTED_CNT) {
btn_dscp[act_id].click_cnt = 0;
return ADC_BTN_STATE_IDLE;
}
btn_dscp[act_id].click_cnt = 0;
// 4.4 根据点击时长判断释放类型
if (btn_dscp[act_id].click_cnt < (info->press_judge_time / ADC_BTN_DETECT_TIME_MS)) {
return ADC_BTN_STATE_RELEASE;
} else {
return ADC_BTN_STATE_LONG_RELEASE;
}
}
// 5. ID和活动ID都有效且相等(持续按下)
btn_dscp[act_id].click_cnt++;
// 5.1 如果达到检测次数阈值,发送按下事件
if (btn_dscp[act_id].click_cnt == ADC_BTN_DETECTED_CNT) {
return ADC_BTN_STATE_PRESSED;
}
// 5.2 如果已经发送过长按事件,保持空闲状态
if (btn_dscp[act_id].long_click) {
return ADC_BTN_STATE_IDLE;
}
// 5.3 如果达到长按判断时间,发送长按事件
if (btn_dscp[act_id].click_cnt >= (info->press_judge_time / ADC_BTN_DETECT_TIME_MS)) {
st = ADC_BTN_STATE_LONG_PRESSED;
btn_dscp[act_id].long_click = 1;
}
return st;
}
下图展示了ADC按键状态的转换过程,包括按下、释放、长按和长按释放的完整状态流转:
多次采样滤波:通过对ADC通道进行多次采样,并使用排序滤波算法(去除最大值和最小值后取平均值),减少ADC读数的波动,提高检测稳定性。
按键ID映射:使用预设的ADC电压阈值范围,将ADC值映射到具体的按键ID,支持在同一个ADC通道上连接多个按键。
状态机设计:使用状态机设计模式,清晰地区分不同的按键状态(空闲、按下、释放、长按、长按释放),并确保状态转换的正确性。
防抖动处理:通过点击计数(click_cnt)和检测次数阈值(ADC_BTN_DETECTED_CNT)实现按键防抖,避免误触发。
长短按区分:通过时间阈值(press_judge_time)区分短按和长按,提供更丰富的交互方式。
多通道支持:支持同时检测多个ADC通道上的按键,通过链表结构管理多个通道的配置和状态。
芯片兼容性:代码中包含了对不同ESP32系列芯片的兼容处理,确保在不同平台上都能正常工作。
通过这种设计,ADC按键检测算法能够准确地检测各种按键事件,并将其传递给应用程序进行处理,为用户提供良好的交互体验。
ADC按键外设产生以下事件类型:
事件数据为按键ID,事件数据长度为ADC通道号。
以下是ADC按键外设的使用示例:
#include "esp_peripherals.h"
#include "periph_adc_button.h"
// ADC按键回调函数
static esp_err_t adc_button_callback(audio_event_iface_msg_t *event, void *context)
{
int btn_id = (int)event->data;
int adc_channel = event->data_len;
switch (event->cmd) {
case PERIPH_ADC_BUTTON_PRESSED:
printf("ADC button %d pressed\n", btn_id);
break;
case PERIPH_ADC_BUTTON_RELEASE:
printf("ADC button %d released\n", btn_id);
break;
case PERIPH_ADC_BUTTON_LONG_PRESSED:
printf("ADC button %d long pressed\n", btn_id);
break;
case PERIPH_ADC_BUTTON_LONG_RELEASE:
printf("ADC button %d long released\n", btn_id);
break;
default:
break;
}
return ESP_OK;
}
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);
// 配置ADC按键
adc_arr_t adc_btn_array[] = {
{
.adc_ch = ADC1_CHANNEL_0, // ADC通道
.total_steps = 4, // 按键数量
.press_judge_time = 2000, // 长按判断时间(毫秒)
.adc_level_step = NULL, // 使用默认阈值
},
};
// ADC按键任务配置
adc_btn_task_cfg_t task_cfg = {
.task_stack = 2048, // 任务栈大小
.task_prio = 5, // 任务优先级
.task_core = 0, // 任务运行的核心
.ext_stack = false, // 不使用外部栈
};
// ADC按键外设配置
periph_adc_button_cfg_t adc_button_cfg = {
.arr = adc_btn_array, // ADC按键配置数组
.arr_size = 1, // 配置数组大小
.task_cfg = task_cfg, // 任务配置
};
// 初始化ADC按键外设
esp_periph_handle_t adc_button_handle = periph_adc_button_init(&adc_button_cfg);
// 添加ADC按键外设到外设集合
esp_periph_start(set, adc_button_handle);
// 设置ADC按键事件回调
audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg);
esp_periph_set_listener(set, evt);
audio_event_iface_set_listener(esp_periph_set_get_event_iface(set), adc_button_callback, NULL);
// 主循环
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}