Skip to content

drivers: wait for DMA EN bit to clear before reconfiguring DShot DMA (F7/H7)#11390

Open
sensei-hacker wants to merge 4 commits intoiNavFlight:maintenance-9.xfrom
sensei-hacker:fix/f7-dshot-dma-en-bit-wait
Open

drivers: wait for DMA EN bit to clear before reconfiguring DShot DMA (F7/H7)#11390
sensei-hacker wants to merge 4 commits intoiNavFlight:maintenance-9.xfrom
sensei-hacker:fix/f7-dshot-dma-en-bit-wait

Conversation

@sensei-hacker
Copy link
Member

@sensei-hacker sensei-hacker commented Mar 2, 2026

Brings a fix already done on H7 over to F7. It may be the cause of some mystery lock ups on F765.
F765 are more likely to hit it because of the caching structure.

Problem

In timer_impl_hal.c :

impl_timerPWMPrepareDMA() calls LL_DMA_DisableStream() then immediately calls LL_DMA_SetDataLength() and LL_DMA_ConfigAddresses() with no wait between them.

Per STM32F7 Reference Manual (RM0410, Section 8.3.5):

The stream can be disabled by resetting the EN bit. [...] Note that it takes a certain number of AHB clock cycles before the stream is effectively disabled. __So it is recommended to check the EN bit = 0 before re-enabling the stream.
Writing to the SxNDTR register when the stream is enabled (bit EN = 1 in the DMA_SxCR register) has no effect on the DMA stream operation.

In the race window between LL_DMA_DisableStream() and EN actually clearing, the writes to DMA_SxNDTR (count) and DMA_SxM0AR (address) are silently discarded by hardware. The subsequent LL_DMA_EnableStream() then fires DMA with the previous transfer's stale count and address, producing garbled Shot frames.

Why This Manifests on F765 More Than F745

The race window is only ~5–18 ns (1–4 AHB cycles at 216 MHz). On STM32F745 (4 KB I-Cache), frequent cache misses in the interrupt handler may add enough pipeline stalls that EN clears naturally before LL_DMA_SetDataLength() is reached. On STM32F765 (16 KB I-Cache), the hot path fits entirely in cache and executes without stalls, consistently landing within the race window.

The race condition exists on all F7/H7 targets; the cache difference may makes it more reproducible on F765.

Fix

Add a bounded spin-wait for EN to clear after LL_DMA_DisableStream(), before any DMA register writes. If EN has not cleared after the timeout (indicating a hardware fault), skip reconfiguration entirely rather than proceeding with stale register values — a skipped DShot cycle holds last good motor values, which is far preferable to a corrupted frame.

Files Changed

  • src/main/drivers/timer_impl_hal.c — add EN-bit wait in impl_timerPWMPrepareDMA()

Testing

Build test: MATEKF765 and FRSKYPILOT targets build cleanly with this change.

Hardware test: Not performed by me — no physical STM32F765 hardware available. The fix is a straightforward application of the RM guidance; the logic is equivalent to the wait loops used in stm32f7xx_hal_dma.c.

Reports of intermittent F765 lockups at arm time (when DShot DMA starts for the first time) are potentially consistent with this race condition.

Per STM32F7 Reference Manual (RM0410 Section 8.3.5), writes to DMA_SxNDTR
and DMA_SxM0AR are silently ignored while the EN bit is asserted. After
calling LL_DMA_DisableStream(), EN does not clear synchronously -- the
hardware may still be completing an in-progress burst.

impl_timerPWMPrepareDMA() previously called LL_DMA_SetDataLength() and
LL_DMA_ConfigAddresses() immediately after LL_DMA_DisableStream() with no
wait. In the race window where EN is still 1, both writes are discarded,
leaving stale count and address in the DMA registers. The subsequent
LL_DMA_EnableStream() then fires DMA with the old (now incorrect) transfer
count and/or buffer address, producing garbled DShot frames.

Fix: add a bounded wait loop for EN to clear before reconfiguring. If EN
has not cleared after the timeout (indicating a hardware fault), skip the
reconfiguration entirely rather than proceeding with stale register values.

This race is most visible on STM32F765 targets, where the larger 16 KB
I-Cache keeps the interrupt handler hot path in cache, resulting in faster
execution that consistently lands within the ~5-18 ns EN clear window.
On STM32F745 (4 KB I-Cache), cache misses add stall cycles that naturally
extend the window enough for EN to clear before reconfiguration begins.
@qodo-code-review
Copy link
Contributor

Review Summary by Qodo

Wait for DMA EN bit to clear before DShot reconfiguration

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Add EN bit wait loop before DMA register reconfiguration
• Prevents silent data corruption from stale DMA count/address
• Skips reconfiguration if EN bit fails to clear (hardware fault)
• Fixes intermittent F765 lockups at DShot DMA startup
Diagram
flowchart LR
  A["LL_DMA_DisableStream"] --> B["Wait for EN bit clear"]
  B --> C{EN cleared?}
  C -->|Yes| D["Reconfigure DMA registers"]
  C -->|No| E["Skip reconfiguration"]
  D --> F["Enable DMA stream"]
  E --> G["Return early"]
Loading

Grey Divider

File Changes

1. src/main/drivers/timer_impl_hal.c 🐞 Bug fix +13/-0

Add DMA EN bit clear wait in PWM prepare

• Add bounded spin-wait loop checking LL_DMA_IsEnabledStream() after LL_DMA_DisableStream()
• Timeout set to 10000 iterations with __NOP() to allow EN bit to clear
• Early return if EN bit fails to clear, preventing register writes with stale values
• Prevents race condition where DMA_SxNDTR and DMA_SxM0AR writes are silently ignored

src/main/drivers/timer_impl_hal.c


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Mar 2, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. dmaState not set on timeout📘 Rule violation ⛯ Reliability
Description
On DMA EN-bit timeout, the new early return exits before restoring a safe/known tch->dmaState,
which can leave the channel in an inconsistent state and prevent later DMA scheduling/progression.
This violates the requirement that embedded/IO failure modes be state-safe and that early returns
not skip required state progression.
Code

src/main/drivers/timer_impl_hal.c[R535-542]

+    uint32_t timeout = 10000;
+    while (LL_DMA_IsEnabledStream(dmaBase, streamLL) && timeout--) {
+        __NOP();
+    }
+    if (LL_DMA_IsEnabledStream(dmaBase, streamLL)) {
+        // EN did not clear - skip reconfiguration to avoid silent data corruption.
+        return;
+    }
Evidence
PR Compliance ID 6 requires state-safe timeout handling and warns against early returns that prevent
required scheduling/state progression. The added timeout path returns immediately when EN does not
clear, which skips the normal path that re-enables the stream and sets `tch->dmaState =
TCH_DMA_READY`, leaving state handling ambiguous on failure.

src/main/drivers/timer_impl_hal.c[535-542]
src/main/drivers/timer_impl_hal.c[544-549]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`impl_timerPWMPrepareDMA()` returns early on DMA EN-bit timeout without updating `tch->dmaState` (or otherwise recording the failure), which can leave the timer/DMA channel in an inconsistent state and block later DMA scheduling.
## Issue Context
The new bounded wait is good, but PR Compliance ID 6 expects state-safe failure handling and discourages early returns that skip required state progression.
## Fix Focus Areas
- src/main/drivers/timer_impl_hal.c[535-549]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

2. Timeout counter underflow🐞 Bug ⛯ Reliability
Description
The loop uses timeout-- in the condition, which underflows to UINT32_MAX when it reaches 0. This
is currently low-impact but is error-prone and can confuse future maintenance or debugging.
Code

src/main/drivers/timer_impl_hal.c[R535-538]

+    uint32_t timeout = 10000;
+    while (LL_DMA_IsEnabledStream(dmaBase, streamLL) && timeout--) {
+        __NOP();
+    }
Evidence
The condition ... && timeout-- performs a post-decrement even on the terminating check; when
timeout reaches 0 it wraps to UINT32_MAX. Even though timeout is not used later today, this
pattern is fragile and easy to misread.

src/main/drivers/timer_impl_hal.c[535-538]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The EN-bit wait loop uses `timeout--` in the `while` condition, which wraps to `UINT32_MAX` when it hits zero. This is a maintainability hazard.
### Issue Context
This is not currently used after the loop, but the pattern is easy to misinterpret or accidentally depend on later.
### Fix Focus Areas
- src/main/drivers/timer_impl_hal.c[535-538]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Test firmware build ready — commit 76cc064

Download firmware for PR #11390

223 targets built. Find your board's .hex file by name on that page (e.g. MATEKF405SE.hex). Files are individually downloadable — no GitHub login required.

Development build for testing only. Use Full Chip Erase when flashing.

…unter underflow

Two issues in the EN-bit wait loop added to impl_timerPWMPrepareDMA():

1. On timeout, the early return skipped setting tch->dmaState, leaving it in
   whatever state it had on entry (could be TCH_DMA_ACTIVE if the DMA TC
   interrupt was suppressed by the preceding ATOMIC_BLOCK). Subsequent calls
   to impl_timerPWMStartDMA() check for TCH_DMA_READY, so an ACTIVE or stale
   state would not cause a spurious fire, but timer.c timerIsMotorBusy() uses
   dmaState != TCH_DMA_IDLE as a busy check, which would return true forever
   on a stuck channel. Fix: explicitly set TCH_DMA_IDLE on timeout.

2. The while-loop condition used post-decrement (timeout--), causing the
   counter to wrap from 0 to UINT32_MAX on the final iteration. The counter
   is not used after the loop so this was harmless, but the pattern is fragile
   and easy to misread. Fix: use a for-loop where timeout decrements cleanly
   to 0 and the loop exit condition is unambiguous.
… fault

The timeout (~140-230 us at 216 MHz) is long enough that any in-progress
DMA transfer has certainly completed or been aborted by the time it expires.
The stuck EN bit means we cannot reconfigure DMA registers this cycle, not
that the hardware is permanently broken. Update the comment to reflect that:
the ESC holds its last command for one missed frame, and EN will almost
certainly have cleared before the next PrepareDMA call (~1-2 ms later).
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