mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-18 19:13:58 +00:00
- IrDmaTxStm32.h: TxStream::active -> volatile (делится поток<->ISR: busy()/poll()/колбэки/спин applyCarrierMultiply) - IrDmaTxStm32.h: + volatile errors_ и errorCount(), инкремент в dmaErrorCb (наблюдаемость Transfer-Error) - test_examples/longData: dmaBackend.poll() в loop, иначе watchdog завершения DMA мёртв в эталонном примере Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
425 lines
16 KiB
C++
425 lines
16 KiB
C++
#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 <Arduino.h>
|
||
#include <HardwareTimer.h>
|
||
|
||
#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<size_t MaxStreams = irproto::kDefaultDmaTxMaxStreams>
|
||
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<MaxStreams>* 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_; }
|
||
/** Сколько раз DMA-передача завершилась по Transfer-Error (наблюдаемость/логи). */
|
||
uint32_t errorCount() const { return errors_; }
|
||
|
||
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;
|
||
|
||
volatile bool active = false; // Fix: делится поток<->ISR (busy()/poll()/колбэки/спин applyCarrierMultiply) — запрет кэширования компилятором
|
||
|
||
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<MaxStreams>* 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 (только контекст потока)
|
||
volatile uint32_t errors_ = 0; // Fix #5: счётчик Transfer-Error (пишется в ISR dmaErrorCb, читается из потока)
|
||
|
||
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->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error
|
||
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<size_t MaxStreams>
|
||
IrDmaTxStm32<MaxStreams>* IrDmaTxStm32<MaxStreams>::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<size_t MaxStreams = irproto::kDefaultDmaTxMaxStreams>
|
||
class IrDmaTxStm32 {};
|
||
|
||
#endif
|