Skip to content

Bidir dshot - testing dont merge - #12027

Draft
error414 wants to merge 36 commits into
iNavFlight:maintenance-10.xfrom
error414:error414/feature/bidir-dshot
Draft

error414 wants to merge 36 commits into
iNavFlight:maintenance-10.xfrom
error414:error414/feature/bidir-dshot

Conversation

@error414

@error414 error414 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

!!!DONT TURN ON BIDIR DSHOT IF YOU USE BLHELI_32 fw VERSION 32.9 AND EARLIER!!!

This PR is based on PR #11605 @halfmanbear thank you very much, and this PR is for easier testing rebased bidir dshot.

  1. normal DSHOT still use circular DMA, and can be used for problematic BLHELI_32 ESC FW
  2. bidir dshot keeps pin hight durring eeprom saving, it's save for all ESC FWs EXCEPT BLHELI_32!!!!

BLHELI_S = no know issue
AM32 = weird issue on discord, must be tested
Bluejay = no know issue
BLHELI_32 = 32.10 hopefully fixed issue
BLHELI_32 = 32.9 and earlier !!!can't properly handle pin hight (no signal)!!! cant be used for bidir dshot
ESCape32 = issue when dshot frames are too close to each other, already fixed in INAV 10, no signal ESCape32 can handle without any problem.

Testing how ESC can handle no 1s no signal

  • ESCape32
  • BLHELI_32 (32.10)
  • BLHELI_S
  • AM32
  • Bluejay

It's not possible to detect what FW ESC uses, so we have to probably add big red warning to configurator.

I decied to keep pin hight instead of set some signal like PWM, multishot, etc... Because then there would be two restarts of ESC. First restart aprox 0.4s after start saving to eeprom, and second aprox 0.4s after finish saving settings to eeprom. ESC would restart first time dshot -> PWM (for example) and then PWM (for example) -> dshot.

If pin is kept hight then there would be only one ESC restart. BLHELI32 issue is that for start spinning was enought only a one correct frame,, which can be caused by interference on signal pin, All other modern ESC FWs needs 6 - 10 valid frames to confirm start spinning!!!

halfmanbear and others added 18 commits May 26, 2026 23:05
Ports bidirectional DShot (GCR) telemetry from Betaflight, enabling
RPM-based gyro filtering without a dedicated ESC telemetry UART.

After each DShot output frame the motor pin is switched to timer input
capture mode via per-channel DMA to receive the ESC's GCR-encoded eRPM
response. The decoded eRPM feeds the existing RPM filter infrastructure.

New settings:
- dshot_bidir_enabled: enable bidirectional DShot telemetry
- dshot_edt_enabled: enable Extended DShot Telemetry (temperature,
  voltage, current in addition to eRPM)

Changes:
- drivers/dshot.c/.h: GCR decode, EDT frame parsing, eRPM-to-RPM
  conversion, motor frequency LPF (ported from Betaflight, GPLv3)
- drivers/pwm_output.c: per-channel DMA direction switching between
  DShot output and GCR input capture on STM32 (StdPeriph/HAL) and AT32
- sensors/esc_sensor.c/.h: DShot bidir data path into ESC sensor
  framework (escSensorSetDshotData, escSensorIsActive)
- flight/rpm_filter.c: DShot telemetry as frequency source alongside
  serial ESC sensor path
- target/common_post.h: USE_RPM_FILTER enabled for any USE_DSHOT target
- fc/fc_init.c: call initDshotTelemetry() at startup; fix RPM filter
  init condition to accept DShot bidir as a valid source
- blackbox/blackbox.c: fix NULL dereference when escSensorGetData()
  returns NULL; fix uninitialized blackboxSlowState_t comparison
- telemetry/srxl.c: replace Betaflight-specific USE_DSHOT_TELEMETRY
  guards with INAV's USE_DSHOT/USE_ESC_SENSOR; use escSensorGetData()

Tested on SPEEDYBEEF405V4 (STM32F405, StdPeriph) with Bluejay ESC
firmware. eRPM telemetry confirmed in blackbox; RPM gyro filter
updating notch frequencies with motor speed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dshotDmaStream() is only called from the USE_HAL_DRIVER code paths, but
was defined unconditionally with a dead fallback stub, so non-HAL F4
targets (e.g. FF_F35_LIGHTNING) built it with -Werror=unused-function.
Scope the definition to USE_HAL_DRIVER to match its call sites.
pwm_output.c called LL_TIM_EnableDMAReq_CCx/LL_TIM_DisableDMAReq_CCx,
which don't exist in ST's LL timer driver (only per-channel CC1..CC4
variants are provided). Add local static inline wrappers that set/clear
the DIER CCxDE bits directly by mask, matching the equivalent helpers
already used internally by timer_impl_hal.c.
…metry settings

dshot_bidir_enabled and dshot_edt_enabled were added to settings.yaml but
docs/Settings.md was never regenerated, failing CI's update_cli_docs.py
check. Ran the generator; purely additive diff, no other drift.
loadDmaBufferDshot() was only defined when USE_DSHOT_DMAR was unset, but
pwmCompleteMotorUpdate() also calls it as the per-channel DMA fallback on
DMAR targets whenever bidir telemetry is enabled (burst DMA can't drive
per-channel direction switching). Define it unconditionally; keep
loadDmaBufferDshotStride() gated behind USE_DSHOT_DMAR since it's only
used by the DMAR-specific burst path.
fix: keep DShot telemetry decode out of ITCM
pwmSetMotorDMACircular() predates bidirectional DSHOT and corrupts its
per-channel direction-switching state machine when a config save runs
(e.g. servo autotrim saving on disarm):

- On USE_DSHOT_DMAR targets the burst path is never armed with bidir, so
  dmaBurstBuffer stays NULL and loadDmaBufferDshotStride() writes the
  zero-throttle frame through the NULL pointer into 0x00000000 - which is
  ITCM RAM holding .tcm_code (FAST_CODE), corrupting the firmware.
- On per-channel targets impl_timerPWMSetDMACircular() re-enables the
  stream without regard to the current transfer direction, and on exit
  disables the TIM DMA request before the stream: a FIFO-enabled
  mem-to-periph stream then cannot drain, EN sticks at 1 and every later
  reconfiguration is silently ignored - the output is dead until reboot.
- Circular output also repeats frames back-to-back, colliding with the
  GCR reply the ESC drives onto the same line after every frame.

Match Betaflight instead: with bidir telemetry active, leave motor DMA
alone during the flash write and accept the frame gap. Saves only happen
disarmed, the ESC holds zero throttle, and the existing decode path
rebuilds the streams (full DeInit/Init) on the first update afterwards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BB55zdRNZq3Q6hZYsDYwTc
…ot-fix

BidirDshot - fix for H7, invertion of timer, reset DMA for reading te…
# Conflicts:
#	src/main/drivers/pwm_output.c
#	src/main/fc/settings.yaml
#	src/main/sensors/esc_sensor.c
Structure:
- dshot.c/h -> bidir_dshot.c/h, compiled only with USE_DSHOT_BIDIR. The
  define comes from common_post.h: USE_DSHOT && STM32H7 && !USE_DSHOT_DMAR
  (direction switching is verified on H7; DMAR has no per-channel stream).
  dshot_bidir_enabled / dshot_edt_enabled are hidden elsewhere.
- Every bidir path in pwm_output.c sits under USE_DSHOT_BIDIR and, at run
  time, under useDshotTelemetry: with the setting off, plain DSHOT keeps the
  timer driver's DMA handler, FIFO setup and channel config from
  maintenance-10.x. The DMAR fallback to per-channel DMA is gone (excluded
  by the define instead).
- USE_RPM_FILTER follows USE_ESC_SENSOR || USE_DSHOT_BIDIR instead of
  USE_DSHOT, so targets without ESC sensor do not grow.
- No circular keep-alive for bidir: the line idles high for the flash
  write, as in Betaflight.

Fixes:
- EDT voltage/current were 10x off (0.25 V and 1 A steps into 0.01 V /
  0.01 A fields): x25 / x100 instead of x250 / x1000.
- ESC sensor RPM taken from the already computed mechanical RPM instead
  of computeRpm((int16_t)eRPM/100); an EDT-only first frame no longer
  publishes rpm = 0.
- GCR decoder: guard against len < 1 (shift by -1) on glitch edges and on
  an exactly filled frame, and against len > 21.
- Bidir ESC sensor data is aged at the serial poll rate, so a silent ESC
  drops out of the combined values like a serial one.
- useDshotTelemetry is set once, by pwmMotorPreconfigure(); the ESC
  sensor asks isDshotTelemetryActive() instead of re-deriving it.
- pwmDshotDmaIrqHandler keeps tch->dmaState in step with the timer
  driver's handler (timerPWMDMAInProgress()).
- srxl.c: RPM comes through escSensorGetData(), no dshot include needed;
  duplicate includes removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@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

Add bidirectional DShot telemetry and RPM filtering

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

Grey Divider

AI Description

• Captures bidirectional DShot eRPM and extended telemetry without a dedicated UART.
• Feeds decoded motor frequency into ESC sensors, RPM filters, and SRXL telemetry.
• Preserves normal DShot keep-alives while safely handling bidirectional DMA turnaround.
Diagram

sequenceDiagram
    participant FC as Flight Loop
    participant PWM as DShot Driver
    participant DMA as Timer DMA
    participant ESC as ESC
    participant DEC as GCR Decoder
    participant SENSOR as ESC Sensor
    participant RPM as RPM Filter
    FC->>PWM: Submit motor frame
    PWM->>DMA: Start output DMA
    DMA->>ESC: Send DShot frame
    PWM->>DMA: Switch to capture
    ESC-->>DMA: Return GCR edges
    DMA->>DEC: Provide edge timings
    DEC->>SENSOR: Publish ESC data
    RPM->>DEC: Read motor frequency
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Dedicated ESC telemetry UART
  • ➕ Uses the existing serial ESC telemetry path
  • ➕ Avoids per-frame timer and DMA direction switching
  • ➕ Has lower sensitivity to motor-pin electrical behavior
  • ➖ Consumes a UART and requires additional wiring
  • ➖ Does not meet the goal of telemetry over the motor signal
  • ➖ May provide telemetry at a lower update rate
2. Validated-platform rollout
  • ➕ Limits initial hardware risk to verified MCU and ESC combinations
  • ➕ Simplifies diagnosis of platform-specific DMA or timer failures
  • ➕ Allows compatibility evidence to accumulate before broader enablement
  • ➖ Reduces initial target coverage
  • ➖ Requires explicit target-family feature gating
  • ➖ Delays availability for potentially compatible boards

Recommendation: The per-channel DMA turnaround approach is appropriate for obtaining high-rate eRPM without extra wiring and reuses existing RPM-filter and ESC-sensor consumers effectively. Before broad release, gate the feature to validated MCU families where necessary and add a prominent configurator warning for incompatible BLHeli_32 firmware; normal DShot should retain its existing circular-DMA behavior.

Files changed (15) +1052 / -39

Enhancement (7) +988 / -15
bidir_dshot.cDecode DShot eRPM and extended telemetry +229/-0

Decode DShot eRPM and extended telemetry

• Implements GCR telemetry state management, eRPM conversion, per-motor frequency filtering, and EDT decoding. Exposes RPM and ESC sensor values for downstream consumers.

src/main/drivers/bidir_dshot.c

bidir_dshot.hDefine the bidirectional DShot telemetry API +89/-0

Define the bidirectional DShot telemetry API

• Introduces telemetry types, packet status constants, per-motor state structures, counters, and accessors used by the motor, sensor, and filter subsystems.

src/main/drivers/bidir_dshot.h

pwm_output.cAdd per-channel DShot transmit and capture turnaround +560/-4

Add per-channel DShot transmit and capture turnaround

• Switches motor timer channels and DMA streams between DShot output and GCR edge capture for HAL, StdPeriph, and AT32 platforms. It also handles inverted signaling, startup pin parking, telemetry timing, flash-write behavior, packet checksums, and deferred decoding while preserving normal DShot circular DMA.

src/main/drivers/pwm_output.c

fc_init.cInitialize DShot telemetry and accept it as an RPM source +9/-1

Initialize DShot telemetry and accept it as an RPM source

• Initializes bidirectional telemetry with the flight loop period. RPM filtering now starts when either serial ESC telemetry or bidirectional DShot is active.

src/main/fc/fc_init.c

rpm_filter.cUse DShot motor frequency for RPM filtering +20/-6

Use DShot motor frequency for RPM filtering

• Selects filtered bidirectional DShot frequency when active and retains serial ESC RPM as the fallback. Serial-specific frequency filters are now compiled only with ESC sensor support.

src/main/flight/rpm_filter.c

esc_sensor.cIntegrate DShot data into the ESC sensor framework +78/-4

Integrate DShot data into the ESC sensor framework

• Adds initialization, publication, activity detection, aggregation, and aging for motor-line DShot telemetry. Stub implementations preserve builds without serial ESC sensor support.

src/main/sensors/esc_sensor.c

esc_sensor.hExpose DShot ESC sensor integration functions +3/-0

Expose DShot ESC sensor integration functions

• Declares APIs for initializing ESC data, publishing decoded DShot measurements, and querying whether any ESC telemetry source is active.

src/main/sensors/esc_sensor.h

Bug fix (1) +5 / -3
blackbox.cGuard unavailable ESC sensor data in blackbox frames +5/-3

Guard unavailable ESC sensor data in blackbox frames

• Avoids dereferencing a null ESC sensor result and initializes temporary slow-state data before comparison. This prevents invalid telemetry sources from causing crashes or undefined frame contents.

src/main/blackbox/blackbox.c

Refactor (1) +2 / -19
srxl.cSource SRXL RPM from the unified ESC sensor API +2/-19

Source SRXL RPM from the unified ESC sensor API

• Removes obsolete split telemetry paths and obtains combined RPM through escSensorGetData, allowing serial and bidirectional DShot sources to share SRXL reporting.

src/main/telemetry/srxl.c

Documentation (1) +20 / -0
Settings.mdDocument bidirectional and extended DShot settings +20/-0

Document bidirectional and extended DShot settings

• Documents the switches for motor-line bidirectional telemetry and Extended DShot Telemetry decoding, including defaults and valid values.

docs/Settings.md

Other (5) +37 / -2
CMakeLists.txtCompile the bidirectional DShot telemetry module +2/-0

Compile the bidirectional DShot telemetry module

• Adds the new bidirectional DShot source and header files to the common firmware build.

src/main/CMakeLists.txt

settings.yamlExpose bidirectional DShot configuration settings +12/-0

Expose bidirectional DShot configuration settings

• Adds disabled-by-default settings for bidirectional DShot and extended telemetry decoding when the target supports the feature.

src/main/fc/settings.yaml

mixer.cRegister new motor telemetry configuration fields +3/-1

Register new motor telemetry configuration fields

• Advances the motor configuration parameter-group version and initializes bidirectional DShot and EDT options to disabled.

src/main/flight/mixer.c

mixer.hStore DShot telemetry options in motor configuration +2/-0

Store DShot telemetry options in motor configuration

• Extends the persistent motor configuration structure with bidirectional telemetry and EDT enable flags.

src/main/flight/mixer.h

common_post.hDerive bidirectional DShot and RPM filter features +18/-1

Derive bidirectional DShot and RPM filter features

• Enables bidirectional DShot for non-DMAR DShot targets, enforces incompatible feature combinations, and enables RPM filtering for either supported telemetry source.

src/main/target/common_post.h

@error414 error414 changed the title Bidir dshot - testing Bidir dshot - testing dont merge Sep 24, 2026
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. SRXL2 telemetry disappears from consumers ✓ Resolved
Description
escSensorIsActive() recognizes only an opened serial ESC port or bidirectional DShot, while SRXL2
initialization enables ESC_SENSOR_ENABLED and populates ESC data without setting either condition.
With SRXL2 telemetry enabled, the guard in escSensorGetData() returns NULL, also withholding
valid data from RPM filtering, Blackbox, OSD, Spektrum/SRXL telemetry, current estimation, and other
shared consumers.
Code

src/main/sensors/esc_sensor.c[R188-190]

+bool escSensorIsActive(void)
+{
+    return escSensorPort || escSensorDshotActive;
Evidence
SRXL2 initialization enables the sensor state and succeeds without setting either escSensorPort or
escSensorDshotActive, even though its update path populates escSensorData. Because
escSensorIsActive() tests only those two fields, the new guard in escSensorGetData() rejects
SRXL2 data before it can be combined or consumed; FC initialization also uses the same predicate for
RPM-filter initialization, while Blackbox and SRXL telemetry now consume the guarded accessor.

src/main/sensors/esc_sensor.c[188-210]
src/main/sensors/esc_sensor.c[273-297]
src/main/sensors/esc_sensor.c[338-386]
src/main/blackbox/blackbox.c[1571-1576]
src/main/telemetry/srxl.c[186-196]
src/main/sensors/esc_sensor.c[188-190]
src/main/sensors/esc_sensor.c[209-211]
src/main/sensors/esc_sensor.c[273-288]
src/main/fc/fc_init.c[801-811]
src/main/sensors/esc_sensor.c[339-381]

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

## Issue description
`escSensorGetData()` uses `escSensorIsActive()` as a prerequisite, but that predicate recognizes only an opened serial ESC port or bidirectional DShot. The SRXL2 initialization path enables `ESC_SENSOR_ENABLED` and supplies valid ESC telemetry without setting either condition, so the accessor suppresses that data and the same predicate can prevent RPM-filter initialization.
## Fix Focus Areas
- src/main/sensors/esc_sensor.c[188-211]
- src/main/sensors/esc_sensor.c[273-297]
## Recommended Fix
Represent enabled SRXL2 telemetry as an active ESC telemetry source and include it in `escSensorIsActive()`, either by tracking SRXL2 activity explicitly or by recognizing its enabled configuration. Preserve the existing serial and bidirectional DShot checks and keep the null guard, while ensuring every successful initialization path that supplies ESC data satisfies the activity predicate.

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


2. AT32 motors never receive usable telemetry ✓ Resolved
Description
The new target rule enables bidirectional DShot for the AT32 target, but its input-capture branch
does not enlarge the timer auto-reload value before recording GCR edges. Each capture timestamp
consequently wraps at the DShot bit period while the decoder subtracts consecutive timestamps across
a 21-bit telemetry frame.
Code

src/main/target/common_post.h[R197-199]

+#if defined(USE_DSHOT) && !defined(USE_DSHOT_DMAR)
+#define USE_DSHOT_BIDIR
+#endif
Evidence
The PR automatically defines bidirectional DShot for all non-DMAR DShot targets, including
IFLIGHT_BLITZ_ATF435. In the HAL implementation the timer is explicitly widened to 0xffff because
telemetry spans about 21 bits, but the AT32 branch arms input capture directly; the decoder then
derives run lengths by subtracting captured timestamps.

src/main/target/common_post.h[188-201]
src/main/target/IFLIGHT_BLITZ_ATF435/target.h[161-164]
src/main/drivers/pwm_output.c[518-545]
src/main/drivers/pwm_output.c[722-726]
src/main/drivers/pwm_output.c[755-768]

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

The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
Issue description
The common target rule enables bidirectional DShot on AT32 targets that use ordinary DShot. The AT32 input-capture implementation records GCR edge timestamps without switching its timer from the DShot bit-period auto-reload to a free-running period, so frame-spanning timestamp differences wrap and cannot be decoded correctly.
Fix Focus Areas
- src/main/target/common_post.h[197-201]
- src/main/drivers/pwm_output.c[755-768]
- src/main/drivers/pwm_output.c[722-726]
Recommended Fix
Either limit `USE_DSHOT_BIDIR` to platforms with a verified capture implementation, or configure the AT32 timer's auto-reload to a sufficiently large free-running value before enabling input capture and restore the DShot period when returning to output mode.

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



Remediation recommended

3. Bidirectional motor updates run slower ✓ Resolved
Description
dshotTelemetryDeadtimeUs adds a complete 16-bit output-frame duration even though
telemetryInputStampUs is recorded only after output DMA has finished. The decode gate therefore
delays each subsequent frame unnecessarily, reducing the approximate DShot 150, 300, and 600 rates
to 3.82, 6.74, and 10.91 kHz instead of their configured 4, 8, and 16 kHz rates.
Code

src/main/drivers/pwm_output.c[R973-974]

+            port->dshotTelemetryDeadtimeUs = DSHOT_TELEMETRY_DEADTIME_US + 1000000 * (16 * DSHOT_MOTOR_BITLENGTH) / dshotHz;
+            pwmDshotSetDirectionOutput(port);
Evidence
The DMA completion handler switches the port to input before telemetryInputStampUs is recorded,
proving that the outgoing frame has already completed. The later check waits for a value calculated
as 35 microseconds plus another 16-bit frame, while the normal update scheduler requests 4, 8, or 16
kHz operation.

src/main/drivers/pwm_output.c[826-853]
src/main/drivers/pwm_output.c[791-792]
src/main/drivers/pwm_output.c[874-886]
src/main/drivers/pwm_output.c[969-975]
src/main/drivers/pwm_output.c[1260-1280]

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 telemetry timestamp is captured after output DMA completion, but the configured wait includes the output-frame duration again. This unnecessarily throttles every bidirectional DShot protocol.
## Fix Focus Areas
- src/main/drivers/pwm_output.c[791-792]
- src/main/drivers/pwm_output.c[874-875]
- src/main/drivers/pwm_output.c[969-975]
## Recommended Fix
Either timestamp the beginning of the outgoing frame and retain a frame-inclusive deadline, or keep the current post-DMA timestamp and configure only the required ESC response/capture interval after output completion.

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


4. Lost telemetry leaves stale RPM filters 🐞 Bug ☼ Reliability
Description
dshotProcessPacket() returns on invalid or edge-less responses without clearing or aging that
motor's stored RPM, filtered frequency, or received-telemetry flag. Once an ESC has supplied one
valid response, a later telemetry outage leaves the RPM filter and reported DShot state using its
last speed indefinitely.
Code

src/main/drivers/bidir_dshot.c[R144-148]

+    if (rawValue == DSHOT_TELEMETRY_INVALID || rawValue == DSHOT_TELEMETRY_NOEDGE) {
+        if (rawValue == DSHOT_TELEMETRY_INVALID) {
+            dshotTelemetryState.invalidPacketCount++;
+        }
+        return rawValue;
Evidence
The invalid-response branch exits before modifying any per-motor values, while valid eRPM
permanently sets the type flag and stored frequency. rpmFilterUpdateTask() selects
getMotorFrequencyHz() solely from the globally configured DShot state and performs no freshness
check.

src/main/drivers/bidir_dshot.c[138-172]
src/main/drivers/bidir_dshot.c[174-183]
src/main/drivers/bidir_dshot.c[203-205]
src/main/flight/rpm_filter.c[194-203]

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

## Issue description
Invalid and missing DShot responses retain the previous per-motor RPM and frequency forever. The RPM filter then continues tracking a speed that is no longer supported by telemetry.
## Fix Focus Areas
- src/main/drivers/bidir_dshot.c[138-172]
- src/main/drivers/bidir_dshot.c[203-205]
- src/main/flight/rpm_filter.c[194-203]
## Recommended Fix
Maintain per-motor telemetry age or consecutive-miss state and invalidate stale eRPM data after a bounded timeout. Clear or safely decay the corresponding RPM-filter frequency and telemetry-presence flag when that timeout expires, resetting age whenever valid eRPM arrives.

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


5. RP2350 accepts telemetry it cannot use ✓ Resolved
Description
common_post.h defines USE_DSHOT_BIDIR for every non-DMAR DShot target, including RP2350 even
though its separate PIO motor-output implementation only transmits non-bidirectional frames. On
RP2350 the bidirectional setting and RPM-filter source are exposed and accepted, but the driver
provides no line turnaround, input capture, or telemetry decoding and never enables
useDshotTelemetry, so neither eRPM nor extended telemetry can result.
Code

src/main/target/common_post.h[R197-199]

+#if defined(USE_DSHOT) && !defined(USE_DSHOT_DMAR)
+#define USE_DSHOT_BIDIR
+#endif
Evidence
RP2350_PICO defines USE_DSHOT without USE_DSHOT_DMAR, so it satisfies the generic condition that
defines USE_DSHOT_BIDIR. Its build selects pwm_output_rp2350.c, whose encoding is explicitly
documented as non-bidirectional and whose PIO-oriented implementation only records the
telemetry-request bit and transmits frames; because it has no receive, capture, or turnaround path
and never sets the global telemetry-active value, initialization leaves telemetry inactive.

src/main/target/common_post.h[188-207]
src/main/target/RP2350_PICO/target.h[69-69]
src/main/drivers/pwm_output_rp2350.c[244-275]
src/main/drivers/pwm_output_rp2350.c[323-351]
src/main/drivers/bidir_dshot.c[109-129]
src/main/target/common_post.h[194-205]
src/main/target/RP2350_PICO/target.h[67-74]
cmake/rp2350.cmake[39-54]
src/main/drivers/pwm_output_rp2350.c[66-78]
src/main/fc/settings.yaml[899-910]

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 common feature condition enables bidirectional-DShot settings and RPM-filter source support on RP2350, but that platform uses a separate PIO motor-output driver that only transmits non-bidirectional frames. Users can therefore configure telemetry functionality that has no receive or decoding path and remains inactive.
## Fix Focus Areas
- src/main/target/common_post.h[188-205]
- src/main/target/RP2350_PICO/target.h[67-74]
- src/main/drivers/pwm_output_rp2350.c[66-78]
- src/main/drivers/pwm_output_rp2350.c[244-275]
## Recommended Fix
Define `USE_DSHOT_BIDIR` only for platforms whose motor driver implements bidirectional line turnaround, edge capture, and telemetry decoding. Exclude RP2350, keep the setting and its RPM-filter source hidden there until its PIO driver gains that receive flow, or implement equivalent bidirectional support in the PIO driver and explicitly advertise it.

ⓘ 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 show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/main/sensors/esc_sensor.c Outdated
Comment thread src/main/drivers/pwm_output.c Outdated
Comment thread src/main/drivers/bidir_dshot.c Outdated
Comment thread src/main/target/common_post.h Outdated
Comment thread src/main/target/common_post.h Outdated
@error414
error414 marked this pull request as draft September 24, 2026 19:10
@sensei-hacker

Copy link
Copy Markdown
Member

Driving the pin high while the ESC has it connected to ground to signal a bit sounds like a dead short, isn't it?
"Infinite" current through the pin, blowing it permanently?

Or do you mean tristating the pin and engaging the pull-up?

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

RAM / Flash usage vs. base commit e87050f — commit 776761b

Using the nearest available size baseline — the PR's exact base commit has no stored baseline yet.

Target Flash Δ RAM Δ
MATEKF405 +2676 B (+0.37%) CCM: -4 B (-0.01%)
RAM: +444 B (+0.43%)
MATEKF722 +344 B (+0.07%) ITCM_RAM: -184 B (-1.47%)
RAM: +104 B (+0.12%)
TCM: ±0 B (±0.00%)
MATEKF765 +660 B (+0.09%) DTCM_RAM: ±0 B (±0.00%)
SRAM1: +104 B (+0.08%)
MATEKH743 +3164 B (+0.40%) D2_RAM: +320 B (+9.80%)
DTCM_RAM: ±0 B (±0.00%)
ITCM_RAM: +600 B (+3.68%)
RAM: +384 B (+0.27%)

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

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Test firmware build ready — commit 776761b

Download firmware for PR #12027

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

@sensei-hacker sensei-hacker added this to the 10.1 milestone Oct 3, 2026
@MrScothh

MrScothh commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

@error414 I tested bidirectional DShot on an F722 (Flywoo F722 Pro V2, stack ESC, four motors, no props) with #11605's head (e6dcaf2, your fixes included), and one motor never reported a correct RPM. I traced it to the timing of the switch to input capture, which #12027 does the same way, plus a flag-clearing bug in the F4 path of both PRs. Numbers below; I hope they help.

The last motor served is armed too late

I added counters to a bench build: per motor, replies decoded or failed, the edge intervals of the last good and bad reply, and cycle-counter stamps of the end-of-frame DMA interrupt and of the capture being armed. All four motors stopped, DShot300, 3 s:

motor DMA interrupt capture armed replies decoded
1 DMA1 S2 61.1 us 67.6 us 100%
2 DMA1 S4 70.0 us 76.9 us 100%
4 DMA1 S5 78.7 us 85.5 us 84.5%
3 DMA1 S6 85.9 us 92.2 us 0%

Times are from the start of the frames. All four frames end together, but the DMA interrupts are served one after another in stream order, and each pwmDshotSetDirectionInput() takes about 7 us here (HAL TIM IC config and start, DMA DeInit and Init). The ESC starts replying about 24 us after the end of the frame, so the third and fourth motors miss the start of the reply. Motor 3's bad replies are its good ones without the first two edges; the decoder pads the missing bits at the end, the bit count passes and the checksum fails. Its RPM then shows the last value that happened to pass the 4-bit checksum, frozen.

Raising one motor's DMA interrupt priority moves the failure: with motor 3 first, motor 3 decodes 100% and motor 4, now last, 0%. With a busy wait added before each switch, the capture has to be armed within about 85 us of the frames' start on this board, which leaves about 6 us per motor for four motors.

A switch written straight to the registers (same DMA and capture settings, no HAL calls, no DMA DeInit/Init) takes about 2.5 us. With it all four motors decode 100%, stopped and spinning at 1080, 1250 and 1400, and motor 3 reads 2,557 / 5,157 / 8,914 RPM at 1080 / 1150 / 1250, like the others.

m1 m2 m3 m4
#11605's switch, all spinning at 1080 100% 100% 0% 86.7%
register switch, all spinning at 1080 100% 100% 100% 100%

Betaflight's timer path (dshot_bitbang = OFF) does the same per-motor switch but keeps it light: handler and switch in FAST_CODE, LL init structs built once, micros() once per cycle. By default it doesn't use that path at all on F7 and H7, nor on F4 with bidir: bitbang samples all the motor pins of a GPIO port with one DMA.

On H7 #12027 is fine. On a Kakute H7 Mini with your head (4a7d4e2) and the same counters, a switch takes about 2.8 us at 480 MHz, the fourth motor is armed at 72 us, and all four decode 100%: stopped, each alone and all together at 1080, 1250 and 1400, RPM in line on all four. #12027 leaves F7 out, but F4 also switches motor by motor in the DMA interrupts, on a slower core, so it may hit the same limit as the F722. I have no F4 board to check.

Related to the dataAge point raised on #11605: a motor whose replies stop decoding keeps its last RPM, and rpmFilterUpdateTask() reads it through getMotorFrequencyHz(), so its notches stay on a frozen frequency.

DMA_CLEAR_FLAG with several flags (F4 path)

DMA_CLEAR_FLAG(port->tch->dma, DMA_IT_TCIF | DMA_IT_HTIF | DMA_IT_TEIF);

The macro expands to flag << shift with no brackets around flag, so only DMA_IT_TEIF is shifted to this stream; TCIF and HTIF are cleared at stream 0's or stream 4's position, which may be another motor's stream. My register switch first had the same call: the snapshot taken at the decode showed the stream still off (EN = 0, NDTR 22) on every motor except the one on DMA1 stream 4, because a stream ignores EN while its old flags are set. With brackets around the flags all four worked. That was on F7; the F4 DMA controller is the same and the F4 path of #11605 (pwm_output.c:671) and #12027 (pwm_output.c:820) makes this call before DMA_Cmd(ENABLE), so on F4 I'd expect only the motors on streams 0 and 4 to report. Does your F4 test show RPM on all four motors?

The register switch I tested (F7, inside the HAL branch of pwmDshotSetDirectionInput)
DMA_Stream_TypeDef *st = port->tch->dma->ref;
TIM_TypeDef *tim = port->tch->timHw->tim;
const unsigned c = port->tch->timHw->channelIndex;
st->CR &= ~DMA_SxCR_EN;
while (st->CR & DMA_SxCR_EN) { }
DMA_CLEAR_FLAG(port->tch->dma, (DMA_IT_TCIF | DMA_IT_HTIF | DMA_IT_TEIF | DMA_IT_DMEIF | DMA_IT_FEIF));
SET_BIT(tim->CR1, TIM_CR1_ARPE);
tim->ARR = 0xffff;
tim->CCER &= ~(TIM_CCER_CC1E << (4 * c));
volatile uint32_t *ccmr = (c < 2) ? &tim->CCMR1 : &tim->CCMR2;
const unsigned sh = 8 * (c & 1);
// input capture on TIx, no prescaler, filter 2
*ccmr = (*ccmr & ~((0xFFU << sh) | (TIM_CCMR1_OC1M_3 << sh))) | (0x21U << sh);
tim->CCER |= (TIM_CCER_CC1P | TIM_CCER_CC1NP | TIM_CCER_CC1E) << (4 * c);
st->NDTR = GCR_TELEMETRY_INPUT_LEN;
st->PAR = (uint32_t)port->ccr;
st->M0AR = (uint32_t)port->dmaBuffer;
st->FCR = DMA_SxFCR_DMDIS;
st->CR = (DMATAG_GET_CHANNEL(port->tch->timHw->dmaTag) << DMA_SxCR_CHSEL_Pos) | DMA_SxCR_MINC | DMA_SxCR_PSIZE_1 | DMA_SxCR_MSIZE_1 | DMA_SxCR_PL_1 | DMA_SxCR_TCIE;
st->CR |= DMA_SxCR_EN;
tim->DIER |= TIM_DIER_CC1DE << c;

Measured on this F722 and on the Kakute H7 Mini; not tested on F4 or AT32. I can share the bench build and the scripts if that helps.

@error414

error414 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

this PR contains all changes. It's weird because DMA_CLEAR_FLAG is already fixed in this PR

https://github.com/error414/inav/blob/4a7d4e2d45d84834d99653edde40a103000171b7/src/main/drivers/dma.h#L103

@error414

error414 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

I tested this PR on KakuteF4Aio. Bluejay FW. It works. Dshot300

image

@MrScothh

MrScothh commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Same on a second F7, a Diatone Mamba F722 (MAMBAF722_2022B: motors on TIM8 and TIM1, DMA2 streams), same bench build on #11605's head. The motor whose DMA interrupt is served last decodes 0.1% of its replies stopped and 62 / 69 / 96% spinning at 1080 / 1250 / 1400, its capture armed at 97 us; with the register switch all four decode 100%, the last one armed at 76.5 us.

@error414

error414 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Please try to this PR #12027, here are many fixes, this branch https://github.com/error414/inav/tree/error414/feature/bidir-dshot

@MrScothh

MrScothh commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

@error414 You're right about DMA_CLEAR_FLAG, sorry: #12027's dma.h has the brackets (61637a5); I checked the call but read the macro from maintenance-10.x. So that point only applies to #11605's head, and your KakuteF4 result fits.

I've now run #12027 itself (4a7d4e2) on the same Flywoo F722, with !defined(STM32F7) taken out of common_post.h for the test (FLYWOOF722PRO fits, flash 99.06%). Your switch is faster than #11605's, about 5 us per motor instead of 7, so the third motor now gets its replies, but the last one still misses the start:

motor capture armed decoded: stopped / 1080 / 1250 / 1400
1 67 us 100%
2 75 us 100%
4 81-82 us 100%
3 88 us 0.2 / 0.4 / 0.9 / 0.0%

Your link counter in MSP2_INAV_ESC_RPM shows it too: motor 3 at 0% decoded of 3,810 replies in the last window, the others at 100%. On this F722 the switch would have to fit in about 6 us per motor, interrupt included, for four motors to make it.

On H7 four motors are fine, but eight reach the same edge. On the Kakute H7 Mini with an 8-motor mixer (ESC on outputs 1-4, outputs 5-8 raised in DMA interrupt priority so that 1-4 are served last), each switch takes about 3 us and the eighth capture is armed at about 85 us:

position in the queue capture armed decoded: stopped / 1080 / 1250 / 1400
7th 82 us 100%
8th 85 us 70 / 73 / 83 / 3%

@error414

error414 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for testing.

I tested it on h7 with 4 motors and on f4 with 4 motors (even I flew my quad with activated dshot).

I did not measured how fast switching is. Probably on f4 is fast enought because f4 does not use HAL.

The case when CPU does not relase pin fast enought is not good at all, it makes short circuit when ESC starts send telemetry back.

I wil check it.

@error414

error414 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

I did fast check before work. You findings is correct, H7 fast enought but it can barely handle 8 motors. I tested 8 motors on MatekH7WLITE, it works, but when I added code for measure it started failing.

F4 uses SPL and by static analysis it can take 0.5- 1.5us, what is enought to drive 8 motors (8. 1.5 = 12us.
I measured H7, time between Irq handlers was 3us, it's worse 3 * 8 = 24us

windows for recofigure all ports is aprox 25 - 35us (depend on ESC), so we can assume 20us as save value.

For plan is:

  1. don use HAL
  2. move pwmDshotSetDirectionInput handler to fast ram (it will be problem because on F7 is not enought space for that)
  3. create struct out of function and just use then
  4. do only minimal settings in pwmDshotSetDirectionInput

@error414

error414 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

I did some test now and seems it works better, could you plase confirm my findings? Have no idea if it will be possible to compile for it for F722, I had to add code to ITCM memory. My OmnibusNXTF7 has plenty of ITCM space, interesting.

1. INAV, Omnibus NXT F7 (F722 @ 216 MHz), 4 motors, DSHOT600

motor spacing to previous motor after frame end
2 – 3.7 µs
4 1.7 µs 5.4 µs
3 1.6 µs 7.0 µs
1 1.4 µs 8.4 µs

2. Betaflight master (4fc1520) timer path, same Omnibus NXT F7, target OMNINXT7, dshot_bitbang = OFF, dshot_burst = OFF, 4 motors, DSHOT300 (same two stamps added to pwm_output_dshot_hal.c; the switch body alone, irqAt → changeDirectionCompletedAt, is 417 cycles = 1.9 µs)

motor spacing to previous motor after frame end
4 – 6.4 µs
2 4.2 µs 10.6 µs
1 2.4 µs 13.0 µs
3 2.5 µs 15.5 µs

3. INAV, MATEKH743 (H743 @ 480 MHz), 4 motors, DSHOT600

motor spacing to previous motor after frame end
1 – 3.4 µs
2 1.2 µs 4.6 µs
3 1.3 µs 5.9 µs
4 1.3 µs 7.2 µs

4. INAV, KakuteF4 (F405 @ 168 MHz), 4 motors, DSHOT600

motor spacing to previous motor after frame end
2 – 4.2 µs
4 1.7 µs 5.9 µs
3 1.8 µs 7.7 µs
1 1.7 µs 9.4 µs

@MrScothh

MrScothh commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

@error414 Confirmed on two F722 boards and an H7: with 776761b every motor decodes all its replies, stopped and spinning.

For the test I enabled bidir on F7 (!defined(STM32F7) taken out of common_post.h). DSHOT300, four motors without props, disarmed motor test; your link counter in MSP2_INAV_ESC_RPM read every 0.2 s, 4 s per step, two runs per board:

board motor served last, before 776761b, every motor
Flywoo F722 Pro V2 (FLYWOOF722PRO) 0.2 / 0.4 / 0.9 / 0.0% decoded stopped / 1080 / 1250 / 1400 (4a7d4e2) 100% at every step
Diatone Mamba F722 (MAMBAF722_2022B) 0.1% stopped, 62-96% spinning (#11605's head) 100% at every step

The Mamba's target defines USE_DSHOT_DMAR, and this PR turns bidir off there; for the test I commented that define out, so it ran on per-channel DMA, as #11605's fallback does. As it stands, DMAR targets get no bidir.

F722 builds with F7 enabled: FLYWOOF722PRO 98.63% flash, MAMBAF722_2022B 98.54% (98.91% without DMAR), MATEKF722SE 99.03%, FLASHHOBBYF722 99.16%. KAKUTEF7 and KAKUTEF7HDV overflow ITCM_RAM, by 488 and 520 B: they were already close to full there. My #12118 (afatfs_poll out of ITCM on F7) frees about 950 B of ITCM on exactly those two targets, so you could put it as a dependency of this PR for the space you need.

On H7 (Kakute H7 Mini, ESC on outputs 1-4): 100% on all four with a four-motor mixer, and still 100% on motors 1-4 with an eight-motor mixer (outputs 5-8 active without an ESC), two runs. I can't place motors 1-4 last in the interrupt queue without my bench counters, which no longer apply to this code, so that doesn't prove the eighth place is fixed.

Not tested with this commit: an eighth motor with its own ESC, F4, and anything armed or flown.

@error414

Copy link
Copy Markdown
Contributor Author

other guy here tested it https://www.rc-network.de/threads/inav-10-bidirektionales-dshot.12139268/#post-13344498

don't know why he didn't wrote here.

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.

5 participants