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:
100
IrDmaTxStm32.h
100
IrDmaTxStm32.h
@ -85,7 +85,6 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
s_instance = this;
|
s_instance = this;
|
||||||
activeCount_ = 0;
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < streamCount_; i++) {
|
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||||
HAL_NVIC_EnableIRQ(streams_[i].dmaIrq);
|
HAL_NVIC_EnableIRQ(streams_[i].dmaIrq);
|
||||||
@ -98,12 +97,19 @@ public:
|
|||||||
return s_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 {
|
bool busy() const {
|
||||||
if (streamCount_ == 0) return false;
|
|
||||||
for (uint8_t i = 0; i < streamCount_; i++) {
|
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) {
|
IR_SendStatus start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) {
|
||||||
@ -126,6 +132,35 @@ public:
|
|||||||
return &streams_[streamIndex].hdma;
|
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:
|
private:
|
||||||
struct TxStream {
|
struct TxStream {
|
||||||
DMA_HandleTypeDef hdma{};
|
DMA_HandleTypeDef hdma{};
|
||||||
@ -152,6 +187,10 @@ private:
|
|||||||
uint32_t totalTicks = 0;
|
uint32_t totalTicks = 0;
|
||||||
volatile uint32_t ticksOutput = 0;
|
volatile uint32_t ticksOutput = 0;
|
||||||
|
|
||||||
|
// Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока).
|
||||||
|
uint32_t lastTicks = 0;
|
||||||
|
uint32_t lastProgressMs = 0;
|
||||||
|
|
||||||
bool active = false;
|
bool active = false;
|
||||||
|
|
||||||
void resetWave() {
|
void resetWave() {
|
||||||
@ -186,29 +225,38 @@ private:
|
|||||||
TxStream streams_[MaxStreams]{};
|
TxStream streams_[MaxStreams]{};
|
||||||
uint8_t streamCount_ = 0;
|
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) {
|
static uint32_t u32ptr(const volatile void* p) {
|
||||||
return (uint32_t)(uintptr_t)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 (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_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_);
|
HAL_TIM_Base_Start(htim_);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void stopTimerIfIdle() {
|
// Fix D: аварийный стоп залипшего потока из контекста потока (не ISR). Маскируем ИМЕННО IRQ
|
||||||
if (htim_ == nullptr) return;
|
// этого DMA-канала, чтобы штатный HT/CPLT/Error-обработчик не вытеснил нас посреди stopStream
|
||||||
if (activeCount_ != 0) return;
|
// (иначе двойной abort/externalFinishSend). Другие потоки и прочие IRQ не трогаем.
|
||||||
|
void forceStop(TxStream& s) {
|
||||||
__HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE);
|
HAL_NVIC_DisableIRQ(s.dmaIrq);
|
||||||
HAL_TIM_Base_Stop(htim_);
|
if (s.active) {
|
||||||
|
stopStream(s);
|
||||||
|
recoveries_++;
|
||||||
|
}
|
||||||
|
HAL_NVIC_EnableIRQ(s.dmaIrq);
|
||||||
}
|
}
|
||||||
|
|
||||||
static TxStream* streamFromDma(DMA_HandleTypeDef* hdma) {
|
static TxStream* streamFromDma(DMA_HandleTypeDef* hdma) {
|
||||||
@ -241,7 +289,7 @@ private:
|
|||||||
|
|
||||||
static void dmaErrorCb(DMA_HandleTypeDef* hdma) {
|
static void dmaErrorCb(DMA_HandleTypeDef* hdma) {
|
||||||
auto* s = streamFromDma(hdma);
|
auto* s = streamFromDma(hdma);
|
||||||
if (s == nullptr) return;
|
if (s == nullptr || !s->active) return; // симметрично HT/CPLT: одиночный stop, без двойного finish
|
||||||
s->onError();
|
s->onError();
|
||||||
if (s_instance != nullptr) {
|
if (s_instance != nullptr) {
|
||||||
s_instance->stopStream(*s);
|
s_instance->stopStream(*s);
|
||||||
@ -319,14 +367,20 @@ private:
|
|||||||
|
|
||||||
s.port->BSRR = s.resetWord;
|
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);
|
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) {
|
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;
|
return IR_SendStatus::DmaStartFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
s.active = true;
|
ensureTimerRunning();
|
||||||
activeCount_++;
|
|
||||||
startTimerIfNeeded();
|
|
||||||
return IR_SendStatus::Success;
|
return IR_SendStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,9 +397,7 @@ private:
|
|||||||
if (s.enc != nullptr) {
|
if (s.enc != nullptr) {
|
||||||
s.enc->externalFinishSend();
|
s.enc->externalFinishSend();
|
||||||
}
|
}
|
||||||
|
// Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика.
|
||||||
if (activeCount_ > 0) activeCount_--;
|
|
||||||
stopTimerIfIdle();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user