Skip to content

Add dshot_reversed_motors: per-motor spin direction via DShot commands 20/21 - #12022

Open
Raffi1202 wants to merge 4 commits into
iNavFlight:maintenance-11.xfrom
Raffi1202:motor-direction-mask-11
Open

Raffi1202 wants to merge 4 commits into
iNavFlight:maintenance-11.xfrom
Raffi1202:motor-direction-mask-11

Conversation

@Raffi1202

@Raffi1202 Raffi1202 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Replaces #12011 after b14ckyy's review there: no ESC configuration is written any more. One setting, no new MSP.

What it does

  • dshot_reversed_motors (uint16 bitmask, bit 0 = motor 1) in motorConfig. A set bit gives that ESC SPIN_DIRECTION_REVERSED (21), a clear bit SPIN_DIRECTION_NORMAL (20). Read and written through the standard settings MSP.
  • The DShot command queue carries one command per motor (uint8_t cmd[MAX_MOTORS] + repeats), so a single frame addresses every ESC. sendDShotCommand() keeps its all-motor semantics for the beacon; sendDShotSpinDirection(mask) builds the per-motor frame. Same on RP2350.
  • Re-sent because the ESC does not store it: on every arm ahead of the first throttle frame (the command frames replace it, so the ESC has its direction before it gets throttle), within 100 ms of the setting changing, and every 2 s while disarmed. Never while the motors run (motor test) or during the DShot beacon guard.
  • Every arm sends the directions, also with an all-zero mask (as Betaflight does): Bluejay and BLHeli_S keep a runtime 21 through an FC reboot. This puts ~20 ms of command frames before the first throttle frame on every DShot arm; reversible-motor setups with an all-zero mask are unchanged. While disarmed, an all-zero mask stays silent.
  • Turtle: tryArm() sends mask ^ all motors, disarm() restores the mask instead of broadcasting 20.
  • PG_MOTOR_CONFIG 11 -> 12 resets min_command, motor_pwm_rate, motor_pwm_protocol and motor_poles on upgrade; diff all restores them by name.

Forward merge from 10.x

Branched from maintenance-11.x as it is today. Merging maintenance-10.x forward conflicts in mixer.h/mixer.c with the SRXL2 fields (struct tail, reset template): keep both blocks and version 12. The bump makes the order safe; without it, a field at offset 10 would read a 10.x config's SRXL2 defaults (7, 1) as mask 0x0107.

Not done

No hardware test: SITL has no DShot. CI builds all targets. Needs a bench test with props off on BLHeli_32 / AM32 / Bluejay: set a bit, arm, watch the motor; battery after USB; turtle in and out.

Configurator part follows as a step in the Mixer tab motor wizard (#2580) instead of the separate dialog from #2794.

…s 20/21

Replaces the ESC-config approach of iNavFlight#12011. A uint16 bitmask in motorConfig
tells each DShot ESC to spin normal (20) or reversed (21). The command queue
now carries one command per motor so one frame addresses all ESCs at once.

The ESC does not store these commands, so they are re-sent on every arm ahead
of the first throttle frame, when the setting changes, and every two seconds
while disarmed. Turtle mode inverts relative to the configured mask and disarm
restores the mask instead of broadcasting "normal". All-zero mask sends nothing,
so existing setups behave as before.

PG_MOTOR_CONFIG bumped to 12 for the new field.
@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

@Raffi1202
Raffi1202 marked this pull request as draft September 23, 2026 13:52
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

PR Summary by Qodo

Add per-motor DShot spin direction control

✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds a DShot motor-direction bitmask without persisting changes to ESC configuration.
• Reapplies per-motor directions at safe lifecycle points and integrates turtle-mode inversion.
• Supports standard and RP2350 DShot paths with user-facing documentation.
Diagram

sequenceDiagram
    participant Config as Motor Config
    participant Core as FC Core
    participant Mixer as Direction Logic
    participant Driver as DShot Driver
    participant ESC as DShot ESCs
    Config->>Mixer: Direction bitmask
    Core->>Mixer: Arm or periodic poll
    alt Normal operation
        Mixer->>Driver: Per-motor direction frame
    else Turtle mode
        Core->>Mixer: Invert or restore mask
        Mixer->>Driver: Inverted direction frame
    end
    Driver->>ESC: Commands 20 and 21
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Persist direction in ESC configuration
  • ➕ Direction survives ESC power cycles without periodic refresh commands.
  • ➕ No command frames need to replace initial throttle frames during arming.
  • ➖ Requires vendor-specific ESC configuration support.
  • ➖ Writes persistent ESC state and may introduce compatibility or flash-wear concerns.
  • ➖ Conflicts with the stated goal of avoiding ESC configuration changes.
2. Queue separate commands per motor
  • ➕ Keeps each queue entry structurally smaller.
  • ➕ Can reuse a scalar command representation.
  • ➖ Requires independent motor targeting and more queue operations.
  • ➖ May apply directions across different frames rather than synchronously.
  • ➖ Complicates ordering before the first throttle frame.

Recommendation: Keep the PR’s volatile, per-frame DShot approach. It uses standardized commands, avoids modifying ESC configuration, preserves synchronized delivery across motors, and explicitly handles ESC restarts through guarded refreshes. Persistent configuration is less portable, while separate per-motor queue operations weaken synchronization and sequencing.

Files changed (9) +193 / -32

Enhancement (6) +164 / -32
pwm_output.cQueue per-motor DShot command frames +43/-18

Queue per-motor DShot command frames

• Replaces scalar DShot queue entries with frames containing one command per motor and a repeat count. Adds spin-direction frame construction while preserving all-motor command behavior, and prevents command sequencing from being pulled into constrained instruction memory.

src/main/drivers/pwm_output.c

pwm_output.hExpose per-motor DShot direction commands +5/-5

Expose per-motor DShot direction commands

• Moves command execution state into the implementation and declares the new masked spin-direction API. Documents the distinction between broadcast commands and per-motor direction frames.

src/main/drivers/pwm_output.h

pwm_output_rp2350.cSupport per-motor directions on RP2350 +15/-6

Support per-motor directions on RP2350

• Changes the RP2350 pending DShot command from a scalar to a per-motor array. Adds masked direction command generation and includes string support for command-array initialization.

src/main/drivers/pwm_output_rp2350.c

fc_core.cIntegrate direction commands with flight lifecycle +14/-2

Integrate direction commands with flight lifecycle

• Applies configured directions before normal arming and invokes periodic refresh processing from realtime callbacks. Turtle arming now inverts the configured mask, while disarming restores configured directions.

src/main/fc/fc_core.c

mixer.cManage safe DShot direction refreshes +75/-1

Manage safe DShot direction refreshes

• Adds direction-mask calculation, turtle inversion, change detection, and two-second disarmed refreshes while avoiding running motors and beacon guard periods. Preserves silent behavior for untouched all-normal setups and bumps PG_MOTOR_CONFIG from version 11 to 12.

src/main/flight/mixer.c

mixer.hStore and expose DShot direction state +12/-0

Store and expose DShot direction state

• Adds the conditional uint16 motor-direction mask to motorConfig_t. Declares APIs for immediate application and lifecycle-driven refresh processing.

src/main/flight/mixer.h

Documentation (2) +22 / -0
ESC and servo outputs.mdDocument DShot motor direction behavior +12/-0

Document DShot motor direction behavior

• Explains the direction bitmask, supported ESC firmware, refresh behavior, turtle-mode inversion, and the DShot-only limitation. Includes an example reversing motors 1 and 3.

docs/ESC and servo outputs.md

Settings.mdDocument the dshot_reversed_motors setting +10/-0

Document the dshot_reversed_motors setting

• Adds generated-style reference documentation for the new bitmask setting, including its range, default, command semantics, and refresh behavior.

docs/Settings.md

Other (1) +7 / -0
settings.yamlRegister the DShot direction bitmask setting +7/-0

Register the DShot direction bitmask setting

• Defines dshot_reversed_motors as a DShot-conditional setting backed by dshotReversedMotors. Restricts values to the supported 12-motor mask and defaults to zero.

src/main/fc/settings.yaml

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

qodo-free-for-open-source-projects Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Motors keep old direction after clearing 🐞 Bug ≡ Correctness
Description
tryArm() queues the configured directions only when dshotReversedMotors is nonzero, even though
clearing the mask may require command 20 to undo a previously sent reversed state. If the pilot arms
before the 100 ms update poll processes that change, the armed-state guard prevents correction and
affected motors continue using their former direction.
Code

src/main/fc/fc_core.c[R617-618]

+        if (motorConfig()->dshotReversedMotors && !STATE(IN_FLIGHT_EMERG_REARM)) {
+            dshotSpinDirectionApply(false);
Evidence
The arm path ignores a zero mask, while the periodic updater checks only every 100 ms and refuses to
send after motors are considered running. dshotSpinDirectionApply(false) is capable of sending the
required all-normal frame, but this timing window bypasses it.

src/main/fc/fc_core.c[613-623]
src/main/flight/mixer.c[1023-1044]
src/main/flight/mixer.c[1046-1076]
src/main/flight/mixer.c[987-1000]

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

## Issue description
Clearing `dshotReversedMotors` and immediately arming can skip the all-normal direction frame because `tryArm()` checks only whether the new mask is nonzero. Track whether a direction was previously sent or centralize the arm-time decision so a transition from nonzero to zero is applied before throttle output.
## Fix Focus Areas
- src/main/fc/fc_core.c[613-619]
- src/main/flight/mixer.c[1023-1044]
- src/main/flight/mixer.h[149-153]
## Recommended Fix
Add an arm-time helper that sends the configured mask when it is nonzero or differs from the last sent mask, and invoke it for every non-emergency DShot arm. This preserves silence for untouched all-zero configurations while ensuring a newly cleared mask queues command 20 before the first throttle frame.

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


2. Upgrades erase existing motor settings 🐞 Bug ≡ Correctness
Description
Registering PG_MOTOR_CONFIG as version 12 makes pgLoad() reject every stored version-11 record
after first resetting the complete group. Upgrading existing installations therefore defaults the
PWM protocol, rate, minimum command, idle offset, and pole count along with initializing the new
direction field.
Code

src/main/flight/mixer.c[94]

+PG_REGISTER_WITH_RESET_TEMPLATE(motorConfig_t, motorConfig, PG_MOTOR_CONFIG, 12);
Evidence
The new registration version is 12, while pgLoad() resets the destination and copies stored bytes
only when the stored and registered versions match exactly. Because the motor group contains
multiple existing settings, every version-11 installation loses the whole group's values rather than
merely receiving a default for the appended field.

src/main/flight/mixer.c[94-104]
src/main/flight/mixer.h[90-103]
src/main/config/parameter_group.c[86-96]
src/main/config/config_eeprom.c[231-237]

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

## Issue description
The motor parameter-group version bump causes existing version-11 records to be discarded because the loader restores data only on an exact version match. Since the new field is appended, preserve old fields while allowing the new tail field to retain its reset default.
## Fix Focus Areas
- src/main/flight/mixer.c[94-104]
- src/main/config/parameter_group.c[86-94]
## Recommended Fix
Keep the motor group compatible with version 11 when appending `dshotReversedMotors`, relying on the existing reset-before-partial-copy behavior to initialize the new tail field to zero. If a version bump is mandatory, add an explicit version-11 migration that copies every prior field and defaults only the new field.

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



Remediation recommended

3. Full queues retain stale motor direction 🐞 Bug ☼ Reliability
Description
dshotSpinDirectionApply records the requested mask and timestamp even when the void
sendDShotSpinDirection call is discarded by a full circular buffer. After eight frames are queued,
this suppresses the changed-mask retry for two seconds, so affected electronic speed controllers
continue using their previous direction.
Code

src/main/flight/mixer.c[R1041-1043]

+    sendDShotSpinDirection(mask);
+    dshotSpinDirectionSent = mask;
+    dshotSpinDirectionSentAtUs = micros();
Evidence
The circular buffer drops full-queue pushes without returning a status, and the new direction sender
cannot detect that rejection. The lifecycle nevertheless updates its sent state and uses that state
to skip subsequent refreshes.

src/main/common/circular_queue.c[34-42]
src/main/drivers/pwm_output.c[493-500]
src/main/flight/mixer.c[1032-1043]
src/main/flight/mixer.c[1058-1067]

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

## Issue description
The generic DShot queue silently rejects pushes when full, but the direction lifecycle records rejected frames as sent. This can suppress retries and leave motors using stale directions.
## Fix Focus Areas
- src/main/common/circular_queue.c[34-42]
- src/main/drivers/pwm_output.c[493-500]
- src/main/flight/mixer.c[1032-1044]
- src/main/fc/fc_core.c[584-619]
## Recommended Fix
Make direction-frame enqueueing report success and update the sent mask and timestamp only after acceptance. Ensure normal and turtle arming do not complete until their required direction frame has been accepted, while disarmed failures remain eligible for retry.

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


4. Changed motor directions can be delayed 🐞 Bug ≡ Correctness
Description
sendDShotSpinDirection() overwrites pendingCmd[] and resets pendingCmdReps even when a prior
direction frame is still being transmitted. A DShot beacon command can arrive during those ten
direction repeats, replacing the remaining packets on RP2350 boards and leaving the new direction
unapplied until a later refresh or arm.
Code

src/main/drivers/pwm_output_rp2350.c[R350-352]

+        pendingCmd[i] = (reversedMotorMask & (1u << i)) ? DSHOT_CMD_SPIN_DIRECTION_REVERSED : DSHOT_CMD_SPIN_DIRECTION_NORMAL;
+    }
+    pendingCmdReps = 10;
Evidence
The RP2350 output loop transmits pendingCmd[i] while pendingCmdReps is positive and decrements
that count once per motor update. The changed direction sender unconditionally replaces both values,
while the DShot beeper independently calls sendDShotCommand(), so a beacon beginning after a
direction update can replace the outstanding direction repetitions. The standard driver avoids this
loss by storing command frames in its circular queue.

src/main/drivers/pwm_output_rp2350.c[259-280]
src/main/drivers/pwm_output_rp2350.c[341-352]
src/main/io/beeper.c[344-354]
src/main/drivers/pwm_output.c[486-530]

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

## Issue description
The RP2350 DShot implementation has only one mutable pending-command slot. A beacon command can overwrite a newly queued per-motor spin-direction command before its required ten transmissions finish, so the ESC may not receive enough direction packets to apply the setting.
## Fix Focus Areas
- src/main/drivers/pwm_output_rp2350.c[195-197]
- src/main/drivers/pwm_output_rp2350.c[347-352]
## Recommended Fix
Replace the single pending command state with a small FIFO of per-motor command frames, including the per-frame repeat count. Enqueue both `sendDShotCommand()` and `sendDShotSpinDirection()` requests, and only load the next frame after the active frame has completed all repeats.

ⓘ 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 choose which labels appear on a finding, and whether they show icons or text

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/main/fc/fc_core.c Outdated
Comment on lines +617 to +618
if (motorConfig()->dshotReversedMotors && !STATE(IN_FLIGHT_EMERG_REARM)) {
dshotSpinDirectionApply(false);

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. Motors keep old direction after clearing 🐞 Bug ≡ Correctness

tryArm() queues the configured directions only when dshotReversedMotors is nonzero, even though
clearing the mask may require command 20 to undo a previously sent reversed state. If the pilot arms
before the 100 ms update poll processes that change, the armed-state guard prevents correction and
affected motors continue using their former direction.
Agent Prompt
## Issue description
Clearing `dshotReversedMotors` and immediately arming can skip the all-normal direction frame because `tryArm()` checks only whether the new mask is nonzero. Track whether a direction was previously sent or centralize the arm-time decision so a transition from nonzero to zero is applied before throttle output.

## Fix Focus Areas
- src/main/fc/fc_core.c[613-619]
- src/main/flight/mixer.c[1023-1044]
- src/main/flight/mixer.h[149-153]

## Recommended Fix
Add an arm-time helper that sends the configured mask when it is nonzero or differs from the last sent mask, and invoke it for every non-emergency DShot arm. This preserves silence for untouched all-zero configurations while ensuring a newly cleared mask queues command 20 before the first throttle frame.

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

Comment thread src/main/flight/mixer.c
);

PG_REGISTER_WITH_RESET_TEMPLATE(motorConfig_t, motorConfig, PG_MOTOR_CONFIG, 11);
PG_REGISTER_WITH_RESET_TEMPLATE(motorConfig_t, motorConfig, PG_MOTOR_CONFIG, 12);

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

2. Upgrades erase existing motor settings 🐞 Bug ≡ Correctness

Registering PG_MOTOR_CONFIG as version 12 makes pgLoad() reject every stored version-11 record
after first resetting the complete group. Upgrading existing installations therefore defaults the
PWM protocol, rate, minimum command, idle offset, and pole count along with initializing the new
direction field.
Agent Prompt
## Issue description
The motor parameter-group version bump causes existing version-11 records to be discarded because the loader restores data only on an exact version match. Since the new field is appended, preserve old fields while allowing the new tail field to retain its reset default.

## Fix Focus Areas
- src/main/flight/mixer.c[94-104]
- src/main/config/parameter_group.c[86-94]

## Recommended Fix
Keep the motor group compatible with version 11 when appending `dshotReversedMotors`, relying on the existing reset-before-partial-copy behavior to initialize the new tail field to zero. If a version bump is mandatory, add an explicit version-11 migration that copies every prior field and defaults only the new field.

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

Comment on lines +350 to +352
pendingCmd[i] = (reversedMotorMask & (1u << i)) ? DSHOT_CMD_SPIN_DIRECTION_REVERSED : DSHOT_CMD_SPIN_DIRECTION_NORMAL;
}
pendingCmdReps = 10;

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

3. Changed motor directions can be delayed 🐞 Bug ≡ Correctness

sendDShotSpinDirection() overwrites pendingCmd[] and resets pendingCmdReps even when a prior
direction frame is still being transmitted. A DShot beacon command can arrive during those ten
direction repeats, replacing the remaining packets on RP2350 boards and leaving the new direction
unapplied until a later refresh or arm.
Agent Prompt
## Issue description
The RP2350 DShot implementation has only one mutable pending-command slot. A beacon command can overwrite a newly queued per-motor spin-direction command before its required ten transmissions finish, so the ESC may not receive enough direction packets to apply the setting.

## Fix Focus Areas
- src/main/drivers/pwm_output_rp2350.c[195-197]
- src/main/drivers/pwm_output_rp2350.c[347-352]

## Recommended Fix
Replace the single pending command state with a small FIFO of per-motor command frames, including the per-frame repeat count. Enqueue both `sendDShotCommand()` and `sendDShotSpinDirection()` requests, and only load the next frame after the active frame has completed all repeats.

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

@Raffi1202
Raffi1202 marked this pull request as ready for review September 23, 2026 14:28
Comment thread src/main/flight/mixer.c
Comment on lines +1041 to +1043
sendDShotSpinDirection(mask);
dshotSpinDirectionSent = mask;
dshotSpinDirectionSentAtUs = micros();

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

3. Full queues retain stale motor direction 🐞 Bug ☼ Reliability

dshotSpinDirectionApply records the requested mask and timestamp even when the void
sendDShotSpinDirection call is discarded by a full circular buffer. After eight frames are queued,
this suppresses the changed-mask retry for two seconds, so affected electronic speed controllers
continue using their previous direction.
Agent Prompt
## Issue description
The generic DShot queue silently rejects pushes when full, but the direction lifecycle records rejected frames as sent. This can suppress retries and leave motors using stale directions.

## Fix Focus Areas
- src/main/common/circular_queue.c[34-42]
- src/main/drivers/pwm_output.c[493-500]
- src/main/flight/mixer.c[1032-1044]
- src/main/fc/fc_core.c[584-619]

## Recommended Fix
Make direction-frame enqueueing report success and update the sent mask and timestamp only after acceptance. Ensure normal and turtle arming do not complete until their required direction frame has been accepted, while disarmed failures remain eligible for retry.

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 69a8cfb

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

RAM / Flash usage vs. base commit 7e82f68 — commit 6db9c5b

Target Flash Δ RAM Δ
MATEKF405 +600 B (+0.09%) CCM: ±0 B (±0.00%)
RAM: +136 B (+0.12%)
MATEKF722 +400 B (+0.08%) ITCM_RAM: -200 B (-1.61%)
RAM: +120 B (+0.14%)
TCM: ±0 B (±0.00%)
MATEKF765 +644 B (+0.09%) DTCM_RAM: ±0 B (±0.00%)
SRAM1: +136 B (+0.11%)
MATEKH743 +1172 B (+0.15%) D2_RAM: ±0 B (±0.00%)
DTCM_RAM: ±0 B (±0.00%)
ITCM_RAM: -280 B (-1.72%)
RAM: +128 B (+0.09%)

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

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Test firmware build ready — commit 6db9c5b

Download firmware for PR #12022

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

Bluejay and BLHeli_S keep a runtime command 21 through a DShot signal
loss. After clearing the mask and rebooting the FC with the battery
connected, nothing sent command 20 again and the motor stayed reversed.
Every arm except an in-flight rearm now sends the configured directions,
as Betaflight does. Reversible-motor setups with an all-zero mask stay
silent as before.
@Raffi1202

Copy link
Copy Markdown
Contributor Author

Pushed 6db9c5b: every arm now sends the directions, also with an all-zero mask, as Betaflight does. Bluejay and BLHeli_S keep a runtime 21 through an FC reboot, so clearing the mask and rebooting left the motor reversed.

@b14ckyy one design point I'd like your call on.

With motorstop_on_low an armed FC sends DShot 0. If the pilot arms before the ESC has finished its startup tones, the arm-time direction frame is lost (ESCs only take commands once they are armed themselves: AM32, Bluejay). The ESC then arms on DShot 0 and spins in its stored direction; the 2 s refresh is off while armed. Without motor stop this can't happen, idle never arms an ESC.

Options:

  1. While armed with all motors stopped (not turtle, mask ≠ 0), re-send every 250 ms. Leaves a ≤ 250 ms window after the ESC arms; up to ~20 ms throttle delay if throttle comes up during a frame.
  2. Docs only: with motor stop, wait for the ESC startup tones before arming.
  3. Block arming for a few seconds after the battery is detected when the mask is non-zero.

I'd take 1 plus a docs line.

@b14ckyy

b14ckyy commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Thanks, this is the shape we discussed and it reads well. I did a full pass with clean A/B builds on F405, F722SE, F765 and H743 (+336 to +1156 B flash, +120 to +160 B RAM, ITCM -200 to -280 B thanks to the NOINLINE), unit tests pass, and I checked the ESC side against the AM32, Bluejay and BLHeli_S sources: all three accept 20/21 only once the ESC itself is armed at zero throttle, and repeating the frame every 2 s has no side effects on any of them. INAV never sends command 12, so nothing gets written to the ESC.

Design call: option 1 plus the docs line. While armed, with all motors at stop, not in turtle and a non-zero mask, re-send every 250 ms. That leaves at most a 250 ms window after the ESC arms and up to ~20 ms throttle hold-off if the pilot opens throttle during a frame, which is fine. Use the turtle-aware apply there, otherwise an armed turtle would get un-inverted. Please don't use areMotorsStopped(), it only looks at motor 0. I suggest to add to the Docu that this is a con venience feature. Manually changing the Motor direction in the ESC config or swapping a motor wire, is the preferred and more robust way.

PG version: please drop the bump and keep version 11. The new uint16 sits at offset 10, past the old 10-byte record, so pgLoad() leaves it at its default 0. The bump would reset protocol, rate, min_command and poles for everyone upgrading, also on targets without DShot, and it would rule out a later 10.1 backport. For the forward merge from 10.x, place the field after the SRXL2 block (offset 14): a 10.x record then still leaves it at 0. At offset 10 it would read the SRXL2 defaults (7, 1) as mask 0x0107 and reverse motors 1–3. Please put that note in the PR text.

Must fix:

  1. docs/ESC and servo outputs.md line 24 overstates coverage: battery-after-USB is only covered once the ESC has started and the 2 s refresh has run, and an ESC that resets in flight does re-arm on DShot 0 with motor stop. Add that the direction now depends on the FC config: after defaults or a settings reset, check the motor tab.

Should fix:
2. After boot with mask 0, a Bluejay/BLHeli_S that still holds a runtime 21 stays reversed until the first arm: the configurator's set/save/reboot is faster than the 100 ms poll, and after reboot mask == 0 && sent == 0 stays quiet. Initialise dshotSpinDirectionSent to a sentinel (e.g. 0xFFFF) so the first poll after boot sends once; skip that for 3D with mask 0 as in tryArm().
3. RP2350: sendDShotCommand() overwrites the single pending slot. disarm() queues the mask restore and then beeps, and the disarm beacon can wipe the remaining repeats. Drop the beacon while pendingCmdReps > 0, or use a second slot.
4. sendDShotSpinDirection() should return whether the push was accepted, and dshotSpinDirectionApply() should only record mask and time on success. The queue is 8 deep, so it's unreachable in practice, but the bookkeeping shouldn't lie.
5. Mention in the PR text that every non-3D DShot arm now sends 10× command 20 and holds motor output for ~20 ms, also with the default mask 0.

Nits:
6. Comments: pwm_output.c:69-71, pwm_output.h:59-63 and mixer.c:1012-1019 are multi-line WHAT blocks, one line WHY each please. The settings.yaml description should say "while disarmed", not "whenever this setting changes".
7. The queue entry grew from 4 to 13 bytes (+84 B RAM with 12 motors). Storing {uint16_t reversedMask; uint8_t cmd; uint8_t repeats} and deriving the per-motor command in executeDShotCommands() would keep it at 4 bytes.
8. A docs line that 3D/reversible-motor setups should leave the mask at 0: AM32 ignores 20/21 in 3D mode and Betaflight skips them entirely with 3D.

Then a bench test with props off on at least two ESC firmwares (set a bit, arm, battery after USB, turtle in and out), and I'll take it for 11.x.

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.

2 participants