Ночник с пультом жил у меня с осени, а в декабре я решил записывать в энергонезависимую память, сколько раз за неделю его включали. Через два дня заметил странность: вечером, когда скетч раз в час сохранял счётчики, пульт отмахивался — жмёшь кнопку, вторую, третью, свет молчит, потом все три команды доезжают скопом. Приёмник жив, пульт жив, скетч не менялся. Виновата оказалась запись в EEPROM: несколько сотен миллисекунд плата проводила с запрещёнными прерываниями, и всё это время мерить длительности кадра было некому. Тогда я впервые понял, что между «приёмник подключён» и «кадры принимаются» лежит целый механизм, у которого есть свои часы и свои паузы.
Эта история — про то, что приём ИК вообще не работа loop(). С первого урока раздела строка IrReceiver.begin() цепляет прерывание, и кадр ловится в его контексте, пока loop спит, печатает или пишет в память. Понимание того, кто на самом деле принимает кадр, объясняет половину странных симптомов: и потерянные нажатия во время записи, и молчание после tone(), и то, почему длинный delay безвреден, а короткая запись в память — смертельна для кадра. Итог урока — короткий чек-лист: где стоит decode, что внутри окна, чем занят таймер приёмника — с этим контролем кадры перестают теряться.
Разберём конвейер приёма по слоям: тик таймера, буфер длительностей, момент передачи кадра decode(). Потом — правила, которых я держусь в каждом проекте с пультом, и таблица того, какие операции приём переживает, а какие убивают. Словарь тот же, что в уроке про attachInterrupt, только здесь обработчик вешает не мы, а библиотека.
Тик 50 мкс: кто на самом деле принимает кадр
Внутри библиотеки тикает таймер с шагом 50 микросекунд — его обработчик смотрит на линию приёмника и отличает вспышку несущей от тишины. Каждая пара «вспышка — пауза» превращается в число длительности, числа складываются в буфер. Размер буфера задаёт константа RAW_BUFFER_LENGTH, по умолчанию 200 отметок, и на Uno это двести байт RAM. Кадр NEC занимает 68 отметок, 16-битные протоколы вроде RC5 — 36, а пульты кондиционеров с их сотнями бит буфер переполняют — библиотека поднимает флаг IRDATA_FLAGS_WAS_OVERFLOW и выбрасывает кадр целиком. Заодно библиотека вешает собственное прерывание на сигнальный пин приёмника — на любой, не только на два аппаратных: фронты ловит пин, а мерит их тиковый таймер.
Пока буфер копит кадр, loop() может делать что угодно — в том числе спать в delay(). Задержка не трогает прерывания: таймер тикает, обработчик мерит, буфер пишется. Можно уйти в delay(5000), напрессовать кнопок за эти пять секунд — и после паузы decode() выдаст кадры один за другим. Границей кадра служит тишина: паузу длиннее восьми миллисекунд (константа RECORD_GAP_MICROS) библиотека считает концом посылки, поэтому кадр и кадр-повтор NEC, разделённые десятками миллисекунд тишины, доезжают как две отдельные записи. В симуляторе ниже этот эксперимент повторяется руками: очередь команд в движке ведёт себя как буфер библиотеки.
А вот три вещи приём убивают. Первая — окно между decode() и resume(): буфер занят разобранным кадром, следующий не принимается, мы ловили это на разборе decodedIRData. Вторая — запрещённые прерывания: cli() или запись в EEPROM переводят процессор в режим, когда тик таймера не случается вовсе, и длительности мерить некому. Третья — чужая работа на таймере приёмника, о ней отдельный абзац ниже. Разница между уровнями принципиальна: окно глухоты выбираете вы, и лечится оно перестановкой строки, а запрет прерываний выбирает библиотека или ваша запись в память — это лечится только планированием.
#include <IRremote.hpp> const int IR_PIN = 2; void setup() { Serial.begin(9600); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK);} void loop() { // имитация тяжёлой работы: приём продолжается и в delay delay(2000); int pribylo = 0; while (IrReceiver.decode()) { Serial.print("cmd="); Serial.println(IrReceiver.decodedIRData.command); IrReceiver.resume(); pribylo = pribylo + 1; } Serial.print("pribylo="); Serial.println(pribylo);}В скетче все кадры, пришедшие за двухсекундную паузу, доезжают: decode() отдаёт их по одному, а цикл while выпивает очередь до дна. За две секунды удержания кнопки пульт успевает отослать около восемнадцати кадров-повторов — по одному каждые 108-110 миллисекунд, мы считали их в уроке про тайминги NEC. Буфер в 200 отметок такое проглатывает: полный кадр — 68 отметок, кадр-повтор — несколько отметок, запаса хватает на два полных кадра плюс россыпь повторов. Если среди прилетевших команд сыпятся одинаковые подряд — это и есть повторы от удержания: в буфере они занимают по несколько отметок, и их помещается десятки.
Но запас не бесконечен, и мой второй тупик был именно про это: я повесил decode() на опрос раз в секунду и стал терять кнопки при быстром наборе. Не из-за delay — из-за переполнения: три полных кадра в буфер уже не влезают, третий выбрасывается с флагом переполнения. Правило, к которому я пришёл: decode() опрашивается чаще, чем раз в 100 миллисекунд, — тогда в буфере максимум один-два кадра, и переполнять его нечем. Всё остальное — короткий loop на millis, мы строили такой в уроке про millis вместо delay.
Свои прерывания рядом с приёмником
Библиотека забрала себе одно прерывание, но аппаратных прерываний на Uno два — пины 2 и 3, и на второй можно вешать свою задачу: кнопку, энкодер, геркон двери. Правило короткое: в обработчике только флаг. volatile bool nazhato = true — и всё. Печать в Serial внутри обработчика вешает порт: UART сам живёт на прерываниях, а запрещать их внутри прерывания — запирать дверь изнутри. Всё остальное делает loop, когда заметит флаг: мой геркон на D3 живёт именно так — поднял флаг, а решение о тревоге принимает уже главный цикл.
Пины я распределяю раз и навсегда: приёмник на 2, собственные кнопки на 3 — по прерыванию на задачу, и никто никому не мешает. Когда пинов не хватает, своя кнопка спокойно живёт и без attachInterrupt: короткий loop с опросом на millis ловит фронт ничуть не хуже аппаратного прерывания. Сигнальную ногу приёмника при этом я всегда держу на пине 2 — привычка из урока про VS1838B, где мы выбирали ей место с запасом на будущее.
Кнопка в прерывании требует уважения к дребезгу: контакты дребезжат 10-20 миллисекунд, и один физический фронт превращается в десяток логических. Внутри обработчика delay запрещён, а millis() не растёт — время в прерывании стоит на месте. Поэтому дребезг глушится в loop: флаг поднялся — запомнили millis(), повторные флаги в течение 20 миллисекунд выбрасываем. Два таймера — один в обработчике, другой в loop — делят обязанности чисто: первый видит фронты, второй умеет ждать.
| Что делает скетч | Приём ИК | Почему |
|---|---|---|
| delay(3000) в loop | продолжается, кадры копятся в буфере | задержка не запрещает прерывания: тик таймера идёт |
| Печать 30 символов в окне decode-resume | теряется следующий кадр | структура результата занята, буфер не принимает — окно глухоты |
| EEPROM.update одной ячейки (около 3,3 мс на Uno) | кадр рвётся посередине | прерывания запрещены на время записи: 3,3 мс длиннее NEC-бита |
| tone() на Uno | приём стоит до restartTimer | таймер приёмника совпадает с таймером tone() |
| Цикл с cli() вокруг критической секции | кадры гибнут целиком | тик 50 мкс не случается: длительности мерить некому |
- Приём кадра живёт в прерывании таймера с тиком 50 мкс; loop может спать в delay — буфер копит кадры сам
- RAW_BUFFER_LENGTH по умолчанию 200 отметок: кадр NEC — 68, RC5 — 36, кондиционеры не влезают и поднимают флаг переполнения
- Окно decode-resume — единственная пауза, когда буфер не принимает: короткая обработка и сразу resume()
- Запись в EEPROM (около 3,3 мс на ячейку на Uno) и cli() запрещают прерывания — кадр гибнет целиком; tone() на Uno занимает таймер приёмника
- decode() чаще раза в 100 мс — и переполнение не страшно; в собственных обработчиках — только флаг volatile
- 1. Кнопки жмут, пока loop спит в delay(5000). Что станет с командами?
- 2. При переполнении буфера длительностей библиотека…
- 3. EEPROM.update одной ячейки занимает на Uno около 3,3 мс. Что это значит для кадра NEC?
- 4. Зуммер с tone() и приёмник IRremote на одной Uno…
- 5. Где место для Serial.print по отношению к decode() и resume()?
Конвейер приёма перестал быть чёрным ящиком: тик таймера, буфер, окно передачи — и становится ясно, почему один скетч молчит после сирены, а второй переживает и запись памяти, и длинную печать. Правило короткого loop — не стилистика, а условие работы: всё, что можно, на millis, всё, что нельзя, — за пределами окна decode-resume. Эти правила понадобятся немедленно: в итоговом проекте раздела на одной плате уживаются приёмник, реле, зуммер и экран. А если записи в EEPROM в вашем проекте станут частыми, перечитайте урок про износ ячеек: писать редко — ещё и нервы приёмнику бережёт.
Частые вопросы по теме урока
- Кнопки жмут, пока loop спит в delay(5000). Что станет с командами?
- Задержка не запрещает прерывания: тик таймера идёт, длительности пишутся в буфер. После паузы decode() выдаст кадры один за другим. Доедут: таймер приёмника тикает и в delay, буфер копит кадры
- При переполнении буфера длительностей библиотека…
- Буфер фиксированный, RAW_BUFFER_LENGTH по умолчанию 200 отметок. Кадр, не влезший в буфер, теряется целиком, о чём сообщается флагом. Поднимает флаг IRDATA_FLAGS_WAS_OVERFLOW и выбрасывает кадр
- EEPROM.update одной ячейки занимает на Uno около 3,3 мс. Что это значит для кадра NEC?
- Бит NEC живёт 1,12 или 2,25 мс. Три с половиной миллисекунды без прерываний — это несколько потерянных битов, и кадр выбрасывается целиком. Кадр может порваться: запись запрещает прерывания дольше NEC-бита
- Зуммер с tone() и приёмник IRremote на одной Uno…
- На AVR приёмный таймер библиотеки совпадает с таймером tone(). Помогает пара stopTimer() перед сиреной и restartTimer() после. Конфликтуют таймерами: приём останавливается после tone()
- Где место для Serial.print по отношению к decode() и resume()?
- Окно между decode() и resume() — единственный момент, когда буфер глух. Строка в 30 символов при 9600 бод стоит около 30 мс — печатайте после resume(). После resume(): печать при 9600 бод съедает десятки миллисекунд