# 触屏版 → 按键版迁移总结 > 适用于 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时间戳判断) ```c // 旧方案 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` 组件,原生支持单击/双击/长按 ```c // 新方案 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); ``` **回调签名变更**: ```c // 旧版(只有按下事件) 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); ``` **注册接口变更**: ```c // 旧版 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 依赖**: ```yaml dependencies: button: ">=3.2.0" ``` ### 3.2 新增按键导航管理器(key_nav模块) 这是本次迁移的**核心新增模块**,集中管理所有按键行为和界面导航逻辑。 **设计思想**: - 上下文状态机:根据当前界面/模式决定按键行为 - 焦点管理系统:Set界面的图标选中和蓝色边框高亮 - 任务派发模式:iot_button 回调在 esp_timer 上下文中执行(不能 vTaskDelay),通过 xTaskCreate 派发到独立任务 **上下文枚举**: ```c 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界面焦点枚举**: ```c 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; ``` **任务派发模式**(关键模式,必须遵循): ```c // 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"); // 派发到独立任务 } } ``` **焦点高亮实现**: ```c #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 硬件级切换**: ```c 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()` 函数: ```c // 绕过 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 处理: ```c 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 定义: ```c #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 项目) 1. **添加依赖**:`idf_component.yml` 添加 `button: ">=3.2.0"` 2. **重构 button 模块**:替换 GPIO 中断为 iot_button,修改回调签名 3. **新建 key_nav 模块**:复制 `main/key_nav/` 目录,根据 S3 项目的界面结构调整上下文枚举和任务函数 4. **移除触摸事件**:各 Screen 文件中移除手势事件函数和 `lv_obj_add_event_cb` 注册 5. **适配 main.c**:移除旧的按键处理函数,添加 `key_nav_init()` 调用 6. **适配 sleep_mgr**:移除按键回调注册,由 key_nav 统一处理唤醒 7. **添加 lcd_fill_color()**(如需应援灯颜色切换优化) 8. **更新 CMakeLists.txt**:添加 key_nav 源文件和头文件路径 9. **适配 GPIO 引脚**:根据 S3 硬件原理图修改按键 GPIO 定义 10. **编译测试**:`idf.py build` 验证,逐个功能测试 --- ## 七、资源变化 | 指标 | 触屏版 | 按键版 | 变化 | |------|--------|--------|------| | button 任务栈 | 3072B(btn_task 队列消费) | 0(iot_button 内部管理) | 节省 3KB | | key_nav 任务栈 | 无 | 3072B(每次按键临时创建,执行完销毁) | 临时占用 | | 触摸事件代码 | ~200 行(手势+点击回调) | 0 | 移除 | | key_nav 代码 | 0 | ~530 行 | 新增 | | iot_button 组件 | 无 | ~20KB Flash | 新增依赖 |