diff --git a/IrDmaTxStm32.h b/IrDmaTxStm32.h index 8b01eb3..56436a0 100644 --- a/IrDmaTxStm32.h +++ b/IrDmaTxStm32.h @@ -85,7 +85,6 @@ public: } s_instance = this; - activeCount_ = 0; for (uint8_t i = 0; i < streamCount_; i++) { HAL_NVIC_EnableIRQ(streams_[i].dmaIrq); @@ -98,12 +97,19 @@ public: 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 { - if (streamCount_ == 0) return false; for (uint8_t i = 0; i < streamCount_; i++) { - if (!streams_[i].active) return false; + if (streams_[i].active) return true; } - return true; + return false; } IR_SendStatus start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) { @@ -126,6 +132,35 @@ public: 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{}; @@ -152,6 +187,10 @@ private: 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() { @@ -186,29 +225,38 @@ private: TxStream streams_[MaxStreams]{}; uint8_t streamCount_ = 0; - volatile uint8_t activeCount_ = 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; } - void startTimerIfNeeded() { + // Fix C: TIM держим free-running. Включаем ИДЕМПОТЕНТНО при старте потока и НИКОГДА не + // останавливаем по разделяемому счётчику (счётчик убран → гонка поток↔ISR устранена). + // Счётчик TIM НЕ сбрасываем: сброс глитчил бы уже активный поток; для ИК ≤1 период несущей + // фазового сдвига пренебрежим. В простое (ни один DMA-канал не заармлен) update-события TIM + // генерят DMA-запросы «в никуда» — безвредно, в GPIO ничего не пишется, светодиод не дёргается. + void ensureTimerRunning() { if (htim_ == nullptr) return; - if (activeCount_ != 1) return; - - __HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE); - __HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE); - __HAL_TIM_SET_COUNTER(htim_, 0); __HAL_TIM_ENABLE_DMA(htim_, TIM_DMA_UPDATE); - HAL_TIM_Base_Start(htim_); + if ((htim_->Instance->CR1 & TIM_CR1_CEN) == 0U) { + __HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE); + HAL_TIM_Base_Start(htim_); + } } - void stopTimerIfIdle() { - if (htim_ == nullptr) return; - if (activeCount_ != 0) return; - - __HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE); - HAL_TIM_Base_Stop(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) { @@ -241,7 +289,7 @@ private: static void dmaErrorCb(DMA_HandleTypeDef* hdma) { auto* s = streamFromDma(hdma); - if (s == nullptr) return; + if (s == nullptr || !s->active) return; // симметрично HT/CPLT: одиночный stop, без двойного finish s->onError(); if (s_instance != nullptr) { s_instance->stopStream(*s); @@ -319,14 +367,20 @@ private: 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; } - s.active = true; - activeCount_++; - startTimerIfNeeded(); + ensureTimerRunning(); return IR_SendStatus::Success; } @@ -343,9 +397,7 @@ private: if (s.enc != nullptr) { s.enc->externalFinishSend(); } - - if (activeCount_ > 0) activeCount_--; - stopTimerIfIdle(); + // Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика. } };