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 platform-specific circular DMA support for STM32F4/F7/H7 and AT32F43x

  • Protects DMA reconfiguration with atomic blocks and proper synchronization


Diagram Walkthrough

flowchart LR
  A["Settings Save Triggered"] --> B["writeConfigToEEPROM"]
  B --> C["Enable Circular DMA Mode"]
  C --> D["Latch Zero Throttle Packets"]
  D --> E["Flash Write Operation"]
  E --> F["DMA Repeats Last Packet"]
  F --> G["Disable Circular DMA Mode"]
  G --> H["Normal Operation Restored"]
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
  • Executes three pwmCompleteMotorUpdate() calls with delays to latch
    zero throttle
  • Calls pwmSetMotorDMACircular(false) after flash writes to restore
    normal mode
+14/-0   
config.c
Clarify circular DMA protection in delayed save                   

src/main/fc/config.c

  • Updated comment in processDelayedSave() to clarify circular DMA
    protection location
  • Removed redundant circular DMA code that was previously in this
    function
+1/-1     
Enhancement
pwm_output.c
Add motor DMA circular mode control function                         

src/main/drivers/pwm_output.c

  • Implements pwmSetMotorDMACircular() function to control DMA circular
    mode
  • Iterates through all motor outputs and calls platform-specific
    implementation
  • Guarded by USE_DSHOT preprocessor check
+14/-0   
pwm_output.h
Export DMA circular mode control function                               

src/main/drivers/pwm_output.h

  • Declares new pwmSetMotorDMACircular() function for public API
+1/-0     
timer_impl.h
Add DMA circular mode interface declaration                           

src/main/drivers/timer_impl.h

  • Declares platform-independent impl_timerPWMSetDMACircular() interface
+1/-0     
timer_impl_hal.c
Implement circular DMA for STM32H7 platform                           

src/main/drivers/timer_impl_hal.c

  • Implements 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 to ensure stream is disabled
  • Switches between LL_DMA_MODE_CIRCULAR and LL_DMA_MODE_NORMAL
  • Preserves and reloads data length after mode change
  • Re-enables DMA stream and timer DMA requests within atomic block
+51/-0   
timer_impl_stdperiph.c
Implement circular DMA for STM32F4/F7 platform                     

src/main/drivers/timer_impl_stdperiph.c

  • Implements impl_timerPWMSetDMACircular() for STM32F4/F7 using
    StdPeriph drivers
  • Temporarily disables DMA while modifying configuration
  • Sets or clears DMA_SxCR_CIRC bit based on circular parameter
  • Re-enables DMA after mode change within atomic block
+23/-0   
timer_impl_stdperiph_at32.c
Implement circular DMA for AT32F43x platform                         

src/main/drivers/timer_impl_stdperiph_at32.c

  • Implements impl_timerPWMSetDMACircular() for AT32F43x platform
  • Temporarily disables DMA channel while modifying configuration
  • Sets or clears ctrl_bit.lm (loop mode) based on circular parameter
  • Re-enables DMA channel after mode change within atomic block
+23/-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)
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.

1 participant