ESP-ADF外设子系统深度解析:esp_peripherals组件架构与核心设计(输入类外设之ADC按键 ADC Button)

目录

  • ESP-ADF外设子系统深度解析:esp_peripherals组件架构与核心设计(输入类外设之ADC按键 ADC Button)
    • 简介
    • 模块概述
      • 功能定义
      • 架构位置
      • 核心特性
    • ADC按键(ADC Button)外设
      • ADC按键外设概述
      • ADC按键外设API和数据结构
        • 外设层API(periph_adc_button.h/periph_adc_button.c)
        • 底层驱动API(adc_button.h/adc_button.c)
      • ADC按键初始化流程
        • 外设层初始化过程(periph_adc_button.c)
        • 底层驱动初始化过程(adc_button.c)
          • ADC按键检测任务流程图
        • ADC按键列表创建过程
        • ADC按键外设完整初始化时序图
      • ADC按键外设销毁流程
        • 外设层销毁过程(periph_adc_button.c)
        • 底层驱动销毁过程(adc_button.c)
          • 任务删除过程
          • 列表销毁过程
        • ADC按键外设完整销毁时序图
      • ADC按键检测算法
        • 外设层事件处理实现(periph_adc_button.c)
        • 底层驱动检测实现(adc_button.c)
          • get_adc_voltage函数
          • get_button_id函数
          • get_adc_btn_state函数
        • ADC按键状态转换图
        • ADC按键检测算法关键点
        • ADC按键事件处理
        • 使用示例

ESP-ADF外设子系统深度解析:esp_peripherals组件架构与核心设计(输入类外设之ADC按键 ADC Button)

版本信息: ESP-ADF v2.7-65-gcf908721

简介

本文档详细分析ESP-ADF中的输入类外设实现机制,包括按键(button)、触摸(touch)和ADC按键(adc_button)等输入类外设的设计模式、接口规范、初始化流程和事件处理机制。ESP-ADF输入类外设基于统一的外设框架设计,通过事件驱动模型实现用户输入的检测和处理,为应用程序提供了灵活且易用的输入接口。

模块概述

功能定义

ESP-ADF输入类外设主要负责检测和处理用户的物理输入操作,将物理信号转换为应用程序可处理的事件。主要功能包括:

  • 物理输入信号检测(按键按下/释放、触摸触发/释放、ADC电平变化等)
  • 输入事件生成(短按、长按、触摸等事件)
  • 事件过滤和防抖处理
  • 向应用程序传递输入事件

架构位置

输入类外设是ESP-ADF外设子系统的重要组成部分,位于硬件驱动层和应用层之间:

应用程序
ESP外设子系统
输入类外设
按键外设
触摸外设
ADC按键外设
GPIO驱动
触摸驱动
ADC驱动

核心特性

  • 多种输入类型支持:支持GPIO按键、电容触摸、ADC按键等多种输入方式
  • 统一事件模型:所有输入外设使用统一的事件模型和接口
  • 丰富的事件类型:支持按下、释放、长按、长按释放等多种事件类型
  • 防抖处理:内置输入信号防抖处理机制
  • 可配置参数:支持灵活配置长按时间、触发阈值等参数
  • 中断和轮询结合:结合中断和定时器轮询提高响应速度和可靠性

ADC按键(ADC Button)外设

ADC按键外设概述

ADC按键外设基于ESP32的ADC(模数转换器)模块实现,通过检测不同电压值来识别不同的按键。它支持多个按键同时使用,可以检测按键按下、释放、长按和长按释放等多种事件。ADC按键外设通过定时器定期采样ADC值,并根据阈值判断按键状态。

ADC按键外设API和数据结构

ADC按键外设的实现分为两个层次:外设层(Peripheral Layer)和底层驱动(Driver Layer)。

外设层API(periph_adc_button.h/periph_adc_button.c)

外设层提供了以下公共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(adc_button.h/adc_button.c)

底层驱动提供了以下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按键初始化流程

ADC按键外设的初始化流程涉及两个层次:外设层(Peripheral Layer)和底层驱动(Driver Layer)。下面分别介绍这两个层次的初始化过程。

外设层初始化过程(periph_adc_button.c)

外设层初始化主要通过periph_adc_button_init函数(位于periph_adc_button.c)完成,主要包括以下步骤:

  1. 创建外设句柄:调用esp_periph_create函数创建外设句柄
  2. 分配内部数据结构:分配periph_adc_btn_t结构体内存
  3. 设置配置参数:设置ADC通道数量和任务配置
  4. 创建ADC按键列表:调用adc_btn_create_list创建ADC按键列表
  5. 注册回调函数:设置初始化和销毁回调函数
// 文件: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_button.c)

底层ADC按键驱动初始化通过adc_btn_init函数(位于adc_button.c)完成,主要包括以下步骤:

  1. 创建事件组:创建用于任务同步的事件组
  2. 分配ADC按键标签结构体:分配adc_btn_tag_t结构体内存
  3. 设置回调函数和用户数据:设置按键回调函数、列表头和用户数据
  4. 创建ADC按键检测任务:配置并创建ADC按键检测任务
// 文件: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按键检测任务中,会进行以下操作:

  1. 配置ADC宽度:根据目标平台配置ADC宽度
  2. 初始化ADC通道:配置所有ADC通道的衰减值
  3. 初始化按键状态:重置所有按键的状态
  4. 循环检测ADC值:定期读取ADC值并判断按键状态
  5. 触发按键事件:当检测到按键状态变化时,调用回调函数
// 文件: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按键检测任务流程图

下图展示了ADC按键检测任务(button_task)的工作流程:

button_task ESP32 ADC模块 辅助函数 回调函数 任务启动 初始化变量 清除DESTROY_BIT事件位 adc1_config_width(ADC_WIDTH_BIT_12/13) reset_btn(btn_dscp, total_steps) adc1_config_channel_atten(adc_ch, ADC_ATTEN_11db) loop [遍历所有ADC通道] get_adc_voltage(adc_ch) ADC电压值 查找活动按键ID get_adc_btn_state(adc, active_id, find) 按键状态 确定按键ID btn_callback(user_data, adc_ch, btn_id, state) alt [状态不是空闲] loop [遍历所有ADC通道] vTaskDelay(ADC_BTN_DETECT_TIME_MS) 检查DESTROY_BIT事件位 跳出主循环 alt [DESTROY_- BIT被设置] loop [主循环 (当_task_flag为true)] 任务结束 释放资源 删除事件组 清理线程 设置_task_flag为false vTaskDelete(NULL) button_task ESP32 ADC模块 辅助函数 回调函数

该时序图展示了ADC按键检测任务的完整生命周期,包括以下主要阶段:

  1. 初始化阶段

    • 初始化变量和清除事件位
    • 配置ADC宽度
    • 初始化所有ADC通道和按键状态
  2. 检测循环阶段

    • 遍历所有ADC通道
    • 获取ADC电压值
    • 查找活动按键ID
    • 获取按键状态
    • 如果状态不是空闲,调用回调函数
    • 等待一段时间
    • 检查是否需要退出任务
  3. 资源释放阶段

    • 释放内存
    • 删除事件组
    • 清理线程
    • 设置任务标志为false
    • 删除任务

这个时序图可以帮助理解ADC按键检测任务的工作原理,特别是按键状态检测和事件触发的流程。

ADC按键列表创建过程

ADC按键列表创建通过adc_btn_create_list函数(位于adc_button.c)完成,主要包括以下步骤:

  1. 初始化列表头:初始化ADC按键列表头指针
  2. 为每个通道创建节点:遍历所有ADC通道,为每个通道创建一个节点
  3. 分配内存:为节点、ADC电平步进值数组和按键描述数组分配内存
  4. 设置ADC电平步进值:设置每个按键的ADC电平阈值
  5. 构建链表:将所有节点连接成链表
// 文件: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按键外设从应用程序调用到底层驱动完成初始化的完整流程:

应用程序 periph_adc_button_init (periph_adc_button.c) esp_periph库 _adc_button_init (periph_adc_button.c) adc_btn_init (adc_button.c) button_task (adc_button.c) ESP32 ADC模块 periph_adc_button_init(config) esp_periph_create(PERIPH_ID_ADC_BTN, "periph_adc_btn") periph 分配 periph_adc_btn_t 结构体 设置ADC通道数量 adc_btn_create_list(config->>arr, config->>arr_size) 为每个通道创建节点 设置ADC电平步进值 ADC按键列表 esp_periph_set_data(periph, periph_adc_btn) esp_periph_set_function(periph, _adc_button_init, NULL, _adc_button_destroy) periph 当外设被添加到外设集合并启动时 _adc_button_init(self) esp_periph_get_data(self) periph_adc_btn adc_btn_init(self, btn_cb, periph_adc_btn->>list, &periph_adc_btn->>task_cfg) 创建事件组 分配adc_btn_tag_t结构体 设置回调函数和用户数据 创建ADC按键任务 启动button_task adc1_config_width() reset_btn() adc1_config_channel_atten() loop [遍历每个ADC通道] 进入主循环,定期检测ADC值 ESP_OK 应用程序 periph_adc_button_init (periph_adc_button.c) esp_periph库 _adc_button_init (periph_adc_button.c) adc_btn_init (adc_button.c) button_task (adc_button.c) ESP32 ADC模块

ADC按键外设销毁流程

ADC按键外设的销毁流程同样涉及两个层次:外设层(Peripheral Layer)和底层驱动(Driver Layer)。下面分别介绍这两个层次的销毁过程。

外设层销毁过程(periph_adc_button.c)

外设层销毁主要通过_adc_button_destroy函数(位于periph_adc_button.c)完成,主要包括以下步骤:

  1. 获取外设数据:获取ADC按键外设的内部数据结构
  2. 删除ADC按键任务:调用底层驱动的任务删除函数
  3. 销毁ADC按键列表:释放ADC按键列表的资源
  4. 释放内部数据结构:释放ADC按键外设的内部数据结构
// 文件: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_button.c)

底层ADC按键驱动销毁通过两个主要函数完成:adc_btn_delete_taskadc_btn_destroy_list

任务删除过程

adc_btn_delete_task函数(位于adc_button.c)负责停止ADC按键检测任务,主要包括以下步骤:

  1. 设置任务标志:设置任务标志为false,通知任务退出
  2. 等待任务退出:等待任务设置DESTROY_BIT事件位
  3. 删除事件组:删除用于同步的事件组
// 文件: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按键列表的内存,主要包括以下步骤:

  1. 检查列表有效性:检查列表头是否为空
  2. 遍历列表:遍历ADC按键列表的所有节点
  3. 释放节点资源:释放每个节点的按键描述数组、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库 _adc_button_destroy (periph_adc_button.c) 底层驱动 (adc_button.c) button_task (adc_button.c) esp_periph_destroy(periph) 当外设被销毁时,调用_adc_button_destroy _adc_button_destroy(self) esp_periph_get_data(self) periph_adc_btn adc_btn_delete_task() 设置_task_flag = false 任务检测到_task_flag为false 设置DESTROY_BIT事件位 释放资源并退出 等待DESTROY_BIT事件位 事件位已设置 vEventGroupDelete(g_event_bit) 返回 adc_btn_destroy_list(periph_adc_btn->>list) 释放按键描述数组内存 释放ADC电平步进值数组内存 释放节点内存 loop [遍历ADC按键列表] ESP_OK audio_free(periph_adc_btn) ESP_OK 返回 应用程序 esp_periph库 _adc_button_destroy (periph_adc_button.c) 底层驱动 (adc_button.c) button_task (adc_button.c)

这个时序图展示了ADC按键外设销毁的完整流程,包括以下关键步骤:

  1. 应用程序调用esp_periph_destroy销毁外设
  2. 外设库调用_adc_button_destroy函数
  3. _adc_button_destroy函数调用底层驱动的adc_btn_delete_task函数
  4. adc_btn_delete_task函数设置任务标志为false,通知任务退出
  5. 任务检测到标志为false,设置DESTROY_BIT事件位并退出
  6. adc_btn_delete_task函数等待DESTROY_BIT事件位并删除事件组
  7. _adc_button_destroy函数调用adc_btn_destroy_list函数销毁ADC按键列表
  8. adc_btn_destroy_list函数释放所有节点的资源
  9. _adc_button_destroy函数释放外设层的资源

这个实现确保了所有资源都被正确释放,包括硬件资源(ADC通道)和软件资源(内存、任务、事件组)。

ADC按键检测算法

ADC按键检测算法结合了定时采样和状态机机制,涉及外设层(Peripheral Layer)和底层驱动(Driver Layer)两个层次。下面分别介绍这两个层次的实现。

外设层事件处理实现(periph_adc_button.c)

外设层通过回调函数将底层驱动的按键状态转换为ESP-ADF的事件系统事件:

  1. 回调函数注册:在初始化时注册按键回调函数
  2. 状态转换:将底层驱动的按键状态转换为外设事件
  3. 事件分发:将按键事件分发到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_button.c)

底层ADC按键驱动实现在adc_button.c中,负责具体的ADC值采样、按键状态检测和事件生成。核心功能由三个函数实现:get_adc_voltageget_button_idget_adc_btn_state

get_adc_voltage函数

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函数

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函数

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
且click_cnt=ADC_BTN_DETECTED_CNT
ADC值不匹配按键ID
且click_cnt<长按阈值
持续按下
且click_cnt>=长按阈值
ADC值不匹配按键ID
状态处理完成
状态处理完成
active_id = -1
long_click = 0
click_cnt = 0
active_id = 按键ID
long_click = 0
click_cnt = ADC_BTN_DETECTED_CNT
返回 ADC_BTN_STATE_PRESSED
active_id = -1
long_click = 0
click_cnt = 0
返回 ADC_BTN_STATE_RELEASE
active_id = 按键ID
long_click = 1
click_cnt >= press_judge_time/ADC_BTN_DETECT_TIME_MS
返回 ADC_BTN_STATE_LONG_PRESSED
active_id = -1
long_click = 0
click_cnt = 0
返回 ADC_BTN_STATE_LONG_RELEASE
ADC按键检测算法关键点
  1. 多次采样滤波:通过对ADC通道进行多次采样,并使用排序滤波算法(去除最大值和最小值后取平均值),减少ADC读数的波动,提高检测稳定性。

  2. 按键ID映射:使用预设的ADC电压阈值范围,将ADC值映射到具体的按键ID,支持在同一个ADC通道上连接多个按键。

  3. 状态机设计:使用状态机设计模式,清晰地区分不同的按键状态(空闲、按下、释放、长按、长按释放),并确保状态转换的正确性。

  4. 防抖动处理:通过点击计数(click_cnt)和检测次数阈值(ADC_BTN_DETECTED_CNT)实现按键防抖,避免误触发。

  5. 长短按区分:通过时间阈值(press_judge_time)区分短按和长按,提供更丰富的交互方式。

  6. 多通道支持:支持同时检测多个ADC通道上的按键,通过链表结构管理多个通道的配置和状态。

  7. 芯片兼容性:代码中包含了对不同ESP32系列芯片的兼容处理,确保在不同平台上都能正常工作。

通过这种设计,ADC按键检测算法能够准确地检测各种按键事件,并将其传递给应用程序进行处理,为用户提供良好的交互体验。

ADC按键事件处理

ADC按键外设产生以下事件类型:

  • PERIPH_ADC_BUTTON_PRESSED:按键按下事件
  • PERIPH_ADC_BUTTON_RELEASE:按键释放事件
  • PERIPH_ADC_BUTTON_LONG_PRESSED:长按事件
  • PERIPH_ADC_BUTTON_LONG_RELEASE:长按后释放事件

事件数据为按键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));
    }
}

你可能感兴趣的:(ESP-ADF,架构,iot,嵌入式硬件)