【Arduino 动手做】采用 IPS 显示屏 ST7789V 的 ESP32 手表
2025-06-012647
AI 快速预览详细
本文介绍了如何使用 ESP32 和 ST7789V IPS 显示屏制作 DIY 智能手表。提供了详细的硬件连接方法和代码示例,适合有一定技术基础的 DIY 爱好者。通过这个项目,你可以轻松实现个性化手表,展示你的创造力和技术实力。
![]() ![]() |
![]() ![]() |
创作许可协议
本项目采用 None(不开放任何权利,保留所有权利) 进行许可。
创作许可协议
本项目采用 None(不开放任何权利,保留所有权利) 进行许可。
项目链接:https://www.instructables.com/ES ... PS-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/htm ... 3171/7/ST7789V.html
JLCBPCB:https://jlcpcb.com/
3D文件:https://content.instructables.co ... FQBPOFMLWDJT48B.stl
GBP文件: https://content.instructables.co ... F2YN6Z9LWEZ7Z65.gbp
问题与未来发展规划
以下是我在PCB编程和焊接过程中遇到的一些问题:
该代码效率不够高,因为它会不断刷新显示,这意味着它会消耗更多的电量来保持运行。
ESP 手表没有过放电保护。即使电池电压达到 3.3V,电池也会耗尽。ESP 手表会持续消耗电量,导致电池电压降至 2.9V 的危险水平。为了防止这种情况发生,我们需要在 PCB 上添加电池保护电路。
输入按钮上没有电容,按下时会产生很大的噪音。以后我打算加个电容,并用一些代码来消除抖动。
还有一些插针挂在表壳外面。以后我会把它们拆下来,给 ESP 手表加一个简单的充电接口。
在时钟选择菜单中更新显示时出现问题。TFT 无法正确更新数字,导致数字一直悬空,直到我们切换到另一个菜单。
之后你就可以组装 ESP 手表了,但记得先焊接电池,
确保电缆没有缠绕或被机箱内的任何东西阻挡
然后先把电池放进去
把侧面按钮
然后将 ESP Watch PCB 放在上面
然后用盒子的顶部将其关闭
我建议使用智能手机后门胶水,因为如果 ESP Watch 内部出现问题,您可以将其剥离。
然后戴上表带
我还没有时间将 NTP 同步添加到时钟中,我可能会稍后添加它
我测试了时钟时间两周,漂移没有达到 1 秒。
我会把代码发布到我的 github 上
[code]#include
#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; }[/code]
我使用 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
这是用户设置的代码
[code]// 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[/code]
对于表壳设计,我首先使用 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视图
注意:此充电站设计不是强制性的,您可以跳过此步骤,只需使用市场上任何可用的锂充电模块即可。
我使用 MH-CD42 作为充电站板。我设计了另一块 PCB 来与模块配对。为了方便使用,我添加了一个 Type-C 接口和一些引脚排列。
MH-CD42 是一款兼具充电和放电功能的电源管理板。它提供锂电池保护功能,包括过压、短路和过温保护。该板还配备 4 级 LED 指示灯。它持续提供 5V 输出,并可提供高达 2.1 安培的电流。此外,它还能以 2 安培的速率为电池充电。
在我为充电站设计的 PCB 上,我使用了 12 针的 Type-c,其中我将 CC1 和 CC2 连接到地面,因此我可以使用 Type-C 到 Type-C 电缆。
是的,这就是我开始使用它而不是传统的 TP4056 的原因。但如果你仍然想使用它,这不是问题。这只是我的偏好。
设计并订购 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下载
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<---- 220R 欧姆 <----GND
就像我给出的示意图一样
您可以查看典型应用或 IC 最低要求的数据表
电池读取
为了读取系统的电池电压,我使用 2 个220kΩ电阻创建了一个简单的分压器。分压器的输出连接到 ESP32 的 34 号引脚。使用分压器的原因是 ESP32 ADC 的输入电压最高只能达到 3.3V。通过使用分压器,电池电压会被降低到 ADC 可以承受的安全水平。然后将降低后的电压乘以系统代码中的相应系数,即可得到实际的电池电压。此设置用于监控电池,让您了解何时需要充电或 ESP32 何时应停止工作以防止深度放电。
我使用与“seeed studio”相同的系统来检查电池电压。
我认为它可能因其他 ESP32 ADC 而异
按钮
我的设计上有几个按钮,一些用于输入,一些用于对 ESP32 进行编程。
对于输入按钮,有四个按钮连接到 ESP GPIO 引脚 35、32、33 和 25。每个按钮都使用 10K 电阻下拉,因此逻辑为 0。按下按钮时,它会向相应的 ESP32 GPIO 引脚发送 HIGH 信号。
对于编程,ESP Devkit中有一个用于启用引脚的按钮和一个用于GPIO 0左右的BOOT按钮。功能与普通Devkit相同。
要使 ESP 进入上传程序模式,需要按下 Boot 按钮(GPIO 0),然后按下 EN 按钮,然后松开 EN 按钮,再松开 Boot 按钮(GPIO 0)
针头
如你所见,PCB 侧面有四个引脚。它们的作用是上传代码并为手表电池充电。最初,这被设计为一个临时解决方案,但现在似乎已经成为一个永久方案了(哈哈)。我现在通过这些引脚给手表充电。它们用于调试和上传代码,包括 RX、TX、5V+ 输入和 GND。我们计划在上传代码后移除它们,并使用能够直接接触充电站引脚的裸铜线为手表充电。不过,目前我们先保留它们。
以下是我在项目中使用的项目列表
对于主电路
-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 购买其中的一些组件。
在我制作的这块手表上,您可以控制日期和时间,还可以控制亮度,甚至可以在里面显示图片。