Ёлку из адресной ленты я собирал третий год подряд, и первый вариант был самым бесполезным: одна радуга на весь вечер. Красиво в первый час, потом глаз устал, а переключать нечем. Второй вариант я перегрузил — пять эффектов, пульт, меню — и понял, что за ужином никто, включая меня, не хочет листать меню на ленте. Рабочая формула нашлась сама: три сцены и одна кнопка. Переливы для фона, звёзды для тишины, вспышки для праздника — и щелчок ручкой листает их по кругу, как канал на старом телевизоре.
Всё, что нужно для этих сцен, в курсе уже было: радуга из fill_rainbow — в уроке про fill_rainbow и параметр hue, случайные вспышки на затухающей ленте — в уроке про искры и random8. Новое здесь — каркас: как уложить три эффекта в один скетч так, чтобы кнопка листала их мгновенно и ни одна сцена не мешала другой. Правило каркаса одно и звучит оно жёстко: ни одной delay() на весь скетч — только millis().
И сразу про количество. Три сцены — не округлённая эстетика, а проверенный потолок: на четвёртой кнопка уже требует подписи, а на пятой гости начинают листать сами, без спроса. Если ваши сценарии не влезают в три — это повод сделать две ёлки, а не меню.
Сцена 1: переливы
Фоновая сцена — та самая радуга, которую глаз не выталкивает из комнаты: fill_rainbow(leds, 16, hue, 16) с шагом оттенка 16 на пиксель даёт один полный цветовой круг на всю ленту, а счётчик hue растёт на единицу каждый кадр. При кадре в 40 миллисекунд полный оборот из 256 оттенков занимает около десяти секунд — медленно, лениво, по-новогоднему. Ускорьте вчетверо, и переливы превратятся в дискотеку: темп здесь решает больше, чем цвета.
На живой плате вращение счётчика удобно отдать макросу EVERY_N_MILLISECONDS(40) { hue++; } — он пропускает свой блок раз в сорок миллисекунд и избавляет от ручной метки millis(); внутри у него тот самый таймер, никакого волшебства. В симуляторе ниже тот же ход собран явным if с millis() — движок страницы работает на нём же, так что логика на экране и на плате совпадает один в один. Кадровый каркас мы строили целиком в уроке про эффекты и millis-кадры — если он у вас ещё не отскочил от зубов, начните с него.
Сцена 2: звёзды
Звёзды — сцена для темноты, и делается она из двух действий, порядок которых не меняется. Первое: каждый кадр вся лента чуть темнеет — fadeToBlackBy(leds, 16, 20) срезает примерно двенадцатую часть яркости, и всё, что горело, тает за секунду-полторы. Второе: изредка, примерно в каждом четвёртом кадре, один случайный пиксель вспыхивает тёплым белым CHSV(35, 40, random(120, 255)) — насыщенность сорок, то есть почти белый с каплей тепла, яркость случайная от половины до полной. При кадре 40 мс это десять вспышек в секунду — небо живое, но не нервное.
Случайные числа здесь — суть эффекта, а не грязный хак: ровные звёзды на равных интервалах выглядят как сортировка, живые — как небо. Движок симулятора умеет обычный random(), и в учебном коде его хватает; на плате для вспышек лучше родной random8() библиотеки — он быстрее и заточен под диапазоны 0-255, вся механика разложена в уроке про random8 и затухание. Разница на шестнадцати пикселях незаметна, на шестистах — уже да.
Сцена 3: гирлянда-вспышки
Третья сцена — прямое цитирование уличной гирлянды из детства: вся лента вспыхивает одним цветом целиком, держит полторы сотни миллисекунд, гаснет на сто, и вспыхивает следующим. Цвета у меня три: красный CRGB(255, 60, 0), золотой CRGB(255, 190, 60) и зелёный CRGB(60, 255, 80) — по кругу. Обратите внимание, что ни один канал не выкручен в полный белый: шестнадцать пикселей белым на полной яркости просят до 960 мА, а с приглушёнными каналами и общей яркостью 150 через setBrightness бюджет падает в разы. Полная арифметика тока — в уроке про setBrightness и бюджет питания; вспышки — самое токовое место всего кластера.
Механика сцены — два таймера вместо одного: флаг «сейчас вспышка или пауза» и метка lastFlash. Прошло 150 мс во вспышке — гаснем; прошло 100 мс в темноте — зажигаем следующий цвет. Ужать паузу до нуля можно, но не стоит: стык «вспышка — тьма — вспышка» и делает сцену гирляндой, без тьмы она превращается в быстрое листание цветов.
Четвёртая сцена добавляется в такой каркас тремя движениями: новый if с телом эффекта, правка потолка в обработчике кнопки и одна строка Serial. Хочется «дыхание» тёплым белым для засыпания — заведите счётчик яркости v, который ходит туда-обратно, и заливайте ленту CHSV(35, 80, v): кадр, кнопка и тишина между ними разделены с самого начала. Единственная ловушка — забыть поправить диапазон: scene больше потолка — и кнопка проматывает пустой номер, а скетч честно рисует тьму.
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 16#define BTN_PIN 2 CRGB leds[NUM_LEDS];int scene = 0; // 0 переливы, 1 звёзды, 2 вспышкиint hue = 0;int flashOn = 0; // во вспышках: 1 светим, 0 паузаint flashColor = 0;unsigned long lastFrame = 0;unsigned long lastFlash = 0;unsigned long lastEdge = 0;int lastBtn = HIGH; void drawFrame() { if (scene == 0) { // переливы fill_rainbow(leds, NUM_LEDS, hue, 16); hue += 1; } if (scene == 1) { // звёзды fadeToBlackBy(leds, NUM_LEDS, 20); if (random8() < 64) { // примерно каждый четвёртый кадр leds[random8(NUM_LEDS)] = CHSV(35, 40, random8(120, 255)); } } // Сцена 2 (вспышки) буфером управляет прямо loop(): цвет заливается в момент // старта вспышки, а кадр лишь выталкивает его show() — так цвета не перезатираются.} void setup() { pinMode(BTN_PIN, INPUT_PULLUP); FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(150); Serial.begin(9600); Serial.println("scena=0 perelivy");} void loop() { int btn = digitalRead(BTN_PIN); if (btn != lastBtn && millis() - lastEdge > 50) { // фронт с антидребезгом lastEdge = millis(); lastBtn = btn; if (btn == LOW) { scene += 1; if (scene > 2) { scene = 0; } FastLED.clear(); FastLED.show(); Serial.print("scena="); Serial.println(scene); } } if (scene == 2) { // темп вспышек на millis if (flashOn == 0 && millis() - lastFlash >= 100) { flashOn = 1; lastFlash = millis(); if (flashColor == 0) { fill_solid(leds, NUM_LEDS, CRGB(255, 60, 0)); } if (flashColor == 1) { fill_solid(leds, NUM_LEDS, CRGB(255, 190, 60)); } if (flashColor == 2) { fill_solid(leds, NUM_LEDS, CRGB(60, 255, 80)); } flashColor += 1; if (flashColor > 2) { flashColor = 0; } } if (flashOn == 1 && millis() - lastFlash >= 150) { flashOn = 0; lastFlash = millis(); FastLED.clear(); } } if (millis() - lastFrame >= 40) { // общий кадр 40 мс lastFrame = millis(); drawFrame(); FastLED.show(); }}Кнопка читается по фронту: сравнение с прошлым состоянием плюс гейт 50 миллисекунд против дребезга — та же механика, что в уроке про кнопку и digitalRead. Листает только нажатие, отпускание игнорируется, и сцена меняется мгновенно: clear() затирает буфер, show() гасит ленту прямо в момент щелчка, а следующий кадр уже рисует новую сцену. Память режима здесь сознательно не нужна — ёлка живёт один вечер, и включаться она обязана с переливов, как телевизор с первого канала. Совсем другое дело — пульт гирлянды, который должен переживать выдёргивание вилки: различение клика и удержания, EEPROM и валидация сохранённого режима разобраны в уроке про кнопочный пульт — там кнопка стала сложнее, здесь она осталась щелчком.
По току у сцен разный характер, и это стоит видеть глазами таблицы: переливы и звёзды — ленивые потребители, вспышки — пиковая нагрузка, которая при жадных цветах способна просадить питание и устроить снег из артефактов. Отсюда и приглушённые каналы в цветах вспышек, и общий потолок яркости 150: сцена обязана выглядеть празднично даже на половине мощности.
Порядок сцен я закрепил после одного ужина: переливы первыми, звёзды вторыми, вспышки третьими. Дело не в эстетике, а в привычке: при включении ёлка всегда стартует со спокойной сцены, а праздник включается щелчком, когда гости уже за столом. Менять местами можно, но это решение о характере комнаты, а не о коде — скетчу все три сцены равнозначны, он просто листает по кругу.
Дальше по кластеру осталась одна ручка без упоров и без памяти: энкодер. Кнопка листает дискретно, потенциометр знает только своё положение, а лампа следующего урока крутится бесконечно — по часовой ярче, против тише, нажатие ручки меняет тепло на холод.
| Сцена | Механика | Темп | Что покрутить |
|---|---|---|---|
| Переливы | fill_rainbow + hue++ | кадр 40 мс, оборот около 10 с | дельту 16 и длительность кадра |
| Звёзды | fadeToBlackBy 20 + редкие вспышки | кадр 40 мс, около 10 вспышек в секунду | вероятность вспышки и яркость |
| Вспышки | fill_solid по кругу цветов | 150 мс свет + 100 мс тьма | цвета и обе длительности |
| Переключение | кнопка D2 по фронту | гейт 50 мс | ничего — работает как есть |
- Три сцены — три случая в каркасе: переливы, звёзды, вспышки; кнопка листает по кругу
- Переливы: fill_rainbow(leds, 16, hue, 16) и hue++ каждые 40 мс — оборот около 10 секунд
- Звёзды: сначала fadeToBlackBy, потом редкая случайная вспышка — порядок действий и есть эффект
- Вспышки: два таймера на millis (150 мс света + 100 мс тьмы), цвета с приглушёнными каналами ради тока
- Кнопка читается по фронту с гейтом 50 мс; delay() в сценах убивает отклик кнопки
- 1. Как в скетче ёлки устроено движение переливов?
- 2. Из чего собирается сцена «звёзды»?
- 3. Почему в вспышках стоят цвета вроде CRGB(255, 60, 0), а не чистый белый?
- 4. Что случится с листанием сцен, если паузы поставить через delay()?
- 5. Сколько длится полный оборот переливов при кадре 40 мс и шаге hue 1?
Частые вопросы по теме урока
- Как в скетче ёлки устроено движение переливов?
- fill_rainbow рисует радугу от стартового оттенка, а живость даёт ручной счётчик: hue += 1 в каждом кадре по 40 мс. Один байт-счётчик — и круг замыкается сам. Счётчик hue растёт на единицу каждый кадр
- Из чего собирается сцена «звёзды»?
- Порядок двух действий и есть эффект: каждый кадр вся лента чуть темнеет, и поверх тающего фона изредка вспыхивает случайный пиксель тёплым белым. Из затухания fadeToBlackBy и редких случайных вспышек
- Почему в вспышках стоят цвета вроде CRGB(255, 60, 0), а не чистый белый?
- Белый зажигает все три кристалла — 60 мА на пиксель, до 960 мА на шестнадцати. Приглушённые каналы и потолок setBrightness 150 держат бюджет вспышек в разумных пределах. Полный белый на 16 пикселях просит до 960 мА — самый токовый цвет
- Что случится с листанием сцен, если паузы поставить через delay()?
- delay() блокирует весь цикл: digitalRead кнопки просто не выполняется. Все паузы каркаса держатся на millis() — loop крутится непрерывно, кнопка отвечает мгновенно, а «пауза» — это условие, которое ещё не дозрело. Кнопка перестанет отзываться: delay останавливает весь loop() и щелчки теряются
- Сколько длится полный оборот переливов при кадре 40 мс и шаге hue 1?
- Оттенок хранится в байте: полный круг — 256 шагов, 256 × 40 мс = около 10,2 секунды. Хотите медленнее — режьте шаг или растягивайте кадр. Около десяти секунд