ESP32搭配TFT LCD模拟时钟:如何实现无拖影、无闪烁的指针单独更新
ESP32搭配TFT LCD模拟时钟:如何实现无拖影、无闪烁的指针单独更新
我之前也遇到过一模一样的问题——用TFT做时钟时,全屏重绘闪得眼睛疼,不重绘又留指针拖影。结合ESP32 Wrover的PSRAM优势,给你一个完美的解决方案,核心思路是用静态背景缓存覆盖旧指针,只绘制新指针,完全不需要全屏重绘:
解决思路
因为你的背景(彩色图、刻度、文字)是完全静态的,只有指针在动,所以我们只需要:
- 初始化时一次性绘好所有静态内容
- 把静态背景的关键区域(指针会覆盖的部分)保存到ESP32的PSRAM缓冲区里
- 每次指针要更新时,先从缓冲区把背景恢复到屏幕上(覆盖旧指针),再画新指针
这样既不会留拖影,也不会闪烁,静态内容全程不会消失。
具体代码修改
1. 先添加全局变量和缓冲区配置
首先要用到Wrover的PSRAM来存背景,所以先定义缓冲区和时钟区域的范围:
#include <TFT_eSPI.h> #include <time.h> #include <WiFi.h> #include <esp_psram.h> // 引入PSRAM相关头文件 TFT_eSPI tft = TFT_eSPI(); #define CENTER_X 50 #define CENTER_Y 60 #define RADIUS 50 #define HOUR_LENGTH 10 #define MINUTE_LENGTH 20 // 时钟区域的包围盒(指针能覆盖的最大范围) #define CLOCK_BOX_X1 (CENTER_X - RADIUS) #define CLOCK_BOX_Y1 (CENTER_Y - RADIUS) #define CLOCK_BOX_X2 (CENTER_X + RADIUS) #define CLOCK_BOX_Y2 (CENTER_Y + RADIUS) #define CLOCK_BOX_WIDTH (CLOCK_BOX_X2 - CLOCK_BOX_X1 + 1) #define CLOCK_BOX_HEIGHT (CLOCK_BOX_Y2 - CLOCK_BOX_Y1 + 1) const char* ssid = "Dina"; const char* password = "Dd78134003Segco"; const char* ntpServer = "pool.ntp.org"; const long gmtOffset_sec = 12600; // ایران +3:30 const int daylightOffset_sec = 0; int lastMinute = -1; uint16_t* bgBuffer = nullptr; // 保存背景的PSRAM缓冲区
2. 初始化时绘制静态背景并保存到缓冲区
修改setup()函数,只初始化一次静态背景,然后把时钟区域的背景读进缓冲区:
void drawClockFace() { // 这里替换成你的彩色背景图,比如用tft.pushImage绘制提前转好的16位色图片 // 示例先保留你的黑色背景+刻度+文字,实际用彩色图的话把fillScreen换成pushImage tft.fillScreen(TFT_BLACK); for (int h = 0; h < 12; h++) { float angle = (h * 30 - 90) * 0.0174533; int x0 = CENTER_X + cos(angle) * (RADIUS - 3); int y0 = CENTER_Y + sin(angle) * (RADIUS - 3); int x1 = CENTER_X + cos(angle) * RADIUS; int y1 = CENTER_Y + sin(angle) * RADIUS; tft.drawLine(x0, y0, x1, y1, TFT_WHITE); } for (int h = 1; h <= 12; h++) { float angle = (h * 30 - 90) * 0.0174533; int x = CENTER_X + cos(angle) * (RADIUS - 15); int y = CENTER_Y + sin(angle) * (RADIUS - 15); tft.setTextColor(TFT_WHITE); tft.setCursor(x - 5, y - 7); tft.print(h); } } void setup() { Serial.begin(115200); tft.init(); tft.setRotation(0); // 连接WiFi WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi Connected"); // 配置NTP时间 configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); // 绘制静态背景(只做一次!) drawClockFace(); // 分配PSRAM缓冲区保存时钟区域的背景 bgBuffer = (uint16_t*)ps_malloc(CLOCK_BOX_WIDTH * CLOCK_BOX_HEIGHT * sizeof(uint16_t)); if (bgBuffer) { // 把屏幕上的时钟区域像素读进缓冲区 tft.readRect(CLOCK_BOX_X1, CLOCK_BOX_Y1, CLOCK_BOX_WIDTH, CLOCK_BOX_HEIGHT, bgBuffer); Serial.println("背景缓冲区初始化成功"); } else { Serial.println("PSRAM分配失败!请检查IDE是否开启PSRAM支持"); } // 初始化第一次的指针 struct tm timeinfo; if (getLocalTime(&timeinfo)) { lastMinute = timeinfo.tm_min; float hourAngle = ((timeinfo.tm_hour % 12) + timeinfo.tm_min / 60.0) * 30 - 90; float minuteAngle = timeinfo.tm_min * 6 - 90; drawHandTriangle(HOUR_LENGTH, hourAngle * 0.0174533, TFT_WHITE, 2); drawHandTriangle(MINUTE_LENGTH, minuteAngle * 0.0174533, TFT_WHITE, 1); } }
3. 修改循环逻辑:只恢复背景+画新指针
现在loop()里不再全屏重绘,只做最小范围的更新:
// draw clock hand as triangle void drawHandTriangle(int length, float angleRad, uint16_t color, int width) { float cosA = cos(angleRad); float sinA = sin(angleRad); int xTip = CENTER_X + cosA * length; int yTip = CENTER_Y + sinA * length; int xLeft = CENTER_X + cos(angleRad + 1.57) * width; int yLeft = CENTER_Y + sin(angleRad + 1.57) * width; int xRight = CENTER_X + cos(angleRad - 1.57) * width; int yRight = CENTER_Y + sin(angleRad - 1.57) * width; tft.fillTriangle(xTip, yTip, xLeft, yLeft, xRight, yRight, color); } void loop() { struct tm timeinfo; if (!getLocalTime(&timeinfo)) { delay(1000); return; } if (timeinfo.tm_min != lastMinute) { // 1. 先恢复时钟区域的背景,把旧指针覆盖掉 if (bgBuffer) { tft.pushImage(CLOCK_BOX_X1, CLOCK_BOX_Y1, CLOCK_BOX_WIDTH, CLOCK_BOX_HEIGHT, bgBuffer); } // 2. 计算新的指针角度 float hourAngle = ((timeinfo.tm_hour % 12) + timeinfo.tm_min / 60.0) * 30 - 90; float minuteAngle = timeinfo.tm_min * 6 - 90; // 3. 绘制新的时针和分针 drawHandTriangle(HOUR_LENGTH, hourAngle * 0.0174533, TFT_WHITE, 2); drawHandTriangle(MINUTE_LENGTH, minuteAngle * 0.0174533, TFT_WHITE, 1); // 更新记录的分钟数 lastMinute = timeinfo.tm_min; } delay(1000); }
关键注意事项
- 必须开启PSRAM支持:在Arduino IDE的「工具」菜单里,找到「PSRAM」选项,选择「OPI PSRAM」(如果是旧款Wrover可能选「Quad PSRAM」),否则会分配内存失败。
- 彩色背景替换:把
drawClockFace()里的tft.fillScreen(TFT_BLACK)换成你的彩色背景图,用TFT_eSPI的tft.pushImage()绘制提前用ImageConverter565生成的16位色图片数组即可,背景图会被一起保存到缓冲区里。 - 效率优化:我们只恢复时钟区域的背景(而不是全屏),这个区域很小(100x100左右),所以速度极快,完全不会有闪烁感。
这样修改后,你的时钟指针会平滑更新,既没有拖影,静态背景和文字也会一直稳定显示,完全解决之前的问题!




