Три урока назад мы зажигали один пиксель через первый урок раздела и радовались, как дети. Сейчас на полке у меня висит лента на 30 пикселей, которая умеет то, ради чего всё и затевалось: пульт листает режимы, микрофон заставляет её дышать с музыкой, каждый кадр рисуется по таймеру из предыдущего урока — и в этом скетче нет ни одного delay. Проект собирается за один вечер, если разложить его на три слоя: ввод (пульт и микрофон), состояние (переменная режима) и отрисовка (кадры по millis). Мой провал первой попытки был ровно обратным: я написал обработку пульта, анимацию и микрофон одной простынёй в loop, и через неделю не мог найти, где кончается один режим и начинается другой. Слои стоят часа отладки.
Режимы: машина состояний вместо спагетти
Состояние гирлянды — одна переменная mode: 0 — сон, 1 — радуга, 2 — микрофонный VU, 3 — дыхание. Обработчик пульта только меняет эту переменную, рисует кадры только таймер — и это разделение не эстетика, а техника: кадр обязан появляться ровно раз в 30 мс независимо от того, в какой момент прилетела команда, а значит рисовать из обработчика кнопки нельзя в принципе. Каждому режиму — своя функция-кадр, а switch(mode) собирает их в одну строку; каркас планирования тот же, что в уроке про планировщик задач. Микрофонный режим целиком приехал из урока про музыкальную подсветку: тот же пик-холд, та же раскраска от зелёного к красному.
Коды кнопок. У моего пульта — самый ходовой CAR MP3 с протоколом NEC: VOL+ шлёт команду 21, VOL− — команду 7, OK — команду 67. У вашего пульта числа почти наверняка другие, поэтому первый вечер всегда начинается одинаково: снимаем паспорт пульта читалкой кодов и вписываем свои константы. И ещё число, которое надо знать заранее: при удержании кнопки NEC шлёт кадры-повторы примерно каждые 108 мс. Окно 150 мс в обработчике их глушит — одно нажатие даёт один щелчок режима; если захотите листать режимы зажатием, поставьте окно 90 мс, и повторы начнут проходить.
| Кнопка пульта | Что делает | Почему именно так |
|---|---|---|
| VOL+ (команда 21) | Следующий режим: радуга → VU → дыхание | Переход циклический: после дыхания снова радуга |
| VOL− (команда 7) | Предыдущий режим | То же кольцо, но против часовой |
| OK (команда 67) | Сон или пробуждение | Во сне лента гаснет, а скетч продолжает опрашивать пульт |
| Любая кнопка | Код печатается в Serial | Паспорт пульта снимается этим же скетчем, пока константы не вписаны |
Скелет проекта
#include <FastLED.h>#include <IRremote.hpp> // IRremote 4.x: IrReceiver.begin / decodedIRData #define LED_PIN 6#define NUM_LEDS 30#define MIC_PIN A0#define CMD_UP 21 // VOL+#define CMD_DOWN 7 // VOL-#define CMD_OK 67 CRGB leds[NUM_LEDS];int mode = 1; // 0 сон, 1 радуга, 2 VU, 3 дыханиеint hue = 0;int level = 0;unsigned long lastCmd = 0;unsigned long lastFrame = 0; void setup() { Serial.begin(9600); IrReceiver.begin(11, ENABLE_LED_FEEDBACK); FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(100); // потолок для USB-питания, см. текст ниже} void loop() { // слой ввода: пульт опрашивается каждым проходом if (IrReceiver.decode()) { int cmd = IrReceiver.decodedIRData.command; IrReceiver.resume(); if (millis() - lastCmd >= 150) { // окно глушит NEC-повторы (108 мс) lastCmd = millis(); if (cmd == CMD_UP) { mode = mode + 1; if (mode > 3) { mode = 1; } } if (cmd == CMD_DOWN) { mode = mode - 1; if (mode < 1) { mode = 3; } } if (cmd == CMD_OK) { if (mode == 0) { mode = 1; } else { mode = 0; } } Serial.print("cmd="); Serial.print(cmd); Serial.print(" mode="); Serial.println(mode); } } // слой отрисовки: кадр раз в 30 мс if (millis() - lastFrame >= 30) { lastFrame = millis(); switch (mode) { case 1: rainbowFrame(); break; case 2: vuFrame(); break; case 3: breatheFrame(); break; default: FastLED.clear(); break; } FastLED.show(); }} void rainbowFrame() { hue = hue + 2; fill_rainbow(leds, NUM_LEDS, hue, 12);} void vuFrame() { int raw = analogRead(MIC_PIN); // в тишине около 512 int dev = raw - 512; if (dev < 0) { dev = -dev; } if (dev > level) { level = dev; } else { level = level - 12; if (level < 0) { level = 0; } } int lit = map(level, 10, 400, 0, NUM_LEDS); lit = constrain(lit, 0, NUM_LEDS); for (int i = 0; i < NUM_LEDS; i++) { if (i < lit) { leds[i] = CHSV(map(level, 10, 400, 120, 0), 255, 255); } else { leds[i].fadeToBlackBy(30); } }} void breatheFrame() { unsigned long t = millis() % 2400; // треугольник 2,4 с if (t > 1200) { t = 2400 - t; } int v = map(t, 0, 1200, 30, 255); fill_solid(leds, NUM_LEDS, CHSV(120, 180, v));}Слой питания — то место, где итоговые проекты обычно и горят. Худший случай этой гирлянды: 30 пикселей × 60 мА = 1,8 А на белом при полной яркости, а USB щедро отдаёт 0,5 А. Я закрываю вопрос тремя ступенями. Первая — setBrightness(100) в setup: потолок яркости прижимает худший случай примерно к 0,7 А, и USB перестаёт уходить в защиту; точнее ток считает урок про ток и бюджет — методика там одна и та же. Вторая — отдельный блок 5 В от 2 А, если нужна полная яркость: тогда лента питается от него, а Arduino — от USB, с общей землёй. Третья — обвязка из урока про питание ленты: конденсатор 1000 мкФ у входа ленты и резистор 330 Ом в линии данных; если лента длиннее пары метров — добавляется точка подведения питания в середину, как в уроке про 5-вольтовую инжекцию. Микрофон и ИК-приёмник в этом бюджете не участвуют: MAX4466 ест 24 мкА, приёмник — миллиамперы, всё питание проекта — это лента.
Куда развивать проект — и это самое приятное: новый режим теперь стоит одну функцию и одну строку в switch. Хотите огонь — берите Fire2012 из урока про пламя, хотите переводить гирлянду на матрицу — вся индексация уже разобрана в уроке про матрицу 8×8. Три идеи из моей практики: кнопка EQ может листать скорость анимации — тот же обработчик, другой код команды; режим стоит запоминать в EEPROM, чтобы после отключения питания гирлянда просыпалась в том же состоянии; а паспорт пульта стоит наклеить изнутри корпуса — второй пульт в доме обязательно окажется с другими кодами, и бумажка сэкономит вечер.
- 1. Что шлёт пульт NEC, пока кнопка удерживается?
- 2. Почему обработчик пульта только меняет mode, а рисует таймер кадров?
- 3. Лента 30 пикселей на белом полной яркости просит около 1,8 А. Какая строка проекта укладывает её в рамки USB?
- 4. Что делает окно 150 мс в обработчике команд?
- 5. У вашего пульта коды не 21/7/67. Что делать?
- Проект = три слоя: ввод (пульт, микрофон), состояние (mode), отрисовка (кадры по millis)
- Коды моего пульта: VOL+ = 21, VOL− = 7, OK = 67; у вашего свои — снимите паспорт читалкой кодов
- Окно 150 мс глушит NEC-повторы (108 мс): одно нажатие — один щелчок режима
- Питание: 30 px просят 1,8 А в худшем случае; setBrightness(100) держит USB, БП 2 А — для полной яркости
- Новый режим — это функция-кадр и строка в switch: огонь и матрица подключаются именно так
Частые вопросы по теме урока
- Что шлёт пульт NEC, пока кнопка удерживается?
- NEC после первой посылки шлёт повторы с шагом около 108 мс. Именно поэтому в обработчике стоит окно 150 мс: оно глушит повторы, и одно удержание не прокручивает все режимы подряд. Кадры-повторы примерно каждые 108 мс
- Почему обработчик пульта только меняет mode, а рисует таймер кадров?
- Разделение ввода и отрисовки — техника, а не эстетика: таймер кадров гарантирует период, а обработчик команды может выполниться в любой момент между кадрами. Если рисовать из обработчика, кадры будут прыгать вслед за кнопками. Так кадр появляется ровно раз в 30 мс, откуда бы команду ни принесли
- Лента 30 пикселей на белом полной яркости просит около 1,8 А. Какая строка проекта укладывает её в рамки USB?
- setBrightness(100) — это 39 % яркости, худший случай прижимается примерно к 0,7 А. Резистор в линии данных защищает вход первого пикселя, но ток ленты не ограничивает — это разные вещи. FastLED.setBrightness(100)
- Что делает окно 150 мс в обработчике команд?
- Повторы NEC идут каждые 108 мс, окно 150 мс их не пропускает — одно нажатие даёт один щелчок режима. Хотите листать зажатием — окно 90 мс. Глушит NEC-повторы при удержании кнопки
- У вашего пульта коды не 21/7/67. Что делать?
- Коды — свойство конкретной модели пульта, а не протокола. Читалка кодов из урока про ИК печатает команду каждой кнопки в монитор порта, после чего три константы в скетче меняются на ваши. Снять паспорт пульта читалкой кодов и вписать свои константы