Пульт от увлажнителя воздуха стал последней каплей: читалка из первых уроков печатала UNKNOWN и на каждое нажатие выдавала новый сюрприз вместо команды. Таблицу кодов на нём не построишь — опознавать нечего. Но кадр при этом доезжал целиком: библиотека честно сложила в буфер каждую вспышку и каждую тишину, просто не нашла в них знакомого узора.
Сырые интервалы — тот же кадр без словаря: пары «вспышка — тишина» в микросекундах, из которых декодеры собирают биты. Научившись их читать, вы перестаёте зависеть от таблицы протоколов: любой пульт превращается в набор чисел, и закономерности в числах видно на глаз. И это не экзотика: вентиляторы, увлажнители, светильники из строительных гипермаркетов — крупнейшие потребители собственных кодировок.
Здесь разберём единицы сырого буфера — тики по 50 микросекунд, — порог тишины, отрезающий один кадр от другого, поправку MARK_EXCESS, из-за которой числа гуляют вокруг даташита, и соберём пороговый автомат, который складывает интервалы в биты. Диктофон-дамп и воспроизведение через sendRaw уже были в уроке про неизвестный пульт, пульты с паузами внутри кадра — в уроке про кондиционер Midea; сегодня сама кухня счёта.
Единицы сырого буфера: тики по 50 микросекунд
Сырое содержимое кадра лежит в decodedIRData.rawDataPtr: поле rawlen говорит, сколько интервалов в записи, массив rawbuf — сами длительности. Хранятся они не в микросекундах, а в тиках приёмника, и тик равен 50 мкс — это шаг таймера, которым библиотека меряет линию. Чётные элементы записи — вспышки, нечётные — тишины, чередование начинается с лидера. Один множитель на 50 — и сырые числа ложатся на величины из даташита. Пара «вспышка — тишина» при этом неделима: автомат читает их только вместе, потому что смысл бита сидит в отношении длительностей, а не в каждой по отдельности.
Разложим NEC-кадр: лидер 9000 мкс — это 180 тиков, пауза шапки 4500 мкс — 90 тиков, дальше биты: ноль как пара 11+11 тиков, единица как 11+34. Множитель работает и в обратную сторону: посчитали в даташите 2.25 мс — делите на 50 и ищете 45 тиков в сырой записи. Не ждите идеальных чисел: тайминги у разных пультов гуляют на тик-два, и именно поэтому декодер сравнивает с допуском, а не на равенство. Точность кадра плюс-минус два тика — норма, а не брак.
Откуда дрожь: поправка MARK_EXCESS. Фотоприёмник раздувает каждую вспышку примерно на 100 мкс — сигнальной цепи нужно время на переключение, — и библиотека откусывает эту добавку обратно при разборе. Без поправки 560-микросекундная вспышка выглядела бы как 660, и границы между «короткой» и «долгой» вспышкой уползли бы в сторону. Значение поправки в четвёртой версии — константа MARK_EXCESS_MICROS: у некоторых приёмников раздувание больше паспортных ста микросекунд, и тогда границы сдвигают константой, а не переписывают декодер. Словарь полей кадра, куда прячется rawDataPtr, мы раскладывали в уроке про decode().
Длину записи полезно уметь считать заранее — она проверяет сама себя. У NEC: два интервала на шапку, по два на каждый из 32 бит и финальная стоп-вспышка — итого 67, и rawlen у честного NEC-кадра именно столько. У записи сильно другой длины две причины: либо это другой протокол, либо кадр разорван тишиной, и в буфере лежит огрызок. Одна цифра в мониторе — и вы уже знаете, с чем имеете дело.
#include <IRremote.hpp> const int IR_PIN = 2; void setup() { Serial.begin(115200); // дамп сырых чисел длинный: 9600 не тянет IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); Serial.println("neznakomyi pult? smotri intervale");} void loop() { if (IrReceiver.decode()) { if (IrReceiver.decodedIRData.protocol == UNKNOWN) { Serial.print("intervalov v kadre: "); Serial.println(IrReceiver.decodedIRData.rawDataPtr->rawlen); IrReceiver.printIRResultRawFormatted(&Serial, true); } IrReceiver.resume(); }}Скорость порта здесь не вкусовщина. Дамп в пару сотен символов при 9600 бод ползёт по проводу секунды: каждый символ — почти миллисекунда, и монитор отстаёт от нажатий. На 115200 тот же дамп вылетает за десятую долю секунды. Готовый печататель, кстати, писать не обязательно: у библиотеки есть printIRResultRawFormatted(), второй аргумент выбирает единицы — тики или микросекунды.
Тот же разбор работает и в обратную сторону: увидели в сырой записи вспышки ровно в 12 и 24 тика — перед вами язык Sony SIRC, где короткая вспышка значит ноль, длинная — единицу. Даташитные числа и сырые тики сходятся на одном множителе, и после пары вечеров сырые записи читаются как открытая книга.
И про память — по-честному. По умолчанию буфер рассчитан на двести интервалов, и каждая пара ест два байта: выходит четыреста байт ОЗУ, а у Uno их всего две тысячи. В скетчах с дисплеями буфер урезают константой RAW_BUFFER_LENGTH в шапке, оставляя запас под самый длинный пульт в доме. Мой увлажнитель влезал со скрипом: записи по 150–170 интервалов, и без урезанной LCD-библиотеки кадры рвались уже на приёме.
Буфер при этом один на все протоколы. Типовому пульту его хватает с запасом, а кадровые монстры вроде кондиционеров — главный пожиратель: длинные паузы внутри их кадров разрывают запись на куски, и этот разрыв — отдельная техника чтения, своя у каждого климатического бренда.
Пороговый автомат: байт из интервалов
Свой декодер — конечный автомат, который читает интервалы и решает по порогам. Для NEC решение принимает одна граница: пауза после вспышки короче 1.1 мс — ноль, длиннее — единица. Точнее: между 560 и 1690 мкс середина около 1125, и допуск в плюс-минус двадцать процентов накрывает дрожь в два тика. Шапку узнаёт вторая граница: 9 мс вспышки и пауза длиннее 4 мс — старт, дальше считаем биты.
Собранные биты раскладываются в байты младшими вперёд — тем самым порядком LSB first, из-за которого готовое слово выглядит задом наперёд; порядок байт и зеркала мы разбирали в предыдущем уроке. Проверка инверсий в конце достаётся бесплатно: собранная пара не даёт 0xFF — кадр выбрасывается, автомат ждёт следующего.
Мерить интервалы самому — навык из той же семьи, что замер эхо у дальномера на pulseIn: счёт микросекунд между фронтами. Разница в дисциплине: ИК-кадр приходит пачкой за десятки миллисекунд, и ловить его фронты из loop() опоздаешь — этим в библиотеке занято прерывание, механика которого разобрана в уроке про attachInterrupt.
Если свести свой декодер к плану, получится пять шагов: поймать начало кадра по шапке; мерить каждый интервал; классифицировать паузу порогом — бит собран; после нужного числа бит сверить инверсии; не сошлось — молча ждать следующий кадр. На этом плане я собирал декодер для пульта люстры, которого в библиотеке не было: сорок строк, и половина из них — печать для отладки. Главное, чему учит сырой уровень: пороги пишутся из ваших измерений, а не из даташита — сначала дамп, потом константы.
| Элемент кадра | Микросекунды | Тики по 50 мкс | Как читает автомат |
|---|---|---|---|
| Шапка: вспышка | 9000 | 180 | старт: дальше ждём длинную паузу |
| Шапка: тишина | 4500 | 90 | полный кадр, а не повтор (у него 2.25 мс) |
| Бит 0 | 560 + 560 | 11 + 11 | пауза короче 1.1 мс — ноль |
| Бит 1 | 560 + 1690 | 11 + 34 | пауза длиннее 1.1 мс — единица |
- 1. В каких единицах библиотека хранит сырые длительности кадра?
- 2. Чётный элемент сырой записи — это...
- 3. Тишина на линии превысила RECORD_GAP_MICROS. Что решает декодер?
- 4. Зачем декодеру допуск в плюс-минус двадцать процентов?
- 5. Старый скетч после sendRaw вызывает enableIRIn() заново. Почему в четвёртой версии аналога не нужно?
- Сырые интервалы лежат в decodedIRData.rawDataPtr: rawlen — сколько пар, rawbuf — тики по 50 мкс; чётные — вспышки, нечётные — тишины
- Лидер NEC 9000 мкс = 180 тиков; бит 0 — 11+11 тиков, бит 1 — 11+34; дрожь плюс-минус два тика — норма
- Тишина длиннее RECORD_GAP_MICROS (8 мс в 4.x) = конец кадра; пульты с паузами внутри кадра распадаются на куски
- MARK_EXCESS около 100 мкс: приёмник раздувает вспышки, библиотека откусывает добавку обратно
- Свой декодер — пороговый автомат: шапка, порог паузы около 1.1 мс для бита, байты младшими вперёд, проверка инверсий в конце бесплатна
Сырые числа — дно пультовой кухни, ниже уже только осциллограф. Дальше решает задача: чужой кадр можно записать и воспроизвести передатчиком — обе стороны показаны в уроке про передачу sendNEC; а если неопознанных пультов в доме накопилось больше, чем строк в таблице протоколов, пороговый автомат из этого урока уже готов стать тем самым декодером, которого в библиотеке не нашлось. И держите в голове цену: сырой дамп — это две сотни чисел на кадр, в мониторе он живёт секунды. Сохраняйте сразу в файл или шапку скетча: вторая распечатка того же пульта отличается на пару тиков, и сравнивать глазами два лога — занятие для очень терпеливых.
Частые вопросы по теме урока
- В каких единицах библиотека хранит сырые длительности кадра?
- Тик приёма равен 50 микросекундам; перевод в привычные величины — умножением на 50. Лидер NEC 9000 мкс — это 180 тиков. в тиках приёмника по 50 мкс
- Чётный элемент сырой записи — это...
- Запись чередуется, начиная с лидера: чётные элементы — вспышки (ON), нечётные — тишины (OFF). вспышка
- Тишина на линии превысила RECORD_GAP_MICROS. Что решает декодер?
- В четвёртой версии граница тишины — 8 мс. Длиннее — конец кадра. Поэтому пульты с паузами внутри кадра распадаются на куски. кадр кончился: дальше — новая посылка
- Зачем декодеру допуск в плюс-минус двадцать процентов?
- Часть дрожи даёт сам приёмник (MARK_EXCESS около 100 мкс), часть — батарейка и расстояние. Допуск накрывает разлёт, и кадр всё равно читается. тайминги пультов дрожат на тик-два, и сравнение на равенство не сходится никогда
- Старый скетч после sendRaw вызывает enableIRIn() заново. Почему в четвёртой версии аналога не нужно?
- Вторая версия отправляла аппаратной ШИМ того же таймера, что приём, и глухла на время передачи. В 4.x передача программная — приём выживает. в 4.x несущую генерирует скетч, и таймер приёма никто не трогает