You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

ESP32搭配TFT LCD模拟时钟:如何实现无拖影、无闪烁的指针单独更新

ESP32搭配TFT LCD模拟时钟:如何实现无拖影、无闪烁的指针单独更新

我之前也遇到过一模一样的问题——用TFT做时钟时,全屏重绘闪得眼睛疼,不重绘又留指针拖影。结合ESP32 Wrover的PSRAM优势,给你一个完美的解决方案,核心思路是用静态背景缓存覆盖旧指针,只绘制新指针,完全不需要全屏重绘:

解决思路

因为你的背景(彩色图、刻度、文字)是完全静态的,只有指针在动,所以我们只需要:

  1. 初始化时一次性绘好所有静态内容
  2. 把静态背景的关键区域(指针会覆盖的部分)保存到ESP32的PSRAM缓冲区里
  3. 每次指针要更新时,先从缓冲区把背景恢复到屏幕上(覆盖旧指针),再画新指针

这样既不会留拖影,也不会闪烁,静态内容全程不会消失。

具体代码修改

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);
}

关键注意事项

  1. 必须开启PSRAM支持:在Arduino IDE的「工具」菜单里,找到「PSRAM」选项,选择「OPI PSRAM」(如果是旧款Wrover可能选「Quad PSRAM」),否则会分配内存失败。
  2. 彩色背景替换:把drawClockFace()里的tft.fillScreen(TFT_BLACK)换成你的彩色背景图,用TFT_eSPItft.pushImage()绘制提前用ImageConverter565生成的16位色图片数组即可,背景图会被一起保存到缓冲区里。
  3. 效率优化:我们只恢复时钟区域的背景(而不是全屏),这个区域很小(100x100左右),所以速度极快,完全不会有闪烁感。

这样修改后,你的时钟指针会平滑更新,既没有拖影,静态背景和文字也会一直稳定显示,完全解决之前的问题!

火山引擎 最新活动