21 KiB
IR-protocol — глубокий анализ (RX-производительность, обработка ошибок, протокол, мины)
Дата: 2026-07-01. Только чтение исходников. Цель: после переноса TX на DMA приём (RX) стал узким местом — разобрать RX-конвейер, обработку ошибок сигнала, дизайн протокола и найти баги/мины. Тайминги:
carrierFrec=38000,bitTakts=37→carrierPeriod=26µs,bitTime≈962µs(комментарий про 1100 вIR_DecoderRaw.h:107устарел),riseTime=962, окно ±300µs,IR_timeout≈15144µs,IR_ResponseDelay≈42мс. Числа длительностей — оценки (замеров в коде нет).
0. Резюме (главное)
- Найдены реальные баги, триггеримые ШУМОМ (не только валидным трафиком):
- CRITICAL — OOB-чтение в
crcCheckприpackSize==1(IR_DecoderRaw.cpp:887):packSize - crcBytes=1-2→uint8_t len=255→crc8читаетdataBuffer[0..254]при массиве 38 → OOB ~217 байт +dataBuffer[255/256]. Мусорный CRC (редко — ложно-валидный кадр) или HardFault у границы SRAM. Нижняя границаpackSizeнигде не проверяется. - HIGH — реальный крэш на Car: null-deref
encoder->sendAccept()без проверки (IR_Decoder.cpp:169). У Car декодер создан сencoder==nullptr(Car/src/IR/IR.cpp:31). Приходит валидный кадрIR_MSG_DATA_ACCEPT, адресованный машинке (addrTo==id, addrFrom≠0 и <broadcast) → черезacceptDelay→sendAcceptпо nullptr → HardFault. Детерминированно, если кто-то шлёт машинке DATA_ACCEPT; плюс шумом (1/65536 на ложный кадр нужного типа). - HIGH — OOB-запись
dataBuffer[38](off-by-one) приpackSize==0(IR_DecoderRaw.cpp:753—>вместо>=): приpackSize==0конец кадра не наступает,i_dataBufferрастёт до 304, записьdataBuffer[304/8=38]затирает соседнее полеprevRise. - MEDIUM — неинициализированные
isWaitingAcceptSend/acceptSendTimer/… (IR_Decoder.cpp:72-79): мусор при старте → возможен спонтанныйsendAccept(и крэш по null-deref) без приёма чего-либо.
- CRITICAL — OOB-чтение в
- RX-bottleneck — НЕ ISR и НЕ тяжёлый декод в прерывании (декода в ISR нет, ISR лёгкий ~2-4µs). Узкое место структурное:
tick()вынимает РОВНО один сырой фронт за вызов → скорость слива кольца привязана к частотеloop(); при медленном loop 250-элементный буфер (~120мс запаса) переполняется и рвёт кадры. Плюс битовый CRC ×2 на конец кадра и маленький hold-фильтр. - Коррекции ошибок по сути нет — detect-and-drop: 2×CRC8 (~16 бит), FEC/повтор отсутствует, однобитный bruteforce выключен («зависает»). Нет seq/дедупа/ретраев; ACK задуман, но отправитель его игнорирует (мёртвый механизм). 5-битная длина молча обрезает payload >24 байт (send возвращает Success, кадр теряется).
1. RX-конвейер и производительность
Стадии (кто где исполняется)
- A. EXTI-фронт → ISR (
IR_DecoderRaw::isr,.cpp:274-308;attachInterrupt(pin, CHANGE)вIR_Decoder::enable :88-89). Приёмник демодулирующий (TSOP), EXTI по фронтам огибающей. В ISR:micros()(вnoInterrupts/interrupts),dir = port->IDR&mask(пост-фактум чтение уровня — при коротком импульсе возможна неверная dir), mute-гейт (isPairSending→ фронт только в edgeTrace и return),subBuffer.push(edge). - B. Сырое кольцо
RingBuffer<FrontStorage,250>(.h:124,subBufferSize=250,IR_config.h:169), ~8 байт/фронт ≈ 2КБ/декодер.push/popпод своей критсекцией.pop()— один элемент за вызов. - C. Фильтр импульсов (
tick :417-444) — по умолчанию выключен (IR_INPUT_MIN_PULSE_US=0), сырой фронт идёт прямо в декод. Включённый: hold-массив[6], отсев пар <minUs как глитч. - D. FSM преамбулы (
preambleProcessEdge :1599-1723): Idle→Candidate→Locked, нужно 2 согласованных периода rise→rise в окне 220-340% bitTime после «тишины» >IR_timeout*2. - E. Декод бита (
processDecodedFront :466-746): инлайн-отсев глитчей, окна таймингов, реконструкция пропущенных бит черезceil_div. - F. Сборка/sync/CRC (
writeToBuffer :748-949): биты вdataBuffer[38], sync 3 бита/байт, длина из первого байта, на конце —crcCheck. - G. Диспетчеризация (
IR_Decoder::_tick :130-172): разбор msgType → gotData/gotBack/gotAccept/gotRequest/gotRaw; авто-ACK для DATA_ACCEPT.
Весь декод (C–G) — в tick()/loop, не в ISR. Это осознанная разгрузка прерывания.
Узкое место
- ISR лёгкий (~2-4µs, оценка), даже при 5-6к фронтов/с ~1.5-2% CPU. Не он предел.
- Структурный предел: 1 сырой pop/tick (
.cpp:409-415) → RX-throughput = частотаloop(). Полный кадр ≈ 836 фронтов ≈ 836 tick'ов. Медленный loop (Serial-отладка, моторы, дисплей) < частоты фронтов → кольцо копится → overflow → рваные кадры → CRC/timeout reject. - Буфер 250 = ~120мс запаса (250/2080 фр/с). Хватает от коротких стопоров, не от устойчивого отставания.
- Финальный CRC — битовый
crc8дважды по всей длине (~5-10µs, оценка).BRUTEFORCE_CHECK(выкл, «зависает») при включении — O(len²·64), отдельная мина.
Переполнение/backpressure
subBufferполон →push=false,isSubBufferOverflow=true(транзиентный, сбрасывается в tick), счётчикrxBriefRawOverflowDrops(u16, насыщение), логQRAW.- hold-фильтр(6) переполнение →
pulseFilterDropHoldOverflow++(стойкий), логHOLD. - Битовый буфер кадра →
isBufferOverflow, логBUF, сброс. QFLT/pulseFilterDroppedByFilteredOverflow()— мёртвый код, всегда 0.- Стойкого счётчика потерянных ПАКЕТОВ нет — потери видны косвенно (CRC/TIMEOUT/SYNC в RX-brief-логе), последний битый кадр — в
rejectBuffer.
RX во время своего TX (mute)
isPairSending(число busy-энкодеров, пересчётrefreshPairMuteStateпод критсекцией) → в ISR фронт отбрасывается на входе (:293-299). Глушится любой чужой кадр на всё время своей передачи (isSending). Для DMA-TX снятие mute зависит отexternalFinishSend()— задержка удлиняет «глухоту» на хвост.- NVIC: библиотека приоритеты почти не трогает; RX-EXTI — дефолт ядра, поднять
setReceiveExtiPreemptPriority(IR_Decoder.cpp:44-62). Нужно: preempt RX-EXTI срочнее DMA-TX, иначе чужой DMA/IRQ сдвигает метку фронта → искажениеrisePeriod(окно всего ±300µs).
Идеи ускорения RX (trade-off)
- Таймер input-capture + DMA вместо софт-EXTI — аппаратный штамп фронтов, убирает per-edge ISR и джиттер метки. Дорого (канал таймера, wrap, восстановление dir), но чистейшие тайминги.
- Батч-pop сырых фронтов за tick (с лимитом на итерацию) — снимает привязку к частоте loop. Главный дешёвый выигрыш.
- subBuffer 250→512/1024 — больше запаса (RAM: 1024≈8КБ).
- Табличный CRC вместо битового ×2 (~8× быстрее, +~512Б flash).
- Поднять preempt RX-EXTI выше DMA-TX (механизм есть).
- Дешёвая метка DWT->CYCCNT вместо
micros()+критсекции в ISR.
2. Обработка ошибок сигнала и протокол
CRC — это НЕ CRC16-CCITT, а два сцепленных CRC8
crc8() (IR_config.cpp:17-33) битовый MSB-first, init=0xFF. Полиномы poly1=0x31, poly2=0x8C (=битовое зеркало 0x31, но алгоритм тот же → фактически два разных CRC8). Сборка (crcCheck :951-970): crc = crc8(0..len,poly1)<<8 | crc8(0..len+1,poly2). Асимметрия: байт2 покрывает данные+байт1, байт1 — только данные. Суммарно ~16 бит, вероятность пропуска ~1/65536, но гарантированной хэмминг-дистанции реального CRC16 нет. Мелочь :959: dataBuffer[len] == (crc>>8) & 0xFF парсится как (==)&0xFF — работает случайно (хрупко при смене crc_t).
Фильтры/устойчивость
- Pulse-filter по длительности — по умолчанию выкл (
IR_INPUT_MIN_PULSE_US=0). - Инлайн-отсев глитчей в декодере — вкл:
IR_SHORT_LOW_GLITCH_REJECT,IR_MICRO_GAP_RISE_REJECT(:484-516), «подтяжка фазы». - Преамбула устойчива к случайному шуму (2 согласованных периода в узком окне). Sync 3 бита/байт — ловит расстройку сетки (до 3 ошибок → reject), но это не коррекция.
Коррекции нет
- FEC отсутствует. Есть эвристическая реконструкция бит по времени (
:636-741) — угадывание, финально валидируется CRC. BRUTEFORCE_CHECK(однобитная коррекция перебором) — выключен,//TODO: зависает(IR_config.h:162). При 16-битном контроле ещё и рискует ложной коррекцией.rejectBuffer— не коррекция, а «detect-and-expose» битого кадра приложению.
Подтверждения (Accept) — механизм есть, но мёртвый
Приёмник на IR_MSG_DATA_ACCEPT планирует sendAccept(from, crc8(данные)) через acceptDelay (IR_Decoder::_tick :157-171). Отправитель Accept игнорирует — логика ожидания/ретрая закомментирована (IR_Encoder.cpp:736-741, rawSend:906 «TODO»). Ретраев/дедупа/seq нет. Гарантий доставки нет.
Форматы кадров
Первый байт (msgType<<5)|(packSize&0x1F), packSize — полный размер кадра, длина 5 бит (0..31). Адреса big-endian.
| Тип | Класс | Поля | data offset | CRC |
|---|---|---|---|---|
| BACK 0 | DataBack | msg, addrFrom(1-2) | 3 | 2 |
| ACCEPT 1 | Accept | msg, addrFrom, customByte(3) | — | 2 |
| REQUEST 2 | Request | msg, addrFrom, addrTo | — | 2 |
| BACK_TO 4 | DataBack | msg, addrFrom, addrTo | 5 | 2 |
| DATA_NOACCEPT 6 | Data | msg, addrFrom, addrTo | 5 | 2 |
| DATA_ACCEPT 7 | Data (нужен Accept) | как 6 | 5 | 2 |
Адресация (IR_config): Broadcast=65000+; id==0 = promiscuous (принимает всё); машинки 1..31999, КТ 32000..63999, пульты 64000..64999.
Что теряется/портится молча
- Переполнение 5-битной длины (серьёзно): накладные Data = 7 байт → payload ≤24, но send разрешает
lenдоbytePerPack=31(IR_Encoder.cpp:691). При len 25..31packSizeпишется как&0x1F(оборот) → декодер читает короткий кадр → CRC не сойдётся → пакет теряется, а send вернул Success. (Комментарийconfig:96сам предупреждает про «31 vs 24».) - Нет seq → дубликаты/пропуски незаметны (любой будущий ретрай = повтор действия).
- Коллизии не детектируются (half-duplex без CSMA/CD;
isPairSendingглушит только свой RX). TODO «отложить TX после приёма» (IR_Encoder.cpp:5) не реализован. - Тайминговая реконструкция может собрать «правдоподобный» буфер, редко проходящий CRC на неверных данных (~1/65536).
- Accept-customByte — 1 байт CRC8 (1/256 ложного подтверждения), но т.к. игнорируется — без эффекта.
3. Мины/баги (severity, подтверждено по коду)
| # | Severity | Файл:line | Суть | Триггер |
|---|---|---|---|---|
| B1 | CRITICAL | IR_DecoderRaw.cpp:887,951-970 |
crcCheck(packSize-crcBytes) при packSize==1 → len=255 → OOB-чтение dataBuffer[0..256] (массив 38) |
шум: первый байт с младш. 5 бит =1 (0x01,0x21,…) после лока преамбулы |
| B2 | HIGH | IR_Decoder.cpp:167-169 |
encoder->sendAccept() без null-check; у Car encoder==nullptr (IR.cpp:31) → HardFault |
валидный DATA_ACCEPT машинке (детерминированно) или шум (1/65536) |
| B3 | HIGH | IR_DecoderRaw.cpp:753 |
off-by-one > вместо >= → OOB-запись dataBuffer[38] (затирает prevRise) при packSize==0 |
шум: первый байт с младш. 5 бит =0, ~38 «дата»-байт подряд |
| B4 | MEDIUM | IR_Decoder.cpp:72-79 (конструктор) |
isWaitingAcceptSend/acceptSendTimer/addrAcceptSendTo/acceptCustomByte не инициализированы → спонтанный sendAccept (+ B2 крэш) |
мусор в RAM при старте |
| B5 | MEDIUM | RingBuffer.h:28-37 + IR_DecoderRaw.cpp:408-415 |
вложенные критсекции не вкладываются (interrupts() в pop() снимает внешний noInterrupts() из tick()) → торн-рид *rawPtr, слот уже свободен для ISR |
заполненное кольцо в узком окне |
| B6 | MEDIUM | IR_Decoder.cpp:112-115, IR_DecoderRaw.cpp:280-282 |
джиттер метки фронта: std::function indirect-call + лишний noInterrupts/interrupts вокруг micros() в ISR → разброс risePeriod |
нагрузка/параллельный DMA |
| B7 | LOW | IR_DecoderRaw.cpp:959 |
приоритет операторов в сравнении CRC — работает случайно | смена crc_t |
| B8 | LOW | IR_DecoderRaw.cpp:448 |
isSubBufferOverflow=false без критсекции параллельно ISR =true → потеря флага (диагностика) |
— |
| B9 | LOW | IR_DecoderRaw.cpp:1612 |
sentinel prevRise==0 при micros()==0 (раз в ~71.6 мин) → разовый глитч преамбулы |
редко |
| B10 | LOW (спит) | IR_DecoderRaw.cpp:980-986, :1676 |
ceil_div /riseTime; при freeFrec==true riseSyncTime может уйти <300 → riseTimeMin overflow / деление на 0 |
только при freeFrec (по умолч. выкл) |
Опровергнуто (REFUTED): wrap micros() в таймаутах (wrap-safe, (uint32_t)(a-b)); переполнение subBuffer за границы (push проверяет isFull); packSize>dataByteSizeMax (5 бит → max 31 < 38, опасна только НИЖНЯЯ граница); overflow highCount/lowCount (гейт по IR_timeout); аллокации в ISR (bind SBO на attachInterrupt, в ISR только indirect-call); реентерабельность одной EXTI (M4 не вытесняет сам себя).
Требует железной проверки: джиттер метки фронта под параллельным DMA-TX; NVIC preempt RX>TX; воспроизведение B1/B3 (подать первый байт 0x01 / 0x00 под отладчиком); RAM-бюджет subBuffer (2КБ/декодер) на F4-пульте с несколькими декодерами.
4. Приоритетный список доработок
Сначала — баги (кандидаты на фикс, все в общей библиотеке → blast radius на все проекты)
- B1 CRITICAL: валидировать
packSize >= crcBytes(и разумную нижнюю границу) ДОcrcCheck; отбрасывать кадр иначе. Однострочная защита от OOB. - B2 HIGH:
if (encoder) encoder->sendAccept(...)(null-check). Спасает Car от HardFault. - B3 HIGH:
>=вместо>в границеwriteToBuffer:753(+ обрабатыватьpackSize==0как невалидный). - B4 MEDIUM: инициализировать accept-поля в конструкторе (
isWaitingAcceptSend=falseи т.д.). - B5 MEDIUM:
pop()отдавать по значению (out-параметр) под одной критсекцией, не указателем во внутренний слот.
Затем — производительность RX (эффект/сложность)
- Батч-pop за tick (высокий эффект / низкая сложность) → снять привязку к частоте loop.
- Табличный CRC; больше subBuffer; preempt RX>TX; (крупно) input-capture+DMA.
Протокол (эффект/сложность)
- Валидация полной длины в send (высокий/низкий): проверять
packSize<=31(Data ≤24, BACK ≤26), возвращатьPayloadTooLarge— убирает тихую потерю. - Seq + дедуп (высокий/средний): 1 байт seq, дедуп по (addrFrom, seq). Предпосылка для любого ретрая.
- Реальный ACK+ретрай на отправителе (высокий/средне-высокий): механизм генерации Accept уже есть, замкнуть петлю (нужен seq).
- Listen-before-talk (средний/низкий): отложить TX пока
isReciving()— TODO уже помечен. - Заголовочный CRC/дублирование длины (средний/низкий): битая длина не должна ломать границу кадра.
- Кадровый повтор как лёгкий FEC (в паре с seq); CRC16-CCITT вместо 2×CRC8 (маргинально, ломает совместимость); фрагментация >31 байт с ACK (если реально нужны большие payload); починить/включить bruteforce (сомнительно).
Ключевые файлы
IR-protocol/: IR_DecoderRaw.{h,cpp} (ISR, декод, CRC, reject, преамбула, mute), IR_Decoder.{h,cpp} (диспетчеризация, Accept-петля, EXTI/NVIC), IR_Encoder.{h,cpp} (сборка кадра, CRC, send, isSending), PacketTypes.{h,cpp} (форматы, адресация), IR_config.{h,cpp} (тайминги, framing, crc8), RingBuffer.h, IrTxIsrBufferedStorage.h.