Экран прибора живёт в одной комнате с вами, и ночью это выясняется жёстким способом. Полночь, темнота, а на полке светится белая табличка: шапка, логотип, две строки статуса — ночью она работает ночником, только некрасивым. Я привык снижать яркость на всём, что имеет подсветку, и первым делом полез искать то же самое у SSD1306 — а нашёл две разные команды и одну распространённую путаницу, которая ночью бьёт по глазам сильнее любого заводского дефолта. Тонкость тут в том, что у OLED нет подсветки, которую можно было бы притушить ШИМом, как у LCD, — приглушать приходится саму панель, и то, как это устроено внутри, не так очевидно, как кажется.
Путаница выглядит так: «сделаем экран потемнее — инвертируем, будут чёрные буквы на белом». Ровно наоборот. Инверсия превращает тёмный фон в сплошное белое поле, и в тёмной комнате такой экран слепит, как фонарик. Настоящий ночной режим собирается из чёрного фона, минимума светящихся пикселей и приглушённого контраста — а инверсия инструмент противоположной ситуации, яркой. Обе команды живут в одной библиотеке и вызываются одной строкой, но ведут себя настолько по-разному, что их постоянно путают местами — и цена путаницы видна ночью сразу. Как обе устроены внутри и где их место — сейчас разберём.
Инверсия: команда панели, а не кадра
display.invertDisplay(true) — это команда самому контроллеру SSD1306, а не операция над вашим кадром: в буфере ничего не меняется, меняется трактовка каждого бита при выводе на матрицу. В коде библиотеки это посылка 0xA7 вместо нормального 0xA6 — тот же канал, что и у begin, который мы проходили в уроке про первый запуск экрана. Действует мгновенно: следующий же display() выйдет наоборот, а если display не вызывать вовсе — панель останется с прежним кадром, но инвертированным. Технически панель хранит один бит на пиксель и при выводе читает его либо как есть, либо наоборот: перекрашивать экран вручную не нужно, контроллер делает это сам на каждой развёртке, тактов процессора команда не отнимает. Отменяется только парным вызовом invertDisplay(false), и на этом удобно строить акценты: подмигнуть всем экраном, показать аварийный режим, добавить контраста на солнце.
Вторая близкая команда — dim(bool). Внутри библиотеки это SETCONTRAST: dim(true) шлёт панели нулевой контраст, dim(false) возвращает рабочее значение 0xCF. Ступеней две — это важно понимать заранее: плавной регулировки яркости у команды нет, и «полутёмный» режим из неё не выжать. И проверьте эффект на своём модуле в первую неделю: жалобы на «dim не работает» в половине случаев заканчиваются не кодом, а клоном на другом контроллере. Провода до экрана те же два, что описаны в уроке про схему подключения, новых цепей ночной режим не требует — всё решается кодом.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1); int ldrPin = A1; // фоторезистор в делителе на A1int night = 0; // режим: 0 - день, 1 - ночьint solar = 0; // подсветка контрастом на ярком светуunsigned long lastCheck = 0; void setup() { Serial.begin(9600); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); display.dim(false); // стартуем в дневном контрасте display.invertDisplay(false);} void loop() { if (millis() - lastCheck >= 200) { // проверка освещённости раз в 200 мс lastCheck = millis(); int light = analogRead(ldrPin); // пороги парами: режим не дребезжит на границе if (night == 0 && light < 300) { // стемнело night = 1; display.invertDisplay(false); display.dim(true); // SETCONTRAST с нулём Serial.println("night: dim(true)"); } if (night == 1 && light > 500) { // рассвело night = 0; display.dim(false); // возврат к 0xCF Serial.println("day: dim(false)"); } if (night == 0 && solar == 0 && light > 950) { // яркий свет solar = 1; display.invertDisplay(true); // белая подложка на солнце Serial.println("solar: invert on"); } if (solar == 1 && light < 850) { solar = 0; display.invertDisplay(false); Serial.println("solar: invert off"); } // кадр: ночью одна короткая строка, днём обычная сводка display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); if (night == 1) { display.print("N"); // восемь белых пикселей вместо сотен } else { display.print("Sveta: "); display.print(light); } display.display(); }}Разбор скетча начнём с порогов, потому что именно здесь схемы бьют чаще всего. Порог включения ночи — 300, выключения — 500: две сотни единиц зазора нужны нарочно, чтобы режим не дребезжал на границе. Фоторезистор в делителе плавает на десятки единиц от одних и тех же сумерек: облако, фара за окном, рука, заслонившая датчик при сборке. С одним порогом такая схема мигает режимами десятки раз за вечер; с парой порогов она переключается один раз и держится. Тот же приём со зазором в сотню-другую единиц я потом переносил на термостаты и полив — везде, где решение принимается по плавающей величине.
Вторая деталь — проверка раз в 200 мс по millis: loop между проверками свободен, и в него же однажды встанут кнопки и связь. Третья — печать команд в Serial: dim(true) и invertDisplay ничего не возвращают, и без лога вы не узнаете, что скетч вообще зашёл в ветку ночи. Такие принты я держу до конца испытаний прибора — потом они спасают при разборе «почему ночью всё погасло», и чаще всего ответ оказывается в одной из напечатанных строк.
Почему ночью инверсия вредна, объясняет физика панели. SSD1306 сам светится: каждый белый пиксель — отдельный источник со своим током, чёрный не жжёт ничего. Полностью белый кадр 128×64 — это тысячи горящих точек и ток порядка двух десятков миллиампер; почти чёрный экран ест единицы. Разница в десять раз — и всё решается тем, сколько белого вы нарисовали. Инвертированный кадр превращает тёмный фон в белое поле из тысяч светящихся точек — в темноте это не приглушённый режим, а переносица через секунду. Настоящий ночной экран собирается наоборот: чёрный фон, минимум белых пикселей — одна короткая строка вместо шапки и логотипа — плюс dim(true), уводящий контраст вниз. Экономия тут тройная: глаза, ток и ресурс панели — заливки белым старят OLED быстрее всего, арифметика старения считалась в уроке про питание и выгорание.
Полутонов у панели нет: пиксель либо горит, либо нет. «Серый» можно только изобразить — шахматным узором из редких точек; я пробовал ровно один раз: с дистанции полуметра узор сливается в серость, вблизи — рябь, и второй попытки идея не получила. Если хочется промежуточной яркости, честнее уменьшить количество белого на экране, чем выдумывать полутона.
Выделение в меню — другой механизм
Отдельно держите в голове разницу между инверсией экрана и инверсией строки. Для выделенного пункта меню панельную команду никто не трогает — вместо этого зона закрашивается fillRect, а перед печатью строки цвета меняются местами: setTextColor(SSD1306_BLACK, SSD1306_WHITE) рисует тёмные буквы на белом фоне. Координаты закраски — те же, что и у текста: fillRect(0, 16, 128, 16, SSD1306_WHITE) накрывает строку двойки по всей ширине, и высота 16 совпадает с высотой ячейки двойки — совпадение удобное, я им пользуюсь в каждом меню. Это операции над буфером: кадр каждый раз пересобирается и уходит целиком, механика — та же, что в уроке про clearDisplay. Навигация по подсвеченным строкам собрана в уроке про меню на кнопках.
Панельная инверсия на это не годится: она переворачивает весь кадр сразу. Переключить invertDisplay перед одной строкой и вернуть обратно после — технически можно, но вы получите два лишних обмена с панелью и мигание на медленной шине; буферная перекраска бесплатна и предсказуема. Плюс у неё есть бонус: подсветку выделения можно делать любой ширины — от одного символа до всей строки — и сдвигать по строкам без единого обращения к панели.
| Инструмент | Что меняет | Где живёт | Когда применять |
|---|---|---|---|
| invertDisplay(true) | каждый бит кадра при выводе: чёрное и белое меняются | состояние панели, команда 0xA7 | акцент на весь экран, солнечный режим |
| dim(true) | контраст панели: SETCONTRAST с нулём | состояние панели, две ступени | ночной режим, тёмная комната |
| setTextColor(BLACK, WHITE) | цвет и фон только для новых символов | буфер, кадр пересобирается целиком | выделение строки в меню |
| clearDisplay() без печати | ни одного светящегося пикселя | буфер | сон прибора, полный покой глаз |
Три детали, о которые я спотыкался сам. Первая: обе настройки — состояние панели, они переживают clearDisplay и display(); отменять их надо явным вызовом с false, а после сброса платы begin возвращает заводской режим заново. Вторая: dim меняет контраст, а не «яркость подсветки» — подсветки у OLED нет, и ожидание выключателя заканчивается недоумением; полностью тёмным экран делают только ноль белых пикселей. Третья: клоны на контроллере SH1106 капризничают и с текстом, и с командами — если dim выглядит как ноль эффекта, сначала проверьте смещение кадра, как в уроке про SH1106, а потом уже ищите причину в контрасте.
И правило таймингов: проверку освещённости я держу на millis-таймере с шагом 200 мс — чаще незачем, освещённость меняется медленнее, а каждый лишний analogRead только добавляет шума в решения по порогам. И помните, что режимы переключаются парами порогов, а не одним числом: гистерезис из скетча выше — не украшение, а условие жизни схемы у окна.
- 1. Что делает display.invertDisplay(true)?
- 2. dim(true) внутри библиотеки Adafruit_SSD1306 — это:
- 3. Экран для ночной комнаты правильно собирать так:
- 4. Что произойдёт с invertDisplay(true) после clearDisplay() и display()?
- 5. Выделить строку в меню инверсными цветами — правильный способ:
- invertDisplay(true) — команда панели 0xA7: чёрное и белое меняются при выводе, буфер не трогается, отмена — только invertDisplay(false).
- dim() — это SETCONTRAST: две ступени, ноль и 0xCF; плавной яркости у панели нет.
- Ночью инверсия вредна: белое поле из тысяч точек слепит; ночной экран — чёрный фон, одна строка и dim(true).
- Выделение в меню — fillRect плюс setTextColor(BLACK, WHITE): инверсия кадра, а не панели.
- Обе настройки переживают clearDisplay и живут до сброса платы или явной отмены.
Частые вопросы по теме урока
- Что делает display.invertDisplay(true)?
- Это команда панели 0xA7: каждый бит кадра при выводе читается наоборот. Буфер не трогается, эффект мгновенный. Меняет местами чёрное и белое во всём кадре при выводе на панель
- dim(true) внутри библиотеки Adafruit_SSD1306 — это:
- Подсветки у OLED нет: dim шлёт панели 0x81 с нулём, dim(false) возвращает 0xCF. Ступеней две, промежуточных значений команда не принимает. Команда SETCONTRAST с нулевым значением
- Экран для ночной комнаты правильно собирать так:
- Каждый белый пиксель светится сам; инверсия ночью превращает экран в фонарь. Тёмный фон, одна строка, приглушённый контраст. Чёрный фон, минимум белых пикселей и dim(true)
- Что произойдёт с invertDisplay(true) после clearDisplay() и display()?
- Обе настройки живут в контроллере панели до явного false или сброса платы; clearDisplay чистит только буфер в ОЗУ. Сброс по желанию делают руками в setup(). Инверсия сохранится — это состояние панели, а не кадра
- Выделить строку в меню инверсными цветами — правильный способ:
- Выделение — буферная операция: прямоугольник фоном и текст с цветом и фоном. invertDisplay инвертировал бы весь экран сразу. fillRect фоном и setTextColor(SSD1306_BLACK, SSD1306_WHITE) перед печатью