mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-18 19:13:58 +00:00
docs
This commit is contained in:
142
docs_analysis/IR_protocol_analysis.md
Normal file
142
docs_analysis/IR_protocol_analysis.md
Normal file
@ -0,0 +1,142 @@
|
||||
# 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. Резюме (главное)
|
||||
1. **Найдены реальные баги, триггеримые ШУМОМ (не только валидным трафиком):**
|
||||
- **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) без приёма чего-либо.
|
||||
2. **RX-bottleneck — НЕ ISR и НЕ тяжёлый декод в прерывании** (декода в ISR нет, ISR лёгкий ~2-4µs). Узкое место структурное: **`tick()` вынимает РОВНО один сырой фронт за вызов** → скорость слива кольца привязана к частоте `loop()`; при медленном loop 250-элементный буфер (~120мс запаса) переполняется и рвёт кадры. Плюс битовый CRC ×2 на конец кадра и маленький hold-фильтр.
|
||||
3. **Коррекции ошибок по сути нет** — 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)
|
||||
1. **Таймер input-capture + DMA вместо софт-EXTI** — аппаратный штамп фронтов, убирает per-edge ISR и джиттер метки. Дорого (канал таймера, wrap, восстановление dir), но чистейшие тайминги.
|
||||
2. **Батч-pop сырых фронтов за tick** (с лимитом на итерацию) — снимает привязку к частоте loop. Главный дешёвый выигрыш.
|
||||
3. **subBuffer 250→512/1024** — больше запаса (RAM: 1024≈8КБ).
|
||||
4. **Табличный CRC** вместо битового ×2 (~8× быстрее, +~512Б flash).
|
||||
5. **Поднять preempt RX-EXTI выше DMA-TX** (механизм есть).
|
||||
6. **Дешёвая метка** 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.
|
||||
|
||||
### Что теряется/портится молча
|
||||
1. **Переполнение 5-битной длины (серьёзно):** накладные Data = 7 байт → payload ≤24, но send разрешает `len` до `bytePerPack=31` (`IR_Encoder.cpp:691`). При len 25..31 `packSize` пишется как `&0x1F` (оборот) → декодер читает короткий кадр → CRC не сойдётся → пакет теряется, **а send вернул Success**. (Комментарий `config:96` сам предупреждает про «31 vs 24».)
|
||||
2. **Нет seq** → дубликаты/пропуски незаметны (любой будущий ретрай = повтор действия).
|
||||
3. **Коллизии не детектируются** (half-duplex без CSMA/CD; `isPairSending` глушит только свой RX). TODO «отложить TX после приёма» (`IR_Encoder.cpp:5`) не реализован.
|
||||
4. Тайминговая реконструкция может собрать «правдоподобный» буфер, редко проходящий CRC на неверных данных (~1/65536).
|
||||
5. 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 на все проекты)
|
||||
1. **B1 CRITICAL:** валидировать `packSize >= crcBytes` (и разумную нижнюю границу) ДО `crcCheck`; отбрасывать кадр иначе. Однострочная защита от OOB.
|
||||
2. **B2 HIGH:** `if (encoder) encoder->sendAccept(...)` (null-check). Спасает Car от HardFault.
|
||||
3. **B3 HIGH:** `>=` вместо `>` в границе `writeToBuffer:753` (+ обрабатывать `packSize==0` как невалидный).
|
||||
4. **B4 MEDIUM:** инициализировать accept-поля в конструкторе (`isWaitingAcceptSend=false` и т.д.).
|
||||
5. **B5 MEDIUM:** `pop()` отдавать по значению (out-параметр) под одной критсекцией, не указателем во внутренний слот.
|
||||
|
||||
### Затем — производительность RX (эффект/сложность)
|
||||
- Батч-pop за tick (высокий эффект / низкая сложность) → снять привязку к частоте loop.
|
||||
- Табличный CRC; больше subBuffer; preempt RX>TX; (крупно) input-capture+DMA.
|
||||
|
||||
### Протокол (эффект/сложность)
|
||||
1. **Валидация полной длины в send** (высокий/низкий): проверять `packSize<=31` (Data ≤24, BACK ≤26), возвращать `PayloadTooLarge` — убирает тихую потерю.
|
||||
2. **Seq + дедуп** (высокий/средний): 1 байт seq, дедуп по (addrFrom, seq). Предпосылка для любого ретрая.
|
||||
3. **Реальный ACK+ретрай на отправителе** (высокий/средне-высокий): механизм генерации Accept уже есть, замкнуть петлю (нужен seq).
|
||||
4. **Listen-before-talk** (средний/низкий): отложить TX пока `isReciving()` — TODO уже помечен.
|
||||
5. **Заголовочный CRC/дублирование длины** (средний/низкий): битая длина не должна ломать границу кадра.
|
||||
6. Кадровый повтор как лёгкий 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`.
|
||||
Reference in New Issue
Block a user