Гирлянда на кухонном окне должна была выглядеть как угорёк, который бежит по проводу: яркая голова, за ней пара тёплых пикселей, и всё это ползёт по кругу. В первом скетче вместо этого лента разом заливалась одним цветом целиком — ровно так, будто я вызвал fill_solid. Код при этом выглядел разумно: сдвинуть каждый пиксель на место соседа, в голову записать новый цвет, показать кадр. Я протрассировал цикл на бумаге и нашёл виновника за минуту: я копировал пиксели в прямом порядке, и каждый копировал уже скопированное. Об этом тупике рассказываю первым делом, потому что на нём ломается каждый второй первый «бегущий огонь» — и на бумаге он чинится за две минуты.
В этом уроке соберём движение честным способом — сдвигом буфера: один цикл for по ленте и одна запись головы. Разберём, почему порядок обхода решает всё, как пустить огонёк в обратную сторону, как замкнуть его по кругу без обрыва на краю и как собрать хвост из нескольких пикселей без единого вызова затухания. Движение — вторая половина навыка ленты: рисовать статичные картинки вы уже умеете после урока про подключение WS2812 и первые пиксели, здесь научим эти картинки ездить.
Сдвиг: движение — это копирование соседа
Лента физически ничего не «двигает»: каждый чип в каждый момент просто показывает цвет, который лежит в его ячейке буфера leds[]. Движение собирается руками, и весь его механизм — копирование: раз в шаг каждый пиксель забирает цвет соседа, а на освободившееся место головы приходит новый цвет. Наглядно: буфер — очередь из шестнадцати стульев, в такт таймеру каждый садится на место соседа, а на первый стул заводят новенького. Со стороны ленты это выглядит как бегущий огонёк, хотя по сути это очередь, которая едет.
Кадр здесь — не киношные 30 мс, а шаг движения, и его темп задаёт таймер на millis(): if (millis() - lastStep >= 200) — шаг, внутри шага сдвиг и отрисовка. Цифры для калибровки: шаг 200 мс на ленте из 16 пикселей даёт полный круг за 3,2 секунды — неспешный угорёк; шаг 50 мс — круг за 0,8 секунды, уже суета; шаг 400 мс — медленный дозор, красиво для ночника. Обе крайности крутятся одной константой, и в этом плюс дискретного шага: скорость эффекта — это буквально одно число в коде. Сам каркас таймера тот же, что в уроке про эффекты на FastLED, просто внутри шага вместо перерисовки всей картинки — сдвиг.
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 16 CRGB leds[NUM_LEDS];unsigned long lastStep = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(120);} void loop() { if (millis() - lastStep >= 200) { lastStep = millis(); // сдвиг: каждый пиксель забирает цвет предыдущего, // обход с конца — чтобы не копировать уже скопированное for (int i = NUM_LEDS - 1; i > 0; i--) { leds[i] = leds[i - 1]; } // новая голова: тёплый оранжевый leds[0] = CHSV(28, 255, 255); FastLED.show(); }}Вся соль урока — направление обхода в этом цикле. Мы идём с конца: i от 15 вниз до 1, и каждый leds[i] забирает цвет leds[i - 1] ДО того, как тот сам будет перезаписан. Разверните цикл — и получите мой новогодний результат: leds[1] заберёт цвет головы, потом leds[2] заберёт уже не старый leds[1], а свежую копию головы, и по цепочке весь буфер заполнится одним цветом за один проход. Лента честно покажет сплошную полосу — формально программа работает, компилятор молчит, а движения нет. Правило, которое я выбил себе с тех пор: сдвиг вверх — обход с конца, сдвиг вниз — обход с начала; любую новую версию цикла сначала прогоняю на бумаге по пяти ячейкам. Про сами циклы и порядок итераций есть отдельный разбор в уроке про циклы for и while.
Скорость и характер огонька живут в двух местах. Константа таймера задаёт темп — 200 мс, 80 мс, 300 мс, под настроение. Строка головы задаёт цвет: CHSV(28, 255, 255) — оранжевый; оттенок 160 сделает огонёк синим, 96 — зелёным, вся мнемоника круга оттенков из урока про HSV-цвета работает один в один. Заведите byte hue, прибавляйте единицу на каждом шаге и подставляйте его в голову — цвет начнёт медленно дрейфовать по кругу оттенков, и бегущий огонь станет переливаться. Планировщик шага взят готовым из урока про millis() вместо delay() — внутри шага можно безнаказанно гонять цикл по шестнадцати пикселям: шестнадцать копирований трёх байтов — это единицы микросекунд на частоте 16 МГц.
| Способ движения | Что в кадре | Хвост | Когда брать |
|---|---|---|---|
| Сдвиг буфера | цикл-копирование соседа плюс новая голова | жёсткий, цифровой: пиксели едут с той яркостью, с какой записаны | бегущие огни, змейки, вращение узора |
| Затухание fadeToBlackBy | гасим всю ленту на долю и рисуем голову заново | плавный, тающий по экспоненте | кометы, искры, следы движения |
| Перерисовка через clear | FastLED.clear() и полная отрисовка картинки с нуля | нет вообще | статичные кадры, шкалы, текст и цифры |
Круг, обратный ход и хвост из буфера
Прямой прогон из скетча выше теряет голову на краю: пиксель доехал до последнего места и исчез, через мгновение вспыхнул снова в начале — для гирлянды рвано. Кольцо делается сохранением одного значения: перед сдвигом запоминаем цвет крайнего пикселя в CRGB tmp = leds[15], после сдвига записываем его в голову — leds[0] = tmp. Всё содержимое буфера теперь ходит по кругу бесконечно, и открывается второй уровень навыка: узор рисуется один раз в setup(), а loop() только вращает его. Именно так устроен симулятор выше: три записи огонька в setup — и дальше ни одной перерисовки цвета, только чистый сдвиг и show(). Этот же приём называется кольцевым буфером, и в больших проектах на сотни пикселей он экономит и время, и нервы.
Обратный ход зеркалит цикл: сдвигаем вниз — for (int i = 0; i < 15; i++) leds[i] = leds[i + 1], голову пишем в leds[15], и огонёк бежит в противоположную сторону. Две головы навстречу — это два сдвига в одном шаге, каждый со своей стороны, а что останется после их столкновения, честно проверьте руками в экспериментах. И заметьте цену вопроса: даже на ленте в сотню пикселей сдвиг не станет узким местом — узким местом остаётся сама передача по проводу, по 30 микросекунд на пиксель при show(), те же полмиллисекунды на шестнадцать пикселей, что и в уроке про подключение. Анимация движением почти бесплатна, платим только честным порядком обхода. Скорость, кстати, можно сделать живой константой: держите шаг не в литерале, а в переменной — потенциометр с map() превратит её в ручку темпа, и одна и та же змейка будет ходить дозором на 400 мс и носиться на 40 мс: на метре 60-пиксельной ленты это круг за 24 секунды против 2,4 секунды.
Хвост без затухания собирается из самих сдвинутых пикселей: всё, что вы записали в голову в предыдущие шаги, сейчас едет по ленте. Огонёк в симуляторе — это три записи в setup(): голова CHSV(28, 255, 255), за ней CHSV(16, 255, 140) и CHSV(8, 255, 70) — один оттенок с убывающей яркостью, и сдвиг сам расставил их в хвост. Хотите длиннее — записывайте пять или восемь пикселей с градацией; хотите пульсирующий — меняйте яркость головы по счётчику шагов, и волна яркости поедет следом за огоньком. Двухцветный вариант смотрится объёмнее монохромного: оранжевое ядро CHSV(28, ...) с ободком CHSV(160, 180, 60) — тёплый центр в холодной кайме, пять записей вместо трёх. В этом разница с тающим хвостом: сдвиг хранит яркость, а не гасит её — след не угаснет, пока не выедет за круг. Когда понадобится след, который именно тает, — это fadeToBlackBy, ему посвящён третий урок этого пакета, и там же разберём затухание поверх сдвига.
Три эксперимента на закрепление — все решаются в симуляторе выше за десять минут, паяльник не нужен.
- Пустите огонёк назад: сдвиг вниз с обходом от 0 к 14 и голова в leds[15]. Одна правка цикла — и направление сменилось.
- Соберите поезд из пяти пикселей с убывающей яркостью и прогоните его по кольцу: пять записей CHSV в setup() вместо трёх.
- Запустите два огонька навстречу: два сдвига в одном шаге, головы на leds[0] и leds[15]. Посмотрите, что останется после столкновения — результат честно удивит.
- Заведите byte hue и прибавляйте его на каждом шаге: цвет головы начнёт дрейфовать по кругу оттенков без единого RGB-числа в коде.
- 1. Почему цикл сдвига вверх пишут с конца — i от 15 к 1?
- 2. Шаг сдвига 200 мс, лента 16 пикселей. За сколько огонёк сделает полный круг?
- 3. Что сохраняет переменная tmp в кольцевом сдвиге?
- 4. Какой хвост даёт чистый сдвиг без затухания?
- 5. Голова задана строкой CHSV(28, 255, 255). Какая строка перекрасит её в синий?
- Движение — это сдвиг буфера: цикл-копирование соседа плюс новая запись в голову.
- Направление обхода — судьба: сдвиг вверх — цикл с конца, сдвиг вниз — с начала; иначе лента заливается одним цветом.
- Кольцо — одно сохранение: tmp до сдвига, запись в голову после; узор рисуется один раз в setup().
- Скорость — константа таймера: шаг 200 мс на 16 пикселях даёт круг за 3,2 с.
- Хвост без затухания — записи головы прошлых шагов: яркость едет по ленте, а не тает.
Частые вопросы по теме урока
- Почему цикл сдвига вверх пишут с конца — i от 15 к 1?
- При обходе с начала leds[1] уже перезаписан, когда очередь доходит до leds[2] — копии головы плодятся по цепочке. Обход с конца читает ещё нетронутых соседей. При прямом порядке каждый пиксель скопирует уже обновлённого соседа, и весь буфер станет одним цветом
- Шаг сдвига 200 мс, лента 16 пикселей. За сколько огонёк сделает полный круг?
- 16 шагов по 200 мс — это 3200 мс. Шаг 50 мс дал бы 0,8 секунды: скорость движения всегда равна шагу таймера, умноженному на длину ленты. 3,2 секунды
- Что сохраняет переменная tmp в кольцевом сдвиге?
- Крайний пиксель первым попадает под перезапись, поэтому его цвет спасают во временную переменную до цикла и возвращают в голову после. Цвет крайнего пикселя, который иначе уехал бы за круг
- Какой хвост даёт чистый сдвиг без затухания?
- Сдвиг ничего не делает с яркостью — он только переставляет значения. Тающий след даёт fadeToBlackBy поверх сдвига, это отдельный приём. Стабильный: пиксели едут с той яркостью, с какой были записаны
- Голова задана строкой CHSV(28, 255, 255). Какая строка перекрасит её в синий?
- 160 — синий сектор круга оттенков. Второй вариант оставит оттенок, но обнулит насыщенность и даст белёсый цвет, третий подставит 28 в красный канал RGB, четвёртый убьёт яркость. CHSV(160, 255, 255)