ST7735 — цветной SPI-экран (128×160)
Цветной TFT-экран на чипе ST7735 через SPI: вывод текста, геометрических
примитивов и простой анимации. В отличие от датчиков этого репозитория,
здесь нет tools/*.py — сам экран уже и есть визуализация.
TFT_display — цветной SPI-экран ST7735 (128×160)
Компонент управляет цветным TFT-экраном на чипе ST7735 через SPI: вывод
текста, геометрических примитивов и простой анимации. В отличие от
компонентов-датчиков этого репозитория, здесь нет tools/*.py — сам экран
уже и есть визуализация, дополнительный слой на компьютере не нужен.
Драйвер построен поверх готовых библиотек Adafruit_GFX/Adafruit_ST7735
(это полноценный, проверенный драйвер экрана — как и RF24 для радио, он не
переписывается заново). Класс TFTDisplay даёт свой прикладной API поверх
Adafruit и не пробрасывает оригинальный объект наружу — подробнее о том,
почему это важно, см. THEORY.md.
Файлы
- include/tft_display.h / src/tft_display.cpp — драйвер.
- src/main.cpp — тестовый стенд: 4 демонстрации (текст, геометрия, анимация, динамические данные).
- THEORY.md — физика TFT-матрицы, протокол SPI+DC, формат цвета RGB565.
- WIRING.md — схема подключения для всех поддерживаемых плат.
- Lesson.md — методический план урока для учителя.
Подключение (кратко, esp32dev)
| ST7735 | ESP32 (esp32dev) |
|---|---|
| VCC | 3.3V |
| GND | GND |
| CS | GPIO5 |
| RST | не подключается (программный ресет) |
| DC | GPIO2 |
| SCK | GPIO18 (аппаратный SPI) |
| MOSI | GPIO23 (аппаратный SPI) |
Пины автоматически переключаются под плату через #if defined(...) в
tft_display.h:13-33 — полная таблица для
esp32c6/esp32-s3/esp8266 и объяснение каждого контакта — в WIRING.md.
Быстрый старт
- Собрать схему по WIRING.md.
- Прошить:
pio run -e esp32dev -t upload. - При старте экран покажет заставку, затем зациклит 4 демонстрации: уведомления (текст), сетка линий, бегущий круг, счётчик времени работы.
API драйвера
| Метод | Назначение |
|---|---|
begin(rotation) | Инициализация чипа ST7735 и поворот экрана. |
clear(color) | Заливка всего экрана цветом. |
showMessage(title, subtitle, titleColor) | Заголовок + подзаголовок в фиксированной раскладке. |
drawBorder(color) | Рамка по периметру экрана. |
drawLine(x0, y0, x1, y1, color) | Линия между двумя точками. |
fillCircle(x, y, radius, color) | Закрашенный круг. |
printAt(x, y, text, color, textSize) | Текст в произвольной позиции произвольным размером. |
width() / height() | Размер экрана в пикселях с учётом поворота. |
Палитра цветов (COLOR_BLACK, COLOR_RED, …) — константы класса,
tft_display.h:42-49, формат RGB565 —
см. THEORY.md.
Известные ограничения
- Плата
esp32c6в этом репозитории падает на известном баге фреймворка Arduino (конфликт макросаSerial/USBSerial) — не регрессия этого компонента, см.COMPONENT_STANDARD.md. Используйтеesp32devилиesp32-s3. printAt()принимает толькоconst char*— для вывода чисел форматируйте строку заранее черезsnprintf(пример — main.cpp, тест 4).
Теория: как экран рисует картинку
1. Матрица пикселей: тысячи маленьких лампочек
TFT (Thin-Film Transistor) экран — это сетка из крошечных ячеек-пикселей, каждая со своим тонкоплёночным транзистором, который управляет тем, сколько света пропускает эта конкретная точка и какого цвета. Экран этого компонента (чип ST7735, часто продаётся с меткой “BLACKTAB” — так называется одна из заводских настроек, tft_display.cpp:8) имеет разрешение 128×160 пикселей — это 20480 отдельных управляемых точек.
Рисовать “линию” или “круг” на таком экране на самом деле означает: вычислить,
какие именно пиксели должны загореться нужным цветом, и передать эти
координаты чипу. Библиотека Adafruit_GFX берёт эту математику на себя
(алгоритмы вроде Брезенхэма для линий/окружностей) — наш класс TFTDisplay
просто передаёт ей “откуда, докуда, каким цветом”
(tft_display.cpp:40-46).
2. SPI + лишний провод DC: как чип понимает, команда это или данные
Экран подключается по SPI — тому же протоколу, что часто используют карты памяти и некоторые датчики. Но у TFT-экранов есть контакт, которого нет у большинства других SPI-устройств: DC (Data/Command).
По SPI идёт просто поток байт — сам по себе он не говорит чипу, значит ли
очередной байт “нарисуй точку здесь” (команда) или “вот значение цвета”
(данные). Пин DC решает эту неоднозначность: пока он в одном логическом
состоянии, чип трактует входящие байты как команды, пока в другом — как
данные для уже выбранной команды. Переключение DC туда-сюда делает
Adafruit_ST7735 автоматически на каждом вызове — этой логики нет и не
должно быть в нашем классе.
CS (Chip Select) — стандартный SPI-пин, который “выбирает” именно этот
чип на шине (на случай, если на одной SPI-линии несколько устройств). RST
(Reset) физически перезагружает чип; в конфигурации этого компонента он
почти везде равен -1 (tft_display.h:13-33) —
это означает “используем программный сброс вместо отдельного провода”,
экран умеет сбрасываться командой по SPI, а не только по физическому пину.
3. Формат цвета RGB565
У этого экрана нет отдельных проводов на “красный”, “зелёный”, “синий” — цвет каждого пикселя передаётся как одно 16-битное число. Формат называется RGB565, потому что 16 бит разбиты неравномерно:
биты: R R R R R | G G G G G G | B B B B B
(5 бит) (6 бит) (5 бит)Зелёному отведён лишний бит (6 вместо 5) — человеческий глаз различает больше оттенков зелёного, чем красного или синего, поэтому такое распределение даёт визуально более точный цвет при том же объёме данных.
Константы палитры класса (tft_display.h:42-49)
— это уже готовые 16-битные числа в этом формате, например
COLOR_RED = 0xF800 — это 11111 000000 00000 в двоичном виде: все 5 бит
красного включены, зелёный и синий — нули.
4. Почему нельзя просто “перерисовать один пиксель”
Кажется естественным при обновлении текста стирать только старые пиксели, но
на практике проще и быстрее залить область целиком новым фоном и нарисовать
заново — именно так устроен showMessage()
(tft_display.cpp:19-33): каждый вызов сначала
чистит весь экран (clear(COLOR_BLACK)), а потом рисует текст поверх.
Частичное обновление (только изменившихся пикселей) возможно, но требует
дополнительной логики отслеживания “что было нарисовано раньше” — этот
компонент, ориентированный на понятность для школьников, такой сложности
намеренно избегает.
5. Почему у класса нет прямого доступа к Adafruit
Adafruit_ST7735 — это уже полноценный, хорошо протестированный драйвер
самого чипа: он умеет говорить с ним по SPI, переключать DC, знает все
команды инициализации конкретной модели матрицы. Класс TFTDisplay не
переписывает эту работу — он использует Adafruit_ST7735 как приватное поле
(tft_display.h:81) и добавляет поверх него свой,
более простой прикладной API: showMessage/drawLine/fillCircle/printAt.
Важно, что этот API не пробрасывает оригинальный объект Adafruit наружу
(в отличие от более раннего варианта этого компонента, где был метод
getDriver()) — весь доступ идёт через методы TFTDisplay. Это тот же
принцип, что у NrfLink в компоненте nRF24l01: библиотека-транспорт остаётся
внутри класса, наружу выходит только осмысленный прикладной интерфейс.
Подключение TFT-экрана ST7735
Основная схема (esp32dev)
ESP32 ST7735
┌───────────┐ ┌─────────────┐
│ 3V3 ├─────────────┤ VCC │
│ GND ├─────────────┤ GND │
│ GPIO5 ├─────────────┤ CS │
│ GPIO2 ├─────────────┤ DC │
│ GPIO18 ├─────────────┤ SCK │
│ GPIO23 ├─────────────┤ MOSI (SDA) │
│ │ │ RST │ ← не подключается, см. ниже
└───────────┘ └─────────────┘Таблица пинов (esp32dev)
| ST7735 | ESP32 (esp32dev) | Назначение |
|---|---|---|
| VCC | 3.3V | Питание |
| GND | GND | Общий провод |
| CS | GPIO5 | SPI Chip Select |
| DC | GPIO2 | Data/Command — см. THEORY.md |
| SCK | GPIO18 | SPI Clock (аппаратный) |
| MOSI | GPIO23 | SPI Master Out / Slave In (аппаратный) |
| RST | не подключён | Программный сброс, см. ниже |
| MISO | не используется | ST7735 в этой библиотеке только принимает данные |
Пины TFT_CS/TFT_DC/TFT_RST определяются автоматически в зависимости от
целевой платы через #if defined(...) в
tft_display.h:13-33 — их не нужно менять
руками при смене платы в platformio.ini.
Почему RST не подключён
Многие модули ST7735 не подключают контакт RST к отдельному пину, а
задают TFT_RST = -1 — это указывает библиотеке Adafruit_ST7735
использовать программный сброс (команда по самой SPI-линии) вместо
физического импульса на отдельном проводе. Экран инициализируется чуть
дольше, но это экономит один GPIO — разумный компромисс для проектов, где
пинов не хватает.
Пины для других плат
| Плата | Окружение PlatformIO | CS | DC | RST |
|---|---|---|---|---|
| ESP32 DevKit (esp32dev) | esp32dev | GPIO5 | GPIO2 | −1 (программный) |
| ESP32-C6-DevKitC-1 | esp32c6 | GPIO18 | GPIO19 | −1 (программный) |
| ESP32-S3 (LoRaWAN-хаб Ebyte EoRa-HUB-900TB) | eora_hub_s3 | GPIO10 | GPIO9 | −1 (программный) |
| ESP8266 NodeMCU | esp8266 | GPIO15 (D8) | GPIO2 (D4) | −1 (программный) |
Во всех случаях SCK/MOSI используют стандартный аппаратный SPI платы — отдельно их задавать не нужно.
Типичные проблемы
| Симптом | Причина | Решение |
|---|---|---|
Экран остаётся белым/пустым после begin() | Перепутаны CS/DC | Сверить с таблицей выше для своей платы |
| Цвета инвертированы или “не те” | Неверный вариант инициализации панели | В tft_display.cpp:8 уже стоит INITR_BLACKTAB — стандарт для большинства модулей; если цвета всё равно неверны, попробуйте INITR_GREENTAB |
| Изображение обрезано / смещено | Неверный поворот | Display.begin(rotation) — попробуйте значения 0-3 |
| Плата esp32c6 не собирается | Известный баг фреймворка (Serial/USBSerial), не связан с экраном | См. COMPONENT_STANDARD.md, использовать esp32dev/esp32-s3 |
Методический план урока: из чисел — в картинку (TFT-экран ST7735)
Паспорт урока
| Параметр | Значение |
|---|---|
| Место в курсе | Components, урок 6 — завершает блок исполнительных устройств |
| Длительность | 45 мин |
| Возраст | 13–17, материал двухуровневый |
| Оборудование | ESP32, экран ST7735 128×160, 7 проводов — на пару; проектор для разбора кода |
| Предварительные знания | setup/loop, работа с библиотекой, понятие шины (SPI или I2C) |
| Предмет | Информатика (кодирование данных, двоичные числа), физика (свет и цвет) |
Образовательные цели
По итогам урока ученик умеет:
| № | Цель | Блум | Миллер | Чем доказывается |
|---|---|---|---|---|
| 6.1 | Объяснить, как из чисел получается изображение | понимать | знает как | Ответ через сетку пикселей |
| 6.2 | Собрать цвет в формате RGB565 и проверить результат | применять | делает | Свой цвет на экране совпал с задуманным |
| 6.3 | Объяснить назначение провода DC | понимать | знает как | Ответ через «команда или данные» |
| 6.4 | Вывести на экран текст, фигуры и простую анимацию | применять | делает | Работающая программа своей задумки |
| 6.5 | Объяснить, почему анимация требует стирания предыдущего кадра | анализировать | показывает как | Предсказывает «хвост» до запуска |
Чего на этом уроке делать НЕ надо. Не разбирать протокол SPI побайтово и не углубляться в устройство библиотеки Adafruit. Ученик должен понять связь «число → пиксель → картинка» и научиться рисовать. Тайминги шины — материал курса схемотехники.
Что должно быть готово заранее
- Экран подключён и проверен лично: прошивка main.cpp выводит все четыре демонстрации
- Проверена распиновка по WIRING.md — у ST7735 семь проводов, и перепутать легко
- На проекторе открыты tft_display.h (таблица цветов) и main.cpp
- Распечатана таблица RGB565 или выведена на доску
- Лупа или увеличенное фото экрана — для блока 1
Полезный реквизит — лупа. Через неё на экране видны отдельные субпиксели: красный, зелёный, синий. Абстракция «картинка состоит из точек» превращается в наблюдаемый факт за десять секунд, и это сильнее любого слайда.
Сценарий занятия (45 минут)
| Время | Блок | Что делает педагог | Что делают ученики |
|---|---|---|---|
| 0–6 | Завязка | Даёт лупу, задаёт вопрос про миллионы картинок | Смотрят, находят точки |
| 6–14 | Пиксель и цвет | Разбирает RGB565 на доске | Собирают свой цвет на бумаге |
| 14–20 | Провод DC | Ведёт к идее «команда или данные» | Формулируют гипотезу |
| 20–34 | Практика | Запускает четыре демонстрации | Прошивают, меняют, наблюдают |
| 34–40 | Анимация и хвост | Убирает стирание кадра | Предсказывают и проверяют |
| 40–45 | Итог и ДЗ | Фиксирует связку число → пиксель | Записывают в журнал |
Ключевой методический принцип — сначала на бумаге, потом на экране. Цвет собирается в тетради как двоичное число, и только затем проверяется на железе. Если начать с кода, ученик будет подбирать цвета наугад и не поймёт, откуда берутся шестнадцатеричные константы.
Блок 1. Завязка (0–6 мин)
Экран показывает картинку. Раздать лупы (или показать увеличенное фото).
«Телевизор показывает миллионы разных изображений. Внутри у него только электроника, которая умеет включать и выключать точки. Как из „включено — выключено“ получается фильм?»
Дать посмотреть в лупу. Видны отдельные светящиеся точки, у каждой три цвета.
«Вот они. У нашего экрана таких точек 128 в ширину и 160 в высоту. Сколько всего?»
Считают: 20 480. Число достаточно большое, чтобы впечатлить, и достаточно маленькое, чтобы уложиться в голове.
Что обычно происходит
| Реакция | Что делать |
|---|---|
| «Там внутри картинка хранится» | «А если я покажу то, чего никто не рисовал заранее?» |
| «Это как мозаика» | Хорошая аналогия, закрепить: каждый камешек своего цвета |
| Пытаются сосчитать точки в лупе | Поощрить: перейти к умножению 128 × 160 |
Блок 2. Пиксель и цвет (6–14 мин)
2.1. Базовый уровень (13–14 лет)
«Каждая точка светится своим цветом. Цвет получается смешением трёх ламп: красной, зелёной и синей. Меняя их яркость, получаем любой оттенок.»
Проверить на лупе: белый участок экрана — горят все три, чёрный — не горит ни одна.
Задача: какой цвет получится, если зажечь красную и зелёную, а синюю погасить?
Обычно отвечают «коричневый» или «оранжевый». Правильный ответ — жёлтый, и это
неожиданно. Проверяем на экране константой COLOR_YELLOW.
2.2. Углублённый уровень (15–17 лет)
Ввести формат хранения:
«Цвет — это число. У нашего экрана на цвет отводится 16 бит: пять на красный, шесть на зелёный, пять на синий. Формат так и называется — RGB565.»
| Канал | Бит | Диапазон |
|---|---|---|
| Красный | 5 | 0…31 |
| Зелёный | 6 | 0…63 |
| Синий | 5 | 0…31 |
Главный вопрос блока:
«Почему зелёному досталось на бит больше? Он что, важнее?»
Ответ: да, для глаза важнее. Человеческий глаз различает оттенки зелёного тоньше, чем красного и синего — эволюционное наследие. Лишний бит потрачен там, где его заметят.
Разбор на доске:
цвет=(R≪11)∣(G≪5)∣BПроверяем на известной константе COLOR_RED = 0xF800:
0xF800 = 1111 1 000000 00000
R=31 G=0 B=0 → чистый красный ✓Задание в тетради (обязательное, 3 минуты): собрать фиолетовый — много красного, немного синего, зелёного ноль. Например, R = 31, B = 15, G = 0.
(31≪11)∣(0≪5)∣15=0xF80FПроверить на экране. Совпало — формат понят.
Блок 3. Провод DC (14–20 мин)
Показать WIRING.md: у экрана семь проводов, среди них DC.
«У датчиков на I2C проводов данных два. Тут — отдельный провод с именем DC. Зачем ему лишний?»
Аналогия, которая снимает вопрос:
Представь, что тебе по телефону диктуют цифры без пауз и пояснений: «пятьсотдвадцатьтринадцатьсемь». Где номер телефона, где сумма, где просьба что-то сделать? Непонятно.
DC — это голос, который перед каждой порцией говорит: «дальше команда» или «дальше данные». Одна и та же восьмёрка байтов будет понята по-разному в зависимости от того, что сказал DC.
Разбор — THEORY.md, раздел 2.
Инженерный вывод, который стоит записать. Данные сами по себе не несут смысла — смысл задаёт контекст. В протоколах этот контекст передают либо отдельной линией (как DC), либо специальными байтами-маркерами внутри потока. Та же идея встретится в разборе протокола NMEA у GPS и в CRSF у пульта БПЛА.
Блок 4. Практика: четыре демонстрации (20–34 мин)
Прошить main.cpp и разобрать по очереди.
| № | Что на экране | Что обсудить |
|---|---|---|
| 1 | Текст — showMessage() | Почему экран очищается целиком, а не стирается старый текст? |
| 2 | Сетка линий — drawLine() в цикле | Как меняется угол каждой линии, почему они выходят из одной точки |
| 3 | Бегущий круг — fillCircle() дважды | Второй вызов рисует чёрным — стирает предыдущее положение |
| 4 | Динамический текст — millis() / 1000 | Почему число нельзя передать напрямую и нужен snprintf |
Задания по уровням
| Уровень | Задание | Критерий |
|---|---|---|
| Базовый | Изменить цвета и текст сообщения на свои | На экране своя надпись своим цветом |
| Базовый+ | Собрать в тетради ещё два цвета (оранжевый, бирюзовый) и проверить | Совпало с задуманным |
| Средний | Нарисовать простую фигуру из линий — домик, звезду | Фигура узнаваема |
| Средний+ | Сделать «индикатор загрузки»: полоса растёт слева направо | Полоса растёт плавно, без мигания всего экрана |
| Продвинутый | Анимация с двумя объектами, движущимися с разной скоростью | Оба движутся, хвостов нет |
| Продвинутый+ | Вывести показания любого датчика из раздела Sensors на экран | Числа обновляются в реальном времени |
Блок 5. Анимация и «хвост» (34–40 мин)
Главный эксперимент урока. Сначала предсказание, потом запуск.
«В демонстрации 3 круг бежит по экрану. В коде два вызова
fillCircle: первый рисует круг цветом, второй — чёрным по старому месту. Что будет, если убрать второй?»
Дать записать прогноз, потом закомментировать строку и прошить.
На экране остаётся след из кругов — «хвост». Ничего не стёрлось, потому что стирать никто не просил.
Вывод, который стоит проговорить. Экран не «показывает объект» — он хранит то, что на нём нарисовали, пока не нарисуют поверх. Анимация — это не движение фигуры, а стирание и перерисовка много раз в секунду. Ровно так работает любая компьютерная графика, от игр до интерфейса телефона.
Для продвинутых — вопрос на подумать:
«Стирать весь экран перед каждым кадром — просто, но медленно: 20 480 точек. Как сделать быстрее?»
Ответ: стирать только то, что изменилось — прямоугольник вокруг старого положения. Этот приём называется частичной перерисовкой, и на нём держится плавность интерфейсов. Разбор — THEORY.md, раздел 4.
Ответы на вопросы, которые прозвучат
«Зачем свой класс TFTDisplay, если библиотека Adafruit уже всё умеет?»
Библиотека универсальная, с большим API — в нём легко утонуть. TFTDisplay даёт
узкий набор методов под учебные задачи: проще объяснить, проще пользоваться.
В реальных проектах так делают постоянно — прячут сложное за понятным.
«Почему у экрана нет Python-визуализатора, как у датчиков?» Потому что экран сам и есть визуализация. У датчика числа иначе не увидеть, а здесь результат виден сразу.
«Что будет, если нарисовать круг радиусом больше экрана?» Библиотека обрежет по границам, программа не упадёт. Стоит проверить экспериментально и обсудить, почему это удобнее аварийного завершения.
«Можно вывести фотографию?» Можно, но она занимает 20 480 × 2 байта ≈ 40 КБ — это много для памяти ESP32. Поэтому картинки либо хранят на SD-карте, либо сжимают, либо рисуют примитивами. Хороший повод обсудить, почему изображения вообще научились сжимать.
«Экран цветной, а сколько всего цветов?» 216=65536. Звучит много, но настоящая фотография использует 224≈16 миллионов — на градиентах разница заметна полосами. Это цена экономии памяти.
Итог урока (30 секунд)
- Картинка — это сетка точек, у каждой свой цвет-число.
- Цвет в RGB565 — 16 бит, и зелёному дан лишний бит, потому что глаз к нему чувствительнее.
- Анимация — это стирание и перерисовка, а не движение фигуры.
Проверка понимания (экспресс, 2 минуты)
| Вопрос | Ожидаемый ответ |
|---|---|
| Сколько точек на экране 128×160? | 20 480 |
| Почему зелёному 6 бит, а не 5? | Глаз различает зелёные оттенки тоньше |
| Зачем провод DC? | Различает команду и данные в общем потоке |
| Почему за фигурой остаётся хвост? | Старое изображение не стёрли |
Типичные затруднения
| Затруднение | Причина | Что делать |
|---|---|---|
| Путают порядок проводов | Их семь, названия похожи | Сверять по WIRING.md, подписать маркером |
| Подбирают цвета наугад | Пропустили задание в тетради | Вернуться к формуле и собрать цвет руками |
| Ждут, что фигура «сама» сотрётся | Опыт работы с готовыми интерфейсами | Эксперимент с хвостом из блока 5 |
Передают число в printAt напрямую | Не видят разницы между числом и строкой | Показать snprintf в демонстрации 4 |
| Экран белый или не включается | Питание или подсветка | Проверить 3,3 В и провод подсветки |
Домашнее задание
Обязательная часть: найти дома три устройства с экранами разного качества (телефон, микроволновка, часы) и описать, чем они отличаются: сколько точек, есть ли цвет, видно ли отдельные пиксели.
Базовый уровень: собрать в тетради коды ещё трёх цветов по формуле RGB565 и проверить их на следующем занятии.
Средний: придумать экран статуса для устройства: что выводить, каким цветом, что должно мигать. Нарисовать макет на клетчатой бумаге 128×160.
Продвинутый: прочитать THEORY.md, раздел 4 и письменно объяснить, почему полная перерисовка экрана даёт мерцание, а частичная — нет.
Связь с курсом
| Что дальше | Где |
|---|---|
| Вывод показаний датчика на экран | Датчики — любой модуль |
| Протоколы и разграничение команд и данных | NMEA у GPS, CRSF в БПЛА |
| Двоичное представление данных | Nand2Cpu, модуль 2 |
| Адресные светодиоды: тоже цвет числом | LED_adress |
Этот урок закрывает блок исполнительных устройств: плата умеет светить, звучать и показывать. Дальше — датчики, и связка «измерил → обработал → показал» становится основой любого проекта.
Чек-лист педагога перед уроком
- Экран подключён и проверен, все четыре демонстрации работают
- Лупа или увеличенное фото экрана готовы
- Таблица RGB565 на доске или распечатана
- Код открыт на проекторе:
tft_display.hс константами цветов иmain.cpp - Заготовлен вариант с закомментированным стиранием — для блока 5
- Провода подписаны или разложены по порядку: семь штук легко перепутать