mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-18 19:13:58 +00:00
fix(ir-dma): eliminate isSending latch — free-running TIM, completion watchdog, busy() OR
C: remove racy activeCount_ + shared-timer gating; TIM17 free-running, per-stream DMA arming only (HAL_DMA_Start_IT/Abort_IT). Sets active before arming; never stops TIM on a shared counter. D: poll() watchdog detects ticksOutput stall (>=stallTimeoutMs_, default 100ms) and forceStop()s the stuck stream under a per-channel NVIC mask; recoveryCount()/setStallTimeoutMs() accessors. dmaErrorCb now checks !active for symmetric single-stop. E: busy() now returns true if ANY stream active (was AND) — honest contract for irAnyTxBusy()/carrier retune; does not serialize the send path. Bug: IR TX rarely latched (LED frozen on/off) because stopStream was reachable only from the DMA HT/CPLT IRQ. Hardware bring-up (logic analyzer, recoveryCount telemetry) pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
IrDmaTxStm32.h
102
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, без разделяемого счётчика.
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user