回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

【Arduino 动手做】采用 IPS 显示屏 ST7789V 的 ESP32 手表 简单

头像 驴友花雕 2025.06.01 7 0

大家好!在这个项目中,我将向大家展示我的作品:一款使用 ESP32 和 ST7789V 驱动器的 1.69 英寸 TFT-LCD 显示屏的 ESP 手表。它尺寸小巧,方便携带。我做这个项目是因为我的手表丢了,与其买新的,我觉得自己做一个更有趣。这样,我可以自由地设计和编程,添加任何我想要的功能。当时,智能手表对我来说非常酷,所以就把它分享出来。

在我制作的这块手表上,您可以控制日期和时间,还可以控制亮度,甚至可以在里面显示图片。

00.jpg
00-.jpg

## 补给品

以下是我在项目中使用的项目列表

对于主电路
-ST7789V 240x280 Pxl 1.69 英寸全彩 TFT IPS 显示屏 (1x)
-ESP32-WROOM32-D 4MB 闪存 WiFi 模块(1x)(您也可以使用具有相同引脚排列的相同类型 ESP32)
-IRLML2502 MOSFET(1个)
-电感器 0620 或 0420 3,3uH-10uH (1x)
-STI3408B电压调节器
-锂聚合物电池 603040,750mAh-800mAh
-22毫米小米手表S1表带/表带

-电容器 0603:
100nF(2x)
22uf(1x)
10uf(1x)
2.2uf(1x)

-电阻器SMD 0603:
22 欧姆 (1x)
220 欧姆 (1x)
1K(1x)
10k(6倍)
22万(2倍)
440K(1倍)
- 引脚接头 2.54mm 1x4 (1x)
-微型 2 针触觉按钮 SMD 3x4x2 (2x)
侧键SMD触觉开关ROHS 3x6x 3x6x3.5

-一些杜邦线
-从 JLCPCB 订购的 PCB Watch
-烙铁、助焊剂和一些焊锡

充电
-MH-CD42充放电模块
-连接器-XH 2.54mm 1x2 针 (1x)
-Type-C 母头
-电阻器 SMD 0603 4.7K(2x)
-从JLCPCB订购的PCB充电站

注意:您也可以使用市场上任何可用的锂充电器模块,我个人使用它是因为我想制作充电站,它是现在可供我使用的充电站(嗯,这就是计划)

为了追逐
为了节省时间并获得详细的结果,在这种情况下,我使用树脂 3D 打印机,我使用的树脂是“来自 SUNLU 的 ABS 类树脂”。
您也可以使用任何普通的 FDM 3D 打印来完成这个项目,但是外壳设计会有点笨重,您需要设置公差。我稍后会进行设计然后在这里分享。
我使用的打印机是“Anycubic Photon Mono SE”
注意:我列出的所有部件可能在您当地的市场上买不到或者非常昂贵,我建议使用 Aliexpress 购买其中的一些组件。

0.jpg
0-.jpg
0-0.jpg
0-1.jpg
0-2.jpg
0-3.jpg

## 步骤1:ESP Watch PCB设计

ESP手表PCB设计
ESP手表PCB设计
ESP手表PCB设计更多图片
这是我的手表的原理图和 PCB 设计,我使用 ESP32 WROOM 32D,您也可以使用其他类型,例如 WROOM 32U 或 32E。在制作 PCB 之前,我使用市场上的 ST7789V 和 GC9A01 模块和一些按钮制作了一个简单的原型。

设计
如果我们看一下 PCB 设计,就会发现 PCB 顶部有一个小切口,这是为了方便用户在发生错误或损坏时移除 ESP 模块。同时也是为了确保天线能够正常接收信号。

我使用的信号轨道宽度为0.254毫米。
我使用的 VCC 轨道宽度为 0.5 毫米。
铜区域为 GND(顶部和底部)

我从JLCBPCB订购了 PCB ,您只需 2 美元即可订购(不含运费)

这是接线
第23章
SCK-18
CS-5
DC-17
RST-16
背光 - 4
电池读数 - 34
按钮1 - 35
按钮2 - 32
按钮3 - 33
按钮4 - 25

ESP32
我使用的模块是 ESP32 WROOM 32D,它是一个具有蓝牙和 WiFi 功能的 SoC(片上系统),具有双核、低功耗、高速和成本效益(便宜)。就价格、小尺寸和高性能而言,乐鑫的这款芯片确实是该项目的完美解决方案。你可以看到我在示意图上放置的位置

ST7789V
我使用的 ST7789V 是一款全彩色 TFT-LCD,尺寸为 1.69 英寸,240x280 像素,逻辑门为 3.3V。它采用 12 针,可接收 8 位/16 位。

为了控制它,我使用来自 ESP 的 SPI,为了控制背光,我使用了一些 MOSFET。

IRLML2502 是一款 N 沟道 MOSFET(增强型),当栅极电压为 3.3V 时可完全打开,非常适合 ESP32,而且它采用小封装(SOT-23),可以驱动更大的电流。为了将 MOSFET 和 ESP32 引脚上的信号设置为低电平,我使用了 440KΩ 电阻。

为了保护背光 LED,我使用 22Ω 电阻

注意:为了保护您的 ESP32 免受来自 MOSFET 的电压反向电流的影响,请使用 1K 欧姆的 mosfet 到您的 ESP32 的信号引脚

有关更多详细信息,请参阅数据表链接:ST7789V

电压调节器
通常,在标准的 ESP32 DevKit 上,会使用 AMS1117 3.3V 稳压器,这在大多数情况下都适用。然而,由于 AMS1117 的压降较大,因此在使用单块锂聚合物电池时,它并不适用。当电池电压降至 3.5V 左右时,AMS1117 无法正常工作。因此,为了最大限度地利用电池,我使用了低压差 (LDO) 降压转换器,例如 STI3408B。

并且足够小,可以安装在 PCB 上。

STI3408B是一款 1.5MHz、1.2A 同步降压转换器,非常适合仅需单节锂电池供电的项目。它的输入范围为 1.2V 至 6V,并可降至 1.2V。关于噪声,到目前为止,我在各种项目中使用此转换器时尚未遇到任何问题。为了减轻潜在的噪声问题,我使用了比推荐值更大的电容和电感。

使用此 IC,即使输入从 4.2 变为 3.3,我们也可以将输出电压设置为始终 3.3V
要将输出设置为 3.3V,您需要在电感器之后的 IC 输出端创建一个分压器,并将其连接到 IC 的 FB 引脚。将一个1kΩ电阻从3.3V输出端(电感器之后)连接到反馈引脚,并将一个220Ω电阻从反馈引脚连接到地(GND)。这样,我们就可以为系统实现 3.3 V 输出。
更像 LX----->电感------>1K 欧姆 ---->FB

## 步骤2:PCB组装

设计并订购 PCB 后,您需要进行组装。您可以使用普通的手动烙铁,无需使用热风焊接工具。只需确保不要使 ESP32 模块过热即可。

首先,焊接稳压器 IC STI3408B(或降压转换器)以及稳压器的其他组件,例如电阻器、电感器和电容器。
然后,用万用表确认稳压器输出 3.3V。如果输出不是 3.3V,则可能是 IC 损坏,也可能是电阻位置错误,或者使用了错误的阻值(我遇到过几次这种情况),或者可能是由于焊接不干净导致短路。
确认输出 3.3V 后,放置并焊接 ESP32 模块。然后,焊接其余按钮。
接下来,焊接引脚接头,以便通过编写简单代码(例如 Serial.println("Hello world"))来检查 ESP32 是否正常工作。注意:请参阅步骤 5 了解如何编写代码。
一切确认无误后,焊接 MOSFET 和主 TFT-LCD 显示屏 ST7789V。这样,您的智能手表就可以进行编程了。
对于锂电池,您可以在完成编程和上传代码后再进行焊接。

如果您想直接焊接然后上传代码,那就没问题了,但我建议将 5V 输入与 ESP 手表断开,以防止电池过度充电。您只需将 RX、TX 和 GND 连接到 UART 即可。

附件
下载 {{ file.name }}Gerber_BottomPasteMaskLayer.GBP下载
下载 {{ file.name }}Gerber_底部丝网层.GBO下载
下载 {{ file.name }}Gerber_底部焊锡掩模层.GBS下载
下载 {{ file.name }}钻孔_NPTH_Through.DRL下载

02.jpg
02-1.jpg

## 步骤3:充电站

注意:此充电站设计不是强制性的,您可以跳过此步骤,只需使用市场上任何可用的锂充电模块即可。

我使用 MH-CD42 作为充电站板。我设计了另一块 PCB 来与模块配对。为了方便使用,我添加了一个 Type-C 接口和一些引脚排列。

MH-CD42 是一款兼具充电和放电功能的电源管理板。它提供锂电池保护功能,包括过压、短路和过温保护。该板还配备 4 级 LED 指示灯。它持续提供 5V 输出,并可提供高达 2.1 安培的电流。此外,它还能以 2 安培的速率为电池充电。

在我为充电站设计的 PCB 上,我使用了 12 针的 Type-c,其中我将 CC1 和 CC2 连接到地面,因此我可以使用 Type-C 到 Type-C 电缆。

是的,这就是我开始使用它而不是传统的 TP4056 的原因。但如果你仍然想使用它,这不是问题。这只是我的偏好。

03=1.png
03-3.jpg
03-4.jpg

## 步骤4:案例设计和印刷

对于表壳设计,我首先使用 CorelDRAW 以二维形式绘制了 PCB、表带、表壳设计、布局和其他元素的整体尺寸。我用卡尺测量了所有部件,并估算出合适的尺寸,所有步骤均未添加公差。然后,我利用收集并记录(在本例中是输入)的数据,使用 SolidWorks 设计了 ​​3D 表壳,并添加了必要的公差。对于树脂 3D 打印,所需的公差约为 0.1 毫米至 0.15 毫米。设计完成后,我在 SolidWorks 中组装了各个部件,检查它们是否能够正确装配。我还设计了不带调试引脚的表壳。最后,我将设计转换为 STL 格式。

注意:对于使用FDM的3D打印,公差约为0.24毫米至0.34毫米,具体取决于设计和尺寸。我的3D打印机是Ender 3 V2,喷嘴直径为0.2毫米,因此公差足够。目前,我设计它用于树脂3D打印。

设计完所有必要的部件后,我使用荔枝切片树脂(Lychee Slicer Resin)进行打印准备。具体设置可以在上面上传的图片中看到。对于支撑,我将其设置为“中等”和“超高”密度。您无需遵循此配置;我之所以采用这个配置,是因为我不想在调整支撑上花费太多时间。然而,出现了一个问题,支撑太多了,当我尝试移除它们时,我的设计底座破裂了,如图所示。(我就是没那么有耐心,哈哈)。

你可以在 Youtube 上观看一些关于如何处理树脂印刷品的视频,不要像我一样。记住要小心谨慎地处理并保护它。

附件
下载 {{ file.name }}Assem1 - 主外壳调试-1.STL下载3D视图
下载 {{ file.name }}装配1-主外壳-1.STL下载3D视图
下载 {{ file.name }}Assem1-侧键-1.STL下载3D视图
下载 {{ file.name }}装配1-盖顶-1.STL下载3D视图

04.png
04-.png
04-1.png
04-2.png
04-3.png
04-4.png
04-5.png
04-6.jpg
04-7.jpg
04-8.png
04-9.png
04-10.png

## 步骤5:编码

我使用 USB 到 UART CP2102 将程序上传到我的 ESP32 手表,您也可以使用我之前提到的其他 UART。

将 ESP Watch RX 连接到 USB UART TX,将 ESP Watch TX 连接到 USB RX。然后连接地面。

并确保已安装驱动程序。

我使用 VSCode

为了对 ESP 进行编码,我们使用Platform.io,我选择的主板是“uPesy ESP32 Wroom Devkit”,您也可以使用 Denky32,但我更喜欢使用它。

为了控制显示,我使用Bodmer的库TFT_eSPI,并在 user_setup 中复制并放置 ST7789 240x280,设置 ID 为 203

这是用户设置的代码

代码
// ST7789 240 x 280 显示屏,无芯片选择线
#define USER_SETUP_ID 203 

#define ST7789_DRIVER // 配置所有寄存器

#define TFT_WIDTH 240 
#define TFT_HEIGHT 280 

#define CGRAM_OFFSET // 库将添加所需的偏移量

//#define TFT_RGB_ORDER TFT_RGB // 颜色顺序为红-绿-蓝
//#define TFT_RGB_ORDER TFT_BGR // 颜色顺序为蓝-绿-红

//#define TFT_INVERSION_ON 
//#define TFT_INVERSION_OFF 

// DSTIKE 升压//# 
define TFT_DC 23 
//#define TFT_RST 32 
//#define TFT_MOSI 26 
//#define TFT_SCLK 27 

// 通用 ESP32 设置
#define TFT_MISO 19 
#define TFT_MOSI 23 
#define TFT_SCLK 18 
#define TFT_CS 5 // 未连接
#define TFT_DC 17 
#define TFT_RST 16 // 连接复位以确保显示初始化

#define LOAD_GLCD // 字体 1. 原始 Adafruit 8 像素字体需要~1820 字节 FLASH 
#define LOAD_FONT2 // 字体 2. 小 16 像素高字体,需要~3534 字节 FLASH,96 个字符
#define LOAD_FONT4 // 字体 4. 中 26 像素高字体,需要~5848 字节 FLASH,96 个字符
#define LOAD_FONT6 // 字体 6. 大 48 像素字体,需要~2666 字节 FLASH,仅字符 1234567890:-.apm 
#define LOAD_FONT7 // 字体 7. 7 段 48 像素字体,需要FLASH 中大约需要 2438 个字节,只有字符 1234567890:. 
#define LOAD_FONT8 // 字体 8. 大型 75 像素字体需要 FLASH 中大约需要 3256 个字节,只有字符 1234567890:-. 
//#define LOAD_FONT8N // 字体 8. 上述字体 8 的替代品,略窄,因此 3 位数字适合 160 像素 TFT 
#define LOAD_GFXFF // FreeFonts.包括访问 48 种 Adafruit_GFX 免费字体 FF1 到 FF48 和自定义字体

#define SMOOTH_FONT 


// #define SPI_FREQUENCY 27000000 
#define SPI_FREQUENCY 40000000 

#define SPI_READ_FREQUENCY 20000000 

#define SPI_TOUCH_FREQUENCY 2500000 

// #define SUPPORT_TRANSACTIONS

以下是主要代码

代码
#include <Arduino.h> 
#include "TFT_eSPI.h" 
#include "MINGO.h" 
#include "Flower_240x280.h" 

#define BL 4 
#define inLed 2 

#define v_R 34 

#define b_1 35 
#define b_2 33 
#define b_3 25 
#define b_4 32 

// TFT 设置
TFT_eSPI tft = TFT_eSPI(); // 调用自定义库
TFT_eSprite spritte = TFT_eSprite(&tft); // 创建 Sprite 对象“spritte” 


// 菜单设置
int screenW = 240; 
int screenH= 280; 
int textWidth; 
int x; 

bool mainMenu = true ; 
bool subMenu = false; 
bool wait = false; 
//SubMenu 
const int menuCount = 4; 
bool menuChange = true; 
int selectedOption = 0; 

const char* menuText[menuCount] = {"亮度","时间","设置","关于"}; 
// 时间设置菜单

// 页面处理程序
int page IRAM_ATTR = 0; 
bool pageChange = true; // 在上一页完成加载之前,停止更改页面的代码
bool pageRefresh = true; // 清理页面

//时间
volatile int sec IRAM_ATTR= 50; 
volatile int minutes IRAM_ATTR= 59; 
volatile int hrs IRAM_ATTR= 23; 
volatile int days IRAM_ATTR= 7; 
volatile int months IRAM_ATTR= 4; 
volatile int yrs IRAM_ATTR= 2024; 

volatile int days_Max IRAM_ATTR; 
volatile int daysOfWeekCount IRAM_ATTR; 

const char* daysOfWeek[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; 
const char* monthsOfYear[] = {"NULL","January", "February", "March", "April", "May", "June", "July", "August", "September", "Octobor", "November", "December"}; 

const unsigned long interval = 1000; 
// 
// 输入
int 电压;
bool inp1,inp2,inp3,inp4; 


// 亮度控制
int mapRange(int input, int input_start, int input_end, int output_start,int output_end) { 
    return (input - input_start) * (output_end - output_start) / (input_end - input_start) + output_start; 
} 
int brighten = 80; 
int Level = 4; 

//时间菜单控制
int timeMenu_Select = 0; 
const int timeMenu = 3; 
bool timePage = false; 

int clockSelect = 0; 
    int dateSelect = 0; 
////////////////////////////////////////////////////////////////////////////////////////////// 
void input(void *pvParameters); 
void v_Read(void *pvParameters); 
void tft_page(void *pvParameters); 
void onTimer(TimerHandle_t xTimer); 


void setup()
{ 

    Serial.begin(115200);

  pinMode(b_1,输入);pinMode(b_2,输入);pinMode(b_3,输入);pinMode(b_4,输入);pinMode(v_R,输入); //设置模式
  pinMode(BL,输出);pinMode(inLed,输出); 
 
  
  
  //设置TFT 
    tft.init(); 
    tft.setRotation(0); 
    tft.setSwapBytes(true); 
    tft.fillScreen(TFT_ORANGE); 

      analogWrite(BL, 0); 
        // for (int i = 0; i < 3; i++) { 
        // digitalWrite(inLed, HIGH); // 打开LED 
        // delay(500); // 等待500毫秒(0.5秒)
        // digitalWrite(inLed, LOW); // 关闭LED 
        // delay(500); // 再等待 500 毫秒
        // } 
      digitalWrite(inLed, HIGH); 
      analogWrite(BL, brighten); 

  xTaskCreatePinnedToCore(input,"button", 1024, NULL, 1, NULL, 1); // 核心 1 
  xTaskCreatePinnedToCore(v_Read,"Voltage Read",1024, NULL, 5, NULL, 0); 
  xTaskCreatePinnedToCore(tft_page,"Page of TFT",20000, NULL, 2, NULL, 1); 

  TimerHandle_t timerHandle = xTimerCreate("timer", pdMS_TO_TICKS(interval), pdTRUE, 0, onTimer); // 名称、周期、自动重新加载、计时器 ID、回调
  if (timerHandle != NULL) { 
    xTimerStart(timerHandle, 0); 
  } 

} 

void loop() { 
  
 
    //Serial.printf("%02d:%02d:%02d\n", hrs, minutes, sec); 

    //Serial.print("months : "); 
    //Serial.println(monthsOfYear[months]); 
    //Serial.print("day : "); 
    //Serial.println(daysOfWeek[days]); 

    //按钮读取// 
   // if (inp1 == HIGH ){ 
    //Serial.print("35"); 
    //} 
    //页面读取// 
    //Serial.print("PG: "); 
    //Serial.println(page); 
    //Serial.print("change: "); 
    //Serial.println(pageChange); 
    //Serial.print("Refresh: "); 
    //Serial.打印(pageRefresh);
      延迟(1000);

} 

void input(void *pvParameters){ 
  
  while(1){ 
  inp1=digitalRead(b_1);
  inp2=digitalRead(b_2);
  inp3=digitalRead(b_3);
  inp4=digitalRead(b_4);

  if(inp1==HIGH && pageChange ==true)
  { 
    pageChange=false; 
    pageRefresh=true; 
    page++; //下一页
  } 
  
  if(inp2==HIGH && pageChange ==true)
  { 
    pageChange=false; 
    pageRefresh=true; 
    page--; //上一页
  } 
  
  if(page ==1 && mainMenu == true&& inp3==HIGH && pageChange ==true && wait==false)//菜单选择控制
  { 
    pageRefresh=true;
     selectedOption = (selectedOption + 1) % menuCount; 
     Serial.println(selectedOption); 
      Serial.println("main"); 
  } 
  
  if(inp4==HIGH && pageChange ==true && mainMenu == true && page == 1 && wait== false) //菜单进入控制
  { 
    wait = true; 
    pageChange = false; 
    mainMenu = false; 
    subMenu= true; 
    pageRefresh=true; 

    Serial.print("INP4"); 

  } 

  if(selectedOption == 1 && inp3==HIGH && subMenu==true && mainMenu==false&& wait==false) //时间设置菜单控制
  { 
    
    timePage =false; 
    pageRefresh=true; 
    //Serial.print("按下时钟菜单"); 
    timeMenu_Select = (timeMenu_Select + 1) % timeMenu ; 
     //Serial.println(timeMenu_Select); 
  } 

  //页码处理程序
  page>3?page=0:page<0?page=3:page=page; 
  vTaskDelay(200); 
  } 
} 


void tft_page(void *pvParameters) 
{ 
  while(1) 
  { 
    if(page==0 && mainMenu == true) // main 
    { 
        if(pageRefresh==true){ 
          tft.fillScreen(TFT_BLACK); 
          pageRefresh= false; 
          Serial.print("refresh"); 
        } 
        if(pageRefresh==false){ 
          //Page 
            tft.setTextColor(TFT_WHITE, TFT_BLACK); 
            // years // 
              tft.setTextSize(3); 
              tft.setCursor(85, 73); //xy 
              tft.printf("%04d\n", yrs); 
            // date/month // 
              tft.setTextSize(3); 
              tft.setCursor(75,101); //xy 
              tft.printf("%02d/%02d\n", days, months); 
            // 时间
              tft.setTextSize(4); 
              tft.setCursor(28, 130); //xy 
              tft.printf("%02d:%02d:%02d\n", hrs, minutes, sec); 
              tft.setTextSize(3); 
            // 天
              textWidth = tft.textWidth(daysOfWeek[daysOfWeekCount]); 
              x = (screenW - textWidth) / 2; 
              tft.setCursor(x, 165); //xy 
              tft.print(daysOfWeek[daysOfWeekCount]); 
              textWidth = tft.textWidth(monthsOfYear[months]); 
            // 月
              x = (screenW - textWidth) / 2; 
              tft.setCursor(x, 193); //xy 
              tft.打印(monthsOfYear[months]); 
            vTaskDelay(100);
            pageChange=true;
        }   
    }   
    else if(page==1 && mainMenu == true && wait==false) //菜单
    { wait ==true; 
        if(pageRefresh==true){ 
          tft.fillScreen(TFT_BLACK); 
          pageRefresh= false; 
        } 
        if(pageRefresh==false){ 
          //isi 
             tft.setTextSize(2); 
                const int menuItemWidth = 200; //每个菜单项的宽度
                const int menuItemHeight = 50; //每个菜单项的高度
                const int menuItemSpacing = 10; //菜单项之间的间距

                int totalMenuHeight = menuCount * (menuItemHeight + menuItemSpacing) - menuItemSpacing; 
                int menuStartY = (tft.height() - totalMenuHeight) / 2; 

                for (int i = 0; i < menuCount; i++) { 
                  
                  int x = (tft.width() - menuItemWidth) / 2; 
                  int y = menuStartY + i * (menuItemHeight + menuItemSpacing); 

                  int xText = (screenW - tft.textWidth(menuText[i]))/ 2; 
                  

                  if (i == selectedOption) { 
                    tft.fillRoundRect(x, y, menuItemWidth, menuItemHeight, 10, TFT_YELLOW); 
                    tft.setTextColor(TFT_BLACK); 
                  } else { 
                    tft.drawRoundRect(x, y, menuItemWidth, menuItemHeight, 10, TFT_WHITE); 
                    tft.setTextColor(TFT_WHITE); 
                  } 

                  tft.drawString(String(menuText[i]) , xText, y + 15); 
                     // Serial.println("wifiSub"); 
                } 
            
            pageChange=true; 
          
        }   
        vTaskDelay(100); 
      wait== false; 
    } 
    else if(page==2 && mainMenu == true) //花
    { 
        if(pageRefresh==true){ 
          tft.fillScreen(TFT_BLACK); 
          pageRefresh= false; 
        } 
        if(pageRefresh==false){ 

          // isi 
            tft.pushImage(0,0,screenW,screenH,Flower_240x280); 
          vTaskDelay(100); 
          pageChange=true; 
        }   

    } 
    else if(page==3 && mainMenu == true) //颜色
    { 
        if(pageRefresh==true){ 
          tft.fillScreen(TFT_BLACK); 
          pageRefresh= false; 
        } 
        if(pageRefresh==false){ 

          // isi 
            tft.fillScreen(TFT_PINK); 
          vTaskDelay(100); 
          pageChange=true; 
        }   

    } 

    //子菜单
    if(mainMenu == false && subMenu ==true &&selectedOption == 0 ){ //亮度控制
        String text = "Brightness"; 
        if(pageRefresh==true){ 
          tft.fillScreen(TFT_BLACK); 
          pageRefresh= false; 
        } 
        if(pageRefresh==false){ 
          //isi 
            tft.setTextColor(TFT_WHITE, TFT_BLACK); 
            tft.setTextSize(3); 
            x = (screenW - tft.textWidth(text)) / 2; 
            tft.setCursor(x,100); 
            tft.print(text); 
            tft.setTextSize(3); 
            x = screenW/2; 
            tft.setCursor(x,180); 
            tft.print(Level); 
              if(inp1 == HIGH){ //增加亮度
                  if(Level < 6){ 
                  Level++;
                  亮度 = mapRange(Level, 1, 6, 20 ,255); 
                  analogWrite(BL, 亮度); 
                  vTaskDelay(100);}} 
                  
                  
              if(inp2 == HIGH){//降低亮度
                if(Level > 1){ 
                Level--;
                亮度 = mapRange(Level, 1, 6, 20 ,255); 
                analogWrite(BL, 亮度); 
                vTaskDelay(100);}} 
                vTaskDelay(100); 

              if(inp4 == HIGH && wait==false){//返回主
                串口。打印(“哎哟”);
             mainMenu=true; 
             subMenu=false; 
             pageChange=true; 
             page=1; 
             pageRefresh=true; 
             vTaskDelay(100); 
              } 
              wait=false; 
           
        }   
        
         
    } 
    if(selectedOption == 1 && mainMenu == false && subMenu ==true ){ // 时间控制
  
            if(pageRefresh==true){ 
                  tft.fillScreen(TFT_BLACK); 
              
                  pageRefresh= false; 
                } 
                if(pageRefresh==false){ 
                  timePage ==false; 
                  // isi 
                    const int timeMenuWidth = 180; // 每个菜单项的宽度
                    const int timeMenuHeight = 60; // 每个菜单项的高度
                    const int timeMenuSpacing = 10; // 菜单项之间的间距
                    
                    
                    String TimeMenuList[timeMenu]={"Clock","Date","Back"}; 

                    int totalMenuHeight = timeMenu * (timeMenuHeight + timeMenuSpacing) - timeMenuSpacing; 
                        int menuStartY = (tft.高度( - 总菜单高度)/ 2;

                        对于(int i = 0; i < timeMenu; i ++){ 
                          int x =(tft.width( - timeMenuWidth)/ 2; 
                          int y = menuStartY + i *(timeMenuHeight + timeMenuSpacing);

                          如果(i == timeMenu_Select){ 
                            tft.fillRoundRect(x,y,timeMenuWidth,timeMenuHeight,10,TFT_YELLOW); 
                            tft.setTextColor(TFT_BLACK); 
                          } else { 
                            tft.drawRoundRect(x,y,timeMenuWidth,timeMenuHeight,10,TFT_WHITE); 
                            tft.setTextColor(TFT_WHITE); 
                          } 

                          int xText =(screenW - tft.textWidth(TimeMenuList [i]))/ 2; 
                          tft.drawString(String(TimeMenuList [i]),xText,y + 15); 
                            // Serial.println("wifiSub"); 
                        } 
                    vTaskDelay(100); 
                    if(inp4==HIGH && wait==false && timeMenu_Select==0){ // 到时间
                      wait=true; 
                      subMenu=false; 
                      pageRefresh=true; 
                      timePage=true; 
                      Serial.println("到时间"); 
                    } 
                    if(inp4==HIGH && wait==false && timeMenu_Select==1){ // 到日期
                      wait=true; 
                      subMenu=false; 
                      pageRefresh=true; 
                      timePage=true; 
                      Serial.println("到日期"); 
                    } 
                    if(inp4 == HIGH && wait==false && timeMenu_Select==2 ){ //返回主
                        Serial.print("Ouch"); 
                    mainMenu=true; 
                    subMenu=false; 
                    pageChange=true; 
                    page=1; 
                    pageRefresh=true; 
                    vTaskDelay(100); 
                      } 
                    wait=false; 
                    
                } 
            } 

    if(selectedOption == 2 && mainMenu == false && subMenu ==true ){ //设置
      
        if(pageRefresh==true){ 
              tft.fillScreen(TFT_BLACK); 
              pageRefresh= false; 
            } 
            if(pageRefresh==false){ 
              // isi 
                tft.pushImage(0,0,screenW,screenH,MINGO); 
                vTaskDelay(100);
                if(inp4 == HIGH && wait==false){//返回主
                串口。打印(“哎哟”);
             mainMenu=true;
             subMenu=false;
             pageChange=true;
             page=1;
             pageRefresh=true;
             vTaskDelay(100);
              } 
              wait=false;
              
            }   
    } 
    if(selectedOption == 3 && mainMenu == false && subMenu ==true){//关于 esp32 

          if(pageRefresh==true){ 
                tft.fillScreen(TFT_BLACK);
                pageRefresh=false;
              } 
              if(pageRefresh==false){ 
                // isi 
                const char *message[] = {"ESP32-WROOM","ESP32-D0WDQ6","Chip-v4.4.3","ID:DCC84E9EF0C8","Speed-240 Mhz","Flash:4.19 MB","F-Speed:80 Mhz","Flash Mode:0","F-Used:674.06 KB","Cores:2","RAM:256 KB"}; 
                menuChange=false; 
                tft.setTextSize(2); 
                tft.setTextColor(TFT_WHITE, TFT_BLACK); 
                textWidth = tft.textWidth(message[0]); 
                x = (screenW - textWidth) / 2; 
                tft.setCursor(x, 27); //xy 
                tft.print(message[0]); 

                textWidth = tft.textWidth(message[1]); 
                x = (屏幕宽 - textWidth) / 2; 
                tft.setCursor(x, 45); //xy 
                tft.print(message[1]); 

                textWidth = tft.textWidth(message[2]); 
                x = (屏幕宽 - textWidth) / 2; 
                tft.setCursor(x, 62); //xy 
                tft.print(message[2]); 

                textWidth = tft.textWidth(message[3]); 
                x = (屏幕宽 - textWidth) / 2; 
                tft.setCursor(x, 79); //xy 
                tft.print(message[3]); 

                textWidth = tft.textWidth(message[4]); 
                x = (屏幕宽 - textWidth) / 2; 
                tft.setCursor(x, 95); //xy 
                tft.print(message[4]); 

                textWidth = tft.textWidth(message[5]); 
                x = (屏幕宽 - textWidth) / 2; 
                tft.setCursor(x, 112); //xy 
                tft.print(message[5]); 

                textWidth = tft.textWidth(message[6]); 
                x = (屏幕宽 - textWidth) / 2; 
                tft.setCursor(x, 128); //xy 
                tft.print(message[6]); 

                textWidth = tft.文本宽度(消息[7]); 
                x =(屏幕宽 - 文本宽度)/ 2;
                tft.setCursor(x, 145); //xy 
                tft.print(message[7]); 

                textWidth = tft.textWidth(message[8]); 
                x = (屏幕宽 - textWidth) / 2; 
                tft.setCursor(x, 163); //xy 
                tft.print(message[8]); 

                textWidth = tft.textWidth(message[9]); 
                x = (屏幕宽 - textWidth) / 2; 
                tft.setCursor(x, 180); //xy 
                tft.print(message[9]); 
                
                textWidth = tft.textWidth(message[10]); 
                x = (屏幕宽 - textWidth) / 2; 
                tft.setCursor(x, 198); //xy 
                tft.print(message[10]); 

                  vTaskDelay(100); 
                  if(inp4 == HIGH && wait==false){ //返回主
                串口。打印("Ouch"); 
             mainMenu=true; 
             subMenu=false; 
             pageChange=true; 
             page=1; 
             pageRefresh=true; 
             vTaskDelay(100); 
              } 
              wait=false; 
                
              }   
    } 

          //时钟设置页面
          if(timeMenu_Select==0 && mainMenu == false && timePage ==true ){ //设置

            if(pageRefresh==true){ 
                  tft.fillScreen(TFT_BLACK); 
                  pageRefresh= false; 
                } 
                if(pageRefresh==false){ 
                 
                  // isi 
                  int menuColumnCount = 3; 
                  int menuRowCount = 1; 

                  const int menuItemSize = 60; //菜单项的较小尺寸
                  const int menuItemSpacing = 10; //菜单项之间的空间
                  const int menuItemCornerRadius = 5; //圆角半径
                  const int highlightsBorderWidth = 3; // 高亮边框的宽度

                  int clockMenuCount = menuColumnCount*menuRowCount; 
                  

                    int clockNOW[] = {hrs,minute,sec}; 
                  
                      int totalMenuWidth = menuColumnCount * (menuItemSize + menuItemSpacing) - menuItemSpacing; 
                      int totalMenuHeight = menuRowCount * (menuItemSize + menuItemSpacing) - menuItemSpacing; 

                      int menuStartX = screenW/2 - totalMenuWidth / 2; 
                      int menuStartY = screenH/2 - totalMenuHeight / 2; 

                      for (int i = 0; i < clockMenuCount; i++) { 
                        int row = i / menuColumnCount;
                        int col = i % menuColumnCount; 

                        int x = menuStartX + col * (menuItemSize + menuItemSpacing); 
                        int y = menuStartY + row * (menuItemSize + menuItemSpacing); 

                        bool isSelected = (i == clockSelect); 
                              tft.setTextSize(2); 
                        if (isSelected) { 
                          // 在菜单项周围绘制一个高亮圆角矩形轮廓
                          tft.drawRoundRect(x - highlightsBorderWidth, y - highlightsBorderWidth, menuItemSize + 2 * highlightsBorderWidth, menuItemSize + 2 * highlightsBorderWidth, menuItemCornerRadius + highlightsBorderWidth, TFT_ORANGE); 
                          tft.setTextColor(TFT_ORANGE,TFT_BLACK); 
                          
                        } else { 
                          tft.drawRoundRect(x, y, menuItemSize, menuItemSize, menuItemCornerRadius, TFT_WHITE); 
                          tft.setTextColor(TFT_WHITE,TFT_BLACK); 
                        } 

                    // 绘制菜单项徽标和文本
                      tft.drawString(String(clockNOW[i]), x + 15, y + menuItemSize / 2 - 5); 
                          } 
                    vTaskDelay(200); 
                    
                    if(inp1==HIGH){ 
                        Serial.println("HIGH 1"); 
                        if(clockSelect == 0){ 
                            hrs++; 
                            if(hrs>23){ 
                              hrs=0; 
                            } 
                        } 
                        if(clockSelect == 1){ 
                          minutes++; 
                          if(minute > 59){ 
                            minutes=0; 
                          } 
                        } 
                        if(clockSelect == 2){ 
                          sec=59; 
                        
                        } 
                    } 
                    if(inp2==HIGH){ 
                        Serial.println("HIGH 2"); 
                        if(clockSelect == 0){ 
                            hrs--; 
                            if(hrs<0){ 
                              hrs=23; 
                            } 
                        } 
                        if(clockSelect == 1){
                          分钟--; 
                            if(分钟 < 0){
                                分钟=59; 
                            } 
                        }
                        if(clockSelect == 2){ 
                          sec = 0; 
                        } 
                    } 
                    if(inp3==HIGH && wait==false){ 
                                pageRefresh=true; 
                                Serial.print("按下时钟选择"); 
                                clockSelect = (clockSelect + 1) % 3; 
                                Serial.println(clockSelect); 
                                vTaskDelay(100); 
                    } 
                    if(inp4 == HIGH && wait==false ){ //返回主
                        Serial.print("哎哟"); 
                    mainMenu=true; 
                    subMenu=false; 
                    timePage=false; 
                    pageChange=true; 
                    page=1; 
                    pageRefresh=true; 
                    vTaskDelay(100); 
                      } 
             
                  wait=false; 
                  
                }   
        } 

        if(timeMenu_Select==1 && mainMenu == false && timePage ==true ){ //设置
          
            if(pageRefresh==true){ 
                  tft.fillScreen(TFT_BLACK); 
                  pageRefresh= false; 
                } 
                if(pageRefresh==false){ 
                   tft.setTextSize(3); 
                const int menuItemWidth = 200; // 每个菜单项的宽度
                const int menuItemHeight = 50; // 每个菜单项的高度
                const int menuItemSpacing = 10; // 菜单项之间的间距

            

                const int dateOption = 3; 
                int date_num[]={days,months,yrs}; 

                int totalMenuHeight = dateOption * (menuItemHeight + menuItemSpacing) - menuItemSpacing; 
                int menuStartY = (tft.height() - totalMenuHeight) / 2; 

                for (int i = 0; i < dateOption; i++) { 
                  
                  int x = (tft.width() - menuItemWidth) / 2; 
                  int y = menuStartY + i * (menuItemHeight + menuItemSpacing);

                  
                  

                  如果(i == dateSelect){ 
                    tft.fillRoundRect(x,y,menuItemWidth,menuItemHeight,10,TFT_YELLOW);
                    tft.setTextColor(TFT_BLACK);
                  } else { 
                    tft.drawRoundRect(x,y,menuItemWidth,menuItemHeight,10,TFT_WHITE);
                    tft.setTextColor(TFT_WHITE);
                  } 

                  tft.drawString(String(date_num [i]),x + 10,y + 15);
                     // Serial.println("wifiSub"); 
                } 
                    vTaskDelay(200); 
                    
                  wait=false; 
                  if(inp1 ==HIGH&&wait==false){ 
                    if(dateSelect==0){ 

                      days++; 
                      if(days>days_Max){ 
                        days=1; 
                      } 
                    } 
                    if(dateSelect==1){ 
                      months++; 
                      if(months>12){ 
                        months=1; 
                      } 
                    } 
                    if(dateSelect==2){ 
                      yrs++; 
                      
                    } 
                  } 
                  if(inp2 ==HIGH&&wait==false) { 
                    if(dateSelect==0){ 
                      days--; 
                      if(days < 1){ 
                        days=days_Max; 
                      } 
                    } 
                    if(dateSelect==1){ 
                      months--; 
                      if(months<1){ 
                        months=12; 
                      } 
                    } 
                    if(dateSelect==2){ 
                      yrs--; 
                 
                      if (yrs <0){ 

                        yrs=9999; 
                      } 
                    } 
                  } 
                  if(inp3 ==HIGH &&wait==false){ 
                       pageRefresh=true; 
                                Serial.print("按下日期选择"); 
                                dateSelect = (dateSelect + 1) % 3; 
                                Serial.println(dateSelect); 
                                vTaskDelay(100); 
                  } 
                  if(inp4 ==HIGH && wait==false){ 
                    Serial.print("哎哟"); 
                    mainMenu=true; 
                    subMenu=false; 
                    timePage 
                    =false; pageChange=true 
                    ; page=1; 
                    pageRefresh=true; 
                    vTaskDelay(100); 
                  } 
                  
                }   
        } 
 
  } void v_Read(void *pvParameters) { 
while (1) {   uint32_t Vbatt = 0;   for(int i = 0; i < 16; i++) {







    Vbatt = Vbatt + analogReadMilliVolts(v_R); // 带校正的 ADC    
  } 
  float Vbattf = 2 * Vbatt / 16 / 1000.0; // 衰减率 1/2,mV --> V 
  Serial.println(Vbattf, 3);
  延迟(1000); 
} 


} 
void onTimer(TimerHandle_t xTimer) { 
    sec++; 
    
    if (sec > 59) { 
        sec = 0;
        分钟++; 
        if (minute > 59) {
            分钟 = 0; 
            hrs++; 
            if (hrs > 23) { 
                hrs = 0; 
                days++; 
                pageRefresh = true; 
                if (months == 2) { 
                        if (yrs % 4 == 0) { 
                            days_Max = 29; 
                        } else { 
                            days_Max = 28; 
                        } 
                    } else if (months == 4 || months == 6 || months == 9 || months == 11) { 
                        days_Max = 30; 
                      } else { 
                        days_Max = 31; 
                      } 
                if (days > days_Max) { 
                    days = 1;     months 
                    ++; 
                    if (months > 12) { 
                        months = 1; 
                        yrs++; 
                    } 
                    
                } 
               
            } 
        }     daysOfWeekCount = (days + (13 * (months + 1)) / 5 + yrs % 100 + (yrs % 100) / 4 + (yrs / 100) / 4 + 5 * (yrs / 100)) % 7;     daysOfWeekCount = (daysOfWeekCount + 6) % 7; }

我提供的代码包含一个来自 ESP32 核心的精密定时器和一个时钟。它可以控制亮度、显示图像,并允许您设置日期和时间。

我还没有时间将 NTP 同步添加到时钟中,我可能会稍后添加它

我测试了时钟时间两周,漂移没有达到 1 秒。

我会把代码发布到我的 github 上

05.jpg
05-.jpg
05-1.jpg
05-2.png
05-3.jpg
05-5.jpg
05-6.jpg
05-7.jpg
05-8.jpg

## 步骤6:组装

之后你就可以组装 ESP 手表了,但记得先焊接电池,

确保电缆没有缠绕或被机箱内的任何东西阻挡
然后先把电池放进去
把侧面按钮
然后将 ESP Watch PCB 放在上面
然后用盒子的顶部将其关闭
我建议使用智能手机后门胶水,因为如果 ESP Watch 内部出现问题,您可以将其剥离。
然后戴上表带

06.jpg
06-.jpg
06-1.jpg
06-2.jpg

## 第七步:问题与未来发展规划

问题与未来发展规划

以下是我在PCB编程和焊接过程中遇到的一些问题:

该代码效率不够高,因为它会不断刷新显示,这意味着它会消耗更多的电量来保持运行。
ESP 手表没有过放电保护。即使电池电压达到 3.3V,电池也会耗尽。ESP 手表会持续消耗电量,导致电池电压降至 2.9V 的危险水平。为了防止这种情况发生,我们需要在 PCB 上添加电池保护电路。
输入按钮上没有电容,按下时会产生很大的噪音。以后我打算加个电容,并用一些代码来消除抖动。
还有一些插针挂在表壳外面。以后我会把它们拆下来,给 ESP 手表加一个简单的充电接口。
在时钟选择菜单中更新显示时出现问题。TFT 无法正确更新数字,导致数字一直悬空,直到我们切换到另一个菜单。

07.jpg

附录

项目链接:https://www.instructables.com/ESP-WATCH-Using-IPS-Display-ST7789V/
项目作者:M_F_T
项目参考:https://www.youtube.com/watch?v=1Pp5RGtFSrU
https://www.youtube.com/watch?v=LUGhLd94f8A&t=348s
TFT_eSPI库:https://github.com/Bodmer/TFT_eSPI
ST7789V数据表链接:https://www.alldatasheet.com/html-pdf/1132511/SITRONIX/ST7789V/3171/7/ST7789V.html
JLCBPCB:https://jlcpcb.com/
3D文件:https://content.instructables.com/FQB/POFM/LWDJT48B/FQBPOFMLWDJT48B.stl
GBP文件: https://content.instructables.com/F2Y/N6Z9/LWEZ7Z65/F2YN6Z9LWEZ7Z65.gbp

附件

评论

user-avatar
icon 他的勋章
    展开更多