mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-04-28 03:08:08 +00:00
142 lines
11 KiB
Markdown
142 lines
11 KiB
Markdown
# IR DMA vs ISR: анализ согласованности сигнала и ответа версии
|
||
|
||
Связка с остальным пультом (модули, настройки): **[`ARCHITECTURE.md`](ARCHITECTURE.md)**.
|
||
|
||
Документ фиксирует наблюдения по переходу машинки (проект Car) на **DMA-передачу** ИК через `IR_Encoder::setExternalTxBackend` и `IrDmaBackend`, сравнение со **старым путём** (таймер + **`_isr()`**), ручную проверку CRC по логу пульта и роль **`buildGateRuns`** в библиотеке **IR-protocol**.
|
||
|
||
---
|
||
|
||
## 1. Контекст
|
||
|
||
- До введения DMA передача шла через **`IR_Encoder::begin(..., IR_Encoder::isr)`**: на каждый тик таймера (`carrierFrec * 2`) вызывается **`_isr()`**, формируются преамбула, данные, синхробиты.
|
||
- После коммита с **IR_DMA** (`Car`, `IR.cpp`): **`beginClockOnly`**, **`setExternalTxBackend`**, фактическая модуляция — **`IrDmaBackend::start`** → **`IR_Encoder::buildGateRuns`** + DMA в **BSRR**.
|
||
- Ответ версии — один из самых **длинных** кадров (до **31 байта** полного кадра по заголовку). Короткие пакеты (эхо `Version_Query`, 8 байт) в логе остаются **FrameOK**; длинный ответ версии даёт **CRC fail** / `Frame reject`.
|
||
|
||
---
|
||
|
||
## 2. Два пути: ISR и DMA
|
||
|
||
| Этап | Старый ISR | DMA |
|
||
|------|------------|-----|
|
||
| Байты пакета + CRC | `sendDataFULL` → `sendBuffer` | То же; в `buildGateRuns` — `memcpy` в локальный буфер размером `dataByteSizeMax` |
|
||
| Развёртка в импульсы | **`_isr()`**: счётчик `toggleCounter`, ветки preamb / data / sync | **`buildGateRuns`**: RLE-сегменты `(gate, lenTicks)` → **`nextWord()`** по тикам таймера |
|
||
| Останов передачи | `signal == noSignal`, `isSending = false` | `ticksOutput >= totalTicks`, `sum(runs[i].lenTicks)` |
|
||
|
||
Идея `buildGateRuns`: **эмулировать** шаги FSM, которые в ISR выполняются при **`toggleCounter == 0`** (см. комментарий в `IR_Encoder.cpp` рядом с внутренним `while`).
|
||
|
||
### 2.1. Приоритеты NVIC: приём ИК выше, чем DMA передачи (STM32)
|
||
|
||
Пока активна **внешняя** передача по DMA (`IrDmaBackend` и т.п.), таймер крутит поток запросов к DMA — срабатывают **`DMA1_Channelx_IRQn`** (половина/конец буфера и т.д.). Если их приоритет **выше**, чем у **EXTI** линии пина приёмника, обработка фронтов на входе ИК **откладывается** → растёт джиттер `micros()` и страдает заполнение `subBuffer` / журнал `@IRF1v1`, хотя алгоритм `tick`/`writeToBuffer` не менялся.
|
||
|
||
**Требование:** числовой приоритет **приёма (EXTI)** должен быть **выше приоритета DMA передачи** (в терминах Cortex-M / STM32 HAL: **меньше** значение preempt priority у EXTI, чем у канала DMA ИК).
|
||
|
||
**В репозитории:**
|
||
|
||
- **`IR_Decoder`**: библиотека **не** задаёт приоритет EXTI по умолчанию. На Arduino STM32 пользователь вызывает **`setReceiveExtiPreemptPriority(preempt)`** (до или после `enable()`); после `attachInterrupt` применяется поверх приоритета ядра. Семейства с укороченной картой EXTI (C0/F0/G0/L0) — без изменения NVIC из этой функции.
|
||
- **DMA ИК-TX** (например **`Car/src/IR/IrDmaBackend.cpp`**): preempt задаётся в прошивке носителя (**`CarIrq::kIrTxDmaPreempt`** и т.д.) и должен быть **больше** (ниже срочность), чем у приёма.
|
||
|
||
Свой проект: пользователь обязан согласовать приоритеты; **ни один** канал DMA ИК-TX не должен вытеснять EXTI приёма (меньший preempt у DMA = ошибка).
|
||
|
||
---
|
||
|
||
## 3. Ключевое наблюдение: `runLenTicks = toggleCounter + 1`
|
||
|
||
В **`IR_Encoder::buildGateRuns`** на каждой итерации внешнего цикла:
|
||
|
||
```cpp
|
||
const uint16_t runLenTicks = (uint16_t)toggleCounterLocal + 1U;
|
||
```
|
||
|
||
В **`_isr()`** при стартовом **`toggleCounter == N`** выполняется **ровно N** раз ветка `if (toggleCounter) { toggleCounter--; }` подряд, пока счётчик не станет **0**; **следующий** тик попадает в `else` и делает один шаг `switch (signal)`.
|
||
|
||
Между двумя такими визитами в `else` проходит **N тиков таймера**, не **N+1**.
|
||
|
||
В `buildGateRuns` для того же начального `toggleCounterLocal` в run записывается **`N + 1` тик**. Это даёт **систематическое удлинение каждого сегмента на 1 тик** относительно модели «счётчик убывает N раз до нуля».
|
||
|
||
**Следствие:**
|
||
|
||
- `totalTicks = Σ lenTicks` в **`IrDmaBackend::startStream`** **больше**, чем число тиков, которое дал бы чистый ISR при том же пакете.
|
||
- Число внешних итераций `buildGateRuns` (шагов FSM) совпадает с числом таких сегментов; приближённо:
|
||
`totalTicks ≈ totalTicks_ISR + (число_внешних_шагов)`.
|
||
|
||
Короткий кадр: ошибка может «теряться» в допусках приёмника. Длинный (версия) — **накопление** ошибки по времени → сдвиг границ битов → **неверные байты**, в том числе **CRC**.
|
||
|
||
---
|
||
|
||
## 4. Ручная проверка CRC по логу (пульт)
|
||
|
||
Алгоритм: **`IR_FOX::crc8`** (`IR_config.cpp`), два байта как в **`sendDataFULL`**:
|
||
|
||
- первый байт CRC = `crc8(data, 0, packSize - 2, poly1)`;
|
||
- второй = `crc8(data, 0, packSize - 1, poly2)` (в расчёт второго входит уже первый байт CRC).
|
||
|
||
Пример **31-байтного** кадра из лога `Frame reject`:
|
||
|
||
- Тело **0…28** (29 байт).
|
||
- Байты **29…30** — CRC на проводе.
|
||
|
||
Для фиксированного дампа байтов **0…28** корректная пара CRC по формуле библиотеки — **`6E 54`**, в логе на проводе — **`96 62`** → **не совпадает**; приёмник обоснованно отклоняет кадр.
|
||
|
||
Это **не** объясняется разницей AVR vs STM32: счёт идёт по массиву `uint8_t` побайтно.
|
||
|
||
Эхо **8 байт** `C8 FA 2A FD E8 5D AA B4`: пересчёт даёт **`AA B4`** — совпадает с последними байтами кадра → для этого пакета цепочка **байт → CRC** согласована.
|
||
|
||
---
|
||
|
||
## 5. Скрипт симуляции
|
||
|
||
В репозитории: **`docs/scripts/ir_protocol_gate_runs_sim.py`**.
|
||
|
||
Запуск:
|
||
|
||
```bash
|
||
python docs/scripts/ir_protocol_gate_runs_sim.py
|
||
```
|
||
|
||
Скрипт:
|
||
|
||
1. Считает **CRC** для примеров пакетов (8 байт эха и 31 байт из reject).
|
||
2. Воспроизводит логику **`buildGateRuns`** (с дополнением буфера до `dataByteSizeMax`, как в C++).
|
||
3. Печатает **`totalTicks`**, число **внешних шагов** FSM и связь **`totalTicks - outer_steps`** как оценку «тиков в модели ISR без +1 на каждый шаг».
|
||
|
||
Пример вывода (значения могут слегка отличаться при смене констант в `IR_config.h`):
|
||
|
||
- `preambToggle = 97`
|
||
- для 8-байт пакета: сотни шагов FSM, `totalTicks` порядка тысяч тиков
|
||
- для 31-байт: больше шагов и `totalTicks` (~25k+ тиков для текущих констант)
|
||
|
||
---
|
||
|
||
## 6. Связь с проектами
|
||
|
||
- **Car** (`Executer.cpp`): ответ версии через **`IR_Module::getENC().sendData(...)`** — тот же **`sendDataFULL`**, затем **`rawSend`** → DMA.
|
||
- **ControlPointUnion** (`CustomCmd.h`, слоты): запрос версии через **`sendResp`** с **`version_query`** — задержка **`IR_ResponseDelay`**, затем **`sendData`** на адрес машинки.
|
||
- **ControlPointUnion** (`Plan_B.ino`): разбор **`version_response`** из **`gotData` / `gotBackData`** только после **успешного CRC** в декодере.
|
||
|
||
---
|
||
|
||
## 7. Выводы
|
||
|
||
1. **Байты в RAM** на передаче формируются корректно библиотекой; проблема «после DMA» укладывается в **расхождение тайминговой развёртки** (`buildGateRuns` + DMA) со **старой** развёрткой ISR, а не в «другой CRC на машинке» при неизменённой библиотеке.
|
||
2. **Подозрение №1:** `runLenTicks = toggleCounter + 1` в **`buildGateRuns`** не совпадает с числом тиков ISR между шагами FSM (**`N`** vs **`N+1`**). Требуется сверка с эталонной трассой ISR или логическим анализатором.
|
||
3. **Проверка на будущее:** сравнить побитово выходы ISR и DMA на **одном** буфере (8 и 31 байт); при необходимости поправить формулу длины run в **`IR-protocol`** и пересобрать Car и пульт.
|
||
4. При **DMA-режиме передачи** на STM32 соблюдать **приоритеты NVIC** (раздел **2.1**): приём EXTI **выше**, чем DMA ИК-TX.
|
||
|
||
---
|
||
|
||
## 8. Ссылки на файлы
|
||
|
||
| Файл | Назначение |
|
||
|------|------------|
|
||
| `Documents/Arduino/libraries/IR-protocol/IR_Encoder.cpp` | `buildGateRuns`, `_isr`, `rawSend` |
|
||
| `Documents/Arduino/libraries/IR-protocol/IR_Decoder.cpp` | `setReceiveExtiPreemptPriority` / `enable`: опциональный `NVIC_SetPriority` для EXTI (Arduino STM32) |
|
||
| `Documents/Arduino/libraries/IR-protocol/IR_config.cpp` | `crc8` |
|
||
| `Car/src/IR/IR.cpp` | `setExternalTxBackend`, `txStart` |
|
||
| `Car/src/IR/IrDmaBackend.cpp` | `startStream`, `totalTicks`, `nextWord`, NVIC DMA из `CarIrq` |
|
||
| `Car/src/IR/IR.cpp` | `setReceiveExtiPreemptPriority` + `enable` декодера |
|
||
| `ControlPointUnion/Plan_B/TestPoints/CustomCmd.h` | `sendResp` / `version_query` для тестовых слотов |
|
||
|
||
---
|
||
|
||
*Документ составлен по обсуждению в чате; при смене версии IR-protocol числа констант и `totalTicks` пересчитывайте скриптом.*
|