Arduino

OLED/LCD-дисплей на Arduino: дублирование интерфейса Qt на устройстве

Введение

Часто полезно показать часть информации напрямую на самом устройстве, не требуя постоянного подключения к компьютеру с Qt-приложением — небольшой OLED или LCD-дисплей, подключённый к Arduino, может отображать текущие показания датчиков или статус устройства. В статье разберём подключение типичного OLED-дисплея (на контроллере SSD1306, по интерфейсу I2C) и организацию синхронизации отображаемой информации с тем, что видно в Qt-интерфейсе.

Концепция

SSD1306 — широко распространённый контроллер маленьких OLED-дисплеев (обычно 128×64 пикселя), подключаемых через I2C (всего два провода для данных, плюс питание) — библиотека Adafruit_SSD1306 в сочетании с Adafruit_GFX предоставляет удобный API для рисования текста и простой графики на дисплее. Поскольку дисплей физически отделён от Qt-приложения, прошивка сама решает, что показывать, основываясь либо на собственных локально считанных данных датчиков (независимо от того, подключено ли в данный момент Qt-приложение), либо на дополнительной информации, явно отправленной с Qt-стороны через протокол.

Пример кода

// firmware/src/main.cpp — отображение показаний датчика на OLED-дисплее
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define DHT_PIN 2

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DHT dht(DHT_PIN, DHT22);

void setup()
{
    Serial.begin(9600);
    dht.begin();

    if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
        Serial.println("ERROR:DISPLAY_INIT_FAILED");
        while (true) {} // без рабочего дисплея продолжать бессмысленно для этого проекта
    }

    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    delay(100);
    Serial.println("READY");
}

void loop()
{
    static unsigned long lastUpdateTime = 0;

    if (millis() - lastUpdateTime >= 2000) {
        const float temperature = dht.readTemperature();
        const float humidity = dht.readHumidity();

        updateDisplay(temperature, humidity);

        if (!isnan(temperature) && !isnan(humidity)) {
            Serial.print("DHT:");
            Serial.print(temperature);
            Serial.print(",");
            Serial.println(humidity);
        }

        lastUpdateTime = millis();
    }
}

void updateDisplay(float temperature, float humidity)
{
    display.clearDisplay();
    display.setTextSize(2);
    display.setCursor(0, 0);

    if (isnan(temperature) || isnan(humidity)) {
        display.println("Ошибка");
        display.println("датчика");
    } else {
        display.print(temperature, 1);
        display.println(" C");
        display.print(humidity, 1);
        display.println(" %");
    }

    display.display(); // изменения в буфере не видны на экране, пока не вызван display()
}

Пояснения к коду

Инициализация display.begin() с явной проверкой результата важна, поскольку дисплей физически может быть не подключён или подключён неправильно (неверный I2C-адрес, обрыв провода) — без проверки прошивка продолжила бы выполнение, пытаясь рисовать на несуществующем дисплее, что не вызвало бы ошибки, но просто не дало бы никакого видимого эффекта, затрудняя диагностику. Адрес 0x3C — типичный I2C-адрес для большинства SSD1306-модулей, хотя некоторые варианты используют 0x3D, что нужно явно проверять по документации конкретного приобретённого модуля.

updateDisplay() показывает типичный паттерн работы с библиотеками отрисовки на основе буфера — все вызовы рисования (setCursor, print, println) изменяют только внутренний буфер в памяти Arduino, и реальное обновление физического экрана происходит только при явном вызове display.display() — это позволяет подготовить весь кадр целиком перед его показом, избегая визуального мерцания от поэлементной перерисовки. Прошивка независимо от Qt-приложения обновляет дисплей и одновременно отправляет те же данные через последовательный порт — это означает, что информация на маленьком экране устройства доступна, даже если Qt-приложение на компьютере в данный момент не запущено или не подключено.

Подводные камни

  • Полное зависание прошивки (while (true) {}) при неудачной инициализации дисплея означает полную потерю функциональности устройства, включая обработку команд через последовательный порт, даже если сам дисплей не критичен для остальной функциональности проекта — для проектов, где дисплей опционален (желателен, но не обязателен), стоит вместо полной остановки просто продолжить работу без дисплея, установив флаг недоступности дисплея и пропуская соответствующие вызовы в updateDisplay().
  • Совместное использование шины I2C несколькими устройствами одновременно (дисплей + другой I2C-датчик) требует, чтобы у каждого устройства был свой уникальный адрес на общей шине — конфликт адресов (два устройства с одинаковым I2C-адресом) проявляется как неработоспособность одного или обоих устройств без явного, понятного сообщения об ошибке, и диагностика такой проблемы часто требует отдельного скетча «сканера I2C-шины» для определения реальных адресов подключённых устройств.
  • Слишком частое обновление дисплея (display.display()), особенно при большом, детальном содержимом кадра, может создавать заметную нагрузку на ограниченный процессор Arduino, поскольку передача всего буфера кадра по I2C на каждое обновление — относительно медленная операция по меркам частоты основного цикла loop() — для дисплеев с часто меняющимся содержимым стоит явно ограничивать частоту обновления (как в примере, не более раза в 2 секунды) и обновлять только реально изменившуюся часть экрана там, где это возможно, а не весь буфер целиком при каждом малейшем изменении данных.
  • Рассинхронизация информации на физическом дисплее и в Qt-интерфейсе, если оба показывают данные с разной задержкой или из разных источников (например, дисплей показывает последнее локально считанное значение датчика, а Qt — то же значение, но полученное с задержкой передачи через последовательный порт) — для большинства практических случаев эта рассинхронизация минимальна и не имеет значения, но для проектов, где синхронность отображения критична, нужно явно проектировать единый источник истины для данных, отображаемых в обоих местах одновременно.