fix msgTypeReceive and isReceive

This commit is contained in:
2026-03-30 13:34:04 +03:00
parent fc1a3bacef
commit af3e012aac
7 changed files with 347 additions and 20 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ bin/*
!.vscode/launch.json
log/*
/.vscode
*.zip

View File

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

View File

@ -1,5 +1,6 @@
#include "IR_DecoderRaw.h"
#include "IR_Encoder.h"
#include <cstring>
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 (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) {
memcpy(rejectBuffer, dataBuffer, packSize);
rejectPackSize = static_cast<uint8_t>(packSize);
isRejectAvailable = true;
}
if (packSize && (i_dataBuffer == 8)) {
msgTypeReceive = (dataBuffer[0]>>5) | 0b11111000;
// SerialUSB.println(msgTypeReceive & IR_MASK_MSG_TYPE);
}
}
@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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                         |       |     
\________________________________________________________________________________________________/       |     
|                                                                                                        |     

View File

@ -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())