Посмотр рубрик

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

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

Это очень неэффективный подход:

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

Обновление по таймеру #

Для наблюдателя-человека нет смысла обновлять дисплей чаще 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();
    }
}

Это очень сильно разгрузит МК и уберёт артефакты дисплея, в сценарии "на дисплей просто выводится значение с датчиков" нужно действовать именно так.

Если в проекте есть кнопки или энкодер, то из-за периодического вывода реакция на нажатия и переключения будет чуть заторможенной: мы постоянно обновляем дисплей, даже если обновлять там нечего!

Прерывания #

Если дисплей надолго блокирует программу, для энкодера или короткого внешнего сигнала можно задействовать прерывание. В обработчике нужно только быстро сохранить событие, например изменить volatile счётчик или поднять флаг. Работу с дисплеем, подавление дребезга кнопки и остальную логику оставляем в loop():

volatile bool event;

void setup() {
    attachInterrupt(...);
}

void isr() {
    event = true;
}

void dispUpd() {
    static uint32_t tmr;
    if (millis() - tmr >= 30) {
        tmr = millis();
        disp.print(val);
        disp.update();
    }
}

Таким образом мы сможем поймать короткий сигнал или фронт энкодера, даже если МК в данный момент занят выводом на дисплей. Обычную кнопку чаще проще регулярно опрашивать в loop() - для неё всё равно нужно подавлять дребезг. Дисплей здесь всё ещё обновляется по таймеру.

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

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

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

int val_prev = 0;

void loop() {
    int val = analogRead(0);
    if (abs(val_prev - val) >= 3) {
        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("      ");
}

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

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


(9 голосов)
Подписаться
Уведомить о
guest

5 комментариев
Старые
Новые Популярные
Прокрутить вверх