Skip to content

fix(motors): migrate dmaInit() to ownership-checked dmaAllocate() - #1397

Draft
nerdCopter wants to merge 6 commits into
emuflight:masterfrom
nerdCopter:fix/dmainit-dshot-migration
Draft

fix(motors): migrate dmaInit() to ownership-checked dmaAllocate()#1397
nerdCopter wants to merge 6 commits into
emuflight:masterfrom
nerdCopter:fix/dmainit-dshot-migration

Conversation

@nerdCopter

@nerdCopter nerdCopter commented Aug 18, 2026

Copy link
Copy Markdown
Member

AI Generated pull-request

Summary

Stage 4 of 5 in IT#1363's fleet-wide dmaInit()dmaAllocate() migration (stage 1, ADC, merged as PR#1389; stage 2, transponder, merged as PR#1393; stage 3, LED strip, merged as PR#1394). Converts pwmDshotMotorHardwareConfig() in both pwm_output_dshot.c (F4) and pwm_output_dshot_hal.c (F7/H7) from unchecked dmaInit() to ownership-checked dmaAllocate() + dmaEnable().

Why this needed more than a mechanical swap

Burst DShot (USE_DSHOT_DMAR, active by default fleet-wide, confirmed live on HELIOSPRING: all 4 motors share TIM8) has multiple motor channels legitimately sharing one DMA stream (OWNER_TIMUP, keyed by timer number, not per-motor). configureTimer — the existing flag gating redundant per-timer setup — is true for every channel of a timer processed consecutively, not just the first (confirmed by tracing getTimerIndex(): dmaMotorTimerCount only advances when a genuinely new timer is registered). A mechanical if (!dmaAllocate(...)) return; swap would make the 2nd, 3rd, and 4th motor's claim on an already-owned stream fail outright — a flight-critical regression on any board with more than one motor per timer, not a defense-in-depth no-op like stages 1-3.

Added dshotDmaClaim(), matching an existing pattern already in this codebase (uartDmaClaim() in serial_uart_stm32f4xx.c, added for UART reconnect-safety): a stream already held by the same owner+resourceIndex is treated as an already-successful claim instead of a conflict, using the existing dmaGetOwner()/dmaGetResourceIndex() accessors (no new DMA API needed).

Review history

Local coderabbit review --agent (round 1) found a real issue: LL_EX_DMA_DeInit()/DMA_DeInit() (resets the DMA stream's registers) ran before the ownership-check in both the OWNER_MOTOR path (both files) and the OWNER_TIMUP burst path (pwm_output_dshot.c only). A failed claim on a stream already owned by something else would have deinitialized it out from under that owner instead of leaving it untouched. Fixed in 7504035e4 by moving both claim checks ahead of any DMA register access, matching the ordering already established for LED strip and transponder. Round 2: 0 findings. opencode second-opinion pass could not complete — 4 different models failed with different errors (timeout, auth, server error) in the same session, consistent with an infra-side outage rather than a model-selection issue; not retried further.

GitHub CodeRabbit analysis (requested independently of the local review above) found a further real issue: GPIO AF configuration and full timer setup (clock enable, prescaler/period, output compare, channel enable) still ran before the DMA ownership check in both files. A claim failure returned with motor->configured unset and the DMA stream untouched, but had already reconfigured that motor channel's GPIO pin and timer state. Fixed in 82d475ca6 by moving both claim checks to immediately after the existing dmaRef == NULL check, before any GPIO or timer register access — matching BF 4.5-maintenance's actual structure, where the equivalent claim happens before timerHardware/motor state is even read. Re-verified: build 7/7, host tests 43/43, local coderabbit review --agent 0 findings.

BF-master verdict check

Per the corrected /bf-gaps process (was diffing against BF master, now uses 4.5-maintenance as reference with master checked as fallback for bugfixes/architecture direction): git log 4.5-maintenance..master for pwm_output_dshot.c/_hal.c shows the shared-timer claim-bypass algorithm (dmaGetOwner()+dmaIsConfigured) is unchanged on master — only retyped by an unrelated opaque-resource refactor (PR#14990). Verdict: not-comparable (type-system divergence only, no gap to adopt or avoid). Full inventory: fix/dmainit-dshot-migration/dmainit-dshot-migration-bf-gaps.md; verdict logged in MIGRATION.md § BF-branch register.

Verification

  • Build: 7/7 targets, no warnings — HELIOSPRING (F4), FOXEERF722V4 (F7), STELLARH7DEV (H7, all 3 real aircraft), plus CRAZYBEEF4FS, MATEKF722HD, SPRACINGH7EF, plus SITL. Re-verified after each review-driven fix.
  • Host unit tests: make test — 43/43 test binaries pass, unchanged from PR#1394's baseline. No test binary directly exercises pwm_output_dshot*.c (same DMA/LL host-test limitation as prior stages).
  • Manually traced the shared-timer claim path against HELIOSPRING's real target config (TIM8 CH1-4, all 4 motors, processed consecutively) to confirm dshotDmaClaim()'s bypass fires correctly for motors 2-4 and the real allocation only happens once, for motor 1.
  • DShot beacon bench-confirmed on real hardware: heard the beacon tone through the motor, confirmed real (not coincidental) by toggling the feature off (silence) then on (tone returned). This exercises the same DMA-driven signal path this PR modifies. This is a bench tone test, not a flight test.
  • Real flight test still required before merge — motors/DSHOT is flight-critical; needs motors confirmed to spin correctly and hold through an actual flight on all 3 real aircraft (HELIOSPRING, FOXEERF722V4, STELLARH7DEV), separate from build/test/beacon verification. APEXF7 excluded (runs BF, not an EF test target).

Related

Summary by CodeRabbit

  • Bug Fixes
    • Improved DShot motor output setup to reliably initialize DMA resources.
    • Fixed repeated DMA configuration scenarios, including shared-timer and burst-update modes.
    • Prevented resource ownership conflicts that could interfere with motor output initialization.
    • Improved consistency when configuring multiple motors using shared hardware resources.

pwm_output_dshot.c (F4) and pwm_output_dshot_hal.c (F7/H7) called
dmaInit(), which silently overwrites any existing DMA owner. Replace
with dmaAllocate() + dmaEnable(), matching the pattern already used
by ADC, transponder, and LED strip.

Burst DShot (USE_DSHOT_DMAR, active by default fleet-wide) shares one
TIM_UP DMA stream across every motor channel on the same timer.
configureTimer is true for every channel of a shared timer processed
consecutively, not just the first, so a naive dmaAllocate() swap
would make the 2nd+ channel's claim on an already-owned stream fail
outright -- a flight-critical regression on any board with multiple
motors per timer (confirmed live on HELIOSPRING: all 4 motors on
TIM8). Added dshotDmaClaim(), matching the existing
uartDmaClaim()-style bypass in serial_uart_stm32f4xx.c: a stream
already held by the same owner+resourceIndex is treated as an
already-successful claim instead of a conflict.

pwm_output_dshot_hal.c had a separate latent issue exposed by this
migration: motors on a shared timer processed out of consecutive
order set motor->configured = true unconditionally, without
verifying the shared timer's DMA claim actually succeeded. Moved the
claim check ahead of that shortcut so it's verified for every motor,
not assumed from the first one's outcome.
Local CodeRabbit review flagged: LL_EX_DMA_DeInit()/DMA_DeInit()
(resets the DMA stream's registers) ran before the dmaAllocate()
check in the OWNER_MOTOR path, and pwm_output_dshot.c had the same
ordering issue in the OWNER_TIMUP burst path too. A failed claim on
a stream already held by an unrelated owner would deinit it out from
under that owner instead of leaving it untouched.

Moved both claim checks (dshotDmaClaim() for OWNER_TIMUP,
dmaAllocate() for OWNER_MOTOR) to before any DMA register access in
both files, matching the ordering already established for LED strip
and transponder.
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e989bfc2-e89f-429c-a686-572fcb0d22ce

📥 Commits

Reviewing files that changed from the base of the PR and between a4ca6ad and 82d475c.

📒 Files selected for processing (2)
  • src/main/drivers/pwm_output_dshot.c
  • src/main/drivers/pwm_output_dshot_hal.c

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Codacy Static Code Analysis
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-15T14:49:12.754Z
Learnt from: nerdCopter
Repo: emuflight/EmuFlight PR: 1189
File: src/main/drivers/bus_spi.c:631-631
Timestamp: 2026-05-15T14:49:12.754Z
Learning: In the EmuFlight repo (emuflight/EmuFlight), the STM32F303 (F303) targets currently do not compile on the master branch due to a pre-existing hardware RAM budget overflow (unrelated to any specific PR). When reviewing driver changes in shared driver files (e.g., src/main/drivers/bus_spi.c), avoid adding or fixing F303-specific driver behavior as part of the current PR; instead, defer those F303-targeted fixes to a dedicated F3-targeted PR and only revisit once the RAM overflow/build issue is resolved.

Applied to files:

  • src/main/drivers/pwm_output_dshot_hal.c
  • src/main/drivers/pwm_output_dshot.c
🔇 Additional comments (2)
src/main/drivers/pwm_output_dshot.c (1)

50-58: LGTM!

Also applies to: 159-172

src/main/drivers/pwm_output_dshot_hal.c (1)

45-53: LGTM!

Also applies to: 153-176


📝 Walkthrough

Walkthrough

DShot DMA setup now supports repeated claims of matching resources. It claims and enables DMA resources before timer configuration for burst and regular motor modes, and removes duplicate ownership initialization.

Changes

DShot DMA ownership

Layer / File(s) Summary
DMA claim contract
src/main/drivers/pwm_output_dshot.c, src/main/drivers/pwm_output_dshot_hal.c
Both implementations add dshotDmaClaim to reuse an existing DMA allocation only when its owner and resource index match.
DMA setup integration
src/main/drivers/pwm_output_dshot.c, src/main/drivers/pwm_output_dshot_hal.c
Burst mode claims timer-update DMA resources. Regular mode claims motor-index DMA resources. The setup enables the resource before configuration and removes duplicate dmaInit calls.

Estimated code review effort: 2 (Simple) | ~15 minutes

Merge Risk: ⚪ Minimal · up to 82d47

This change replaces unchecked DMA initialization with ownership-checked allocation while preserving shared-timer motor claims; the documented fixes and verification leave no actionable merge-blocking risk at the current head.

Sequence Diagram(s)

sequenceDiagram
  participant DShotSetup
  participant dshotDmaClaim
  participant DMAResource
  participant TimerSetup
  DShotSetup->>dshotDmaClaim: Claim burst or motor DMA resource
  dshotDmaClaim->>DMAResource: Reuse matching claim or allocate resource
  DShotSetup->>DMAResource: Enable resource
  DShotSetup->>TimerSetup: Configure timer and DMA
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: replacing unchecked dmaInit() with ownership-checked dmaAllocate().
Description check ✅ Passed The description provides detailed scope, rationale, review history, verification results, and remaining flight-test requirements.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nerdCopter

Copy link
Copy Markdown
Member Author

@coderabbitai, AI generated comment

Requesting analysis on the shared-timer DMA claim logic specifically, not a full review.

pwm_output_dshot.c and pwm_output_dshot_hal.c: burst DShot (USE_DSHOT_DMAR) shares one DMA stream (OWNER_TIMUP, keyed by timerGetTIMNumber()) across every motor channel on the same physical timer. configureTimer is true for every consecutive channel of a shared timer, not just the first (dmaMotorTimerCount only advances when a genuinely new timer is registered in getTimerIndex()).

dshotDmaClaim() (new, both files) is meant to make this safe: a stream already owned by the same owner+resourceIndex is treated as an already-successful claim rather than a conflict, matching the existing uartDmaClaim() pattern in serial_uart_stm32f4xx.c.

Please analyze:

  1. Does dshotDmaClaim()'s bypass correctly cover every processing order for channels on a shared timer (consecutive AND non-consecutive), not just the common case?
  2. In pwm_output_dshot_hal.c, is there any path where a motor on a shared timer sets motor->configured = true without dshotDmaClaim() having actually verified the claim for that specific call?
  3. Any DMA register access (DeInit, Enable, GPIO/TIM config) that still occurs before the claim check succeeds, in either file, for either the OWNER_TIMUP or OWNER_MOTOR path.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Locate relevant files ==='
fd -t f -a '^(pwm_output_dshot(_hal)?|serial_uart_stm32f4xx)\.c$' . || true

printf '%s\n' '=== Symbol locations and control-flow context ==='
rg -n -C 8 \
  -e '\bdshotDmaClaim\s*\(' \
  -e '\bpwmDshotMotorHardwareConfig\s*\(' \
  -e '\bgetTimerIndex\s*\(' \
  -e '\bmotor->configured\b' \
  -e '\bdmaAllocate\s*\(' \
  -e '\bdmaEnable\s*\(' \
  -e '\b(?:LL_EX_DMA_DeInit|DMA_DeInit|dmaInit)\s*\(' \
  -e '\buartDmaClaim\s*\(' \
  --glob 'pwm_output_dshot.c' \
  --glob 'pwm_output_dshot_hal.c' \
  --glob 'serial_uart_stm32f4xx.c' \
  .

printf '%s\n' '=== AST outlines ==='
while IFS= read -r f; do
  printf '\n--- %s ---\n' "$f"
  ast-grep outline "$f" --items all --type function --view expanded || true
done < <(fd -t f -a '^(pwm_output_dshot(_hal)?|serial_uart_stm32f4xx)\.c$' .)

Length of output: 28070


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== pwm_output_dshot.c: configuration function ==='
sed -n '141,273p' src/main/drivers/pwm_output_dshot.c | nl -ba -v141

printf '%s\n' '=== pwm_output_dshot_hal.c: configuration function ==='
sed -n '140,284p' src/main/drivers/pwm_output_dshot_hal.c | nl -ba -v140

printf '%s\n' '=== DMA ownership API declarations and definitions ==='
rg -n -C 12 \
  -e '^\s*(?:bool|void|resourceOwner_e|uint8_t)\s+dma(?:Allocate|Enable|GetOwner|GetResourceIndex)\s*\(' \
  -e '^\s*(?:bool|void|resourceOwner_e|uint8_t)\s+dma(?:Allocate|Enable|GetOwner|GetResourceIndex)\b' \
  src

printf '%s\n' '=== All direct DMA operations in the two configuration functions ==='
rg -n -C 3 \
  -e '\b(?:DMA_|LL_DMA_|LL_EX_DMA_|dma[A-Z][A-Za-z0-9_]*)' \
  src/main/drivers/pwm_output_dshot.c \
  src/main/drivers/pwm_output_dshot_hal.c

Length of output: 1253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

numbered_range() {
  local file="$1"
  local start="$2"
  local end="$3"
  awk -v start="$start" -v end="$end" \
    'NR >= start && NR <= end { printf "%d\t%s\n", NR, $0 }' "$file"
}

printf '%s\n' '=== pwm_output_dshot.c: configuration function ==='
numbered_range src/main/drivers/pwm_output_dshot.c 141 273

printf '%s\n' '=== pwm_output_dshot_hal.c: configuration function ==='
numbered_range src/main/drivers/pwm_output_dshot_hal.c 140 284

printf '%s\n' '=== Ownership API implementation candidates ==='
rg -n -C 10 \
  -e '\bdmaAllocate\s*\([^;]*\)\s*\{' \
  -e '\bdmaEnable\s*\([^;]*\)\s*\{' \
  -e '\bdmaGetOwner\s*\([^;]*\)\s*\{' \
  -e '\bdmaGetResourceIndex\s*\([^;]*\)\s*\{' \
  src/main

printf '%s\n' '=== Claim helper and timer-index definitions ==='
for f in src/main/drivers/pwm_output_dshot.c src/main/drivers/pwm_output_dshot_hal.c; do
  printf '\n--- %s ---\n' "$f"
  numbered_range "$f" 45 70
done

Length of output: 54473


@nerdCopter, I analyzed the requested paths only.

1. Shared-timer processing order

dshotDmaClaim() covers both consecutive and non-consecutive channels of one physical timer.

For each burst-mode call, both files use:

dshotDmaClaim(
    timerHardware->dmaTimUPIrqHandler,
    OWNER_TIMUP,
    timerGetTIMNumber(timerHardware->tim))

The bypass requires both of these values to match:

  • OWNER_TIMUP
  • The physical timer number from timerGetTIMNumber()

This is independent of configureTimer.

For a non-consecutive order such as TIM8 CH1, TIM1 CH1, TIM8 CH2:

  1. The first TIM8 call allocates the stream as OWNER_TIMUP with the TIM8 resource index.
  2. The TIM1 call adds a new timer entry.
  3. The second TIM8 call gets its original timer index from getTimerIndex().
  4. configureTimer becomes false because TIM8 is no longer the final timer entry.
  5. The burst path still calls dshotDmaClaim() before it branches on configureTimer.
  6. The matching owner and resource index cause the bypass to return true.

The same result applies to consecutive channels. Therefore, the bypass is not limited to the common consecutive case.

2. pwm_output_dshot_hal.c and motor->configured

I found no burst-mode path that sets motor->configured = true without a successful claim check in that call.

There are two assignments:

  • Line 231: the !configureTimer early-return path.
  • Line 283: the normal DMA setup completion path.

The early-return path is ordered as follows:

  1. dshotDmaClaim() at Line 225.
  2. Return on failed claim at Line 226.
  3. dmaEnable() at Line 228.
  4. motor->configured = true at Line 231.

The normal completion path also reaches Line 283 only after the burst claim at Line 225 or the non-burst OWNER_MOTOR allocation at Line 237 succeeds.

Thus, each HAL shared-timer motor call verifies the claim before it marks that motor configured.

3. Hardware access before the claim

DMA stream access

I found no direct DMA stream register access before the applicable claim succeeds.

In pwm_output_dshot.c:

  • dmaEnable() follows the claim at Lines 211–214 or Lines 219–222.
  • DMA_Cmd(), DMA_DeInit(), DMA_Init(), and DMA_ITConfig() follow the claim at Lines 226–271.

In pwm_output_dshot_hal.c:

  • dmaEnable() follows the claim at Lines 225–228 or Lines 237–240.
  • LL_EX_DMA_DeInit(), LL_EX_DMA_Init(), and LL_EX_DMA_EnableIT_TC() follow the claim at Lines 244–282.

This prevents a failed ownership claim from resetting or reconfiguring a DMA stream owned by another peripheral.

GPIO and timer access

GPIO and timer configuration still occurs before the DMA claim in both files.

In pwm_output_dshot.c, this includes:

  • IOConfigGPIOAF() at Line 171.
  • Timer clock, counter, base, output compare, channel, and preload configuration at Lines 172–207.

In pwm_output_dshot_hal.c, this includes:

  • IOConfigGPIOAF() at Lines 164–169.
  • Timer clock, counter, base, output compare, channel, and preload configuration at Lines 170–221.

Therefore, if the DMA claim fails, the function returns with motor->configured unset and without touching the contested DMA stream. However, it has already changed the GPIO and timer configuration for that motor channel.

This is separate from the fixed DMA-stream ownership issue. It is relevant only if the intended failure contract requires no GPIO or timer changes when DMA allocation fails.

You are interacting with an AI system.

GitHub CodeRabbit analysis on PR#1397 found: GPIO AF config and full
timer setup (clock enable, counter/prescaler/period, output compare,
channel enable) still ran before the DMA ownership check in both
files. A claim failure returned with motor->configured unset and the
DMA stream untouched, but had already reconfigured that motor
channel's GPIO pin and timer state.

Moved both claim checks (dshotDmaClaim() for OWNER_TIMUP,
dmaAllocate() for OWNER_MOTOR) to immediately after the existing
dmaRef == NULL check, before any GPIO or timer register access --
matching BF 4.5-maintenance's actual structure, where the equivalent
claim happens before timerHardware/motor state is even read, let
alone GPIO/timer configured.
@nerdCopter

Copy link
Copy Markdown
Member Author

@coderabbitai, AI generated comment

Addressed finding #3 (GPIO and timer access before the DMA claim) from the analysis in #1397 (comment).

Fixed in commit 82d475ca6: moved both claim checks (dshotDmaClaim() for OWNER_TIMUP, dmaAllocate() for OWNER_MOTOR) to immediately after the existing dmaRef == NULL check, before IOConfigGPIOAF() and any timer register access, in both pwm_output_dshot.c and pwm_output_dshot_hal.c. Matches BF 4.5-maintenance's actual structure (stm32/pwm_output_dshot.c:246-267), where the equivalent claim happens before timerHardware/motor state is read at all.

Findings #1 and #2 (shared-timer claim-order coverage, motor->configured reachability) required no code change -- already correct as analyzed.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Commit diff for the two affected files ==='
git show --format=fuller --find-renames -- \
  82d475ca6 \
  src/main/drivers/pwm_output_dshot.c \
  src/main/drivers/pwm_output_dshot_hal.c

printf '%s\n' '=== Function outlines ==='
ast-grep outline src/main/drivers/pwm_output_dshot.c \
  --items all --type function --match 'pwmDshotMotorHardwareConfig|dshotDmaClaim' --view expanded
ast-grep outline src/main/drivers/pwm_output_dshot_hal.c \
  --items all --type function --match 'pwmDshotMotorHardwareConfig|dshotDmaClaim' --view expanded

printf '%s\n' '=== Ordering-sensitive operations ==='
rg -n -C 5 \
  -e '\bdmaRef\s*==\s*NULL\b' \
  -e '\bdshotDmaClaim\s*\(' \
  -e '\bdmaAllocate\s*\(' \
  -e '\bIOConfigGPIOAF\s*\(' \
  -e '\b(?:DMA_DeInit|LL_EX_DMA_DeInit|DMA_Init|LL_EX_DMA_Init|DMA_Cmd|dmaEnable)\s*\(' \
  src/main/drivers/pwm_output_dshot.c \
  src/main/drivers/pwm_output_dshot_hal.c

Length of output: 21948


@nerdCopter, verified commit 82d475ca6.

Both implementations now perform the applicable ownership check immediately after the dmaRef == NULL guard:

  • src/main/drivers/pwm_output_dshot.c: Lines 161–171.
  • src/main/drivers/pwm_output_dshot_hal.c: Lines 155–165.

If dshotDmaClaim() or dmaAllocate() fails, the function returns before GPIO configuration, timer configuration, DMA_DeInit()/LL_EX_DMA_DeInit(), or other DMA register configuration.

This resolves finding 3. Findings 1 and 2 remain correct without changes.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@nerdCopter
nerdCopter marked this pull request as ready for review August 18, 2026 18:35
@nerdCopter
nerdCopter marked this pull request as draft August 18, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant