Подсветку полки я раскрашивал градиентом циклом: тридцать пикселей, в каждом — ColorFromPalette с индексом, умноженным на шаг, и всё это на кадр. Работало, но пять строк на то, что по смыслу одно действие, раздражали с каждым новым эффектом: в каждой сцене гирлянды этот цикл приходилось копировать, и однажды в одной из копий я оставил чужой шаг — градиент поехал со сбитой частотой, и я полчаса искал, куда делся мой плавный перелив.
Вызов, который заменяет весь этот цикл, — fill_palette: одна строка красит всю ленту из палитры, да ещё и с готовыми ручками движения и растяжения. Под капотом там тот же ColorFromPalette с линейным смешиванием — механика палитр и индексов 0–255 уже разобрана в уроке про CRGBPalette16, — но вызов принимает сразу две координаты: с какой позиции круга начать и на какой шаг сдвигаться от пикселя к пикселю.
Здесь разберём ровно эти ручки: как из startIndex сделать движение градиента по ленте, как deltaHue растягивает одну прокрутку на всю ленту или режет её на повторы, и как устроить автосмену палитр по таймеру так, чтобы глаз не ловил щелчок. Опорные цвета для своих палитр берутся из урока про свой градиент, готовые наборы — из урока про PartyColors, а каркас кадров по-прежнему стоит на millis() из урока про millis вместо delay.
Разбор вызова: startIndex двигает, deltaHue растягивает
Сигнатура на пальцах: fill_palette(leds, NUM_LEDS, startIndex, deltaHue, палитра, яркость, LINEARBLEND). Первый пиксель берёт цвет из позиции startIndex круга палитры, каждый следующий — на deltaHue дальше по кругу. Отсюда два следствия, которые стоит проговорить вслух. При deltaHue = 256/N (для тридцати пикселей это восемь-девять) градиент ложится ровно один раз на всю ленту: нулевой и последний пиксель — почти один цвет, стык замаскирован. При deltaHue больше — на ленте возникает несколько прокруток: полосатость, частота которых растёт линейно, 16 для тридцати пикселей — уже два повтора.
Движение — самое приятное следствие формы вызова. Пока startIndex растёт между кадрами, вся картина едет по ленте без единого пересчёта координат: приращение на единицу каждые 40 мс даёт неспешный перелив, на два-три — оживлённый. Направление меняется знаком шага, а переполнение байта на 256 здесь не грабля, а фича — круг палитры замкнут, и старт заворачивается сам. Яркость шестым аргументом — отдельная ручка поверх общего FastLED.setBrightness: она красит именно этот кадр, и мерцать сценой можно, не трогая глобальный потолок.
Смена палитр — отдельная история. В лоб, за один кадр, старую картинку не заменяют без щелчка: глаз ловит мгновенную подмену, как ловил её в сценах гирлянды до кроссфейда. Каноничное решение — пара «текущая и целевая» палитра с плавным переездом: механика переезда с лимитом байт за вызов уже собрана в уроке про кроссфейд палитр, здесь её не повторяю. Ключевая мысль для fill_palette: эффект рисует только currentPalette, а все источники целей — таймер, кнопка, пульт — пишут лишь в targetPalette, и смена на рисование не влияет.
Про LINEARBLEND стоит понимать буквально: шестнадцать опорных цветов внутри палитры связаны пятнадцатью отрезками, и на каждом отрезке канал красного, зелёного и синего едет линейно от опоры к опоре. Режим NOBLEND отдаёт ближайшую опору — картинка распадается на шестнадцать ступеней, что на тридцати пикселях заметно глазу. Я один раз отлаживал «полосатую» палитру часа два, пока не увидел, что забыл флаг смешивания в вызове.
#include <FastLED.h>#define LED_PIN 6#define NUM_LEDS 30 CRGB leds[NUM_LEDS]; // градиентные палитры: пары «позиция 0-255, r, g, b», живут во flashDEFINE_GRADIENT_PALETTE( sunset_gp ) { 0, 255, 90, 20, 64, 255, 160, 40,128, 200, 60, 90,192, 90, 20, 120,255, 40, 10, 90};DEFINE_GRADIENT_PALETTE( aurora_gp ) { 0, 0, 40, 120, 64, 30, 120, 190,128, 90, 200, 170,192, 160, 230, 140,255, 240, 250, 160}; CRGBPalette16 currentPalette = sunset_gp;CRGBPalette16 targetPalette = aurora_gp;uint8_t startIndex = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(170);} void loop() { EVERY_N_MILLISECONDS(40) { startIndex++; } // движение градиента EVERY_N_MILLISECONDS(100) { // переезд к цели по чуть-чуть nblendPaletteTowardPalette(currentPalette, targetPalette, 24); } EVERY_N_SECONDS(9) { // автосмена: таймер пишет цель static bool pickAurora = true; targetPalette = pickAurora ? aurora_gp : sunset_gp; pickAurora = !pickAurora; } // deltaHue = 256 / NUM_LEDS -> один градиент на всю ленту fill_palette(leds, NUM_LEDS, startIndex, 8, currentPalette, 200, LINEARBLEND); FastLED.show();}Три грабли и цена памяти
Три места, где я платил за невнимательность. Первое — растяжение вслепую: deltaHue подбирается под длину ленты, и перенос скетча с тридцати пикселей на шестьдесят без правки шага незаметно удваивает число прокруток — сцена «незаметно» превращается в полосатую. Держите рядом с вызовом комментарий, на какую длину шаг рассчитан. Второе — двойное ужатие яркости: если передавать в fill_palette яркость 120, а глобально стоять на 150, итог окажется в три раза темнее ожидаемого — обе ручки умножаются. Третье — кнопка и таймер, пишущие в одну targetPalette: это нормально и даже удобно, но событие смены стоит печатать в Serial, иначе на длинной сцене не отличишь автосмену от своего нажатия.
Про память — одна цифра. CRGBPalette16 стоит 48 байт оперативки, и десяток сцен на Uno съедает полкилобайта из двух. Градиентная запись через DEFINE_GRADIENT_PALETTE держит опорные точки во flash — пары «позиция, r, g, b» превращаются в палитру при присваивании, и постоянная память достаётся именно им. При пяти-шести сценах экономия мелкая, при двадцати — уже заметная; заодно выигрывает чистота кода: пять строк опорных читаются лучше, чем шестнадцать CRGB подряд.
Ток на градиентах тоже стоит прикинуть заранее. Тридцать пикселей в белом на полной яркости — это до 1,8 ампера, и USB-порт с его лимитом в 0,5 ампера такой кадр не переживёт. Палитры спасают дважды: тёмные опорные точки режут ток в разы, а аргумент яркости в fill_palette срезает пик пропорционально. Мой рабочий потолок для стола — 170 из 255: сцена яркая, а тестер показывает порядка пол-ампера даже на закатной палитре с белёсыми краями.
Выбор самих палитр — отдельное ремесло, и у него есть два правила, которые я вынес из портящейся сцены. Первое: палитры в одной ротации стоит выравнивать по суммарной светлоте — закат из тёмно-багрового и белёсый рассвет при мгновенной подмене читаются как вспышка в комнате, даже если движение не прерывалось. Второе: для комнаты берите палитры с двумя-тремя соседними оттенками, а не со всем колесом — пятиопорная аврора из синего в зелёный живёт часами, а полная радуга за десять секунд превращает любую комнату в фойе детского сада. Полная радуга — легитимный выбор, но у неё своя роль, и радужный вызов без палитры вовсе разобран в уроке про fill_rainbow.
И последнее про яркость, поверх всех ручек. Аргумент яркости в fill_palette и глобальный setBrightness умножаются, поэтому нормируйте где-то одно: я держу глобальный потолок за неприкосновенный предел питания, а аргументом вызываю ход света — так мерцание сцены не путается с экономией тока. Если сцена должна днём сиять, а ночью дышать, лучше править именно аргумент вызова из своего планировщика, чем переписывать глобальный уровень десять раз за минуту.
Проверьте в симуляторе две крайности ручки. Слева, при шаге четыре, вся лента — кусочек одного маршрута: перелив ленивый, и глаз читает его как смену освещения, а не как паттерн. Справа, при пятнадцати, градиент нарезан на повторяющиеся полосы — полезно увидеть это нарочно, потому что в реальных проектах полосатость обычно находят не в коде, а на стене, уже после монтажа. Затем нажмите кнопку посреди автосмены: оба источника пишут в одну переменную цели, и перебить таймер на лету — нормально.
Отдельно посмотрите на бег. Палитра меняется мгновенно — модели переезда в симе нет, — но картинки щелчка нет, потому что не прерывается движение: старт едет с одной и той же скоростью и до, и после смены. На плате к этому добавится плавный переезд из урока про кроссфейд, и получится полный канон: непрерывный ход плюс плавная подмена маршрута. Если смена всё равно режет глаз — значит, палитры разной светлоты, и выравнивать надо суммы каналов, а не темп переезда.
И третье наблюдение для платной версии: на старте лента вспыхивает с нуля, но при последующих включениях скетч помнит только startIndex — палитра в нём жёстко зашита. Это нормально для ночника-ротатора, но если палитры выбираются кнопкой и должны переживать перезагрузку, номер сцены пишут в EEPROM, как это сделано в уроке про ночник с режимами: одна ячейка — и автосмена начинается с того места, где вы выключили свет.
И про цену кадра на всякий случай. show() отдаёт пиксели последовательно, по тридцать микросекунд на каждый: тридцать пикселей — 0,9 мс, сто сорок четыре — уже 4,3 мс. Для fill_palette с его тихими переливами это не проблема, но если повесите градиент в общий цикл сцены с быстрыми эффектами, помните: время вывода не зависит от того, насколько простая картинка, — даже чёрная лента едет по проводу те же тридцать микросекунд на пиксель.
| Способ | Как пишется | Когда брать |
|---|---|---|
| Цикл с ColorFromPalette | for по пикселям, индекс i*шаг+смещение в каждой итерации | Нелинейные раскладки: индекс считается из физики эффекта |
| fill_palette одним вызовом | Одна строка: старт, шаг, палитра, яркость, смешивание | Градиент на всю ленту с движением и растяжением |
| fill_rainbow | fill_rainbow(leds, N, hue, deltaHue) без палитры вовсе | Радуга, когда маршрут цвета не важен |
| Ручные if-сектора | Три-четыре ветки if с границами по индексу | Жёсткие зоны подсветки, ступени шкалы |
- 1. Сколько байт оперативки занимает одна CRGBPalette16?
- 2. Что произойдёт, если deltaHue в fill_palette сильно больше 256, делённого на число пикселей?
- 3. За что отвечает третий аргумент fill_palette — startIndex?
- 4. Зачем градиентные палитры описывают через DEFINE_GRADIENT_PALETTE во flash?
- 5. Смена палитры «в лоб», за один кадр, без переезда — чем плоха?
- fill_palette красит всю ленту из палитры одной строкой: старт, шаг, палитра, яркость, режим смешивания
- startIndex, увеличенный на 1–3 каждые 40 мс, двигает градиент по ленте без пересчёта координат
- deltaHue около 256/N — один градиент на ленту; больше — повторы, и полосатость видна на стене раньше, чем в коде
- Автосмена: таймер и кнопка пишут только в targetPalette, рисует всегда currentPalette — переезд делает nblendPaletteTowardPalette
- Градиентные описания палитр держите во flash: развёрнутая CRGBPalette16 стоит 48 байт RAM, которых на Uno всего 2 КБ
Частые вопросы по теме урока
- Сколько байт оперативки занимает одна CRGBPalette16?
- Палитра — шестнадцать ячеек CRGB, три байта каждая: 48 байт RAM. Градиентная запись опорных точек живёт во flash, но развёрнутая палитра всё равно занимает полные сорок восемь. Сорок восемь — шестнадцать цветов по три канала
- Что произойдёт, если deltaHue в fill_palette сильно больше 256, делённого на число пикселей?
- Шаг больше 256/N означает, что круг палитры наматывается по ленте несколько раз. Число прокруток равно deltaHue умножить на пиксели и поделить на 256. На ленте появятся повторы градиента — полосатость
- За что отвечает третий аргумент fill_palette — startIndex?
- Старт задаёт, откуда на замкнутом круге палитры начинается раскладка. Его приращение между кадрами — и есть движение градиента по ленте. За позицию круга палитры, с которой красится первый пиксель
- Зачем градиентные палитры описывают через DEFINE_GRADIENT_PALETTE во flash?
- Опорные точки градиента лежат во flash, разворачиваются в CRGBPalette16 при присваивании. Десяток сцен по 48 байт RAM заметен на двухкилобайтной Uno. Экономия RAM: на Uno её всего 2 КБ, а палитр в проекте десяток
- Смена палитры «в лоб», за один кадр, без переезда — чем плоха?
- Мгновенная подмена палитры перекрашивает всю ленту за один кадр, и смена события выдаёт самоделку. Канон — пара текущая/целевая с плавным переездом. Глаз ловит мгновенную подмену картины — щелчок на ровном месте