Моё меню первой версии сменяло экраны мгновенно — и в этом была проблема. Пользователь, моя же жена за самодельным инкубатором, не замечал переключения: значения менялись, а глаз цеплялся за то же место. Я решил добавить плавности — и заработал второй грех: display() в каждом проходе loop, кадры тридцать раз в секунду, шина на 100 кГц забита под завязку, кнопка режима нажимается через раз.
Оба греха лечатся одной моделью: анимация — это последовательность кадров, и у неё есть цена. Кадр OLED по I2C занимает около 92 мс на стандартных 100 кГц и примерно 23 мс после Wire.setClock(400000) — цифры мы считали в уроке про буфер и display(). Значит, о плавности сверх десяти кадров в секунду на 100 кГц говорить нечего, и это нормально: глазу хватает.
Разберём три техники: сдвиг контента кадрами на millis, аппаратный скроллинг SSD1306 и переходы через промежуточные состояния. Плюс обратная сторона — где анимация мешает читать и как я её ограничиваю в приборах.
Кадры на millis: из чего складывается плавность
Любая анимация на OLED собирается из одного конструктора: раз в N миллисекунд — очистить буфер, нарисовать кадр со сдвигом, отправить display(). Расписание держит каркас из урока про millis вместо delay: if (millis() - lastFrame >= 100), и внутри — сборка кадра. Шаг 80–120 мс даёт 8–12 кадров в секунду: движение на глаз уже слитное, а шина при 92-миллисекундном кадре не захлёбывается. Захотели чаще — сначала Wire.setClock(400000) сразу после begin(), иначе кадры начнут налезать друг на друга.
Сдвиг контента — печать с меняющейся координатой. Заголовок, въезжающий справа: в кадре с номером k печатаем текст с setCursor(k, 0), где k убывает по клеткам шрифта — сетку 21×8 и клетку 6 пикселей мы считали в уроке про текст. Шесть кадров по четыре клетки — переход занимает полсекунды. Буквы за краем экрана честно обрезаются, ровно как на реальном железе: буфер просто не имеет колонок дальше 128 пикселей.
Fade на монохроме — не полупрозрачность, а промежуточные состояния. Плавное гашение даёт display.dim(): контраст уходит ступенями, если держать паузу 50–80 мс между переключениями; обратное включение возвращает яркость. Выделение — invert(): инверсия зоны на две-четыре вспышки. А имитация затухания кадра — рамка вокруг нового экрана, затем контент: двух промежуточных кадров между старым экраном и новым достаточно, чтобы смена читалась как событие, а не как скачок.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1);long scrollStart = 0;bool scrolling = false; void setup() { Serial.begin(9600); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.println("TEPLO OBORUDOVANIE"); display.setCursor(0, 56); display.println("Eta stroka bezhit sama"); display.display(); // скролл страницы 7 (нижняя строка), шаг каждые 5 кадров развёртки: // сдвигает сам контроллер дисплея, Arduino не тратит ни микросекунды display.startscrollright(0x07, 0x07, 0x05); scrollStart = millis(); scrolling = true;} void loop() { // стоп по таймеру: забытый stopscroll крутит строку вечно if (scrolling && millis() - scrollStart >= 5000) { display.stopscroll(); scrolling = false; display.clearDisplay(); // скролл сдвигает вид, кадр перерисовываем display.setTextSize(1); display.setCursor(0, 56); display.println("Scroll ostanovlen"); display.display(); Serial.println("scroll stop"); }}Когда анимация вредит
Моё правило с границей: двигаю декор — заголовки, разделители, индикаторы, — а числа ставлю на место мгновенно. Значение читается полторы-две секунды; если цифра уезжает или мигает, пользователь читает её по частям и ошибается глазами. В приборах, где на экране уставка или порог, любая анимация в зоне данных — минус к безопасности, и я снимаю её даже в ущерб красоте.
Второе ограничение — количество. Один анимированный элемент на экран: бегущая строка ИЛИ въезд заголовка, не оба сразу. Восемь-двенадцать кадров в секунду достаточно — выше глаз уже не различает, а шина упирается в физику: каждый кадр — 92 мс передачи на 100 кГц, и при двух анимациях полного кадра кнопки начинают отвечать через раз. Это не теория: моя первая версия с display() в каждом проходе loop умирала именно так.
И третье — скроллинг не бесплатен для внимания. Аппаратный скролл не тратит ни микросекунды Arduino: контроллер дисплея сдвигает строки сам. Но он сдвигает весь заданный диапазон страниц, включая те, где у вас статус. Скролльте отдельную строку — страницу 7 при 64 пикселях, нижнюю — и останавливайте командой stopscroll по таймеру. Забытый скролл уносит строку навсегда: грабли, на которые я встал в первый же вечер.
| Техника | Что видит глаз | Цена и границы |
|---|---|---|
| Сдвиг контента (slide) | Контент въезжает по клеткам шрифта | 6–10 кадров по 80–100 мс; полная перерисовка кадра |
| Аппаратный скроллинг | Строка бежит сама, без участия Arduino | 0 мс процессора; только строка-декор, нужен stopscroll |
| Гашение через dim | Экран плавно теряет контраст | 2–4 шага по 50–80 мс; дежурный режим и сон |
| Вспышка invert | Зона мигает инверсией | 2–4 вспышки; выделение пункта, не чаще |
Куда развивать дальше: шторки и рамки из примитивов — растущий fillRect по кадрам — живут в уроке про графику, а расписание из нескольких таймеров — кадр, точка данных, стоп скролла — собирается по схеме из урока про планировщик на millis. В финальном проекте раздела — дашборде с меню и графиком — переходы пригодятся в умеренной дозе: один въезд заголовка при смене экрана и ноль анимации в зоне значений.
Домашнее задание: возьмите двухэкранный скетч из симулятора, добавьте третий экран и въезд заголовка на четыре кадра. Потом посчитайте по Serial, сколько кадров прошло за десять секунд, и умножьте на 92 мс — получите долю шины, занятую анимацией на 100 кГц. Если доля выше половины, уведите шаг кадров до 120 мс и сравните, как изменилась отзывчивость кнопки.
- 1. Сколько примерно длится один кадр 128×64 по I2C на 100 кГц?
- 2. Что даёт аппаратный скроллинг SSD1306?
- 3. На чём строят кадры анимации?
- 4. Скролл включён, stopscroll забыли, кадр перерисовали через display(). Что на экране?
- 5. Где у анимации место по моему правилу?
- Анимация — кадры на millis: шаг 80–120 мс даёт 8–12 кадров в секунду, выше на 100 кГц шина не тянет
- Сдвиг контента: setCursor с убывающей координатой; буквы за краем обрезаются честно, как на плате
- Аппаратный скроллинг: startscrollright(страница, страница, скорость) и stopscroll(); CPU свободен, скролл останавливаем по таймеру
- Fade на монохроме: dim() ступенями или два-три промежуточных кадра между экранами
- Двигаем декор, числа ставим мгновенно; один анимированный элемент на экран
Частые вопросы по теме урока
- Сколько примерно длится один кадр 128×64 по I2C на 100 кГц?
- 1024 байта × 9 бит / 100 000 бит/с — примерно 92 мс. После Wire.setClock(400000) остаётся около 23 мс. Около 92 мс
- Что даёт аппаратный скроллинг SSD1306?
- Сдвиг выполняет контроллер дисплея: процессор отдаёт команду и свободен. Но скролл надо останавливать stopscroll(). Сдвигает строки сам, Arduino не тратит время
- На чём строят кадры анимации?
- Расписание кадров держит millis: интервал проверяется без блокировки, loop остаётся свободным для кнопок и датчиков. На if (millis() - lastFrame >= 100)
- Скролл включён, stopscroll забыли, кадр перерисовали через display(). Что на экране?
- Скролл живёт в контроллере и не зависит от буфера. Без stopscroll() строка уезжает навсегда, даже после display(). Строка продолжает бежать поверх нового кадра
- Где у анимации место по моему правилу?
- Числа читаются полторы-две секунды — их место на месте. Двигают заголовки и индикаторы; один анимированный элемент на экран. В декоре и при смене экранов, не в числах