From 8f45f6e2144c184d531103d17804902b9e3027b5 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Mon, 7 Sep 2026 12:22:22 +0300 Subject: [PATCH] =?UTF-8?q?feat(rx):=20=D0=BD=D0=B0=D0=B1=D0=BB=D1=8E?= =?UTF-8?q?=D0=B4=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D1=91=D0=BC=D0=B0=20=D0=BF=D0=BE=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8E=20+=20airtime=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=B4=D1=80=D0=B0=20=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IR_config.h: irFrameAirtimeUs/irFrameDecodeEndUs/irLockToDecodeEndUs/irLockLatencyUs, irMaxPackSize (31), irDataPackSize/irBackPackSize — все из констант FSM передатчика (преамбула 6×98 тактов, байт 11 бит × 74 такта, такт = полпериода несущей). - IR_Encoder::calculateSendTime по той же формуле (раньше синхробиты считались один раз на кадр → занижение 21-26%, потребители компенсировали +30%). - IR_DecoderRaw: rxLockSeq/rxLockTimeUs (лок преамбулы), rxMsgType, rxExpectedEndUs (по объявленной длине), rxLastEnd {seq, reason Ok/Crc/Timeout/Abort, msgType, packSize, tUs, expectedEndUs}. Терминалы: конец кадра, таймаут тишины, немедленный abort при sync-ошибке / длине <3 (в т.ч. 0) / переполнении — битый кадр больше не держит isReciving до конца чужой передачи + 30 мс. - Порядок в tick: checkTimeout до listenStart (TIMEOUT-лог с реальной длиной), истечение кандидата преамбулы без фронтов, ложный PREAMB-инкремент на старте кандидата убран. - rxMaxPackSize() = протокольные 31 (было 38 = размер буфера). Совместимость: только добавления; существующие сигнатуры не тронуты. Собрано для G4 (Car) и F4 (КУ). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq --- IR_DecoderRaw.cpp | 68 ++++++++++++++++++++++++++++++++++++++++++----- IR_DecoderRaw.h | 26 +++++++++++++++++- IR_Encoder.cpp | 22 ++------------- IR_config.h | 23 ++++++++++++++++ 4 files changed, 111 insertions(+), 28 deletions(-) diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index 325d52a..7359dc8 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -403,6 +403,7 @@ inline void IR_DecoderRaw::checkTimeout() #endif const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U; rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros()); + noteRxEnd(RxEndReason::Timeout, micros()); isRecive = false; // приём завершён msgTypeReceive = 0; // Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся @@ -417,6 +418,49 @@ inline void IR_DecoderRaw::checkTimeout() } // ==================================================================== +void IR_DecoderRaw::noteRxEnd(RxEndReason reason, uint32_t tUs) +{ + rxEnd.seq++; + rxEnd.reason = reason; + rxEnd.msgType = (i_dataBuffer >= 8U * msgBytes) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF; + rxEnd.packSize = (uint8_t)packSize; + rxEnd.tUs = tUs; + rxEnd.expectedEndUs = (packSize >= msgBytes + crcBytes) + ? rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize) + irTicksToUs((uint32_t)syncBits * irBitTicks) + : 0U; +} + +void IR_DecoderRaw::abortFrame(uint32_t tUs) +{ +#if defined(IRDEBUG_SERIAL_PACK) + packTraceOnTimeoutOrAbort(false); +#endif + noteRxEnd(RxEndReason::Abort, tUs); + isRecive = false; + isReciveRaw = false; + msgTypeReceive = 0; + firstRX(); +} + +void IR_DecoderRaw::expirePreambleCandidate() +{ + if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy()) + return; + if ((micros() - preambleCandidateLastEdgeTime) > IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT) + { + if (preambleGoodPeriods) + rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, micros()); + preambleResetToIdle(); + } +} + +uint32_t IR_DecoderRaw::rxExpectedEndUs() const +{ + if (!isRecive || preambleState != PreambleState::Locked || isWrongPack || packSize < msgBytes + crcBytes) + return 0; + return rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize); +} + void IR_DecoderRaw::tick() { #if IR_RX_BRIEF_LOG @@ -471,15 +515,16 @@ void IR_DecoderRaw::tick() if (!processedFront) { isSubBufferOverflow = false; - listenStart(); checkTimeout(); + listenStart(); + expirePreambleCandidate(); #if defined(IR_EDGE_TRACE) while (edgeTraceFlushChunk(Serial, 48) > 0) {} #endif return; } // Если данных нет - ничего не делаем - listenStart(); checkTimeout(); + listenStart(); #if IR_RX_BRIEF_LOG rxBriefFlushDeferredIsrLogs(); #endif @@ -780,10 +825,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) { // Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и // pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered. - isRecive = false; - isReciveRaw = false; - msgTypeReceive = 0; - firstRX(); + abortFrame(micros()); return; } @@ -851,6 +893,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) #if defined(IRDEBUG_SERIAL_PACK) packTraceEmitErrorFlash(F("ERROR: Wrong sync bit")); #endif + abortFrame(micros()); // битый кадр не удерживает приёмник до таймаута + return; } } } @@ -878,8 +922,12 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) // B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё. // Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38). // packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно. - if (packSize != 0 && packSize < msgBytes + crcBytes) + if (packSize < msgBytes + crcBytes) // 0..2: кадр физически не несёт CRC — шум/битьё + { isWrongPack = true; + abortFrame(micros()); + return; + } } // Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец». @@ -903,6 +951,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) preambleResetToIdle(); msgTypeReceive = 0; isAvailable = crcCheck(packSize - crcBytes, crcValue); + noteRxEnd(isAvailable ? RxEndReason::Ok : RxEndReason::Crc, micros()); #ifdef BRUTEFORCE_CHECK { @@ -1622,7 +1671,10 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) if (!isReciveRaw && front.dir && ((prevRise == 0U && front.time > longSilence) || (prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence))) + { preambleStartCandidate(front); + return true; + } } if (preambleState == PreambleState::Candidate) @@ -1703,6 +1755,8 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) isRecive = true; isReciveRaw = true; risePeriod = preambleMeanPeriod; + rxLockSeqCnt++; + rxLockTimeUsVal = front.time; #if defined(IRDEBUG_SERIAL_PACK) packTraceResetFrame(); packTraceOpen = true; diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index 19b7955..4b12363 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -67,7 +67,25 @@ public: return (isRecive && packSize && !isWrongPack) ? packSize : 0; } // Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего. - static constexpr uint16_t rxMaxPackSize() { return (uint16_t)dataByteSizeMax; } + static constexpr uint16_t rxMaxPackSize() { return (uint16_t)irMaxPackSize; } + + // ---- Наблюдаемость приёма по СОСТОЯНИЮ: лок / ожидаемый конец / факт завершения с причиной ---- + enum class RxEndReason : uint8_t { None = 0, Ok, Crc, Timeout, Abort }; + struct RxEndInfo { + uint16_t seq = 0; // номер завершения (растёт на каждом терминале) + RxEndReason reason = RxEndReason::None; + uint8_t msgType = 0xFF; // 0xFF = первый байт не был принят + uint8_t packSize = 0; // объявленная длина (0 = неизвестна) + uint32_t tUs = 0; // micros() терминала + uint32_t expectedEndUs = 0; // расчётный конец кадра в эфире (0 = неизвестен) + }; + uint16_t rxLockSeq() const { return rxLockSeqCnt; } // ++ в момент лока преамбулы + uint32_t rxLockTimeUs() const { return rxLockTimeUsVal; } // метка фронта лока (ISR-время) + /// Тип принимаемого кадра (3 бита) после первого байта; 0xFF пока неизвестен / приём не идёт. + uint8_t rxMsgType() const { return (isRecive && packSize) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF; } + const RxEndInfo &rxLastEnd() const { return rxEnd; } + /// Расчётный момент последнего бита данных текущего кадра (по объявленной длине); 0 = не Locked / длина неизвестна. + uint32_t rxExpectedEndUs() const; uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; } uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; } uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; } @@ -163,6 +181,12 @@ private: Locked = 2 }; PreambleState preambleState = PreambleState::Idle; + uint16_t rxLockSeqCnt = 0; + uint32_t rxLockTimeUsVal = 0; + RxEndInfo rxEnd; + void noteRxEnd(RxEndReason reason, uint32_t tUs); // терминал: фиксирует тип/длину/расчётный конец + void abortFrame(uint32_t tUs); // немедленный сброс битого кадра (sync/длина/overflow) + void expirePreambleCandidate(); // кандидат без фронтов дольше таймаута → Idle uint8_t preambleGoodPeriods = 0; uint16_t preambleMeanPeriod = 0; uint32_t preambleCandidateLastEdgeTime = 0; diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index c7c96c6..8663ccb 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -1183,26 +1183,8 @@ uint8_t IR_Encoder::bitLow[2] = { uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const { - // Расчет времени отправки пакета в миллисекундах - - // Время преамбулы: preambPulse * 2 фронта * bitTakts тактов - uint32_t preambTime = preambPulse * 2 * bitTakts; - - // Время данных: количество бит * bitTakts тактов - uint32_t dataTime = packSize * 8 * bitTakts; - - // Время синхронизации: syncBits * 2 фронта * bitTakts тактов - uint32_t syncTime = syncBits * 2 * bitTakts; - - // Общее время в тактах - uint32_t totalTakts = preambTime + dataTime + syncTime; - - // Конвертируем в миллисекунды - // carrierPeriod - период несущей в микросекундах - // totalTakts * carrierPeriod / 1000 = время в миллисекундах - uint32_t sendTimeMs = (totalTakts * carrierPeriod) / 1000; - - return sendTimeMs; + // Полное время кадра в эфире по формуле FSM (преамбула + байты с синхробитами), округление вверх до мс. + return (irFrameAirtimeUs(packSize) + 999U) / 1000U; } // Функции для тестирования времени отправки без фактической отправки diff --git a/IR_config.h b/IR_config.h index 31f439e..223a209 100644 --- a/IR_config.h +++ b/IR_config.h @@ -264,6 +264,29 @@ typedef uint16_t crc_t; #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #define tolerance 300U +// ---- Длительности и размеры кадра ФОРМУЛАМИ из FSM передатчика (IR_Encoder::txAdvanceBoundary) ---- +// Логический такт TX = полпериода несущей (toggleCounter считает полупериоды). Преамбула = 6 ран по +// (preambToggle+1) тактов; лок декодера — на 3-м RISE (конец 5-й раны); байт = (8 данных + 3 sync) бит по 74 такта. +constexpr uint32_t irTxTickNs = 1000000000UL / (carrierFrec * 2U); +constexpr uint32_t irPreambleTicks = (uint32_t)preambPulse * 2U * ((uint32_t)preambToggle + 1U); +constexpr uint32_t irLockTicks = ((uint32_t)preambPulse * 2U - 1U) * ((uint32_t)preambToggle + 1U); +constexpr uint32_t irBitTicks = (uint32_t)bitTakts * 2U; +constexpr uint32_t irByteTicks = ((uint32_t)bitPerByte + (uint32_t)syncBits) * irBitTicks; +constexpr uint32_t irTicksToUs(uint32_t ticks) { return (uint32_t)(((uint64_t)ticks * irTxTickNs + 500U) / 1000U); } +/// Полное время кадра в эфире (от первой несущей до последнего sync-бита), мкс. +constexpr uint32_t irFrameAirtimeUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks); } +/// От старта кадра до последнего БИТА ДАННЫХ (момент, когда декодер отдаёт кадр), мкс. +constexpr uint32_t irFrameDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); } +/// От лока декодера (3-й RISE преамбулы) до последнего бита данных, мкс. +constexpr uint32_t irLockToDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks - irLockTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); } +/// Латентность лока: от первой несущей чужого кадра до лока декодера, мкс. +constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks); +/// Протокольный максимум длины кадра (5-битное поле длины). +constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO; +/// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from). +constexpr uint8_t irDataPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes * 2 + payload + crcBytes); } +constexpr uint8_t irBackPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes + payload + crcBytes); } + constexpr uint16_t test_all_Time = bitTime; constexpr uint16_t test_all_Takts = bitTakts * 2; constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0);