Skip to content

Conversation

@sensei-hacker
Copy link
Member

@sensei-hacker sensei-hacker commented Jan 19, 2026

User description

Summary

Fixes ESC spinup/reboot when saving settings via configurator by enabling DMA circular mode during flash write operations.

Problem

Internal flash writes block CPU core execution for 20-200ms on STM32F4/F7/AT32F43x and STM32H7. This interrupts DShot signal transmission, causing ESCs to timeout and spin up motors.

Solution

Enable DMA circular mode before flash writes to allow DMA hardware to automatically repeat the last DShot packet (zero throttle) without CPU intervention.

Implementation:

  • STM32F4/F7: Set DMA_SxCR_CIRC bit
  • STM32H7: Use LL_DMA_MODE_CIRCULAR with proper synchronization (wait for EN bit clear)
  • AT32F43x: Set ctrl_bit.lm (loop mode)
  • All platforms: ATOMIC_BLOCK protection, disable/re-enable timer DMA requests

Testing

Tested with oscilloscope monitoring DShot signal during settings save:

  • STM32F7 (AOCODARCF7MINI_V1): Bug reproduced, fix verified ✅
  • AT32F435 (BLUEBERRYF435WING): Bug reproduced, fix verified ✅
  • STM32H7 (JHEMCUH743HD): Bug reproduced, fix verified ✅
  • STM32F4: Covered by F7 (identical code) ✅

Related Issues

Fixes #10913

Note: While related to Betaflight PR #12544, this addresses a different scenario:

  • Betaflight issue: DShot beacon timing gaps
  • INAV issue: Settings save (MSP_EEPROM_WRITE) triggering flash writes

INAV does not have the DShot beacon issue - motor values persist during beacon gaps.


PR Type

Bug fix


Description

  • Prevents ESC spinup during settings save by enabling DMA circular mode

  • Allows DMA to automatically repeat zero-throttle DShot packets during flash writes

  • Implements circular DMA support for STM32F4/F7, STM32H7, and AT32F43x platforms

  • Protects DMA reconfiguration with atomic blocks and timeout handling


Diagram Walkthrough

flowchart LR
  A["Settings Save"] --> B["writeConfigToEEPROM"]
  B --> C["Enable Circular DMA"]
  C --> D["Latch Zero Throttle"]
  D --> E["Flash Write"]
  E --> F["DMA Repeats Last Packet"]
  F --> G["Disable Circular DMA"]
  G --> H["Normal Operation"]
Loading

File Walkthrough

Relevant files
Bug fix
config_eeprom.c
Enable circular DMA during EEPROM writes                                 

src/main/config/config_eeprom.c

  • Added pwm_output.h include for DMA circular mode control
  • Calls pwmSetMotorDMACircular(true) before flash writes to enable
    circular DMA
  • Forces motor updates via pwmCompleteMotorUpdate() to latch zero
    throttle
  • Restores normal DMA mode with pwmSetMotorDMACircular(false) after
    writes
+18/-0   
timer_impl_hal.c
Implement circular DMA for STM32H7 platforms                         

src/main/drivers/timer_impl_hal.c

  • Implemented impl_timerPWMSetDMACircular() for STM32H7 using LL drivers
  • Disables timer DMA requests and DMA stream before mode change
  • Waits for EN bit to clear with timeout protection
  • Switches between LL_DMA_MODE_CIRCULAR and LL_DMA_MODE_NORMAL
  • Preserves transfer count and re-enables DMA with atomic protection
+58/-0   
timer_impl_stdperiph.c
Implement circular DMA for STM32F4/F7 platforms                   

src/main/drivers/timer_impl_stdperiph.c

  • Implemented impl_timerPWMSetDMACircular() for STM32F4/F7 using
    StdPeriph
  • Disables DMA stream before modifying configuration
  • Waits for EN bit to clear with timeout protection
  • Sets or clears DMA_SxCR_CIRC bit based on circular parameter
  • Re-enables DMA with atomic protection
+36/-0   
timer_impl_stdperiph_at32.c
Implement circular DMA for AT32F43x platforms                       

src/main/drivers/timer_impl_stdperiph_at32.c

  • Implemented impl_timerPWMSetDMACircular() for AT32F43x platform
  • Disables DMA channel before modifying configuration
  • Waits for enable bit to clear with timeout protection
  • Sets or clears ctrl_bit.lm (loop mode) based on circular parameter
  • Re-enables DMA with atomic protection
+35/-0   
Enhancement
pwm_output.h
Add DMA circular mode control function                                     

src/main/drivers/pwm_output.h

  • Added pwmSetMotorDMACircular() function declaration
  • Allows runtime switching between circular and normal DMA modes
+1/-0     
pwm_output.c
Implement motor DMA circular mode wrapper                               

src/main/drivers/pwm_output.c

  • Implemented pwmSetMotorDMACircular() function
  • Iterates through all motor outputs and calls platform-specific
    implementation
  • Includes conditional compilation for DShot and SITL builds
+14/-0   
timer_impl.h
Add platform DMA circular mode interface                                 

src/main/drivers/timer_impl.h

  • Added impl_timerPWMSetDMACircular() function declaration
  • Platform-specific implementation for DMA mode switching
+1/-0     

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.
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 (a6ba116) 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: iNavFlight#10913
Related: iNavFlight#9441
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: iNavFlight#10913
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.
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 19, 2026

PR Compliance Guide 🔍

All compliance sections have been disabled in the configurations.

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.
@sensei-hacker sensei-hacker added this to the 9.1 milestone Jan 19, 2026
@sensei-hacker sensei-hacker marked this pull request as draft January 19, 2026 07:51
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.
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.
- 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)
@Jetrell
Copy link

Jetrell commented Jan 22, 2026

I ran some props removed bench testing with Dshot beeper enabled. And tried many conditions to make the motors start when disarmed. Even saving files to eeprom via the configurator connection. Which used to occasionally trigger uncontrolled motor startup.
But haven't had any issues so far.
This was with the copter my report was based on. It uses flash logging.

But it will likely require testing with a broader range of FC boards that log with SDIO, due the use of circular mode. Because it might cause conflicts depending on the allocation of the DMA streams on some flight controllers.

@sensei-hacker
Copy link
Member Author

sensei-hacker commented Jan 22, 2026

Thanks Jetrell!

From earlier testing I did, and then further research, it seems to be entirely driven by writing to the internal EEPROM (the same place the code is stored). The EEPROM can't be written and read at the same time, so code can't execute while writing to the EEPROM.

Writing to a port that happens to have some kind of external flash on the other end doesn't seem to matter, because the external flash doesn't need to be read for code to execute. That is, it's not writing the storage that it is trying to run code from. So blackbox doesn't matter.

I tested the blackbox stuff more extensively than I intended to, and found that it doesn't produce a glitch. :)
Then I realized I had accidentally spent hours running tests on the wrong dang thing - that no matter what I did to blackbox, that was never going to reproduce the glitch, because why would it?

Eventually realizing my mistake, I wrote to EEPROM instead and the DSHOT output immediately glitched - exactly like the STM documentation said it would. 😁

@sensei-hacker sensei-hacker marked this pull request as ready for review January 22, 2026 03:13
@qodo-code-review
Copy link
Contributor

PR Compliance Guide 🔍

All compliance sections have been disabled in the configurations.

Comment on lines +606 to +616
uint32_t timeout = 10000;
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Fix the incorrect DMA timeout check by re-evaluating the stream status after the loop instead of checking if timeout is zero. [possible issue, importance: 8]

Suggested change
uint32_t timeout = 10000;
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;
}
uint32_t timeout = 10000;
while (LL_DMA_IsEnabledStream(dmaBase, streamLL) && timeout--) {
__NOP();
}
// If timeout occurred, DMA stream is still enabled - abort reconfiguration
if (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;
}

Comment on lines +233 to +237
for (int i = 0; i < getMotorCount(); i++) {
if (motors[i].pwmPort && motors[i].pwmPort->tch) {
impl_timerPWMSetDMACircular(motors[i].pwmPort->tch, circular);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a check for motors[i].pwmPort->configured before changing the DMA mode to ensure the motor port is configured. [general, importance: 6]

Suggested change
for (int i = 0; i < getMotorCount(); i++) {
if (motors[i].pwmPort && motors[i].pwmPort->tch) {
impl_timerPWMSetDMACircular(motors[i].pwmPort->tch, circular);
}
}
for (int i = 0; i < getMotorCount(); i++) {
if (motors[i].pwmPort && motors[i].pwmPort->tch && motors[i].pwmPort->configured) {
impl_timerPWMSetDMACircular(motors[i].pwmPort->tch, circular);
}
}

Comment on lines +325 to +335
#if !defined(SITL_BUILD) && defined(USE_DSHOT)
// 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();
#endif
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Gate the DMA mode switch on the runtime motor protocol (e.g., isMotorProtocolDshot()), and skip the whole sequence if DShot isn’t active to avoid unintended DMA reconfiguration on other setups. [Learned best practice, importance: 6]

Suggested change
#if !defined(SITL_BUILD) && defined(USE_DSHOT)
// 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();
#endif
#if !defined(SITL_BUILD) && defined(USE_DSHOT)
if (isMotorProtocolDshot()) {
// 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();
}
#endif

Comment on lines +530 to +556
ATOMIC_BLOCK(NVIC_PRIO_MAX) {
// 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
} else {
tch->dma->ref->CR &= ~DMA_SxCR_CIRC; // Clear circular bit
}

// Re-enable DMA
DMA_Cmd(tch->dma->ref, ENABLE);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Match the HAL implementation by disabling the timer’s DMA request source before disabling/reconfiguring the DMA stream, then re-enable it afterward to prevent mid-transition DMA triggers. [Learned best practice, importance: 5]

Suggested change
ATOMIC_BLOCK(NVIC_PRIO_MAX) {
// 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
} else {
tch->dma->ref->CR &= ~DMA_SxCR_CIRC; // Clear circular bit
}
// Re-enable DMA
DMA_Cmd(tch->dma->ref, ENABLE);
}
ATOMIC_BLOCK(NVIC_PRIO_MAX) {
// Disable timer DMA request first to stop new transfer triggers
TIM_DMACmd(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], DISABLE);
// Temporarily disable DMA while modifying configuration
DMA_Cmd(tch->dma->ref, DISABLE);
uint32_t timeout = 10000;
while ((tch->dma->ref->CR & DMA_SxCR_EN) && timeout--) {
__NOP();
}
if (timeout == 0 && (tch->dma->ref->CR & DMA_SxCR_EN)) {
// Restore timer DMA requests and return to avoid unstable state
TIM_DMACmd(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], ENABLE);
DMA_Cmd(tch->dma->ref, ENABLE);
return;
}
if (circular) {
tch->dma->ref->CR |= DMA_SxCR_CIRC;
} else {
tch->dma->ref->CR &= ~DMA_SxCR_CIRC;
}
DMA_Cmd(tch->dma->ref, ENABLE);
TIM_DMACmd(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], ENABLE);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants