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>
332 lines
13 KiB
Markdown
332 lines
13 KiB
Markdown
# 触屏版 → 按键版迁移总结
|
||
|
||
> 适用于 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 | 新增依赖 |
|