Радуга на пробной ленте из шестнадцати пикселей шла гладко, как по маслу. Я перенёс тот же скетч на полтора метра — сто сорок четыре пикселя — и анимация рассыпалась в слайдшоу: кадры переключались рывками, а бегущая полоса будто цеплялась за что-то невидимое. Полчаса я подозревал millis-таймер, ещё полчаса — просевший блок питания. Виноват оказался третий подозреваемый, которого я вообще не рассматривал: у кадра на WS2812 есть цена, и она растёт с длиной ленты. Смысл этого урока — научиться считать эту цену до того, как она съест вашу плавность.
Откуда берётся цена. Протокол WS2812 однопроводной, и бит за битом его выталкивает сам Arduino: 24 бита на пиксель при 800 кГц — это 1,25 микросекунды на бит и ровно 30 мкс на пиксель целиком. Эту арифметику мы разобрали в уроке про однопроводной протокол, а FastLED.show() честно платит её за каждый пиксель буфера. Шестнадцать пикселей — 0,5 мс, шестьдесят — 1,8 мс, сто сорок четыре — 4,3 мс, триста — уже 9 мс. После последнего бита лента просит ещё около 50 мкс тишины на «защёлку» кадра, но на фоне передачи это мелочь. Рекордсмены вроде ESP32 с двумя тысячами пикселей платят за кадр 60 мс — потолок 16 кадров в секунду ещё до всякой математики; в больших проектах ленту режут на куски и шьют параллельно, но это уже другая история.
| Пикселей | Передача show() | Строка из 15 символов на 9600 бод | Остаток бюджета при кадре 33 мс |
|---|---|---|---|
| 16 | 0,5 мс | 15,6 мс | около 32 мс |
| 60 | 1,8 мс | 15,6 мс | около 31 мс |
| 144 | 4,3 мс | 15,6 мс | около 29 мс |
| 300 | 9,0 мс | 15,6 мс | около 24 мс |
Прежде чем считать, договоримся о цели. Кинопроектор показывает 24 кадра в секунду — и мозг верит в движение; на ленте глаз придирчивее: 15–20 кадров уже читаются как рывки, 30 — спокойно, 50 и выше — «аналоговая» плавность. Отсюда мой рабочий бюджет: кадр 20–33 мс. И ловить надо не среднюю скорость, а худший кадр: анимация с икотой из-за одного перегруженного кадра выглядит хуже, чем ровные тридцать.
Перегрузка коварна тем, что не роняет систему — она растягивает период. Таймер «раз в 20 мс» при теле кадра в 35 мс превращается в «раз в 35 мс», без единого предупреждения в мониторе: просто глаз замечает, что радуга стала ленивой. Поэтому первое, что я делаю с затормозившим эффектом, — вешаю хронометр и смотрю на цифры, а не на ленту.
Бюджет кадра считается просто: период, на который вы замахнулись, минус передача, минус математика, минус печать. При цели 30 кадров в секунду период — 33 мс, и даже лента на 300 пикселей оставляет около 24 мс на всё остальное. Ловить надо не «тяжёлые эффекты», а суммы: передача 9 мс плюс математика 6 мс плюс println на 9600 за 16 мс — и вот ваши 30 мс превращаются в 31, кадры тянутся один за другим, а глаз читает это как «эффект тормозит», хотя fill_rainbow тут ни при чём. Правило, которое я вынес из той недели с таймером в руках: любую цифру бюджета мерять, а не оценивать на глаз — micros() с его разрешением около 4 мкс вокруг каждого куска кадра и отчёт раз в секунду показывают вора за минуту.
То, что кадры собираются на millis-таймере без единого delay(), мы уже проговаривали в уроке про эффекты и в millis() вместо delay() — здесь появляется только замер. Смотрите, как он устроен.
Хронометр: меряем, а не гадаем
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 60 CRGB leds[NUM_LEDS];unsigned long lastFrame = 0;unsigned long lastReport = 0;int hue = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(180); Serial.begin(115200);} void loop() { if (millis() - lastFrame >= 33) { // цель: 30 кадров в секунду lastFrame = millis(); unsigned long t0 = micros(); fill_rainbow(leds, NUM_LEDS, hue, 5); // математика кадра hue += 1; unsigned long tMath = micros() - t0; unsigned long t1 = micros(); FastLED.show(); // передача по проводу unsigned long tShow = micros() - t1; if (millis() - lastReport >= 1000) { // печать раз в секунду! lastReport = millis(); Serial.print("math_us="); Serial.print(tMath); Serial.print(" show_us="); Serial.print(tShow); Serial.print(" fps_potolok="); Serial.println(1000000UL / (tMath + tShow)); } }}У меня на Uno с шестьюдесятью пикселями этот скетч стабильно печатает math_us в диапазоне 40–60 и show_us около 1780–1830: радуга почти бесплатна, а передача занимает свою законную долю — 1,8 мс. Потолок из этих двух слагаемых выходит около 540 кадров в секунду, то есть до предела далеко, и именно так надо читать эти числа: не «много или мало», а «сколько осталось от моих 33 миллисекунд». На матрице 8×8 из урока про индексацию XY цифры те же — 64 пикселя платят те же 1,9 мс.
Отдельная страница грабель — отладочная печать в loop(). На 9600 бод один символ стоит около миллисекунды, строка из пятнадцати — пятнадцать, как передача целых пятисот пикселей. Печатайте раз в секунду и на 115200 бод: там символ стоит 87 мкс, и даже подробный дамп не встанет поперёк кадра. И второе, о чём мало кто думает до первого пульта: на время show() FastLED отключает прерывания — битам WS2812 нужны точные интервалы, и ATmega им не помеха. Но приёмник IRremote ловит биты длиной 0,6–2,4 мс, и пауза в 1,8 мс посреди посылки рвёт приём: у меня пульт от гирлянды работал то с одного нажатия, то с пятого, ровно пропорционально длине ленты.
Когда бюджет не сходится: пять рычагов
Первый рычаг — рисовать реже. Медленным эффектам вроде дыхания или градиентного перелива хватает 15–20 кадров в секунду: глаз на плавном поле не отличит их от пятидесяти, а запас бюджета удваивается. Быстрые искры и бегущие точки — наоборот, частый кадр, иначе движение рассыпается в стробоскоп. Одна цифра на весь эффект в константе STEP_MS — и скорость настраивается без правки логики.
Второй — не рисовать вообще, когда нечего менять: если сцена статична или идёт пауза между вспышками, show() можно пропустить, и самые дорогие 9 мс обнуляются. Третий — дешёвая математика: sin8() на плате читает готовую таблицу из 256 байт вместо честного синуса, и на трёхстах пикселях разница набегает в сотни микросекунд каждый кадр. Четвёртый — геометрия: две ленты на одном пине удваивают время передачи, об этом целиком урок про две ленты, а разнос по разным пинам шьёт их параллельно. Пятый — признать, что Uno дотянулась до предела: от полутора тысяч пикселей честнее взять ESP32 или RP2040, там передача и математика расходятся по ядрам.
Фазы меняются каждые сорок кадров, а Serial печатает расчётную цену кадра, а не измеренную: в движке печать и передача бесплатны, поэтому их цену изображает delay() — 15 мс в тихой фазе (это 9 мс show() для трёхсот пикселей плюс 6 мс математики) и 35 мс в фазе печати (строка из двадцати символов на 9600 бод). Следите за парой used= и fps=: цена 15 мс спокойно живёт внутри бюджета 20 мс, потолок 50 кадров в секунду; с печатью она подскакивает до 35 мс, период растягивается за бюджет, и потолок падает до 28 — комета на ленте двигается вдвое ленивее, хотя ни одна строка математики не изменилась. На живой ленте на 300 пикселей то же самое проявится без всяких эмуляций: печать в каждый кадр на 9600 бод стоит как лишние двести пикселей передачи.
Два эксперимента поверх. Растяните фазу: замените 40 в (frame / 40) % 2 на 200 — печать станет редким визитом, и глаз перестанет ловить фазу рывков. А потом перенесите цену в светлое: поднимите delay(35) до delay(60) — это сценарий «лента на полтысячи пикселей», и даже тихая фаза перестанет влезать в двадцать миллисекунд: комета задышит рывками без единой строки печати.
- 1. Сколько времени FastLED.show() передаёт по проводу кадр из 60 пикселей WS2812B?
- 2. Строка отладки из 15 символов печатается в каждый кадр на 9600 бод. Что происходит с бюджетом кадра?
- 3. Что происходит с прерываниями на Arduino Uno во время передачи кадра через FastLED.show()?
- 4. Реальный потолок кадров в секунду для ленты на 300 пикселей на Uno?
- 5. Чем замерить куски кадра точнее, чем millis()?
- show() стоит 30 мкс × пикселей: 60 px — 1,8 мс, 144 px — 4,3 мс, 300 px — 9 мс; цену передачи узнайте до того, как списывать тормоза на математику.
- Бюджет кадра = передача + математика + печать; при переполнении FPS падает молча, и монитор порта ни о чём не предупредит.
- Отладка — раз в секунду и на 115200 бод: 9600 стоит миллисекунду за символ, строка из 15 — полкадра.
- micros() вокруг каждого куска кадра — замеряйте, а не гадайте.
- На Uno во время show() прерывания закрыты: IR-пульты и SoftwareSerial с длинными лентами не дружат.
Частые вопросы по теме урока
- Сколько времени FastLED.show() передаёт по проводу кадр из 60 пикселей WS2812B?
- 24 бита на пиксель при 800 кГц — это 30 мкс; 60 × 30 = 1800 мкс = 1,8 мс. Яркость на время передачи не влияет: по проводу всегда едут все 24 бита, а вот длина ленты влияет всегда. Около 1,8 мс — по 30 мкс на пиксель
- Строка отладки из 15 символов печатается в каждый кадр на 9600 бод. Что происходит с бюджетом кадра?
- На 9600 бод символ идёт около 1,04 мс, и печать синхронная: 15 символов — примерно 15 мс из 33-миллисекундного бюджета, почти полкадра. Минус 15 мс — по миллисекунде на символ
- Что происходит с прерываниями на Arduino Uno во время передачи кадра через FastLED.show()?
- Битам WS2812 нужны точные интервалы, поэтому FastLED запрещает прерывания на время show(). IRremote и SoftwareSerial от этого ломаются первыми — особенно на длинных лентах. Отключаются на время передачи кадра
- Реальный потолок кадров в секунду для ленты на 300 пикселей на Uno?
- Передача 300 пикселей стоит 9 мс, и даже при нулевой математике один show() не даст чаще, чем раз в 9 мс — около 110 кадров в секунду как физический потолок. Около 110 — только 9 мс уходит на передачу, остальное на математику
- Чем замерить куски кадра точнее, чем millis()?
- micros() считает микросекунды с разрешением около 4 мкс — как раз то, что нужно для замера математики и show(), у которых счёт на десятки и тысячи мкс. micros() — разрешение около 4 мкс