Контроллер полива в моей мастерской работал безупречно и выглядел как все: клеточный шрифт 6×8, ноль графики, только цифры. Заказчик, привыкший к плеерам и кофемашинам с нарисованными экранами, спросил: «А логотип будет?» Не будет, сказал я, памяти мало. Вечером я посчитал по-настоящему — и вышло, что врал: иконка весит меньше, чем одна строка меню, которую я и так храню.
Собираем фирменный экран целиком: иконка 16×16 в углу, заголовок гарнитурой Free-семейства, рабочие цифры базовым шрифтом. Гарнитуры мы разбирали в уроке про Free-шрифты — там же таблица цен каждого кегля во флеше; механику drawBitmap раскладывали в уроке про битмапы. Сегодня новая часть — посчитать весь комплект в байтах и уложить его в память так, чтобы прибор дожил до следующей доработки.
Спойлер бюджета: иконка 16×16 — 32 байта, FreeSans9pt7b — около 1,8 КБ флеша, кадр целиком битмапом — 1024 байта ОЗУ, половина всей оперативки Uno. Разница между этими тремя цифрами и есть вся дисциплина темы.
Зачем вообще платить за красоту, если цифры и так видны? Мой ответ после нескольких приборов: фирменный экран — это не украшение, а система отличий. Поливальщик с каплей на экране путают с соседским меньше, заголовок гарнитурой читается с двух шагов, и статус прибора узнаётся периферийным зрением, без очков. Цена вопроса — килобайт-два флеша, который иначе сгорит в пустоте, потому что флеш не расходуется сам по себе.
Иконка 16x16: от клетки до PROGMEM
Иконка начинается не с кода, а с клетки: я рисую 16×16 на бумаге или в любом пиксельном редакторе, включая встроенный в конвертер. Дальше превращение в код: картинку загружают в конвертер, выставляют размер, включают вертикальный байтовый порядок — он совпадает с тем, как drawBitmap читает столбики пикселей, — и забирают готовый массив. Процесс по шагам с настройками разобран в уроке про image2cpp; здесь важен итог — 16 строк по два байта, ровно 32 байта на иконку.
Слово PROGMEM перед массивом отправляет эти байты во flash: в оперативке они не живут вообще, а drawBitmap из Adafruit_GFX читает их оттуда сама через pgm_read_byte — ничего дописывать не нужно. Это бесплатно, безусловно и не обсуждается: иконка без PROGMEM съедает 32 байта ОЗУ, а их у Uno и так в обрез. Шрифты устроены так же, только крупнее: это тоже битмапы, подключаются include-заголовком из папки Fonts и включаются setFont; базовая линия у Free-кеглей и пропорциональная ширина — те ловушки, что мы уже проходили, повторять не буду.
Иконку проектируйте под сетку, а не наоборот. Детали мельче пикселя на 16×16 не существуют, диагонали получаются ступенчатыми, и мой первый логотип с округлыми буквами после конвертации рассыпался в зерно. Живут у меня в наборе силуэты с двумя-тремя крупными массами: капля, вентилятор, антенна. Линию держите толщиной от двух пикселей — одиночный столбик в один пиксель на матрице сливается с шумом, и иконка смотрится грязной, хотя байты верные.
Теперь цена. FreeSans9pt7b по заголовкам библиотеки — около 1,8 КБ флеша; тот же рисунок в жирном 24-м кегле — 8143 байта одних битмапов, почти десять килобайт с таблицей глифов. У Uno 32 КБ флеша, скетчу доступны порядка тридцати: один крупный кегль — треть бюджета, второй уже теснит код. Моё правило комплекта: один заголовочный шрифт 9-12 пунктов, одна-две иконки, всё остальное рисует базовый 6×8 с нулевой ценой.
| Элемент экрана | Байты | Где живёт |
|---|---|---|
| иконка 8x8 | 8 байт | PROGMEM — flash |
| иконка 16x16 | 32 байта | PROGMEM — flash |
| кадр 128x64 битмапом | 1024 байта | без PROGMEM — SRAM: половина ОЗУ Uno |
| FreeSans9pt7b | около 1,8 КБ | flash через include из папки Fonts |
| FreeSansBold24pt7b | почти 10 КБ | треть доступных скетчу ~30 КБ flash |
Скетч: иконка в углу, гарнитура в шапке
Разложите кадр по сетке до кода: иконка занимает квадрат 16×16 в левом верхнем углу, заголовок начинается правее с запасом в восемь пикселей, живые цифры живут ниже. Такую раскладку мы чертили в уроке про макет кадра — здесь она спасает особенно: у Free-шрифтов курсор ставится по базовой линии, и заголовок, поставленный наугад, уезжает вверх за стекло. Для 9-го кегля держите y не меньше 13.
Отступ после иконки — не восемь, а двадцать четыре пикселя до текста: квадрат иконки 16 пикселей плюс восемь воздуха. Ближе — и заголовок липнет к картинке, дальше — кадр разваливается на два острова. Шрифт в моём комплекте задаёт и высоту шапки: 9-й кегль живёт в полосе высотой 16 пикселей, и вся верхняя зона кадра — иконка плюс заголовок — укладывается в них без борьбы за пиксели.
В скетче ниже статичная часть — иконка и заголовок — рисуется один раз в setup, а живая строка цифр обновляется в loop только своей зоной: fillRect по границам строки и печать заново. Кадр при этом едет по шине полосой, а не целиком — приём частичного обновления экономит десятки миллисекунд на каждом тике. Строки-подписи держите в flash через F() — разбор макроса из соседнего раздела; в связке с PROGMEM-иконкой весь экран живёт во flash, кроме буфера дисплея.
Координаты комплекта выносите в именованные константы: X_IKONKI, Y_ZAGOLOVKA, Y_STROKA. Числа 0, 16, 24, 26, 32 в четырёх местах рассинхронизируются при первой же правке — я именно так и потерял час на заголовок, наехавший на иконку после смены гарнитуры. С константами переезд на другой кегль — это правка двух строк, а не вылавливание магических чисел по скетчу с фонариком.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>#include <Fonts/FreeSans9pt7b.h> Adafruit_SSD1306 display(128, 64, &Wire, -1); // Ikonka "kaplya" 16x16: 32 bayta, vertikalnyy poryadok bitov// iz konvertera. PROGMEM = flash, v SRAM ne popadaet.const uint8_t PROGMEM kaplya[] = { 0x01, 0x00, 0x41, 0x80, 0x61, 0xC0, 0x71, 0xE0, 0x79, 0xF0, 0x3F, 0xF8, 0x7F, 0xFC, 0x7F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFE, 0x3F, 0xF8, 0x79, 0xF0, 0x41, 0xC0, 0x01, 0x80 }; long tKadr = 0;int vlazhn = 60; void risujStatiku() { // 1. Ikonka v uglu: drawBitmap chitaet PROGMEM sama display.drawBitmap(0, 0, kaplya, 16, 16, SSD1306_WHITE); // 2. Zagolovok garrituroy: kurs po bazovoy linii, y >= 13 display.setFont(&FreeSans9pt7b); display.setTextColor(SSD1306_WHITE); display.setCursor(24, 14); display.print("POLIV"); display.setFont(NULL); // vozvrat k bazovomu 6x8} void risujZnacheniya() { // tochka zhivoy stroyki: fillRect stirayet tolko svoyu polosu display.fillRect(24, 24, 103, 10, SSD1306_BLACK); display.setTextSize(1); display.setFont(NULL); display.setTextColor(SSD1306_WHITE); display.setCursor(24, 26); display.print("Vlazhnost: "); display.print(vlazhn); display.print("%");} void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.clearDisplay(); risujStatiku(); risujZnacheniya(); display.display();} void loop() { long t = millis(); if (t - tKadr >= 500) { tKadr = t; vlazhn = map(analogRead(A0), 0, 1023, 0, 100); risujZnacheniya(); display.display(); } delay(2);}Разбор по слоям. drawBitmap получает PROGMEM-массив и сам вытаскивает байты из flash через pgm_read_byte — никакой ручной пересылки нет. Заголовок живёт с setFont(&FreeSans9pt7b) и возвращается к базовому шрифту setFont(NULL): смена шрифта — это переключатель, а не отдельный слой экрана. Живая зона обновляется заливкой ровно по своей полосе: иконка и заголовок не трогаются месяцами, и перерисовывать их каждый тик — просто лишние миллисекунды на шине.
Итоговый бюджет скетча считайте по таблице выше: иконка 32 байта, FreeSans9pt7b — 1,8 КБ, всё остальное — бесплатный базовый шрифт. Комплект уложился меньше чем в два килобайта флеша — против почти десяти за один кегль 24 пунктов. Когда захочется анимации, каркас кадров из урока про спрайт-ходьбу добавит вторую иконку и таймер на millis — и даже тогда смета останется в пределах трёх килобайт.
И последнее, о чём забывают чаще всего: гарнитура меняет не только вид, но и размеры. Пропорциональные буквы шире клеток 6×8, и строка, влезавшая в 21 колонку, может не влезть в свои пиксели новым шрифтом — центрируйте и обрезайте по getTextBounds, приёмы которого мы разбирали на том же уроке про Free-семейство. Я пробовал подбирать кегли на глаз и вернулся к измерению: две минуты с прямоугольником границ экономят десять перезаливок.
Раскладка в симуляторе настоящая, пусть и нарисованная заглушками: иконка в квадранте 16×16, заголовок с отступом в 24 пикселя, живая строка под ними. Координаты переносятся на плату один в один — замените setTextSize(2) на setFont(&FreeSans9pt7b), строки символов на drawBitmap, и кадр станет тем самым «фирменным». Разница будет видна сразу: у FreeSans глифы пропорциональные, заголовок занимает меньше места, чем те же буквы клетками, а иконка читается картинкой, а не набором символов.
Отдельно про пороги в живой строке: подпись режима «полив» и «ожидание» — это те же иконки, только словами. Как только захотите заменить её картинкой, место под неё уже готово — 16×16 в правом углу строки, ещё 32 байта во flash и одна ветка в отрисовке. Интерфейс наращивается такими приращениями: сначала текст, потом иконка, и прибор не переделывается целиком под каждую новую хотелку.
Если после переноса вместо иконки видны вертикальные полосы — перепутан байтовый порядок в конвертере, это первая проверка; вторая — размеры в вызове drawBitmap обязаны совпадать с размерами картинки, мы это проходили в уроке про битмапы. Третий контроль в моём шаблоне — печать свободной ОЗУ в Serial на старте: буфер дисплея отъедает свою тысячу сразу, и если после добавления комплекта цифра упала ниже 300-400 байт, стеку скоро станет тесно.
В симуляторе ручка A0 гоняет влажность через весь диапазон: следите, как строка режима переключается на пороге, а светодиод повторяет её состояние. Бюджет в подвале кадра — 32 байта иконки и 1800 байт шрифта — написан не для красоты: это те самые цифры из сметы, и на живом приборе я печатаю их при старте, чтобы смета не жила только в моей голове. Прибор, который знает свой бюджет, дорабатывается до конца сезона.
- 1. Сколько байт занимает иконка 16x16?
- 2. Что даёт массиву слово PROGMEM?
- 3. Кадр 128x64 лежит обычным массивом без PROGMEM. Какая доля ОЗУ Uno уйдёт под него?
- 4. Сколько флеша стоит FreeSansBold24pt7b?
- 5. Заголовок Free-шрифтом исчез с экрана после setCursor(0, 0). Виновник?
- Иконка 16x16 — 32 байта: рисуете в клетку, конвертер отдаёт массив, PROGMEM отправляет во flash
- drawBitmap читает PROGMEM сама через pgm_read_byte — пересылать байты вручную не нужно
- Цена гарнитур: FreeSans9pt7b около 1,8 КБ flash, 24-й кегль — почти 10 КБ, базовый 6x8 — ноль
- Кадр 128x64 битмапом без PROGMEM — 1024 байта SRAM: половина ОЗУ Uno плюс тысяча буфера дисплея
- Статичную часть рисуют один раз в setup, живую зону обновляют fillRect по своей полосе
Частые вопросы по теме урока
- Сколько байт занимает иконка 16x16?
- 16 строк по два байта: восемь пикселей упакованы в байт. Кадр целиком 128x64 весил бы уже 1024 байта. 32
- Что даёт массиву слово PROGMEM?
- PROGMEM держит байты во flash; drawBitmap читает их оттуда через pgm_read_byte. Скорость не меняется — меняется место хранения. Байты лежат во flash и не тратят SRAM
- Кадр 128x64 лежит обычным массивом без PROGMEM. Какая доля ОЗУ Uno уйдёт под него?
- 1024 байта SRAM, а рядом ещё столько же держит буфер дисплея. Компилятор предупредит о низкой памяти — это не шутка, а прогноз глюков. Половина — 1024 байта из 2048
- Сколько флеша стоит FreeSansBold24pt7b?
- 8143 байта одних битмапов плюс таблица глифов. Один такой кегль теснит код сильнее, чем весь остальной прибор. Почти 10 КБ — треть доступных скетчу ~30 КБ
- Заголовок Free-шрифтом исчез с экрана после setCursor(0, 0). Виновник?
- Курсор Free-шрифтов ставится по базовой линии, а не по верху клетки. Для 9-го кегля держите y не меньше 13. У Free-шрифтов y — базовая линия, и глифы ушли выше верхнего края