From 2be65e05eb63792c7c244105904e2f1a6ebc9282 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 2 Mar 2026 16:08:17 -0600 Subject: [PATCH 1/4] drivers: wait for DMA EN bit to clear before reconfiguring DShot DMA 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. --- src/main/drivers/timer_impl_hal.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index 8df0f7024d3..090a383c4d1 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -528,6 +528,19 @@ void impl_timerPWMPrepareDMA(TCH_t * tch, uint32_t dmaBufferElementCount) DMA_CLEAR_FLAG(tch->dma, DMA_IT_TCIF); } + // Wait for EN bit to actually clear before reconfiguring DMA registers. + // Per STM32F7 RM: writes to DMA_SxNDTR and DMA_SxM0AR are ignored while EN=1. + // The EN bit does not clear synchronously - hardware may still be completing an + // in-progress burst when software writes 0 to EN. + 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; + } + LL_DMA_SetDataLength(dmaBase, streamLL, dmaBufferElementCount); LL_DMA_ConfigAddresses(dmaBase, streamLL, (uint32_t)tch->dmaBuffer, (uint32_t)impl_timerCCR(tch), LL_DMA_DIRECTION_MEMORY_TO_PERIPH); LL_DMA_EnableIT_TC(dmaBase, streamLL); From 52ba52bc6f6d93cf82405461d46af6a15d796d16 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 2 Mar 2026 16:43:33 -0600 Subject: [PATCH 2/4] =?UTF-8?q?drivers:=20fix=20DMA=20EN-bit=20timeout=20p?= =?UTF-8?q?ath=20=E2=80=94=20set=20dmaState=20IDLE=20and=20avoid=20counter?= =?UTF-8?q?=20underflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/main/drivers/timer_impl_hal.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index 090a383c4d1..a2cd24ae69e 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -532,12 +532,13 @@ void impl_timerPWMPrepareDMA(TCH_t * tch, uint32_t dmaBufferElementCount) // Per STM32F7 RM: writes to DMA_SxNDTR and DMA_SxM0AR are ignored while EN=1. // The EN bit does not clear synchronously - hardware may still be completing an // in-progress burst when software writes 0 to EN. - uint32_t timeout = 10000; - while (LL_DMA_IsEnabledStream(dmaBase, streamLL) && timeout--) { + for (uint32_t timeout = 10000; timeout && LL_DMA_IsEnabledStream(dmaBase, streamLL); timeout--) { __NOP(); } if (LL_DMA_IsEnabledStream(dmaBase, streamLL)) { - // EN did not clear - skip reconfiguration to avoid silent data corruption. + // EN did not clear - hardware fault. Set channel idle so it is skipped + // on the next StartDMA call rather than being left in an ambiguous state. + tch->dmaState = TCH_DMA_IDLE; return; } From 0735c57fe79ed8e2560e8edba72c7a3b8c57fade Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 2 Mar 2026 16:53:25 -0600 Subject: [PATCH 3/4] =?UTF-8?q?drivers:=20clarify=20EN-bit=20timeout=20com?= =?UTF-8?q?ment=20=E2=80=94=20skipped=20frame,=20not=20hardware=20fault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- src/main/drivers/timer_impl_hal.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index a2cd24ae69e..b61ec6813fa 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -536,8 +536,12 @@ void impl_timerPWMPrepareDMA(TCH_t * tch, uint32_t dmaBufferElementCount) __NOP(); } if (LL_DMA_IsEnabledStream(dmaBase, streamLL)) { - // EN did not clear - hardware fault. Set channel idle so it is skipped - // on the next StartDMA call rather than being left in an ambiguous state. + // EN did not clear within the timeout (~140-230 us at 216 MHz). Any in-progress + // transfer has long since completed or been aborted, so no data corruption risk + // remains - but we cannot safely reconfigure the DMA registers this cycle. + // Skip this frame (ESC holds its last command) and mark the channel IDLE so + // StartDMA passes over it. EN will almost certainly have cleared by the next + // PrepareDMA call (~1-2 ms later), allowing normal operation to resume. tch->dmaState = TCH_DMA_IDLE; return; } From 76cc064fc0e417f0da8116628aad387d92405322 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 2 Mar 2026 16:55:00 -0600 Subject: [PATCH 4/4] =?UTF-8?q?drivers:=20tighten=20EN-bit=20timeout=20com?= =?UTF-8?q?ment=20=E2=80=94=20concise=20and=20accurate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/drivers/timer_impl_hal.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index b61ec6813fa..93ebec5de86 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -536,12 +536,8 @@ void impl_timerPWMPrepareDMA(TCH_t * tch, uint32_t dmaBufferElementCount) __NOP(); } if (LL_DMA_IsEnabledStream(dmaBase, streamLL)) { - // EN did not clear within the timeout (~140-230 us at 216 MHz). Any in-progress - // transfer has long since completed or been aborted, so no data corruption risk - // remains - but we cannot safely reconfigure the DMA registers this cycle. - // Skip this frame (ESC holds its last command) and mark the channel IDLE so - // StartDMA passes over it. EN will almost certainly have cleared by the next - // PrepareDMA call (~1-2 ms later), allowing normal operation to resume. + // EN did not clear - cannot reconfigure this cycle. Skip frame (ESC holds + // last command); EN should clear before the next call. tch->dmaState = TCH_DMA_IDLE; return; }