Я хотел зажечь пиксель во второй строке, третьем столбце — и зажёг его соседа где-то на другом краю платы. В коде стоял leds[10] — по моей логике, «строка 1, столбец 2». На матрице вспыхнул совершенно другой угол, и полчаса я изучал плату на предмет брака. Брака не было: матрица из WS2812 спаяна не сеткой, а змейкой, и номер пикселя в буфере связан с координатами (x, y) не напрямую, а через раскладку проводов на подложке. Понять эту раскладку — дело часа. Не понять — гадать каждый вечер, почему «двойка» светит в чужом ряду. Электрическая база при этом та же, что у плоской ленты: резистор в линию данных, конденсатор у входа, общий минус — всё это разобрано в первом уроке раздела, повторять его здесь не буду.
План урока: как из ленты делают экран и почему выбрана змейка; формула, которая переводит (x, y) в номер пикселя; как за минуту определить раскладку незнакомой матрицы; что делать с токами 64 пикселей. Всё проверяем в симуляторе на 64 пикселях: он показывает ленту, но нумерация в ней честная, матричная — увидите сами по Serial.
Змейка: как лента становится экраном
Готовая матрица 8 на 8 — это 64 чипа WS2812B, у которых выход DO каждого соединён с входом DI следующего. Провод данных один, как у ленты, но маршрут по подложке бывает разный. Прогрессивная раскладка: каждый ряд начинается с той же стороны, что и предыдущий, — пиксели 0–7 это верхняя строка слева направо, 8–15 вторая, и так до 63. Серпантин, он же змейка: ряд идёт слева направо, следующий справа налево, потом снова слева — как пашут поле бороной, разворачивая её на каждой борозде.
Производители массовых матриц почти всегда выбирают змейку, и причина экономическая: соединение соседних рядов делается поворотом самого ряда на месте, без длинных перемычек через всю плату. Дорожки тоньше, плата дешевле, пайка надёжнее. Для нас это означает одну вещь: чётные ряды нумеруются слева направо, нечётные — справа налево, и именно эту череду должна воспроизводить функция перевода координат. Увидеть разницу глазами проще всего на заводской матрице: направьте на неё фонарик под углом — повороты змейки видны по изгибам дорожек между рядами.
Формула для серпантина шириной восемь: чётный ряд — i = y * 8 + x, нечётный — i = y * 8 + (7 - x). Пройдём по числам. Пиксель (0, 0) — нулевой, он же самый первый в буфере. (7, 0) — седьмой, правый край верхней строки. (0, 1) — уже пятнадцатый: первый ряд прошёл справа налево, и его «начало» физически стоит справа. (4, 3) — 3 × 8 = 24 плюс 7 - 4 = 3, итого 27. Отсюда и мой конфуз из начала урока: leds[10] — это y = 1, x = 5 — шестой столбец второй строки, а вовсе не то, что я представлял.
Обратите внимание на порядок в записи (x, y): я всюду пишу сначала столбец, потом строку — и держу этот порядок весь проект. Половина путаницы в матричных скетчах — перепутанные местами x и y, а не электрика. И держите в голове длину ряда: восьмёрка в формуле — это ширина вашей матрицы, для 16 на 16 она станет шестнадцатью в двух местах функции.
| Координаты (x, y) | Номер в буфере | Почему |
|---|---|---|
| (0, 0) | 0 | нулевой ряд, начало слева |
| (7, 0) | 7 | нулевой ряд, правый край |
| (0, 1) | 15 | ряд нечётный: прошёл справа налево |
| (4, 3) | 27 | 3 × 8 = 24, плюс 7 − 4 = 3 |
Функция XY(): две строки арифметики
#include <FastLED.h> #define LED_PIN 6#define WIDTH 8#define HEIGHT 8 CRGB leds[WIDTH * HEIGHT]; int XY(int x, int y) { if (x >= WIDTH || y >= HEIGHT) return -1; // за границами не рисуем if (y % 2 == 0) return y * 8 + x; // чётный ряд: слева направо return y * 8 + (7 - x); // нечётный: справа налево} void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, WIDTH * HEIGHT); FastLED.setBrightness(80); int i0 = XY(0, 0); if (i0 >= 0) leds[i0] = CRGB::White; // угловая метка int i1 = XY(1, 0); if (i1 >= 0) leds[i1] = CRGB::Red; // красный справа от белого — раскладка обычная FastLED.show();} void loop() {}Функция занимает три строки, и каждая несёт смысл. Проверка границ возвращает -1 для координат за пределами экрана: вызывайте XY() через промежуточную переменную и рисуйте только при i >= 0. Один пиксель за границей портит чужую ячейку буфера — а на бегущих строках и скроллинге «заэкранье» приезжает регулярно, и мусор с краёв — самая частая жалоба в матричных проектах. Условие y % 2 == 0 — сердце змейки: чётность ряда решает, откуда начинается отсчёт. Прибавка y * 8 — просто длина ряда: восемь пикселей на строку, второй ряд начинается с шестнадцатого.
Циклы, на которых всё это ездит, мы щупали ещё в уроке про циклы for и while, и там же — правило, что переменная цикла и индекс массива — одно и то же число в разных ролях. Здесь оно сработает на полную: внешний цикл по y от 0 до 7, внутренний по x, внутри — leds[XY(x, y)]. Никакой магии, чистая индексация.
Незнакомую матрицу или самодельную спайку тестируют двумя пикселями. Зажгите (0, 0) белым и (1, 0) красным. Белый в левом верхнем углу, красный справа от него — раскладка совпала с формулой, работайте дальше. Красный оказался слева — змейка в этой матрице отражённая: поменяйте x и 7 - x местами в нечётной ветке. Белый вспыхнул внизу — матрица перевёрнута: рисуйте через XY(x, 7 - y), не переворачивая плату в корпусе. Бывает и колоночная змейка — пиксели идут по столбцам сверху вниз; тогда в функции меняются местами x и y целиком, а чётность проверяется у столбца: это ровно та же змейка, просто матрица повёрнута на девяносто градусов.
Три тестовых пикселя — и любая заводская матрица описана за минуту, без чтения форумов и разворотов головой. Я клею на обратную сторону каждой матрицы кусочек малярного скотча и подписываю: «строки, чёт слева» или «колонки, низ слева». Через полгода, когда скетч придётся дорабатывать, подпись окупится минутами жизни и парой нервных клеток.
Смотрите на монитор порта: на чётных рядах x растёт с каждым шагом, на нечётных — убывает, а номер i идёт ровно по порядку, ноль, один, два и далее до шестидесяти трёх. Это и есть змейка, увиденная изнутри буфера. Теперь поменяйте в функции местами x и 7 - x в нечётной ветке — рисунок зеркально отразится, хотя в буфере по-прежнему будет ровно один горящий пиксель. Вся магия и вся свобода матричного проекта живут в этих двух строках: буфер знает только номера, а координаты — договорённость, которую вы контролируете. Перевернётся корпус на бок в готовом проекте — меняете одну формулу, а не весь скетч.
Кадры в скетче идут по таймеру millis() с шагом 250 мс — тот самый каркас из урока про эффекты, и здесь он на своём месте: между шагами плата свободна, и в реальном проекте в этих промежутках живёт остальная логика. Почему сравнение делается вычитанием millis() - lastStep, а метка обновляется внутри if, мы разбирали в уроке про millis() вместо delay() — здесь применяем каркас как готовую деталь. Хотите быстрее — уменьшайте шаг до 50 мс; ограничение одно: show() на 64 пикселях занимает около 1,9 мс, чаще дёргать физически бессмысленно.
И ток — тема, которую на матрице нельзя пропустить. Шестьдесят четыре пикселя белым в полную силу — до 3,8 ампера: это уже не «питание от платы», это отдельный блок на 5 вольт, и про точки подпитки мы считали числа в уроке про питание WS2812. Для настольного индикатора я ставлю setBrightness(60–80) и забываю про силовую часть: белые заливки на такой яркости берут меньше ампера, а радуги и эффекты — в разы меньше. И последняя мелкость в копилку кадров: кадр из 64 пикселей — это 64 × 24 = 1536 бита, на 800 кГц меньше двух миллисекунд, так что даже тридцать кадров в секунду съедают меньше шести процентов времени платы.
- Диагонали: зажгите пиксели, где x == y, синим, а где x + y == 7 — жёлтым. Обе линии должны пересечься в центре; если нет — вернитесь к тесту двух пикселей.
- Рамка: цикл по x зажигает XY(x, 0) и XY(x, 7), цикл по y — XY(0, y) и XY(7, y). Должно получиться ровно 28 пикселей по периметру.
- Шахматка: if ((x + y) % 2 == 0) leds[XY(x, y)] = CRGB::White; — клетки встали в шахматном порядке? Тогда ваша XY() верна.
- Радуга рядами: fill_rainbow(leds, 64, hue, 4) из урока про радугу даст горизонтальные полосы — объясните себе, почему полосы легли перпендикулярно змейке.
- Матрица 8x8 из WS2812 — лента, спаянная змейкой: чётные ряды слева направо, нечётные справа налево
- Формула: чётный ряд i = y * 8 + x, нечётный i = y * 8 + (7 - x); охранная проверка границ обязательна
- Незнакомая матрица тестируется двумя пикселями: (0, 0) белым и (1, 0) красным — направление змейки видно сразу
- Перевёрнутая или колоночная раскладка лечится внутри XY(): замена x на 7 - x или перестановка x и y местами
- 64 пикселя белым — до 3,8 А: отдельный блок 5 В или setBrightness 60–80; кадр уходит в ленту за ~1,9 мс
- 1. Как нумеруются пиксели второго ряда (y = 1) в серпантинной матрице 8x8?
- 2. Какой номер в буфере у пикселя (2, 2)?
- 3. Зачем в XY() проверка x >= WIDTH с возвратом -1?
- 4. Красный тестовый пиксель (1, 0) оказался слева от белого (0, 0). Что делать?
- 5. Сколько тока берёт матрица 8x8, залитая белым на яркость 255?
Координаты больше не врут: (x, y) честно превращаются в номер пикселя, а любая заводская раскладка опознаётся за две тестовые точки. Следующий шаг — рисовать: точка, рамка, линия и цифры из массива байтов — примитивы, из которых складывается и погодный экран, и счёт в самодельной игре. Встречаемся в уроке про фигуры, линии и буквы.
Частые вопросы по теме урока
- Как нумеруются пиксели второго ряда (y = 1) в серпантинной матрице 8x8?
- Ряд нечётный, поэтому змейка идёт справа налево, и его пиксели занимают буферные номера 8–15: номер 8 — правый край. Справа налево, номера 8–15
- Какой номер в буфере у пикселя (2, 2)?
- Ряд чётный: i = 2 × 8 + 2 = 18. Проверьте себя на (4, 3) — там змейка уже развёрнута, и номер 27. 18
- Зачем в XY() проверка x >= WIDTH с возвратом -1?
- Без охраны координаты «за экраном» тихо пишут в соседние ячейки — на скроллинге и бегущих строках мусор с краёв приезжает регулярно. Чтобы пиксель за границей не испортил чужую ячейку буфера
- Красный тестовый пиксель (1, 0) оказался слева от белого (0, 0). Что делать?
- У некоторых матриц направление рядов зеркальное. Правка одной строки в XY() возвращает картинку на место — буфер трогать не нужно. Поменять местами x и 7 - x в нечётной ветке XY() — змейка отражённая
- Сколько тока берёт матрица 8x8, залитая белым на яркость 255?
- 64 пикселя по 60 мА — до 3,84 А. Для такой заливки нужен отдельный блок на 5 В; для настольных эффектов хватает setBrightness 60–80. До 3,8 А