From a6ba1168fa693d176ba1324982d05ff5351a7841 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sat, 17 Jan 2026 21:32:53 -0600 Subject: [PATCH 01/11] WIP: Attempt circular DShot DMA during settings save This approach doesn't prevent DShot interruption during EEPROM writes. Committing for potential future refinement. Changes: - Added impl_timerPWMSetDMACircular() to switch DMA mode at runtime - Modified processDelayedSave() to use circular mode during writeEEPROM() - Called pwmCompleteMotorUpdate() 3x to latch DShot 0 packets Issue: DShot still shows gaps during settings save on oscilloscope. Next approach: Test with simple GPIO high instead of DShot. --- src/main/drivers/pwm_output.c | 14 ++++++++++++++ src/main/drivers/pwm_output.h | 1 + src/main/drivers/timer_impl.h | 1 + src/main/drivers/timer_impl_hal.c | 23 +++++++++++++++++++++++ src/main/drivers/timer_impl_stdperiph.c | 20 ++++++++++++++++++++ src/main/fc/config.c | 14 ++++++++++++++ 6 files changed, 73 insertions(+) diff --git a/src/main/drivers/pwm_output.c b/src/main/drivers/pwm_output.c index 619f4b95db5..42debf6cb48 100644 --- a/src/main/drivers/pwm_output.c +++ b/src/main/drivers/pwm_output.c @@ -226,6 +226,20 @@ void pwmEnableMotors(void) pwmMotorsEnabled = true; } +void pwmSetMotorDMACircular(bool circular) +{ +#ifdef USE_DSHOT + // Set DMA circular mode for all motor outputs + for (int i = 0; i < getMotorCount(); i++) { + if (motors[i].pwmPort && motors[i].pwmPort->tch) { + impl_timerPWMSetDMACircular(motors[i].pwmPort->tch, circular); + } + } +#else + UNUSED(circular); +#endif +} + bool isMotorBrushed(uint16_t motorPwmRateHz) { return (motorPwmRateHz > 500); diff --git a/src/main/drivers/pwm_output.h b/src/main/drivers/pwm_output.h index 1041ace04fa..1dc644f7f4e 100644 --- a/src/main/drivers/pwm_output.h +++ b/src/main/drivers/pwm_output.h @@ -50,6 +50,7 @@ void pwmWriteServo(uint8_t index, uint16_t value); void pwmDisableMotors(void); void pwmEnableMotors(void); +void pwmSetMotorDMACircular(bool circular); struct timerHardware_s; void pwmMotorPreconfigure(void); diff --git a/src/main/drivers/timer_impl.h b/src/main/drivers/timer_impl.h index 4d0a87f9aa5..83a7a93f2b1 100644 --- a/src/main/drivers/timer_impl.h +++ b/src/main/drivers/timer_impl.h @@ -84,6 +84,7 @@ bool impl_timerPWMConfigChannelDMA(TCH_t * tch, void * dmaBuffer, uint8_t dmaBuf void impl_timerPWMPrepareDMA(TCH_t * tch, uint32_t dmaBufferElementCount); void impl_timerPWMStartDMA(TCH_t * tch); void impl_timerPWMStopDMA(TCH_t * tch); +void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular); #ifdef USE_DSHOT_DMAR bool impl_timerPWMConfigDMABurst(burstDmaTimer_t *burstDmaTimer, TCH_t * tch, void * dmaBuffer, uint8_t dmaBufferElementSize, uint32_t dmaBufferElementCount); diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index 8df0f7024d3..6390b4beef5 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -580,3 +580,26 @@ void impl_timerPWMStopDMA(TCH_t * tch) HAL_TIM_Base_Start(tch->timCtx->timHandle); } + +void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) +{ + if (!tch->dma || !tch->dma->dma) { + return; + } + + const uint32_t streamLL = lookupDMALLStreamTable[DMATAG_GET_STREAM(tch->timHw->dmaTag)]; + DMA_TypeDef *dmaBase = tch->dma->dma; + + // Temporarily disable DMA while modifying configuration + LL_DMA_DisableStream(dmaBase, streamLL); + + // Modify the DMA mode + if (circular) { + LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_CIRCULAR); + } else { + LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_NORMAL); + } + + // Re-enable DMA + LL_DMA_EnableStream(dmaBase, streamLL); +} diff --git a/src/main/drivers/timer_impl_stdperiph.c b/src/main/drivers/timer_impl_stdperiph.c index d2bd35dd521..2c82b052890 100644 --- a/src/main/drivers/timer_impl_stdperiph.c +++ b/src/main/drivers/timer_impl_stdperiph.c @@ -519,3 +519,23 @@ void impl_timerPWMStopDMA(TCH_t * tch) tch->dmaState = TCH_DMA_IDLE; TIM_Cmd(tch->timHw->tim, ENABLE); } + +void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) +{ + if (!tch->dma || !tch->dma->ref) { + return; + } + + // Temporarily disable DMA while modifying configuration + DMA_Cmd(tch->dma->ref, DISABLE); + + // Modify the DMA mode + if (circular) { + tch->dma->ref->CR |= DMA_SxCR_CIRC; // Set circular bit + } else { + tch->dma->ref->CR &= ~DMA_SxCR_CIRC; // Clear circular bit + } + + // Re-enable DMA + DMA_Cmd(tch->dma->ref, ENABLE); +} diff --git a/src/main/fc/config.c b/src/main/fc/config.c index d3021317ae5..b2e29caf4e0 100755 --- a/src/main/fc/config.c +++ b/src/main/fc/config.c @@ -420,7 +420,21 @@ void processDelayedSave(void) saveState = SAVESTATE_NONE; } else if (saveState == SAVESTATE_SAVEONLY) { suspendRxSignal(); + + // Prevent ESC spinup during settings save + // Switch to circular mode first + pwmSetMotorDMACircular(true); + // Force motor updates to latch current (zero) throttle into circular DMA buffer + pwmCompleteMotorUpdate(); + delayMicroseconds(200); + pwmCompleteMotorUpdate(); + delayMicroseconds(200); + pwmCompleteMotorUpdate(); + writeEEPROM(); + + pwmSetMotorDMACircular(false); + resumeRxSignal(); saveState = SAVESTATE_NONE; } From d32cf254e68c718e61680acaa6f6917e8cdccc9a Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 18 Jan 2026 00:04:47 -0600 Subject: [PATCH 02/11] Fix ESC spinup during MSP settings save (#10913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move circular DShot DMA code from processDelayedSave() to writeConfigToEEPROM(). This ensures the fix works for MSP_EEPROM_WRITE commands, not just delayed saves. The MSP call path is: MSP_EEPROM_WRITE → writeEEPROM() → writeConfigToEEPROM() → writeSettingsToEEPROM() The previous commit (a6ba1168f) had circular DMA in processDelayedSave(), which is only called for delayed saves (on disarm), not MSP commands. Changes: - Move circular DMA setup to writeConfigToEEPROM() in config_eeprom.c - Remove unused pwmSetMotorPinsHigh() function - Add pwm_output.h include to config_eeprom.c Test method: - MSP_EEPROM_WRITE command sent once per second - DShot signal monitored on oscilloscope - Confirmed: DShot no longer interrupted during settings save Issue: #10913 Related: #9441 --- src/main/config/config_eeprom.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/config/config_eeprom.c b/src/main/config/config_eeprom.c index 6c1dfc3dc7e..38b35937019 100755 --- a/src/main/config/config_eeprom.c +++ b/src/main/config/config_eeprom.c @@ -33,6 +33,7 @@ #include "drivers/system.h" #include "drivers/flash.h" +#include "drivers/pwm_output.h" #include "fc/config.h" @@ -321,6 +322,16 @@ static bool writeSettingsToEEPROM(void) void writeConfigToEEPROM(void) { + // Prevent ESC spinup during settings save using circular DMA + pwmSetMotorDMACircular(true); + + // Force motor updates to latch current (zero) throttle into circular DMA buffer + pwmCompleteMotorUpdate(); + delayMicroseconds(200); + pwmCompleteMotorUpdate(); + delayMicroseconds(200); + pwmCompleteMotorUpdate(); + bool success = false; // write it for (int attempt = 0; attempt < 3 && !success; attempt++) { @@ -333,6 +344,9 @@ void writeConfigToEEPROM(void) } } + // Restore normal DMA mode + pwmSetMotorDMACircular(false); + if (success && isEEPROMContentValid()) { return; } From 4249b5576587211f32fd06ce85aa7b00c34a04f6 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 18 Jan 2026 15:35:17 -0600 Subject: [PATCH 03/11] Address code review feedback for ESC spinup fix Based on code-reviewer agent feedback: 1. Add missing AT32 platform implementation - Implement impl_timerPWMSetDMACircular() for AT32F43x targets - Uses AT32 loop_mode (ctrl_bit.lm) instead of DMA_SxCR_CIRC 2. Remove duplicate circular DMA code from config.c - processDelayedSave() calls writeEEPROM() which calls writeConfigToEEPROM() - writeConfigToEEPROM() already has circular DMA protection - Removed redundant nested enable/disable from config.c 3. Add ATOMIC_BLOCK protection to DMA mode switch - Consistent with existing impl_timerPWMStopDMA() pattern - Prevents interrupt interference during DMA reconfiguration - Applied to HAL, StdPeriph, and AT32 implementations Issue: #10913 --- src/main/drivers/timer_impl_hal.c | 23 +++++++++++--------- src/main/drivers/timer_impl_stdperiph.c | 23 +++++++++++--------- src/main/drivers/timer_impl_stdperiph_at32.c | 23 ++++++++++++++++++++ src/main/fc/config.c | 16 +------------- 4 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index 6390b4beef5..0036a6d50fb 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -590,16 +590,19 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) const uint32_t streamLL = lookupDMALLStreamTable[DMATAG_GET_STREAM(tch->timHw->dmaTag)]; DMA_TypeDef *dmaBase = tch->dma->dma; - // Temporarily disable DMA while modifying configuration - LL_DMA_DisableStream(dmaBase, streamLL); + // Protect DMA reconfiguration from interrupt interference + ATOMIC_BLOCK(NVIC_PRIO_MAX) { + // Temporarily disable DMA while modifying configuration + LL_DMA_DisableStream(dmaBase, streamLL); - // Modify the DMA mode - if (circular) { - LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_CIRCULAR); - } else { - LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_NORMAL); - } + // Modify the DMA mode + if (circular) { + LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_CIRCULAR); + } else { + LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_NORMAL); + } - // Re-enable DMA - LL_DMA_EnableStream(dmaBase, streamLL); + // Re-enable DMA + LL_DMA_EnableStream(dmaBase, streamLL); + } } diff --git a/src/main/drivers/timer_impl_stdperiph.c b/src/main/drivers/timer_impl_stdperiph.c index 2c82b052890..d3bb0555004 100644 --- a/src/main/drivers/timer_impl_stdperiph.c +++ b/src/main/drivers/timer_impl_stdperiph.c @@ -526,16 +526,19 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) return; } - // Temporarily disable DMA while modifying configuration - DMA_Cmd(tch->dma->ref, DISABLE); + // Protect DMA reconfiguration from interrupt interference + ATOMIC_BLOCK(NVIC_PRIO_MAX) { + // Temporarily disable DMA while modifying configuration + DMA_Cmd(tch->dma->ref, DISABLE); - // Modify the DMA mode - if (circular) { - tch->dma->ref->CR |= DMA_SxCR_CIRC; // Set circular bit - } else { - tch->dma->ref->CR &= ~DMA_SxCR_CIRC; // Clear circular bit - } + // Modify the DMA mode + if (circular) { + tch->dma->ref->CR |= DMA_SxCR_CIRC; // Set circular bit + } else { + tch->dma->ref->CR &= ~DMA_SxCR_CIRC; // Clear circular bit + } - // Re-enable DMA - DMA_Cmd(tch->dma->ref, ENABLE); + // Re-enable DMA + DMA_Cmd(tch->dma->ref, ENABLE); + } } diff --git a/src/main/drivers/timer_impl_stdperiph_at32.c b/src/main/drivers/timer_impl_stdperiph_at32.c index 0cc194897d9..b74fd4f18a4 100644 --- a/src/main/drivers/timer_impl_stdperiph_at32.c +++ b/src/main/drivers/timer_impl_stdperiph_at32.c @@ -407,3 +407,26 @@ void impl_timerPWMStopDMA(TCH_t * tch) tch->dmaState = TCH_DMA_IDLE; tmr_counter_enable(tch->timHw->tim, TRUE); } + +void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) +{ + if (!tch->dma || !tch->dma->ref) { + return; + } + + // Protect DMA reconfiguration from interrupt interference + ATOMIC_BLOCK(NVIC_PRIO_MAX) { + // Temporarily disable DMA while modifying configuration + dma_channel_enable(tch->dma->ref, FALSE); + + // Modify the DMA loop mode (AT32's equivalent of circular mode) + if (circular) { + tch->dma->ref->ctrl_bit.lm = TRUE; // Enable loop mode + } else { + tch->dma->ref->ctrl_bit.lm = FALSE; // Disable loop mode + } + + // Re-enable DMA + dma_channel_enable(tch->dma->ref, TRUE); + } +} diff --git a/src/main/fc/config.c b/src/main/fc/config.c index b2e29caf4e0..fcc40d530e0 100755 --- a/src/main/fc/config.c +++ b/src/main/fc/config.c @@ -420,21 +420,7 @@ void processDelayedSave(void) saveState = SAVESTATE_NONE; } else if (saveState == SAVESTATE_SAVEONLY) { suspendRxSignal(); - - // Prevent ESC spinup during settings save - // Switch to circular mode first - pwmSetMotorDMACircular(true); - // Force motor updates to latch current (zero) throttle into circular DMA buffer - pwmCompleteMotorUpdate(); - delayMicroseconds(200); - pwmCompleteMotorUpdate(); - delayMicroseconds(200); - pwmCompleteMotorUpdate(); - - writeEEPROM(); - - pwmSetMotorDMACircular(false); - + writeEEPROM(); // Circular DMA protection is inside writeConfigToEEPROM() resumeRxSignal(); saveState = SAVESTATE_NONE; } From ebcd802ff1724af418065d613e05edf90d113361 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 18 Jan 2026 23:59:18 -0600 Subject: [PATCH 04/11] Fix H7 circular DMA implementation - wait for stream disable Critical fixes for STM32H7 DMA circular mode: - Wait for EN bit to actually clear before changing mode (was the primary bug) - Disable/re-enable timer DMA requests during reconfiguration - Reload DMA transfer count after mode change - Clear pending DMA flags Without these changes, the mode change was being ignored because the DMA stream was still active when we tried to modify the configuration. --- src/main/drivers/timer_impl_hal.c | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index 0036a6d50fb..5d62fc42128 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -590,11 +590,27 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) const uint32_t streamLL = lookupDMALLStreamTable[DMATAG_GET_STREAM(tch->timHw->dmaTag)]; DMA_TypeDef *dmaBase = tch->dma->dma; + // Save the current transfer count before disabling + uint32_t dataLength = LL_DMA_GetDataLength(dmaBase, streamLL); + // Protect DMA reconfiguration from interrupt interference ATOMIC_BLOCK(NVIC_PRIO_MAX) { - // Temporarily disable DMA while modifying configuration + // Disable timer DMA request first to stop new transfer triggers + LL_TIM_DisableDMAReq_CCx(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex]); + + // Disable the DMA stream LL_DMA_DisableStream(dmaBase, streamLL); + // CRITICAL: Wait for stream to actually become disabled + // The EN bit doesn't clear immediately, especially if transfer is in progress + uint32_t timeout = 10000; + while (LL_DMA_IsEnabledStream(dmaBase, streamLL) && timeout--) { + __NOP(); + } + + // Clear any pending transfer complete flags + DMA_CLEAR_FLAG(tch->dma, DMA_IT_TCIF); + // Modify the DMA mode if (circular) { LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_CIRCULAR); @@ -602,7 +618,16 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_NORMAL); } - // Re-enable DMA + // Reload the transfer count (required after mode change) + // If dataLength was 0 (transfer completed), keep it at 0 - the next motor update will reload it + if (dataLength > 0) { + LL_DMA_SetDataLength(dmaBase, streamLL, dataLength); + } + + // Re-enable DMA stream LL_DMA_EnableStream(dmaBase, streamLL); + + // Re-enable timer DMA requests + LL_TIM_EnableDMAReq_CCx(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex]); } } From 9c2c1f4483edf7c7beaadcff68ec3725647c8252 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 19 Jan 2026 01:13:42 -0600 Subject: [PATCH 05/11] Fix SITL build: Guard DShot circular DMA calls with SITL_BUILD check SITL doesn't have real PWM/motor hardware, so pwmSetMotorDMACircular() and pwmCompleteMotorUpdate() don't exist in SITL builds. Wrap these calls with #if \!defined(SITL_BUILD) to allow SITL builds to compile while preserving the ESC spinup fix for hardware builds. --- src/main/config/config_eeprom.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/config/config_eeprom.c b/src/main/config/config_eeprom.c index 38b35937019..1efadd2a3fd 100755 --- a/src/main/config/config_eeprom.c +++ b/src/main/config/config_eeprom.c @@ -322,6 +322,7 @@ static bool writeSettingsToEEPROM(void) void writeConfigToEEPROM(void) { +#if !defined(SITL_BUILD) // Prevent ESC spinup during settings save using circular DMA pwmSetMotorDMACircular(true); @@ -331,6 +332,7 @@ void writeConfigToEEPROM(void) pwmCompleteMotorUpdate(); delayMicroseconds(200); pwmCompleteMotorUpdate(); +#endif bool success = false; // write it @@ -344,8 +346,10 @@ void writeConfigToEEPROM(void) } } +#if !defined(SITL_BUILD) // Restore normal DMA mode pwmSetMotorDMACircular(false); +#endif if (success && isEEPROMContentValid()) { return; From eda9f2207fc9014419f2f754b007c23e9dece925 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 19 Jan 2026 15:21:05 -0600 Subject: [PATCH 06/11] Add DMA disable timeout handling to all platforms Address qodo-code-review feedback: Add defensive timeout checks when waiting for DMA streams/channels to disable before reconfiguring. Changes: - H7 (timer_impl_hal.c): Check if timeout expired and abort if DMA still enabled - F4/F7 (timer_impl_stdperiph.c): Add wait loop for EN bit to clear with timeout check - AT32 (timer_impl_stdperiph_at32.c): Add wait loop for chen bit to clear with timeout check This prevents potential race conditions where DMA configuration could be modified while the stream is still active, which could cause unstable behavior. --- src/main/drivers/timer_impl_hal.c | 7 +++++++ src/main/drivers/timer_impl_stdperiph.c | 13 +++++++++++++ src/main/drivers/timer_impl_stdperiph_at32.c | 13 +++++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index 5d62fc42128..85a8429f8f9 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -608,6 +608,13 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) __NOP(); } + // If timeout occurred, DMA stream is still enabled - abort reconfiguration + if (timeout == 0 && LL_DMA_IsEnabledStream(dmaBase, streamLL)) { + // Re-enable timer DMA request and return to avoid unstable state + LL_TIM_EnableDMAReq_CCx(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex]); + return; + } + // Clear any pending transfer complete flags DMA_CLEAR_FLAG(tch->dma, DMA_IT_TCIF); diff --git a/src/main/drivers/timer_impl_stdperiph.c b/src/main/drivers/timer_impl_stdperiph.c index d3bb0555004..af685d0a5ba 100644 --- a/src/main/drivers/timer_impl_stdperiph.c +++ b/src/main/drivers/timer_impl_stdperiph.c @@ -531,6 +531,19 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) // Temporarily disable DMA while modifying configuration DMA_Cmd(tch->dma->ref, DISABLE); + // Wait for DMA stream to actually be disabled + // The EN bit doesn't clear immediately, especially if transfer is in progress + uint32_t timeout = 10000; + while ((tch->dma->ref->CR & DMA_SxCR_EN) && timeout--) { + __NOP(); + } + + // If timeout occurred, DMA stream is still enabled - abort reconfiguration + if (timeout == 0 && (tch->dma->ref->CR & DMA_SxCR_EN)) { + DMA_Cmd(tch->dma->ref, ENABLE); // Re-enable and return + return; + } + // Modify the DMA mode if (circular) { tch->dma->ref->CR |= DMA_SxCR_CIRC; // Set circular bit diff --git a/src/main/drivers/timer_impl_stdperiph_at32.c b/src/main/drivers/timer_impl_stdperiph_at32.c index b74fd4f18a4..e39cef7b9ca 100644 --- a/src/main/drivers/timer_impl_stdperiph_at32.c +++ b/src/main/drivers/timer_impl_stdperiph_at32.c @@ -419,6 +419,19 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) // Temporarily disable DMA while modifying configuration dma_channel_enable(tch->dma->ref, FALSE); + // Wait for DMA channel to actually be disabled + // The enable bit doesn't clear immediately, especially if transfer is in progress + uint32_t timeout = 10000; + while (tch->dma->ref->ctrl_bit.chen && timeout--) { + __NOP(); + } + + // If timeout occurred, DMA channel is still enabled - abort reconfiguration + if (timeout == 0 && tch->dma->ref->ctrl_bit.chen) { + dma_channel_enable(tch->dma->ref, TRUE); // Re-enable and return + return; + } + // Modify the DMA loop mode (AT32's equivalent of circular mode) if (circular) { tch->dma->ref->ctrl_bit.lm = TRUE; // Enable loop mode From c7b342df488428a43a513ecdb779a123aa2348fc Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 19 Jan 2026 19:39:55 -0600 Subject: [PATCH 07/11] Fix build for targets without DShot: Add USE_DSHOT guard Some hardware targets don't compile DShot support, causing linker errors when trying to call pwmSetMotorDMACircular() and pwmCompleteMotorUpdate(). Change guard from: #if \!defined(SITL_BUILD) To: #if \!defined(SITL_BUILD) && defined(USE_DSHOT) This ensures the functions are only called on targets that actually have DShot compiled in, fixing build failures on targets like BEEROTORF4. --- src/main/config/config_eeprom.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/config/config_eeprom.c b/src/main/config/config_eeprom.c index 1efadd2a3fd..e4a22aaa581 100755 --- a/src/main/config/config_eeprom.c +++ b/src/main/config/config_eeprom.c @@ -322,7 +322,7 @@ static bool writeSettingsToEEPROM(void) void writeConfigToEEPROM(void) { -#if !defined(SITL_BUILD) +#if !defined(SITL_BUILD) && defined(USE_DSHOT) // Prevent ESC spinup during settings save using circular DMA pwmSetMotorDMACircular(true); @@ -346,7 +346,7 @@ void writeConfigToEEPROM(void) } } -#if !defined(SITL_BUILD) +#if !defined(SITL_BUILD) && defined(USE_DSHOT) // Restore normal DMA mode pwmSetMotorDMACircular(false); #endif From e7075c5b5044f81eb1799d7ffb0659eb3a3e3deb Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 19 Jan 2026 21:05:30 -0600 Subject: [PATCH 08/11] Remove unnecessary comments - config.c: Remove comment about circular DMA protection location (obvious from context) - timer_impl_stdperiph_at32.c: Remove redundant comment about loop mode (already clear from 'Enable loop mode' / 'Disable loop mode' comments) --- src/main/drivers/timer_impl_stdperiph_at32.c | 1 - src/main/fc/config.c | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/drivers/timer_impl_stdperiph_at32.c b/src/main/drivers/timer_impl_stdperiph_at32.c index e39cef7b9ca..d9433ba1f66 100644 --- a/src/main/drivers/timer_impl_stdperiph_at32.c +++ b/src/main/drivers/timer_impl_stdperiph_at32.c @@ -432,7 +432,6 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) return; } - // Modify the DMA loop mode (AT32's equivalent of circular mode) if (circular) { tch->dma->ref->ctrl_bit.lm = TRUE; // Enable loop mode } else { diff --git a/src/main/fc/config.c b/src/main/fc/config.c index fcc40d530e0..d3021317ae5 100755 --- a/src/main/fc/config.c +++ b/src/main/fc/config.c @@ -420,7 +420,7 @@ void processDelayedSave(void) saveState = SAVESTATE_NONE; } else if (saveState == SAVESTATE_SAVEONLY) { suspendRxSignal(); - writeEEPROM(); // Circular DMA protection is inside writeConfigToEEPROM() + writeEEPROM(); resumeRxSignal(); saveState = SAVESTATE_NONE; } From 7801a8e16569c32b3c929dbe9b1af84dab6a7c53 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Thu, 19 Mar 2026 13:34:12 -0500 Subject: [PATCH 09/11] Fix circular DMA reliability: IRQ handler guard, data length reload, DMAR support The circular DMA protection was unreliable because: - DMA TC interrupt handler immediately disabled the stream on every circular cycle completion, defeating the circular mode entirely - H7 could enable circular mode with zero data length (undefined per RM) - pwmCompleteMotorUpdate() rate limiter silently swallowed the zero-throttle latching calls - Burst DMA (USE_DSHOT_DMAR) targets were not handled Fixes: - Add TCH_DMA_CIRCULAR state so IRQ handlers skip stream-disable - Disable TC interrupt during circular mode as belt-and-suspenders - Always reload transfer count to buffer size when enabling circular - Load zero-throttle directly into DMA buffers, bypassing rate limiter - Add burst DMA circular support for DMAR targets (F4/F7/H7) - Disable timer DMA request during reconfiguration on all platforms - Add __DSB() barrier before re-enabling DMA streams - Add configured check consistent with pwmCompleteMotorUpdate() --- src/main/config/config_eeprom.c | 12 +-- src/main/drivers/pwm_output.c | 56 +++++++++++-- src/main/drivers/timer.h | 1 + src/main/drivers/timer_impl.h | 3 +- src/main/drivers/timer_impl_hal.c | 83 ++++++++++++++------ src/main/drivers/timer_impl_stdperiph.c | 80 ++++++++++++++++--- src/main/drivers/timer_impl_stdperiph_at32.c | 39 ++++++--- 7 files changed, 211 insertions(+), 63 deletions(-) diff --git a/src/main/config/config_eeprom.c b/src/main/config/config_eeprom.c index e4a22aaa581..608a08a0a84 100755 --- a/src/main/config/config_eeprom.c +++ b/src/main/config/config_eeprom.c @@ -323,15 +323,10 @@ static bool writeSettingsToEEPROM(void) void writeConfigToEEPROM(void) { #if !defined(SITL_BUILD) && defined(USE_DSHOT) - // Prevent ESC spinup during settings save using circular DMA + // Enable circular DMA so hardware keeps repeating zero-throttle DShot + // packets during flash writes (which block the CPU for 20-200ms). + // Without this, ESCs lose signal and may spin up or reboot. pwmSetMotorDMACircular(true); - - // Force motor updates to latch current (zero) throttle into circular DMA buffer - pwmCompleteMotorUpdate(); - delayMicroseconds(200); - pwmCompleteMotorUpdate(); - delayMicroseconds(200); - pwmCompleteMotorUpdate(); #endif bool success = false; @@ -347,7 +342,6 @@ void writeConfigToEEPROM(void) } #if !defined(SITL_BUILD) && defined(USE_DSHOT) - // Restore normal DMA mode pwmSetMotorDMACircular(false); #endif diff --git a/src/main/drivers/pwm_output.c b/src/main/drivers/pwm_output.c index 42debf6cb48..ca1838f197b 100644 --- a/src/main/drivers/pwm_output.c +++ b/src/main/drivers/pwm_output.c @@ -124,6 +124,15 @@ static timeUs_t commandPostDelay = 0; static circularBuffer_t commandsCircularBuffer; static uint8_t commandsBuff[DHSOT_COMMAND_QUEUE_SIZE]; static currentExecutingCommand_t currentExecutingCommand; + +static uint16_t prepareDshotPacket(const uint16_t value, bool requestTelemetry); +static void loadDmaBufferDshot(timerDMASafeType_t *dmaBuffer, uint16_t packet); +static void loadDmaBufferDshotStride(timerDMASafeType_t *dmaBuffer, int stride, uint16_t packet); + +#ifdef USE_DSHOT_DMAR +burstDmaTimer_t burstDmaTimers[MAX_DMA_TIMERS]; +uint8_t burstDmaTimersCount = 0; +#endif #endif static void pwmOutConfigTimer(pwmOutputPort_t * p, TCH_t * tch, uint32_t hz, uint16_t period, uint16_t value) @@ -229,12 +238,48 @@ void pwmEnableMotors(void) void pwmSetMotorDMACircular(bool circular) { #ifdef USE_DSHOT - // Set DMA circular mode for all motor outputs - for (int i = 0; i < getMotorCount(); i++) { - if (motors[i].pwmPort && motors[i].pwmPort->tch) { - impl_timerPWMSetDMACircular(motors[i].pwmPort->tch, circular); + if (!isMotorProtocolDshot()) { + return; + } + + int motorCount = getMotorCount(); + + if (circular) { + // Load zero-throttle packets directly into DMA buffers, + // bypassing the rate limiter in pwmCompleteMotorUpdate() + uint16_t packet = prepareDshotPacket(0, false); + for (int i = 0; i < motorCount; i++) { + if (motors[i].pwmPort && motors[i].pwmPort->configured) { +#ifdef USE_DSHOT_DMAR + loadDmaBufferDshotStride(&motors[i].pwmPort->dmaBurstBuffer[motors[i].pwmPort->tch->timHw->channelIndex], 4, packet); +#else + loadDmaBufferDshot(motors[i].pwmPort->dmaBuffer, packet); +#endif + } + } + } + +#ifdef USE_DSHOT_DMAR + // Burst DMA: one DMA stream per timer, shared across channels + for (int i = 0; i < burstDmaTimersCount; i++) { + burstDmaTimer_t *burstDmaTimer = &burstDmaTimers[i]; + // Find the first motor using this timer to get the TCH for DMA state + for (int m = 0; m < motorCount; m++) { + if (motors[m].pwmPort && motors[m].pwmPort->configured && motors[m].pwmPort->tch + && motors[m].pwmPort->tch->timHw->tim == burstDmaTimer->timer) { + impl_pwmBurstDMASetCircular(burstDmaTimer, motors[m].pwmPort->tch, circular, DSHOT_DMA_BUFFER_SIZE * 4); + break; + } } } +#else + // Per-channel DMA: one DMA stream per motor + for (int i = 0; i < motorCount; i++) { + if (motors[i].pwmPort && motors[i].pwmPort->configured && motors[i].pwmPort->tch) { + impl_timerPWMSetDMACircular(motors[i].pwmPort->tch, circular, DSHOT_DMA_BUFFER_SIZE); + } + } +#endif #else UNUSED(circular); #endif @@ -278,9 +323,6 @@ uint32_t getDshotHz(motorPwmProtocolTypes_e pwmProtocolType) } #ifdef USE_DSHOT_DMAR -burstDmaTimer_t burstDmaTimers[MAX_DMA_TIMERS]; -uint8_t burstDmaTimersCount = 0; - static uint8_t getBurstDmaTimerIndex(TIM_TypeDef *timer) { for (int i = 0; i < burstDmaTimersCount; i++) { diff --git a/src/main/drivers/timer.h b/src/main/drivers/timer.h index d87e0400d52..512b0d7c106 100644 --- a/src/main/drivers/timer.h +++ b/src/main/drivers/timer.h @@ -138,6 +138,7 @@ typedef enum { TCH_DMA_IDLE = 0, TCH_DMA_READY, TCH_DMA_ACTIVE, + TCH_DMA_CIRCULAR, } tchDmaState_e; // Some forward declarations for types diff --git a/src/main/drivers/timer_impl.h b/src/main/drivers/timer_impl.h index 83a7a93f2b1..6a302f57cb4 100644 --- a/src/main/drivers/timer_impl.h +++ b/src/main/drivers/timer_impl.h @@ -84,9 +84,10 @@ bool impl_timerPWMConfigChannelDMA(TCH_t * tch, void * dmaBuffer, uint8_t dmaBuf void impl_timerPWMPrepareDMA(TCH_t * tch, uint32_t dmaBufferElementCount); void impl_timerPWMStartDMA(TCH_t * tch); void impl_timerPWMStopDMA(TCH_t * tch); -void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular); +void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferSize); #ifdef USE_DSHOT_DMAR bool impl_timerPWMConfigDMABurst(burstDmaTimer_t *burstDmaTimer, TCH_t * tch, void * dmaBuffer, uint8_t dmaBufferElementSize, uint32_t dmaBufferElementCount); void impl_pwmBurstDMAStart(burstDmaTimer_t * burstDmaTimer, uint32_t BurstLength); +void impl_pwmBurstDMASetCircular(burstDmaTimer_t * burstDmaTimer, TCH_t * tch, bool circular, uint32_t dmaBufferSize); #endif \ No newline at end of file diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index 85a8429f8f9..111fa6ffb19 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -319,6 +319,12 @@ static void impl_timerDMA_IRQHandler(DMA_t descriptor) if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { TCH_t * tch = (TCH_t *)descriptor->userParam; + // In circular mode, let DMA keep running - don't disable the stream + if (tch->dmaState == TCH_DMA_CIRCULAR) { + DMA_CLEAR_FLAG(descriptor, DMA_IT_TCIF); + return; + } + // If it was ACTIVE - switch to IDLE if (tch->dmaState == TCH_DMA_ACTIVE) { tch->dmaState = TCH_DMA_IDLE; @@ -512,6 +518,46 @@ void impl_pwmBurstDMAStart(burstDmaTimer_t * burstDmaTimer, uint32_t BurstLength //LL_TIM_EnableDMAReq_UPDATE(burstDmaTimer->timer); LL_TIM_EnableDMAReq_CCx(burstDmaTimer->timer, burstDmaTimer->burstRequestSource); } + +void impl_pwmBurstDMASetCircular(burstDmaTimer_t * burstDmaTimer, TCH_t * tch, bool circular, uint32_t dmaBufferSize) +{ + if (!tch->dma || !tch->dma->dma) { + return; + } + + ATOMIC_BLOCK(NVIC_PRIO_MAX) { + LL_TIM_DisableDMAReq_CCx(burstDmaTimer->timer, burstDmaTimer->burstRequestSource); + LL_DMA_DisableStream(burstDmaTimer->dma, burstDmaTimer->streamLL); + + uint32_t timeout = 10000; + while (LL_DMA_IsEnabledStream(burstDmaTimer->dma, burstDmaTimer->streamLL) && timeout--) { + __NOP(); + } + + if (timeout == 0 && LL_DMA_IsEnabledStream(burstDmaTimer->dma, burstDmaTimer->streamLL)) { + LL_TIM_EnableDMAReq_CCx(burstDmaTimer->timer, burstDmaTimer->burstRequestSource); + return; + } + + DMA_CLEAR_FLAG(tch->dma, DMA_IT_TCIF); + + if (circular) { + LL_DMA_SetMode(burstDmaTimer->dma, burstDmaTimer->streamLL, LL_DMA_MODE_CIRCULAR); + LL_DMA_SetDataLength(burstDmaTimer->dma, burstDmaTimer->streamLL, dmaBufferSize); + LL_DMA_DisableIT_TC(burstDmaTimer->dma, burstDmaTimer->streamLL); + tch->dmaState = TCH_DMA_CIRCULAR; + } else { + LL_DMA_SetMode(burstDmaTimer->dma, burstDmaTimer->streamLL, LL_DMA_MODE_NORMAL); + LL_DMA_EnableIT_TC(burstDmaTimer->dma, burstDmaTimer->streamLL); + tch->dmaState = TCH_DMA_IDLE; + } + + __DSB(); + + LL_DMA_EnableStream(burstDmaTimer->dma, burstDmaTimer->streamLL); + LL_TIM_EnableDMAReq_CCx(burstDmaTimer->timer, burstDmaTimer->burstRequestSource); + } +} #endif void impl_timerPWMPrepareDMA(TCH_t * tch, uint32_t dmaBufferElementCount) @@ -581,7 +627,7 @@ void impl_timerPWMStopDMA(TCH_t * tch) HAL_TIM_Base_Start(tch->timCtx->timHandle); } -void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) +void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferSize) { if (!tch->dma || !tch->dma->dma) { return; @@ -590,51 +636,42 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) const uint32_t streamLL = lookupDMALLStreamTable[DMATAG_GET_STREAM(tch->timHw->dmaTag)]; DMA_TypeDef *dmaBase = tch->dma->dma; - // Save the current transfer count before disabling - uint32_t dataLength = LL_DMA_GetDataLength(dmaBase, streamLL); - - // Protect DMA reconfiguration from interrupt interference ATOMIC_BLOCK(NVIC_PRIO_MAX) { - // Disable timer DMA request first to stop new transfer triggers + // Stop new transfer triggers before reconfiguring LL_TIM_DisableDMAReq_CCx(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex]); - - // Disable the DMA stream LL_DMA_DisableStream(dmaBase, streamLL); - // CRITICAL: Wait for stream to actually become disabled - // The EN bit doesn't clear immediately, especially if transfer is in progress - uint32_t timeout = 10000; + // STM32H7 RM: poll EN bit until stream is actually disabled + uint32_t timeout = 10000; // ~20us at 480MHz, well above worst-case disable latency while (LL_DMA_IsEnabledStream(dmaBase, streamLL) && timeout--) { __NOP(); } - // If timeout occurred, DMA stream is still enabled - abort reconfiguration if (timeout == 0 && LL_DMA_IsEnabledStream(dmaBase, streamLL)) { - // Re-enable timer DMA request and return to avoid unstable state LL_TIM_EnableDMAReq_CCx(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex]); return; } - // Clear any pending transfer complete flags DMA_CLEAR_FLAG(tch->dma, DMA_IT_TCIF); - // Modify the DMA mode if (circular) { LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_CIRCULAR); + // Circular mode requires non-zero NDTR (STM32H7 RM constraint) + LL_DMA_SetDataLength(dmaBase, streamLL, dmaBufferSize); + // Disable TC interrupt — in circular mode, TC fires every cycle + // and the IRQ handler would otherwise disable the stream + LL_DMA_DisableIT_TC(dmaBase, streamLL); + tch->dmaState = TCH_DMA_CIRCULAR; } else { LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_NORMAL); + LL_DMA_EnableIT_TC(dmaBase, streamLL); + tch->dmaState = TCH_DMA_IDLE; } - // Reload the transfer count (required after mode change) - // If dataLength was 0 (transfer completed), keep it at 0 - the next motor update will reload it - if (dataLength > 0) { - LL_DMA_SetDataLength(dmaBase, streamLL, dataLength); - } + // Ensure register writes are visible to DMA before re-enabling + __DSB(); - // Re-enable DMA stream LL_DMA_EnableStream(dmaBase, streamLL); - - // Re-enable timer DMA requests LL_TIM_EnableDMAReq_CCx(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex]); } } diff --git a/src/main/drivers/timer_impl_stdperiph.c b/src/main/drivers/timer_impl_stdperiph.c index af685d0a5ba..5f939b1ac34 100644 --- a/src/main/drivers/timer_impl_stdperiph.c +++ b/src/main/drivers/timer_impl_stdperiph.c @@ -270,6 +270,13 @@ static void impl_timerDMA_IRQHandler(DMA_t descriptor) { if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { TCH_t * tch = (TCH_t *)descriptor->userParam; + + // In circular mode, let DMA keep running - don't disable the stream + if (tch->dmaState == TCH_DMA_CIRCULAR) { + DMA_CLEAR_FLAG(descriptor, DMA_IT_TCIF); + return; + } + tch->dmaState = TCH_DMA_IDLE; DMA_Cmd(tch->dma->ref, DISABLE); @@ -463,6 +470,46 @@ void impl_pwmBurstDMAStart(burstDmaTimer_t * burstDmaTimer, uint32_t BurstLength TIM_DMAConfig(burstDmaTimer->timer, TIM_DMABase_CCR1, TIM_DMABurstLength_4Transfers); TIM_DMACmd(burstDmaTimer->timer, burstDmaTimer->burstRequestSource, ENABLE); } + +void impl_pwmBurstDMASetCircular(burstDmaTimer_t * burstDmaTimer, TCH_t * tch, bool circular, uint32_t dmaBufferSize) +{ + if (!tch->dma || !tch->dma->ref) { + return; + } + + ATOMIC_BLOCK(NVIC_PRIO_MAX) { + TIM_DMACmd(burstDmaTimer->timer, burstDmaTimer->burstRequestSource, DISABLE); + DMA_Cmd(burstDmaTimer->dmaBurstStream, DISABLE); + + uint32_t timeout = 10000; + while ((burstDmaTimer->dmaBurstStream->CR & DMA_SxCR_EN) && timeout--) { + __NOP(); + } + + if (timeout == 0 && (burstDmaTimer->dmaBurstStream->CR & DMA_SxCR_EN)) { + TIM_DMACmd(burstDmaTimer->timer, burstDmaTimer->burstRequestSource, ENABLE); + return; + } + + DMA_CLEAR_FLAG(tch->dma, DMA_IT_TCIF); + + if (circular) { + burstDmaTimer->dmaBurstStream->CR |= DMA_SxCR_CIRC; + DMA_SetCurrDataCounter(burstDmaTimer->dmaBurstStream, dmaBufferSize); + DMA_ITConfig(burstDmaTimer->dmaBurstStream, DMA_IT_TC, DISABLE); + tch->dmaState = TCH_DMA_CIRCULAR; + } else { + burstDmaTimer->dmaBurstStream->CR &= ~DMA_SxCR_CIRC; + DMA_ITConfig(burstDmaTimer->dmaBurstStream, DMA_IT_TC, ENABLE); + tch->dmaState = TCH_DMA_IDLE; + } + + __DSB(); + + DMA_Cmd(burstDmaTimer->dmaBurstStream, ENABLE); + TIM_DMACmd(burstDmaTimer->timer, burstDmaTimer->burstRequestSource, ENABLE); + } +} #endif void impl_timerPWMPrepareDMA(TCH_t * tch, uint32_t dmaBufferElementCount) @@ -520,38 +567,47 @@ void impl_timerPWMStopDMA(TCH_t * tch) TIM_Cmd(tch->timHw->tim, ENABLE); } -void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) +void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferSize) { if (!tch->dma || !tch->dma->ref) { return; } - // Protect DMA reconfiguration from interrupt interference ATOMIC_BLOCK(NVIC_PRIO_MAX) { - // Temporarily disable DMA while modifying configuration + // Stop new transfer triggers before reconfiguring + TIM_DMACmd(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], DISABLE); DMA_Cmd(tch->dma->ref, DISABLE); - // Wait for DMA stream to actually be disabled - // The EN bit doesn't clear immediately, especially if transfer is in progress - uint32_t timeout = 10000; + // STM32F4/F7 RM: poll EN bit until stream is actually disabled + uint32_t timeout = 10000; // ~60us at 168MHz, well above worst-case disable latency while ((tch->dma->ref->CR & DMA_SxCR_EN) && timeout--) { __NOP(); } - // If timeout occurred, DMA stream is still enabled - abort reconfiguration if (timeout == 0 && (tch->dma->ref->CR & DMA_SxCR_EN)) { - DMA_Cmd(tch->dma->ref, ENABLE); // Re-enable and return + TIM_DMACmd(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], ENABLE); return; } - // Modify the DMA mode + DMA_CLEAR_FLAG(tch->dma, DMA_IT_TCIF); + if (circular) { - tch->dma->ref->CR |= DMA_SxCR_CIRC; // Set circular bit + tch->dma->ref->CR |= DMA_SxCR_CIRC; + DMA_SetCurrDataCounter(tch->dma->ref, dmaBufferSize); + // Disable TC interrupt — in circular mode, TC fires every cycle + // and the IRQ handler would otherwise disable the stream + DMA_ITConfig(tch->dma->ref, DMA_IT_TC, DISABLE); + tch->dmaState = TCH_DMA_CIRCULAR; } else { - tch->dma->ref->CR &= ~DMA_SxCR_CIRC; // Clear circular bit + tch->dma->ref->CR &= ~DMA_SxCR_CIRC; + DMA_ITConfig(tch->dma->ref, DMA_IT_TC, ENABLE); + tch->dmaState = TCH_DMA_IDLE; } - // Re-enable DMA + // Ensure register writes are visible to DMA before re-enabling + __DSB(); + DMA_Cmd(tch->dma->ref, ENABLE); + TIM_DMACmd(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], ENABLE); } } diff --git a/src/main/drivers/timer_impl_stdperiph_at32.c b/src/main/drivers/timer_impl_stdperiph_at32.c index d9433ba1f66..8317f1f5795 100644 --- a/src/main/drivers/timer_impl_stdperiph_at32.c +++ b/src/main/drivers/timer_impl_stdperiph_at32.c @@ -269,6 +269,13 @@ static void impl_timerDMA_IRQHandler(DMA_t descriptor) { if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { TCH_t * tch = (TCH_t *)descriptor->userParam; + + // In circular mode, let DMA keep running - don't disable the channel + if (tch->dmaState == TCH_DMA_CIRCULAR) { + DMA_CLEAR_FLAG(descriptor, DMA_IT_TCIF); + return; + } + tch->dmaState = TCH_DMA_IDLE; dma_channel_enable(tch->dma->ref,FALSE); tmr_dma_request_enable(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], FALSE); @@ -408,37 +415,47 @@ void impl_timerPWMStopDMA(TCH_t * tch) tmr_counter_enable(tch->timHw->tim, TRUE); } -void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular) +void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferSize) { if (!tch->dma || !tch->dma->ref) { return; } - // Protect DMA reconfiguration from interrupt interference ATOMIC_BLOCK(NVIC_PRIO_MAX) { - // Temporarily disable DMA while modifying configuration + // Stop new transfer triggers before reconfiguring + tmr_dma_request_enable(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], FALSE); dma_channel_enable(tch->dma->ref, FALSE); - // Wait for DMA channel to actually be disabled - // The enable bit doesn't clear immediately, especially if transfer is in progress - uint32_t timeout = 10000; + // AT32: poll enable bit until channel is actually disabled + uint32_t timeout = 10000; // ~40us at 288MHz, well above worst-case disable latency while (tch->dma->ref->ctrl_bit.chen && timeout--) { __NOP(); } - // If timeout occurred, DMA channel is still enabled - abort reconfiguration if (timeout == 0 && tch->dma->ref->ctrl_bit.chen) { - dma_channel_enable(tch->dma->ref, TRUE); // Re-enable and return + tmr_dma_request_enable(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], TRUE); return; } + DMA_CLEAR_FLAG(tch->dma, DMA_IT_TCIF); + if (circular) { - tch->dma->ref->ctrl_bit.lm = TRUE; // Enable loop mode + tch->dma->ref->ctrl_bit.lm = TRUE; + dma_data_number_set(tch->dma->ref, dmaBufferSize); + // Disable TC interrupt — in circular mode, TC fires every cycle + // and the IRQ handler would otherwise disable the channel + dma_interrupt_enable(tch->dma->ref, DMA_IT_TCIF, FALSE); + tch->dmaState = TCH_DMA_CIRCULAR; } else { - tch->dma->ref->ctrl_bit.lm = FALSE; // Disable loop mode + tch->dma->ref->ctrl_bit.lm = FALSE; + dma_interrupt_enable(tch->dma->ref, DMA_IT_TCIF, TRUE); + tch->dmaState = TCH_DMA_IDLE; } - // Re-enable DMA + // Ensure register writes are visible to DMA before re-enabling + __DSB(); + dma_channel_enable(tch->dma->ref, TRUE); + tmr_dma_request_enable(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], TRUE); } } From 6af160dd6e9966e1a980ec812f261929dc7cdd33 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Fri, 20 Mar 2026 01:14:10 -0500 Subject: [PATCH 10/11] Fix forward declaration guards for loadDmaBufferDshot/Stride --- src/main/drivers/pwm_output.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/drivers/pwm_output.c b/src/main/drivers/pwm_output.c index ca1838f197b..a4efb9e3008 100644 --- a/src/main/drivers/pwm_output.c +++ b/src/main/drivers/pwm_output.c @@ -126,8 +126,11 @@ static uint8_t commandsBuff[DHSOT_COMMAND_QUEUE_SIZE]; static currentExecutingCommand_t currentExecutingCommand; static uint16_t prepareDshotPacket(const uint16_t value, bool requestTelemetry); +#ifndef USE_DSHOT_DMAR static void loadDmaBufferDshot(timerDMASafeType_t *dmaBuffer, uint16_t packet); +#else static void loadDmaBufferDshotStride(timerDMASafeType_t *dmaBuffer, int stride, uint16_t packet); +#endif #ifdef USE_DSHOT_DMAR burstDmaTimer_t burstDmaTimers[MAX_DMA_TIMERS]; From 49ef6fc7e568e7550f7ad5b678f278b8e0a30d5f Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Tue, 5 May 2026 22:58:28 -0500 Subject: [PATCH 11/11] Fix DMA disable timeout: uint32_t wraparound made abort path unreachable uint32_t post-decrement from 0 wraps to UINT32_MAX, so timeout == 0 can never be true after the loop exits. The stream-still-enabled check alone correctly identifies whether the timeout was exhausted. --- src/main/drivers/timer_impl_hal.c | 4 ++-- src/main/drivers/timer_impl_stdperiph.c | 4 ++-- src/main/drivers/timer_impl_stdperiph_at32.c | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index 111fa6ffb19..8cfe3642021 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -534,7 +534,7 @@ void impl_pwmBurstDMASetCircular(burstDmaTimer_t * burstDmaTimer, TCH_t * tch, b __NOP(); } - if (timeout == 0 && LL_DMA_IsEnabledStream(burstDmaTimer->dma, burstDmaTimer->streamLL)) { + if (LL_DMA_IsEnabledStream(burstDmaTimer->dma, burstDmaTimer->streamLL)) { LL_TIM_EnableDMAReq_CCx(burstDmaTimer->timer, burstDmaTimer->burstRequestSource); return; } @@ -647,7 +647,7 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferS __NOP(); } - if (timeout == 0 && LL_DMA_IsEnabledStream(dmaBase, streamLL)) { + if (LL_DMA_IsEnabledStream(dmaBase, streamLL)) { LL_TIM_EnableDMAReq_CCx(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex]); return; } diff --git a/src/main/drivers/timer_impl_stdperiph.c b/src/main/drivers/timer_impl_stdperiph.c index 5f939b1ac34..5b52cb862a4 100644 --- a/src/main/drivers/timer_impl_stdperiph.c +++ b/src/main/drivers/timer_impl_stdperiph.c @@ -486,7 +486,7 @@ void impl_pwmBurstDMASetCircular(burstDmaTimer_t * burstDmaTimer, TCH_t * tch, b __NOP(); } - if (timeout == 0 && (burstDmaTimer->dmaBurstStream->CR & DMA_SxCR_EN)) { + if (burstDmaTimer->dmaBurstStream->CR & DMA_SxCR_EN) { TIM_DMACmd(burstDmaTimer->timer, burstDmaTimer->burstRequestSource, ENABLE); return; } @@ -584,7 +584,7 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferS __NOP(); } - if (timeout == 0 && (tch->dma->ref->CR & DMA_SxCR_EN)) { + if (tch->dma->ref->CR & DMA_SxCR_EN) { TIM_DMACmd(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], ENABLE); return; } diff --git a/src/main/drivers/timer_impl_stdperiph_at32.c b/src/main/drivers/timer_impl_stdperiph_at32.c index 8317f1f5795..782943ab25d 100644 --- a/src/main/drivers/timer_impl_stdperiph_at32.c +++ b/src/main/drivers/timer_impl_stdperiph_at32.c @@ -432,7 +432,7 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferS __NOP(); } - if (timeout == 0 && tch->dma->ref->ctrl_bit.chen) { + if (tch->dma->ref->ctrl_bit.chen) { tmr_dma_request_enable(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], TRUE); return; }