Files
IR-protocol/IR_DMA_ISR_signal_analysis.md
2026-04-02 17:25:10 +03:00

11 KiB
Raw Permalink Blame History

IR DMA vs ISR: анализ согласованности сигнала и ответа версии

Связка с остальным пультом (модули, настройки): 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::startIR_Encoder::buildGateRuns + DMA в BSRR.
  • Ответ версии — один из самых длинных кадров (до 31 байта полного кадра по заголовку). Короткие пакеты (эхо Version_Query, 8 байт) в логе остаются FrameOK; длинный ответ версии даёт CRC fail / Frame reject.

2. Два пути: ISR и DMA

Этап Старый ISR DMA
Байты пакета + CRC sendDataFULLsendBuffer То же; в buildGateRunsmemcpy в локальный буфер размером 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 на каждой итерации внешнего цикла:

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.

Запуск:

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 пересчитывайте скриптом.