В многих проектах для esp8266 я использую TFT экран с тачскрином. В зависимости, от проекта интерфейс может быть простым, например, текстовая консоль, выводящая лог работы приложения или просто график изменения входного сигнала. А в некоторых — сложный GUI, с несколькими экранами, графическими кнопками, строками ввода текста и даже виртуальной клавиатурой.
В этой статье хочу поделиться опытом, как можно подключить экран с тачскрином к esp8266 и реализовать графический интерфейс в среде Arduino.
Видео-тизер:
Итак, приступим
Первая часть, аппаратная
Выбор модели экрана
Сейчас на рынке, читай на aliexpress, продается огромное количество моделей TFT экранов — разных размеров, с разными разрешениями, с тачскрином или без.
TFT экраны отличаются способом подключения — есть экраны, которые подключаются по шине SPI, а другие по параллельной шине.
Минимально, для подключения по шине SPI, требуется всего 4 сигнальных вывода (GPIO), а для подключения по параллельной шине — минимум 10 (и это без учета контроллера тачскрина). Подключение по параллельной шине дает большую скорость, т.к. за один такт передается сразу 8 бит информации, а у SPI — только 1 бит.
Однако, у подключения по SPI, есть другое преимущество — меньшее количество задействованных выводов. У esp8266 количество требуемых выводов является решающим фактором: количество свободных GPIO ограничено.
Я бы выделил такие наиболее популярные модули TFT экранов с SPI интерфейсом:
ili9341, 320x240, 2.4", с контроллером тачскрина ссылкаili9225, 176x220, 2.2, без тачскрина ссылкаst7735, 160x128, 1.8", без тачскрина ссылка
Подключение TFT экрана к esp8266.
У большинства SPI экранов похожие аппаратные интерфейсы, и поэтому, они подключаются к процессору по одинаковой схеме, с точностью до небольших вариаций в названиях. В табличке ниже список выводов — и описание что они обозначают:
| Вывод | Альтернативные названия | Подключение к esp8266 | Назначение |
|---|---|---|---|
| SCLK | SCK,SCL | GPIO14 | Тактирование шины SPI |
| SDA | MOSI | GPIO13 | Передача данных SPI от процессора к экрану |
| CS | SS | GPIO5 | Выбор чипа (к шине SPI может быть подключено несколько устройств) |
| A0 | RS,D/C | GPIO15 | Выбор режима передача данных/команд |
| RESET | RST | VCC | Аппаратный сброс |
| SDO | MISO | - | Передача данных от экрана к процессору (опционально) |
| LED+ | LED | VCC | Включение подсветки |
| VSS | VCC | VCC | Питание экрана, +3.3V |
| GND | GND | Земля |
Выводы RESET и LED+ можно подключить к шине питания +3.3 вольта, однако, при желании их можно подключить к GPIO esp8266, и получить возможность программного управления сбросом и подсветкой экрана.
Выводы CS и A0 можно подключить к любым удобным GPIO esp8266.
Самая важная часть — выводы SCLK и SDA. Они отвечают за передачу данных по шине SPI. Их нужно подключить к соответствующим выводам контроллера SPI esp8266. Это GPIO14 и GPIO13 соответственно.
Если у экрана есть тачскрин контроллер, его так же следует подключить к шине SPI:
| Вывод | Подключение к esp8266 | Назначение |
|---|---|---|
| T_CLK | GPIO14 | Тактирование шины SPI |
| T_DIN | GPIO13 | Передача данных SPI от процессора к контроллеру тачскрина |
| T_CS | GPIO16 | Выбор чипа (к шине SPI может быть подключено несколько устройств) |
| T_DO | GPIO12 | Передача данных от контроллера к процессору |
| T_IRQ | GPIO4 | Признак нажатия на тачскрин |
Обратите внимание, выводы esp8266 GPIO14 и GPIO13 подключены параллельно к экрану и контроллеру тачскрина. Дело в том, что к шине SPI можно подключить несколько устройств. Выбор устройства происходит установкой уровня логического 0 на выводе CS требуемого устройства.
Схема подключения экрана ili9341 к esp8266

Часть вторая, программная
Мы определились с экраном и схемой подключения, теперь пора перейти к программной части. В первую очередь — выберем графическую библиотеку, с которой мы будем реализовывать GUI.
Попробовав несколько десятков библиотек, я остановил свой выбор на библиотеке uGFX. На мой взгляд, это одна из лучших графических библиотек для микроконтроллеров. Богатая функциональность сочетается с модульностью и в проект включаются только требуемые компоненты. Библиотека open source и бесплатна для не коммерческого использования. У библиотеки есть качественная документация, доступная на сайте проекта.
Большим плюсом библиотеки uGFX является развитый движок рендеринга шрифтов, с поддержкой utf8. В комплект входит программа генерации шрифтов из ttf файлов, в том числе и с кириллицей.
Библиотека кросс-платформенна — это означает, что GUI часть приложения можно собрать под любой процессор, в том числе и esp8266.
Драйвера экранов и тачскринов подключаются выделенными модулями, и в случае, если нужных драйверов нет в комплекте — их можно реализовать самостоятельно.
Кроме этого, в комплект uGFX входит uGFX studio — WYSWIG редактор интерфейса, в котором можно визуально подготовить макеты интерфейсы, а uGFX studio автоматически сгенерирует код и разложит ресурсы. К сожалению, сейчас uGFX studio еще в статусе beta версии, и чтобы получить бетку, нужно написать разработчикам на форуме.
И, финальная вишенка на торт: GUI код приложения, можно собрать под десктоп (Linux/Windows/OSX) и посмотреть прямо на компьютере, как будет выглядеть интерфейс.

Подключаем uGFX к проекту
Библиотека использует свою систему сборки, которая "из коробки" не поддерживается системой сборки Arduino. Но это ограничение можно обойти. В качестве reference использовал эту статью
Текст ниже предполагает, что среда для разработки Arduino с поддержкой esp8266 уже установлена и настроена. Если еще нет, то про установку и настройку среды можно прочитать в этой статье на geektimes
Теперь по шагам, как подключить библиотеку:
- Найти папку Libraries от среды Arduino. В зависимости от платформы, она может располагаться в таких местах:
OSX—/Users/<username>/Documents/Arduino/libraries/Windows—C:\Users\<username>\My Documents\Arduino\LibrariesLinux-/home/<username>/Documents/Arduino/libraries
Склонировать или скопировать uGFX в папку Libraries. Скачать можно отсюда — версию, с уже встроенными кириллическими шрифтами.
Сделать библиотеку "обертку", которая будет содержать реализацию ввода/вывода для драйверов экрана и тачскрина, а так же подключать к сборке нужные нам компоненты uGFX. Для этого, в папке Libraries нужно создать подпапку uGFXesp, с примерно таким содержимым:
uGFXesp ├── library.properties └── src ├── board_ILI9341.cpp ├── board_ILI9341.h ├── gdisp_lld_config.h ├── gdisp_lld_ili9341.c ├── gfxconf.h ├── gfxlib.c ├── gmouse_lld_ADS7843.c ├── gmouse_lld_ADS7843_board.cpp └── gmouse_lld_ADS7843_board.h
- Файл library.properties — это описание библиотеки для среды Arduino:
name=uGFXesp version=1.0.0 author=Oleg V. Gerasimov <ogerasimov@gmail.com> maintainer=Oleg V. Gerasimov <ogerasimov@gmail.com> sentence=UI features of esp paragraph=This library add support screen and touch panel of esp board<br />Requires uGFX library<br /> category=Display architectures=esp8266 includes=gfx.h url=http://github.com
- Файлы
gdisp_lld_ili9341.c,gmouse_lld_ADS7843.c,gdisp_lld_config.h— подключение к сборке драйверов контроллера тачскрина и экрана. - Файл
gfxlib.c— подключение к сборке самой библиотеки - Файл
gfxconf.h— конфигурация, с которой собирается библиотека uGFX — в нем можно включать/отключать требуемую функциональность - Файл board_ILI9341.cpp — реализация ввода/вывода по SPI для драйвера экрана. Остновлюсь на нем подробнее, это самая важная часть интеграции графической библиотеки с esp8266
#include <Arduino.h> #include <SPI.h> extern "C" { #include "user_interface.h" } // Pin, к которому подключен вывод RS экрана #define ESP_LCD_RS 15 // Pin, к которому подключен вывод CS экрана #define ESP_LCD_CS 5 // Скорость работы SPI с экраном в момент инициализации: 1мбит/сек #define SPILOWSPEED 1000000 // Скорость работы SPI с экраном: 32мбит/сек #define SPIHIGHSPEED 32000000 static SPISettings spiSettings(SPILOWSPEED, MSBFIRST, SPI_MODE0); // Включить режим команд экрана static inline void cmdmode() { digitalWrite(ESP_LCD_RS, 0); } // Включить режим данных экрана static inline void datamode() { digitalWrite(ESP_LCD_RS, 1); } // Инициализация драйвера extern "C" void esp_lcd_init_board(void) { SPI.begin(); pinMode(ESP_LCD_CS, OUTPUT); digitalWrite(ESP_LCD_CS, 1); pinMode(ESP_LCD_RS, OUTPUT); datamode(); } // Пост-инициализации драйвера - переводим SPI на нормальную скорость extern "C" void esp_lcd_post_init_board(void) { spiSettings = SPISettings(SPIHIGHSPEED, MSBFIRST, SPI_MODE0); } static int aquire_count = 0; // Захватить шину SPI: устанавливаем 0 на выводе CS и начинаем транзакцию SPI на выбранной скорости extern "C" void esp_lcd_aquirebus(void) { if (!aquire_count++) { SPI.beginTransaction(spiSettings); digitalWrite(ESP_LCD_CS, 0); } } // Отпустить шину SPI: устанавливаем 1 на выводе CS и завершаем транзакцию SPI extern "C" void esp_lcd_releasebus(void) { if (aquire_count && !--aquire_count) { digitalWrite(ESP_LCD_CS, 1); SPI.endTransaction(); } } // Передать команду extern "C" void esp_lcd_write_index(uint16_t cmd) { cmdmode(); SPI.write(cmd); datamode(); } // Передать байт данных extern "C" void esp_lcd_write_data(uint16_t data) { SPI.write(data); }
- Файл gmouse_lld_ADS7843_board.cpp — реализация ввода/вывода по SPI для драйвера экрана. Так же остановлюсь на нем подробнее:
#include <Arduino.h> #include <SPI.h> extern "C" { #include "user_interface.h" } // Pin, к которому подключен вывод TC_IRQ контроллера тачскрина #define ESP_TC_IRQ 4 // Pin, к которому подключен вывод CS контроллера тачскрина #define ESP_TC_CS 16 // Скорость работы SPI с контроллером тачскрина 2Мбит/сек #define SPISPEED 2000000 static SPISettings spiSettings(SPISPEED, MSBFIRST, SPI_MODE0); // Инициализация драйвера extern "C" int esp_gmouse_init_board() { pinMode(ESP_TC_CS, OUTPUT); digitalWrite(ESP_TC_CS, 1); pinMode(ESP_TC_IRQ, INPUT); return 1; } // Проверка состояния вывода TC_IRQ (признак нажатия на тачскрин) extern "C" int esp_getpin_pressed() { // В этом месте мы сбрасываем watch dog, что бы esp8266 не перезагрузился system_soft_wdt_feed (); // Флаг нажатия инверсный return digitalRead (ESP_TC_IRQ)==0; } static int aquire_count = 0; // Захватить шину SPI: устанавливаем 0 на выводе CS и начинаем транзакцию SPI на выбранной скорости extern "C" void esp_aquire_bus() { if (!aquire_count++) { SPI.beginTransaction(spiSettings); digitalWrite(ESP_TC_CS, 0); } } // Отпустить шину SPI: устанавливаем 1 на выводе CS и завершаем транзакцию SPI extern "C" void esp_release_bus() { if (aquire_count && !--aquire_count) { digitalWrite(ESP_TC_CS, 1); SPI.endTransaction(); } } // Считать значение координаты из контроллера extern "C" uint16_t esp_read_value(uint16_t port) { SPI.write (port); return SPI.transfer16(0); }
Собственно, на этом подготовительные работы закончены. Набор файлов исходников есть на гитхабе
Теперь подготовка закончена, приступим к разработке программы или скетча с красивым и удобным GUI.
Разрабатываем скетч с GUI
В прошлой статье я написал про "умный" удлинитель для новодней елочки. GUI у скетча состоит из двух экранов
- заставка с елочкой
- экран с кнопками включения/выключения гирлянд:

Ниже по тексту несколько "снипетов" из кода, как реализовано GUI в этом проекте:
// Окно контейнер GHandle ghContainerMain; // Кнопки GHandle ghButton1,ghButton2,ghButton3,ghButton4,ghButtonAll,ghButtonVoice,ghButtonTree; // Картинки gdispImage ballImg,bearImg,candleImg,microphoneImg,treeImg,bigTreeImg,lightsImg; // Слушатель событий GListener glistener; // Кастомная функция отрисовки кнопки с картинкой и подписью extern "C" void gwinButtonDraw_ImageText(GWidgetObject *gw, void *param);
void guiCreate(void) { gfxInit(); // Создаем "слушателя" события geventListenerInit(&glistener); geventAttachSource(&glistener, ginputGetKeyboard(0), 0); gwinAttachListener(&glistener); // Устанавливаем дефолтные стили GUI gwinSetDefaultFont(gdispOpenFont("DejaVuSans16")); gwinSetDefaultStyle(&WhiteWidgetStyle, FALSE); gwinSetDefaultColor(HTML2COLOR(0x000000)); gwinSetDefaultBgColor(HTML2COLOR(0xFFFFFF)); // Загружаем картинки, они должны находиться в файловой системе SPIFFs gdispImageOpenFile(&ballImg, "ball.bmp"); gdispImageOpenFile(&bearImg, "bear.bmp"); gdispImageOpenFile(&candleImg, "candle.bmp"); gdispImageOpenFile(µphoneImg, "music.bmp"); gdispImageOpenFile(&treeImg, "tree.bmp"); gdispImageOpenFile(&lightsImg, "lights.bmp"); gdispImageOpenFile(&bigTreeImg, "bigtree.bmp"); // Создаем сами элементы GUI GWidgetInit wi; gwinWidgetClearInit(&wi); wi.g.x = 0; wi.g.y = 0; wi.g.width = 176; wi.g.height = 220; wi.g.show = TRUE; ghContainerMain = gwinContainerCreate(0, &wi, 0); wi.g.parent = ghContainerMain; wi.customDraw = gwinButtonDraw_ImageText; wi.customStyle = 0; wi.customParam = &bigTreeImg; wi.g.x = 0; wi.g.y = 0; wi.text = ""; ghButtonTree = gwinButtonCreate(0, &wi); wi.g.show = FALSE; wi.customParam = &ballImg; wi.g.width = 88; wi.g.height = 73; wi.text = "Шарики"; ghButton1 = gwinButtonCreate(0, &wi); wi.customParam = &candleImg; wi.g.x = 88; wi.g.y = 0; wi.text = "Свечки"; ghButton2 = gwinButtonCreate(0, &wi); wi.customParam = &bearImg; wi.g.x = 0; wi.g.y = 73; wi.text = "Мишки"; ghButton3 = gwinButtonCreate(0, &wi); wi.customParam = &lightsImg; wi.g.x = 88; wi.g.y = 73; wi.text = "Огоньки"; ghButton4 = gwinButtonCreate(0, &wi); wi.customParam = &treeImg; wi.g.x = 0; wi.g.y = 146; wi.text = "Все"; ghButtonAll = gwinButtonCreate(0, &wi); wi.customParam = µphoneImg; wi.g.x = 88; wi.g.y = 146; wi.text = "Музыка"; ghButtonVoice = gwinButtonCreate(0, &wi); }
static bool screenSaver = false; // Переключение экрана между кнопками и скринсэвером void switchScreen (bool flag) { gwinSetVisible (ghButton1,flag); gwinSetVisible (ghButton2,flag); gwinSetVisible (ghButton3,flag); gwinSetVisible (ghButton4,flag); gwinSetVisible (ghButtonAll,flag); gwinSetVisible (ghButtonVoice,flag); gwinSetVisible (ghButtonTree,!flag); screenSaver = !flag; } static unsigned long timeLastActivity =0; void loop() { unsigned long now = millis(); // Проверяем наличие событий от библоитеки GEvent* pe = geventEventWait(&glistener, 2); if (pe && pe->type == GEVENT_GWIN_BUTTON) { GEventGWinButton *we = (GEventGWinButton *)pe; if (we->gwin == ghButton1) {/* Действие по кнопке */} if (we->gwin == ghButton2) {/* Действие по кнопке */} if (we->gwin == ghButton3) {/* Действие по кнопке */} if (we->gwin == ghButton4) {/* Действие по кнопке */} if (we->gwin == ghButtonAll) {/* Действие по кнопке */}; if (we->gwin == ghButtonVoice) {/* Действие по кнопке */}; if (we->gwin == ghButtonTree) {switchScreen (true); startRecognize();} timeLastActivity = now; } // Проверяем если ничего не нажимали 10 секунд, то запускаем скринсэйвер if (!screenSaver && now - timeLastActivity > 10000) { switchScreen (false); } delay (10); }
Как результат — разумное количество строчек кода дает полноценный, и на мой вкус, красивый GUI. На гитхабе полная версия скетча
Еще примеры использования
Простой осцилограф
Исходники на гитхабе: сам скетч
Tetris
Исходники на гитхабе: tetris
исходники скетча с первого видео
Итого, в этой статье у нас получилось сделать удобное и красивое GUI решение, с использованием доступных в Open Source библиотек.