From 31ac7a362507f0514bc248038b2487984df4b734 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Mon, 20 Apr 2026 14:48:45 +0300 Subject: [PATCH 1/4] Refactor IR decoder and encoder for improved pulse filtering and ISR handling. Removed unused filtered sub-buffer, updated pulse filter methods, and added support for buffered ISR storage in the encoder. Enhanced documentation for clarity on DMA TX backend and ISR modes. --- IR_DecoderRaw.cpp | 122 +++++++++++++++---------------- IR_DecoderRaw.h | 13 ++-- IR_Encoder.cpp | 154 ++++++++++++++++++++++++++++++++------- IR_Encoder.h | 31 +++++--- IrDmaTxStm32.h | 8 ++ IrTxIsrBufferedStorage.h | 64 ++++++++++++++++ ref/IR_DMA_TX_backend.md | 2 + ref/IR_RX_BRIEF_LOG.md | 2 +- 8 files changed, 291 insertions(+), 105 deletions(-) create mode 100644 IrTxIsrBufferedStorage.h diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index 81e7463..cdbf6b7 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -74,7 +74,6 @@ bool IR_DecoderRaw::availableRaw() void IR_DecoderRaw::pulseFilterResetStats() { - pulseFilterDropFilteredOverflow = 0; pulseFilterDropHoldOverflow = 0; pulseFilterDropGlitchPairs = 0; } @@ -344,7 +343,7 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const if (pulseFilterHoldCount != 0U) return true; noInterrupts(); - const bool busy = !subBuffer.isEmpty() || !filteredSubBuffer.isEmpty(); + const bool busy = !subBuffer.isEmpty(); interrupts(); return busy; } @@ -403,10 +402,9 @@ void IR_DecoderRaw::tick() // Не в начале до pop: иначе после checkTimeout lastEdgeTime vs micros() расходятся // с метками ISR из очереди → ложные TIMEOUT (bits=0) каждый пакет. - FrontStorage currentFront; - bool hasCurrentFront = false; FrontStorage rawFront; bool hasRawFront = false; + bool processedFront = false; noInterrupts(); FrontStorage *rawPtr = subBuffer.pop(); if (rawPtr != nullptr) @@ -419,26 +417,33 @@ void IR_DecoderRaw::tick() if (IR_INPUT_MIN_PULSE_US > 0U) { if (hasRawFront) - pulseFilterFeedOneRaw(rawFront); - else - pulseFilterFlushTimeout(micros()); - - noInterrupts(); - FrontStorage *flt = filteredSubBuffer.pop(); - if (flt != nullptr) { - currentFront = *flt; - hasCurrentFront = true; + pulseFilterPushRaw(rawFront); + FrontStorage confirmedFront; + while (pulseFilterTryTakeConfirmed(confirmedFront)) + { + processDecodedFront(confirmedFront); + processedFront = true; + } + } + else + { + const uint32_t nowUs = micros(); + FrontStorage confirmedFront; + while (pulseFilterTryFlushOne(nowUs, confirmedFront)) + { + processDecodedFront(confirmedFront); + processedFront = true; + } } - interrupts(); } else if (hasRawFront) { - currentFront = rawFront; - hasCurrentFront = true; + processDecodedFront(rawFront); + processedFront = true; } - if (!hasCurrentFront) + if (!processedFront) { isSubBufferOverflow = false; listenStart(); @@ -448,11 +453,22 @@ void IR_DecoderRaw::tick() #endif return; } // Если данных нет - ничего не делаем + listenStart(); + checkTimeout(); +#if IR_RX_BRIEF_LOG + rxBriefFlushDeferredIsrLogs(); +#endif +#if defined(IR_EDGE_TRACE) + while (edgeTraceFlushChunk(Serial, 48) > 0) {} +#endif +} +void IR_DecoderRaw::processDecodedFront(const FrontStorage ¤tFront) +{ if (preambleProcessEdge(currentFront)) { lastEdgeTime = currentFront.time; - goto END; + return; } lastEdgeTime = currentFront.time; // запоминаем любой фронт @@ -478,7 +494,7 @@ void IR_DecoderRaw::tick() #if IR_GLITCH_REJECT_PHASE_NUDGE irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise); #endif - goto END; + return; } #endif #if IR_MICRO_GAP_RISE_REJECT @@ -495,7 +511,7 @@ void IR_DecoderRaw::tick() #if IR_GLITCH_REJECT_PHASE_NUDGE irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise); #endif - goto END; + return; } #endif if (candRp <= riseTimeMax / 4U && !highCount && !lowCount) @@ -504,7 +520,7 @@ void IR_DecoderRaw::tick() #if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Timing, irClampU16(candRp), 0, currentFront.time); #endif - goto END; + return; } if (candRp > riseTimeMax / 4 || highCount || lowCount) @@ -557,7 +573,7 @@ void IR_DecoderRaw::tick() } } #ifdef IRDEBUG - // goto END; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // return; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #endif //---------------------------------------------------------------------------------- #ifdef IRDEBUG @@ -572,7 +588,7 @@ void IR_DecoderRaw::tick() rxBriefLog(RxBriefReason::Timing, irClampU16((uint32_t)risePeriod), irClampU16((uint32_t)highTime), currentFront.time); #endif - goto END; + return; } // определить направление фронта @@ -727,17 +743,6 @@ void IR_DecoderRaw::tick() else { // Если ```\__ ↓ } - -//////////////////////////////////////////////////////////////////////////////////////////////////////////// -END:; - listenStart(); - checkTimeout(); -#if IR_RX_BRIEF_LOG - rxBriefFlushDeferredIsrLogs(); -#endif -#if defined(IR_EDGE_TRACE) - while (edgeTraceFlushChunk(Serial, 48) > 0) {} -#endif } void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) @@ -1455,34 +1460,18 @@ void IR_DecoderRaw::pulseFilterShiftLeft(uint8_t n) pulseFilterHoldCount = newCount; } -bool IR_DecoderRaw::pulseFilterEmit(const FrontStorage &e) -{ - if (filteredSubBuffer.isFull()) - { - pulseFilterDropFilteredOverflow++; -#if IR_RX_BRIEF_LOG - rxBriefLog(RxBriefReason::FilterOverflow, irClampU16(pulseFilterDropFilteredOverflow), 0, e.time); -#endif - return false; - } - filteredSubBuffer.push(e); - return true; -} - void IR_DecoderRaw::pulseFilterReset() { pulseFilterHoldCount = 0; pulseFilterLastRawValid = false; pulseFilterLastRawTime = 0; - while (filteredSubBuffer.pop() != nullptr) {} } -void IR_DecoderRaw::pulseFilterFeedOneRaw(const FrontStorage &e) +void IR_DecoderRaw::pulseFilterPushRaw(const FrontStorage &e) { const uint32_t minUs = IR_INPUT_MIN_PULSE_US; if (minUs == 0U) { - pulseFilterEmit(e); return; } @@ -1495,44 +1484,53 @@ void IR_DecoderRaw::pulseFilterFeedOneRaw(const FrontStorage &e) #if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::HoldOverflow, irClampU16(pulseFilterDropHoldOverflow), 0, e.time); #endif - pulseFilterEmit(pulseFilterHoldEdges[0]); pulseFilterShiftLeft(1); } pulseFilterHoldEdges[pulseFilterHoldCount++] = e; - const uint8_t holdback = irPulseFilterHoldbackEdges(); +} +bool IR_DecoderRaw::pulseFilterTryTakeConfirmed(FrontStorage &out, uint32_t logTime) +{ + const uint32_t minUs = IR_INPUT_MIN_PULSE_US; + if (minUs == 0U) + return false; + + const uint8_t holdback = irPulseFilterHoldbackEdges(); + if (logTime == 0U) + logTime = pulseFilterLastRawTime; for (;;) { if (pulseFilterHoldCount < 2) - return; + return false; const uint32_t dt = pulseFilterHoldEdges[1].time - pulseFilterHoldEdges[0].time; if (dt < minUs) { pulseFilterDropGlitchPairs++; #if IR_RX_BRIEF_LOG - rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, e.time); + rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, logTime); #endif pulseFilterShiftLeft(2); continue; } if (pulseFilterHoldCount <= holdback) - return; + return false; - pulseFilterEmit(pulseFilterHoldEdges[0]); + out = pulseFilterHoldEdges[0]; pulseFilterShiftLeft(1); + return true; } } -void IR_DecoderRaw::pulseFilterFlushTimeout(uint32_t nowUs) +bool IR_DecoderRaw::pulseFilterTryFlushOne(uint32_t nowUs, FrontStorage &out) { if (IR_INPUT_MIN_PULSE_US == 0U || !pulseFilterLastRawValid || pulseFilterHoldCount == 0) - return; + return false; const uint32_t waitUs = IR_INPUT_MIN_PULSE_US * (uint32_t)IR_INPUT_FILTER_TIMEOUT_MULT; if ((uint32_t)(nowUs - pulseFilterLastRawTime) < waitUs) - return; + return false; while (pulseFilterHoldCount > 0) { @@ -1549,9 +1547,11 @@ void IR_DecoderRaw::pulseFilterFlushTimeout(uint32_t nowUs) continue; } } - pulseFilterEmit(pulseFilterHoldEdges[0]); + out = pulseFilterHoldEdges[0]; pulseFilterShiftLeft(1); + return true; } + return false; } uint32_t IR_DecoderRaw::preambleJitterTolUs(uint32_t baselineUs) const diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index 55503a7..6cc3dee 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -52,7 +52,7 @@ public: inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился bool isSubOverflow(); volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета - uint32_t pulseFilterDroppedByFilteredOverflow() const { return pulseFilterDropFilteredOverflow; } + uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; } uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; } uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; } void pulseFilterResetStats(); @@ -122,8 +122,6 @@ private: // volatile FrontStorage subBuffer[subBufferSize]; // вспомогательный буфер для хранения необработанных фронтов/спадов RingBuffer subBuffer; - /** Очередь фронтов после потокового анти-глитча; tick() читает из неё при включённом фильтре. */ - RingBuffer filteredSubBuffer; IR_Encoder *pairMuteEncoders[IR_PAIR_MUTE_MAX_ENCODERS]{}; uint8_t pairMuteEncoderCount = 0; static constexpr uint8_t kPulseFilterHoldCap = 6; @@ -131,7 +129,6 @@ private: uint8_t pulseFilterHoldCount = 0; bool pulseFilterLastRawValid = false; uint32_t pulseFilterLastRawTime = 0; - uint32_t pulseFilterDropFilteredOverflow = 0; uint32_t pulseFilterDropHoldOverflow = 0; uint32_t pulseFilterDropGlitchPairs = 0; static constexpr uint8_t kPreambleLockNeed = (uint8_t)IR_PREAMBLE_LOCK_RISE_PERIODS; @@ -197,12 +194,12 @@ bool isReciveRaw = false; void checkTimeout(); // /** В очередях/hold фильтра ещё есть фронты — не оценивать таймаут по micros()-lastEdgeTime (ложный TIMEOUT). */ bool rxTimeoutPipelineBusy() const; - /** Один сырой фронт из subBuffer -> потоковый holdback-антиглитч. */ - void pulseFilterFeedOneRaw(const FrontStorage &e); - void pulseFilterFlushTimeout(uint32_t nowUs); - bool pulseFilterEmit(const FrontStorage &e); + void pulseFilterPushRaw(const FrontStorage &e); + bool pulseFilterTryTakeConfirmed(FrontStorage &out, uint32_t logTime = 0); + bool pulseFilterTryFlushOne(uint32_t nowUs, FrontStorage &out); void pulseFilterShiftLeft(uint8_t n); void pulseFilterReset(); + void processDecodedFront(const FrontStorage ¤tFront); static uint32_t absDiffU32(uint32_t a, uint32_t b); bool registerPairMuteEncoder(IR_Encoder *enc); void refreshPairMuteState(); diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index ce9f163..e116c49 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -1,7 +1,24 @@ #include "IR_Encoder.h" #include "IR_DecoderRaw.h" +#include "IrTxIsrBufferedStorage.h" #include +#if defined(_MSC_VER) +#define IRPROTO_PRAGMA_MESSAGE(text) __pragma(message(text)) +#else +#define IRPROTO_PRAGMA_MESSAGE(text) _Pragma(#text) +#endif + +#if defined(ARDUINO_ARCH_STM32) + #if defined(STM32G4xx) +IRPROTO_PRAGMA_MESSAGE(message("[IR-protocol] TX backends: ISR + built-in DMA")) + #elif defined(STM32F4xx) +IRPROTO_PRAGMA_MESSAGE(message("[IR-protocol] TX backends: ISR only")) + #else +IRPROTO_PRAGMA_MESSAGE(message("[IR-protocol] TX backends: ISR")) + #endif +#endif + #define LoopOut 12 #define ISR_Out 10 #define TestOut 13 @@ -14,6 +31,7 @@ IR_Encoder::IR_Encoder(uint8_t pin, uint16_t addr, IR_DecoderRaw *decPair, bool { setPin(pin); id = addr; + txIsrMode_ = txIsrLegacyMode_ ? TxIsrMode::Legacy : TxIsrMode::Buffered; this->decPair = decPair; if (decPair != nullptr) { @@ -45,7 +63,7 @@ HardwareTimer* IR_Encoder::IR_Timer = nullptr; IR_Encoder::ExternalTxStartFn IR_Encoder::externalTxStartFn = nullptr; IR_Encoder::ExternalTxBusyFn IR_Encoder::externalTxBusyFn = nullptr; void *IR_Encoder::externalTxCtx = nullptr; -bool IR_Encoder::txIsrLegacyMode_ = false; +bool IR_Encoder::txIsrLegacyMode_ = true; uint16_t IR_Encoder::s_carrierMultiply = 2; void IR_Encoder::setCarrierMultiply(uint16_t multiply) @@ -146,6 +164,11 @@ bool IR_Encoder::scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, si void IR_Encoder::setTxIsrLegacyMode(bool legacy) { txIsrLegacyMode_ = legacy; + const TxIsrMode mode = legacy ? TxIsrMode::Legacy : TxIsrMode::Buffered; + for (IR_Encoder *p = head; p != nullptr; p = p->next) + { + p->txIsrMode_ = mode; + } } bool IR_Encoder::txIsrLegacyMode() @@ -153,6 +176,54 @@ bool IR_Encoder::txIsrLegacyMode() return txIsrLegacyMode_; } +void IR_Encoder::attachBufferedIsrStorage(IrTxIsrBufferedStorageBase& storage) +{ + txBufferedCtx_ = &storage; +} + +void IR_Encoder::detachBufferedIsrStorage() +{ + txBufferedCtx_ = nullptr; + if (!isSending) + { + txActiveBufferedCtx_ = nullptr; + txUseBufferedIsr_ = false; + } +} + +bool IR_Encoder::hasBufferedIsrStorage() const +{ + return txBufferedCtx_ != nullptr && txBufferedCtx_->isValid(); +} + +void IR_Encoder::enableBufferedIsr(IrTxIsrBufferedStorageBase& storage) +{ + attachBufferedIsrStorage(storage); + txIsrMode_ = TxIsrMode::Buffered; +} + +void IR_Encoder::disableBufferedIsr() +{ + txIsrMode_ = TxIsrMode::Legacy; + if (!isSending) + { + txActiveBufferedCtx_ = nullptr; + txUseBufferedIsr_ = false; + } +} + +IR_Encoder::TxIsrMode IR_Encoder::txIsrMode() const +{ + return txIsrMode_; +} + +bool IR_Encoder::shouldUseBufferedIsr() const +{ + return txIsrMode_ == TxIsrMode::Buffered && + txBufferedCtx_ != nullptr && + txBufferedCtx_->isValid(); +} + bool IR_Encoder::txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal) { while (true) @@ -336,6 +407,8 @@ void IR_Encoder::externalFinishSend() } isSending = false; + txUseBufferedIsr_ = false; + txActiveBufferedCtx_ = nullptr; refreshBlindDecoderMuteState(); } @@ -704,6 +777,8 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) } sendLen = len; + txUseBufferedIsr_ = false; + txActiveBufferedCtx_ = nullptr; isSending = true; refreshBlindDecoderMuteState(); @@ -727,7 +802,11 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) } sendLen = len; - if (txIsrLegacyMode_) + const bool useBufferedIsr = shouldUseBufferedIsr(); + txUseBufferedIsr_ = useBufferedIsr; + txActiveBufferedCtx_ = useBufferedIsr ? txBufferedCtx_ : nullptr; + + if (!useBufferedIsr) { toggleCounter = preambToggle; dataBitCounter = bitPerByte - 1; @@ -756,22 +835,36 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) return; } - size_t nRuns = buildGateRuns(sendBuffer, len, txGateRuns_, irproto::kIsrTxMaxGateRuns); - if (nRuns == 0U) + IrTxIsrBufferedStorageBase* buf = txActiveBufferedCtx_; + if (buf == nullptr || !buf->isValid()) { + txUseBufferedIsr_ = false; + txActiveBufferedCtx_ = nullptr; return; } - if (!scaleGateRunsToPhysical(txGateRuns_, &nRuns, irproto::kIsrTxMaxGateRuns, carrierMultiply())) + + buf->resetRuntimeState(); + + size_t nRuns = buildGateRuns(sendBuffer, len, buf->gateRuns, buf->maxGateRuns); + if (nRuns == 0U) { + txUseBufferedIsr_ = false; + txActiveBufferedCtx_ = nullptr; + return; + } + if (!scaleGateRunsToPhysical(buf->gateRuns, &nRuns, buf->maxGateRuns, carrierMultiply())) + { + txUseBufferedIsr_ = false; + txActiveBufferedCtx_ = nullptr; return; } uint32_t total = 0; for (size_t i = 0; i < nRuns; i++) { - total += txGateRuns_[i].lenTicks; + total += buf->gateRuns[i].lenTicks; } - txBsrrTotalTicks_ = total; + buf->totalTicks = total; const uint32_t setW = (uint32_t)mask; const uint32_t resetW = ((uint32_t)mask) << 16U; @@ -780,13 +873,8 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) const uint16_t cap = maxPowerNumerator(); txPowerSnap_ = (powerNumerator_ > cap) ? cap : powerNumerator_; } - txBsrrWave_.configure(setW, resetW, txGateRuns_, nRuns, txMultiplySnap_, txPowerSnap_); - - txBsrrHalfLen_ = (uint16_t)(irproto::kIsrTxBsrrWordCount / 2U); - txBsrrWave_.fill(txBsrrWords_, irproto::kIsrTxBsrrWordCount); - - txBsrrReadIdx_ = 0; - txBsrrTicksSent_ = 0; + buf->wave.configure(setW, resetW, buf->gateRuns, nRuns, txMultiplySnap_, txPowerSnap_); + buf->wave.fill(buf->bsrrWords, buf->wordCount); isSending = true; refreshBlindDecoderMuteState(); @@ -815,7 +903,7 @@ void IR_Encoder::_isr() if (port == nullptr) return; - if (txIsrLegacyMode_) + if (!txUseBufferedIsr_) { const uint32_t setW = (uint32_t)mask; const uint32_t resetW = ((uint32_t)mask) << 16U; @@ -850,33 +938,49 @@ void IR_Encoder::_isr() { port->BSRR = resetW; isSending = false; + txUseBufferedIsr_ = false; + txActiveBufferedCtx_ = nullptr; refreshBlindDecoderMuteState(); carrierStopPending = true; } return; } - port->BSRR = txBsrrWords_[txBsrrReadIdx_]; - txBsrrReadIdx_++; - txBsrrTicksSent_++; - - if (txBsrrTicksSent_ >= txBsrrTotalTicks_) + IrTxIsrBufferedStorageBase* buf = txActiveBufferedCtx_; + if (buf == nullptr || !buf->isValid()) { port->BSRR = ((uint32_t)mask) << 16U; isSending = false; + txUseBufferedIsr_ = false; + txActiveBufferedCtx_ = nullptr; refreshBlindDecoderMuteState(); carrierStopPending = true; return; } - if (txBsrrReadIdx_ == txBsrrHalfLen_) + port->BSRR = buf->bsrrWords[buf->readIdx]; + buf->readIdx++; + buf->ticksSent++; + + if (buf->ticksSent >= buf->totalTicks) { - txBsrrWave_.fill(&txBsrrWords_[0], txBsrrHalfLen_); + port->BSRR = ((uint32_t)mask) << 16U; + isSending = false; + txUseBufferedIsr_ = false; + txActiveBufferedCtx_ = nullptr; + refreshBlindDecoderMuteState(); + carrierStopPending = true; + return; } - else if (txBsrrReadIdx_ >= irproto::kIsrTxBsrrWordCount) + + if (buf->readIdx == buf->halfLen) { - txBsrrReadIdx_ = 0; - txBsrrWave_.fill(&txBsrrWords_[txBsrrHalfLen_], txBsrrHalfLen_); + buf->wave.fill(&buf->bsrrWords[0], buf->halfLen); + } + else if (buf->readIdx >= buf->wordCount) + { + buf->readIdx = 0; + buf->wave.fill(&buf->bsrrWords[buf->halfLen], buf->halfLen); } } diff --git a/IR_Encoder.h b/IR_Encoder.h index ff5ced4..9123a33 100644 --- a/IR_Encoder.h +++ b/IR_Encoder.h @@ -1,6 +1,6 @@ #pragma once #include "IR_config.h" -#include "IrTxBsrrWave.h" +#include "IrTxGateTypes.h" // TODO: Отложенная передача после завершения приема @@ -14,6 +14,7 @@ struct IR_SendResult { }; class IR_DecoderRaw; +class IrTxIsrBufferedStorageBase; class IR_Encoder : public IR_FOX { friend IR_DecoderRaw; @@ -24,6 +25,10 @@ public: static HardwareTimer* IR_Timer; using IR_TxGateRun = IrTxGateRun; + enum class TxIsrMode : uint8_t { + Legacy = 0, + Buffered = 1 + }; using ExternalTxBusyFn = bool (*)(void *ctx); using ExternalTxStartFn = bool (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len); @@ -69,10 +74,19 @@ public: /** * Режим внутреннего TX без DMA: false — BSRR + кольцо (buildGateRuns + scaleGateRunsToPhysical); * true — FSM «налету» + скважность несущей как у буферного пути (подшаги multiply/2 на шаг FSM). - * Выставить до begin/rawSend (глобально на все IR_Encoder). Игнорируется при externalTxStartFn. + * По умолчанию включён legacy=true для обратной совместимости. Вызов меняет default и обновляет + * все зарегистрированные encoder-объекты. Buffered ISR реально используется только если у encoder + * привязан storage через attachBufferedIsrStorage()/enableBufferedIsr(). + * Выставить до begin/rawSend. Игнорируется при externalTxStartFn. */ static void setTxIsrLegacyMode(bool legacy); static bool txIsrLegacyMode(); + void attachBufferedIsrStorage(IrTxIsrBufferedStorageBase& storage); + void detachBufferedIsrStorage(); + bool hasBufferedIsrStorage() const; + void enableBufferedIsr(IrTxIsrBufferedStorageBase& storage); + void disableBufferedIsr(); + TxIsrMode txIsrMode() const; /** Optional: register external TX backend (e.g. DMA driver). */ static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx); @@ -175,14 +189,7 @@ private: static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut); void loadTxFsmFromMembers(TxFsmState &st) const; void storeTxFsmToMembers(const TxFsmState &st); - - IrTxBsrrWave txBsrrWave_{}; - IR_TxGateRun txGateRuns_[irproto::kIsrTxMaxGateRuns]{}; - uint32_t txBsrrWords_[irproto::kIsrTxBsrrWordCount]{}; - uint16_t txBsrrReadIdx_ = 0; - uint16_t txBsrrHalfLen_ = 0; - uint32_t txBsrrTotalTicks_ = 0; - uint32_t txBsrrTicksSent_ = 0; + bool shouldUseBufferedIsr() const; /** Снимок на старт TX (буферный и legacy путь). */ uint16_t txPowerSnap_ = 1; @@ -194,6 +201,10 @@ private: uint16_t legacySlotInPeriod_ = 0; volatile uint16_t powerNumerator_ = 1; + IrTxIsrBufferedStorageBase* txBufferedCtx_ = nullptr; + IrTxIsrBufferedStorageBase* txActiveBufferedCtx_ = nullptr; + TxIsrMode txIsrMode_ = TxIsrMode::Legacy; + bool txUseBufferedIsr_ = false; IR_DecoderRaw *decPair = nullptr; IR_DecoderRaw *singleBlindDecoder = nullptr; diff --git a/IrDmaTxStm32.h b/IrDmaTxStm32.h index f14435d..9c6699c 100644 --- a/IrDmaTxStm32.h +++ b/IrDmaTxStm32.h @@ -5,6 +5,14 @@ #if defined(ARDUINO_ARCH_STM32) && defined(STM32G4xx) +#if defined(_MSC_VER) +#define IRPROTO_DMA_PRAGMA_MESSAGE(text) __pragma(message(text)) +#else +#define IRPROTO_DMA_PRAGMA_MESSAGE(text) _Pragma(#text) +#endif + +IRPROTO_DMA_PRAGMA_MESSAGE(message("[IR-protocol] TX path available: built-in DMA")) + #include #include diff --git a/IrTxIsrBufferedStorage.h b/IrTxIsrBufferedStorage.h new file mode 100644 index 0000000..96f8d29 --- /dev/null +++ b/IrTxIsrBufferedStorage.h @@ -0,0 +1,64 @@ +#pragma once + +#include "IR_config.h" +#include "IrTxBsrrWave.h" + +class IrTxIsrBufferedStorageBase { +public: + IrTxGateRun* gateRuns = nullptr; + size_t maxGateRuns = 0; + uint32_t* bsrrWords = nullptr; + uint16_t wordCount = 0; + + IrTxBsrrWave wave{}; + uint16_t readIdx = 0; + uint16_t halfLen = 0; + uint32_t totalTicks = 0; + uint32_t ticksSent = 0; + + bool isValid() const { + return gateRuns != nullptr && + maxGateRuns != 0U && + bsrrWords != nullptr && + wordCount >= 2U && + (wordCount & 1U) == 0U; + } + + void resetRuntimeState() { + readIdx = 0; + halfLen = static_cast(wordCount / 2U); + totalTicks = 0; + ticksSent = 0; + } +}; + +class IrTxIsrBufferedStorageView : public IrTxIsrBufferedStorageBase { +public: + IrTxIsrBufferedStorageView(IrTxGateRun* runs, size_t runCount, uint32_t* words, uint16_t wordsCount) { + gateRuns = runs; + maxGateRuns = runCount; + bsrrWords = words; + wordCount = wordsCount; + resetRuntimeState(); + } +}; + +template +class IrTxIsrBufferedStorage : public IrTxIsrBufferedStorageBase { + static_assert(MaxGateRuns > 0U, "IrTxIsrBufferedStorage: MaxGateRuns > 0"); + static_assert(WordCount >= 2U, "IrTxIsrBufferedStorage: WordCount >= 2"); + static_assert((WordCount & 1U) == 0U, "IrTxIsrBufferedStorage: WordCount must be even"); + +public: + IrTxIsrBufferedStorage() { + gateRuns = gateRunsStorage_; + maxGateRuns = MaxGateRuns; + bsrrWords = bsrrWordsStorage_; + wordCount = WordCount; + resetRuntimeState(); + } + +private: + IrTxGateRun gateRunsStorage_[MaxGateRuns]{}; + uint32_t bsrrWordsStorage_[WordCount]{}; +}; diff --git a/ref/IR_DMA_TX_backend.md b/ref/IR_DMA_TX_backend.md index e8d566d..8ecb9f9 100644 --- a/ref/IR_DMA_TX_backend.md +++ b/ref/IR_DMA_TX_backend.md @@ -1,5 +1,7 @@ # Контракт бэкенда DMA-TX ИК (`IrDmaTxStm32`) +См. также: [IR_TX_MODES.md](IR_TX_MODES.md) — общая схема выбора `legacy ISR`, `buffered ISR` и `external backend`. + Платформа: **STM32G4**, Arduino STM32. Передача: **DMA memory → GPIO BSRR**, запрос от **TIM UPDATE** (частота `carrierFrec×2` из `IR_Encoder::beginClockOnly`). ### Число потоков (шаблон) diff --git a/ref/IR_RX_BRIEF_LOG.md b/ref/IR_RX_BRIEF_LOG.md index ffd7414..f8369c9 100644 --- a/ref/IR_RX_BRIEF_LOG.md +++ b/ref/IR_RX_BRIEF_LOG.md @@ -55,7 +55,7 @@ IRRX t=1234988 rsn=MUTE_END cnt=42 ## Когда смотреть подробный debug - `listenStart` / `checkTimeout` — в конце обработки фронта (`END:`) и во ветке «нет фронта» в `tick()`; не в начале до `pop`, иначе после таймаута `lastEdgeTime` расходится с метками ISR из очереди → ложные `TIMEOUT` (`bits=0`). -- Пока в `subBuffer` / `filteredSubBuffer` или в hold фильтра есть необработанные фронты, таймаут по `micros() - lastEdgeTime` **не оценивается** (`rxTimeoutPipelineBusy`): иначе при хвосте очереди «тихая пауза» считается слишком длинной и снова ложный `TIMEOUT`. +- Пока в `subBuffer` или в hold фильтра есть необработанные фронты, таймаут по `micros() - lastEdgeTime` **не оценивается** (`rxTimeoutPipelineBusy`): иначе при хвосте очереди «тихая пауза» считается слишком длинной и снова ложный `TIMEOUT`. - Если нужен полный поток битов и sync: включать `IRDEBUG_SERIAL_PACK` - Если нужно понять, какие именно фронты пришли в ISR: включать `IR_EDGE_TRACE` - `IR_RX_BRIEF_LOG` нужен как короткий always-on-ish индикатор сути проблемы, без длинного дампа From 2f5b57680b3a6f59e95ac5639ed22121d7bc5206 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Mon, 20 Apr 2026 14:49:38 +0300 Subject: [PATCH 2/4] Add documentation for IR TX modes in the library --- ref/IR_TX_MODES.md | 146 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 ref/IR_TX_MODES.md diff --git a/ref/IR_TX_MODES.md b/ref/IR_TX_MODES.md new file mode 100644 index 0000000..b435c17 --- /dev/null +++ b/ref/IR_TX_MODES.md @@ -0,0 +1,146 @@ +# Режимы IR TX в библиотеке + +Этот документ описывает, как в библиотеке выбирается путь передачи IR и как его правильно использовать в проектах Arduino STM32. + +## Кратко + +У библиотеки есть три варианта TX: + +- `legacy ISR` — внутренний ISR-путь без внешнего backend. Это путь по умолчанию для обратной совместимости. +- `buffered ISR` — внутренний ISR-путь с предварительной подготовкой BSRR-слов и кольцевым буфером. +- `external backend` — передача делегируется проекту через `IR_Encoder::setExternalTxBackend(...)`, например в DMA backend. + +Порядок выбора такой: + +1. Если зарегистрирован `external backend`, используется он. +2. Иначе используется внутренний ISR библиотеки. +3. Для внутреннего ISR: + - по умолчанию включён `legacy ISR` + - `buffered ISR` включается явно: нужно привязать storage к encoder и переключить режим + +## 1. Legacy ISR + +Это режим по умолчанию. Старые проекты могут ничего не менять: + +```cpp +static HardwareTimer timer(TIM11); +static IR_Encoder enc(PA9, 42, &dec); + +void setup() { + IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0); + enc.enable(); +} +``` + +Если проект не регистрирует внешний backend и не переключает режим явно, библиотека работает в `legacy ISR`. + +Для явного выбора можно написать: + +```cpp +IR_Encoder::setTxIsrLegacyMode(true); +IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0); +``` + +## 2. Buffered ISR + +Этот режим использует внутренний буферный ISR-путь библиотеки. Он включается только явно: + +```cpp +#include + +static IrTxIsrBufferedStorage<> txStorage; + +enc.enableBufferedIsr(txStorage); +IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0); +``` + +Нижнеуровневый вариант API — отдельно привязать storage через `attachBufferedIsrStorage(...)`, но в обычном проекте удобнее использовать `enableBufferedIsr(...)`. + +Смысл режима: + +- пакет сначала превращается в `gate runs` +- затем в поток слов `GPIO->BSRR` +- ISR выдаёт готовые слова из кольцевого буфера + +### Важно про RAM + +В текущей реализации память под буферный ISR вынесена из `IR_Encoder` в отдельный storage-объект. + +То есть: + +- `legacy ISR` не тянет buffered-буферы в RAM самого `IR_Encoder` +- память под `gate runs` и `BSRR words` появляется только там, где проект сам создал `IrTxIsrBufferedStorage<>` + +Это важно для STM32 с небольшим объёмом RAM, например для `STM32F401`. + +## 3. External backend + +Если проект хочет полностью взять TX на себя, библиотека позволяет зарегистрировать внешний backend: + +```cpp +static bool txBusy(void* ctx); +static bool txStart(void* ctx, IR_Encoder* enc, const uint8_t* packet, uint8_t len); + +void setup() { + IR_Encoder::beginClockOnly(&timer); + IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr); + enc.enable(); +} +``` + +После вызова `setExternalTxBackend(...)` библиотека больше не использует свои внутренние ISR-пути для фактической передачи. + +В этом режиме: + +- `setTxIsrLegacyMode(true/false)` игнорируется +- завершение передачи должен сигнализировать сам backend через `enc->externalFinishSend()` + +Подробности по встроенному DMA backend для `STM32G4xx`: см. [IR_DMA_TX_backend.md](IR_DMA_TX_backend.md). + +## Когда какой режим использовать + +### Старый проект, который ничего не настраивает + +Использовать как есть: + +```cpp +IR_Encoder::begin(...); +``` + +Итог: `legacy ISR` + +### Нужен новый внутренний буферный ISR + +Включить явно: + +```cpp +#include + +static IrTxIsrBufferedStorage<> txStorage; + +enc.enableBufferedIsr(txStorage); +IR_Encoder::begin(...); +``` + +Итог: `buffered ISR` + +### Нужен проектный DMA или другой свой транспорт + +Подключить внешний backend: + +```cpp +IR_Encoder::beginClockOnly(...); +IR_Encoder::setExternalTxBackend(...); +``` + +Итог: `external backend` + +## Рекомендация для совместимости + +Для старых проектов безопаснее не вызывать `setTxIsrLegacyMode(false)`, если нет явной причины переходить на buffered ISR. + +Если задача — сохранить старое поведение без неожиданного роста нагрузки на TX-логику, оставляйте default `legacy ISR` или задавайте его явно: + +```cpp +IR_Encoder::setTxIsrLegacyMode(true); +``` From 38d93edd1b52fef41601cd16a1fa0c7babb23bcb Mon Sep 17 00:00:00 2001 From: DashyFox Date: Mon, 20 Apr 2026 17:47:33 +0300 Subject: [PATCH 3/4] upd --- IR_config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IR_config.h b/IR_config.h index 855d8d5..d585d9e 100644 --- a/IR_config.h +++ b/IR_config.h @@ -21,7 +21,7 @@ static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be eve // Краткий лог причин, почему физический сигнал не дошёл до распознанного пакета. // Формат и коды: ref/IR_RX_BRIEF_LOG.md #ifndef IR_RX_BRIEF_LOG -#define IR_RX_BRIEF_LOG 1 +#define IR_RX_BRIEF_LOG 0 #endif // 1: печатать только отклонённые/ошибочные события; успехи и шумовые PREAMB скрыть. #ifndef IR_RX_BRIEF_LOG_REJECT_ONLY From 8361828c44c9b283dc1da470cc672ecc904e1c45 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Tue, 21 Apr 2026 10:00:46 +0300 Subject: [PATCH 4/4] tx error tracking and inline glitch filter --- IR_Encoder.cpp | 213 ++++++++++++++++++++++++++++++++++++++++-------- IR_Encoder.h | 43 +++++++--- IrDmaTxStm32.h | 32 +++----- IrTxGateTypes.h | 4 +- 4 files changed, 228 insertions(+), 64 deletions(-) diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index e116c49..872e47e 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -66,6 +66,41 @@ void *IR_Encoder::externalTxCtx = nullptr; bool IR_Encoder::txIsrLegacyMode_ = true; uint16_t IR_Encoder::s_carrierMultiply = 2; +const char* irSendStatusToString(IR_SendStatus status) +{ + switch (status) + { + case IR_SendStatus::Success: + return "Success"; + case IR_SendStatus::PayloadTooLarge: + return "PayloadTooLarge"; + case IR_SendStatus::EncoderBusy: + return "EncoderBusy"; + case IR_SendStatus::BufferTooLarge: + return "BufferTooLarge"; + case IR_SendStatus::ExternalBackendBusy: + return "ExternalBackendBusy"; + case IR_SendStatus::ExternalStartFailed: + return "ExternalStartFailed"; + case IR_SendStatus::ExternalNoStream: + return "ExternalNoStream"; + case IR_SendStatus::ExternalInvalidConfig: + return "ExternalInvalidConfig"; + case IR_SendStatus::BuildGateRunsFailed: + return "BuildGateRunsFailed"; + case IR_SendStatus::ScaleGateRunsFailed: + return "ScaleGateRunsFailed"; + case IR_SendStatus::DmaStartFailed: + return "DmaStartFailed"; + case IR_SendStatus::EncoderPinUnavailable: + return "EncoderPinUnavailable"; + case IR_SendStatus::BufferedStorageInvalid: + return "BufferedStorageInvalid"; + default: + return "Unknown"; + } +} + void IR_Encoder::setCarrierMultiply(uint16_t multiply) { if (multiply < 2) @@ -465,6 +500,107 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu return runCount; } +size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply) +{ + if (packet == nullptr || outRuns == nullptr || maxRuns == 0) + { + return 0; + } + if (len == 0 || len > dataByteSizeMax) + { + return 0; + } + if (multiply < 2) + { + multiply = 2; + } + + // Copy into fixed-size buffer to match original encoder behavior (safe reads past sendLen). + uint8_t sendBufferLocal[dataByteSizeMax] = {0}; + memcpy(sendBufferLocal, packet, len); + + TxFsmState st{}; + st.sendLen = len; + st.toggleCounter = preambToggle; + st.dataBitCounter = bitPerByte - 1; + st.dataByteCounter = 0; + st.preambFrontCounter = preambPulse * 2 - 1; + st.dataSequenceCounter = bitPerByte * 2; + st.syncSequenceCounter = syncBits * 2; + st.syncLastBit = false; + st.signal = preamb; + st.state = HIGH; + st.currentBitSequence = bitHigh; + + auto appendPhysicalRun = [&](bool gate, uint32_t logicalLen, size_t& runCount) -> bool { + if (logicalLen == 0) + { + return true; + } + + uint32_t phys = (logicalLen * (uint32_t)multiply) / 2U; + if (logicalLen > 0 && phys == 0) + { + phys = 1; + } + + while (phys > 0) + { + if (runCount >= maxRuns) + { + return false; + } + + const uint32_t chunk = phys > 65535U ? 65535U : phys; + outRuns[runCount].gate = gate; + outRuns[runCount].lenTicks = static_cast(chunk); + runCount++; + phys -= chunk; + } + return true; + }; + + size_t runCount = 0; + bool currentGate = false; + uint32_t currentLogicalLen = 0; + bool havePendingRun = false; + bool isActive = true; + while (isActive) + { + bool gate = false; + isActive = txEmitTick(st, sendBufferLocal, gate); + + if (!havePendingRun) + { + currentGate = gate; + currentLogicalLen = 1U; + havePendingRun = true; + continue; + } + + if (currentGate == gate) + { + currentLogicalLen++; + continue; + } + + if (!appendPhysicalRun(currentGate, currentLogicalLen, runCount)) + { + return 0; + } + + currentGate = gate; + currentLogicalLen = 1U; + } + + if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount)) + { + return 0; + } + + return runCount; +} + void IR_Encoder::enable() { @@ -555,7 +691,7 @@ IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8 if (len > bytePerPack) { Serial.println("IR Pack to big"); - return IR_SendResult(false, 0); + return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge); } constexpr uint8_t dataStart = msgBytes + addrBytes + addrBytes; memset(sendBuffer, 0x00, dataByteSizeMax); @@ -605,11 +741,15 @@ IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8 // } // отправка - rawSend(sendBuffer, packSize); - + const IR_SendStatus status = rawSend(sendBuffer, packSize); + if (status != IR_SendStatus::Success) + { + return IR_SendResult(false, 0, status); + } + // Возвращаем результат отправки uint32_t sendTime = calculateSendTime(packSize); - return IR_SendResult(true, sendTime); + return IR_SendResult(true, sendTime, status); } @@ -633,11 +773,15 @@ IR_SendResult IR_Encoder::sendAccept(uint16_t addrTo, uint8_t customByte) sendBuffer[4] = crc8(sendBuffer, 0, 4, poly1) & 0xFF; sendBuffer[5] = crc8(sendBuffer, 0, 5, poly2) & 0xFF; - rawSend(sendBuffer, packsize); - + const IR_SendStatus status = rawSend(sendBuffer, packsize); + if (status != IR_SendStatus::Success) + { + return IR_SendResult(false, 0, status); + } + // Возвращаем результат отправки uint32_t sendTime = calculateSendTime(packsize); - return IR_SendResult(true, sendTime); + return IR_SendResult(true, sendTime, status); } IR_SendResult IR_Encoder::sendRequest(uint16_t addrTo) @@ -659,11 +803,15 @@ IR_SendResult IR_Encoder::sendRequest(uint16_t addrTo) sendBuffer[5] = crc8(sendBuffer, 0, 5, poly1) & 0xFF; sendBuffer[6] = crc8(sendBuffer, 0, 6, poly2) & 0xFF; - rawSend(sendBuffer, packsize); - + const IR_SendStatus status = rawSend(sendBuffer, packsize); + if (status != IR_SendStatus::Success) + { + return IR_SendResult(false, 0, status); + } + // Возвращаем результат отправки uint32_t sendTime = calculateSendTime(packsize); - return IR_SendResult(true, sendTime); + return IR_SendResult(true, sendTime, status); } IR_SendResult IR_Encoder::sendBack(uint8_t data) @@ -685,7 +833,7 @@ IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *d { if (len > bytePerPack) { - return IR_SendResult(false, 0); + return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge); } memset(sendBuffer, 0x00, dataByteSizeMax); uint8_t dataStart = msgBytes + addrBytes + (isAdressed ? addrBytes : 0); @@ -716,11 +864,15 @@ IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *d sendBuffer[packSize - crcBytes + 1] = crc8(sendBuffer, 0, packSize - crcBytes + 1, poly2) & 0xFF; // отправка - rawSend(sendBuffer, packSize); - + const IR_SendStatus status = rawSend(sendBuffer, packSize); + if (status != IR_SendStatus::Success) + { + return IR_SendResult(false, 0, status); + } + // Возвращаем результат отправки uint32_t sendTime = calculateSendTime(packSize); - return IR_SendResult(true, sendTime); + return IR_SendResult(true, sendTime, status); } void IR_Encoder::registerWithBlindDecoders() @@ -747,18 +899,18 @@ void IR_Encoder::refreshBlindDecoderMuteState() } } -void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) +IR_SendStatus IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) { if (isSending) { // TODO: Обработка повторной отправки - return; + return IR_SendStatus::EncoderBusy; } // Проверка на переполнение буфера if (len > dataByteSizeMax) { - return; + return IR_SendStatus::BufferTooLarge; } // Serial.print("IR tx hex: "); @@ -773,7 +925,7 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) { if (externalTxBusyFn != nullptr && externalTxBusyFn(externalTxCtx)) { - return; + return IR_SendStatus::ExternalBackendBusy; } sendLen = len; @@ -782,18 +934,18 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) isSending = true; refreshBlindDecoderMuteState(); - const bool ok = externalTxStartFn(externalTxCtx, this, ptr, len); - if (!ok) + const IR_SendStatus status = externalTxStartFn(externalTxCtx, this, ptr, len); + if (status != IR_SendStatus::Success) { isSending = false; refreshBlindDecoderMuteState(); } - return; + return status; } if (port == nullptr || mask == 0) { - return; + return IR_SendStatus::EncoderPinUnavailable; } if (ptr != sendBuffer) @@ -832,7 +984,7 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) isSending = true; refreshBlindDecoderMuteState(); IR_Encoder::carrierResume(); - return; + return IR_SendStatus::Success; } IrTxIsrBufferedStorageBase* buf = txActiveBufferedCtx_; @@ -840,23 +992,18 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) { txUseBufferedIsr_ = false; txActiveBufferedCtx_ = nullptr; - return; + return IR_SendStatus::BufferedStorageInvalid; } buf->resetRuntimeState(); - size_t nRuns = buildGateRuns(sendBuffer, len, buf->gateRuns, buf->maxGateRuns); + txMultiplySnap_ = carrierMultiply(); + size_t nRuns = buildPhysicalGateRuns(sendBuffer, len, buf->gateRuns, buf->maxGateRuns, txMultiplySnap_); if (nRuns == 0U) { txUseBufferedIsr_ = false; txActiveBufferedCtx_ = nullptr; - return; - } - if (!scaleGateRunsToPhysical(buf->gateRuns, &nRuns, buf->maxGateRuns, carrierMultiply())) - { - txUseBufferedIsr_ = false; - txActiveBufferedCtx_ = nullptr; - return; + return IR_SendStatus::BuildGateRunsFailed; } uint32_t total = 0; @@ -868,7 +1015,6 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) const uint32_t setW = (uint32_t)mask; const uint32_t resetW = ((uint32_t)mask) << 16U; - txMultiplySnap_ = carrierMultiply(); { const uint16_t cap = maxPowerNumerator(); txPowerSnap_ = (powerNumerator_ > cap) ? cap : powerNumerator_; @@ -883,6 +1029,7 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) port->BSRR = resetW; } IR_Encoder::carrierResume(); + return IR_SendStatus::Success; } void IR_Encoder::isr() diff --git a/IR_Encoder.h b/IR_Encoder.h index 9123a33..bb21d4b 100644 --- a/IR_Encoder.h +++ b/IR_Encoder.h @@ -4,13 +4,34 @@ // TODO: Отложенная передача после завершения приема +enum class IR_SendStatus : uint8_t { + Success = 0, + PayloadTooLarge, + EncoderBusy, + BufferTooLarge, + ExternalBackendBusy, + ExternalStartFailed, + ExternalNoStream, + ExternalInvalidConfig, + BuildGateRunsFailed, + ScaleGateRunsFailed, + DmaStartFailed, + EncoderPinUnavailable, + BufferedStorageInvalid, +}; + +const char* irSendStatusToString(IR_SendStatus status); + // Структура для возврата результата отправки struct IR_SendResult { - bool success; // Флаг успешности отправки - uint32_t sendTimeMs; // Время отправки пакета в миллисекундах - - IR_SendResult(bool success = false, uint32_t sendTimeMs = 0) - : success(success), sendTimeMs(sendTimeMs) {} + bool success; // Флаг успешности отправки + uint32_t sendTimeMs; // Время отправки пакета в миллисекундах + IR_SendStatus status; // Детализированный статус старта передачи + + IR_SendResult(bool success = false, + uint32_t sendTimeMs = 0, + IR_SendStatus status = IR_SendStatus::ExternalStartFailed) + : success(success), sendTimeMs(sendTimeMs), status(status) {} }; class IR_DecoderRaw; @@ -31,7 +52,7 @@ public: }; using ExternalTxBusyFn = bool (*)(void *ctx); - using ExternalTxStartFn = bool (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len); + using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len); private: // uint16_t id; /// @brief Адрес передатчика public: @@ -62,7 +83,7 @@ public: /** p∈[0,100] → ближайший допустимый числитель; 100% даёт N = maxPowerNumerator(). */ void setPowerPercent(uint8_t p); - /** После buildGateRuns: lenTicks в тактах 2×Fc → физические тики (carrierFrec×multiply). Может разбить сегменты. */ + /** Legacy helper: lenTicks в тактах 2×Fc → физические тики (carrierFrec×multiply). Может разбить сегменты. */ static bool scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, size_t maxRuns, uint16_t multiply); /** Configure timer frequency for TX clock (carrierFrec × multiply) without attaching ISR. */ @@ -72,7 +93,7 @@ public: static void tick(); /** - * Режим внутреннего TX без DMA: false — BSRR + кольцо (buildGateRuns + scaleGateRunsToPhysical); + * Режим внутреннего TX без DMA: false — BSRR + кольцо (direct physical gate-runs builder); * true — FSM «налету» + скважность несущей как у буферного пути (подшаги multiply/2 на шаг FSM). * По умолчанию включён legacy=true для обратной совместимости. Вызов меняет default и обновляет * все зарегистрированные encoder-объекты. Buffered ISR реально используется только если у encoder @@ -94,8 +115,10 @@ public: /** Called by external TX backend on actual end of transmission. */ void externalFinishSend(); - /** Build RLE runs of carrier gate for a packet (no HW access). */ + /** Build RLE runs of carrier gate for a packet in logical 2×Fc ticks (no HW access). */ static size_t buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns); + /** Build RLE runs directly in physical carrierFrec×multiply ticks (DMA/buffered ISR path). */ + static size_t buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply); void enable(); void disable(); @@ -108,7 +131,7 @@ public: "IR_Encoder::setBlindDecoders: array size exceeds IR_PAIR_MUTE_MAX_ENCODERS"); setBlindDecoders(decoders, static_cast(N)); } - void rawSend(uint8_t *ptr, uint8_t len); + IR_SendStatus rawSend(uint8_t *ptr, uint8_t len); IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false); IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false); diff --git a/IrDmaTxStm32.h b/IrDmaTxStm32.h index 9c6699c..8b01eb3 100644 --- a/IrDmaTxStm32.h +++ b/IrDmaTxStm32.h @@ -106,14 +106,14 @@ public: return true; } - bool start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) { - if (enc == nullptr) return false; + IR_SendStatus start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) { + if (enc == nullptr) return IR_SendStatus::ExternalNoStream; for (uint8_t i = 0; i < streamCount_; i++) { if (streams_[i].enc == enc) { return startStream(streams_[i], packet, len); } } - return false; + return IR_SendStatus::ExternalNoStream; } void irqForStream(size_t streamIndex) { @@ -290,28 +290,22 @@ private: return true; } - bool startStream(TxStream& s, const uint8_t* packet, uint8_t len) { - if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return false; - if (s.active) return false; - if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return false; - if (s.runs == nullptr || s.maxRuns == 0) return false; + IR_SendStatus startStream(TxStream& s, const uint8_t* packet, uint8_t len) { + if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig; + if (s.active) return IR_SendStatus::EncoderBusy; + if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig; + if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig; s.resetWave(); - s.runCount = IR_Encoder::buildGateRuns(packet, len, s.runs, s.maxRuns); - if (s.runCount == 0) return false; - - size_t rc = s.runCount; - if (!IR_Encoder::scaleGateRunsToPhysical(s.runs, &rc, s.maxRuns, IR_Encoder::carrierMultiply())) { - return false; - } - s.runCount = rc; + const uint16_t mult = IR_Encoder::carrierMultiply(); + s.runCount = IR_Encoder::buildPhysicalGateRuns(packet, len, s.runs, s.maxRuns, mult); + if (s.runCount == 0) return IR_SendStatus::BuildGateRunsFailed; uint32_t total = 0; for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks; s.totalTicks = total; - const uint16_t mult = IR_Encoder::carrierMultiply(); uint16_t pwr = mult / 2U; if (s.enc != nullptr) { const uint16_t want = s.enc->powerNumerator(); @@ -327,13 +321,13 @@ private: const uint32_t dst = u32ptr(&s.port->BSRR); if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) { - return false; + return IR_SendStatus::DmaStartFailed; } s.active = true; activeCount_++; startTimerIfNeeded(); - return true; + return IR_SendStatus::Success; } void stopStream(TxStream& s) { diff --git a/IrTxGateTypes.h b/IrTxGateTypes.h index 78c3682..d5bba86 100644 --- a/IrTxGateTypes.h +++ b/IrTxGateTypes.h @@ -4,8 +4,8 @@ /** * Один RLE-сегмент огибающей несущей. - * В buildGateRuns: lenTicks в тактах логической шкалы 2×carrierFrec (как раньше). - * После IR_Encoder::scaleGateRunsToPhysical — в физических тиках carrierFrec×multiply. + * В legacy buildGateRuns: lenTicks в тактах логической шкалы 2×carrierFrec. + * В современном DMA/buffered ISR пути buildPhysicalGateRuns строит lenTicks сразу в физических тиках carrierFrec×multiply. */ struct IrTxGateRun { uint16_t lenTicks;