Fix DShot output for reversible motors when disarmed - #11847
Fix DShot output for reversible motors when disarmed#11847sensei-hacker wants to merge 3 commits into
Conversation
writeMotors() picked its DShot scaling formula from reversibleMotorsThrottleState/throttleRangeMin/throttleRangeMax, but those are only updated by mixTable()'s armed-flight direction-switching logic, which never runs while disarmed. As a result, 3D/reversible motor testing via MSP_SET_MOTOR (the configurator's motor-test UI) never produced reverse thrust, and even forward values were checked against the RC-stick deadband instead of the ESC's configured 3D deadband. While disarmed, infer direction directly from the motor value against reversibleMotorsConfig()->deadband_low/deadband_high instead. Armed-flight behavior is unchanged. Companion to inav-configurator PR iNavFlight#2595.
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
PR Summary by QodoFix disarmed DShot output for reversible motors
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1.
|
|
Test firmware build ready — commit Download firmware for PR #11847 245 targets built. Find your board's
|
|
RAM / Flash usage vs. base branch — commit
See RAM/flash optimization guide for techniques to reduce usage. |
The disarmed-motor-testing scaling assigned scaleRangef()'s float result directly into an unsigned motorValue before constrain() ran (undefined behavior for negative results, reachable since min_command/MSP_SET_MOTOR allow it), could divide by zero when min_command/3d_deadband_low or 3d_deadband_high/max_throttle are configured equal, and used exclusive boundary comparisons where the rest of the codebase (mixTable's direction switch, handleOutputScaling's stop check) treats boundary values as thrust, not deadband. Extracted the calculation into calculateDisarmedReversibleMotorsDshotValue(), using inclusive boundaries, explicit degenerate-config guards before scaling, and a signed intermediate before constrain(). Not SITL_BUILD-gated (pure arithmetic), so it stays isolated from the surrounding hardware-only code.
|
Addressed the 4 Qodo findings (all verified reachable via valid CLI configuration, not just theoretical):
Unit tests: 60/60 passing. Rebuilt SITL and MATEKF405 ( |
The unit test for the disarmed reversible-motor DShot scaling only ever called a hand-reproduction of the logic, since writeMotors() is SITL_BUILD-gated and can't be linked into the host test harness. A hand-copy can't catch a regression introduced directly in mixer.c. Extracted calculateDisarmedReversibleMotorsDshotValue() into its own file (mixer_disarmed_dshot.c/.h), with no INAV-specific dependencies beyond common/maths.c, and linked it into mixer_unittest.cc via CMakeLists.txt's existing `depends` mechanism (already used for telemetry/hott.c, io/rcdevice.c, etc.). The test now calls the real function directly. DSHOT_* constants moved to their own mixer_dshot_constants.h so the new file doesn't need to pull in mixer.h's drivers/timer.h dependency.
|
Follow-up: fully addressed Qodo finding #4 ("tests execute copied implementation").
Verified this actually catches regressions: reverted the boundary comparisons in the real file to the old buggy Unit tests: 58/58 passing. SITL and MATEKF405 ( |
|
@Pikkuboo I don't suppose you could test this with iNavFlight/inav-configurator#2595 ? |
Summary
Fixes
writeMotors()so DShot output is correct for 3D/reversible motors during motor-testing while the FC is disarmed (the MSP_SET_MOTOR path used by the configurator's motor-test UI). Firmware companion to configurator PR #2595.Problem
writeMotors()picked its DShot scaling formula fromreversibleMotorsThrottleState/throttleRangeMin/throttleRangeMax, but those are only updated bymixTable()'s armed-flight direction-switching logic, which never runs while disarmed (mixTable()early-returns before reaching it). Consequences while disarmed:reversibleMotorsThrottleStatestays FORWARD, so motor values that should produce reverse thrust were instead sentDSHOT_DISARM_COMMAND.throttleRangeMin, ~1550) instead of the ESC's configured 3D deadband (reversibleMotorsConfig()->deadband_high, ~1514).This matches field-reported behavior on a SpeedyBee F7 V3 (comment on #2595, 2026-03-19): motor testing works forward-only with reversible motors enabled.
Changes
src/main/flight/mixer.c: added anif (!ARMING_FLAG(ARMED))branch insidewriteMotors()'sFEATURE_REVERSIBLE_MOTORSDShot dispatch. While disarmed, direction is inferred directly from the motor value againstreversibleMotorsConfig()->deadband_high/deadband_low, scaled into the DShot ranges. Armed-flight dispatch is completely unchanged.src/test/unit/mixer_unittest.cc(new): unit tests covering forward/reverse/deadband scaling while disarmed, boundary values at exactlydeadband_high/deadband_low, and regression coverage for armed flight and non-reversible motor testing. Includes "SourceSync" tests that read the livemixer.cat test time to guard against silent drift (the code under test can't be linked directly into this host test harness, sincewriteMotors()is#if !defined(SITL_BUILD)-gated and the harness always definesSITL_BUILD).Testing
make check— 57/57 passing (newmixer_unittest: 16/16).MATEKF405with-DWARNINGS_AS_ERRORS=ON(matches CI recipe) — clean, Flash 68.14%, RAM 82.40%.Code Review
Reviewed with inav-code-review agent — no critical issues; addressed the review's findings in the test file (removed an unverifiable precedent citation, fixed a boundary-condition mismatch between the test's oracle helpers and the real code's strict comparisons, added explicit boundary tests, trimmed changelog-style comments).
Related
Companion to inav-configurator PR #2595.