Skip to content

Fix silent DSHOT loss on 11 F4/F7 targets from shared DMA request lines - #11803

Open
sensei-hacker wants to merge 5 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:investigate-shared-tim-dma-request-lines
Open

Fix silent DSHOT loss on 11 F4/F7 targets from shared DMA request lines#11803
sensei-hacker wants to merge 5 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:investigate-shared-tim-dma-request-lines

Conversation

@sensei-hacker

Copy link
Copy Markdown
Member

Summary

Several F4/F7 targets declare two motor outputs whose dmavar index (the DMA-option selector in DEF_TIM(tim, ch, pin, usage, flags, dmavar)) resolves to the identical DMA stream. INAV claims DMA in strict motor-init order and silently drops DSHOT on whichever output initializes second — no boot-time error, the output just never spins under DSHOT.

Changes

  • AIKONF4, ATOMRCF405MINI, ATOMRCF405NAVI, FF_PIKOF4, FISHDRONEF4, FRSKYF4, FRSKYPILOT, PIXRACER, TMOTORF7, WARPF7: moved the losing output to an alternate dmavar/timer-channel option on the same physical pin (no PCB rewire needed).
  • BEEROTORF4: two of its collisions have no alternative on either side (both timer/channel combinations are hard-wired to the same DMA stream on this MCU) — documented with a comment rather than a functional change, since there's no fix available on this hardware revision.
  • FF_PIKOF4 has two build variants (FF_PIKOF4OSD and default) sharing one timerHardware[] array; both had the same collision and both are fixed identically.

Testing

  • Verified each candidate fix against simulate_pwm_roles.py's DMA-stream-collision model (a deterministic port of the firmware's own pwmClaimTimer/pwmBuildTimerOutputList DMA-claim logic) before applying it, confirming no new collision is introduced and the existing one is resolved.
  • Compile-only build verified clean for all 11 targets (no errors, no warnings).
  • Not tested on real hardware — I don't have any of these 11 boards. The fix only changes which DMA stream/timer-channel a motor output uses; it does not change pin assignments, so a working build should not regress, but I'd appreciate testing from anyone with the hardware before this merges.

Related Issues

None filed yet — found via a broader audit of shared-DMA-request-line defects across F4/F7 targets.

Several targets declared two motor outputs on DMA-option indices
(dmavar) that resolve to the identical DMA stream. The firmware claims
DMA in strict motor-init order and silently drops DSHOT on whichever
output initializes second, with no boot-time error, so the affected
output never spins under DSHOT.

Fixed by moving the losing output to an alternate dmavar/timer-channel
option on the same physical pin (no rewiring needed) where one exists,
verified with simulate_pwm_roles.py's DMA-stream-collision check.
BEEROTORF4 has two collisions where both sides are hard-wired to the
same DMA stream with no alternative; documented with a comment instead
since there is no fix available on this hardware revision.
@qodo-code-review

Copy link
Copy Markdown
Contributor

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix silent DSHOT loss from shared DMA request lines on F4/F7 targets

🐞 Bug fix ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Fix DSHOT silently failing on specific motor outputs due to DMA-stream collisions.
• Reassign affected outputs to alternate dmavar/timer-channel options on the same pins.
• Document unfixable collisions on hardware with no alternative DMA mapping.
Diagram

graph TD
  A["Target "target.c" timerHardware[]"] --> B["DEF_TIM mappings (dmavar)"] --> C["pwmBuildTimerOutputList"] --> D["pwmClaimTimer"] --> E[("DMA stream allocation")]
  E --> F{"DMA stream collision?"}
  F -->|"No"| G["All motors DSHOT OK"]
  F -->|"Yes"| H["One output loses DSHOT"]

  subgraph Legend
    direction LR
    _cfg["Config/Target"] ~~~ _dma[("DMA resource")] ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add boot-time DMA collision detection (fail fast)
  • ➕ Makes failures visible immediately instead of silently losing DSHOT
  • ➕ Helps catch future target configuration regressions automatically
  • ➖ Does not fix affected boards by itself; only surfaces the issue
  • ➖ May require defining what "fatal" means vs fallback-to-PWM behavior
2. Auto-select a non-colliding dmavar at runtime
  • ➕ Reduces target-by-target manual dmavar tuning
  • ➕ Can adapt initialization ordering to avoid collisions
  • ➖ More complex timer/DMA selection logic; higher regression risk
  • ➖ May be limited by MCUs where both options still map to the same stream
3. Maintain a per-MCU DMA capability map and validate target tables in CI
  • ➕ Prevents shipping targets that cannot support DSHOT on declared outputs
  • ➕ Keeps target definitions honest across variants
  • ➖ Requires ongoing maintenance of MCU DMA mapping metadata
  • ➖ CI validation may be non-trivial for conditional/variant target tables

Recommendation: Merge this PR as the pragmatic, low-risk fix: it keeps the same physical pins while moving specific outputs onto alternate DMA options where available, and documents where hardware makes the issue unfixable. As a follow-up, consider adding boot-time detection and/or CI validation so future DMA-stream collisions don’t remain silent.

Files changed (11) +24 / -24

Bug fix (11) +24 / -24
target.cMove TIM8 CH2/CH3 outputs to dmavar 1 to avoid DMA sharing +2/-2

Move TIM8 CH2/CH3 outputs to dmavar 1 to avoid DMA sharing

• Switches S2 and S3 on TIM8 to dmavar 1 so they use a dedicated DMA stream rather than sharing TIM8 CH1’s combined-channel request. Adds inline comments explaining the collision avoidance rationale.

src/main/target/AIKONF4/target.c

target.cRe-route S4 timer and adjust S7 dmavar to eliminate DMA collisions +2/-2

Re-route S4 timer and adjust S7 dmavar to eliminate DMA collisions

• Moves S4 from TIM1_CH3N to TIM3_CH4 on the same PB1 pin to avoid unavoidable sharing with S3. Changes S7 (TIM2_CH4 PB11) to dmavar 1 to avoid sharing DMA1 Stream7 with S8.

src/main/target/ATOMRCF405MINI/target.c

target.cRe-route S4 timer and adjust S7 dmavar to eliminate DMA collisions +2/-2

Re-route S4 timer and adjust S7 dmavar to eliminate DMA collisions

• Moves S4 from TIM1_CH3N to TIM3_CH4 on PB1 to avoid DMA stream sharing with S3. Changes S7 to dmavar 1 to avoid a DMA collision with S8 on DMA1 Stream7.

src/main/target/ATOMRCF405NAVI/target.c

target.cFix one collision and document two unfixable DSHOT outputs +3/-3

Fix one collision and document two unfixable DSHOT outputs

• Documents that M2 (TIM1_CH3N PB1) shares M1’s DMA stream with no alternative, so DSHOT won’t work on M2. Moves M6 (TIM8_CH2 PC7) to dmavar 1 to avoid sharing M5’s combined-channel request, and documents that M7 shares M4’s DMA stream with no alternative.

src/main/target/BEEROTORF4/target.c

target.cSwitch TIM2 CH4 to dmavar 1 and annotate TIM12 as non-DSHOT-capable +4/-4

Switch TIM2 CH4 to dmavar 1 and annotate TIM12 as non-DSHOT-capable

• Changes TIM2_CH4 (PA3) to dmavar 1 in both FF_PIKOF4OSD and default variants to avoid a DMA1 Stream7 collision with TIM3_CH3. Adds explicit comments that TIM12 has no DMA request line on this MCU, so DSHOT cannot work on those outputs regardless of dmavar.

src/main/target/FF_PIKOF4/target.c

target.cMove TIM8 CH2/CH3 outputs to dmavar 1 to avoid DMA sharing +2/-2

Move TIM8 CH2/CH3 outputs to dmavar 1 to avoid DMA sharing

• Updates TIM8 CH2 and CH3 to dmavar 1 so they use a dedicated DMA stream and avoid sharing TIM8 CH1’s combined-channel request. Keeps pins and timer channels the same, only changing the DMA option selection.

src/main/target/FISHDRONEF4/target.c

target.cSwitch TIM2 CH4 output to dmavar 1 to avoid stream collision +1/-1

Switch TIM2 CH4 output to dmavar 1 to avoid stream collision

• Changes S3_OUT (TIM2_CH4 PA3) to dmavar 1 so it no longer shares a DMA stream with S1_OUT. Adds an inline comment to capture the reason for the dmavar selection.

src/main/target/FRSKYF4/target.c

target.cAdjust TIM1 dmavar for S3/S4 and document an unfixable collision on S8 +3/-3

Adjust TIM1 dmavar for S3/S4 and document an unfixable collision on S8

• Moves S3 and S4 (TIM1 CH1/CH2) to dmavar 1 to avoid combined-channel DMA request sharing within TIM1 outputs. Documents that S8 (TIM4_CH3) has no alternate dmavar and shares a DMA stream, so DSHOT will not work on that output.

src/main/target/FRSKYPILOT/target.c

target.cSwitch TIM1 CH1/CH2 outputs to dmavar 1 to avoid combined-channel DMA sharing +2/-2

Switch TIM1 CH1/CH2 outputs to dmavar 1 to avoid combined-channel DMA sharing

• Updates S3_OUT and S4_OUT (TIM1 CH2/CH1) to dmavar 1 so they use dedicated streams rather than sharing S2_OUT’s combined-channel request. No pin changes; only DMA option selection is adjusted.

src/main/target/PIXRACER/target.c

target.cFlip TIM8 CH2 dmavar and document an unfixable TIM4/TIM3 DMA collision +2/-2

Flip TIM8 CH2 dmavar and document an unfixable TIM4/TIM3 DMA collision

• Changes TIM8 CH2 to dmavar 0 on F7 to select the dedicated stream (noting the index order difference vs F4) and avoid sharing CH1’s combined-channel request. Documents that TIM4_CH3 shares a DMA stream with TIM3_CH3 with no alternative, so DSHOT won’t work on that output.

src/main/target/TMOTORF7/target.c

target.cSwitch TIM2 CH4 output to dmavar 1 to avoid DMA stream collision +1/-1

Switch TIM2 CH4 output to dmavar 1 to avoid DMA stream collision

• Changes S4 (TIM2_CH4 PB11) to dmavar 1 to avoid sharing a DMA stream with S1. Keeps the same pin assignment while selecting a non-colliding DMA option.

src/main/target/WARPF7/target.c

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Servo timer capacity reduced 🐞 Bug ≡ Correctness
Description
On ATOMRCF405MINI and ATOMRCF405NAVI, moving S4 to TIM3 causes a 4-motor setup to claim TIM3 for
motors, which blocks S8 (also on TIM3) from being allocated as a servo output. This reduces the
remaining servo outputs after allocating 4 motors and can trigger
PWM_INIT_ERROR_NOT_ENOUGH_SERVO_OUTPUTS for mixers requiring 4+ servos.
Code

src/main/target/ATOMRCF405MINI/target.c[40]

+    DEF_TIM(TIM3, CH4,  PB1,    TIM_USE_OUTPUT_AUTO,    0, 0), // S4 -- moved off TIM1_CH3N (DMA-stuck sharing S3's stream on both dmavar options)
Evidence
S4 and S8 both map to TIM3, so when S4 is assigned as a motor output, pwmAssignOutput() ends up
calling pwmClaimTimer() which propagates motor-only usageFlags across all channels on that
timer, effectively marking S8 as motor-timer-associated as well. During servo allocation, the code
explicitly rejects any output whose timer already has a motor on it (!pwmHasMotorOnTimer(...)), so
S8 becomes ineligible for servo assignment; if the mixer’s required servo count then exceeds the
remaining eligible outputs, servo initialization fails with the not-enough-servo-outputs error.

src/main/target/ATOMRCF405MINI/target.c[36-47]
src/main/drivers/pwm_mapping.c[277-317]
src/main/drivers/pwm_mapping.c[362-395]
src/main/drivers/pwm_mapping.c[483-492]
src/main/target/ATOMRCF405NAVI/target.c[36-47]

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

## Issue description
ATOMRCF405MINI and ATOMRCF405NAVI changed S4 from TIM1_CH3N to TIM3_CH4 (same PB1 pin), but S8 is also on TIM3. In a typical 4-motor configuration, INAV assigns motors in `timerHardware[]` order and uses `pwmClaimTimer()` to apply motor usage to all channels on that timer, which makes S8 unusable for servo outputs; this reduces the available servo output count and can cause `PWM_INIT_ERROR_NOT_ENOUGH_SERVO_OUTPUTS` on servo-heavy mixers.

## Issue Context
- Targets affected: `ATOMRCF405MINI`, `ATOMRCF405NAVI`
- Change: S4 now uses `TIM3` while S8 already uses `TIM3`.
- Allocation rule: servos cannot share a timer with any motor output, and motor claiming propagates to all channels of the timer.

## Fix Focus Areas
- src/main/target/ATOMRCF405MINI/target.c[36-47]
- src/main/target/ATOMRCF405NAVI/target.c[36-47]
- src/main/drivers/pwm_mapping.c[277-317]
- src/main/drivers/pwm_mapping.c[362-395]
- src/main/drivers/pwm_mapping.c[483-492]

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


Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

DEF_TIM(TIM8, CH4, PC9, TIM_USE_OUTPUT_AUTO, 0, 0), // S2
DEF_TIM(TIM1, CH1, PA8, TIM_USE_OUTPUT_AUTO, 0, 0), // S3
DEF_TIM(TIM1, CH3N, PB1, TIM_USE_OUTPUT_AUTO, 0, 0), // S4
DEF_TIM(TIM3, CH4, PB1, TIM_USE_OUTPUT_AUTO, 0, 0), // S4 -- moved off TIM1_CH3N (DMA-stuck sharing S3's stream on both dmavar options)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Servo timer capacity reduced 🐞 Bug ≡ Correctness

On ATOMRCF405MINI and ATOMRCF405NAVI, moving S4 to TIM3 causes a 4-motor setup to claim TIM3 for
motors, which blocks S8 (also on TIM3) from being allocated as a servo output. This reduces the
remaining servo outputs after allocating 4 motors and can trigger
PWM_INIT_ERROR_NOT_ENOUGH_SERVO_OUTPUTS for mixers requiring 4+ servos.
Agent Prompt
## Issue description
ATOMRCF405MINI and ATOMRCF405NAVI changed S4 from TIM1_CH3N to TIM3_CH4 (same PB1 pin), but S8 is also on TIM3. In a typical 4-motor configuration, INAV assigns motors in `timerHardware[]` order and uses `pwmClaimTimer()` to apply motor usage to all channels on that timer, which makes S8 unusable for servo outputs; this reduces the available servo output count and can cause `PWM_INIT_ERROR_NOT_ENOUGH_SERVO_OUTPUTS` on servo-heavy mixers.

## Issue Context
- Targets affected: `ATOMRCF405MINI`, `ATOMRCF405NAVI`
- Change: S4 now uses `TIM3` while S8 already uses `TIM3`.
- Allocation rule: servos cannot share a timer with any motor output, and motor claiming propagates to all channels of the timer.

## Fix Focus Areas
- src/main/target/ATOMRCF405MINI/target.c[36-47]
- src/main/target/ATOMRCF405NAVI/target.c[36-47]
- src/main/drivers/pwm_mapping.c[277-317]
- src/main/drivers/pwm_mapping.c[362-395]
- src/main/drivers/pwm_mapping.c[483-492]

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

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

RAM / Flash usage vs. base branch — commit 05f40e4

Target Flash Δ RAM Δ
MATEKF405 ±0 B (±0.00%) ±0 B (±0.00%)
MATEKF722 ±0 B (±0.00%) ±0 B (±0.00%)
MATEKF765 ±0 B (±0.00%) ±0 B (±0.00%)
MATEKH743 ±0 B (±0.00%) ±0 B (±0.00%)

See RAM/flash optimization guide for techniques to reduce usage.

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Test firmware build ready — commit 05f40e4

Download firmware for PR #11803

244 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.

TIM2_CH1 (TAIL, PA15) and TIM3_CH2 (S2, PB5) both resolve to DMA1
Stream5 and both have exactly one DMA option in the F7 table -- no
alternate dmavar or same-pin timer channel exists on either side, so
this can't be fixed without a PCB rewire. Same known-unfixable pair
already documented on BEEROTORF4's M4/M7. TAIL loses the claim and
DSHOT silently doesn't work on it.

Comment-only change; both targets build clean.
37 targets, comment-only: appended an explanatory comment to the losing
DEF_TIM line for each undocumented NOTICE-tier motor-vs-motor collision
(S5+ output silently loses DSHOT, basic quad unaffected) and each
LED-strip collision (motor always wins over TIM_USE_LED, motors init
before ledStripInit() in fc_init.c -- LED silently doesn't light).

6 targets (HGLRCF722, MATEKF411, MATEKF722SE, SPEEDYBEEF7MINI, TMOTORF7,
TMOTORVELOXF7V2) were already fully documented and untouched.

No DEF_TIM argument changed anywhere -- verified programmatically and by
independent code review that every diff line is comment-only.

MAMBAF405US excluded from this pass: its default (non-I2C) build variant
has a genuine, previously-undetected CERTAIN hazard behind an undocumented
#ifdef, found while isolating a same-(tim,ch) duplicate during this work.
Needs the same fix-or-document workflow as the original 11 CERTAIN
targets, not a NOTICE-tier comment -- tracked as a follow-up.
…ion)

TIM8_CH3 (S1_OUT, PC8) used dmavar 0, resolving to DMA2 Stream2 --
the same stream TIM1_CH2 (S4_OUT, PA9, array position 0) claims with
dmavar 1. Same stream, different channel selector, so on a basic quad
build S1_OUT silently lost DSHOT since it initializes later.

Fixed by switching TIM8_CH3 to dmavar 1 (DMA2 Stream4), matching the
value the target's #ifdef MAMBAF405US_I2C build variant already used
safely for the same pin. That made both branches byte-identical, so
collapsed the #ifdef/#else/#endif into one unconditional block --
confirmed via grep that MAMBAF405US_I2C is otherwise only used for
USB string and I2C bus/pin selection, unrelated to timerHardware[].

Both MAMBAF405US and MAMBAF405US_I2C build clean. classify_collisions.py
and a full motorCount 1-12 sweep confirm zero hazards on MAMBAF405US now.
The default (#else) branch's old "S4_OUT..S1_OUT" labels had the
convention backwards. Confirmed with the board owner: silkscreen
labels are always assigned in top-to-bottom array declaration order,
same as the MAMBAF405US_I2C branch's plain "S1".."S4" labels this
target.c used to carry before the two branches were collapsed. Fixed
to match: S1=TIM1_CH2, S2=TIM1_CH1, S3=TIM8_CH4, S4=TIM8_CH3.

This also corrects which physical output was actually losing DSHOT
in the previous commit's description: it's S4 (TIM8_CH3, array
position 3), not S1. The dmavar fix itself (454391b) is unaffected
-- comment-only change.
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