В многих проектах для esp8266 я использую TFT экран с тачскрином. В зависимости, от проекта интерфейс может быть простым, например, текстовая консоль, выводящая лог работы приложения или просто график изменения входного сигнала. А в некоторых — сложный GUI, с несколькими экранами, графическими кнопками, строками ввода текста и даже виртуальной клавиатурой.


В этой статье хочу поделиться опытом, как можно подключить экран с тачскрином к esp8266 и реализовать графический интерфейс в среде Arduino.


Видео-тизер:



Итак, приступим


Первая часть, аппаратная


Выбор модели экрана


Сейчас на рынке, читай на aliexpress, продается огромное количество моделей TFT экранов — разных размеров, с разными разрешениями, с тачскрином или без.


TFT экраны отличаются способом подключения — есть экраны, которые подключаются по шине SPI, а другие по параллельной шине.


Минимально, для подключения по шине SPI, требуется всего 4 сигнальных вывода (GPIO), а для подключения по параллельной шине — минимум 10 (и это без учета контроллера тачскрина). Подключение по параллельной шине дает большую скорость, т.к. за один такт передается сразу 8 бит информации, а у SPI — только 1 бит.


Однако, у подключения по SPI, есть другое преимущество — меньшее количество задействованных выводов. У esp8266 количество требуемых выводов является решающим фактором: количество свободных GPIO ограничено.


Я бы выделил такие наиболее популярные модули TFT экранов с SPI интерфейсом:


  1. ili9341, 320x240, 2.4", с контроллером тачскрина ссылка
  2. ili9225, 176x220, 2.2, без тачскрина ссылка
  3. 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


esp8266-screen


Часть вторая, программная


Мы определились с экраном и схемой подключения, теперь пора перейти к программной части. В первую очередь — выберем графическую библиотеку, с которой мы будем реализовывать GUI.


Попробовав несколько десятков библиотек, я остановил свой выбор на библиотеке uGFX. На мой взгляд, это одна из лучших графических библиотек для микроконтроллеров. Богатая функциональность сочетается с модульностью и в проект включаются только требуемые компоненты. Библиотека open source и бесплатна для не коммерческого использования. У библиотеки есть качественная документация, доступная на сайте проекта.


Большим плюсом библиотеки uGFX является развитый движок рендеринга шрифтов, с поддержкой utf8. В комплект входит программа генерации шрифтов из ttf файлов, в том числе и с кириллицей.


Библиотека кросс-платформенна — это означает, что GUI часть приложения можно собрать под любой процессор, в том числе и esp8266.


Драйвера экранов и тачскринов подключаются выделенными модулями, и в случае, если нужных драйверов нет в комплекте — их можно реализовать самостоятельно.


Кроме этого, в комплект uGFX входит uGFX studio — WYSWIG редактор интерфейса, в котором можно визуально подготовить макеты интерфейсы, а uGFX studio автоматически сгенерирует код и разложит ресурсы. К сожалению, сейчас uGFX studio еще в статусе beta версии, и чтобы получить бетку, нужно написать разработчикам на форуме.


И, финальная вишенка на торт: GUI код приложения, можно собрать под десктоп (Linux/Windows/OSX) и посмотреть прямо на компьютере, как будет выглядеть интерфейс.


pc-demo


Подключаем uGFX к проекту


Библиотека использует свою систему сборки, которая "из коробки" не поддерживается системой сборки Arduino. Но это ограничение можно обойти. В качестве reference использовал эту статью


Текст ниже предполагает, что среда для разработки Arduino с поддержкой esp8266 уже установлена и настроена. Если еще нет, то про установку и настройку среды можно прочитать в этой статье на geektimes


Теперь по шагам, как подключить библиотеку:


  1. Найти папку Libraries от среды Arduino. В зависимости от платформы, она может располагаться в таких местах:

  • OSX/Users/<username>/Documents/Arduino/libraries/
  • WindowsC:\Users\<username>\My Documents\Arduino\Libraries
  • Linux -/home/<username>/Documents/Arduino/libraries

  1. Склонировать или скопировать uGFX в папку Libraries. Скачать можно отсюда — версию, с уже встроенными кириллическими шрифтами.


  2. Сделать библиотеку "обертку", которая будет содержать реализацию ввода/вывода для драйверов экрана и тачскрина, а так же подключать к сборке нужные нам компоненты 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:

Содержимое library.properties
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

Содержимое board_ILI9341.cpp
#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 для драйвера экрана. Так же остановлюсь на нем подробнее:

Содержимое gmouse_lld_ADS7843_board.cpp
#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 у скетча состоит из двух экранов


  • заставка с елочкой
  • экран с кнопками включения/выключения гирлянд:

w1 w2


Ниже по тексту несколько "снипетов" из кода, как реализовано 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);

Функция инициализации GUI
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(&microphoneImg, "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 = &microphoneImg; 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)


  1. DjPhoeniX
    09.01.2018 04:48

    Пишу на esp8266 (и не в arduino) уже некоторое время, и по прочтению статьи остался всего один вопрос — почему пин «T_IRQ» (генерирующий прерывание для МК, чтобы тот прочитал тачскрин) повесили на GPIO16 — единственный GPIO-пин ESP, который как раз аппаратных прерываний не умеет?


    1. olegator99 Автор
      09.01.2018 10:09

      Согласен, в примере схемы подключения, с T_IRQ получилось не логично. Скорректирую чуть позже схему, что бы при желании можно было использовать обработчик прерывания.


      Однако, для текущей программной реализации это не критично, т.к. uGFX приложения — loop based, и состояние PIN-a опрашивается в основном цикле.


      Да и читать из SPI в обработчике прерывания, увы не получится — нет гарантий, что в момент прерывания шина будет свободна...


    1. olegator99 Автор
      10.01.2018 10:05

      Поправил


      1. DjPhoeniX
        11.01.2018 06:44

        Не до конца — на схеме всё ещё старая распиновка :)


        1. olegator99 Автор
          11.01.2018 11:33

          Вот жеж! А ведь поправил еще вчера, но почему то картинка не обновилась, похоже где то в кэше осела старая. Поменял имя файла, теперь точно должно быть ок.


  1. latonita
    09.01.2018 09:48

    Сколько Кб добавляет линковка этой библиотеки к прошивке?


    1. olegator99 Автор
      09.01.2018 10:12

      Примерно от 20Кб до 80Кб, в зависимости от используемого функционала.


  1. Nizametdinov
    10.01.2018 03:09

    Очень интересная либа, спасибо за статью.