Введение
Часто полезно показать часть информации напрямую на самом устройстве, не требуя постоянного подключения к компьютеру с 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 — то же значение, но полученное с задержкой передачи через последовательный порт) — для большинства практических случаев эта рассинхронизация минимальна и не имеет значения, но для проектов, где синхронность отображения критична, нужно явно проектировать единый источник истины для данных, отображаемых в обоих местах одновременно.