diff --git a/.gitignore b/.gitignore index 9403dff..7396430 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bin/* !.vscode/launch.json log/* /.vscode +*.zip diff --git a/IR_DMA_ISR_signal_analysis.md b/IR_DMA_ISR_signal_analysis.md new file mode 100644 index 0000000..f8c1863 --- /dev/null +++ b/IR_DMA_ISR_signal_analysis.md @@ -0,0 +1,125 @@ +# 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`). + +--- + +## 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 и пульт. + +--- + +## 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` пересчитывайте скриптом.* diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index 5cfd3ce..2e0c5a7 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -1,5 +1,6 @@ #include "IR_DecoderRaw.h" #include "IR_Encoder.h" +#include IR_DecoderRaw::IR_DecoderRaw(const uint8_t pin, uint16_t addr, IR_Encoder *encPair) : encoder(encPair) { @@ -587,6 +588,15 @@ void IR_DecoderRaw::writeToBuffer(bool bit) #endif } + // Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец». + // Иначе при packSize==1 один и тот же шаг i_dataBuffer==8 одновременно «закрывает» кадр (msgTypeReceive=0) + // и снова выставляет msgTypeReceive ниже — флаг залипает, пока не придёт ошибка/другой кадр. + if (packSize && (i_dataBuffer == 8)) + { + msgTypeReceive = (dataBuffer[0] >> 5) | 0b11111000; + // SerialUSB.println(msgTypeReceive & IR_MASK_MSG_TYPE); + } + if (packSize && (i_dataBuffer == packSize * bitPerByte)) { // Конец #ifdef IRDEBUG_INFO @@ -631,12 +641,11 @@ void IR_DecoderRaw::writeToBuffer(bool bit) } OUT_BRUTEFORCE:; #endif - } - - if (packSize && (i_dataBuffer == 8)) { - msgTypeReceive = (dataBuffer[0]>>5) | 0b11111000; - // SerialUSB.println(msgTypeReceive & IR_MASK_MSG_TYPE); - + if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) { + memcpy(rejectBuffer, dataBuffer, packSize); + rejectPackSize = static_cast(packSize); + isRejectAvailable = true; + } } } @@ -651,9 +660,7 @@ bool IR_DecoderRaw::crcCheck(uint8_t len, crc_t &crc) crc = (crc8(dataBuffer, 0, len, poly1) << 8) & ~((crc_t)0xFF); crc |= crc8(dataBuffer, 0, len + 1, poly2) & (crc_t)0xFF; - if ( - crc && - dataBuffer[len] == (crc >> 8) & 0xFF && + if (dataBuffer[len] == (crc >> 8) & 0xFF && dataBuffer[len + 1] == (crc & 0xFF)) { crcOK = true; @@ -666,6 +673,14 @@ bool IR_DecoderRaw::crcCheck(uint8_t len, crc_t &crc) return crcOK; } +bool IR_DecoderRaw::availableReject() +{ + if (!isRejectAvailable) + return false; + isRejectAvailable = false; + return true; +} + uint16_t IR_DecoderRaw::ceil_div(uint16_t val, uint16_t divider) { int ret = val / divider; diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index 7361982..6e895a8 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -51,8 +51,17 @@ public: bool isSubOverflow(); volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета + /// Кадр собран по длине из заголовка, но CRC не сошёлся — один раз можно прочитать копию сырых байтов. + bool availableReject(); + uint8_t getRejectSize() const { return rejectPackSize; } + const uint8_t* getRejectBuffer() const { return rejectBuffer; } + ////////////////////////////////////////////////////////////////////////// private: + bool isRejectAvailable = false; + uint8_t rejectPackSize = 0; + uint8_t rejectBuffer[dataByteSizeMax]{}; + ErrorsStruct errors; bool isAvailable = false; uint16_t packSize; diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index ba663ae..bf5a372 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -463,7 +463,7 @@ IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *d uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + min(uint8_t(1), len) + crcBytes; uint8_t msgType = - ((isAdressed ? IR_MSG_BACK_TO : IR_MSG_BACK) << 5) | ((packSize) & (IR_MASK_MSG_INFO >> 1)); + ((isAdressed ? IR_MSG_BACK_TO : IR_MSG_BACK) << 5) | ((packSize) & IR_MASK_MSG_INFO); // формирование массива // msg_type diff --git a/IR_config.h b/IR_config.h index 48ca2aa..1915f26 100644 --- a/IR_config.h +++ b/IR_config.h @@ -48,12 +48,13 @@ \____________________________________________________________________________________________________/     msg type: -                                        //  __________ -                                        // | 01234567 | -                                        //  ---------- -                                        // | xxx..... | = тип сообщения -                                        // | ...xxxxx | = длина (максимум 31 бита) - не больше 24 байт на тело пакета -                                        //  ---------- */ +                                //  __________ +                                // | 01234567 | +                                //  ---------- +                                // | xxx..... | = тип сообщения (биты 7..5) +                                // | ...xxxxx | = полная длина кадра в байтах (5 бит, 0..31, IR_MASK_MSG_INFO), не «31 бит» и не отдельный лимит «24 байта» +                                // Полезная нагрузка в data pack: до bytePerPack байт (см. #define bytePerPack). +                                //  ---------- */ #define IR_MSG_BACK 0U // | 000...... | = Задний сигнал машинки #define IR_MSG_ACCEPT 1U // | 001..... | = подтверждение #define IR_MSG_REQUEST 2U // | 010..... | = запрос @@ -81,12 +82,13 @@ msg type: /`````````````````````` Задний сигнал машинки без адресации ``````````````````````\         + // Первый байт: (IR_MSG_BACK<<5) | (packSize & IR_MASK_MSG_INFO) — как у data pack (тип + длина 0..31).                                                                                             {``````````} [````````````````````````] [````````````````````````] [``````````````]         { msg type } [ addr_from uint16_t ] [====== data bytes ======] [ CRC Bytes ]         {..........} [........................] [........................] [..............]                                                                                                     - { 0000xxxx } [addr_from_H][addr_from_L] [data_H][data_n..][data_L] [ crc1 ][ crc2 ]         + { xxx..|..xxxxx } [addr_from_H][addr_from_L] [data_H][data_n..][data_L] [ crc1 ][ crc2 ]         |     0           1            2            3                         |       |             \_____________________________________________________________________/       |             |                                                                             |             @@ -95,12 +97,13 @@ msg type: /```````````````````````````````````` Задний сигнал машинки с адресацией ````````````````````````````````````\  -                                                                                      + // Первый байт: (IR_MSG_BACK_TO<<5) | (packSize & IR_MASK_MSG_INFO) — IR_MSG_BACK_TO в битах 7..5, длина 0..31. +                                                                                            {``````````} [````````````````````````] [````````````````````````] [````````````````````````] [``````````````]  { msg type } [ addr_from uint16_t ] [ addr_to uint16_t ] [====== data bytes ======] [ CRC Bytes ]  {..........} [........................] [........................] [........................] [..............]  -                                                                                                                 - { 0001xxxx } [addr_from_H][addr_from_L] [addr_from_H][addr_from_L] [data_H][data_n..][data_L] [ crc1 ][ crc2 ]  +                                                                                                                  + { xxx..|..xxxxx } [addr_from_H][addr_from_L] [addr_to_H][addr_to_L] [data_H][data_n..][data_L] [ crc1 ][ crc2 ]  |     0           1            2              3           4            5                         |       |      \________________________________________________________________________________________________/       |      |                                                                                                        |      diff --git a/ir_protocol_gate_runs_sim.py b/ir_protocol_gate_runs_sim.py new file mode 100644 index 0000000..ae72860 --- /dev/null +++ b/ir_protocol_gate_runs_sim.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Симуляция логики IR_Encoder::buildGateRuns (IR-protocol) и утилиты CRC8. +Запуск из корня репозитория: python docs/scripts/ir_protocol_gate_runs_sim.py +Или: python ir_protocol_gate_runs_sim.py из каталога scripts/ +""" + +from __future__ import annotations + +import sys + +# --- IR_config.h (фрагмент) --- +bitPauseTakts = 12 +bitActiveTakts = 25 +preambPulse = 3 +syncBits = 3 +bitPerByte = 8 +preambToggle = ((bitPauseTakts * 2 + bitActiveTakts) * 2 - 1) +bitHigh = [(bitPauseTakts) * 2 - 1, (bitActiveTakts) * 2 - 1] +bitLow = [(bitPauseTakts // 2 + bitActiveTakts) * 2 - 1, (bitPauseTakts) - 1] + +preamb, data, sync, noSignal = 0, 1, 2, 3 +HIGH = True + + +def crc8(data: bytes, start: int, end: int, poly: int) -> int: + """Как IR_FOX::crc8 в IR_config.cpp: [start, end).""" + crc = 0xFF + for i in range(start, end): + crc ^= data[i] + for _ in range(8): + if (crc & 0x80) != 0: + crc = ((crc << 1) ^ poly) & 0xFF + else: + crc = (crc << 1) & 0xFF + return crc + + +def crc_pair_over_wire(packet: bytes) -> tuple[int, int]: + """Два байта CRC как в IR_Encoder::sendDataFULL (poly1 старший, poly2 младший).""" + ps = len(packet) + if ps < 2: + return 0, 0 + b1 = crc8(packet, 0, ps - 2, 0x31) & 0xFF + b2 = crc8(packet, 0, ps - 1, 0x8C) & 0xFF + return b1, b2 + + +# Как dataByteSizeMax в IR_config.h (msg+addr+addr+bytePerPack+crc) +DATA_BYTE_SIZE_MAX = 1 + 2 + 2 + 31 + 2 + + +def build_gate_runs(packet: bytes): + """ + Повторяет IR_Encoder::buildGateRuns: список (gate: bool, lenTicks: int), сумма lenTicks = totalTicks DMA. + Буфер дополняется нулями до dataByteSizeMax, как sendBufferLocal[dataByteSizeMax] в C++. + """ + send_len = len(packet) + send_buf = bytearray(packet) + bytes(max(0, DATA_BYTE_SIZE_MAX - len(packet))) + + toggle = preambToggle + data_bit = bitPerByte - 1 + data_byte = 0 + preamb_front = preambPulse * 2 - 1 + data_seq = bitPerByte * 2 + sync_seq = syncBits * 2 + sync_last = False + sig = preamb + state = HIGH + cur_seq = bitHigh + + runs: list[tuple[bool, int]] = [] + outer_steps = 0 + + while True: + outer_steps += 1 + gate = state + run_len = toggle + 1 # как в C++: (uint16_t)toggleCounterLocal + 1U + + if runs and runs[-1][0] == gate: + g, ln = runs[-1] + runs[-1] = (g, ln + run_len) + else: + runs.append((gate, run_len)) + + while True: + if sig == noSignal: + return runs, outer_steps + + if sig == preamb: + if preamb_front: + preamb_front -= 1 + toggle = preambToggle + break + sig = data + state = not False + continue + + if sig == data: + if data_seq: + if not (data_seq & 1): + cur_seq = bitHigh if ((send_buf[data_byte] >> data_bit) & 1) else bitLow + data_bit -= 1 + toggle = cur_seq[not state] + data_seq -= 1 + break + sync_last = send_buf[data_byte] & 1 + data_byte += 1 + data_bit = bitPerByte - 1 + data_seq = bitPerByte * 2 + sig = sync + continue + + if sig == sync: + if sync_seq: + if not (sync_seq & 1): + if sync_seq == 2: + cur_seq = bitLow if (send_buf[data_byte] & 0x80) else bitHigh + else: + cur_seq = bitLow if sync_last else bitHigh + sync_last = not sync_last + toggle = cur_seq[not state] + sync_seq -= 1 + break + sig = data + sync_seq = syncBits * 2 + if data_byte >= send_len: + sig = noSignal + continue + + return [], 0 + + state = not state + + +def main() -> int: + print("IR-protocol: preambToggle =", preambToggle) + print() + + # Пример из лога: 8-байтный эхо-пакет Version_Query (CRC OK на приёме) + echo = bytes.fromhex("C8 FA 2A FD E8 5D AA B4") + c1, c2 = crc_pair_over_wire(echo) + print("8 байт (эхо): CRC вычисленный:", f"{c1:02X}", f"{c2:02X}", "| на проводе:", f"{echo[6]:02X}", f"{echo[7]:02X}") + + runs8, steps8 = build_gate_runs(echo) + total8 = sum(r[1] for r in runs8) + print(" buildGateRuns: внешних шагов FSM =", steps8, ", totalTicks =", total8, ", число run-сегментов =", len(runs8)) + print() + + # 31 байт из лога Frame reject (пример) + reject = bytes.fromhex( + "DF 00 00 FA 2A 5E 43 61 72 5F 76 34 2E 33 2E 38 5F 5B 31 32 4D 68 7A 5D 6B ED 1D 9A 53 96 62" + ) + if len(reject) == 31: + c1, c2 = crc_pair_over_wire(reject) + print("31 байт (reject): CRC по телу 0..28 должен быть:", f"{c1:02X}", f"{c2:02X}", "| байты [29:31]:", f"{reject[29]:02X}", f"{reject[30]:02X}") + print(" Совпадение с формулой:", c1 == reject[29] and c2 == reject[30]) + + runs31, steps31 = build_gate_runs(reject) + total31 = sum(r[1] for r in runs31) + print(" buildGateRuns: внешних шагов =", steps31, ", totalTicks =", total31, ", run-сегментов =", len(runs31)) + print() + + # Связь totalTicks с моделью «N тиков на сегмент до шага FSM» + # total_build = sum(toggle_i + 1); если бы было sum(toggle_i), разница = steps + theoretical_isr_ticks = total31 - steps31 + print("Для 31-байт пакета: totalTicks (buildGateRuns) =", total31) + print(" Если каждый внешний шаг даёт +1 к длине сегмента относительно ISR (runLen = toggle+1 vs toggle),") + print(" оценка «ISR-тиков» как totalTicks - outer_steps =", theoretical_isr_ticks) + return 0 + + +if __name__ == "__main__": + sys.exit(main())