Skip to content

Fix PWM motor-role double-counting in pwmEnsureEnoughtMotors() - #11787

Open
sensei-hacker wants to merge 2 commits into
iNavFlight:release/9.1from
sensei-hacker:fix-pwm-motor-double-counting
Open

Fix PWM motor-role double-counting in pwmEnsureEnoughtMotors()#11787
sensei-hacker wants to merge 2 commits into
iNavFlight:release/9.1from
sensei-hacker:fix-pwm-motor-double-counting

Conversation

@sensei-hacker

Copy link
Copy Markdown
Member

Summary

Pass 1 of pwmEnsureEnoughtMotors() in src/main/drivers/pwm_mapping.c overcounts motor-only outputs that share a physical timer: a group of n shared-timer outputs that are already motor-only at the start of pass 1 inflates the motorOnlyOutputs counter by 2n - 1 instead of n.

pwmClaimTimer() force-syncs every output sharing a physical timer as soon as the first one is visited. Each sibling then independently satisfies TIM_IS_MOTOR_ONLY when the loop reaches its own index later in the same pass, and gets counted again — pass 1 has no guard against this, unlike pass 2's !TIM_IS_MOTOR_ONLY(...) check for the same class of re-visit.

The inflated count makes pass 2 more conservative than it should be when deciding whether to promote remaining AUTO outputs to motors. This can silently demote an output the user configured as a motor down to servo, with no error, log message, or other indication.

Trigger paths

Both hit an ordinary user, not just unusual target.c authoring:

  • Compile-time: target.c declares 2+ channels sharing one physical timer as plain TIM_USE_MOTOR (not TIM_USE_OUTPUT_AUTO) — hit at boot regardless of user action. A survey of the target tree found 34 targets with this pattern (e.g. KROOZX, SPRACINGF7DUAL, IFLIGHT_BLITZ_F7_AIO).
  • Runtime: the standard timer_output_mode <timer> MOTORS override, exposed in Configurator's Mixer tab, applied to a physical timer serving 2+ outputs. timerHardwareOverride() applies the override before the TIM_IS_MOTOR_ONLY check, so an override-forced group hits the same inflation as a compile-time-declared one.

Fix

Adds a per-physical-timer timerCounted[] dedup guard to pass 1, so each physical timer's motor-only group is counted exactly once regardless of how many of its channels get individually re-visited later in the same pass — mirroring the de-duplication pass 2 already has.

Branch scope

Confirmed present on release/9.1. Confirmed not present on maintenance-10.x — that branch's pwmEnsureEnoughtMotors() was already replaced by a unified pwmBuildTimerOutputList() (direct per-pad assignment, no separate inflatable counter) as an apparent unintentional side effect of unrelated refactor work, so no maintenance-10.x fix is needed.

Testing

  • SITL build (cmake -DSITL=ON, make SITL.elf) compiles cleanly with no new warnings.
  • Verified via a deterministic Python port of this exact algorithm (simulate_pwm_roles.py in the INAV harness tooling, not part of this PR) — before the fix, a synthetic 2-channel shared-timer group forced to MOTORS inflated the count to 3 and silently demoted a third output at motorCount=3; after the fix, the group counts correctly as 2 and the third output promotes as expected.

Related Issues

None filed yet — found while debugging PWM/DSHOT output setup on a custom target.

Pass 1 counted a shared-timer motor-only group of n outputs as 2n-1
instead of n: pwmClaimTimer() force-syncs every sibling on the same
physical timer as soon as the first one is visited, and the loop had
no guard against re-counting a sibling that was already promoted by
that broadcast when it reached its own turn later in the same pass.

The inflated count made pass 2 more conservative than it should be,
silently demoting a later AUTO output from motor to servo with no
warning. Triggerable both by target.c declaring 2+ TIM_USE_MOTOR
channels on one timer (unconditional at boot) and by the ordinary
runtime timer_output_mode MOTORS override exposed in Configurator's
Mixer tab.

Adds a per-physical-timer dedup guard, matching the de-duplication
pass 2 already has via its !TIM_IS_MOTOR_ONLY(...) check.
@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 PWM motor-role double-counting in pwmEnsureEnoughtMotors()

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Prevent pass-1 motor-only output counting from double-counting shared-timer siblings
• Ensure AUTO outputs aren’t conservatively blocked from being promoted to motors
• Align pass-1 counting behavior with pass-2’s existing de-dup logic
Diagram

graph TD
  A["pwmBuildTimerOutputList"] --> B["pwmEnsureEnoughtMotors"] --> C["Pass 1: count motor-only"] --> D["timerCounted[] (by timer2id)"] --> E["pwmClaimTimer"] --> F[("timerHardware table")]
  B --> G["Pass 2: promote AUTO to motors"] --> E
  C --> F
  G --> F
Loading
High-Level Assessment

The chosen approach (dedup motor-only counting by physical timer id in pass 1) is the most direct and lowest-risk fix: it corrects the inflated counter while preserving existing two-pass behavior and matching pass 2’s established de-dup pattern. Considered alternatives like refactoring the algorithm to operate on pre-grouped timers or changing pwmClaimTimer() semantics would be more invasive for the same outcome.

Files changed (1) +9 / -1

Bug fix (1) +9 / -1
pwm_mapping.cDeduplicate pass-1 motor-only counting by physical timer +9/-1

Deduplicate pass-1 motor-only counting by physical timer

• Adds a per-physical-timer timerCounted[] guard in pwmEnsureEnoughtMotors() pass 1. Prevents shared-timer motor-only sibling outputs from being re-counted after pwmClaimTimer() synchronizes the entire timer group, avoiding an inflated motorOnlyOutputs baseline that can block correct AUTO-to-motor promotion in pass 2.

src/main/drivers/pwm_mapping.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


Action required

1. Motor outputs undercounted 🐞 Bug ≡ Correctness
Description
Pass 1 now de-duplicates motor-only counting per physical timer, but it only adds 1 +
pwmClaimTimer(changed), so a timer whose channels are already TIM_USE_MOTOR contributes just 1
regardless of how many motor pads it has. This undercount can make pass 2 promote
TIM_USE_OUTPUT_AUTO pads to motors and claim whole timers as motor-only, silently removing
servo-capable outputs.
Code

src/main/drivers/pwm_mapping.c[R293-296]

+        if (TIM_IS_MOTOR_ONLY(timHw->usageFlags) && !timerCounted[timer2id(timHw->tim)]) {
+            timerCounted[timer2id(timHw->tim)] = true;
            motorOnlyOutputs++;
            motorOnlyOutputs += pwmClaimTimer(timHw->tim, timHw->usageFlags);
Evidence
pwmClaimTimer() returns only the number of sibling pads whose usageFlags actually change, so for
a timer where all pads are already motor-only, pass 1 will add just motorOnlyOutputs++ (1) and
then mark the timer counted, skipping the remaining motor pads. Pass 2 promotes AUTO pads while
motorOnlyOutputs < motorCount, and promotion claims the entire timer via pwmClaimTimer(); those
newly-claimed siblings won’t be demoted later because the pass-2 branch only runs for
TIM_IS_MOTOR(...) && !TIM_IS_MOTOR_ONLY(...).

A concrete repo example (IFLIGHT_BLITZ_F7_AIO) has 4 motor-only pads on TIM3 plus additional
AUTO outputs on other timers, so the new “count once per timer” behavior can trigger unnecessary
AUTO-to-motor promotion even though sufficient motor pads already exist.

src/main/drivers/pwm_mapping.c[260-317]
src/main/target/IFLIGHT_BLITZ_F7_AIO/target.c[32-45]

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

## Issue description
`pwmEnsureEnoughtMotors()` pass 1 now uses `timerCounted[]` to avoid double-counting a physical timer, but it still relies on `motorOnlyOutputs++` plus `pwmClaimTimer()`'s **changed** count to represent the number of motor outputs on that timer. If all channels on a timer are already motor-only, `pwmClaimTimer()` returns 0, so the timer contributes only 1 to `motorOnlyOutputs` even if it has 2–4 motor pads.

This can leave `motorOnlyOutputs < motorCount` and cause pass 2 to promote `TIM_USE_OUTPUT_AUTO` pads to motors unnecessarily; because promotion calls `pwmClaimTimer()`, it can claim an entire unrelated timer as motor-only and those siblings will not be demoted later (they no longer match the pass-2 `!TIM_IS_MOTOR_ONLY(...)` guard).

## Issue Context
Example target with 4 motor pads on a single timer plus AUTO pads:
- `IFLIGHT_BLITZ_F7_AIO` has 4x `TIM_USE_MOTOR` on `TIM3` and additional `TIM_USE_OUTPUT_AUTO` on `TIM1`/`TIM4`, so after this change pass 1 counts the 4 TIM3 motors as **1**, potentially triggering pass-2 promotion of AUTO outputs.

## Fix Focus Areas
- src/main/drivers/pwm_mapping.c[260-317]
- src/main/target/IFLIGHT_BLITZ_F7_AIO/target.c[32-45]

## Suggested fix approach
When encountering the first motor-only pad for a physical timer (i.e., when `timerCounted[timerId]` is false), compute the number of usable pads for that timer and add that count once, instead of adding `1 + pwmClaimTimer(changed)`.

Concretely:
1. Compute `timerId = timer2id(timHw->tim)` once.
2. If motor-only and not yet counted:
  - Count pads that share `timHw->tim` (optionally excluding conflicts via `checkPwmTimerConflicts()` if the intent is “usable motor outputs”).
  - Add that pad-count to `motorOnlyOutputs`.
  - Call `pwmClaimTimer()` to sync flags, but do **not** use its return value for the motor output count.

This preserves de-duplication while keeping the counter accurate for timers where channels were already motor-only.

ⓘ 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 route each action level your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +293 to 296
if (TIM_IS_MOTOR_ONLY(timHw->usageFlags) && !timerCounted[timer2id(timHw->tim)]) {
timerCounted[timer2id(timHw->tim)] = true;
motorOnlyOutputs++;
motorOnlyOutputs += pwmClaimTimer(timHw->tim, timHw->usageFlags);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Motor outputs undercounted 🐞 Bug ≡ Correctness

Pass 1 now de-duplicates motor-only counting per physical timer, but it only adds 1 +
pwmClaimTimer(changed), so a timer whose channels are already TIM_USE_MOTOR contributes just 1
regardless of how many motor pads it has. This undercount can make pass 2 promote
TIM_USE_OUTPUT_AUTO pads to motors and claim whole timers as motor-only, silently removing
servo-capable outputs.
Agent Prompt
## Issue description
`pwmEnsureEnoughtMotors()` pass 1 now uses `timerCounted[]` to avoid double-counting a physical timer, but it still relies on `motorOnlyOutputs++` plus `pwmClaimTimer()`'s **changed** count to represent the number of motor outputs on that timer. If all channels on a timer are already motor-only, `pwmClaimTimer()` returns 0, so the timer contributes only 1 to `motorOnlyOutputs` even if it has 2–4 motor pads.

This can leave `motorOnlyOutputs < motorCount` and cause pass 2 to promote `TIM_USE_OUTPUT_AUTO` pads to motors unnecessarily; because promotion calls `pwmClaimTimer()`, it can claim an entire unrelated timer as motor-only and those siblings will not be demoted later (they no longer match the pass-2 `!TIM_IS_MOTOR_ONLY(...)` guard).

## Issue Context
Example target with 4 motor pads on a single timer plus AUTO pads:
- `IFLIGHT_BLITZ_F7_AIO` has 4x `TIM_USE_MOTOR` on `TIM3` and additional `TIM_USE_OUTPUT_AUTO` on `TIM1`/`TIM4`, so after this change pass 1 counts the 4 TIM3 motors as **1**, potentially triggering pass-2 promotion of AUTO outputs.

## Fix Focus Areas
- src/main/drivers/pwm_mapping.c[260-317]
- src/main/target/IFLIGHT_BLITZ_F7_AIO/target.c[32-45]

## Suggested fix approach
When encountering the first motor-only pad for a physical timer (i.e., when `timerCounted[timerId]` is false), compute the number of usable pads for that timer and add that count once, instead of adding `1 + pwmClaimTimer(changed)`.

Concretely:
1. Compute `timerId = timer2id(timHw->tim)` once.
2. If motor-only and not yet counted:
   - Count pads that share `timHw->tim` (optionally excluding conflicts via `checkPwmTimerConflicts()` if the intent is “usable motor outputs”).
   - Add that pad-count to `motorOnlyOutputs`.
   - Call `pwmClaimTimer()` to sync flags, but do **not** use its return value for the motor output count.

This preserves de-duplication while keeping the counter accurate for timers where channels were already motor-only.

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

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