news 2026/9/24 10:55:00

ESP32翻页时钟DIY:从硬件选型到TFT_eSPI驱动与动画实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32翻页时钟DIY:从硬件选型到TFT_eSPI驱动与动画实现

1. 为什么我最终选了ESP32而不是Arduino Uno来做这台翻页时钟

1.1 从一块吃灰的1.8寸TFT屏说起

手头攒了好几块1.8寸的TFT彩屏,ST7735驱动,128x160分辨率,SPI接口,之前做小项目剩下的。某天整理元件盒翻出来,想着总不能一直让它吃灰,就琢磨着做个桌面翻页时钟。翻页时钟这个品类其实已经烂大街了,但市售成品要么是黑底白字的单色屏,要么是那种机械翻页结构,价格不便宜,而且尺寸都偏大。我想要的是一个能放在显示器旁边、不占地方、能一眼看清时间、最好还能显示日期和温湿度的小东西。

一开始想用Arduino Uno,毕竟手头就有,而且社区资源丰富。但仔细算了一下资源占用就发现问题了:Uno的ATmega328P只有2KB SRAM,而一块128x160的16位色TFT屏,光是全屏帧缓冲就需要128×160×2=40960字节,也就是40KB。Uno那2KB连零头都不够。当然可以用局部刷新、不用帧缓冲的方式来驱动,但翻页动画需要频繁重绘,没有帧缓冲的话闪烁会非常严重。

ESP32就不一样了,ESP32-WROOM-32模组自带520KB SRAM,还有4MB的Flash,双核240MHz,跑TFT_eSPI库驱动ST7735绰绰有余。而且ESP32自带WiFi和蓝牙,后面想加网络对时、手机调参这些功能都很方便。价格上,一块ESP32开发板也就二十来块钱,比Uno贵不了多少,但性能完全不是一个量级。

1.2 硬件选型清单与成本核算

最终确定的硬件清单如下:

元件型号/规格数量参考单价
主控板ESP32-WROOM-32开发板122元
显示屏1.8寸TFT ST7735 128x160 SPI115元
时钟模块DS3231高精度RTC模块18元
温湿度传感器DHT22(AM2302)112元
外壳3D打印件1约10元
杜邦线、排针等-若干约5元

总计不到80元。DS3231这个模块值得单独说一下,它内置温度补偿晶振,精度可以做到±2ppm,也就是一年误差大概一分钟左右。相比DS1302那种几块钱的RTC,DS3231走时准得多,而且自带纽扣电池座,断电后时间不丢。DHT22比DHT11贵一点,但精度和量程都好不少,温度±0.5°C,湿度±2%,对于桌面时钟来说完全够用。

1.3 屏幕驱动的选择:TFT_eSPI还是Adafruit_ST7735

驱动ST7735的库主要有两个选择:Adafruit_ST7735和TFT_eSPI。Adafruit的库胜在简单易用,文档齐全,但性能一般,刷新率上不去。TFT_eSPI是专门为ESP32、ESP8266这些高性能MCU优化的库,支持DMA传输、SPI频率可以拉到40MHz甚至更高,刷新速度快很多。

我最终选了TFT_eSPI,原因很直接:翻页动画对刷新率有要求。如果用Adafruit的库,全屏刷新一次大概要100ms以上,翻页动画会明显卡顿。TFT_eSPI在ESP32上SPI频率设到40MHz,全屏刷新可以压到20ms以内,动画流畅度完全不一样。

TFT_eSPI的配置方式稍微有点特殊,它不是通过代码配置引脚,而是通过修改库文件夹里的User_Setup.h文件来配置。这一点后面会详细说,也是很多人第一次用TFT_eSPI时容易踩坑的地方。

2. 接线方案与TFT_eSPI配置里那些容易翻车的地方

2.1 ESP32与ST7735的SPI接线

ST7735是SPI接口的屏幕,ESP32有两个硬件SPI接口,VSPI和HSPI。我习惯用VSPI,因为默认的TFT_eSPI配置就是基于VSPI的。接线如下:

ST7735引脚ESP32引脚说明
VCC3.3V供电
GNDGND共地
CSGPIO5片选
RESETGPIO4复位
DC/RSGPIO2数据/命令切换
SDA/MOSIGPIO23数据线
SCL/SCKGPIO18时钟线
BLK/LED3.3V背光,可接PWM调光

这里有个细节:ST7735的背光引脚(BLK)可以直接接3.3V常亮,也可以接一个支持PWM的GPIO来做亮度调节。我一开始接的是3.3V,后来发现晚上太刺眼,就改接到了GPIO21,用ledcWrite()做PWM调光,晚上自动降低亮度。

另外,ST7735模块上通常还有一个CS引脚和一个额外的DC引脚,有些模块还带触摸功能(比如ST7735S带触摸的版本),如果不用触摸功能,触摸相关的引脚悬空即可。

2.2 TFT_eSPI的User_Setup.h配置详解

TFT_eSPI库安装之后,需要找到库文件夹下的User_Setup.h文件进行修改。在Arduino IDE中,库文件夹通常在文档/Arduino/libraries/TFT_eSPI/下面。用文本编辑器打开User_Setup.h,需要修改以下几个地方:

第一,注释掉所有默认的驱动定义,取消注释ST7735的驱动定义:

// #define ILI9341_DRIVER #define ST7735_DRIVER

第二,设置屏幕尺寸。ST7735有几种不同的分辨率变体,常见的有128x160、128x128、160x128等。我用的这块是128x160:

#define TFT_WIDTH 128 #define TFT_HEIGHT 160

第三,配置引脚。找到引脚定义区域,修改为实际接线:

#define TFT_CS 5 #define TFT_DC 2 #define TFT_RST 4 #define TFT_MOSI 23 #define TFT_SCLK 18 #define TFT_BL 21

第四,设置SPI频率。默认是27MHz,可以提高到40MHz:

#define SPI_FREQUENCY 40000000

这里有个坑:不是所有ST7735模块都能稳定跑40MHz,有些便宜模块的排线质量差,40MHz下会出现花屏或者颜色错乱。如果遇到这种情况,把频率降到27MHz或者20MHz试试。我手头这块模块40MHz没问题,但另一块同型号的就不行,最后降到27MHz才稳定。

2.3 偏移量问题:为什么屏幕边缘有黑边或者显示不全

ST7735有一个很烦人的问题:不同厂家生产的模块,屏幕在驱动IC上的起始地址偏移量不一样。TFT_eSPI提供了偏移量设置:

#define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0

但实际用的时候,如果发现图像整体偏移了,比如左边或上边有黑边,或者右边或下边被截断,就需要调整这两个值。常见的偏移量有(0,0)、(0,24)、(24,0)、(2,1)等。我手头这块模块需要设置成:

#define TFT_OFFSET_X 2 #define TFT_OFFSET_Y 1

这个没有统一标准,只能根据实际显示效果来试。建议在setup()里先填充全屏红色,看看边缘是否对齐,然后再微调偏移量。

2.4 DS3231和DHT22的I2C接线

DS3231是I2C接口,ESP32的默认I2C引脚是GPIO21(SDA)和GPIO22(SCL)。接线很简单:

DS3231引脚ESP32引脚
VCC3.3V
GNDGND
SDAGPIO21
SCLGPIO22

DHT22是单总线接口,接任意GPIO都可以,我接的是GPIO15。需要注意的是,DHT22的数据线需要接一个4.7kΩ到10kΩ的上拉电阻到3.3V。有些DHT22模块自带这个电阻,如果是裸传感器就需要自己加。

3. 翻页动画的数学原理与代码实现

3.1 翻页效果的本质:垂直缩放+裁剪

翻页时钟的动画效果,本质上就是把数字的显示区域做一个垂直方向的缩放和裁剪。想象一张纸从中间对折,上半部分向下翻,下半部分不动。在屏幕上实现这个效果,就是把数字分成上下两半,上半部分做垂直缩放(从正常高度缩到0),下半部分保持不动,同时新的数字上半部分从0开始展开。

具体到代码层面,TFT_eSPI提供了setAddrWindow()pushImage()函数,可以往指定区域推送图像数据。翻页动画的核心就是:先把旧数字的上半部分逐行擦除(或者用背景色覆盖),然后把新数字的上半部分逐行绘制出来。每一帧改变擦除/绘制的行数,就形成了动画效果。

更高效的做法是用TFT_eSPI的Sprite功能。Sprite是一块内存中的画布,可以先把数字渲染到Sprite上,然后再把Sprite推送到屏幕。翻页动画时,只需要改变Sprite的推送区域和缩放比例,不需要反复重绘整个屏幕。

3.2 用Sprite实现流畅翻页动画

先创建一个全屏大小的Sprite:

TFT_eSPI tft = TFT_eSPI(); TFT_eSprite spr = TFT_eSprite(&tft); void setup() { tft.init(); tft.setRotation(1); // 横屏显示 spr.createSprite(160, 128); // 注意旋转后的宽高 spr.setColorDepth(16); }

然后定义一个绘制数字的函数,把数字渲染到Sprite的指定位置:

void drawDigitToSprite(TFT_eSprite &s, int digit, int x, int y, uint16_t color) { s.setTextColor(color); s.setTextSize(4); s.setCursor(x, y); s.print(digit); }

翻页动画的实现思路是:把数字区域分成上下两半,上半部分高度为h,下半部分高度也为h。动画过程中,上半部分的高度从h逐渐变为0,同时新数字的上半部分从0逐渐变为h。用循环实现:

void flipAnimation(int oldDigit, int newDigit, int x, int y, int w, int h) { int halfH = h / 2; for (int i = 0; i <= halfH; i += 2) { // 擦除旧数字上半部分 spr.fillRect(x, y, w, halfH - i, BACKGROUND_COLOR); // 绘制新数字上半部分 spr.fillRect(x, y + halfH - i, w, i, BACKGROUND_COLOR); drawDigitToSprite(spr, newDigit, x, y, FOREGROUND_COLOR); // 只推送上半部分区域 spr.pushSprite(0, 0); delay(10); } }

这个实现比较粗糙,实际效果会有闪烁。更好的做法是只推送变化区域,而不是全屏推送。TFT_eSPI的pushSprite(x, y)可以指定推送位置,但推送区域大小是固定的。如果要只推送局部区域,可以用pushImage()配合setAddrWindow()

我实际用的方案是:把每个数字单独做成一个Sprite,翻页时只操作这个数字的Sprite,然后只推送这个数字所在的区域。这样效率高很多,动画也更流畅。

3.3 帧率与动画时长的平衡

翻页动画的帧率和时长需要平衡。帧率太高(比如每帧delay 1ms),动画太快,看不清翻页效果;帧率太低(比如每帧delay 50ms),动画卡顿。我试下来,每帧delay 8-12ms比较合适,整个翻页动画持续200-300ms,既有翻页的视觉效果,又不会让人觉得慢。

另外,动画的缓动函数也很重要。线性动画看起来比较生硬,用ease-in-out缓动会自然很多。简单的实现方式是用正弦函数:

float t = (float)i / halfH; float eased = (1 - cos(t * PI)) / 2; int currentH = halfH * (1 - eased);

这样动画开始和结束时会慢一些,中间快一些,看起来更自然。

4. 时间同步与传感器数据融合的实操细节

4.1 DS3231的初始化和时间设置

DS3231用RTClib库驱动,初始化很简单:

#include <RTClib.h> RTC_DS3231 rtc; void setup() { Wire.begin(21, 22); if (!rtc.begin()) { Serial.println("RTC初始化失败"); } if (rtc.lostPower()) { Serial.println("RTC掉电,需要重新设置时间"); rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); } }

rtc.lostPower()用来检测RTC是否掉过电。如果纽扣电池没电或者第一次使用,这个函数会返回true,此时需要用编译时间(__DATE____TIME__)来初始化RTC。编译时间是电脑编译代码时的时间,精度到秒,对于桌面时钟来说够用了。

读取时间:

DateTime now = rtc.now(); int hour = now.hour(); int minute = now.minute(); int second = now.second();

4.2 DHT22的读取与数据滤波

DHT22用DHT库读取:

#include <DHT.h> DHT dht(15, DHT22); void setup() { dht.begin(); } void loop() { float temp = dht.readTemperature(); float humi = dht.readHumidity(); if (isnan(temp) || isnan(humi)) { Serial.println("DHT22读取失败"); return; } }

DHT22的读取间隔不能太短,官方建议至少2秒一次。如果读取太频繁,传感器会返回NaN。我实际用的时候是每5秒读一次,读到的数据做滑动平均滤波,避免数值跳动太厉害。

滑动平均滤波的实现:

const int FILTER_SIZE = 5; float tempBuffer[FILTER_SIZE]; int bufferIndex = 0; float filterTemp(float newTemp) { tempBuffer[bufferIndex] = newTemp; bufferIndex = (bufferIndex + 1) % FILTER_SIZE; float sum = 0; for (int i = 0; i < FILTER_SIZE; i++) { sum += tempBuffer[i]; } return sum / FILTER_SIZE; }

4.3 屏幕布局设计与字体选择

128x160的屏幕横屏之后是160x128,空间比较紧张。我的布局是这样的:上半部分显示时间,大号字体,HH:MM格式,中间一个冒号闪烁;下半部分左边显示日期(MM/DD),右边显示温湿度。

字体方面,TFT_eSPI自带了几种字体,但大号数字字体只有FreeFonts系列。我用的FreeSansBold24pt7b,数字高度大概30像素左右,在160宽度的屏幕上显示四个数字加一个冒号刚好合适。

如果要更精细的控制,可以用自定义字体。TFT_eSPI支持从TTF字体转换成的位图字体数组,网上有工具可以把TTF转成C数组。不过对于这个项目来说,自带字体够用了。

4.4 冒号闪烁与整点报时

冒号闪烁很简单,每秒切换一次显示状态:

if (now.second() % 2 == 0) { spr.setTextColor(FOREGROUND_COLOR); } else { spr.setTextColor(BACKGROUND_COLOR); } spr.setCursor(colonX, colonY); spr.print(":");

整点报时我加了一个小功能:整点时屏幕边框闪烁三下,同时如果接了蜂鸣器可以响一声。这个功能用简单的状态机实现就行,不需要额外的库。

5. 外壳设计与装配中的那些坑

5.1 3D打印外壳的设计要点

外壳用Fusion 360设计,主要考虑几个点:屏幕开窗尺寸要精确,ESP32和DS3231的USB接口和电池仓要留出来,整体厚度要控制好。屏幕开窗我一开始留了1mm余量,结果装上去发现缝隙太大,后来改成0.3mm余量才合适。

外壳分前后两部分,用卡扣连接。前壳开屏幕窗和DHT22的通风孔,后壳开USB口和散热孔。DHT22的通风孔很重要,如果传感器被完全封在壳里,测出来的温度会偏高,因为ESP32和屏幕本身会发热。

5.2 屏幕固定与排线处理

ST7735模块通常是用双面胶或者热熔胶固定在面板上的。我用的是3M双面胶,先在屏幕背面贴一层,然后对准开窗位置粘上去。注意排线要朝下或者朝侧面,不要折成90度,否则容易断。

排线如果太长,可以折叠后用胶带固定,但折叠半径不能太小,否则内部铜箔会断裂。我一般折叠半径至少5mm。

5.3 供电方案:USB供电还是锂电池

USB供电最简单,插上就能用。但如果想做成便携的,可以用18650锂电池加TP4056充电模块。ESP32的工作电流大概在80-120mA(WiFi关闭时),18650电池2000mAh的话可以撑十几个小时。不过屏幕背光是大头,背光全亮时电流能到100mA以上,所以如果要用电池供电,背光亮度要调低。

我最终用的是USB供电,因为放在桌面上,旁边就有USB口,没必要折腾电池。而且USB供电可以一直开着,不用担心充电问题。

6. 调试过程中遇到的几个典型问题与解决思路

6.1 屏幕花屏与SPI频率的关系

前面提到过,SPI频率太高会导致花屏。我遇到的具体现象是:屏幕大部分区域显示正常,但偶尔会出现几条彩色条纹,或者某个数字显示成乱码。一开始以为是代码问题,查了半天才发现是SPI频率的问题。把SPI_FREQUENCY从40MHz降到27MHz之后,花屏现象消失。

这个问题的根本原因是SPI信号完整性。杜邦线太长、接触不良、或者模块本身的排线质量差,都会导致高频下信号失真。如果降到27MHz还有问题,可以试试20MHz或者16MHz。另外,缩短杜邦线长度、用排线代替杜邦线,也能改善信号质量。

6.2 DHT22读取失败的几种情况

DHT22读取失败返回NaN,常见原因有几个:一是读取间隔太短,DHT22需要至少2秒的转换时间;二是上拉电阻没接或者阻值不对,DHT22的数据线需要4.7kΩ到10kΩ的上拉;三是电源电压不稳,DHT22对电源比较敏感,如果ESP32的3.3V输出纹波太大,也会导致读取失败。

我遇到的是第二种情况,模块自带的10kΩ上拉电阻虚焊了,重新焊了一下就好了。如果用的是裸传感器,记得一定要加上拉电阻。

6.3 时间走时不准的排查

DS3231走时不准,一般不会是RTC本身的问题,而是初始化或者读取的问题。常见原因:一是rtc.adjust()只在掉电后调用,如果每次上电都调用,时间会被重置成编译时间;二是I2C通信不稳定,导致读取的时间数据错误;三是纽扣电池没电,RTC掉电后时间丢失。

我遇到过一次走时偏快的问题,查下来是I2C上拉电阻的问题。ESP32的I2C引脚内部有上拉,但阻值比较大(大概几十kΩ),如果DS3231模块上没有外部上拉电阻,I2C通信在高速下会不稳定。后来在SDA和SCL上各加了一个4.7kΩ的上拉电阻,问题解决。

6.4 翻页动画卡顿的优化

翻页动画卡顿,主要原因有两个:一是SPI刷新速度不够,二是动画逻辑本身效率低。优化方向:提高SPI频率(在稳定的前提下)、减少每帧推送的数据量(只推送变化区域)、用DMA传输(TFT_eSPI支持DMA,但需要配置)。

我实际优化下来,把全屏推送改成局部推送之后,帧率从大概15fps提升到了30fps以上,动画明显流畅了。局部推送的关键是计算好变化区域的坐标和大小,只推送这个区域。

7. 后续可以扩展的几个方向

这台翻页时钟目前只做了基础功能:显示时间、日期、温湿度。后面还可以加不少东西。比如加一个光敏电阻,根据环境光自动调节屏幕亮度;加一个蜂鸣器做整点报时;加一个按钮切换显示模式,比如显示秒表、倒计时、或者自定义文字。

网络功能方面,ESP32自带WiFi,可以加NTP对时,这样就不需要手动设置时间了。还可以加一个简单的Web服务器,用手机浏览器就能调整时间、亮度、显示模式这些参数。甚至可以用MQTT接入智能家居系统,把温湿度数据推送到Home Assistant。

如果嫌128x160的屏幕太小,可以换成2.4寸或2.8寸的ILI9341屏幕,分辨率240x320,显示内容更丰富。不过屏幕大了之后,外壳也要重新设计,整体尺寸会大一圈。

代码方面,目前是单文件结构,所有逻辑都在一个.ino文件里。如果功能继续增加,建议拆分成多个模块:显示模块、RTC模块、传感器模块、网络模块,每个模块一个.h.cpp文件,主文件只负责调度。这样代码可维护性会好很多。

我在实际做这个项目的过程中,最大的体会是:硬件项目里,软件问题往往比硬件问题更难查。屏幕花屏、传感器读数异常、时间走时不准,这些现象背后可能是接线问题、电源问题、信号完整性问题,也可能是代码逻辑问题。排查的时候要有耐心,一步一步缩小范围,先确认硬件没问题,再查软件。另外,TFT_eSPI的User_Setup.h配置一定要仔细核对,引脚、驱动型号、偏移量,任何一个不对都会导致显示异常。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 10:51:09

GD32F30x驱动CS1237实现PT1000高精度测温方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 10:47:56

Altium Designer 22导出带丝印3D模型到SolidWorks的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 10:45:20

《现代数字信号处理》全套PPT课件2026(中国矿业大学)

《现代数字信号处理》全套PPT课件2026&#xff08;中国矿业大学&#xff09; 课件内容&#xff1a; 第0章绪论.ppt 第1章离散时间信号与系统的时域分析.ppt 第2章离散时间信号与系统的频域分析.ppt 第3章 离散傅里叶变换.ppt 第4章快速傅里叶变换.ppt 第5章IR数字滤波器的设计.…

作者头像 李华
网站建设 2026/9/24 10:43:40

【Springboot毕设全套源码+文档】基于Java+spring boot的学生档案管理系统设计与实现(丰富项目+远程调试+讲解+定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/24 10:39:12

GMSL2-CSI2链路设计:MAX9295/9296的4种MIPI PHY模式与数据通路配置详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华