CrowPanel 3.5인치 ESP32 디스플레이가 탑재된 디지털 시계
이 튜토리얼에서는 CrowPanel 3.5인치 ESP32 디스플레이를 사용하여 항상 정확한 디지털 시계를 만드는 방법을 배웁니다 . Wi-Fi를 통해 인터넷 시간 제공업체와 시계를 동기화하고 TFT_eSPI 라이브러리를 사용하여 디스플레이에 보기 좋은 사용자 인터페이스를 구축할 것입니다.

시작해 볼까요!
개요
필수 부품
- CrowPanel 3.5인치 ESP32 디스플레이
CrowPanel 3.5인치 ESP32 디스플레이의 특징
- CrowPanel ESP32 디스플레이 시리즈
프로젝트 구조 생성
터치스크린 보정
인터넷에서 시간 데이터 다운로드
디지털 시계 구현하기
- 프로젝트 폴더 구조
- 요일 및 월 이름
- 메인 프로그램
- 라이브러리
- 상수와 객체
- 버튼 기능
- 버튼 초기화
- 버튼 이벤트 처리
- 시간 동기화 여부를 확인하세요
- 시간 동기화
- 디스플레이 업데이트
- 설정 기능
- 반복문
결론
링크
필수 부품
이 프로젝트에는 ELECROW의 CrowPanel 3.5인치 ESP32 디스플레이와 Arduino IDE만 있으면 됩니다. 패널에는 USB 케이블과 4핀 듀폰 케이블이 포함되어 있으므로 추가 케이블은 필요하지 않습니다.
CrowPanel 3.5인치 ESP32 디스플레이의 특징
ELECROW 의 CrowPanel 3.5인치 ESP32 디스플레이는 480*320 해상도의 TFT 저항막 터치스크린이며, 제어 프로세서로 ESP32-WROVER-B 가 내장되어 있습니다 . 아래 사진과 같이 깔끔한 아크릴 케이스가 함께 제공되므로 별도의 케이스를 제작할 필요가 없습니다.

CrowPanel 3.5인치 ESP32 디스플레이 (하우징 포함)
그 외에도 보드에는 TF 카드 슬롯, UART 인터페이스, I2C 인터페이스, 스피커 인터페이스, 충전 기능이 있는 배터리 커넥터, 그리고 GPIO 핀 두 개가 있는 소형 GPIO 포트가 있습니다. 아래 핀 배치도를 참조하십시오.

CrowPanel 3.5인치 ESP32 디스플레이 핀 배치도
다음 표는 세 가지 IO 인터페이스 중 어떤 GPIO 핀이 어떤 인터페이스에 할당되는지를 보여줍니다.
| GPIO_D | IO25; IO32 |
| UART | RX(IO16); TX(IO17) |
| I2C | SDA(IO22); SCL(IO21) |
이 보드는 USB 포트(5V, 2A)를 통해 전원을 공급받거나 표준 3.7V LiPo 배터리를 BAT 커넥터에 연결하여 전원을 공급받을 수 있습니다. 커넥터와 배터리의 극성을 반드시 확인하십시오. USB 케이블과 배터리가 동시에 연결되면 보드는 배터리를 충전합니다(최대 충전 전류는 500mA).
이 보드는 Arduino IDE, Espressif IDF, Lua RTOS, Micro Python 등 다양한 개발 환경을 사용하여 프로그래밍할 수 있으며, LVGL 그래픽 라이브러리 와도 호환됩니다 . 하지만 이 튜토리얼에서는 Arduino IDE와 TFT_eSPI 그래픽 라이브러리를 사용하여 시계의 사용자 인터페이스를 만들겠습니다.
CrowPanel ESP32 디스플레이 시리즈
참고로, CrowPanel 3.5인치 ESP32 디스플레이는 2.4인치부터 7인치까지 다양한 크기의 디스플레이 제품군 중 하나입니다. 크기 외에도 해상도, 디스플레이 드라이버 및 ESP32 모델에서 차이가 있습니다. 아래 표를 참조하십시오.



CrowPanel ESP32 디스플레이 ( 출처 )
이 튜토리얼에서는 3.5인치 디스플레이를 사용합니다 . 하지만 2.4인치 및 2.8 인치 디스플레이 도 동일하거나 유사한 디스플레이 드라이버(ILI9341, ILI9488)를 사용하므로 코드 예제와 설정 절차는 매우 유사합니다. 위 표에서 노란색으로 표시된 부분을 참조하십시오.
하지만 4.3인치, 5인치, 7인치와 같은 대형 디스플레이의 경우 TFT_eSPI 라이브러리가 해당 드라이버(NV3047, IL6122, EK9716BD3)를 지원하지 않는 것으로 보이므로 코드 예제가 제대로 작동하지 않을 수 있습니다. 다만, 제가 직접 테스트해 본 것은 아닙니다. 혹시 테스트해 보신 분이 있다면 댓글로 알려주시면 감사하겠습니다.
프로젝트 구조 생성
자세한 내용을 살펴보기 전에 TFT_eSPI 라이브러리 의 프로젝트 구조와 설정을 먼저 만들어 보겠습니다 .
아두이노 IDE를 열고 "프로젝트 digiclock"라는 이름의 프로젝트를 생성한 후 저장(다른 이름으로 저장)하세요. 그러면 "프로젝트 digiclock" 폴더가 생성되고 그 안에 "프로젝트 digiclock.ino" 파일이 만들어집니다. 이 폴더 안에 "프로젝트"라는 이름의 파일을 하나 더 생성하세요 tft_setup.h. 프로젝트 폴더는 다음과 같은 구조가 되어야 합니다.

프로젝트 폴더 구조
이제 아두이노 IDE에 " digiclock.ino"와 " tft_setup.h"라는 이름의 탭 두 개가 표시될 것입니다.

탭 두 개가 있는 아두이노 IDE
탭을 클릭하여 tft_setup.h파일을 열고 다음 코드를 복사하여 붙여넣으세요.
중요사항 - 터치 기능 동작하지 않을 때
최신 3.5인치 화면 버전은 2.2입니다. 터치 구동 IC가 다릅니다.
버전 2.0에서는
TFT_MISO가 12번 핀이고
Touch_CS는 33번 핀입니다.
버전 2.2:
TFT_MISO는 33번 핀이고,
Touch_CS는 12번 핀입니다.
아래 코드에서 변경하세요. 아래 코드에서 주석처리하고 변경한 부분 참고하세요.
#define ILI9488_DRIVER
#define TFT_WIDTH 480
#define TFT_HEIGHT 320
#define TFT_BACKLIGHT_ON HIGH
#define TFT_BL 27
//#define TFT_MISO 12
#define TFT_MISO 33
#define TFT_MOSI 13
#define TFT_SCLK 14
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST -1
//#define TOUCH_CS 33
#define TOUCH_CS 12
#define SPI_FREQUENCY 27000000
#define SPI_TOUCH_FREQUENCY 2500000
#define SPI_READ_FREQUENCY 16000000
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
이 코드는 TFT_eSPI 라이브러리에 어떤 디스플레이를 사용하고 있는지 알려줍니다. 구체적으로, 라이브러리에 디스플레이 크기(TFT_WIDTH, TFT_WIDTH), 디스플레이 드라이버(ILI9488_DRIVER), 디스플레이 제어에 사용되는 SPI 핀, 그리고 로드할 글꼴을 알려줍니다. 이러한 설정이 없으면 디스플레이에 아무것도 표시할 수 없습니다.
더 자세한 정보는 CrowPanel 2.8″ ESP32 디스플레이: 간편 설정 가이드 튜토리얼을 참조하세요 . 해당 튜토리얼에서는 2.8인치 디스플레이 설정 방법을 설명하고 있지만, 3.5인치 디스플레이에도 동일한 단계와 설명이 적용됩니다. 화면 크기와 드라이버 설정만 다릅니다.
터치스크린 보정
CrowPanel 3.5인치 디스플레이에는 TFT_eSPI 라이브러리를 사용하기 전에 먼저 보정해야 하는 저항막 터치스크린이 포함되어 있습니다. 아래 코드를 " digiclock.ino" 파일에 복사하고 컴파일한 다음 CrowPanel 3.5인치 디스플레이에 업로드하십시오.
// digiclock.ino
#include "tft_setup.h"
#include "TFT_eSPI.h"
TFT_eSPI tft = TFT_eSPI();
void setup() {
Serial.begin(115200);
tft.begin();
tft.setRotation(0);
}
void loop() {
uint16_t cal[5];
tft.setRotation(1); // Landscape orientation!
tft.fillScreen(TFT_BLACK);
tft.setCursor(20, 0);
tft.setTextFont(2);
tft.setTextSize(1);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.print("Touch corners ... ");
tft.calibrateTouch(cal, TFT_MAGENTA, TFT_BLACK, 15);
tft.println("done.");
Serial.printf("cal: {%d, %d, %d, %d, %d}\n",
cal[0], cal[1], cal[2], cal[3], cal[4]);
delay(10000);
}
코드가 실행되면 디스플레이에 화살표가 표시되고 화살표가 가리키는 모서리를 터치하라는 메시지가 나타납니다. 나머지 세 모서리에 대해서도 동일한 과정이 반복됩니다. 디스플레이에 포함된 작은 펜을 사용하여 최대한 정확하게 모서리를 터치해 보세요.

터치스크린 보정
보정 과정이 끝나면 코드는 5개의 보정 매개변수(코너 좌표 및 화면 방향)를 시리얼 모니터에 출력합니다. 다음과 같은 내용이 표시될 것입니다.
cal: { 243, 3669, 216, 3553, 7 }
시계 코드에서 필요하므로 매개변수를 어딘가에 복사해 두세요. 보정은 10초마다 반복되므로 여러 번 시도하여 가장 정확한 매개변수를 얻을 수 있습니다.
보정 과정에 대한 자세한 내용은 CrowPanel 2.8″ ESP32 디스플레이: 간편 설정 가이드 튜토리얼을 참조하세요 . 단, 해당 튜토리얼에서는 디스플레이를 세로 방향으로 설정하여 보정하는 반면, 여기서는 가로 방향으로 설정하여 보정합니다( setRotation(1)위 보정 코드 참조).
다음으로, 인터넷에서 시간 정보를 얻는 방법에 대해 이야기해 보겠습니다.
인터넷에서 시간 데이터 다운로드
저는 일광 절약 시간에 자동으로 맞춰지고 항상 정확한 시간을 유지하는 시계를 좋아합니다. 그래야 제가 직접 시간을 설정할 필요가 없으니까요. Wi-Fi와 인터넷이 있다면, 가장 쉬운 방법은 시간 서버에서 정기적으로 현재 시간을 다운로드한 다음 ESP32의 내부 시계를 조정하는 것입니다. 이와 관련한 자세한 내용은 '일광 절약 시간 자동 시계 제작 방법' 튜토리얼을 참고하세요 .
여기서는 이전 튜토리얼과 동일한 방법을 사용하겠습니다. 즉, WorldTimeAPI 를 사용하여 시간 데이터를 가져올 것입니다. WorldTimeAPI 는 현재 시간을 일반 텍스트 또는 JSON 형식으로 반환하는 간단한 웹 서비스입니다. 웹사이트를 사용하여 특정 시간대의 시간을 가져오거나 컴퓨터의 IP 주소를 기반으로 시간을 가져올 수 있습니다. 우리는 후자를 사용하겠습니다. 이 방법이 더 간편하고, 시계가 일광 절약 시간제(DST)뿐만 아니라 다른 시간대로 이동할 때도 자동으로 조정됩니다.
WorldTimeAPI를 사용해 보는 것은 매우 쉽습니다. 이 링크( http://worldtimeapi.org/api/ip) 를 클릭 하거나 브라우저 검색창에 입력하기만 하면 됩니다. IP 주소를 직접 입력할 필요는 없습니다. 웹 서비스가 쿼리가 시작된 위치를 기반으로 자동으로 IP 주소를 파악합니다.
브라우저에서 다음과 유사한 출력을 볼 수 있습니다(읽기 쉽게 서식을 추가하고 IP 주소를 가렸습니다).
{
"abbreviation": "AEDT",
"client_ip": "122.150.000.000",
"datetime": "2023-11-16T12:09:46.409360+11:00",
"day_of_week": 4,
"day_of_year": 320,
"dst": true,
"dst_from": "2023-09-30T16:00:00+00:00",
"dst_offset": 3600,
"dst_until": "2024-04-06T16:00:00+00:00",
"raw_offset": 36000,
"timezone": "Australia/Melbourne",
"unixtime": 1700096986,
"utc_datetime": "2023-11-16T01:09:46.409360+00:00",
"utc_offset": "+11:00",
"week_number": 46
}
이 출력은 JSON 형식입니다. 현재 시간 외에도 다른 정보가 포함되어 있지만, 우리는 특히 datetime현재 현지 시간을 나타내는 필드에 관심이 있습니다. 이 예시에서는 해당 필드가 입니다 "2023-11-16T12:09:46.409360+11:00".
이 링크를 클릭할 때마다 업데이트된 시간을 확인할 수 있습니다. 하지만 너무 자주 클릭하지 마세요. 그렇지 않으면 차단될 수 있습니다 !
이로써 디지털 시계를 구현하는 데 필요한 모든 구성 요소를 갖추게 되었습니다.
디지털 시계 구현하기
이 섹션에서는 디지털 시계를 구현할 것입니다. 화면은 다음과 같습니다.

디지털 시계의 표시 예시
이 제품은 현재 시간(인터넷 동기화), 요일, 월, 연도 및 두 개의 버튼이 표시됩니다. "24시간" 버튼을 사용하면 24시간제와 12시간제 형식을 전환할 수 있으며, "요일" 버튼을 사용하면 낮과 밤에 맞춰 디스플레이 밝기를 조절할 수 있습니다.
이 시계를 구현하기 위해 먼저 프로젝트 구조부터 살펴보겠습니다.
프로젝트 폴더 구조
이전 단계에서 이미 " digiclock" 프로젝트 폴더를 만들고 그 안에 " digiclock.ino"와 " " 파일을 생성했습니다. 이제 " "라는 파일 이름을 하나 더 추가하세요. 프로젝트 폴더는 다음과 같아야 합니다.tft_setup.hdatestr.h

프로젝트 폴더 구조
이제 아두이노 IDE에 " digiclock.ino", " datestr.h" 및 " tft_setup.h" 탭이 세 개 표시되어야 합니다.

아두이노 IDE의 탭
" tft_setup.h" 파일에는 이미 올바른 코드가 채워져 있습니다. 하지만 나머지 두 파일(" digiclock.ino", " datestr.h")은 업데이트해야 합니다. 먼저 " datestr.h" 파일부터 시작해 보겠습니다.
요일 및 월 이름
시간 외에도 현재 날짜를 텍스트로 표시하고 싶습니다. 예를 들어, "2024년 5월 30일 목요일"처럼요. 하지만 WorldTimeAPI 에서는 현재 월이나 일의 이름을 제공하지 않습니다. 따라서 숫자 5와 같은 월 이름을 "5월"과 같은 월 이름으로 변환해야 합니다.
나중에 사용할 TimeLib 라이브러리에는 실제로 이를 위한 함수가 있지만, 제대로 작동하지 않았습니다. sprintf를 통해 문자열에 쓰기 시 발생하는 메모리 할당 문제 때문이었는데, 해결하는 데 많은 시간을 들이고 싶지 않았습니다.
따라서 우리는 자체적인 월과 요일 이름을 정의합니다. 이는 충분히 간단하며 원하는 대로 이름을 변경할 수 있다는 장점이 있습니다. 전체 이름(월요일), 세 글자(월), 두 글자(월), 대문자 또는 소문자, 다른 언어 등… 모두 여러분의 선택에 달려 있습니다. 다음 코드를 "datestr.h" 파일에 복사한 후 원하는 대로 수정하세요.
// datestr.h
const char *DAYSTR[] = {
"ERROR",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat"
};
const char *MONTHSTR[] = {
"ERROR",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
메인 프로그램
이 섹션에서는 시계의 핵심인 메인 프로그램을 구현할 것입니다. 다음 코드를 " digiclock.ino" 파일에 복사하세요. 기존의 보정 코드를 완전히 덮어쓰세요.
// digiclock.ino
#include "tft_setup.h"
#include "stdarg.h"
#include "WiFi.h"
#include "TFT_eSPI.h"
#include "TFT_eWidget.h"
#include "HTTPClient.h"
#include "ArduinoJson.h"
#include "TimeLib.h"
#include "datestrs.h"
#define WIFI_SSID "YOUR_SSID"
#define WIFI_PASSPHRASE "YOUR_PWD"
#define URL "http://worldtimeapi.org/api/ip"
StaticJsonDocument<2048> doc;
uint16_t cal[5] = { 243, 3669, 216, 3553, 7 };
char timeStr[20];
char dateStr[40];
TFT_eSPI tft = TFT_eSPI();
ButtonWidget btn1 = ButtonWidget(&tft);
ButtonWidget btn2 = ButtonWidget(&tft);
ButtonWidget* btns[] = { &btn1, &btn2 };
bool is24h = true;
bool isDay = true;
void btn1_pressed(void) {
if (btn1.justPressed()) {
is24h = !btn1.getState();
btn1.drawSmoothButton(is24h, 1, TFT_DARKGREY, is24h ? "24h" : "12h");
}
}
void btn2_pressed(void) {
if (btn2.justPressed()) {
isDay = !btn2.getState();
btn2.drawSmoothButton(isDay, 1, TFT_DARKGREY, isDay ? "day" : "night");
}
}
void initButtons() {
uint16_t w = 100;
uint16_t h = 50;
uint16_t y = tft.height() - h + 12;
uint16_t x = tft.width() / 2;
tft.setTextFont(4);
btn1.initButtonUL(x - w - 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "24h", 1);
btn1.setPressAction(btn1_pressed);
btn1.drawSmoothButton(is24h, 1, TFT_BLACK);
btn2.initButtonUL(x + 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "day", 1);
btn2.setPressAction(btn2_pressed);
btn2.drawSmoothButton(isDay, 1, TFT_BLACK);
}
void handleButtons() {
tft.setTextFont(4);
uint8_t nBtns = sizeof(btns) / sizeof(btns[0]);
uint16_t x = 0, y = 0;
bool touched = tft.getTouch(&x, &y);
for (uint8_t b = 0; b < nBtns; b++) {
if (touched) {
if (btns[b]->contains(x, y)) {
btns[b]->press(true);
btns[b]->pressAction();
}
} else {
btns[b]->press(false);
btns[b]->releaseAction();
}
}
}
bool shouldSyncTime() {
time_t t = now();
bool wifi_on = WiFi.status() == WL_CONNECTED;
bool should_sync = (minute(t) == 0 && second(t) == 3) || (year(t) == 1970);
return wifi_on && should_sync;
}
void syncTime() {
delay(1000);
HTTPClient http;
http.begin(URL);
if (http.GET() > 0) {
String json = http.getString();
auto error = deserializeJson(doc, json);
if (!error) {
int Y, M, D, h, m, s, ms, tzh, tzm;
sscanf(doc["datetime"], "%d-%d-%dT%d:%d:%d.%d+%d:%d",
&Y, &M, &D, &h, &m, &s, &ms, &tzh, &tzm);
setTime(h, m, s, D, M, Y);
}
}
http.end();
}
void updateDisplay() {
time_t t = now();
sprintf(timeStr, " %2d:%02d ",
(is24h ? hour(t) : hourFormat12(t)), minute(t));
sprintf(dateStr, " %s, %s %d, %d ",
DAYSTR[weekday(t)], MONTHSTR[month(t)], day(t), year(t));
uint16_t color = isDay ? TFT_WHITE : TFT_DARKGREY;
tft.setTextColor(color, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(3);
tft.drawString(timeStr, tft.width() / 2, 120, 7);
tft.setTextSize(1);
tft.drawString(dateStr, tft.width() / 2, 230, 4);
}
void setup(void) {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
while (WiFi.status() != WL_CONNECTED)
delay(500);
tft.init();
tft.setTouch(cal);
tft.fillScreen(TFT_BLACK);
tft.setRotation(1);
initButtons();
}
void loop() {
if (shouldSyncTime())
syncTime();
updateDisplay();
handleButtons();
delay(50);
}
이 코드는 꽤 방대합니다. 다음 섹션에서는 이 코드를 분석하고 각 부분이 어떻게 서로 연관되어 작동하는지 살펴보겠습니다.
이 코드의 대부분은 다음 세 가지 튜토리얼을 기반으로 합니다. "자동 일광 절약 시간 시계 만드는 방법 ", "WS2812를 사용한 LED 링 시계 만드는 방법 " , 그리고 " CrowPanel 2.8인치 ESP32 디스플레이: 간편 설정 가이드" . 아래 코드나 설명 중 이해하기 어려운 부분이 있다면 해당 튜토리얼을 참고하세요.
라이브러리
먼저 필요한 라이브러리를 설치합니다. " TFT_eSPI " 라이브러리 외에도 " TFT_eWidget ", " ArduinoJson ", " TimeLib " 라이브러리를 설치 해야 합니다 . 평소처럼 아두이노 IDE의 라이브러리 관리자를 사용하면 됩니다.
"stdarg", " WiFi ", " HTTPClient " 등 의 다른 라이브러리는 ESP32 코어에 포함되어 있으므로 별도로 설치할 필요가 없습니다. " tft_setup.h"와 " datestrs.h"는 이전에 생성한 Arduino 프로젝트 파일입니다 digiclock. 이미 존재하지만, 표시된 대로 포함시켜야 합니다.
#include "tft_setup.h"
#include "stdarg.h"
#include "WiFi.h"
#include "TFT_eSPI.h"
#include "TFT_eWidget.h"
#include "HTTPClient.h"
#include "ArduinoJson.h"
#include "TimeLib.h"
#include "datestrs.h"
이 라이브러리들은 Wi-Fi 연결, HTTP 요청, JSON 파싱, 시간 관리, 그리고 시계의 사용자 인터페이스 생성에 필요한 기능들을 제공합니다.
상수와 객체
다음으로 몇 가지 상수와 객체를 정의합니다. StaticJsonDocument`capture` 객체는 WebTime API가 시간 요청에 대해 반환하는 응답을 저장합니다. ` calcalibration` 객체에는 디스플레이의 보정 매개변수가 포함됩니다. `time` timeStr과 ` dateStrdate`는 표시되는 시간과 날짜의 형식을 지정하는 데 사용할 문자 버퍼입니다.
StaticJsonDocument<2048> doc;
uint16_t cal[5] = { 243, 3669, 216, 3553, 7 };
char timeStr[20];
char dateStr[40];
TFT_eSPI tft = TFT_eSPI();
ButtonWidget btn1 = ButtonWidget(&tft);
ButtonWidget btn2 = ButtonWidget(&tft);
ButtonWidget* btns[] = { &btn1, &btn2 };
bool is24h = true;
bool isDay = true;
TFT_eSPI는 디스플레이를 제어하는 객체이며, btn1과 btn2는 디스플레이에 표시되는 두 개의 버튼 객체입니다. 버튼 이벤트 처리를 단순화하기 위해 버튼 객체들을 btns 배열에 저장합니다. 또한 is24h와 isDay는 두 버튼의 상태를 나타내는 두 개의 부울 플래그입니다.
버튼 기능
btn1_pressed() 및 btn2_pressed() 함수는 터치 스크린의 해당 버튼이 터치될 때 호출됩니다. 이 함수들은 버튼 상태 플래그인 is24h 및 isDay를 설정하고 버튼의 모양을 변경합니다.
void btn1_pressed(void) {
if (btn1.justPressed()) {
is24h = !btn1.getState();
btn1.drawSmoothButton(is24h, 1, TFT_DARKGREY, is24h ? "24h" : "12h");
}
}
void btn2_pressed(void) {
if (btn2.justPressed()) {
isDay = !btn2.getState();
btn2.drawSmoothButton(isDay, 1, TFT_DARKGREY, isDay ? "day" : "night");
}
}
버튼 초기화
이 initButtons()함수는 초기 화면을 생성하고 화면에서 두 버튼의 위치를 정의합니다.
void initButtons() {
uint16_t w = 100;
uint16_t h = 50;
uint16_t y = tft.height() - h + 12;
uint16_t x = tft.width() / 2;
tft.setTextFont(4);
btn1.initButtonUL(x - w - 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "24h", 1);
btn1.setPressAction(btn1_pressed);
btn1.drawSmoothButton(is24h, 1, TFT_BLACK);
btn2.initButtonUL(x + 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "day", 1);
btn2.setPressAction(btn2_pressed);
btn2.drawSmoothButton(isDay, 1, TFT_BLACK);
}
버튼이 완전히 표시되면 아래 그림과 같이 모서리가 둥근 직사각형 모양으로 나타납니다.

버튼의 원래 모양
하지만 버튼 위치를 화면 밖으로 절반 정도 나오도록 설정했더니 탭처럼 보이네요.

부분적으로 보이는 버튼
제 생각엔 이게 더 나아 보이지만, 마음에 안 드시면 y 좌표를 바꿔서 y = tft.height() - h - 10 버튼이 완전히 표시되도록 하시면 됩니다.
tft.setTextFont(4)또한 해당 initButtonUL()함수에는 글꼴을 설정하는 매개변수가 없으므로 직접 호출해야 한다는 점에 유의하십시오 .
버튼 이벤트 처리
이 handleButtons()함수는 버튼 이벤트를 처리합니다. btns배열에 저장된 모든 버튼을 순회하며, 터치 이벤트가 감지되고 getTouch()이벤트 x,y의 좌표가 버튼 영역( btns[b]->contains(x, y)) 내에 있으면 해당 버튼 함수가 호출됩니다.
void handleButtons() {
tft.setTextFont(4);
uint8_t nBtns = sizeof(btns) / sizeof(btns[0]);
uint16_t x = 0, y = 0;
bool touched = tft.getTouch(&x, &y);
for (uint8_t b = 0; b < nBtns; b++) {
if (touched) {
if (btns[b]->contains(x, y)) {
btns[b]->press(true);
btns[b]->pressAction();
}
} else {
btns[b]->press(false);
btns[b]->releaseAction();
}
}
}
시간 동기화 여부를 확인하세요
ESP32의 내부 타이머를 WorldTimeAPI 에서 다운로드한 인터넷 시간과 동기화하려고 합니다 . 하지만 너무 자주 동기화하면 WorldTimeAPI가 차단되므로 주의해야 합니다.
` shouldSyncTime(();` 함수는 현재 분이 0분 3초이거나 현재 연도가 1970년인지 확인하고, 두 조건이 모두 충족되면 시간을 동기화해야 함을 나타내는 `true`를 반환합니다. 즉, 매시간 시간을 동기화하며, 정시 3초 후에 동기화를 수행하여 정시에 발생할 수 있는 일광 절약 시간제 변경 사항을 반영합니다. 따라서 최악의 경우에도 시계는 3초 정도 차이가 날 수 있습니다.
bool shouldSyncTime() {
time_t t = now();
bool wifi_on = WiFi.status() == WL_CONNECTED;
bool should_sync = (minute(t) == 0 && second(t) == 3) || (year(t) == 1970);
return wifi_on && should_sync;
}
또한 현재 연도가 1970년인지 확인합니다. ESP32 시계는 처음 시작될 때( 유닉스 시간 시작 시점 ) 1970년을 표시합니다. 즉, ESP32 전원이 켜지면 정각이 아니더라도 가장 먼저 시간을 동기화합니다. 이 과정이 없으면 최대 한 시간 동안 시간이 완전히 잘못될 수 있습니다!
시간 동기화
이 syncTime()함수는 WorldTimeAPI에서 현재 시간을 다운로드하고 ESP32의 내부 시계를 그에 맞게 설정합니다. 이를 위해 지정된 URL로 GET 요청을 보내고 ArduinoJson 라이브러리를 사용하여 응답을 JSON 형식으로 파싱합니다 .
JSON 구조에서 연도, 월, 일, 시, 분, 초 및 시간대 정보를 추출하고 TimeLib 라이브러리 setTime() 의 함수를 사용하여 ESP32의 내부 시간을 설정합니다 .
void syncTime() {
delay(1000);
HTTPClient http;
http.begin(URL);
if (http.GET() > 0) {
String json = http.getString();
auto error = deserializeJson(doc, json);
if (!error) {
int Y, M, D, h, m, s, ms, tzh, tzm;
sscanf(doc["datetime"], "%d-%d-%dT%d:%d:%d.%d+%d:%d",
&Y, &M, &D, &h, &m, &s, &ms, &tzh, &tzm);
setTime(h, m, s, D, M, Y);
}
}
http.end();
}
디스플레이 업데이트
이 updateDisplay()함수는 먼저 현재 내부 시간을 읽어오고 now(), 이를 기반으로 시간과 날짜의 텍스트 표현으로 버퍼를 채웁니다 timeStr. 시간 형식 버튼의 dateStr상태에 따라 is24h시간은 24시간제 또는 12시간제로 표시됩니다.
형식 문자열에 추가 공백이 있는 것에 유의하세요(예: ) " %s, %s %d, %d ". 시간과 날짜 문자열은 길이가 다양하기 때문에 이 updateDisplay()함수는 화면을 지우는 대신 현재 표시된 시간을 덮어쓰기 때문에 공백이 필요합니다. 이렇게 하면 화면 깜빡임을 방지할 수 있지만 공백이 필수적입니다.
void updateDisplay() {
time_t t = now();
sprintf(timeStr, " %2d:%02d ",
(is24h ? hour(t) : hourFormat12(t)), minute(t));
sprintf(dateStr, " %s, %s %d, %d ",
DAYSTR[weekday(t)], MONTHSTR[month(t)], day(t), year(t));
uint16_t color = isDay ? TFT_WHITE : TFT_DARKGREY;
tft.setTextColor(color, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(3);
tft.drawString(timeStr, tft.width() / 2, 120, 7);
tft.setTextSize(1);
tft.drawString(dateStr, tft.width() / 2, 230, 4);
}
문자열 버퍼에 시간과 날짜를 저장한 후에는 해당 문자열과 좌표를 인수로 drawString()을 호출하여 화면에 표시합니다.
setTextDatum(MC_DATUM)은 텍스트를 중앙 정렬하고, setTextColor()는 주야 버튼의 isDay 상태에 따라 흰색 또는 짙은 회색을 사용합니다.
updateDisplay() 함수는 기본적으로 상태 is24h와 isDay플래그에 따라 네 가지 다른 화면을 표시합니다. 아래 이미지는 이 네 가지 화면을 보여줍니다.

시계의 네 가지 다른 화면
카메라와 조명 때문에 화면이 푸른빛을 띠는 것처럼 보이지만, 실제 눈으로 보는 색상은 검정, 흰색, 회색입니다. 또한, 주간 모드와 야간 모드의 차이는 실제로 보면 훨씬 더 뚜렷합니다.
화면 크기에 따라 일부 수정 이 필요할 updateDisplay()수 있습니다. 글꼴을 변경하거나 글꼴 크기를 조정해야 합니다. 또한 시간과 날짜 문자열의 세로 위치가 고정되어 있으므로 조정해야 합니다.
설정 기능
setup() 함수에서 먼저 Wi-Fi 연결을 설정하고 TFT 디스플레이를 초기화합니다. 중요한 것은 setTouch(cal)을 통한 터치 스크린 보정이며, 여기서 보정 매개변수를 사용합니다. 이를 수행하지 않으면 버튼이 제대로 작동하지 않습니다. 또한 setRotation(1)을 통해 화면을 가로 모드로 설정한다는 점도 유의하십시오.
void setup(void) {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
while (WiFi.status() != WL_CONNECTED)
delay(500);
tft.init();
tft.setTouch(cal);
tft.fillScreen(TFT_BLACK);
tft.setRotation(1);
initButtons();
}
저는 검은색 배경색을 사용하지만 다른 색상을 선택할 수도 있습니다. 다만, updateDisplay()와 initButtons()에서 문자열과 버튼의 배경색도 함께 조정해야 합니다.
설정의 마지막 부분은 initButtons()버튼을 생성하고 그리는 함수 호출입니다.
반복문
위의 모든 헬퍼 함수 덕분에 메인 루프는 이제 매우 간단해졌습니다. 먼저 ESP32를 인터넷 시간과 동기화해야 하는지 확인합니다. 그렇다면 syncTime()을 호출하여 동기화를 수행합니다.
void loop() {
if (shouldSyncTime())
syncTime();
updateDisplay();
handleButtons();
delay(50);
}
그 후 현재 시간 정보를 화면에 표시하고 handleButtons()버튼 이벤트를 처리하는 함수를 호출합니다. 루프는 50ms마다 실행되므로 버튼 클릭에 반응하기에 충분히 빠릅니다. 다른 함수는 100~500ms와 같이 더 느린 주기로 실행될 수도 있지만, 빠른 속도로 실행해도 문제가 없습니다.
자, 이렇게 해서 언제나 정확하고 멋진 작은 시계가 완성되었습니다!
결론
저희가 도입한 디지털 시계는 아주 잘 작동하지만, 더 나은 시계로 만들기 위해 몇 가지 기능을 추가해 보시면 좋을 것 같습니다.
예를 들어, 밝기를 수동으로 조절하는 대신 주변 조도에 따라 자동으로 밝기를 조절하는 광저항 센서(LDR)를 추가할 수 있습니다. 또한 주변에 아무도 없다면 시간을 표시하는 것은 의미가 없습니다. PIR 센서를 사용하면 움직임을 감지하여 사람이 감지되면 시계를 작동시킬 수 있습니다. 자세한 내용은 WS2812를 사용한 LED 링 시계 튜토리얼을 참조하세요 .
날짜 형식을 변경하거나 외부 조명을 켜고 끄는 버튼을 추가할 수도 있습니다. CrowPanel 2.8″ ESP32 디스플레이: 간편 설정 가이드를 참고하시면 디스플레이에서 두 개의 LED를 제어하는 방법을 확인할 수 있습니다. WorldTimeAPI를 사용하면 시간대를 지정할 수도 있으므로 시간대 또는 도시 간 전환 버튼을 만들 수 있습니다.
또는 WorldTimeAPI 대신 SNTP 서버에 연결하여 더 자주 시간을 동기화할 수도 있습니다. ESP32 시계를 SNTP 서버와 동기화하는 방법에 대한 튜토리얼을 참조하세요 .
CrowPanel은 I2C 인터페이스를 제공하므로 온도, 습도 또는 공기질 센서를 쉽게 추가하여 디스플레이에 더 많은 정보를 표시할 수 있습니다. 자세한 내용은 AM2320 디지털 온도 및 습도 센서 아두이노 튜토리얼 과 SGP30 다용도 공기질 센서 와 아두이노 인터페이스 연결 튜토리얼을 참조하세요.
마지막으로, 시간 정보 외에도 인터넷에서 현재 날씨 데이터를 다운로드하여 표시할 수 있습니다. OpenWeather 를 살펴보세요 . 시간별 날씨 업데이트를 위한 충분한 데이터 용량을 제공하는 무료 플랜이 있습니다.
더 궁금한 점이나 아이디어가 있으면 댓글로 남겨주세요. 그럼 즐거운 시간 보내세요 ; )
모래밭
이 글을 쓰는 데 도움이 된 링크 몇 개를 소개합니다.
Links
Here some links that I found helpful for writing this post.
- CrowPanel 3.5″-ESP32 Display
- CrowPanel ESP32 Display Wiki
- CrowPanel ESP32 Display User_Manual
- CrowPanel 3.5″ Schematic Diagram
- CrowPanel ESP32 Display Video Tutorials
- ESP32-LVGL-DESK-CLOCK
- TFT 3.5″ Touch Screen & ESP32 built in – Elecrow review
- Getting Started Tips 3.5″ Elecrow TFT ESP32
- Review of Elecrow’s 3.5-inch and 7.0-inch ESP32 display modules using Arduino programming
'ESP32' 카테고리의 다른 글
| ESP32-C6 RGB LED 제어 (0) | 2026.01.01 |
|---|---|
| CrowPanel ESP32 Display 3.5인치 모듈 터치 기능 동작하지 않을 때 (0) | 2026.01.01 |
| ESP32 3.5inch TFT-LCD 개발 환경 설정 - 문서로 제공할 것 (0) | 2026.01.01 |
| ESP32 4G/5G 및 GPS 인터페이스 (0) | 2026.01.01 |
| ESP32 및 ESP8266을 이용한 MicroPython 프로그래밍 (0) | 2025.12.29 |
| DS3231 모듈을 사용한 ESP32 실시간 시계 (1) | 2025.12.26 |
| ESP32와 OLED 디스플레이를 이용한 인터넷 시계 (0) | 2025.12.26 |
| ESP-IDF Programming Guide (0) | 2025.12.24 |
취업, 창업의 막막함, 외주 관리, 제품 부재!
당신의 고민은 무엇입니까? 현실과 동떨어진 교육, 실패만 반복하는 외주 계약,
아이디어는 있지만 구현할 기술이 없는 막막함.
우리는 알고 있습니다. 문제의 원인은 '명확한 학습, 실전 경험과 신뢰할 수 있는 기술력의 부재'에서 시작됩니다.
이제 고민을 멈추고, 캐어랩을 만나세요!
코딩(펌웨어), 전자부품과 디지털 회로설계, PCB 설계 제작, 고객(시장/수출) 발굴과 마케팅 전략으로 당신을 지원합니다.
제품 설계의 고수는 성공이 만든 게 아니라 실패가 만듭니다. 아이디어를 양산 가능한 제품으로!
귀사의 제품을 만드세요. 교육과 개발 실적으로 신뢰할 수 있는 파트너를 확보하세요.
캐어랩