Подключение круглого TFT-дисплея GC9A01 к ESP32

          За последние 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. Ниже представлена схема подключения ЖК-индикатора к отладочной плате, а также фотография собранного макета:

Схема подключения GC9A01 к ESP32

Программный код (скетч)

          Стрелочные часы с круглым циферблатом – самый наглядный пример, который демонстрирует наш индикатор во всей красе. Рассмотрим написание проекта с применением специализированного функционального пакета от 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), который применяется для синхронизации времени устройств.

Читайте также:  Акселерометр и гироскоп MPU-6050: цифровой угломер на ESP32

          Если слишком долго не удаётся получить данные по 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.

Читайте также:  Магнитометр HMC5883L (QMC5883L): компас на ESP32

          Работать с дисплеем можно с использованием функциональных модулей tft_eSPI и Adafruit_GC9A01A_Library. Они позволяют эффективно управлять индикатором, рисовать циферблат, стрелки и метки. Вы можете легко адаптировать приведённый код под свои проекты: добавить дату, температуру, или отображать уровень заряда аккумулятора в виде сегментированной шкалы по окружности. Круглый дисплей является отличным решением для DIY-устройств с нестандартным форм-фактором.