Свой первый бегущий огонь я собрал на delay: шаг, delay(200), снова шаг. Лента бежала, но кнопка режимов отвечала с опозданием на двести миллисекунд, а при попытке печатать в Serial волна дёргалась — у Serial своё время, и кадр вставал. Второй вариант был честным по структуре, но с плавающей точкой: sin(), float и таблица значений, которую я пересчитывал каждый кадр, — и всё это ради восьми бит яркости, которым хватало целых чисел. Обе дороги лишние: в FastLED синус уже есть, считает байтами и говорит на языке BPM. Основу волн разобрали в уроке про sin8 и beat88; здесь — beatsin8, фаза пикселя и волна, которая бежит по ленте сама: без delay, без сдвига массива, без состояния.
План такой. Контракт beatsin8 и цена цикла в миллисекундах. Фаза — пятый аргумент, из которой считается длина волны. Сравнение со сдвиговым огнём из урока про бегущий огонь и граница, где сдвиг всё-таки нужен.
beatsin8: синус по контракту
Контракт такой: beatsin8(BPM, low, high, timebase, phase) возвращает байт, качающийся между low и high, а полный цикл занимает 60000 / BPM миллисекунд. При BPM 30 это две секунды на взлёт и спад, при 90 — две трети секунды, при 500 — 120 мс, и волна уже не волна, а мерцание. Внутри никакого float: lib8tion считает синус в целых байтах, дешевле собственной таблицы значений, и места в флеш-памяти почти не ест. Timebase нужен редко — он сдвигает начало отсчёта, если двум волнам надо стартовать не с нуля; в обычных скетчах там стоит ноль. Дыхание яркости из урока про пульсацию — частный случай всего этого: один вызов без фазы, лента дышит целиком.
Волну делает пятый аргумент — фаза. beatsin8(30, 0, 255, 0, i * 16) отдаёт каждому пикселю свою точку цикла: сосед опаздывает на шестнадцать единиц из 256, и гребень уезжает по ленте. Длина волны считается на салфетке: 256 разделить на шаг фазы. Шаг 16 — волна на 16 пикселей, одна на ленту. Шаг 6 — волна на 43 пикселя: на коротком отрезке видно полтора горба, и движение читается заметно живее. Шаг 128 — сосед в противофазе: лента качается шахматкой, и это уже отдельный эффект, а не поломка синуса.
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 30 CRGB leds[NUM_LEDS];unsigned long lastFrame = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(180);} void loop() { if (millis() - lastFrame < 30) return; // кадр раз в 30 мс, без delay lastFrame = millis(); for (int i = 0; i < NUM_LEDS; i++) { // BPM 40: цикл 1,5 с; шаг фазы 14: волна длиной 256/14 = 18 пикселей byte v = beatsin8(40, 10, 255, 0, i * 14); leds[i] = CHSV(25, 230, v); } FastLED.show();}Про каркас. Ни одного delay: кадр держит millis-метка из урока про millis вместо delay, а раз в 30 мс лента целиком пересчитывается из формулы — состояния нет нигде, кроме счётчика кадра. Это принципиальное отличие от сдвигового огня: там хвост жил в самой ленте, смена режима означала потерю шлейфа и пустую ленту на полсекунды. Здесь смена режима — новый вызов с другими аргументами, и лента мгновенно рисует новую картину из той же формулы. Сколько времени съедает сам кадр и почему 30 мс — не догма, разобрано в уроке про время кадра.
Нижнюю границу low я ставлю не нулём, а десятком-двадцаткой: гребень уходит, но лента не гаснет полностью, и на тёмной сцене не теряется ощущение, что она вообще горит. Это вкусовщина, но проверяемая: поставьте 0 на ленте в спальне — и каждый цикл комната мигает темнотой, что читается как помеха, а не как волна.
Сдвиг против формулы
Как выбирать между двумя школами движения. Сдвиг массива хранит историю: новый пиксель рисуется в голову, хвост подминается — так получаются огонь и искры с настоящим шлейфом, у которого своя биография. Фазовая волна не хранит ничего: пересчитал — забыл. Шлейф добавляется поверх: fadeToBlackBy из урока про затухание гасит ленту каждый кадр, а формула рисует гребень заново — получается след, но без массива истории. Моё мнение после нескольких гирлянд: на лентах до двадцати пикселей разницы глаз не видит, берите что проще отлаживать. На матрице 8x8 фазовый подход выигрывает сразу — сдвиг по кругу там превращается в головную боль с индексами, которые разобраны в уроке про матрицу.
И про честность модели. beatsin8 — готовая математика, а не концепция: на плате она считается за такты, а в симуляторе ниже та же волна собрана из треугольника, потому что движок страницы работает на целочисленной арифметике. На восьми битах и короткой ленте глаз треугольник от синуса не отличает — разница прячется в вершинах, а вершины смазаны мягкой нижней границей.
Разбор модели. Цикл длится 256 шагов фазы, ручка меняет паузу между шагами от 5 до 37 мс — полный оборот получается от 1,3 до 9,5 секунды, то есть от 115 до 6 BPM, весь практический коридор в одной ручке. Кнопка переключает шаг фазы: при 16 лента показывает один горб от края до края, при 6 — два с половиной, и та же формула даёт совсем другой характер. Заметьте, что сдвиг считается из millis(): движение продолжается, пока лента подключена, и не зависит от того, сколько кадров вы пропустили — подойдёт и для кадра в 30 мс, и для рваного цикла с тяжёлой обработкой.
Верните себе внимание на нижнюю границу: в модели значение колеблется от 0 до 254, и на впадинах лента гаснет целиком. На живой плате я бы поставил low равным 20 — гребень ходил бы по ленте, но чёрных провалов не было бы. Попробуйте сами: добавьте к waveVal постоянную двадцатку и посмотрите, как меняется характер — на плате это первый аргумент low у beatsin8.
| Приём | Что двигается | Когда беру |
|---|---|---|
| beatsin8 с фазой | формула без состояния, пересчёт всей ленты за кадр | волны, дыхание, противофаза, смена режимов без потерь |
| Сдвиг массива | голова рисуется, хвост подминается | огонь и искры с настоящим шлейфом-биографией |
| fill_rainbow + fadeToBlackBy | радуга каждый кадр гаснет и перерисовывается | лёгкий фейерверк на витрине, без формул |
| inoise8 (Перлин-шум) | поле шевелится, но не бежит | пламя и вода, где движение должно быть неровным |
- 1. Сколько длится полный цикл beatsin8 при BPM 60?
- 2. Шаг фазы 4. Какова длина волны на ленте?
- 3. Чем фазовая волна лучше сдвига массива при смене режимов?
- 4. При каком BPM волна рассыпается в строб?
- 5. Как сделать половины ленты в противофазе?
- beatsin8(BPM, low, high, timebase, phase): цикл 60000/BPM мс, вся математика в целых байтах
- Фаза на пиксель делает волну бегущей: длина волны = 256 / шаг фазы
- Движение stateless: без delay и без сдвига массива, смена режима — новые аргументы
- Шлейф — fadeToBlackBy каждый кадр поверх формулы, история не нужна
- Коридор 10-90 BPM; два слоя складывать через qadd8, фазы слоёв — разные
Частые вопросы по теме урока
- Сколько длится полный цикл beatsin8 при BPM 60?
- BPM — число циклов в минуту. При 60 цикл ровно секунда; при 30 — две секунды, при 500 — 120 мс, где волна уже читается как мерцание. 1 секунду: 60000 / 60
- Шаг фазы 4. Какова длина волны на ленте?
- Длина волны равна 256, делённому на шаг фазы: 256 / 4 = 64 пикселя на полный горб. Шаг 16 даёт волну на 16 пикселей, шаг 128 — противофазу соседей. 64 пикселя
- Чем фазовая волна лучше сдвига массива при смене режимов?
- Сдвиг хранит историю в самой ленте, и смена режима её теряет. Фазовая волна stateless: поменяли аргументы — лента мгновенно рисует новый узор. Ничего не хранит: новая картина пересчитывается из формулы сразу
- При каком BPM волна рассыпается в строб?
- Быстрее сотни циклов в минуту глаз перестаёт следить за гребнем и видит мерцание. Рабочий коридор 10-90 BPM. 500 — цикл 120 мс
- Как сделать половины ленты в противофазе?
- Фаза 128 — половина цикла из 256: пока первая половина на гребне, вторая во впадине. Увеличение BPM лишь ускорит синхронную волну. Сдвиг фазы 128 у второй половины пикселей