8.9 KiB
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::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).
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
Скрипт:
- Считает CRC для примеров пакетов (8 байт эха и 31 байт из reject).
- Воспроизводит логику
buildGateRuns(с дополнением буфера доdataByteSizeMax, как в C++). - Печатает
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. Выводы
- Байты в RAM на передаче формируются корректно библиотекой; проблема «после DMA» укладывается в расхождение тайминговой развёртки (
buildGateRuns+ DMA) со старой развёрткой ISR, а не в «другой CRC на машинке» при неизменённой библиотеке. - Подозрение №1:
runLenTicks = toggleCounter + 1вbuildGateRunsне совпадает с числом тиков ISR между шагами FSM (NvsN+1). Требуется сверка с эталонной трассой ISR или логическим анализатором. - Проверка на будущее: сравнить побитово выходы ISR и DMA на одном буфере (8 и 31 байт); при необходимости поправить формулу длины run в
IR-protocolи пересобрать Car и пульт.
8. Ссылки на файлы
| Файл | Назначение |
|---|---|
Documents/Arduino/libraries/IR-protocol/IR_Encoder.cpp |
buildGateRuns, _isr, rawSend |
Documents/Arduino/libraries/IR-protocol/IR_config.cpp |
crc8 |
Car/src/IR/IR.cpp |
setExternalTxBackend, txStart |
Car/src/IR/IrDmaBackend.cpp |
startStream, totalTicks, nextWord |
ControlPointUnion/Plan_B/TestPoints/CustomCmd.h |
sendResp / version_query для тестовых слотов |
Документ составлен по обсуждению в чате; при смене версии IR-protocol числа констант и totalTicks пересчитывайте скриптом.