#pragma once #include "IR_Encoder.h" #include "IrTxBsrrWave.h" #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 #if defined(__GNUC__) #define IR_DMA_TX_HOT __attribute__((always_inline)) inline #else #define IR_DMA_TX_HOT inline #endif #include "stm32g4xx_hal.h" /** * STM32G4: ИК TX через DMA в GPIO BSRR, такт от TIM UPDATE (carrierFrec × IR_Encoder::carrierMultiply()). * * Число слотов потоков — параметр шаблона (без макросов), например IrDmaTxStm32<2>. * По умолчанию: IrDmaTxStm32<> ≡ irproto::kDefaultDmaTxMaxStreams (см. IR_config.h). * * Контракт: ref/IR_DMA_TX_backend.md */ template class IrDmaTxStm32 { static_assert(MaxStreams >= 1U, "IrDmaTxStm32: MaxStreams >= 1"); public: struct StreamCfg { DMA_Channel_TypeDef* instance = nullptr; IRQn_Type irq = IRQn_Type(0); uint32_t dmamuxRequest = 0; IR_Encoder* enc = nullptr; uint32_t* dmaWords = nullptr; uint16_t dmaWordCount = 0; IrTxGateRun* gateRuns = nullptr; size_t maxGateRuns = 0; }; struct Config { HardwareTimer* timer = nullptr; uint8_t streamCount = 0; StreamCfg streams[MaxStreams]; }; bool begin(const Config& cfg) { cfg_ = cfg; streamCount_ = cfg.streamCount; if (cfg_.timer == nullptr || streamCount_ == 0) return false; if (streamCount_ > MaxStreams) return false; htim_ = cfg_.timer->getHandle(); if (htim_ == nullptr) return false; for (uint8_t i = 0; i < streamCount_; i++) { const StreamCfg& sc = cfg_.streams[i]; if (sc.enc == nullptr || sc.instance == nullptr) return false; if (sc.dmaWords == nullptr || sc.dmaWordCount < 2U) return false; if ((sc.dmaWordCount & 1U) != 0U) return false; if (sc.gateRuns == nullptr || sc.maxGateRuns == 0U) return false; } __HAL_RCC_DMA1_CLK_ENABLE(); __HAL_RCC_DMAMUX1_CLK_ENABLE(); for (uint8_t i = 0; i < streamCount_; i++) { const StreamCfg& sc = cfg_.streams[i]; if (!initStream(streams_[i], sc)) { return false; } } s_instance = this; for (uint8_t i = 0; i < streamCount_; i++) { HAL_NVIC_EnableIRQ(streams_[i].dmaIrq); } return true; } static IrDmaTxStm32* instance() { return s_instance; } // Fix E (контракт busy): "занят" = АКТИВЕН ХОТЯ БЫ ОДИН поток (OR). // Раньше было AND (все потоки сразу) — почти всегда false, не отражало занятости и было // избыточно (per-stream s.active в startStream + per-encoder isSending уже не дают повторный // старт того же потока). OR нужен, чтобы irAnyTxBusy()/applyCarrierMultiply реально дожидались // простоя ВСЕХ эмиттеров перед ретюном несущей. // ВНИМАНИЕ: это НЕ сериализует независимые front/rear — путь отправки гейтится per-stream; // внешний busy()-гейт в IR_Encoder::rawSend намеренно нейтрализован в прошивке (txBusy()=false). // Чтобы ВКЛЮЧИТЬ сериализацию front/rear — вернуть txBusy()=busy() в Car/src/IR/IR.cpp. bool busy() const { for (uint8_t i = 0; i < streamCount_; i++) { if (streams_[i].active) return true; } 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 IR_SendStatus::ExternalNoStream; } void irqForStream(size_t streamIndex) { if (streamIndex >= streamCount_) return; HAL_DMA_IRQHandler(&streams_[streamIndex].hdma); } DMA_HandleTypeDef* dmaHandle(size_t streamIndex) { if (streamIndex >= streamCount_) return nullptr; return &streams_[streamIndex].hdma; } // Fix D: не-IRQ страховка завершения. Вызывать из главного цикла (IR_Module::tick()). // Штатно isSending сбрасывает только DMA HT/CPLT IRQ; если он не сработает (пропуск/слипание // флагов, Transfer-Error, последствие любой гонки) — поток «залипает» навсегда. Здесь ловим // застой прогресса ticksOutput по стенным часам и форсим stopStream как аварийное // восстановление. Это recovery (не prevention): фикс C убирает саму гонку, а poll() страхует // структурный SPOF завершения и делает редкий баг наблюдаемым (recoveryCount()). void poll() { const uint32_t now = millis(); for (uint8_t i = 0; i < streamCount_; i++) { TxStream& s = streams_[i]; if (!s.active) continue; // atomic: bool const uint32_t t = s.ticksOutput; // atomic: выровненный u32 на Cortex-M if (t != s.lastTicks) { s.lastTicks = t; s.lastProgressMs = now; continue; } if ((uint32_t)(now - s.lastProgressMs) >= stallTimeoutMs_) { forceStop(s); } } } /** Порог застоя для watchdog (мс). По умолчанию 100; должен быть заметно больше времени вывода одной половины DMA-буфера на самой медленной несущей. 0 игнорируется. */ void setStallTimeoutMs(uint32_t ms) { if (ms != 0U) stallTimeoutMs_ = ms; } /** Сколько раз watchdog аварийно восстановил залипший поток (наблюдаемость/логи). */ uint32_t recoveryCount() const { return recoveries_; } private: struct TxStream { DMA_HandleTypeDef hdma{}; DMA_Channel_TypeDef* dmaInstance = nullptr; IRQn_Type dmaIrq = IRQn_Type(0); uint32_t dmamuxRequest = 0; IR_Encoder* enc = nullptr; GPIO_TypeDef* port = nullptr; uint16_t mask = 0; uint32_t setWord = 0; uint32_t resetWord = 0; uint32_t* dmaBuf = nullptr; uint16_t bufLen = 0; uint16_t halfLen = 0; IrTxGateRun* runs = nullptr; size_t maxRuns = 0; size_t runCount = 0; IrTxBsrrWave wave{}; uint32_t totalTicks = 0; volatile uint32_t ticksOutput = 0; // Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока). uint32_t lastTicks = 0; uint32_t lastProgressMs = 0; bool active = false; void resetWave() { wave.configure(setWord, resetWord, nullptr, 0, 2, 1); ticksOutput = 0; totalTicks = 0; runCount = 0; } IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) { wave.fill(dst, count); } void onHalf() { ticksOutput += halfLen; fill(&dmaBuf[0], halfLen); } void onComplete() { ticksOutput += halfLen; fill(&dmaBuf[halfLen], halfLen); } void onError() {} }; static IrDmaTxStm32* s_instance; Config cfg_{}; TIM_HandleTypeDef* htim_ = nullptr; TxStream streams_[MaxStreams]{}; uint8_t streamCount_ = 0; // Fix C: разделяемый activeCount_ удалён — он и был источником гонки поток↔ISR. uint32_t stallTimeoutMs_ = 100; // Fix D uint32_t recoveries_ = 0; // Fix D (только контекст потока) static uint32_t u32ptr(const volatile void* p) { return (uint32_t)(uintptr_t)p; } // Fix C: TIM держим free-running. Включаем ИДЕМПОТЕНТНО при старте потока и НИКОГДА не // останавливаем по разделяемому счётчику (счётчик убран → гонка поток↔ISR устранена). // Счётчик TIM НЕ сбрасываем: сброс глитчил бы уже активный поток; для ИК ≤1 период несущей // фазового сдвига пренебрежим. В простое (ни один DMA-канал не заармлен) update-события TIM // генерят DMA-запросы «в никуда» — безвредно, в GPIO ничего не пишется, светодиод не дёргается. void ensureTimerRunning() { if (htim_ == nullptr) return; __HAL_TIM_ENABLE_DMA(htim_, TIM_DMA_UPDATE); if ((htim_->Instance->CR1 & TIM_CR1_CEN) == 0U) { __HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE); HAL_TIM_Base_Start(htim_); } } // Fix D: аварийный стоп залипшего потока из контекста потока (не ISR). Маскируем ИМЕННО IRQ // этого DMA-канала, чтобы штатный HT/CPLT/Error-обработчик не вытеснил нас посреди stopStream // (иначе двойной abort/externalFinishSend). Другие потоки и прочие IRQ не трогаем. void forceStop(TxStream& s) { HAL_NVIC_DisableIRQ(s.dmaIrq); if (s.active) { stopStream(s); recoveries_++; } HAL_NVIC_EnableIRQ(s.dmaIrq); } static TxStream* streamFromDma(DMA_HandleTypeDef* hdma) { if (s_instance == nullptr || hdma == nullptr) return nullptr; for (uint8_t i = 0; i < s_instance->streamCount_; i++) { if (hdma == &s_instance->streams_[i].hdma) { return &s_instance->streams_[i]; } } return nullptr; } static void dmaHalfCpltCb(DMA_HandleTypeDef* hdma) { auto* s = streamFromDma(hdma); if (s == nullptr || !s->active) return; s->onHalf(); if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) { s_instance->stopStream(*s); } } static void dmaCpltCb(DMA_HandleTypeDef* hdma) { auto* s = streamFromDma(hdma); if (s == nullptr || !s->active) return; s->onComplete(); if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) { s_instance->stopStream(*s); } } static void dmaErrorCb(DMA_HandleTypeDef* hdma) { auto* s = streamFromDma(hdma); if (s == nullptr || !s->active) return; // симметрично HT/CPLT: одиночный stop, без двойного finish s->onError(); if (s_instance != nullptr) { s_instance->stopStream(*s); } } bool initStream(TxStream& s, const StreamCfg& chCfg) { s.enc = chCfg.enc; s.dmaInstance = chCfg.instance; s.dmaIrq = chCfg.irq; s.dmamuxRequest = chCfg.dmamuxRequest; s.dmaBuf = chCfg.dmaWords; s.bufLen = chCfg.dmaWordCount; s.halfLen = (uint16_t)(chCfg.dmaWordCount / 2U); s.runs = chCfg.gateRuns; s.maxRuns = chCfg.maxGateRuns; s.port = (s.enc != nullptr) ? s.enc->getPort() : nullptr; s.mask = (s.enc != nullptr) ? s.enc->getPinMask() : 0; s.setWord = (uint32_t)s.mask; s.resetWord = ((uint32_t)s.mask) << 16; s.resetWave(); s.hdma.Instance = s.dmaInstance; s.hdma.Init.Request = s.dmamuxRequest; s.hdma.Init.Direction = DMA_MEMORY_TO_PERIPH; s.hdma.Init.PeriphInc = DMA_PINC_DISABLE; s.hdma.Init.MemInc = DMA_MINC_ENABLE; s.hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; s.hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; s.hdma.Init.Mode = DMA_CIRCULAR; s.hdma.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_DeInit(&s.hdma); if (HAL_DMA_Init(&s.hdma) != HAL_OK) { return false; } s.hdma.XferHalfCpltCallback = dmaHalfCpltCb; s.hdma.XferCpltCallback = dmaCpltCb; s.hdma.XferErrorCallback = dmaErrorCb; s.hdma.XferAbortCallback = nullptr; return true; } 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(); 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; uint16_t pwr = mult / 2U; if (s.enc != nullptr) { const uint16_t want = s.enc->powerNumerator(); const uint16_t cap = IR_Encoder::maxPowerNumerator(); pwr = (want > cap) ? cap : want; } s.wave.configure(s.setWord, s.resetWord, s.runs, s.runCount, mult, pwr); s.fill(&s.dmaBuf[0], s.bufLen); s.port->BSRR = s.resetWord; // Fix C/D: помечаем active и инициализируем watchdog ДО арминга DMA — иначе ранний // HT/CPLT IRQ при active==false выйдет из callback'а и пропустит refill → недосчёт // ticksOutput → латч. TIM уже (или станет) free-running, старт по счётчику не нужен. s.lastTicks = 0; s.lastProgressMs = millis(); s.active = true; 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) { s.active = false; return IR_SendStatus::DmaStartFailed; } ensureTimerRunning(); return IR_SendStatus::Success; } void stopStream(TxStream& s) { if (!s.active) return; s.active = false; HAL_DMA_Abort_IT(&s.hdma); if (s.port != nullptr) { s.port->BSRR = s.resetWord; } if (s.enc != nullptr) { s.enc->externalFinishSend(); } // Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика. } }; template IrDmaTxStm32* IrDmaTxStm32::s_instance = nullptr; inline void IrDmaTxStm32_onDmaHandle(DMA_HandleTypeDef* hdma) { HAL_DMA_IRQHandler(hdma); } #elif defined(ARDUINO_ARCH_STM32) #error "IrDmaTxStm32: добавьте ветку HAL для вашей серии STM32 (сейчас только STM32G4xx)." #else template class IrDmaTxStm32 {}; #endif