Підключення GC9A01 до ESP32-S3 — робоче налаштування TFT_eSPI та годинник по Wi-Fi

Час збірки: 30 хв.

Складність: Низька

Компоненти (BOM)

  • Display GC9A01
  • ESP32 S3 WROOM 30pin
  • DuPont

Підключення дисплея GC9A01 до ESP32-S3 — робоче налаштування TFT_eSPI та годинник по Wi-Fi

Використовуємо:

Arduino IDE та бібліотека TFT_eSPI

 Круглі TFT дисплеї GC9A01 з роздільною здатністю 240×240 — популярне рішення для годинників, міні-панелей, погодних станцій та інтерфейсів на ESP32. Найчастіше такі дисплеї продаються як “1.28 IPS GC9A01 SPI display” і чудово підходять для проєктів з Wi-Fi та анімацією.

 У моєму випадку спочатку планувалося підключення до ESP32-S2 Mini, оскільки в нього достатньо вільних GPIO. Однак із підключеним дисплеєм плата поводилася нестабільно — були проблеми з прошивкою та завантаженням.

 Після переходу на ESP32-S3 WROOM все запрацювало нормально без додаткових маніпуляцій.

  •  Наприкінці статті — робоча конфігурація TFT_eSPI та готова логіка годинника по Wi-Fi.

Що вийшло в результаті

За підсумками налаштування ви отримаєте:

  • повністю робочий дисплей GC9A01 на ESP32-S3
  • коректну роботу через TFT_eSPI
  • підключення до Wi-Fi
  • автоматичну синхронізацію часу через інтернет
  • перемикання між аналоговим та цифровим годинником
  • готову базу для DIY годинника або smart display проєкту
  • Налаштування TFT_eSPI для GC9A01

Для роботи знадобиться бібліотека TFT_eSPI. Після встановлення бібліотеки відкрийте файл та налаштуйте конфігураційний файл:

C:\Users\Name\Documents\Arduino\libraries\TFT_eSPI\User_Setup.h

У User_Setup.h вкажіть наступні параметри:

#define GC9A01_DRIVER

#define TFT_WIDTH  240
#define TFT_HEIGHT 240

#define TFT_CS     15  // Chip Select
#define TFT_DC     23  // Data/Command
#define TFT_RST    4   // Reset
#define TFT_MOSI   12  // SPI Data
#define TFT_SCLK   14  // SPI Clock

#define LOAD_GLCD
#define LOAD_FONT2
#define LOAD_FONT4
#define LOAD_FONT6
#define LOAD_FONT7
#define LOAD_FONT8
#define LOAD_GFXFF

#define SPI_FREQUENCY  27000000

#define USER_SETUP_ID 931


Відповідність пінів: GC9A01 | ESP32-S3 (продубльовано на зображенні)

Висновок дисплеяПідключення до ESP32‑S3
VCC3.3V
GNDGND
SCL / CLKGPIO 14
SDA / DINGPIO 12
DCGPIO 23
CSGPIO 15
RSTGPIO 4
BL3.3V

Підсвічування BL краще одразу підключити до 3.3V, інакше екран може залишатися повністю чорним навіть при успішній ініціалізації SPI.

Важливий нюанс із ESP32-S2

Під час тестування ESP32-S2 Mini працював нестабільно при підключеному дисплеї.

Симптоми були такі:

  • плата не завжди прошивалася
  • іноді зависало завантаження
  • COM-порт міг зникати після підключення дисплея

Точної причини визначити не вдалося, але після переходу на ESP32-S3 проблема повністю зникла.

Якщо дисплей заважає прошивці:

спробуйте тимчасово вимкнути живлення дисплея

перевірте GPIO boot strap піни

або використовуйте ESP32-S3

Логіка роботи проєкту

Дисплей використовується як Wi-Fi годинник з двома режимами відображення:

  • аналоговий годинник
  • цифровий годинник
  • Час автоматично синхронізується через інтернет за NTP.
  • На GPIO 22 можна підключити кнопку:
  • при подачі LOW рівня відображення перемикається між цифровим та аналоговим інтерфейсом.

Чому відмовився від зображення циферблата

Спочатку була спроба використати готове зображення циферблата у вигляді масиву bitmap, але з’явилися проблеми:

  • сильна пікселізація
  • великий обсяг масиву
  • незручно змінювати дизайн
  • зайва витрата пам’яті

У підсумку виявилося простіше і краще малювати інтерфейс прямо в коді:

  • лінії,
  • стрілки,
  • кола,
  • текст.

Так зображення виглядає охайніше і легше масштабується під різні проєкти.

Перевірка після прошивки

Після завантаження скетчу перевірте:

  • чи з’являється зображення
  • чи працює підсвічування
  • чи підключається Wi-Fi
  • чи оновлюється час
  • чи реагує кнопка на GPIO 22

Можливі проблеми

Чорний екран

Найчастіше:

  • не підключено BL
  • помилка в SPI пінах
  • поганий контакт Dupont

Білий екран

Зазвичай:

  • проблема з ініціалізацією SPI
  • невірне підключення DC або CS
  • Не прошивається ESP32

Можливий конфлікт boot GPIO при підключеному дисплеї.

Артефакти або сміття на екрані

Підсумок

GC9A01 — один із найзручніших круглих SPI дисплеїв для ESP32 проєктів.

Що важливо враховувати:

  • ESP32-S3 працює з ним помітно стабільніше
  • TFT_eSPI повністю вирішує завдання
  • підсвічування BL обов’язкове
  • графіку зручніше малювати кодом, а не bitmap масивами

Головне правило:

Якщо дисплей визначається, але екран порожній — спочатку перевіряйте BL, SPI піни та частоту SPI, а не сам код.

Code for Arduino ADE
#include <wifi.h>
#include <time.h>
#include <tft_espi.h>

TFT_eSPI tft = TFT_eSPI();

// Настройки часов
#define CENTER_X 120
#define CENTER_Y 120
#define RADIUS 100
#define DOT_RADIUS 3
#define TRACK_RADIUS 110

// Пины
#define PIN_BUTTON 22
#define DEBOUNCE_DELAY 200

// Wi-Fi настройки
const char* ssid = "YouWiFi";
const char* password = "YouPASS";
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 3 * 3600;
const int daylightOffset_sec = 0;

// Переменные времени
struct tm timeinfo;
uint8_t last_second = 61;
bool showAnalogClock = true;

// Позиции стрелок
int16_t last_hx, last_hy, last_mx, last_my;

// Прототипы функций
void drawClockFace();
void updateAnalogClock(uint8_t prevSecond);
void drawDotAtSecond(uint8_t sec, uint16_t color);
void displayDigitalTime();

void setup() {
  Serial.begin(115200);
  pinMode(PIN_BUTTON, INPUT_PULLUP);

  tft.init();
  tft.setRotation(2);
  tft.fillScreen(TFT_BLACK);

  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.setTextSize(2);
  tft.drawString("CONNECTING...", CENTER_X, CENTER_Y);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  tft.fillRect(CENTER_X - 80, CENTER_Y - 10, 160, 20, TFT_BLACK);
  tft.drawString("CONNECTED!", CENTER_X, CENTER_Y);
  delay(1000);

  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  while (!getLocalTime(&timeinfo)) {
    Serial.println("Waiting for time...");
    delay(500);
  }

  drawClockFace();
}

void loop() {
  static uint32_t lastUpdate = 0;

  if (digitalRead(PIN_BUTTON) == LOW) {
    delay(DEBOUNCE_DELAY);
    if (digitalRead(PIN_BUTTON) == LOW) {
      showAnalogClock = !showAnalogClock;
      tft.fillScreen(TFT_BLACK);
      if (showAnalogClock) drawClockFace();
      while (digitalRead(PIN_BUTTON) == LOW) delay(10);
    }
  }

  if (millis() - lastUpdate >= 1000) {
    lastUpdate = millis();

    if (getLocalTime(&timeinfo)) {
      if (timeinfo.tm_sec != last_second) {
        uint8_t prev_second = last_second;
        last_second = timeinfo.tm_sec;

        if (showAnalogClock) {
          updateAnalogClock(prev_second);
        } else {
          displayDigitalTime();
        }
      }
    }
  }
}

void drawClockFace() {
  tft.fillScreen(TFT_BLACK);
  tft.drawCircle(CENTER_X, CENTER_Y, RADIUS, TFT_WHITE);

  // Цифры 12, 3, 6, 9
  const char* labels[] = {"12", "3", "6", "9"};
  int angles[] = {270, 0, 90, 180};  // поправлено

  tft.setTextSize(2);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);

  for (int i = 0; i < 4; i++) {
    float angle = angles[i] * PI / 180.0;
    int x = CENTER_X + (RADIUS - 25) * cos(angle);
    int y = CENTER_Y + (RADIUS - 25) * sin(angle);
    tft.drawString(labels[i], x, y);
  }

  // Минутные метки
  for (int i = 0; i < 60; i++) {
    float angle = i * 6 * PI / 180;
    int length = (i % 5 == 0) ? 12 : 6;
    int x1 = CENTER_X + (RADIUS - length) * cos(angle);
    int y1 = CENTER_Y + (RADIUS - length) * sin(angle);
    int x2 = CENTER_X + RADIUS * cos(angle);
    int y2 = CENTER_Y + RADIUS * sin(angle);
    tft.drawLine(x1, y1, x2, y2, (i % 5 == 0) ? TFT_WHITE : TFT_DARKGREY);
  }
}

void updateAnalogClock(uint8_t prevSecond) {
  // Стираем старые стрелки и точку
  tft.drawLine(CENTER_X, CENTER_Y, last_hx, last_hy, TFT_BLACK);
  tft.drawLine(CENTER_X, CENTER_Y, last_mx, last_my, TFT_BLACK);
  drawDotAtSecond(prevSecond, TFT_BLACK);

  // Часовая стрелка
  float hourAngle = (timeinfo.tm_hour % 12 + timeinfo.tm_min / 60.0) * PI / 6;
  last_hx = CENTER_X + RADIUS * 0.5 * sin(hourAngle);
  last_hy = CENTER_Y - RADIUS * 0.5 * cos(hourAngle);

  // Минутная стрелка
  float minAngle = timeinfo.tm_min * PI / 30;
  last_mx = CENTER_X + RADIUS * 0.7 * sin(minAngle);
  last_my = CENTER_Y - RADIUS * 0.7 * cos(minAngle);

  // Рисуем часовую и минутную стрелки
  tft.drawLine(CENTER_X, CENTER_Y, last_hx, last_hy, TFT_WHITE);
  tft.drawLine(CENTER_X, CENTER_Y, last_mx, last_my, TFT_WHITE);

  // Рисуем текущую точку-секунду
  drawDotAtSecond(timeinfo.tm_sec, TFT_RED);
}

void drawDotAtSecond(uint8_t sec, uint16_t color) {
  float angle = sec * PI / 30;
  int x = CENTER_X + TRACK_RADIUS * sin(angle);
  int y = CENTER_Y - TRACK_RADIUS * cos(angle);
  tft.fillCircle(x, y, DOT_RADIUS, color);
}

void displayDigitalTime() {
  static char timeStr[9];
  sprintf(timeStr, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);

  tft.setTextSize(3);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.drawString(timeStr, CENTER_X, CENTER_Y);
}