Дисплей и кнопка/энкодер

Для Arduino в продаже есть множество различных графических и символьных дисплеев, оформленных в виде удобных модулей: LCD, OLED, TFT всех цветов и размеров. Несмотря на это многообразие, а также разницу в подключении, управляются все дисплеи плюс минус одинаково в рамках “экосистемы” Arduino и имеют общую особенность: отправка данных на любой дисплей занимает время, и если делать это неэффективно – микроконтроллер не сможет корректно работать с другими железками, например кнопками, энкодерами, шаговыми моторами и т.д. Решение схожих задач в рамках одноядерного процессора описано в уроке про многозадачность – первого в этом блоке уроков, изучите сначала его. Ко мне настолько часто обращаются с проблемами в написании скетча с дисплеем и кнопками/энкодером, что придётся разобрать данный вопрос более детально.

Для работы с кнопками и энкодерами рекомендую использовать библиотеку EncButton, документация и примеры – по ссылке.

Обновление дисплея


Независимо от типа дисплея, существует два основных подхода отправки данных на дисплей, в библиотеках обычно применяется один из них. Определив “тип” библиотеки можно более эффективно организовать работу своей программы.

Отправка напрямую в дисплей


В этом случае любая “рисующая” или печатающая функция сразу отправляет данные на дисплей, то есть блокирует выполнение программы, пока указанная графика не будет отправлена. Распознать этот подход очень просто: графика выводится на дисплей сразу после вызова любой функции, например вызвали рисование окружности – она сразу появилась на дисплее. Примеры:

  • GyverOLED в режиме “без буфера”
  • LCD1602/LCD2004 I2C
  • Почти все TFT дисплеи и библиотеки к ним

Буфер на стороне МК


В этом случае всё “рисование” происходит в памяти микроконтроллера, программа “закрашивает пиксели” в буфере. Рисующие функции выполняются очень быстро и практически не блокируют выполнение программы, так как данные никуда не передаются и дисплей не обновляется после их вызова. Чтобы обновить дисплей, в такой библиотеке обычно предусмотрена функция с названием update/refresh/show/draw/redraw и прочими синонимами слова “обновление”. Вызов этой функции отправит весь буфер на дисплей и в 99% библиотек выполнение программы будет заблокировано на время отправки, которое зависит от разрешения, скорости и интерфейса подключения дисплея. Примеры:

  • GyverOLED в режиме “с буфером”
  • Практически все библиотеки для OLED дисплеев
  • Дисплей LCD 128×64

Основные подходы


Большинство начинающих ардуинщиков допускают одну и ту же ошибку: начинают обновлять дисплей или выводить данные прямо в основном цикле программы, вместе с обработкой остальных железок:

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();
  }
}

Таким образом мы сможем поймать сигнал с кнопки или энкодера, даже если МК в данный момент занят выводом на дисплей. Дисплей здесь всё ещё обновляется по таймеру.

На платах с esp8266/esp32 прерывания можно подключить на любой пин, а вот на AVR Arduino стандартных прерываний может не хватить. Подключить прерывания на любой пин можно при помощи библиотеки PinChangeInterrupt

Обновление по факту


Второй подход – обновлять дисплей только в том случае, если есть какие-то изменения, которые нужно отобразить. Это касается как значения с датчика, так и управления кнопками/энкодерами (например, меню).

В случае с датчиком это можно абстрактно описать вот так, храня предыдущее значение и сравнивая с текущим, а на дисплей выводить только если они не совпадают:

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("  ");
}

Данный подход в несколько раз ускорит обновление дисплея, а также уберёт мерцание при обновлении. Обратите внимание на трюк с очисткой пробелами вместо очистки всего дисплея – это тоже хороший способ ускорить манипуляции с дисплеем, не всегда нужно очищать его полностью.

Полезные страницы


5/5 - (1 голос)
Подписаться
Уведомить о
guest

7 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии