Дисплей и кнопка/энкодер
Для Arduino в продаже есть множество различных графических и символьных дисплеев, оформленных в виде удобных модулей: LCD, OLED, TFT всех цветов и размеров. Несмотря на это многообразие, а также разницу в подключении, управляются все дисплеи плюс минус одинаково в рамках "экосистемы" Arduino и имеют общую особенность: отправка данных на любой дисплей занимает время, и если делать это неэффективно - микроконтроллер не сможет корректно работать с другими железками, например кнопками, энкодерами, шаговыми моторами и т.д. Решение схожих задач в рамках одноядерного процессора описано в уроке про многозадачность - первого в этом блоке уроков, изучите сначала его. Ко мне настолько часто обращаются с проблемами в написании скетча с дисплеем и кнопками/энкодером, что придётся разобрать данный вопрос более детально.
Обновление дисплея
Независимо от типа дисплея, существует два основных подхода отправки данных на дисплей, в библиотеках обычно применяется один из них. Определив "тип" библиотеки можно более эффективно организовать работу своей программы.
Отправка напрямую в дисплей
В этом случае любая "рисующая" или печатающая функция сразу отправляет данные на дисплей, то есть блокирует выполнение программы, пока указанная графика не будет отправлена. Распознать этот подход очень просто: графика выводится на дисплей сразу после вызова любой функции, например вызвали рисование окружности - она сразу появилась на дисплее. Примеры:
- GyverOLED в режиме "без буфера"
- LCD1602/LCD2004 I2C
- Почти все TFT дисплеи и библиотеки к ним
Буфер на стороне МК
В этом случае всё "рисование" происходит в памяти микроконтроллера, программа "закрашивает пиксели" в буфере. Рисующие функции выполняются очень быстро и практически не блокируют выполнение программы, так как данные никуда не передаются и дисплей не обновляется после их вызова. Чтобы обновить дисплей, в такой библиотеке обычно предусмотрена функция с названием update/refresh/show/draw/redraw и прочими синонимами слова "обновление". Вызов этой функции отправит весь буфер на дисплей и в 99% библиотек выполнение программы будет заблокировано на время отправки, которое зависит от разрешения, скорости и интерфейса подключения дисплея. Примеры:
- GyverOLED в режиме "с буфером"
- Практически все библиотеки для OLED дисплеев
- Дисплей LCD 128x64
Основные подходы
Большинство начинающих ардуинщиков допускают одну и ту же ошибку: начинают обновлять дисплей или выводить данные прямо в основном цикле программы, вместе с обработкой остальных железок:
void loop() { enc_or_button(); disp.print(val); disp.update(); }
Это максимально плохой и неправильный подход:
- Программа 99.999% времени будет просто обновлять дисплей.
- Если опрос кнопки и сможет пробиться, то про энкодер вообще можно забыть.
- У большинства дисплеев слишком частое обновление приводит к тому, что данные блендеют, накладываются друг на друга, либо вовсе не отображаются!
Обновление по таймеру
Для наблюдателя-человека нет смысла обновлять дисплей чаще 10.. 30 раз в секунду (т.е. каждые 30.. 100 миллисекунд). Добавление delay()
в конце основного цикла решит проблему с артефактами частого обновления, но усугубит проблему с опросом органов управления:
void loop() { enc_or_button(); disp.print(val); disp.update(); delay(30); }
Более правильным вариантом будет таймер на millis():
void loop() { enc_or_button(); dispUpd(); } void dispUpd() { static uint32_t tmr; if (millis() - tmr >= 30) { tmr = millis(); disp.print(val); disp.update(); } }
Это очень сильно разгрузит микроконтроллер и уберёт артефакты дисплея, в сценарии "на дисплей просто выводится значение с датчиков" нужно действовать именно так.
Если в проекте есть кнопки или энкодер, то из-за периодического вывода реакция на нажатия и переключения будет чуть заторможенной: мы постоянно обновляем дисплей, даже если обновлять там нечего!
Прерывания
Для повышения качества опроса органов управления всегда можно задействовать прерывания, урок про них.
void setup() { attachInterrupt(...); } void isr() { enc_or_button(); } void dispUpd() { static uint32_t tmr; if (millis() - tmr >= 30) { tmr = millis(); disp.print(val); disp.update(); } }
Таким образом мы сможем поймать сигнал с кнопки или энкодера, даже если МК в данный момент занят выводом на дисплей. Дисплей здесь всё ещё обновляется по таймеру.
Обновление по факту
Второй подход - обновлять дисплей только в том случае, если есть какие-то изменения, которые нужно отобразить. Это касается как значения с датчика, так и управления кнопками/энкодерами (например, меню).
В случае с датчиком это можно абстрактно описать вот так, храня предыдущее значение и сравнивая с текущим, а на дисплей выводить только если они не совпадают:
int val_prev = 0; void loop() { int val = analogRead(0); if (val_prev != val) { disp.print(val); disp.update(); val_prev = val; } }
В случае с кнопками/энкодером и экранным меню - обновлять дисплей нужно только при действиях с органов управления, т.е. клик по кнопке, щелчок энкодера, и т.д. Абстрактный пример: вывод счётчика энкодера на дисплей:
int counter = 0; void loop() { if (encoder_tick()) { if (encoder_right()) counter++; else if (encoder_left()) counter--; disp.print(counter); disp.update(); } }
Оптимизация
Не выводить то, что не меняется
Очень хороший способ ускорить вывод на дисплей. Задача: выводить подпись и значение рядом, на примере дисплея LCD1602.
Способ №1:
- Очистить дисплей
- Вывести подпись
- Вывести значение
int val; // ..... lcd.clear(); // очистить lcd.home(); // курсор в начало lcd.print("Value: "); lcd.print(val);
Это не очень оптимально, потому что мы очищаем весь дисплей, затем переписываем по новой подпись, и только после этого выводим значение. В случае с LCD дисплеем это будет достаточно долго, а также дисплей ощутимо "мигнёт" при обновлении.
Способ №2:
- Однократно вывести подпись
- Поставить курсор после надписи
- Вывести значение
- Вывести следом пару пробелов, чтобы удалить потенциальные остатки предыдущего значения (если оно было короче)
void setup() { lcd.home(); // курсор в начало lcd.print("Value: "); // подпись } void loop() { int val; // ..... lcd.setCursor(7, 0); // курсор на конец надписи lcd.print(val); lcd.print(" "); }
Данный подход в несколько раз ускорит обновление дисплея, а также уберёт мерцание при обновлении. Обратите внимание на трюк с очисткой пробелами вместо очистки всего дисплея - это тоже хороший способ ускорить манипуляции с дисплеем, не всегда нужно очищать его полностью.
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])