Метеоузел на Uno я довёл до нужной кондиции и захотел историю за сутки: DS18B20, кнопка, дневник в EEPROM, протокол в Serial, дашборд на OLED. Скетч собирался, всё работало — пока однажды узел не начал зависать раз в несколько часов: строки интерфейса превращались в кашу, потом плата перезагружалась. Виновника я нашёл не сразу: свободной SRAM почти не осталось, а главный съедальщик сидел на A4/A5 — кадр OLED в 1024 байта.
Это не повод выкидывать экран, но повод посчитать память всерьёз. В этом уроке — где живут те самые 1024 байта, что можно с них спросить, что реально экономит зональная перерисовка (и чего она не экономит — будет честное признание), и куда идти, когда после всех оптимизаций буфер всё равно не помещается: в page-режим U8g2, который рисует страницу за страницей со 128 байтами буфера.
Модель буфера — рисуем в ОЗУ, показываем по вызову — уже разобрана в уроке про clearDisplay и display(); здесь речь о цене этой роскоши на маленькой плате.
Куда уходят 1024 байта и что от них остаётся
Сложим бюджет SRAM Uno: 2048 байт всего. Буфер OLED — 1024. Wire держит два буфера по 32 байта под транзакции, Serial — кольцо на 64. Каждый long — четыре байта, каждая String со своим куском кучи — ещё кусок, который потом фрагментирует всю остальную память. После буфера экрана и библиотечных накладных свободными остаются сотни байт, и каждый новый блок статистики откусывает от них. Отсюда правило, к которому я пришёл через те зависания: на Uno раскладку памяти считаю до подключения нового устройства, а не после первого падения в три часа ночи.
Что съедает память помимо буфера: глобальные массивы, чьи размеры вы задали на глаз с тройным запасом; String, которые в loop плодят новые куски кучи на каждой итерации; библиотеки с собственными кэшами и буферами. Приём простой: глобальные переменные — константных размеров, String — только там, где без них никак, и после каждого нового устройства проверяйте, что скетч всё ещё дышит: зависание через часы работы — верный признак того, что куча кончилась.
Зональная перерисовка — первое, что советуют в таких случаях, и честно разложим, что именно она экономит. Явно: процессорное время и сложность кадра — вы не пересобираете шапку и статичные зоны, обновляете одну ячейку. Скрыто: ничего больше. Буфер в Adafruit_SSD1306 всегда полный и всегда в памяти, а display() отправляет на контроллер все 1024 байта целиком — библиотека не умеет слать полкадра, сколько бы зон вы ни обновили. Это не жадность разработчиков, а устройство контроллера: его видеопамять — сплошное зеркало кадра.
Следствие неприятное, но честное: зональность лечит тормоза и запутанный код, но не лечит нехватку памяти. Если после всех оптимизаций свободных байт всё равно не хватает — решение меняет библиотеку или экран, а не стиль рисования.
| Подход | Буфер в ОЗУ | Что идёт по шине | Кому подходит |
|---|---|---|---|
| Adafruit_SSD1306, полный кадр | 1024 байта | 1024 байта за display(), ~92 мс на 100 кГц | Mega и ESP32, где RAM в избытке |
| Adafruit + зональная перерисовка | те же 1024 байта | те же 1024 — display() шлёт всё | порядок в коде, статичные шапки |
| U8g2 page mode (_1_) | 128 байт на страницу | по 16 байт на страницу, кадр из 8 заходов | Uno и Nano, когда байты на счету |
| U8x8, текстовый режим | 0 байт | байт на символ, знакоместами | только текст, без графики |
Раз буфер всё равно наш, им можно распоряжаться напрямую: метод display.getBuffer() отдаёт указатель на те самые 1024 байта, и в них можно читать и писать байтами — быстро гасить нижние страницы, сравнивать кадры между собой, готовить заготовки. Это нижний ярус библиотеки: без проверок границ и без комфорта, зато без копирований и перерисовок там, где нужен голый скорость-байт. Честная оговорка: работа байтами — техника последних байтов экономии; для девяти интерфейсов из десяти хватает зональной схемы ниже.
Рабочая схема зонального экрана, которую я применяю: в setup рисуем всё статичное — шапку, разделители, подписи — и отправляем кадр один раз. В loop трогаем только ячейку данных: затираем строку пробелами той же ширины, печатаем новое значение, display(). clearDisplay() из цикла исчезает — вместе с ним исчезает и перерисовка всего подряд. Ловушка одна: print поверх непустых символов складывает кашу, поэтому затирание — обязательная половина приёма; ту же механику стирания мы разбирали на ластике в уроке про графику. Зоны держите в константах и не пересекайте: затирание делайте не короче самого длинного значения ячейки, иначе хвост старого числа доживёт до следующего кадра.
Расписание обновлений при этом не меняется: между кадрами цикл живёт по millis без delay, а если задач много — по планировщику на millis. Зональность хорошо уживается с идеей обновлять экран только при изменении данных: шапка не перерисовывается никогда, ячейка — только когда пришло новое значение.
#include <U8g2lib.h>#include <Wire.h> // _1_ вместо _F_: буфер одной страницы 128 байт, а не полный кадрU8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); void setup() { u8g2.begin();} void loop() { u8g2.firstPage(); do { u8g2.setFont(u8g2_font_6x12_tr); u8g2.drawStr(0, 12, "Bufer: 128 bajt"); u8g2.drawStr(0, 28, "Stranica za stranicej"); u8g2.setCursor(0, 44); u8g2.print(millis() / 1000); u8g2.print(" s"); } while (u8g2.nextPage());}Page mode: как рисуют без полного кадра
Разберём, что делает скетч выше. U8g2 в варианте _1_ держит буфер одной страницы — 128 байт, восемь пиксельных строк. Пара firstPage/nextPage — это цикл: библиотека готовит страницу, вы её рисуете, nextPage отправляет 128 байт на контроллер и переключается на следующую. Кадр собирается за восемь заходов, каждый — 16 байт полезной нагрузки, около полутора миллисекунд по шине на 100 кГц. Полного кадра в памяти платы не существует ни в один момент времени. Мелкий бонус для шины: короткие посылки оставляют паузы между страницами, в которые сосед по I2C успевает ответить — полный кадр на 92 мс такими окнами не балует.
Цена очевидна из описания: ваш код рисования выполняется восемь раз за кадр, и всё внутри цикла должно от прохода к проходу давать одинаковую картинку — без инкрементов и случайных значений между страницами, иначе страницы разъедутся. Мерцания нет: контроллер по-прежнему показывает свою видеопамять, страница просто перезаписывает свою часть. А требования к ОЗУ падают в восемь раз: на Uno рядом с экраном снова помещается полноценный датчик со своими структурами.
Отдельно про четвёртую строку таблицы: U8x8 — режим совсем без буфера. Он пишет знакоместами 8×8 прямо в контроллер, минуя ОЗУ платы: один символ — одна посылка по шине. Платите вы универсальностью: никаких примитивов, никаких своих шрифтов и позиций с точностью до пикселя — только текстовая сетка. Для журнала событий на голодном узле — то что надо; для интерфейса с зонами — тесно.
Как выбираю сам: интерфейсы с зонами, шрифтами и статикой остаются на Adafruit даже на Uno, если бюджет памяти сходится; узлы, где RAM упёрлась в потолок, переезжают на U8g2 page mode целиком — экономить байты внутри библиотеки с полным буфером бессмысленно. Третий путь — модуль 128×32 из предыдущего урока: буфер 512 байт без смены библиотеки, и часто этого хватает.
- Буфер OLED 128×64 — 1024 байта из 2048 SRAM Uno: бюджет памяти считайте до подключения нового устройства
- Зональная перерисовка экономит процессор и сложность кода, но не RAM и не шину: display() шлёт весь кадр
- Рабочая схема: статика в setup, в loop — затирание строки пробелами и новое значение, без clearDisplay
- display.getBuffer() даёт прямой доступ к байтам буфера — быстро и без проверок границ
- U8g2 page mode: буфер 128 байт, кадр из восьми страниц — RAM экономится в восемь раз, код рисуется восемь раз
- 1. Что экономит зональная перерисовка в Adafruit_SSD1306?
- 2. Сколько байт держит page-буфер U8g2 в варианте _1_?
- 3. display() вызван после обновления одной ячейки. Что уйдёт по шине?
- 4. Что обязательно перед печатью нового значения при зональной перерисовке?
- 5. RAM после всех оптимизаций всё равно мало. Где честнее всего её взять?
После этого урока бюджет памяти перестаёт быть чёрным ящиком: 1024 байта буфера — известная величина, зональность — инструмент наведения порядка, page-режим — запасной выход, который всегда под рукой. Приятно и то, что зональная схема улучшает код сама по себе: статика отделена от динамики, зоны названы и не пересекаются, и через полгода в скетч добавляется новая ячейка без перестройки всего экрана. Дальше по разделу — питание: сколько миллиампер стоит каждая горящая строка и почему статичной картинке на OLED нужна жизнь по расписанию.
Домашнее задание: переведите свой дашборд на зональную схему — статика в setup, обновление одной строки в loop, счётчики полных и зонных обновлений в Serial. Прогоните минуту и посмотрите на соотношение: у меня после первой минуты оно обычно выходит пять зонных против нуля полных. Потом на плате поставьте U8g2 из менеджера, соберите page-скетч из урока и сравните с Adafruit-версией того же экрана: картинка одинаковая, буфер в восемь раз меньше.
Частые вопросы по теме урока
- Что экономит зональная перерисовка в Adafruit_SSD1306?
- Буфер остаётся полным — 1024 байта, display() отправляет весь кадр. Экономится работа по пересборке зон: процессор и простота кода, а на статичных шапках ещё и время, которое раньше тратилось на их повторное рисование. Процессорное время и сложность кода
- Сколько байт держит page-буфер U8g2 в варианте _1_?
- Одна страница — восемь пиксельных строк: 128 × 8 / 8 = 128 байт. Полный кадр собирается из восьми страниц без хранения целого кадра в ОЗУ. 128
- display() вызван после обновления одной ячейки. Что уйдёт по шине?
- Adafruit_SSD1306 не умеет частичных отправок: кадр переливается целиком, независимо от того, сколько зон вы тронули. Весь буфер, 1024 байта
- Что обязательно перед печатью нового значения при зональной перерисовке?
- print рисует поверх старых символов — получится каша. Сначала гасим зону той же ширины, потом печатаем новое значение — затирание не короче самого длинного числа ячейки. Затирание зоны пробелами или ластиком
- RAM после всех оптимизаций всё равно мало. Где честнее всего её взять?
- Полный буфер Adafruit не уменьшается настройками. Реальные пути: U8g2 _1_ со 128 байтами страницы или модуль 128×32 с буфером 512 байт. Сменить библиотеку на page-режим или экран поменьше