За последние 5 лет на рынке завоевали популярность круглые TFT IPS дисплеи на базе контроллера GC9A01. Благодаря низкой цене и поддержке популярных библиотек данные ЖК-индикаторы широко применяются в различных DIY-проектах: от простых часов и компаса до эквалайзера и «пузырькового» строительного уровня. В рамках данной статьи мы разберёмся, как подключить дисплей GC9A01 к отладочной плате ESP32 и как вывести на экран стрелочные Wi-Fi часы, используя программные пакеты tft_eSPI и Adafruit_GC9A01A_Library.

Содержание
Вариации модулей
На рынке существует множество вариаций плат индикаторов под управлением контроллера GC9A01:
– на 8-угольная черного или синего цвета

– прямоугольная с закруглениями, имеется разъём для подключения micrоSD-карт памяти

– синяя округлая с прямоугольным выступом под разъём

– только индикатор со шлейфом, без платы

Я приобрёл наиболее популярную модель – округлую синюю:

Общее описание
Герой нашей статьи – жидкокристаллический TFT-дисплей с круглой матрицей, выполненной по технологии IPS, что обеспечивает широкие углы обзора (более 160°), высокую яркость (до 400 кд/м2), а также полноцветный режим с глубиной цвета до 262144 оттенков.
Диаметр круглой поверхности: 35 мм.
Диаметр отображаемой части: 1,28 дюйма.
Разрешение экрана: 240 ×240 пикселей.
Интерфейс: SPI.
БЕЗ тачскрина (без сенсорной панели).
Напряжение питания: 3,3 В.
Максимальной ток потребления: до 40 мА при максимальной яркости и полной заливке.
Схема питания подсветки уже распаяна, внешний токоограничивающий резистор не требуется.
На большинстве моделей установлен линейный понижающий стабилизатор напряжения на 3,3 В. Поэтому устройство можно питать от 5 В.
Однако контроллер работает с логикой на уровне 3,3 В. Поэтому рекомендуется напрямую подключать сигнальные выводы только к контроллеру с соответствующим уровнем. Если подключать к Arduino UNO с 5-вольтовой логикой, то следует применять преобразователи логических уровней (резистивные делители напряжения, преобразователи на биполярных транзисторах, специальные микросхемы типа TXS0108).
Ниже представлена распиновка платы:

Как подключить GC9A01 к ESP32
Для экспериментов воспользуемся отладочной платой NodeMCU-32S (38 pin), которая построена на модуле ESP-WROOM-32. Ниже представлена схема подключения ЖК-индикатора к отладочной плате, а также фотография собранного макета:


Программный код (скетч)
Стрелочные часы с круглым циферблатом – самый наглядный пример, который демонстрирует наш индикатор во всей красе. Рассмотрим написание проекта с применением специализированного функционального пакета от Adafruit и популярного пакета tft_eSPI.
Библиотека tft_eSPI
Для удобной разработки рекомендую среду программирования PlatformIO (расширение для редактора кода Visual Studio Code) с использованием фреймворка Arduino.
Очень наглядная инструкция, в которой подробно и пошагово описаны процессы подготовки проекта в среде PlatformIO и настройки функционального пакета tft_eSPI, представлена в статье про двухосевой джойстик KY-023.
Предельно важно уделить внимание настройке конфигурационного файла User_Setup, подготавливая библиотеку под конкретную модель контроллера ЖК-индикатора. Ниже я укажу на конкретные блоки, которые обязательно нужно проверить (остальные оставить по умолчанию):
– выбор драйвера
– определение разрешения экрана
– цветовая калибровка дисплея. На моём экземпляре защитная наклейка с зелёным ярлыком, потому я попробовал выбрать «GREENTAB». В итоге цвета отображаются корректно. Если Вы назначаете тексту один цвет, а по факту отображается другой, то это означает ошибку в выборе типа цветовой калибровки.
– назначение пинов на отладочной плате для подключения дисплея
Если Вы хотите создать проект в среде Arduino IDE 2, то подробная инструкция по настройке представлена в статье про Интернет-часы на базе ESP32-C3 или про микрофон INMP441. А как настроить пакет tft_eSPI рассказывается в статье про лазерный дальномер VL53L0X.
Ниже представлен очень подробно прокомментированный код для стрелочных Wi-Fi часов:
// Подключаем библиотеку для работы с Wi-Fi (нужна для доступа в интернет)
#include <WiFi.h>
// Подключаем библиотеку для работы с временем (получение времени через NTP)
#include <time.h>
// Подключаем библиотеку для работы с дисплеем TFT_eSPI (адаптирована под GC9A01)
#include <TFT_eSPI.h>
// Имя (SSID) вашей Wi-Fi сети - замените на своё
const char* ssid = "TP-Link_01_12";
// Пароль от вашей Wi-Fi сети
const char* password = "sprytron_ru";
// Смещение от UTC в секундах (10800 секунд = 3 часа, Москва UTC+3)
const long gmtOffset_sec = 10800;
// Смещение на летнее время (0 - не используем, т.к. в России отменено)
const int daylightOffset_sec = 0;
// Адрес NTP-сервера для получения точного времени
const char* ntpServer = "pool.ntp.org";
// Создаём объект tft для управления дисплеем
TFT_eSPI tft = TFT_eSPI();
// Координата X центра циферблата (120 - центр для дисплея 240x240)
const int centerX = 120;
// Координата Y центра циферблата
const int centerY = 120;
// Радиус циферблата (отступ от края 10 пикселей)
const int radius = 110;
// Храним прошлые значения для удаления старых стрелок
int lastHour = -1; // Последний отрисованный час (12-часовой формат)
int lastMinute = -1; // Последняя отрисованная минута
int lastSecond = -1; // Последняя отрисованная секунда
int lastExtra = -1; // Доп. значение для часовой стрелки (учёт минут)
// Отрисовка циферблата
void drawDial() {
// Заливаем весь экран чёрным цветом (очистка)
tft.fillScreen(TFT_BLACK);
// Рисуем внешнюю рамку циферблата (основной круг)
tft.drawCircle(centerX, centerY, radius, TFT_WHITE);
// Рисуем второй круг на 1 пиксель меньше для толщины
tft.drawCircle(centerX, centerY, radius - 1, TFT_WHITE);
// Рисуем 60 минутных/часовых меток
for (int i = 0; i < 60; i++) {
// Преобразуем номер метки в угол (6 градусов = 1 минута)
float angle = (i * 6) * PI / 180;
int startRadius, endRadius;
// Если метка кратна 5 (т.е. часовая метка)
if (i % 5 == 0) {
// Часовая метка длиннее - от края до отступа 25
startRadius = radius - 1;
endRadius = radius - 25;
} else {
// Минутная метка короче - от отступа 5 до отступа 12
startRadius = radius - 5;
endRadius = radius - 12;
}
// Вычисляем координаты начала метки
int x1 = centerX + startRadius * sin(angle);
int y1 = centerY - startRadius * cos(angle);
// Вычисляем координаты конца метки
int x2 = centerX + endRadius * sin(angle);
int y2 = centerY - endRadius * cos(angle);
// Рисуем линию-метку белым цветом
tft.drawLine(x1, y1, x2, y2, TFT_WHITE);
}
// Настраиваем цвет и размер шрифта для цифр
tft.setTextColor(TFT_WHITE);
tft.setTextSize(2);
// Рисуем число "12" в верхней части (сдвиг для центрирования)
tft.setCursor(centerX - 12, 37);
tft.print("12");
// Рисуем цифру "3" справа
tft.setCursor(centerX + radius - 45, centerY - 8);
tft.print("3");
// Рисуем цифру "6" снизу
tft.setCursor(centerX - 6, centerY + radius - 45);
tft.print("6");
// Рисуем цифру "9" слева
tft.setCursor(centerX - radius + 35, centerY - 8);
tft.print("9");
// Рисуем центральную точку (ось вращения стрелок)
tft.fillCircle(centerX, centerY, 5, TFT_WHITE);
}
// Восстановление меток
void restoreMarks() {
// Перебираем все 60 меток
for (int i = 0; i < 60; i++) {
// Вычисляем угол для текущей метки
float angle = (i * 6) * PI / 180;
int startRadius, endRadius;
if (i % 5 == 0) {
// Для часовых меток: от отступа 15 до отступа 25
startRadius = radius - 15;
endRadius = radius - 25;
} else {
// Для минутных меток: от отступа 5 до отступа 12
startRadius = radius - 5;
endRadius = radius - 12;
}
// Вычисляем координаты
int x1 = centerX + startRadius * sin(angle);
int y1 = centerY - startRadius * cos(angle);
int x2 = centerX + endRadius * sin(angle);
int y2 = centerY - endRadius * cos(angle);
// Перерисовываем метку белым цветом
tft.drawLine(x1, y1, x2, y2, TFT_WHITE);
}
}
// Восстановление цифр
void restoreNumbers() {
// Настраиваем цвет и размер
tft.setTextColor(TFT_WHITE);
tft.setTextSize(2);
// Перерисовываем цифры в тех же местах
tft.setCursor(centerX - 12, 37);
tft.print("12");
tft.setCursor(centerX + radius - 45, centerY - 8);
tft.print("3");
tft.setCursor(centerX - 6, centerY + radius - 45);
tft.print("6");
tft.setCursor(centerX - radius + 35, centerY - 8);
tft.print("9");
}
// Отрисовка стрелок часов и минут
void drawHand(int oldValue, int newValue, int oldExtra, int newExtra,
int length, uint16_t color, bool isHour) {
// Стирание стрелки
if (oldValue != -1) {
float oldAngle;
// Вычисляем угол старой стрелки
if (isHour) {
// Часовая: 30 градусов на час + 0.5 градуса на минуту
oldAngle = (oldValue * 30 + oldExtra * 0.5) * PI / 180;
} else {
// Минутная: 6 градусов на минуту
oldAngle = (oldValue * 6) * PI / 180;
}
// Координаты конца старой стрелки
int oldX = centerX + length * sin(oldAngle);
int oldY = centerY - length * cos(oldAngle);
// Рисуем чёрную линию поверх старой стрелки (стираем)
tft.drawLine(centerX, centerY, oldX, oldY, TFT_BLACK);
// Восстанавливаем центральную точку (была закрыта стрелкой)
tft.fillCircle(centerX, centerY, 5, TFT_BLACK);
tft.fillCircle(centerX, centerY, 5, TFT_WHITE);
// Для минутной стрелки восстанавливаем метки и цифры
if (!isHour) {
restoreMarks(); // Восстанавливаем все метки
restoreNumbers(); // Восстанавливаем все цифры
}
}
// РОтрисовка новой стрелки
float newAngle;
if (isHour) {
// Угол для часовой стрелки с учётом минут
newAngle = (newValue * 30 + newExtra * 0.5) * PI / 180;
} else {
// Угол для минутной стрелки
newAngle = (newValue * 6) * PI / 180;
}
// Координаты конца новой стрелки
int newX = centerX + length * sin(newAngle);
int newY = centerY - length * cos(newAngle);
// Рисуем новую стрелку указанным цветом
tft.drawLine(centerX, centerY, newX, newY, color);
// Восстанавливаем центральную точку (поверх стрелки)
tft.fillCircle(centerX, centerY, 5, TFT_BLACK);
tft.fillCircle(centerX, centerY, 5, TFT_WHITE);
}
// Отрисовка секундной стрелки (красная точка)
void drawSecond(int oldValue, int newValue, int length, uint16_t color) {
// Стираем старую точку
if (oldValue != -1) {
// Угол старой позиции (6 градусов на секунду)
float oldAngle = (oldValue * 6) * PI / 180;
int oldX = centerX + length * sin(oldAngle);
int oldY = centerY - length * cos(oldAngle);
// Рисуем чёрный кружок БОЛЬШЕГО радиуса (4 пикселя)
// чтобы гарантированно стереть старую точку
tft.fillCircle(oldX, oldY, 4, TFT_BLACK);
}
// Отрисовка новой точки
float newAngle = (newValue * 6) * PI / 180;
int newX = centerX + length * sin(newAngle);
int newY = centerY - length * cos(newAngle);
// Рисуем красную точку радиусом 3 пикселя
tft.fillCircle(newX, newY, 3, color);
// Восстанавливаем внешнюю рамку циферблата
tft.drawCircle(centerX, centerY, radius, TFT_WHITE);
tft.drawCircle(centerX, centerY, radius - 1, TFT_WHITE);
}
void setup() {
// Запускаем последовательный порт для отладки
Serial.begin(115200);
tft.begin(); // Запускаем дисплей
tft.setRotation(2); // Поворачиваем на 180 градусов
tft.fillScreen(TFT_BLACK); // Заливаем экран чёрным (очистка)
tft.setTextColor(TFT_BLUE, TFT_BLACK); // Настраиваем цвет текста (синий) и фон (чёрный)
tft.setTextSize(3); // Устанавливаем размер шрифта
int textX = 25; // Подобранное значение для центрирования
int textY = 105; // Позиция по Y: центр экрана минус половина высоты текста
tft.setCursor(textX, textY); // Устанавливаем курсор
tft.print("SPRYTRON.RU"); // Выводим текст
Serial.print("Подключение к Wi-Fi");
// Запускаем подключение к указанной сети
WiFi.begin(ssid, password);
// Ждём подключения (проверяем статус)
while (WiFi.status() != WL_CONNECTED) {
delay(500); // Ждём полсекунды
Serial.print("."); // Печатаем точки для индикации процесса
}
Serial.println("\nWi-Fi подключен!");
// Передаём NTP-серверу настройки смещения
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
// Ждём, пока время не будет получено
struct tm timeinfo;
while (!getLocalTime(&timeinfo)) {
Serial.println("Ждём NTP...");
delay(1000); // Ждём 1 секунду перед следующей попыткой
}
Serial.println("Время получено!");
// Отрисовка циферблата (один раз)
drawDial();
}
void loop() {
// Получаем текущее локальное время
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
// Если не удалось получить время - выводим ошибку и выходим
Serial.println("Не удалось получить время");
return;
}
// Извлекаем часы, минуты, секунды
int hours = timeinfo.tm_hour;
int minutes = timeinfo.tm_min;
int seconds = timeinfo.tm_sec;
// Преобразуем в 12-часовой формат (для циферблата)
int hours12 = hours % 12;
// Определяем, изменились ли значения (для оптимизации перерисовки)
bool hourChanged = (hours12 != lastHour);
bool minuteChanged = (minutes != lastMinute);
bool secondChanged = (seconds != lastSecond);
// Часовая стрелка
// Вычисляем дополнительное смещение для часов (учёт минут)
int currentExtra = minutes;
// Если час изменился ИЛИ изменились минуты (смещение часов)
if (hourChanged || (currentExtra != lastExtra)) {
// Рисуем часовую стрелку: длина 70, оранжевый цвет
drawHand(lastHour, hours12, lastExtra, currentExtra, 70, TFT_ORANGE, true);
// Сохраняем текущие значения
lastHour = hours12;
lastExtra = currentExtra;
}
// Минутная стрелка
if (minuteChanged) {
// Рисуем минутную стрелку: длина 100, голубой цвет
drawHand(lastMinute, minutes, 0, 0, 100, TFT_CYAN, false);
// Сохраняем текущее значение
lastMinute = minutes;
}
// Секундная стрелка(точка)
if (secondChanged) {
// Рисуем красную точку на расстоянии 105 пикселей от центра
drawSecond(lastSecond, seconds, 105, TFT_RED);
// Сохраняем текущее значение
lastSecond = seconds;
}
// Небольшая задержка для стабильности и снижения нагрузки
delay(50);
}
После подачи питания микроконтроллер инициализируется. Далее инициализируется ЖК-индикатор, а потом микроконтроллер отладочной платы подключается к указанной Wi-Fi сети. Подключившись, выполняется запрос на сервер pool.ntp.org посредством Интернет-протокола NTP (Network Time Protocol), который применяется для синхронизации времени устройств.
Если слишком долго не удаётся получить данные по NTP, то перезагрузите микроконтроллер.
Получив точное значение UTC-времени, прибавляем 3 часа (10800 секунд) для соответствия часовому поясу Москвы.
В процессе выполнения основного цикла loop время проверяется каждые 50 миллисекунд. Секундная стрелка представлена в виде красной точки, которая перемещается по внутренней стороне белой окружности.
Для оптимизации отображения перерисовываются только те элементы, которые должны смещаться (стрелки часов и минут, точка секунд). Раз в минуту обновляются минутные метки на циферблате после полного оборота секундной точки. Дополнительно обновляются цифры (если их «затёрли стрелки»).
Библиотека Adafruit
Если нужно максимально упростить работу с экраном, то рекомендуется использовать библиотеку Adafruit_GC91A01A_Library:
Дополнительно нужно подключить графический пакет Adafruit_GFX:
Связка этих двух программных модулей позволяет настраивать яркость и контраст, управлять ориентацией отображения (setRotation), выполнять заливку всего экрана или его части (fillScreen, fillRect), рисовать базовые геометрические фигуры (линии, прямоугольники, круги), выводить изображения и текст с разными шрифтами.
После подключения всех пакетов проверьте их наличие в файле platformio.ini:

Ниже представлен подробно прокомментированный код:
// Подключаем библиотеку для работы с Wi-Fi (нужна для доступа в интернет)
#include <WiFi.h>
// Подключаем библиотеку для работы с временем (получение времени через NTP)
#include <time.h>
// Подключаем библиотеки Adafruit для работы с дисплеем GC9A01
#include <Adafruit_GFX.h>
#include <Adafruit_GC9A01A.h>
// Пины подключения к ESP32 (настройте под Вашу схему)
#define TFT_CS 15 // Выбор устройства
#define TFT_DC 2 // Данные/команда
#define TFT_RST 4 // Reset
#define TFT_MOSI 23 // SDA (данные)
#define TFT_SCLK 18 // SCL (тактирование)
// Создаём объект для аппаратного SPI
SPIClass *spi = nullptr;
// Объявляем объект дисплея
Adafruit_GC9A01A *tft = nullptr;
// Имя (SSID) вашей Wi-Fi сети - замените на своё
const char* ssid = "TP-Link_01_12";
// Пароль от вашей Wi-Fi сети
const char* password = "sprytron_ru";
// Смещение от UTC в секундах (10800 секунд = 3 часа, Москва UTC+3)
const long gmtOffset_sec = 10800;
// Смещение на летнее время (0 - не используем, так как в России отменено)
const int daylightOffset_sec = 0;
// Адрес NTP-сервера для получения точного времени
const char* ntpServer = "pool.ntp.org";
// Координата X центра циферблата (120 - центр для дисплея 240x240)
const int centerX = 120;
// Координата Y центра циферблата
const int centerY = 120;
// Радиус циферблата (отступ от края 10 пикселей)
const int radius = 110;
// Храним прошлые значения для удаления старых стрелок
int lastHour = -1; // Последний отрисованный час (12-часовой формат)
int lastMinute = -1; // Последняя отрисованная минута
int lastSecond = -1; // Последняя отрисованная секунда
int lastExtra = -1; // Доп. значение для часовой стрелки (учёт минут)
// Отрисовка циферблата
void drawDial() {
// Заливаем весь экран чёрным цветом (очистка)
tft->fillScreen(GC9A01A_BLACK);
// Рисуем внешнюю рамку циферблата (основной круг)
tft->drawCircle(centerX, centerY, radius, GC9A01A_WHITE);
// Рисуем второй круг на 1 пиксель меньше для толщины
tft->drawCircle(centerX, centerY, radius - 1, GC9A01A_WHITE);
// Рисуем 60 минутных/часовых меток
for (int i = 0; i < 60; i++) {
// Преобразуем номер метки в угол (6 градусов = 1 минута)
float angle = (i * 6) * PI / 180;
int startRadius, endRadius;
// Если метка кратна 5 (т.е. часовая метка)
if (i % 5 == 0) {
// Часовая метка длиннее - от края до отступа 25
startRadius = radius - 1;
endRadius = radius - 25;
} else {
// Минутная метка короче - от отступа 5 до отступа 12
startRadius = radius - 5;
endRadius = radius - 12;
}
// Вычисляем координаты начала метки
int x1 = centerX + startRadius * sin(angle);
int y1 = centerY - startRadius * cos(angle);
// Вычисляем координаты конца метки
int x2 = centerX + endRadius * sin(angle);
int y2 = centerY - endRadius * cos(angle);
// Рисуем линию-метку белым цветом
tft->drawLine(x1, y1, x2, y2, GC9A01A_WHITE);
}
// Настраиваем цвет и размер шрифта для цифр
tft->setTextColor(GC9A01A_WHITE);
tft->setTextSize(2);
// Рисуем число "12" в верхней части (сдвиг для центрирования)
tft->setCursor(centerX - 12, 37);
tft->print("12");
// Рисуем цифру "3" справа
tft->setCursor(centerX + radius - 45, centerY - 8);
tft->print("3");
// Рисуем цифру "6" снизу
tft->setCursor(centerX - 6, centerY + radius - 45);
tft->print("6");
// Рисуем цифру "9" слева
tft->setCursor(centerX - radius + 35, centerY - 8);
tft->print("9");
// Рисуем центральную точку (ось вращения стрелок)
tft->fillCircle(centerX, centerY, 5, GC9A01A_WHITE);
}
// Восстановление меток
void restoreMarks() {
// Перебираем все 60 меток
for (int i = 0; i < 60; i++) {
// Вычисляем угол для текущей метки
float angle = (i * 6) * PI / 180;
int startRadius, endRadius;
if (i % 5 == 0) {
// Для часовых меток: от отступа 15 до отступа 25
startRadius = radius - 15;
endRadius = radius - 25;
} else {
// Для минутных меток: от отступа 5 до отступа 12
startRadius = radius - 5;
endRadius = radius - 12;
}
// Вычисляем координаты
int x1 = centerX + startRadius * sin(angle);
int y1 = centerY - startRadius * cos(angle);
int x2 = centerX + endRadius * sin(angle);
int y2 = centerY - endRadius * cos(angle);
// Перерисовываем метку белым цветом
tft->drawLine(x1, y1, x2, y2, GC9A01A_WHITE);
}
}
// Восстановление цифр
void restoreNumbers() {
// Настраиваем цвет и размер
tft->setTextColor(GC9A01A_WHITE);
tft->setTextSize(2);
// Перерисовываем цифры в тех же местах
tft->setCursor(centerX - 12, 37);
tft->print("12");
tft->setCursor(centerX + radius - 45, centerY - 8);
tft->print("3");
tft->setCursor(centerX - 6, centerY + radius - 45);
tft->print("6");
tft->setCursor(centerX - radius + 35, centerY - 8);
tft->print("9");
}
// Отрисока стрелок часов и минут
void drawHand(int oldValue, int newValue, int oldExtra, int newExtra,
int length, uint16_t color, bool isHour) {
// Стирание старой стрелки
if (oldValue != -1) {
float oldAngle;
// Вычисляем угол старой стрелки
if (isHour) {
// Часовая: 30 градусов на час + 0.5 градуса на минуту
oldAngle = (oldValue * 30 + oldExtra * 0.5) * PI / 180;
} else {
// Минутная: 6 градусов на минуту
oldAngle = (oldValue * 6) * PI / 180;
}
// Координаты конца старой стрелки
int oldX = centerX + length * sin(oldAngle);
int oldY = centerY - length * cos(oldAngle);
// Рисуем чёрную линию поверх старой стрелки (стираем)
tft->drawLine(centerX, centerY, oldX, oldY, GC9A01A_BLACK);
// Восстанавливаем центральную точку (была закрыта стрелкой)
tft->fillCircle(centerX, centerY, 5, GC9A01A_BLACK);
tft->fillCircle(centerX, centerY, 5, GC9A01A_WHITE);
// Для минутной стрелки восстанавливаем метки и цифры
if (!isHour) {
restoreMarks(); // Восстанавливаем все метки
restoreNumbers(); // Восстанавливаем все цифры
}
}
// Рисование новой стрелки
float newAngle;
if (isHour) {
// Угол для часовой стрелки с учётом минут
newAngle = (newValue * 30 + newExtra * 0.5) * PI / 180;
} else {
// Угол для минутной стрелки
newAngle = (newValue * 6) * PI / 180;
}
// Координаты конца новой стрелки
int newX = centerX + length * sin(newAngle);
int newY = centerY - length * cos(newAngle);
// Рисуем новую стрелку указанным цветом
tft->drawLine(centerX, centerY, newX, newY, color);
// Восстанавливаем центральную точку (поверх стрелки)
tft->fillCircle(centerX, centerY, 5, GC9A01A_BLACK);
tft->fillCircle(centerX, centerY, 5, GC9A01A_WHITE);
}
// Отрисовка секундной стрелки (красной точки)
void drawSecond(int oldValue, int newValue, int length, uint16_t color) {
// СТИРАНИЕ СТАРОЙ ТОЧКИ
if (oldValue != -1) {
// Угол старой позиции (6 градусов на секунду)
float oldAngle = (oldValue * 6) * PI / 180;
int oldX = centerX + length * sin(oldAngle);
int oldY = centerY - length * cos(oldAngle);
// Рисуем чёрный кружок БОЛЬШЕГО радиуса (4 пикселя)
// чтобы гарантированно стереть старую точку
tft->fillCircle(oldX, oldY, 4, GC9A01A_BLACK);
}
// Отрисовка новой точки
float newAngle = (newValue * 6) * PI / 180;
int newX = centerX + length * sin(newAngle);
int newY = centerY - length * cos(newAngle);
// Рисуем красную точку радиусом 3 пикселя
tft->fillCircle(newX, newY, 3, color);
// Восстанавливаем внешнюю рамку циферблата (могла быть повреждена)
tft->drawCircle(centerX, centerY, radius, GC9A01A_WHITE);
tft->drawCircle(centerX, centerY, radius - 1, GC9A01A_WHITE);
}
void setup() {
// Запускаем последовательный порт для отладки
Serial.begin(115200);
// Создаём и инициализируем SPI
spi = new SPIClass(VSPI);
spi->begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS);
// Создаём объект дисплея с использованием аппаратного SPI
tft = new Adafruit_GC9A01A(spi, TFT_DC, TFT_RST, TFT_CS);
// Запускаем дисплей
tft->begin();
tft->setRotation(2); // Поворачиваем на 180 градусов (зависит от монтажа)
tft->fillScreen(GC9A01A_BLACK); // Заливаем экран чёрным
Serial.println("Дисплей инициализирован");
// Настраиваем цвет текста (синий) и фон (чёрный)
tft->setTextColor(GC9A01A_BLUE, GC9A01A_BLACK);
// Устанавливаем размер шрифта (3 - крупный)
tft->setTextSize(3);
// Подобранное значение для центрирования (для слова "SPRYTRON.RU")
int textX = 25;
// Позиция по Y: центр экрана минус половина высоты текста
int textY = 105;
// Устанавливаем курсор
tft->setCursor(textX, textY);
// Выводим текст
tft->print("SPRYTRON.RU");
// Подключение к Wi-Fi
Serial.print("Подключение к Wi-Fi");
// Запускаем подключение к указанной сети
WiFi.begin(ssid, password);
// Ждём подключения (проверяем статус)
while (WiFi.status() != WL_CONNECTED) {
delay(500); // Ждём полсекунды
Serial.print("."); // Печатаем точки для индикации процесса
}
Serial.println("\nWi-Fi подключен!");
// Передаём NTP-серверу настройки смещения
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
// Ждём, пока время не будет получено
struct tm timeinfo;
while (!getLocalTime(&timeinfo)) {
Serial.println("Ждём NTP...");
delay(1000); // Ждём 1 секунду перед следующей попыткой
}
Serial.println("Время получено!");
// Отрисовка циферблата
drawDial();
}
void loop() {
// Получаем текущее локальное время
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
// Если не удалось получить время - выводим ошибку и выходим
Serial.println("Не удалось получить время");
return;
}
// Извлекаем часы, минуты, секунды
int hours = timeinfo.tm_hour;
int minutes = timeinfo.tm_min;
int seconds = timeinfo.tm_sec;
// Преобразуем в 12-часовой формат (для циферблата)
int hours12 = hours % 12;
// Определяем, изменились ли значения (для оптимизации перерисовки)
bool hourChanged = (hours12 != lastHour);
bool minuteChanged = (minutes != lastMinute);
bool secondChanged = (seconds != lastSecond);
// Часовая стрелка
// Вычисляем дополнительное смещение для часов (учёт минут)
int currentExtra = minutes;
// Если час изменился ИЛИ изменились минуты (смещение часов)
if (hourChanged || (currentExtra != lastExtra)) {
// Рисуем часовую стрелку: длина 70, оранжевый цвет
drawHand(lastHour, hours12, lastExtra, currentExtra, 70, GC9A01A_ORANGE, true);
// Сохраняем текущие значения
lastHour = hours12;
lastExtra = currentExtra;
}
// Минутная стрелка
if (minuteChanged) {
// Рисуем минутную стрелку: длина 100, голубой цвет
drawHand(lastMinute, minutes, 0, 0, 100, GC9A01A_CYAN, false);
// Сохраняем текущее значение
lastMinute = minutes;
}
// Секундная стрелка (точка)
if (secondChanged) {
// Рисуем красную точку на расстоянии 105 пикселей от центра
drawSecond(lastSecond, seconds, 105, GC9A01A_RED);
// Сохраняем текущее значение
lastSecond = seconds;
}
// Небольшая задержка для стабильности и снижения нагрузки
delay(50);
}
Заключение
В данной статье мы рассмотрели как подключить TFT-дисплей GC9A01 к ESP32 и разработали простые стрелочные Wi-Fi часы с синхронизацией времени через NTP.
Работать с дисплеем можно с использованием функциональных модулей tft_eSPI и Adafruit_GC9A01A_Library. Они позволяют эффективно управлять индикатором, рисовать циферблат, стрелки и метки. Вы можете легко адаптировать приведённый код под свои проекты: добавить дату, температуру, или отображать уровень заряда аккумулятора в виде сегментированной шкалы по окружности. Круглый дисплей является отличным решением для DIY-устройств с нестандартным форм-фактором.






