Лампу в коридоре я повесил на пульт за вечер, а на следующее утро получил эффектный глюк: одно нажатие кнопки, реле щёлкает три раза, свет мигает как в диско. Скетч был наивный: приёмник, decode(), сравнение команды, переключение. В нём не было ни одной строки про повтор — и зря. Пока палец лежит на кнопке, NEC-пульт не молчит: после первого полного кадра каждые 108 мс летит кадр-повтор. Девять кадров в секунду — и все девять попадают в мой switch.
Кадр-повтор устроен скупо: 9 мс несущей, пауза 2,25 мс, стоп-бит 560 мкс — и ни одного бита команды. Батарейку так беречь разумно: тянуть полные 67,5 мс битов при зажатой громкости расточительно. В старых версиях IRremote этот приходил со значением 0xFFFFFFFF — отсюда и легендарная строчка из туториалов про repeat-код ноль эфэф. В четвёртой версии библиотеки повтор видно по флагу: IRDATA_FLAGS_IS_REPEAT внутри decodedIRData.flags. Разбор структуры кадра с таймингами я делал в уроке про NEC-кадры, базовое чтение команд — в уроке про приём кодов, здесь только практика: как не дать удержанию сработать девять раз.
Повторы — не каприз производителя, а экономика батареек и привычка пользователя: держишь громкость — звук ползёт ровно, отпустил — встал. Так телевизоры работают десятилетиями, и рука ожидает, что платформа отреагирует на удержание. Плата, которая на каждый кадр дёргает реле, превращает эту привычку в стробоскоп. Правило у меня теперь жёсткое: пока в скетче есть пульт, в нём есть и политика повторов — гасить, дросселировать или превращать в качель, но не делать вид, что потока нет.
Три честных способа: гасить, дросселировать, качель
Первый способ — гасить на входе. Если команда типа переключателя — замок, реле, вкл-выкл, — каждый повтор обязан умирать до логики: свежий кадр обрабатываем, кадр с флагом повтора выбрасываем. Это самый короткий и самый надёжный вариант, сбоев я на нём не видел ни разу. Одна тонкость: часть пультов шлёт первый повтор не сразу, а после второго полного кадра — у меня из пяти NEC-пультов так ведёт себя один. Логику это не ломает, но при отладке не удивляйтесь, что удержание первые двести миллисекунд молчит.
Второй — дросселировать millis-окном: пропускаем команды не чаще одной в N миллисекунд, остальное в корзину. Метод грубее флага, но выручает там, где флаг не поднимается: RC5 с неполной поддержкой повторов, сырые кадры неизвестных протоколов, пульты от китайских люстр. И на командах меню он честнее: два случайных клика подряд хуже, чем один пропущенный. Окно держите 150-300 мс: меньше 100 мс не фильтрует почти ничего, больше 400 мс превращает пульт в тормоз. Механика меток времени та же, что в millis-планировщике.
Третий — качель: удержание превращается в плавное изменение. Каждый прошедший кадр делает шаг яркости или громкости, окно между шагами 150-200 мс. Так работают кнопки плюс-минус на настоящей технике: одна качелька на клик, плавный набор при удержании, и рука читает это как родное поведение. Разбор плюса-минуса с настоящим пультом есть в уроке про яркость по ИК.
#include <IRremote.hpp> const int IR_PIN = 11;const int LED_PIN = 9; // ШИМ-пин под светодиод или мосфет const int CMD_MODE = 69; // режим: только свежий кадрconst int CMD_PLUS = 21; // удержание — ярчеconst int CMD_MINUS = 7; // удержание — темнее int brightness = 128;unsigned long tLastStep = 0; void setup() { pinMode(LED_PIN, OUTPUT); Serial.begin(9600); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); analogWrite(LED_PIN, brightness);} void loop() { if (IrReceiver.decode()) { bool isRepeat = IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT; int cmd = IrReceiver.decodedIRData.command; if (cmd == CMD_MODE && !isRepeat) { // переключателям нужен только свежий кадр Serial.println("rezhim pereklyuchen"); } if (cmd == CMD_PLUS || cmd == CMD_MINUS) { int delta = (cmd == CMD_PLUS) ? 16 : -16; stepBrightness(delta); } IrReceiver.resume(); }} void stepBrightness(int delta) { unsigned long now = millis(); if (now - tLastStep < 180) return; // окно шага: и клики, и повторы tLastStep = now; brightness = constrain(brightness + delta, 0, 255); analogWrite(LED_PIN, brightness); Serial.print("yarkost: "); Serial.println(brightness);}Почему окно 180 мс, а не 108? Повторы приходят с джиттером: у моих пультов реальный интервал гулял от 108 до 122 мс. Окно вплотную к 108 мс пропускает каждый второй кадр неровно, то густо то пусто. При 180 мс шаги ложатся ровно, а одиночный клик после паузы всегда проходит первым же кадром. Кнопка режима стоит на флаге, а не на окне: режим должен переключаться строго по свежим кадрам, даже если пальцы нервные.
После отпускания кнопки прилетает ещё один-два повтора из воздушного хвоста — качель сделает лишний шаг, к этому надо быть готовым морально. Это не сбой, это физика: пульт не шлёт прощального пакета. Для лампы лишние 16 единиц яркости неважны, для счётчика оборотов — уже так себе, там качель заменяют на окно подлиннее или на явное требование отпустить кнопку перед следующим приростом.
Качель имеет право на мгновенный отклик, а не после окна: свежий кадр шагает сразу, а через окно пропускают только повторы — реализация в два ветвления и три строки. У счётчиков судьба другая: прирост на клик плюс запрет удержания. Требование «отпусти и жми снова» выглядит примитивно, зато исключает проскоченные позиции: счётчик, который за удержание пробежал четыре позиции, пользователю не объяснишь ничем, кроме перезагрузки.
Физику повторов полезно понимать буквально. Пока палец на кнопке, пульт мигает светодиодом на несущей 38 кГц: полный кадр с битами команды один раз, дальше короткие вспышки-маркеры. Приёмник TSOP демодулирует всё это в обычные импульсы, а библиотека решает, что перед ней: полные кадры она раскладывает в адрес и команду, маркер без битов — в флаг повтора. Больше в эфире ничего нет: ни номера нажатия, ни счётчика времени удержания. Если нужен настоящий таймер удержания — строите его сами на millis от первого свежего кадра.
Экономия энергии здесь зримая: маркер в 9 мс против 67,5 мс полного кадра — в семь раз меньше света за один цикл, поэтому пульт живёт годы на двух пальчиковых батарейках. Плата, наоборот, честно декодирует каждый маркер: обработка повтора дешевле полного кадра, и поток девять кадров в секунду для Uno пустяк. Вся тяжесть не в производительности, а в логике: сколько раз щёлкнуть реле и на сколько шагов двинуть яркость — это и решает фильтр.
| Стратегия | Как выглядит в коде | Для каких команд | Грабля |
|---|---|---|---|
| Гасить флагом | проверка IRDATA_FLAGS_IS_REPEAT до логики | вкл-выкл, замок, реле | забытый resume() глушит приём после первого кадра |
| Millis-окно | метка tLastCmd и порог 150-300 мс | меню, счётчик, вкладки | окно меньше 100 мс не фильтрует ничего: повторы идут через 108-122 мс |
| Качель | окно плюс шаг за каждый прошедший кадр | яркость, громкость, позиция | без окна шаги дёргаются девять раз в секунду |
| Комбинация | флаг для switch, окно для шагов | пульт целиком | два механизма — в отладке печатайте оба решения, иначе правите одно, а ломается второе |
Прогон фильтра в симуляторе
На плате тот же скетч покажет разницу сразу: удержание завалит монитор строками povtor otseyan с интервалом около 110 мс, а обычная работа пальцем даст череду prinyato. Окно 300 мс заодно съедает неторопливый двойной клик человека — это 200-350 мс между нажатиями. Для лампы это фича, для пагинации меню — грабля: ставьте окно под задачу, а не по привычке.
Отдельная благодарность окну за защиту от дребезга кнопки: у моего пульта с треснувшим корпусом один клик давал по два свежих кадра, и без фильтра реле двоило. Флаг здесь бессилен: дребезг приходит полными кадрами, библиотека верит, что пульт не лжёт. Механические странности пульта лечит именно окно — оно смотрит на время между кадрами, а не на честность протокола.
- 1. Что физически шлёт NEC-пульт, пока кнопку держат?
- 2. Как в IRremote 4.x отличить повтор от свежего кадра?
- 3. Команда вкл-выкл срабатывает при каждом удержании девять раз. Самое точное лечение?
- 4. Окно дросселя поставили 90 мс. Что увидим?
- 5. Скетч из старого туториала со сравнением results.value и 0xFFFFFFFF в IRremote 4.x...
- Удержание кнопки — это поток повторов каждые 108-122 мс, планируйте логику заранее
- Свежий кадр отличает флаг IRDATA_FLAGS_IS_REPEAT: 0xFFFFFFFF — наследие старых версий
- Окно millis 150-300 мс режет и повторы, и нервные двойные клики, размер подбирают по монитору
- Переключателям — флаг, меню и счётчикам — окно, плавным регулировкам — качель с шагом 150-200 мс
- resume() нужен на каждый принятый кадр, иначе после первого приёмник молчит
Частые вопросы по теме урока
- Что физически шлёт NEC-пульт, пока кнопку держат?
- Повтор — это 9 мс несущей, 2,25 мс паузы и 560 мкс стоп-бита. Команды в нём нет, поэтому библиотека поднимает флаг повтора, а не декодирует команду заново. Полный кадр при удержании уходит в эфир только один раз — в момент нажатия. repeat-кадр из маркера и стоп-бита, без битов команды
- Как в IRremote 4.x отличить повтор от свежего кадра?
- Четвёртая версия кладёт служебные отметки в decodedIRData.flags. Значение 0xFFFFFFFF из старых туториалов относилось к результатам второй версии библиотеки. Флаг выставлен уже в момент декодирования — считать кадры вручную не нужно. проверить флаг IRDATA_FLAGS_IS_REPEAT в decodedIRData.flags
- Команда вкл-выкл срабатывает при каждом удержании девять раз. Самое точное лечение?
- Переключателю нужно ровно одно срабатывание на нажатие, и флаг делает это без побочных потерь. Delay в loop заодно тормозит приём и индикацию, а для меню или счётчика вместо флага берут окно — оно страхует и от двойного клика. гасить повторы флагом и обрабатывать только свежий кадр
- Окно дросселя поставили 90 мс. Что увидим?
- Повторы идут каждые 108-122 мс, окно в 90 мс между ними пустует. Рабочий порог начинается примерно со 150 мс и упирается в терпение пользователя сверху. окно меньше кадра повтора, почти все повторы проходят
- Скетч из старого туториала со сравнением results.value и 0xFFFFFFFF в IRremote 4.x...
- Структуры results в четвёртой версии нет. Повтор теперь флаг IRDATA_FLAGS_IS_REPEAT, а сырые 32 бита живут в decodedRawData для непонятых протоколов. молчит или не собирается: API переехал в decodedIRData