ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ESP32打造HUD风格番茄钟:手势控制与自动转屏实战

ESP32打造HUD风格番茄钟:手势控制与自动转屏实战 大家在做 ESP32 桌面小项目时经常遇到三类问题屏幕界面太单调像开发板出厂测试程序想加交互功能但按钮和菜单做起来又重又乱屏幕方向固定设备横着放竖着放都要重新改代码。这篇文章分享一个综合实战方案用 ESP32 驱动 2.8 寸 SPI 屏实现一套 HUD 风格的番茄钟配合手势切换工作/休息模式再通过读取板载姿态传感器实现自动转屏。整个过程会拆解到尾插接线、依赖库、核心源码和排错思路新手可以照着搭有基础的可以直接抄 UI 框架。1. 项目背景与功能拆解1.1 这个项目解决什么问题先解释一下标题里的几个关键词方便对号入座Deep Warp HUD指一种偏科幻风格的界面设计通常包含大号数字、高对比配色、轻微扫光或描边效果适合做时钟、计时器、仪表盘类桌面摆件。在 2.8 寸屏幕上这种风格比默认的矩形色块好看很多。番茄钟Pomodoro Timer一种时间管理方法默认 25 分钟专注加 5 分钟休息。项目核心是让屏幕上实时显示剩余时间模式切换时用视觉或声音提示。手势控制在本文中手势不是摄像头识别而是通过触摸屏滑动或按键组合触发切换。触摸屏方案更贴近工程落地也更容易在源码中理解事件处理结构。自动转屏读取 ESP32 板载 IMU惯性测量单元的姿态数据判断屏幕当前是横放还是竖放动态旋转 UI 坐标让画面始终正立。1.2 功能范围与技术栈本文示例项目包含以下核心功能2.8 寸 ILI9341 SPI 屏幕驱动使用 TFT_eSPI 库。番茄钟状态机空闲、专注、休息、暂停。触摸手势识别左滑切换模式、右滑重置、长按进入设置。自动旋转屏幕利用 MPU6050 或 ESP32 板载 IMU 数据动态判断方向。HUD 风格 UI包含时间进度环、模式标签、状态文字。技术栈为开发板ESP32 DevKitC V4 或兼容板ESP32-WROOM-32。屏幕2.8 寸 SPI 屏主控 ILI9341分辨率 320x240。触摸XPT2046 电阻触摸控制器常见于集成触摸的 2.8 寸模块。姿态传感器MPU6050I2C 接口或使用 ESP32 部分开发板自带的 IMU。开发环境Arduino IDE配合 Espressif ESP32 开发包。2. 环境准备与接线方案2.1 Arduino IDE 环境搭建ESP32 开发板支持 Arduino 框架整体上手成本比 ESP-IDF 低适合做显示类、控制类项目。第一步安装 Arduino IDE建议使用 2.x 以上版本界面和库管理都更友好。下载后正常安装即可。第二步添加 ESP32 开发板地址打开 Arduino IDE进入「文件 - 首选项 - 附加开发板管理器网址」添加以下地址https://espressif.github.io/arduino-esp32/package_esp32_index.json如果国内网络访问慢可以手动下载 Espressif 的离线包或使用社区镜像将 JSON 地址替换为镜像地址然后在「开发板管理器」里搜索 ESP32选择对应版本安装。第三步安装离线包可选如果在线安装长期失败可以在 GitHub 或其他可靠渠道下载arduino-esp32离线安装包按压缩包内说明拷贝到 Arduino 的hardware/espressif目录。注意版本要与 Arduino IDE 兼容推荐 3.0.x 或 2.0.x 系列中的一个稳定版具体以你下载的包说明为准。装完后在「开发板」中选择ESP32 Dev Module。2.2 硬件清单与接线本文涉及的模块清单如下ESP32 开发板 x12.8 寸 SPI TFT 屏ILI9341 XPT2046 x1MPU6050 模块 x1若板载 IMU 则不需要杜邦线若干面包板或 PCB 固定板屏幕和 ESP32 的接线需要特别注意 SPI 引脚冲突。常见的 TFT_eSPI 配置使用如下引脚TFT 引脚ESP32 GPIOTFT_SCLKGPIO 18TFT_MOSIGPIO 23TFT_CSGPIO 5TFT_DCGPIO 2TFT_RSTGPIO 4TFT_BLGPIO 15或接 3.3V触摸引脚如果模块集成 XPT2046触摸引脚ESP32 GPIOT_IRQGPIO 36T_DOGPIO 19T_DINGPIO 23与 TFT_MOSI 共用T_CSGPIO 22T_CLKGPIO 18与 TFT_SCLK 共用MPU6050 接线MPU6050ESP32 GPIOVCC3.3VGNDGNDSCLGPIO 22SDAGPIO 21实际接线时应根据你手里屏幕的丝印标识调整。很多模块把 VCC 标为 5V但逻辑电平是 3.3V建议先查看模块手册确认。2.3 安装所需库在 Arduino IDE 的库管理器中搜索并安装TFT_eSPI用于驱动 ILI9341 等屏幕内置字体和图形渲染性能优秀。XPT2046_Touchscreen读取触摸坐标。MadgwickAHRS或Adafruit MPU6050用于姿态解算和传感器读取。如果使用 TFT_eSPI需要修改库目录下的User_Setup.h确认启用 ILI9341 驱动并将引脚配置为上面的接线值。不同版本的 TFT_eSPI 配置方式略有差异以你下载版本的注释说明为准。3. 系统架构与核心原理3.1 界面架构分层刷新2.8 寸屏分辨率是 320x240刷新率不高不能像手机那样全屏高速刷新。HUD 风格的番茄钟应该把界面拆成三层背景层绘制网格、边框、半透明色块只在初始化或转屏时重绘。数据层时间数字、进度条、模式文字每秒或每事件更新。特效层扫光、闪烁、切换动画作为装饰性元素更新频率可以降低。这样做的好处是减少 SPI 数据传输量避免界面闪烁。3.2 番茄钟状态机番茄钟逻辑看起来简单但如果没有明确状态机很容易写出判断混乱的代码。本文采用如下状态机IDLE待机显示当前时间或上次完成记录。FOCUS专注倒计时默认 25 分钟。BREAK休息倒计时默认 5 分钟。PAUSED暂停状态保留剩余时间。状态之间转换关系为IDLE 启动进入 FOCUSFOCUS 正常倒计时结束进入 BREAKBREAK 结束回到 IDLE 或自动开始下一个 FOCUSPAUSED 可由任意倒计时状态进入恢复后回到原状态。这套状态机用枚举表示配合一个tick()函数处理秒级递减。3.3 手势识别原理触摸屏手势识别的核心不是“看懂动作”而是“识别轨迹模式”。最简单可靠的手势是滑动方向识别记录触摸按下时的起始坐标。在触摸移动过程中持续更新当前坐标。松开后对比起始坐标和结束坐标。如果横向位移大于纵向位移且超过阈值则判定为左右滑动。阈值建议设为屏幕宽度的 1/4也就是 80 像素左右。为了防止误触还可以加入时间窗口超过 1.5 秒的拖动不算滑动。3.4 自动转屏原理自动转屏通过读取 MPU6050 的重力加速度分量来判断屏幕方向。当设备平稳放置时重力在三个轴上的分量可以反映屏幕的姿态屏幕正立竖直放置Z 轴接近 1g其他轴接近 0。屏幕顺时针旋转 90°重力分量主要落在 X 轴或 Y 轴。屏幕倒置Z 轴接近 -1g。更通用的做法是用 Madgwick 滤波或 Mahony 滤波解算出四元数再转成欧拉角根据 roll 和 pitch 判断方向。本文为了控制代码量使用简化的加速度分量判断避免复杂滤波但会说明如何扩展到完整姿态解算。4. 完整实战案例4.1 项目结构本文示例采用单文件方式便于新手阅读但代码按功能分块实际工程建议拆分为多文件esp32_pomodoro_hud/ ├── esp32_pomodoro_hud.ino ├── ui.h ├── touch.h ├── imu.h └── config.h下面讲解过程中代码全部围绕核心模块展开。4.2 编写主程序框架先创建esp32_pomodoro_hud.ino实现初始化、主循环和状态机。这里给出一个可以编译通过的顶层框架重点展示结构。// 文件路径esp32_pomodoro_hud/esp32_pomodoro_hud.ino #include TFT_eSPI.h #include XPT2046_Touchscreen.h #include Wire.h #include MPU6050.h #include config.h #include ui.h #include touch.h #include imu.h TFT_eSPI tft; XPT2046_Touchscreen ts(TOUCH_CS_PIN); MPU6050 mpu; // 番茄钟状态 enum PomodoroState { IDLE, FOCUS, BREAK, PAUSED }; PomodoroState state IDLE; unsigned long lastTick 0; int remainingSeconds FOCUS_DURATION_SECONDS; int totalSeconds FOCUS_DURATION_SECONDS; void setup() { Serial.begin(115200); Wire.begin(); tft.init(); tft.setRotation(0); tft.fillScreen(TFT_BLACK); ts.begin(); ts.setRotation(0); if (!mpu.begin()) { Serial.println(MPU6050 not found!); } initIMU(); uiDrawBackground(); } void loop() { handleTouch(); if (state FOCUS || state BREAK) { handleTimerTick(); } updateScreen(); handleAutoRotation(); delay(20); }这里有几个注意点TOUCH_CS_PIN在config.h中定义。屏幕和触摸的旋转方向在自动转屏时会被修改初始值可以保持一致。handleTimerTick()应该基于millis()做非阻塞计时而不是delay()。4.3 配置文件config.h里集中管理引脚和参数方便后续调整硬件接线。// 文件路径esp32_pomodoro_hud/config.h #ifndef CONFIG_H #define CONFIG_H // SPI 屏幕引脚 #define TFT_SCLK_PIN 18 #define TFT_MOSI_PIN 23 #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 #define TFT_BL_PIN 15 // 触摸引脚 #define TOUCH_CS_PIN 22 #define TOUCH_IRQ_PIN 36 // 番茄钟时长秒 #define FOCUS_DURATION_SECONDS 25 * 60 #define BREAK_DURATION_SECONDS 5 * 60 // 手势阈值 #define SWIPE_THRESHOLD 80 #endif如果你的屏幕模块和引脚不同只需要改这里不需要动主逻辑。4.4 UI 层实现UI 层负责绘制 HUD 风格界面。核心思路是先用图形函数画出静态背景再在刷新时只更新动态部分。下面给出ui.h的接口和部分实现。// 文件路径esp32_pomodoro_hud/ui.h #ifndef UI_H #define UI_H #include TFT_eSPI.h extern TFT_eSPI tft; void uiDrawBackground(); void uiDrawTime(int seconds); void uiDrawProgress(float progress); void uiDrawMode(const char* mode); void uiDrawHUD(); #endif对应实现文件ui.cpp或直接写在.ino文件末尾// 文件路径esp32_pomodoro_hud/ui.cpp节选 #define HUD_GREEN 0x7FE0 #define HUD_BLUE 0x841F #define HUD_ORANGE 0xFD20 void drawRoundRectHelper(int x, int y, int w, int h, int r, uint16_t color) { tft.drawRoundRect(x, y, w, h, r, color); } void uiDrawBackground() { tft.fillScreen(TFT_BLACK); // 绘制外边框和 HUD 网格 tft.drawRect(5, 5, 310, 230, HUD_BLUE); for (int i 10; i 320; i 20) { tft.drawPixel(i, 230, HUD_BLUE); } // 模式标签区域 tft.setTextDatum(MC_DATUM); tft.setTextColor(HUD_BLUE, TFT_BLACK); tft.drawString(POMODORO, 160, 220, 2); } void uiDrawTime(int seconds) { char buf[8]; int min seconds / 60; int sec seconds % 60; sprintf(buf, %02d:%02d, min, sec); // 清除上一次时间区域保留背景 tft.fillRect(60, 60, 200, 90, TFT_BLACK); tft.setTextColor(HUD_GREEN, TFT_BLACK); tft.setTextDatum(MC_DATUM); tft.drawString(buf, 160, 100, 7); } void uiDrawProgress(float progress) { // 进度条从 0 到 280 int barWidth (int)(280 * progress); tft.fillRect(20, 180, 280, 12, TFT_BLACK); tft.fillRect(20, 180, barWidth, 12, HUD_ORANGE); tft.drawRect(20, 180, 280, 12, HUD_BLUE); } void uiDrawMode(const char* mode) { tft.fillRect(100, 20, 120, 30, TFT_BLACK); tft.setTextDatum(MC_DATUM); tft.setTextColor(HUD_BLUE, TFT_BLACK); tft.drawString(mode, 160, 35, 2); }UI 层的关键点字体大小选择要匹配分辨率。2.8 寸屏在 240 高度下7 号字体适合显示大时间。每次更新数据前用fillRect清除旧的数字避免残影。HUD 风格不需要花哨关键是颜色对比和布局留白。4.5 触摸手势实现实现触摸手势时需要记录触摸状态、起始坐标和结束坐标。下面给出手势识别核心逻辑。// 文件路径esp32_pomodoro_hud/touch.h #ifndef TOUCH_H #define TOUCH_H #include XPT2046_Touchscreen.h enum Gesture { GESTURE_NONE, GESTURE_LEFT, GESTURE_RIGHT, GESTURE_LONG_PRESS }; void initTouch(int rotation); Gesture readGesture(); #endif对应的touch.cpp实现// 文件路径esp32_pomodoro_hud/touch.cpp #include touch.h #include config.h XPT2046_Touchscreen ts(TOUCH_CS_PIN); Gesture currentGesture GESTURE_NONE; uint16_t startX, startY; uint16_t endX, endY; bool isTouching false; unsigned long touchStartMs 0; void initTouch(int rotation) { ts.begin(); ts.setRotation(rotation); } Gesture readGesture() { currentGesture GESTURE_NONE; if (ts.tirqTouched() ts.touched()) { TS_Point p ts.getPoint(); // 根据屏幕方向做坐标映射这里的映射需要结合触摸旋转校准 uint16_t x map(p.x, 200, 3800, 0, 320); uint16_t y map(p.y, 240, 3800, 0, 240); if (!isTouching) { isTouching true; touchStartMs millis(); startX x; startY y; endX x; endY y; } else { endX x; endY y; } } else { if (isTouching) { isTouching false; unsigned long duration millis() - touchStartMs; // 长按判断 if (duration 1000) { currentGesture GESTURE_LONG_PRESS; return currentGesture; } int dx endX - startX; int dy endY - startY; if (abs(dx) abs(dy) abs(dx) SWIPE_THRESHOLD) { currentGesture (dx 0) ? GESTURE_RIGHT : GESTURE_LEFT; } } } return currentGesture; }触摸坐标映射是新手最容易踩坑的地方。XPT2046 的原始坐标范围通常在几百到四千之间不同屏幕模块差异很大需要根据实际输出调整map()的输入范围。建议先用串口打印原始坐标观察四个角的数值后再设置映射参数。4.6 姿态检测与自动转屏MPU6050 读取加速度计的代码如下// 文件路径esp32_pomodoro_hud/imu.h #ifndef IMU_H #define IMU_H void initIMU(); int getScreenRotation(); #endif实现文件中通过加速度分量判断方向// 文件路径esp32_pomodoro_hud/imu.cpp #include imu.h #include Wire.h #include MPU6050.h MPU6050 mpu; void initIMU() { Wire.begin(); if (!mpu.begin()) { Serial.println(MPU6050 connection failed); } mpu.setAccelerometerRange(MPU6050_RANGE_8_G); } int getScreenRotation() { sensors_event_t a, g, temp; mpu.getEvent(a, g, temp); float x a.acceleration.x; float y a.acceleration.y; float z a.acceleration.z; // 用重力分量判断屏幕方向 if (fabs(z) fabs(x) fabs(z) fabs(y)) { return z 0 ? 0 : 2; // 正放 0倒置 2 } else if (fabs(x) fabs(y)) { return x 0 ? 1 : 3; // 右转 1左转 3 } else { return y 0 ? 3 : 1; // 根据实际传感器安装调整 } }这里需要特别说明MPU6050 在开发板上的安装方向不同会造成坐标轴不同因此返回值需要根据实际模块调整。最稳妥的方法是把模块四个方向放置通过串口打印a.acceleration.x/y/z的数值确认对应关系后再优化判断逻辑。主循环中检测屏幕旋转的完整逻辑void handleAutoRotation() { int rot getScreenRotation(); static int lastRot -1; if (rot ! lastRot) { lastRot rot; tft.setRotation(rot); ts.setRotation(rot); // 旋转后必须重绘整个界面 uiDrawBackground(); updateScreen(); } }这里有一个重要工程细节转屏后只修改setRotation是不够的旧画面的坐标不会自动重算必须调用fillScreen清屏并重绘。如果不清屏屏幕上会出现旋转残留的色块。4.7 番茄钟逻辑完善前面已经给出了状态机和计时骨架下面补全时钟逻辑和手势动作映射。void handleTimerTick() { unsigned long now millis(); if (now - lastTick 1000) { lastTick now; remainingSeconds--; if (remainingSeconds 0) { if (state FOCUS) { state BREAK; remainingSeconds BREAK_DURATION_SECONDS; totalSeconds remainingSeconds; uiDrawMode(BREAK); } else if (state BREAK) { state IDLE; remainingSeconds FOCUS_DURATION_SECONDS; totalSeconds remainingSeconds; uiDrawMode(READY); } } } } void handleTouch() { Gesture g readGesture(); if (g GESTURE_LEFT) { // 向左滑开始或切换模式 if (state IDLE) { state FOCUS; remainingSeconds FOCUS_DURATION_SECONDS; totalSeconds remainingSeconds; lastTick millis(); uiDrawMode(FOCUS); } else if (state FOCUS) { // 手动切换到休息 state BREAK; remainingSeconds BREAK_DURATION_SECONDS; totalSeconds remainingSeconds; lastTick millis(); uiDrawMode(BREAK); } else if (state BREAK) { state FOCUS; remainingSeconds FOCUS_DURATION_SECONDS; totalSeconds remainingSeconds; lastTick millis(); uiDrawMode(FOCUS); } } else if (g GESTURE_RIGHT) { // 向右滑返回待机 state IDLE; uiDrawMode(READY); } else if (g GESTURE_LONG_PRESS) { // 长按暂停或恢复 if (state FOCUS || state BREAK) { state PAUSED; uiDrawMode(PAUSED); } else if (state PAUSED) { state (totalSeconds FOCUS_DURATION_SECONDS) ? FOCUS : BREAK; lastTick millis(); } } }注意 PAUSED 状态下handleTimerTick判断条件不会执行所以时间会停止。恢复时要重置lastTick否则下一秒会瞬间扣掉暂停期间的时间。4.8 刷新屏幕updateScreen()负责根据状态动态刷新数据区和进度条void updateScreen() { static int lastRemaining -1; static PomodoroState lastState IDLE; if (lastRemaining ! remainingSeconds) { lastRemaining remainingSeconds; uiDrawTime(remainingSeconds); } if (lastState ! state) { lastState state; if (state FOCUS) { uiDrawMode(FOCUS); } else if (state BREAK) { uiDrawMode(BREAK); } else if (state IDLE) { uiDrawMode(READY); } else if (state PAUSED) { uiDrawMode(PAUSED); } } // 进度条每次刷新 float progress 1.0 - ((float)remainingSeconds / totalSeconds); uiDrawProgress(progress); }这种方式避免了每帧都重绘全部内容SPI 总线压力小屏幕刷新时也不会明显闪烁。5. 编译与烧录验证5.1 编译前检查在 Arduino IDE 中点击「项目 - 导出已编译的二进制文件」前先确认以下几项TFT_eSPI 库的User_Setup.h是否已经修改引脚。开发板选择是否准确比如ESP32 Dev Module。Flash Size 是否和开发板一致常见为 4MB。如果使用离线包确保版本与 IDE 匹配。如果编译报错提示找不到MPU6050.h或XPT2046_Touchscreen.h说明库没有安装成功回到库管理器重新搜索安装。5.2 烧录后现象烧录成功后打开串口监视器波特率 115200应该看到MPU6050 not found!或初始化成功信息。如果 MPU6050 初始化失败检查I2C 引脚是否接反SCL 和 SDA 不能互换。模块供电是否是 3.3V部分模块需要 5V 供电但逻辑电平是 3.3V。是否多个 I2C 设备地址冲突。屏幕正常显示后触摸屏幕向左滑动番茄钟应该进入专注状态左上角或顶部模式标签变成FOCUS中间出现 25:00 倒计时。将开发板竖起来或横放屏幕方向应跟随变化HUD 布局保持正立。5.3 典型输出预期串口输出样例具体数值因模块而异MPU6050 connection ok Touch: x2100, y3800 Gesture: LEFT Timer tick: 1499 Screen rotation changed to 1这些输出有助于调试触摸坐标映射和姿态判断。6. 常见问题与排查思路6.1 屏幕白屏或花屏问题现象常见原因解决思路白屏SPI 引脚错误或 TFT_CS 未拉低检查 User_Setup.h 引脚配置花屏屏幕初始化参数错误确认 TFT_eSPI 是否选择 ILI9341 驱动颜色不对屏幕 RGB 顺序不同在 User_Setup.h 中调整 TFT_RGB_ORDER亮度异常背光引脚未控制将 TFT_BL 接高电平或 PWM 输出白屏最直接的排查方法是测量 SPI 信号确认 MOSI、SCLK 是否有波形。没有示波器时可以先写一个点亮全屏红色的测试程序排除接线问题。6.2 触摸坐标偏移触摸不准是最常见的问题。XPT2046 的原始坐标范围不是固定的常见范围如下X 轴约 200 到 3800Y 轴约 240 到 3800但这个范围会因模块批次不同而变化。排查步骤去掉map()映射直接打印p.x和p.y。触摸屏幕四个角记录原始值。根据记录的四个角最小值、最大值修正map()范围。加入触摸校准流程在开机时让用户点击四个角。另外触摸方向和屏幕方向要一致。ts.setRotation()的值和tft.setRotation()的值必须同步修改否则触摸坐标会整体偏移 90 度。6.3 自动转屏不灵敏或方向反问题现象常见原因解决思路转屏不灵敏加速度阈值判断抖动加入滞回比较旋转变化至少持续 1 秒方向相反MPU6050 安装方向不同通过串口打印加速度值调整判断公式旋转后 UI 错乱忘记清屏重绘旋转后必须 fillScreen 并调用 uiDrawBackground自动转屏建议加入防抖逻辑只有连续读到的旋转值相同且持续 500ms 以上才真正切换屏幕方向。6.4 番茄钟时间不准时间不准通常是lastTick处理不当导致的。核心原因lastTick在暂停/恢复时没有重置。主循环里有多处delay()导致millis()更新频率不稳定。使用delay(1000)做计时循环中其他操作耗时太长实际间隔超过 1 秒。解决办法使用非阻塞计时所有延迟都改成delay(20)或更短依赖millis()判断秒数变化。6.5 编译报错常见编译错误及对策错误信息原因与对策TFT_eSPI.h: No such file or directory库未安装或 User_Setup.h 配置不完整MPU6050 not found编译成功但运行时初始化失败检查接线Unknown type name TS_PointXPT2046 库未包含检查#include7. 最佳实践与工程建议7.1 代码组织与模块化虽然示例代码为了便于阅读放在单文件但真实项目建议按功能拆成多个文件。基本分层思路硬件抽象层DisplayDriver、TouchDriver、IMUSensor封装底层库调用。业务逻辑层PomodoroStateMachine只关注状态转换和时间计算。表现层HUDView根据状态和时间刷新界面。应用层AppController协调各模块。这样做的好处是以后换屏幕、换传感器时只需要替换底层封装不需要重写业务逻辑。7.2 UI 绘制优化2.8 寸屏性能有限绘制时注意优先使用fillRect而不是drawLine去清区域。文字刷新前先清除旧区域但清除范围不能太大否则会闪烁。静态元素在初始化时画好刷新时保留。进度条渐变动画时每帧只画变化的部分不要全条重画。7.3 计时与功耗番茄钟长期运行需要注意屏幕背光如果一直开启功耗会偏高。可以在 IDLE 状态降低背光亮度。如果使用电池供电增加深度睡眠模式在倒计时结束时唤醒。delay(20)的主循环在纯时钟场景下已经足够高效不需要过度优化。7.4 生产环境安全与权限涉及硬件控制需要说明两点如果你的项目扩展到通过 WiFi 远程控制必须做好身份认证避免局域网内任意设备修改你的番茄钟状态。如果加入 OTA 升级必须验证固件签名防止恶意固件覆盖。操作 GPIO 时避免在带电状态下插拔杜邦线防止损坏引脚。7.5 可维护性建议最后给几条工程经验所有常量集中在config.h不要散落在代码中。每次修改硬件接线后先跑一次 GPIO 扫描测试程序再跑业务逻辑。状态机相关代码尽量用 switch-case不要用多层 if-else否则后续加功能容易乱。日志统一加前缀比如[TIMER]、[TOUCH]、[IMU]方便串口监视器过滤。8. 进一步扩展方向这个项目基础版本完成后可以继续扩展WiFi 时间同步通过 NTP 获取网络时间替代本地 RTC实现开机自动校时。Web 远程控制面板ESP32 开启 WebServer手机浏览器访问页面查看剩余时间、暂停/切换模式。手势升级为传感器方案使用 APDS9960 手势传感器实现非接触式隔空手势控制。UI 动画提速改用 LVGL 图形库支持更复杂的动画和字体渲染。低功耗时钟模式在 IDLE 状态下关闭屏幕背光使用外部中断唤醒适合桌面睡眠场景。多番茄轮次记录使用 Preferences 库保存到 Flash统计每天完成番茄数量。如果你打算用 LVGL需要先学习它的对象树和刷新机制移植到 2.8 寸 SPI 屏也比较成熟社区有大量现成示例。这次实战项目最有价值的地方在于把屏幕驱动、触摸手势、传感器姿态和业务状态机整合在一个小系统里代码量不算大但涉及的知识链路很完整。后续在此基础上加功能比如网络校时、统计报表或语音提示都只需要在对应模块上扩展不需要推翻重写。建议先按本文示例把硬件跑通然后自己修改阈值、颜色和布局逐步形成自己的代码库。
返回列表