View Categories

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

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

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

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

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