Логотип для своего термостата я честно раскладывал по листку в клетку: 32 строки, в каждой по 16 байтов. К третьей строке я запутался, где клетка строка, а где столбец, одна съехавшая единица сломала всю картинку, и искал её минут десять, сверяя байты с крестиками. Ручная раскладка из прошлого урока отлична для понимания и невыносима для практики: картинку в пятьсот пикселей так не делают, у меня после той недели дёргался глаз.
Дальше было хуже: я нашёл конвертер, вставил логотип из векторного файла размером 400х300, нажал кнопку и получил кашу. Инструмент честно ужал картинку сам, тонкие линии логотипа утонули при сжатии, а порог по умолчанию съел ещё и полтолщины оставшихся. Виноват был не конвертер, а я: картинку готовят заранее точного размера, и никакой порог после сжатия не спасает.
Здесь собираем правильный конвейер: онлайн-инструмент image2cpp, пять настроек, из которых решают три, и вставка массива в скетч так, чтобы всё собралось с первого раза. Структуру массива мы уже знаем из урока про drawBitmap — конвертер делает ту же работу за минуту и без клетчатого листка. К концу урока соберёте заставку: логотип по центру, две секунды, пропуск по кнопке.
Пять шагов: от PNG к массиву байтов
image2cpp — открытый конвертер, который живёт в браузере: PNG или JPG на входе, массив байтов на выходе, никакой установки. Есть и обратная функция: вставьте готовый массив в поле исходника — увидите превью картинки. Для проверки чужих массивов из интернета это удобнее моего листка в клетку: видно сразу, а не после прошивки.
Шаги у меня устоялись такие. Первый — размер: иконка 16х16 для строки дашборда, знак 32х32, логотип 64х32 или полный кадр 128х64; вес в байтах мы считали в первом уроке про OLED. Картинку я заранее привожу к точному размеру в редакторе: сжатие силами конвертера рубит тонкие линии, и потом это не лечится порогом. Второй — порог, threshold: граница яркости, ниже которой пиксель становится чёрным. Двигаете ползунок и смотрите на превью, пока картинка не соберётся: уведёте далеко — тонкие штрихи исчезнут, вернёте — светлое поле зальётся пятнами.
Рабочий цикл у меня двухпроходный: первый прогон — черновой, чтобы понять, где картинка деградирует; потом правлю PNG — утолщаю линии, чищу фон — и второй прогон с сохранением массива. Десять минут на цикл, зато массив попадает в скетч один раз и навсегда, а не через десять итераций перегенерации.
Третий — инверсия: чёрное и белое меняются местами. Для OLED, где светятся белые пиксели, норма — белый рисунок на чёрном фоне: фон не ест ток, а картинка светится. Инверсию включают, когда нужен тёмный знак на светлой плашке — как ластик и плашка из уроков про буфер и текст. Четвёртый — режим вывода, к нему вернёмся ниже: он ломает больше всего чужих экспортов. Пятый — галка PROGMEM и кнопка генерации: на выходе готовый фрагмент кода с массивом.
Копируете массив целиком — вместе с объявлением const unsigned char PROGMEM, которое конвертер уже написал, — и вставляете над setup. Размеры полезно сверить глазами: w и h из настроек конвертера должны совпасть с числами в будущем drawBitmap, ведь библиотека размеры из массива не читает.
| Настройка в image2cpp | Что делает | Моя практика |
|---|---|---|
| Размер и масштаб | во что ужмётся картинка по пикселям | точный размер готовлю в редакторе заранее |
| Порог (threshold) | какой яркости пиксель станет чёрным | двигаю по превью, пока линии не соберутся |
| Инверсия | меняет чёрное и белое местами | выключена: белое на чёрном экономит ток OLED |
| Режим вывода | раскладка байтов: по строке или по столбцу | Horizontal для Adafruit drawBitmap, проверяю всегда |
| Галка PROGMEM | дописывает PROGMEM в объявление массива | включена для всего крупнее 32 байтов на Uno |
Режим вывода — главная ловушка. Adafruit drawBitmap ждёт горизонтальную раскладку: байт — восемь соседних пикселей строки, байты идут слева направо, на строку шириной w — (w + 7) / 8 байтов. Режим Vertical укладывает байт столбцом из восьми пикселей — так ждут данные библиотеки другого семейства и экраны вроде Nokia-шных. Перепутали — массив корректный, код корректный, а на экране вертикальные полосы вместо логотипа. Диагноз ставится за секунду: полосы ровные и периодичные — значит, раскладка, а не картинка виновата.
С горизонтальной раскладкой картинка оживёт по центру: drawBitmap((128 - w) / 2, (64 - h) / 2, logo, w, h, SSD1306_WHITE). Приём, который живёт почти в каждом моём приборе, — сплэш-скрин. После begin() рисуем логотип и зовём display() — экран показывает кадр сам, без участия Arduino, буфер работает как в уроке про буфер и display(). Через две секунды по millis рисуем рабочий экран; кнопка пусть пропускает заставку, если пользователь нетерпеливый.
Офлайн-альтернатива — LCD Assistant: десктопная программа, которой на вход нужен монохромный BMP. Беру её там, где нет интернета; остальное — онлайн, потому что браузерный конвертер обновляется чаще и показывает превью сразу. Оба выдают совместимую горизонтальную раскладку, но проверка байтов из прошлого урока — восемь пикселей в строке, старший бит слева — спасала меня от чужих настроек чаще, чем я готов признать. Для мультиков существуют конвертеры целых GIF в последовательность кадров — механика та же, но вес считается умножением на число кадров, и на Uno такой мультик редко бывает дешевле пары мигающих иконок.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1); // массив из image2cpp: логотип 32x16, режим Horizontal, галка PROGMEMconst unsigned char logo [64] PROGMEM = { 0x00, 0x20, 0x00, 0x80, 0x00, 0x50, 0x01, 0x40, 0x00, 0x50, 0x01, 0x40, 0x00, 0x88, 0x02, 0x20, 0x00, 0x88, 0x02, 0x20, 0x01, 0x04, 0x04, 0x10, 0x01, 0x04, 0x04, 0x10, 0x01, 0xFC, 0x07, 0xF0, 0x03, 0xFE, 0x0F, 0xF8, 0x02, 0x02, 0x08, 0x08, 0x04, 0x01, 0x10, 0x04, 0x04, 0x01, 0x10, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00}; void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); pinMode(2, INPUT_PULLUP); // заставка рисуется один раз, экран держит кадр сам display.clearDisplay(); display.drawBitmap(48, 24, logo, 32, 16, SSD1306_WHITE); // (128-32)/2 display.setTextSize(1); display.setCursor(19, 46); display.print("ARDUINO ARSENAL"); display.display();} void loop() { if (digitalRead(2) == LOW) { // кнопка пропускает заставку display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.print("Rabochii ekran"); display.display(); while (digitalRead(2) == LOW) { // ждём отпускания delay(10); } } // в приборе здесь ещё таймер на millis: через 2 секунды рабочий экран}Порог, ток и живые кадры
Порог — инструмент с характером. Логотипы с тонкими штрихами и мелким текстом ведут себя капризно: при одном пороге исчезает линия, при другом штрих обрастает бородой. Мой порядок такой: сначала готовлю картинку в редакторе — утолщаю линии до двух пикселей, убираю полутени и мелочь, которую всё равно не видно с трёх шагов по считалке размеров из урока про текст, — и только потом кручу порог в конвертере. Полутоновое фото одним порогом обычно разваливается на пятна: для фотографий нужен дизеринг, и это отдельная кухня, а не image2cpp.
Размер я подбираю по месту в интерфейсе: 16х16 для строки дашборда рядом с size(1), 24х24 рядом с крупным значением size(2), 64х32 или 128х64 — только для заставки и справочных экранов. Логика простая: иконка не должна быть выше своей строки, иначе она распирает зоны и ломает раскладку, которую мы собирали в уроке про текст.
Инверсия у меня связана не с красотой, а с током: у OLED каждый горящий пиксель потребляет ток, и залитый белый фон на батарейном узле заметно прожорливее чёрного. Условие честности: на экране 0.96 дюйма речь о единицах миллиампер — на макетке от USB вы этого не увидите, на батарейке за месяц увидите. На экранах покрупнее — 1.3 дюйма и выше — разница ещё заметнее: залитый фон стоит там десятки миллиампер. Поэтому заставки я делаю чёрными с белым рисунком, а инверсию включаю только для коротких плашек, где она оправдана смыслом.
Живые картинки — кадры, как в мультике: массив на кадр, смена по millis из урока про millis вместо delay. Вес кадра берётся из таблицы прошлого урока: полный 128х64 — килобайт, десять кадров — десять КБ флеша, треть памяти Uno; иконок 16х16 по 32 байта можно десятки. На ESP32 флеша десятки мегабайт и ограничение снимается само, но темп всё равно держит шина: около 23 кадров в секунду — потолок, который мы считали в уроке про графику.
- Конвейер: картинка точного размера в редакторе → image2cpp → массив с PROGMEM → drawBitmap
- Порог решает, какой яркости пиксель станет чёрным; тонкие линии готовят в редакторе, а не порогом
- Для Adafruit drawBitmap — режим Horizontal: байт = 8 пикселей строки; Vertical даёт вертикальные полосы
- Сплэш-скрин: логотип по центру x = (128 - w) / 2, показ один раз в setup, пропуск по кнопке
- Залитый фон ест ток OLED — заставки чёрные с белым рисунком; кадры в PROGMEM, десяток полных кадров — треть флеша Uno
- 1. Какой режим вывода в image2cpp нужен для Adafruit drawBitmap?
- 2. На экране вертикальные полосы вместо логотипа. Причина?
- 3. Что делает порог (threshold) в конвертере?
- 4. Сколько байтов даст кадр 128х64?
- 5. Зачем готовить PNG заранее точного размера, а не растягивать в конвертере?
Конвейер картин готов целиком: рисуете в редакторе точного размера, конвертируете с проверенным порогом, вставляете массив в PROGMEM и показываете вызовом drawBitmap. Чужие массивы из интернета теперь читаются на просвет: восемь пикселей в байте, старший бит слева — и кашу видно до прошивки, а не после неё.
Домашнее задание: заставка своего прибора. Картинка 64х32 в редакторе, image2cpp с проверкой превью, массив в скетч, заставка на две секунды с пропуском по кнопке. Когда заставка надоест — пусть переключается из меню и помнит выбор после выключения: для хранения настроек на плате есть EEPROM, а экран после этого урока принимает любую картинку, которую вы сумеете нарисовать. И держите исходники PNG рядом со скетчем: через полгода править картинку будете не по массиву байтов, а по исходнику — конвертер перегенерирует его за минуту, листок в клетку за вечер.
Частые вопросы по теме урока
- Какой режим вывода в image2cpp нужен для Adafruit drawBitmap?
- Adafruit ждёт горизонтальную раскладку: байт — восемь соседних пикселей строки, старший бит слева. Вертикальная — для библиотек другого семейства. Horizontal
- На экране вертикальные полосы вместо логотипа. Причина?
- Массив свёрстан столбцами по восемь пикселей, а рисуется по строкам — картинка перемешивается. Перегенерируйте экспорт в Horizontal. Перепутан режим вывода байтов
- Что делает порог (threshold) в конвертере?
- Порог делит пиксели на чёрные и белые по яркости. Уведёте далеко — исчезнут тонкие штрихи, вернёте — светлое поле зальётся пятнами. Определяет, какой яркости пиксель считать чёрным
- Сколько байтов даст кадр 128х64?
- 8192 пикселя / 8 — 1024 байта, ровно как буфер экрана в ОЗУ. Десять таких кадров — 10 КБ, треть флеша Uno. 1024
- Зачем готовить PNG заранее точного размера, а не растягивать в конвертере?
- Масштабирование силами конвертера съедает детали: линии утонут при сжатии, и никакой порог после этого не поможет собирать картинку заново. Сжатие рубит тонкие линии, порог их потом не вернёт