Панель 16x16 я купил после 8x8, будучи уверенным, что просто поменяю две цифры в формуле. Первая же картинка легла «рубашкой»: правая половина зеркалила левую, диагональ изломалась, и полчаса я изучал панель на предмет брака. Брака не было: при переносе с восьми на шестнадцать я поправил множитель ряда, но в нечётной ветке остался старый разворот «7 - x» вместо «15 - x», и каждый нечётный ряд повернулся не туда. Базовую механику змейки, формулу и тест двух пикселей разбирали в уроке про матрицу 8x8 — здесь тот же принцип на 256 пикселях, где перепутанный ряд стоит дороже, а проверок нужно больше.
Зачем вообще шестнадцать на шестнадцать, если восьмёрка уже показывала буквы. 256 пикселей — настоящий экран, где живут часы, погода, змейка и скроллинг на две строки; восьмёрка — витрина, шестнадцатая панель — устройство. Цена входа при этом одна: правильная XY() и десять минут на проверку топологии, и я плачу их при каждой новой панели, потому что даже партии одной модели иногда отличаются направлением ряда — производитель правит подложку, не меняя маркировку. Заодно проверьте панель при покупке: попросите включить её «вполсилы» — на живом серпантине видно, как бегут ряды, и перепутанное направление честно проявляется ещё на прилавке.
План урока. Что меняется вместе с шириной: буфер, ток, время кадра — и где именно в функции сидит ширина. Рисование в координатах: точка и линия через свою XY() с охраной границ. И главный инструмент диагностики — диагональ: одна линия x == y отвечает за всю топологию сразу, по её изломам читается любая путаница с направлениями рядов, от зеркала до колоночной змейки.
От 8x8 к 16x16: что меняется, кроме счёта пикселей
Ширина сидит в XY() в двух местах: множитель y * WIDTH и разворот нечётного ряда (WIDTH - 1 - x). Держите оба за одной константой или за одним числом, вбитым в обе строки, и следите, чтобы они не разъехались. Моя «рубашка» случилась именно из-за разъехавшихся чисел: множитель поправил на шестнадцать, разворот оставил семёркой — чётные ряды легли правильно, нечётные съехали на восемь позиций, и картинка разорвалась зигзагом по каждой второй строке. Это хуже честного зеркала: половина пикселей уезжает в чужие ряды, и глазами такое находишь дольше, чем перевёрнутую картинку.
Буфер растёт вчетверо: 256 пикселей по три байта — 768 байт оперативки, больше трети всей RAM на Uno с её 2048, ещё до шрифтов и Serial-буферов. Дальше ток: 256 пикселей белым на полной яркости — до 15 амперов теоретических, и это уже разговор не про USB, а про отдельный блок 5 В на 10 амперов с инжекцией с двух сторон — механику подачи с нескольких точек считали в уроке про блок 5 В и инжекцию. Для настольной панели я ставлю setBrightness 50-70 и живу в пределах ампера даже на светлых сценах.
Время кадра — третья цифра, о которой на 8x8 можно не думать. show() отдаёт пиксели последовательно, по 30 микросекунд на каждый: 256 пикселей — 7,7 мс только на вывод, как это считать по длине ленты, разобрано в уроке про время кадра. При кадре 20 мс вывод съедает треть бюджета, и эффект с тяжёлой математикой или двойным show() за кадр такая панель не простит. Практический потолок на Uno — 60-70 полных кадров в секунду по одному только выводу, без учёта пересчёта картинки. Отсюда практика: на 16x16 я сразу отказываюсь от кадров быстрее 20 мс — глаз скорости всё равно не различает, а запас на остальную логику остаётся.
#include <FastLED.h>#define LED_PIN 6#define WIDTH 16#define HEIGHT 16#define NUM_LEDS (WIDTH * HEIGHT) CRGB leds[NUM_LEDS]; int XY(int x, int y) { if (x < 0 || y < 0 || x >= WIDTH || y >= HEIGHT) return -1; if (y % 2 == 0) return y * WIDTH + x; // чётный ряд: слева направо return y * WIDTH + (WIDTH - 1 - x); // нечётный: справа налево} void drawPixel(int x, int y, CRGB c) { int i = XY(x, y); if (i >= 0) leds[i] = c; // за границей не рисуем} void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(60); // 256 пикселей: стол, не витрина // тест топологии: две диагонали обязаны пересечься ровным крестом for (int k = 0; k < WIDTH; k++) { drawPixel(k, k, CRGB::Green); drawPixel(k, HEIGHT - 1 - k, CRGB::Blue); } FastLED.show(); delay(2000); // две секунды на осмотр} void loop() { static uint8_t t = 0; static unsigned long last = 0; if (millis() - last < 60) return; // кадр ~16 FPS last = millis(); fadeToBlackBy(leds, NUM_LEDS, 45); // хвост за бегущей линией for (int y = 0; y < HEIGHT; y++) { drawPixel((y + t) % WIDTH, y, CHSV(150, 220, 200)); // наклонная линия } FastLED.show(); t++;}Три функции — и вся матрица. XY() возвращает -1 за пределами экрана, drawPixel() прячет проверку за собой: рисуйте только через неё, «голый» leds[i] оставьте для внутренностей эффектов. Диагонали в setup() — обязательный ритуал при переносе на новую панель: зелёная x == y и синяя x + y == 15 обязаны пересечься в центре ровным крестом. Если хоть одна изломана — дальше не идите, чините XY(): любой сбой топологии диагональ показывает раньше, чем картинка, потому что она пересекает все шестнадцать рядов по одному пикселю.
Линии строятся циклами без арифметики со строками: горизонталь — for по x при фиксированном y, вертикаль — наоборот, наклонная — x = y + сдвиг, как в скетче. Обратите внимание: ни один цикл не знает про змейку — вся укладка живёт в XY(). Это и есть главная выгода координатного подхода: эффекты описываются в координатах, физика проводов — в одном месте, и перенос скетча на панель другой ширины не трогает ни один цикл рисования. Тот же каркас легко удваивается: вторая линия со сдвигом на половину ширины рисуется тем же циклом, и обе живут в одном кадре без взаимных помех.
Хвост за линией держит fadeToBlackBy: каждый кадр вся лента гаснет на 45 из 255, и линия, нарисованная в координатах, оставляет след в номерах ленты автоматически. Смотрите, как мало знает этот эффект: один цикл по y, одно сложение со сдвигом, ноль знаний о змейке. Замените наклонную прямую волной вида x = центр + sin8(y * 20 + t) — получите переливающийся флаг; построение не изменится ни на йоту и на панели 32x32. Вся разница между «просто светящейся лентой» и экраном живёт в трёх строках функции перевода.
Диагональ как тест топологии
В исправном режиме четыре светящихся номера — 0, 5, 10, 15 — идут ровной диагональю, а в ленте выглядят как точки через четыре ячейки. Нажмите кнопку: чётный ряд развернулся, и на диагонали вылезает брешь — пиксель из верхнего ряда уехал в чужой столбец, хотя в координатах код не менялся ни на символ. Это и есть «картинка рубашкой» в миниатюре: на 16x16 тот же сбой рвёт линию на каждом нечётном ряду и зеркалит половину экрана. Проверьте и обратное: после повторного нажатия номера 0, 5, 10, 15 возвращаются — топология чинится одной строкой, и Serial это показывает раньше, чем глаз.
Диагональ ловит любую раскладку, не только перепутанный ряд. Колоночная змейка рвёт её в ступеньку, перевёрнутая панель ведёт её мимо центра, отражённая — ломает на каждой второй строке. Полный набор проверок с рамкой, шахматкой и буквами собирали в уроке про фигуры на матрице; сам я пользуюсь только диагональю — за несколько панелей она не пропустила ни одной путаницы, а на чужих скетчах отвечает за секунду.
Отдельная история — панели, спаянные из блоков: 16x16 часто собирают из четырёх модулей 8x8, и серпантин внутри блока сочетается с обходом блоков, у которого направление своё. Тогда XY() становится двухэтажной: старшие биты координат выбирают блок, младшие — пиксель внутри блока, и внутреннее направление у каждого блока проверяется отдельно. Несколько лент на разных пинах собираются иначе — механика AddLeds разобрана в уроке про несколько лент. Формула усложняется, но принцип не меняется: вся топология по-прежнему в одной функции, и эффекты о ней не знают.
| Параметр | Матрица 8x8 | Матрица 16x16 |
|---|---|---|
| Пиксели и буфер CRGB | 64 пикселя, 192 байта RAM | 256 пикселей, 768 байт из 2048 на Uno |
| Белая заливка на яркости 255 | до 3,8 А | до 15 А: блок 5 В 10 А, инжекция с двух сторон |
| Время show() на 800 кГц | около 1,9 мс | около 7,7 мс — треть кадра 20 мс |
| Правый нижний угол | (7,7) даёт номер 56 | (15,15) даёт номер 240: нечётный ряд идёт справа налево |
Мнение за несколько матричных проектов: для анимаций адресовать пиксели координатами проще, чем номерами ленты. Я перешёл на координаты после змейки: игра из урока про змейку на матрице вся держится на XY() — голова, хвост и еда живут в координатах, и ни один сдвиг ленты их не трогает. Адресовать буфер номерами можно, но каждый эффект таит в себе арифметику рядов, и перенос на панель другой ширины перелопачивает весь скетч, а не одну функцию. Плюс координаты читаются: через полгода в собственном скетче «y = 3, x от 5 до 9» понятнее, чем «ячейки 53-57 с учётом разворота»: координатный индекс сам документирует замысел, номер требует таблицы рядом.
Правило, которое я вывел: эффекты пишут в координатах, физика — в одной функции. Смена панели с 8x8 на 16x16 стоила мне правки двух констант и переподбора темпов — ни один цикл рисования не изменился. Лента без матрицы тоже легальна: радуга одним вызовом и дыхание не знают ни x, ни y, и на длинной линейной ленте они уместнее. Разница всплывает в тот день, когда хочется нарисовать фигуру или двигать точку по горизонтали — и тут координаты окупаются с процентами.
- 1. Пиксель (0, 15) в серпантинной матрице 16x16 — какой у него номер в буфере?
- 2. Что случится с картинкой, если в нечётной ветке XY() остался разворот 7 - x от восьми?
- 3. Сколько байт оперативки занимает буфер 256 пикселей CRGB?
- 4. Почему тест топологии делают именно диагональю?
- 5. Сколько времени занимает show() для 256 пикселей при 800 кГц?
- Ширина сидит в XY() дважды: множитель y * 16 и разворот нечётного ряда 15 - x — держите оба за одной константой
- Буфер 256 пикселей — 768 байт RAM; белая заливка — до 15 А; show() — 7,7 мс: три цифры, которые растут вместе с панелью
- Рисуйте через drawPixel с охраной XY() = -1: индекс за границей портит чужую ячейку буфера
- Диагональ x == y и x + y == 15 — тест топологии: излом ловит перепутанный ряд раньше картинки
- Эффекты — в координатах, физика проводов — в одной XY(): перенос на другую панель не трогает циклы рисования
Частые вопросы по теме урока
- Пиксель (0, 15) в серпантинной матрице 16x16 — какой у него номер в буфере?
- Ряд y = 15 нечётный, идёт справа налево: i = 15*16 + (15 - 0) = 255. Левый край нижнего ряда — самый конец буфера. 255
- Что случится с картинкой, если в нечётной ветке XY() остался разворот 7 - x от восьми?
- Множитель ряда пересчитан на 16, а разворот остался восьмёрочным — половина пикселей уезжает в чужие ряды. Именно так выглядит перенос формулы «вполсилы». Нечётные ряды съедут на восемь позиций: картинка разорвана зигзагом
- Сколько байт оперативки занимает буфер 256 пикселей CRGB?
- Пиксель — три канала по байту: 256 * 3 = 768 байт, больше трети RAM на Uno. Шрифты и палитры в таких проектах уезжают во flash. 768
- Почему тест топологии делают именно диагональю?
- Диагональ x == y проходит по одному пикселю каждого ряда: перепутанное направление, зеркало или колоночная укладка ломают её на первом же ряду. Она пересекает каждый ряд и каждый столбец — любой сбой направления виден изломом
- Сколько времени занимает show() для 256 пикселей при 800 кГц?
- Пиксель отдаётся 30 мкс: 256 * 30 = 7680 мкс. На 8x8 то же самое занимало 1,9 мс — рост панели вчетверо растягивает и вывод. около 7,7 мс