В многих проектах для 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\Libraries
Linux
-/home/<username>/Documents/Arduino/libraries
Склонировать или скопировать uGFX в папку Libraries. Скачать можно отсюда — версию, с уже встроенными кириллическими шрифтами.
- Сделать библиотеку "обертку", которая будет содержать реализацию ввода/вывода для драйверов экрана и тачскрина, а так же подключать к сборке нужные нам компоненты uGFX. Для этого, в папке Libraries нужно создать подпапку uGFXesp, с примерно таким содержимым:
uGFXesp
+-- library.properties
L-- 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
L-- 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 библиотек.
Комментарии (8)
latonita
09.01.2018 09:48Сколько Кб добавляет линковка этой библиотеки к прошивке?
olegator99 Автор
09.01.2018 10:12Примерно от 20Кб до 80Кб, в зависимости от используемого функционала.
DjPhoeniX
Пишу на esp8266 (и не в arduino) уже некоторое время, и по прочтению статьи остался всего один вопрос — почему пин «T_IRQ» (генерирующий прерывание для МК, чтобы тот прочитал тачскрин) повесили на GPIO16 — единственный GPIO-пин ESP, который как раз аппаратных прерываний не умеет?
olegator99 Автор
Согласен, в примере схемы подключения, с T_IRQ получилось не логично. Скорректирую чуть позже схему, что бы при желании можно было использовать обработчик прерывания.
Однако, для текущей программной реализации это не критично, т.к. uGFX приложения — loop based, и состояние PIN-a опрашивается в основном цикле.
Да и читать из SPI в обработчике прерывания, увы не получится — нет гарантий, что в момент прерывания шина будет свободна...
olegator99 Автор
Поправил
DjPhoeniX
Не до конца — на схеме всё ещё старая распиновка :)
olegator99 Автор
Вот жеж! А ведь поправил еще вчера, но почему то картинка не обновилась, похоже где то в кэше осела старая. Поменял имя файла, теперь точно должно быть ок.