Первую «волну» на гирлянде я собрал счётчиком: бегущая точка, за ней хвост затухающих пикселей — смотрелось как полицейская вспышка, а не волна. Попробовал треугольную огибающую через abs(i - center): яркость спадает от центра линейно, излом виден на каждом пикселе, движение дёрганое. Float-синус с millis() спасал на пробных 16 пикселях, но на гирлянде из 144 расчёт кадра подтормаживал, и волна шла рывками — прямо на глазах у гостей, что обидно вдвойне.
Ровно для этого в FastLED есть свой набор волновых функций: sin8, beat8, beat88, beatsin8 — синусы, уложенные в один байт, и фазы, которые бегут сами, без ваших таймеров. С ними волна на 144 пикселях считается за микросекунды, а код сжимается до одной строки на пиксель. Разберём, как из theta в диапазоне 0–255 получить плавную яркость, что такое фаза со скоростью «ударов в минуту» и как склеить волну во времени с волной в пространстве. В симуляторе ниже волна собрана обычным sin() — движок считает честно, — а на живой плате тот же кадр дают функции из этого урока.
sin8: синус, уложенный в байт
Школьный sin() принимает радианы и возвращает число от −1 до 1: плавающая точка, знак, несколько байт на значение. sin8(theta) живёт в другом договоре: theta от 0 до 255 — один полный оборот синусоиды, результат — яркость от 0 до 255 без знака. Ноль волны сидит на 128: theta = 0 даёт 128, theta = 64 — максимум 255, theta = 128 — снова 128, theta = 192 — минимум 0. Отрицательных чисел нет по определению, у байта их не бывает.
Дёшево это потому, что внутри не float-математика, а табличная аппроксимация: компактный массив опорных значений и интерполяция между ними, пара сложений на вызов. На ATmega328P при 16 МГц один float-синус тянёт порядка сотни микросекунд, sin8 — единицы. В масштабе ленты: 144 пикселя с float-синусом — это около 14 мс только на математику одного кадра, из 33 мс бюджета 30 кадров в секунду; sin8 укладывается в доли миллисекунды. Точности для яркости светодиода хватает с запасом — глаз разницу между аппроксимацией и настоящим синусом не отличит.
Договор проверяется руками по четырём точкам: sin8(0) равен 128, sin8(64) — максимум 255, sin8(128) — снова 128, sin8(192) — ноль. Обратный пересчёт из привычных единиц тоже простой: фазу в радианах умножайте на 256 и делите на 6,283, фазу в градусах — умножайте на 256 и делите на 360. Числа выходят дробными, округляйте смело: сдвиг фазы на единицу байта глаз не различает, здесь точность куда выше чувствительности сетчатки.
| Инструмент | Вход | Выход | Когда беру |
|---|---|---|---|
| sin() | радианы | от −1 до 1, float | Математика вне ленты, симуляция |
| sin8(theta) | 0–255, полный оборот | 0–255 без знака | Волны яркости и цвета на ленте |
| sin16(theta) | 0–65535 | от −32767 до 32767 | Медленные волны на длинных лентах |
| beat8 / beat88 | обороты в минуту | набегающая фаза 0–255 | Фаза, которая бежит со временем |
| beatsin8(...) | BPM, границы, фаза | 0–255 между low и high | Пульсы и дыхание одним вызовом |
Формула волны — одна строка: angle = t * 3.0 + i * 0.4, где t — секунды с запуска, i — номер пикселя. Слагаемое t * 3.0 заставляет фазу бежать со временем: три радиана в секунду, полный оборот 6,28 — цикл проходит примерно за 2,1 секунды. Слагаемое i * 0.4 раскладывает волну в пространстве: соседние пиксели сдвинуты по фазе на 0,4 радиана, и полный оборот набирается за 6,28 / 0,4 — на наших 16 пикселях ровно одна волна. Яркость собирается обратно в байт: 128 плюс-минус 120 единиц амплитуды.
В терминах sin8 те же договорённости звучат проще: сдвиг фазы на пиксель i * 16 при обороте в 256 даёт одну волну на 16 пикселях, потому что 16 умножить на 16 — ровно 256. На 144-пиксельной ленте та же дельта уложит девять волн подряд; i * 32 — волны вдвое короче и вдвое чаще, i * 8 — полволны на всю ленту с одним изломом посередине. Правило то же, что у дельты радуги из урока про fill_rainbow: множитель на число пикселей сравнивается с 256.
Симулятор печатает v0 раз в полсекунды — яркость нулевого пикселя. По этим числам удобно проверять период: максимумы 248 приходят через 2,1 секунды друг от друга, и между ними ярко видно синус — медленный подъём у краёв, быстрый проход середины. На плате Serial-пара «имя=значение» делает ту же работу: v0=248 в логе — живая метка, по которой эффект сверяется с расчётом, а не только с глазом.
beat8 и beat88: фаза, которая бежит сама
beat8(BPM) — не волна, а её подготовка: функция возвращает линейно набегающую фазу от 0 до 255, скорость которой задана «оборотами в минуту». beat8(60) пробегает полный цикл за одну секунду, beat8(15) — за четыре. Волна собирается связкой: sin8(beat8(45) + i * 16) — фаза бежит со временем, множитель раскладывает её по ленте. Никаких float, никаких таймеров — фазу считает библиотека, вам остаётся только выбрать скорость.
Цифра 88 в имени второго варианта — про формат аргумента: beat88 хранит скорость с восемью дробными битами, поэтому 60 оборотов в минуту в неё пишут как 60 * 256 = 15360. Это позволяет задать темп вроде 16 с половиной циклов в минуту, который целым числом не выразить; для целых скоростей достаточно beat8. Забегая вперёд: beatsin8 использует ту же фазу внутри, просто сразу прогоняет её через sin8 и вписывает результат в ваши границы яркости.
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 16 CRGB leds[NUM_LEDS]; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(140);} void loop() { byte phase = beat8(45); // фаза бежит: 45 циклов в минуту for (int i = 0; i < NUM_LEDS; i++) { // волна: i * 16 — одна волна на 16 пикселей leds[i] = CHSV(160, 255, sin8(phase + i * 16)); // альтернатива одним вызовом: // leds[i] = CHSV(160, 255, beatsin8(30, 30, 255, 0, i * 16)); } FastLED.show(); FastLED.delay(20);}Числа скетча: beat8(45) бежит 45 циклов в минуту, период одной волны 60/45 = 1,3 секунды; умножение i * 16 раскладывает фазу по ленте, одна волна на 16 пикселей. Поменяйте 45 на 15 — волна замедлится втрое и станет «океанской». Строка с beatsin8 делает то же самое одним вызовом: первый аргумент — скорость в циклах в минуту, второй и третий — границы яркости, между которыми ходит волна, четвёртый — точка отсчёта времени (по умолчанию millis()), пятый — фазовый сдвиг пикселя. beatsin8(30, 30, 255, 0, i * 16) — волна, которая не гаснет ниже 30 и не слепит выше 255, с периодом две секунды.
Скорость подбирается под сцену, и здесь помогают минуты: 45 циклов — живая рябь для рабочего стола, 15 — медленный океан для спальни, 120 — нервный шум для вечеринки. Я начинаю с 30 и правлю на месте: экран монитора не передаёт масштаб ленты, глазу нужен реальный размер, чтобы выбрать темп честно.
Фазовый сдвиг в пятом аргументе — отдельное удовольствие: две ленты на разных пинах можно запустить в противофазе, просто передав им разные фазы, а на одной ленте сдвиг превращает пульс в бегущую волну. Точка отсчёта timebase — это тот же millis(), чьей дисциплине учит урок про millis вместо delay: фаза не копится в вашей переменной, а считается из времени запуска, поэтому даже долгие блокировки кода её не потеряют. И не забывайте про цену отправки: 144 пикселя едут по проводу за 4,3 мс — по 30 мкс на пиксель, как мы считали в уроке про однопроводной протокол; на эти 4,3 мс кадровый бюджет урезается при любой волновой функции.
Эксперименты. Первый — в симуляторе: уберите слагаемое i * 0.4, и волна по месту превратится в пульс по времени — вся лента задышит одним циклом, это мостик к следующему уроку. Второй — на коде для платы: сложите две волны со скоростями 30 и 47 циклов в минуту и усредните яркость — интерференция даст медленно ползущие «биения», которые на счётчиках не собираются вовсе. Третий: прогоните через волну индекс палитры из прошлого урока: ColorFromPalette(sunset, sin8(beat8(30) + i * 16)) — и лента покатится цветными волнами без единого счётчика. Три волны и одна палитра — уже целая гирлянда из урока про эффекты, только плавнее.
Волна в пространстве плюс волна во времени — полный арсенал движения. Остался самый минималистичный случай: волна только во времени, один пульс на все пиксели сразу. Это дыхание — и ему целиком посвящён следующий урок.
- sin8(theta): theta 0–255 — полный оборот, результат 0–255; ноль волны на 128, амплитуда плюс-минус 127
- sin8 табличный: единицы микросекунд против порядка сотни у float-синуса на 16 МГц
- Множитель на пиксель как у радуги: i * 16 на 16 пикселях — одна волна; правило «множитель, умноженный на пиксели, против 256»
- beat8(BPM) — набегающая фаза; beat88 принимает дробные скорости в формате с 8 дробными битами (60 * 256)
- beatsin8(BPM, low, high, timebase, phase) — готовый синус между границами; phase_offset раскладывает волну по ленте
- 1. Сколько шагов theta занимает полный оборот sin8?
- 2. Что вернёт sin8(64)?
- 3. Сдвиг фазы i * 16 на ленте из 16 пикселей даст:
- 4. Что генерирует beat8(60)?
- 5. beatsin8(30, 40, 200): в каких границах ходит значение?
Частые вопросы по теме урока
- Сколько шагов theta занимает полный оборот sin8?
- theta — один байт: оборот от 0 до 255, то есть 256 шагов. Каждые 64 шага — четверть оборота. 256
- Что вернёт sin8(64)?
- 64 — четверть оборота, максимум волны. Ноль волны сидит на 128: sin8(0) и sin8(128) вернут 128. 255
- Сдвиг фазы i * 16 на ленте из 16 пикселей даст:
- 16 пикселей по 16 шагов — 256, полный оборот: одна волна на ленту. Две волны даст i * 32. Одну волну
- Что генерирует beat8(60)?
- beat8 — линейно набегающая фаза, а не волна: 60 оборотов в минуту — один цикл 0–255 в секунду. Синус из неё делает sin8. Набегающую фазу: полный цикл 0–255 за секунду
- beatsin8(30, 40, 200): в каких границах ходит значение?
- Второй и третий аргументы — нижняя и верхняя границы хода. Скорость 30 сидит в первом аргументе. 40–200