На матрице 8 на 8 я хотел показать уличную температуру — крупной цифрой, чтобы читать с дивана. Открыл скетч и завис: в руках leds[64] и формула XY() из прошлого урока, но как «нарисовать» четвёрку? Пиксель за пикселем вручную — восемнадцать строк на одну цифру, а их десять, плюс точка градуса, плюс минус. Выход оказался скучным и оттого красивым: сначала три примитива — точка, линия, рамка, — а потом цифры как массивы байтов, где каждый бит — клетка экрана. На этих же примитивах потом рисуются и иконки погоды, и змейка, и бегущая строка.
План: собрать мини-графическую библиотеку на пару десятков строк — точку, линию, рамку; разобраться, что делать с диагоналями; упаковать цифры три на пять в массив и переключать их по таймеру кадров. К концу урока матрица станет экраном в полном смысле: вы будете писать координаты, а не номера. И — приятный бонус — весь этот набор переживёт переезд с матрицы 8 на 8 на экран любого размера без единой правки в примитивах: меняется только XY() внутри.
Примитивы поверх XY
Библиотека начинается с одной функции: px(x, y, цвет) — она вызывает XY(), проверяет i >= 0 и пишет в буфер. Всё остальное строится поверх неё. Горизонтальная линия — цикл по x с px(x, y0) внутри. Вертикальная — цикл по y. Рамка — четыре линии по краям, на матрице восемь на восемь это двадцать восемь пикселей. Заливка прямоугольника — вложенные циклы: внешний по строкам, внутренний по столбцам. Ни один примитив не знает про змейку — вся хитрость раскладки спрятана внутри XY(), и это главное правило слоёв: графика не спрашивает у электрики, как проложены дорожки.
Считать пиксели примитивов полезно не ради теории: 28 пикселей рамки на полной яркости белым — это 1,7 ампера, 64 пикселя заливки — 3,8. Бюджет тока у вас уже заведён с прошлых уроков, теперь он считается по картинке ещё до включения. Тот же счёт помогает при отладке: рамка, которая должна занять 28 пикселей, а занимает 26, — это уже диагноз с адресом, а не «что-то не так».
И договоримся об именах раз и навсегда: короткие функции без префиксов — px, ramka, liniya, zalivka. Их будут звать десятки раз на каждый кадр, и строка px(x + 3, y, CRGB::Green) должна читаться с листа, как команда человеку, а не как объявление переменной. Префиксы и длинные имена оставим обработчикам, которые вызываются раз в секунду — у графики другая экономика.
| Примитив | Код в одну строку | Пикселей на матрице 8x8 |
|---|---|---|
| Точка | px(x, y, цвет) | 1 |
| Горизонталь | for по x: px(x, y0) | 8 |
| Рамка по периметру | ramka(0, 0, 7, 7) | 28 |
| Заливка | два вложенных for | 64 |
#include <FastLED.h> #define LED_PIN 6#define W 8#define H 8 CRGB leds[W * H]; int XY(int x, int y) { if (x >= W || y >= H) return -1; if (y % 2 == 0) return y * 8 + x; return y * 8 + (7 - x);} void px(int x, int y, CRGB c) { int i = XY(x, y); if (i >= 0) leds[i] = c;} void ramka(int x0, int y0, int x1, int y1, CRGB c) { for (int x = x0; x <= x1; x++) { px(x, y0, c); px(x, y1, c); } for (int y = y0; y <= y1; y++) { px(x0, y, c); px(x1, y, c); }} void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, W * H); FastLED.setBrightness(90); ramka(0, 0, 7, 7, CRGB::Blue); // периметр, 28 пикселей for (int i = 0; i < 8; i++) px(i, i, CRGB::Red); // диагональ из угла в угол FastLED.show();} void loop() {}В скетче два слоя. Нижний — px() с охраной границ: за пределами экрана рисование тихо игнорируется, и мусор не утекает в чужие ячейки. Верхний — рамка и диагональ: ramka() рисует периметр двумя циклами, диагональ — одним проходом, где x и y растут вместе. Заметьте, сколько строк заняла «графика»: две функции, из которых обе — циклы по знакомым вам правилам. Экран из 64 пикселей прощает наивные реализации, и потому лучший способ научиться — писать примитивы самому: позже, на экране 16 на 16, вы будете понимать каждую строчку своего инструмента, а не лепить чужой код наугад.
Диагональ честно выглядит ступеньками — у экрана без сглаживания нет другого класса точности, и стесняться этого не стоит. Линия под произвольным углом, скажем «две клетки вправо, одна вверх», собирается из if с накоплением погрешности — это уже алгоритм Брезенхэма, и он пригодится, когда экран вырастет до 16 на 16 и ступеньки в два пикселя перестанут нравиться. На восьми рядах хватает диагоналей и ручных маршрутов — я свой первый «крест» так и рисовал, восемью пикселями через центр.
Пока мы считаем память: буфер 64 пикселей — это 64 × 3 = 192 байта RAM, один кадр целиком. На Uno с её двумя килобайтами остаётся достаточно и под шрифт, и под логику, но помните цену каждой заливки: полный белый кадр — 192 байта под цвет плюс 3,8 ампера под питание. Показать кадр — show() — занимает около 1,9 миллисекунды, так что в кадре из 30 мс отрисовка забирает шесть процентов времени, а девяносто четыре остаются вам. Такие числа я держу в голове прежде, чем обещать плате ещё один эффект одновременно.
Цифры и буквы: шрифт из массива
Теперь знаки. Идея шрифта древняя, как сама растровая графика: каждой цифре — свой маленький растр три клетки в ширину, пять в высоту, один бит на клетку. Цифра упаковывается в пять байтов — по одному на строку растра, старший бит слева. Ноль — строки 111, 101, 101, 101, 111, то есть байты 7, 5, 5, 5, 7. Единица — 2, 6, 2, 2, 2. Двойка — 7, 1, 7, 4, 7: три полных строки и две «полки» слева и справа. Чтобы нарисовать знак в точке (x0, y0), идём по пяти строкам, внутри каждой — по трём битам: if ((rowBits >> (2 - c)) & 1) px(x0 + c, y0 + r, цвет). Сдвиг и маска — та же механика, что при разборе байтов в уроке про однопроводной протокол, только биты превращаются в клетки экрана.
Десять цифр — пятьдесят байтов, они спокойно живут в обычном массиве; в скетче я держу шрифт в одномерном массиве на пятьдесят ячеек и достаю строку по индексу d * 5 + r — на реальной плате работает точно так же, а двумерный массив — дело вкуса, компилятор развернёт его в ту же арифметику. Полный шрифт из букв и цифр пять на семь — девяносто пять печатных символов по пять байтов, уже 475 байтов; на Uno это повод вынести шрифт во флеш через PROGMEM, а не съедать оперативку. Пока данных меньше сотни байтов, я их не выношу: экономия копеечная, а код читается хуже. Приёмы работы с массивами мы подробно проходили в уроке про массивы и паттерны.
Счётчик цифр шагает раз в секунду по millis() — каркас кадров из урока про эффекты, который мы уже собирали, здесь работает без переделок: между кадрами плата свободна, и в эти промежутки просится всё остальное — опрос кнопки из урока про подключение кнопки, команды из Serial, чтение датчика. Замените счётчик одной строкой n = millis() / 1000 % 10 — и матрица превратится в секундомер, рисующий последнюю цифру числа секунд с включения. Абстрактные «миллисекунды с включения» становятся видимой вещью, на которую можно смотреть с дивана.
Цифры с этой страницы — готовый дисплей для метеостанции и таймеров. Когда понадобится показывать число из Serial, вспомните урок про числа из порта: пришедшее значение раскладывается на десятки и единицы — d10 = n / 10 и d01 = n % 10 — и каждая рисуется своим глифом в своей колонке, скажем в позициях x0 = 1 и x0 = 5. Две цифры — это два вызова одной и той же функции с разными координатами; для температуры с минусом добавьте третий глиф для черты — растр 0, 0, 0, 0, 7. Так и растут интерфейсы: не новая магия, а старые примитивы в новых позициях.
- Буква А: растр 2, 5, 7, 5, 5 — нарисуйте её рядом с цифрой, в позиции x0 = 5. Буквы отличаются от цифр только массивами.
- Заливка: напишите zalivka(x0, y0, x1, y1, цвет) двумя вложенными циклами и залейте внутренность рамки тускло-серым CRGB(30, 30, 30) — рамка должна остаться синей.
- Секундомер: n = millis() / 1000 % 10 вместо счётчика. Проверьте глазами, что деление и остаток дают ровно последнюю цифру.
- Лестница: нарисуйте маршрут из левого нижнего угла в правый верхний: две клетки вправо, одна вверх. Пять проверок if — и у вас свой диагональный стиль.
- Вся графика начинается с px(x, y): функция прячет XY() и охрану границ — примитивы не знают про змейку
- Рамка 8x8 — 28 пикселей, горизонталь — 8, заливка — 64: считайте пиксели, чтобы прикидывать ток до включения
- Диагонали на экране из 64 точек — честные ступеньки; произвольные углы — алгоритм Брезенхэма, когда матрица вырастет
- Цифра 3x5 — пять байтов, бит на клетку: (rowBits >> (2 - c)) & 1 решает, светить ли клетку
- 10 цифр — 50 байтов в RAM; полный шрифт 5x7 — 475 байтов и повод вынести его в PROGMEM на Uno
- 1. Сколько пикселей занимает рамка по периметру матрицы 8x8?
- 2. Сколько байтов занимает одна цифра растра 3x5?
- 3. Что означает байт 5 в строке растра?
- 4. Зачем PROGMEM на Uno при большом шрифте?
- 5. Как выглядит диагональ на матрице без сглаживания?
У вас в руках мини-графическая библиотека: точка, линия, рамка и шрифт из массива — пара десятков строк, которые переживают переезд на любой экран. Дальше по разделу она вырастет в бегущую строку со шрифтом пять на семь, а потом — в змейку, где примитивы рисуют поле, а счёт выводится цифрами с этой страницы. Соберите секундомер из экспериментов — это первый экран, который вы нарисовали целиком сами: от формулы XY до последнего бита.
Частые вопросы по теме урока
- Сколько пикселей занимает рамка по периметру матрицы 8x8?
- Две горизонтальные линии по 8 плюс две вертикальные по 8, минус четыре угла, посчитанные дважды: 8 + 8 + 6 + 6 = 28. 28
- Сколько байтов занимает одна цифра растра 3x5?
- По одному байту на строку растра — пять строк, пять байтов. Биты внутри байта — это клетки строки. 5
- Что означает байт 5 в строке растра?
- 5 — это 101 в двоичном виде: крайние клетки строки горят, средняя нет. Каждый бит — одна клетка экрана. Двоичное 101: светят первая и третья клетка
- Зачем PROGMEM на Uno при большом шрифте?
- У Uno два килобайта RAM и 32 килобайта флеша. Шрифт на 475 байтов разумно держать во флеше и читать через pgm_read_byte_near. Хранит шрифт во флеш-памяти, не занимая оперативку
- Как выглядит диагональ на матрице без сглаживания?
- Пиксель — минимальная единица, поэтому диагональ — ступеньки из клеток. Брезенхэм понадобится на больших экранах для произвольных углов. Лестницей по клеткам: x и y растут вместе