Для Arduino в продаже есть множество различных графических и символьных дисплеев, оформленных в виде удобных модулей: LCD, OLED, TFT всех цветов и размеров. Несмотря на это многообразие, а также разницу в подключении, управляются все дисплеи плюс минус одинаково в рамках "экосистемы" Arduino и имеют общую особенность: отправка данных на любой дисплей занимает время, если делать это неэффективно - МК не сможет корректно работать с другими железками, например кнопками, энкодерами, шаговыми моторами и т.д. Решение схожих задач в рамках одноядерного процессора описано в предыдущих уроках.
Ко мне настолько часто обращаются с проблемами в написании скетча с дисплеем и кнопками/энкодером, что придётся разобрать данный вопрос более детально.
Обновление дисплея #
Независимо от типа дисплея, существует два основных подхода отправки данных на дисплей, в библиотеках обычно применяется один из них. Определив "тип" библиотеки можно более эффективно организовать работу своей программы.
Отправка напрямую в дисплей #
В этом случае любая "рисующая" или печатающая функция сразу отправляет данные на дисплей, то есть блокирует выполнение программы, пока указанная графика не будет отправлена. Распознать этот подход очень просто: графика выводится на дисплей сразу после вызова любой функции, например вызвали рисование окружности - она сразу появилась на дисплее. Примеры:
- GyverOLED в режиме "без буфера"
- LCD1602/LCD2004 I2C
- Почти все TFT дисплеи и библиотеки к ним
Буфер на стороне МК #
В этом случае всё "рисование" происходит в памяти МК, программа закрашивает "виртуальные пиксели" в буфере. Рисующие функции выполняются очень быстро и практически не блокируют выполнение программы, так как данные никуда не передаются и дисплей не обновляется после их вызова. Чтобы обновить дисплей, в такой библиотеке обычно предусмотрена функция с названием update
/refresh
/show
/draw
/redraw
и прочими синонимами слова "обновление". Вызов этой функции отправит весь буфер на дисплей и в 99% библиотек выполнение программы будет заблокировано на время отправки, которое зависит от разрешения, скорости и интерфейса подключения дисплея. Примеры:
- GyverOLED в режиме "с буфером"
- Практически все библиотеки для OLED дисплеев
- Дисплей LCD 128x64
Основные подходы #
Большинство начинающих ардуинщиков допускают одну и ту же ошибку: начинают обновлять дисплей или выводить данные прямо в основном цикле программы, вместе с обработкой остальных железок:
void loop() {
опрос_кнопки();
disp.print(val);
disp.update();
}
Это максимально плохой и неправильный подход:
- Программа 99.999% времени будет просто обновлять дисплей
- Если опрос кнопки и сможет пробиться, то про энкодер можно забыть
- У большинства дисплеев слишком частое обновление приводит к тому, что данные блендеют, накладываются друг на друга, либо вовсе не отображаются!
Обновление по таймеру #
Для наблюдателя-человека нет смысла обновлять дисплей чаще 10.. 30 раз в секунду (т.е. каждые 30.. 100 миллисекунд). Добавление delay()
в конце основного цикла решит проблему с артефактами частого обновления, но усугубит проблему с опросом органов управления:
void loop() {
опрос_кнопки();
disp.print(val);
disp.update();
delay(30);
}
Более правильным вариантом будет "таймер на millis()":
void loop() {
опрос_кнопки();
dispUpd();
}
void dispUpd() {
static uint32_t tmr;
if (millis() - tmr >= 30) {
tmr = millis();
disp.print(val);
disp.update();
}
}
Это очень сильно разгрузит МК и уберёт артефакты дисплея, в сценарии "на дисплей просто выводится значение с датчиков" нужно действовать именно так.
Если в проекте есть кнопки или энкодер, то из-за периодического вывода реакция на нажатия и переключения будет чуть заторможенной: мы постоянно обновляем дисплей, даже если обновлять там нечего!
Прерывания #
Для повышения качества опроса органов управления всегда можно задействовать прерывания:
void setup() {
attachInterrupt(...);
}
void isr() {
опрос_кнопки();
}
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(" ");
}
Данный подход в несколько раз ускорит обновление дисплея, а также уберёт мерцание при обновлении. Обратите внимание на трюк с очисткой пробелами вместо очистки всего дисплея - это тоже хороший способ ускорить манипуляции с дисплеем, не всегда нужно очищать его полностью.