1. 按键驱动重构:GPIO中断+手动去抖 → iot_button组件(单击/双击/长按) 2. 新增key_nav按键导航管理器:上下文状态机 + Set界面焦点蓝色边框高亮 3. 移除所有触摸手势/点击事件(ScreenHome/ScreenImg/ScreenSet) 4. 应援灯颜色切换优化:DISPOFF→直接写GRAM→DISPON,消除分band刷新 5. 亮度调节按键化:BOOT +10% / KEY -10% / KEY长按退出 6. 休眠管理适配:按键唤醒统一由key_nav处理 7. 新增迁移总结文档 docs/touch-to-button-migration.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 KiB
13 KiB
触屏版 → 按键版迁移总结
适用于 ESP32-C3 / ESP32-S3 电子吧唧项目,从触摸屏交互迁移到两键物理按键交互。
一、迁移背景
取消触摸芯片以降低硬件成本,所有用户交互通过两个物理按键实现:
- BOOT按键(GPIO9):确认/执行/返回
- KEY按键(GPIO8):导航/切换
二、变更文件清单
| 文件 | 变更类型 | 说明 |
|---|---|---|
main/button/button.c |
重构 | GPIO中断+手动去抖 → iot_button 组件 |
main/button/include/button.h |
重构 | 新增事件类型枚举,回调签名变更 |
main/key_nav/key_nav.c |
新增 | 按键导航管理器(核心模块) |
main/key_nav/include/key_nav.h |
新增 | 导航上下文和焦点状态枚举 |
main/main.c |
修改 | 移除 boot_btn_handler,集成 key_nav_init |
main/ui/screens/ui_ScreenHome.c |
修改 | 移除手势事件,界面导航交由 key_nav |
main/ui/screens/ui_ScreenHome.h |
修改 | 移除 ui_event_ScreenHome 声明 |
main/ui/screens/ui_ScreenImg.c |
修改 | 移除手势/点击事件,保留 SCREEN_LOADED 事件 |
main/ui/screens/ui_ScreenSet.c |
修改 | 移除手势/滑块/节能/删除点击事件,新增按键版颜色切换 |
main/ui/screens/ui_ScreenSet.h |
修改 | 新增 flashlight_switch_color/restart_blink 声明 |
main/lcd/lcd.c |
修改 | 新增 lcd_fill_color() 直接写 GRAM |
main/lcd/include/lcd.h |
修改 | 新增 lcd_fill_color() 声明 |
main/sleep_mgr/sleep_mgr.c |
修改 | 移除按键回调注册,交由 key_nav 统一处理 |
main/CMakeLists.txt |
修改 | 添加 key_nav 源文件和头文件路径 |
main/idf_component.yml |
修改 | 添加 button: ">=3.2.0" 依赖 |
sdkconfig |
修改 | 新增 IoT Button 配置项 |
三、核心架构变更
3.1 按键驱动重构(button模块)
触屏版:自定义 GPIO 中断 + FreeRTOS 队列 + 手动去抖(200ms时间戳判断)
// 旧方案
gpio_isr_handler_add(PIN_BTN_BOOT, gpio_isr_handler, (void *)PIN_BTN_BOOT);
xTaskCreate(btn_task, "btn_task", 3072, NULL, 5, NULL); // 队列消费任务
按键版:ESP-IDF 官方 iot_button 组件,原生支持单击/双击/长按
// 新方案
button_config_t btn_cfg = {
.long_press_time = 2000, // 长按2秒
.short_press_time = 0, // 使用默认180ms(双击检测窗口)
};
button_gpio_config_t gpio_cfg = {
.gpio_num = PIN_BTN_BOOT,
.active_level = 0, // 低电平有效
};
iot_button_new_gpio_device(&btn_cfg, &gpio_cfg, &boot_btn_handle);
iot_button_register_cb(boot_btn_handle, BUTTON_SINGLE_CLICK, NULL, boot_click_cb, NULL);
iot_button_register_cb(boot_btn_handle, BUTTON_DOUBLE_CLICK, NULL, boot_double_click_cb, NULL);
iot_button_register_cb(boot_btn_handle, BUTTON_LONG_PRESS_START, NULL, boot_long_press_cb, NULL);
回调签名变更:
// 旧版(只有按下事件)
typedef void (*btn_event_cb_t)(int gpio_num, void *usr_data);
// 新版(区分单击/双击/长按)
typedef enum {
BTN_EVT_CLICK,
BTN_EVT_DOUBLE_CLICK,
BTN_EVT_LONG_PRESS,
} btn_event_type_t;
typedef void (*btn_event_cb_t)(int gpio_num, btn_event_type_t event, void *usr_data);
注册接口变更:
// 旧版
button_on_boot_press(cb, usr_data);
button_on_key2_press(cb, usr_data);
// 新版
button_on_boot_event(cb, usr_data);
button_on_key2_event(cb, usr_data);
idf_component.yml 依赖:
dependencies:
button: ">=3.2.0"
3.2 新增按键导航管理器(key_nav模块)
这是本次迁移的核心新增模块,集中管理所有按键行为和界面导航逻辑。
设计思想:
- 上下文状态机:根据当前界面/模式决定按键行为
- 焦点管理系统:Set界面的图标选中和蓝色边框高亮
- 任务派发模式:iot_button 回调在 esp_timer 上下文中执行(不能 vTaskDelay),通过 xTaskCreate 派发到独立任务
上下文枚举:
typedef enum {
NAV_CTX_HOME, // Home界面
NAV_CTX_IMG, // Img界面(正常浏览)
NAV_CTX_IMG_DELETE, // Img界面(删除模式)
NAV_CTX_SET, // Set界面(焦点导航)
NAV_CTX_SET_BRIGHTNESS, // Set界面(亮度调节模式)
NAV_CTX_FLASHLIGHT, // 应援灯全屏模式
} nav_context_t;
Set界面焦点枚举:
typedef enum {
SET_FOCUS_NONE = -1, // 无选中
SET_FOCUS_LOW_POWER = 0,// 节能
SET_FOCUS_FLASHLIGHT, // 应援灯
SET_FOCUS_DELETE, // 删除
SET_FOCUS_BRIGHTNESS, // 亮度
SET_FOCUS_COUNT, // 焦点总数(用于循环)
} set_focus_item_t;
任务派发模式(关键模式,必须遵循):
// iot_button 回调在 esp_timer 上下文中,不能 vTaskDelay
// 必须派发到独立 FreeRTOS 任务执行需要延时的操作
static void dispatch_task(TaskFunction_t func, const char *name)
{
xTaskCreate(func, name, 3072, NULL, 5, NULL);
}
static void boot_event_handler(int gpio_num, btn_event_type_t event, void *usr_data)
{
if (event == BTN_EVT_CLICK) {
dispatch_task(nav_task_home_boot_click, "h_boot"); // 派发到独立任务
}
}
焦点高亮实现:
#define FOCUS_BORDER_COLOR 0x2196F3 // Material Blue
#define FOCUS_BORDER_WIDTH 3
static void set_focus_border(int index) {
lv_obj_set_style_border_color(obj, lv_color_hex(FOCUS_BORDER_COLOR), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, FOCUS_BORDER_WIDTH, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_opa(obj, LV_OPA_COVER, LV_PART_MAIN | LV_STATE_DEFAULT);
}
3.3 各界面触摸事件移除
ScreenHome
- 移除
ui_event_ScreenHome()函数(下滑→Set、左滑/右滑→Img 手势) - 移除
lv_obj_add_event_cb(ui_ScreenHome, ui_event_ScreenHome, LV_EVENT_ALL, NULL) - 移除
#include "ui_ScreenSet.h"依赖
ScreenImg
- 移除
ui_event_ImageDel()(删除按钮点击) - 移除
ui_event_ImageReturn()(返回按钮点击) - 移除所有手势事件(上滑→Home、下滑→Set、左滑→下一张、右滑→上一张)
- 保留
LV_EVENT_SCREEN_LOADED事件(首次加载图片初始化) - 保留
should_show_container标志逻辑(从Set删除图标进入时显示) - 移除
lv_obj_add_event_cb对 ImageDel 和 ImageReturn 的注册
ScreenSet
- 移除
ui_event_ScreenSet()(上滑返回手势) - 移除
ui_event_SliderBrightness()(滑块值变更事件) - 移除
ui_event_ImgLowPower()(节能图标点击事件) - 移除
ui_event_ImgDelete()(删除图标点击事件) - 保留
ui_event_ImgFlashlight()的LV_EVENT_CLICKED(由 key_nav 通过lv_event_send触发) - 移除
lv_obj_add_event_cb对节能、删除、滑块、手势的注册
四、新增功能和优化
4.1 按键导航系统
| 界面 | BOOT单击 | KEY单击 | BOOT双击 | KEY长按 |
|---|---|---|---|---|
| Home | →Set | →Img | - | - |
| Img(浏览) | →Home | 下一张图 | →Home | - |
| Img(删除) | 确认删除 | 取消删除 | →Home | - |
| Set(无焦点) | →Home | 选中第一个 | →Home | - |
| Set(有焦点) | 激活功能 | 下一个焦点 | →Home | - |
| Set(亮度调节) | 亮度+10% | 亮度-10% | →Home | 退出调节 |
| 应援灯 | 切换颜色 | 退出→Set | →Home | - |
4.2 Set界面焦点蓝色边框高亮
- 用 LVGL
border样式实现焦点指示(Material Blue #2196F3) - 焦点切换时自动清除旧边框、设置新边框
- 离开界面或返回 Home 自动清除所有边框
- 进入删除模式时,ImageDel 控件也显示蓝色边框提示
4.3 应援灯颜色切换优化
触屏版直接修改 LVGL 样式,LVGL 30行分band渲染导致从上到下的视觉刷新感。
按键版优化为 LCD 硬件级切换:
void flashlight_switch_color(void) {
lcd_disp_on_off(false); // DISPOFF 0x28:LCD停止输出
lcd_fill_color(new_color); // 直接写GRAM,绕过LVGL(~35ms同步阻塞)
lcd_disp_on_off(true); // DISPON 0x29:LCD瞬间恢复,GRAM已完整
}
新增 lcd_fill_color() 函数:
// 绕过 LVGL 分band渲染,直接用 esp_lcd_panel_draw_bitmap 写 GRAM
// 40行为单位,DMA 内存,同步阻塞写入
void lcd_fill_color(uint32_t color_rgb) {
lv_color_t c = lv_color_hex(color_rgb); // RGB888→RGB565(含byte swap)
uint16_t *buf = heap_caps_malloc(LCD_WID * 40 * sizeof(uint16_t), MALLOC_CAP_DMA);
for (int y = 0; y < LCD_HIGH; y += 40) {
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, LCD_WID, y + lines, buf);
}
heap_caps_free(buf);
}
4.4 亮度调节按键化
触屏版通过滑块拖动调节。按键版改为:
- BOOT单击 +10%,KEY单击 -10%
- 范围 10%~100%
- 同步更新滑块控件值和百分比标签
- KEY长按2秒退出调节模式
4.5 休眠管理适配
- 移除 sleep_mgr 中的按键回调注册(旧版在 sleep_mgr_init 中注册 KEY2 回调)
- 按键唤醒逻辑统一由 key_nav 处理:
if (sleep_mgr_is_screen_off()) { sleep_mgr_notify_activity(); // 唤醒屏幕 return; // 仅唤醒,不触发业务 } sleep_mgr_notify_activity(); // 重置休眠计时器
4.6 main.c 简化
- 移除 ~50 行的
boot_btn_handler()函数(手电筒退出+界面切换+亮度恢复逻辑) - 所有按键处理逻辑集中到 key_nav 模块
- 初始化顺序调整:
button_init()→sleep_mgr_init()→key_nav_init()
五、关键技术要点(迁移到 S3 时注意)
5.1 iot_button 状态机特性
BUTTON_SINGLE_CLICK和BUTTON_DOUBLE_CLICK互斥,不会同时触发- 按键释放后等待
short_press_time(默认180ms)决定是单击还是双击 short_press_time控制双击检测窗口,不是去抖时间(去抖由CONFIG_BUTTON_DEBOUNCE_TICKS控制,默认10ms)- 设为 0 表示使用默认值 180ms
5.2 iot_button 回调上下文
- 回调在
esp_timer任务中执行 - 禁止在回调中调用
vTaskDelay(),否则阻塞 esp_timer 任务,导致 LVGL tick 停止 - 解决方案:通过
xTaskCreate派发到独立 FreeRTOS 任务
5.3 LVGL 操作线程安全
- 修改 UI 必须加锁:
lvgl_port_lock(timeout_ms)/lvgl_port_unlock() - 建议超时 50-100ms,避免死锁
- 永久等待用
lvgl_port_lock(-1)
5.4 手电筒退出时序
亮度→0 → flashlight_exit() → vTaskDelay(80ms) → 切换界面 → vTaskDelay(150ms) → 恢复亮度
- 80ms 等待 overlay 删除(LVGL 需要 15+ 刷新周期)
- 150ms 等待新界面渲染完成
- 先切换界面再恢复亮度,避免用户看到旧界面
5.5 GPIO 引脚适配
S3 项目需要根据实际硬件修改 button.h 中的 GPIO 定义:
#define PIN_BTN_BOOT 9 // 根据S3硬件原理图修改
#define PIN_BTN_KEY2 8 // 根据S3硬件原理图修改
5.6 sdkconfig 配置
新增 IoT Button 相关配置(menuconfig 或直接修改 sdkconfig):
CONFIG_BUTTON_PERIOD_TIME_MS=5
CONFIG_BUTTON_DEBOUNCE_TICKS=2
CONFIG_BUTTON_SHORT_PRESS_TIME_MS=180
CONFIG_BUTTON_LONG_PRESS_TIME_MS=1500
六、迁移步骤(适用于 ESP32-S3 项目)
- 添加依赖:
idf_component.yml添加button: ">=3.2.0" - 重构 button 模块:替换 GPIO 中断为 iot_button,修改回调签名
- 新建 key_nav 模块:复制
main/key_nav/目录,根据 S3 项目的界面结构调整上下文枚举和任务函数 - 移除触摸事件:各 Screen 文件中移除手势事件函数和
lv_obj_add_event_cb注册 - 适配 main.c:移除旧的按键处理函数,添加
key_nav_init()调用 - 适配 sleep_mgr:移除按键回调注册,由 key_nav 统一处理唤醒
- 添加 lcd_fill_color()(如需应援灯颜色切换优化)
- 更新 CMakeLists.txt:添加 key_nav 源文件和头文件路径
- 适配 GPIO 引脚:根据 S3 硬件原理图修改按键 GPIO 定义
- 编译测试:
idf.py build验证,逐个功能测试
七、资源变化
| 指标 | 触屏版 | 按键版 | 变化 |
|---|---|---|---|
| button 任务栈 | 3072B(btn_task 队列消费) | 0(iot_button 内部管理) | 节省 3KB |
| key_nav 任务栈 | 无 | 3072B(每次按键临时创建,执行完销毁) | 临时占用 |
| 触摸事件代码 | ~200 行(手势+点击回调) | 0 | 移除 |
| key_nav 代码 | 0 | ~530 行 | 新增 |
| iot_button 组件 | 无 | ~20KB Flash | 新增依赖 |