Skip to content

Add bidirectional DShot ESC telemetry - #11605

Open
halfmanbear wants to merge 8 commits into
iNavFlight:maintenance-10.xfrom
halfmanbear:master
Open

Add bidirectional DShot ESC telemetry#11605
halfmanbear wants to merge 8 commits into
iNavFlight:maintenance-10.xfrom
halfmanbear:master

Conversation

@halfmanbear

Copy link
Copy Markdown

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;

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>
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@github-actions

Copy link
Copy Markdown

Branch Targeting Suggestion

You've targeted the master branch with this PR. Please consider if a version branch might be more appropriate:

  • maintenance-9.x - If your change is backward-compatible and won't create compatibility issues between INAV firmware and Configurator 9.x versions. This will allow your PR to be included in the next 9.x release.

  • maintenance-10.x - If your change introduces compatibility requirements between firmware and configurator that would break 9.x compatibility. This is for PRs which will be included in INAV 10.x

If master is the correct target for this change, no action is needed.


This is an automated suggestion to help route contributions to the appropriate branch.

@Jetrell

Jetrell commented May 30, 2026

Copy link
Copy Markdown

Have you had a chance to look at log data. To compare the latency improvement bdshot dynamic notch has over the matrix filter ? Or even over ESC telemetry RPM, to be sure it's working correctly.

@halfmanbear

Copy link
Copy Markdown
Author

Have you had a chance to look at log data. To compare the latency improvement bdshot dynamic notch has over the matrix filter ? Or even over ESC telemetry RPM, to be sure it's working correctly.

I can't compare with any ESC UART telemetry as my stack doesn't support this (see below). One of the main reasons for implementing bidirectional DSHOT.

Screenshot_20260601-191457~2

@Jetrell

Jetrell commented Jun 2, 2026

Copy link
Copy Markdown

@halfmanbear INAV is about to enter the 9.1 release phase. Once that is finished I will conduct some testing.

@sensei-hacker

sensei-hacker commented Jun 2, 2026

Copy link
Copy Markdown
Member

Thanks for your work in this.
I have a pile of notes about bidirectional DSHOT that I will go through and compare your implementation here.

Getting it to work is one thing, the impact on performance with everything else on a hexcopter running on a less-capable chip another issue.

@halfmanbear halfmanbear mentioned this pull request Jun 28, 2026
@course-up

Copy link
Copy Markdown

I can't compare with any ESC UART telemetry as my stack doesn't support this (see below). One of the main reasons for implementing bidirectional DSHOT.

For testing you can flash Bluejay, it is the recommended firmware with all new features for old BLHELI_S
I reflashed all my BLHELI_S ESCs with Bluejay for iNav and Betaflight drones and they are working great.

@sensei-hacker

sensei-hacker commented Jul 26, 2026

Copy link
Copy Markdown
Member

Thanks again for all of your work on this. Sorry it took so long for me to review.
There is a lot to look at and consider, even with an AI tool assisting me in reading through the history Betaflight has had with bidirectional Dshot, the data sheets, etc.

Going through your excellent PR, it looks like it mostly matches the plan I had put together but didn't actually implement in full. Since we independently came up with the same design, that might be a hint that it's a good design. :) I could go over various points that look good, but suffice to say - nice work!

It is my understanding that on the H7, the D-cache means DMA-written data isn't automatically visible to the CPU without `SCB_InvalidateDCache_by_Addr() being called, so we may need to call that? F7 has the same D-cache, but it's disabled in INAV.

I currently see the build fails with:
/home/runner/work/inav/inav/src/main/drivers/pwm_output.c:367:17: error: 'dshotDmaStream' defined but not used [-Werror=unused-function]
367 | static uint32_t dshotDmaStream(const pwmOutputPort_t *port)

There may be a need to gate this on #if (MCU_FLASH_SIZE > 512), for F722.

It's my understanding this won't work in CH1N-CH4N, which is probably okay because it looks no targets use that on the first four outputs (quad motors). But we'd need to keep the unidirectional DShot for hexacopters and octocopters.
Similarly for these outputs which need DMAR:

┌───────────────────
│ target_name │ Output number
├───────────────────
│ AXISFLYINGH743PRO │ 12
│ DAKEFPVH743 │ 8
│ DAKEFPVH743 │ 10
│ DAKEFPVH743PRO │ 12
│ DAKEFPVH743_SLIM │ 12
│ HAKRCH743 │ 8
│ HAKRCH743 │ 10

@sensei-hacker

sensei-hacker commented Jul 27, 2026

Copy link
Copy Markdown
Member

A note re F722 for future reference: this feature would add 2,972 bytes of flash.
We might be able to find something that size to remove, if we chose to.

@sensei-hacker
sensei-hacker changed the base branch from master to maintenance-10.x August 2, 2026 17:00
@sensei-hacker

Copy link
Copy Markdown
Member

Current build errors:

ggdb3 -DNDEBUG -std=gnu99 -flto=auto -fno-fat-lto-objects -ffunction-sections -fdata-sections -fno-common -mthumb -mcpu=cortex-m4 -march=armv7e-m -mfloat-abi=hard -mfpu=fpv4-sp-d16 -fsingle-precision-constant -Wdouble-promotion -Werror -O2 -Wall -Wextra -Wunsafe-loop-optimizations -Wstrict-prototypes -Werror=switch -MD -MT src/main/target/FF_F35_LIGHTNING/CMakeFiles/WINGFC.elf.dir///drivers/pwm_output.c.obj -MF src/main/target/FF_F35_LIGHTNING/CMakeFiles/WINGFC.elf.dir///drivers/pwm_output.c.obj.d -o src/main/target/FF_F35_LIGHTNING/CMakeFiles/WINGFC.elf.dir///drivers/pwm_output.c.obj -c /home/runner/work/inav/inav/src/main/drivers/pwm_output.c
/home/runner/work/inav/inav/src/main/drivers/pwm_output.c:367:17: error: 'dshotDmaStream' defined but not used [-Werror=unused-function]
367 | static uint32_t dshotDmaStream(const pwmOutputPort_t *port)
| ^~~~~~~~~~~~~~
cc1: all warnings being treated as errors
[199/3225] Building C object src/main/target/FF_F35_LIGHTNING/CMakeFiles/WINGFC.elf.dir///drivers/pinio.c.obj
[200/3225] Generating ZEEZF7V2/settings_generated.h, ZEEZF7V2/settings_generated.c

Also:
'Error: "docs/Settings.md" is not up to date, please run "src/utils/update_cli_docs.py"'

@sensei-hacker

Copy link
Copy Markdown
Member

Just an FYI for contributors: The tentative schedule for INAV 10 is to have a full release in mid December. That means RC2 needs to be in early to mid November, which places INAV 10.0RC1 at September 1. Please plan to have any new features for INAV 10.0 ready for RC1 no later than September 1. After that, 10.1 will follow about six to seven months later.

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.
@sensei-hacker

sensei-hacker commented Aug 20, 2026

Copy link
Copy Markdown
Member

If someone is able, I would appreciate it if someone could run a CPU load check, just to confirm:

For a pilot with a bench setup: FC + bidir-capable ESC (BLHeli32, AM32, or
Bluejay) + motor. This measures real telemetry decode cost, which our own
bench testing couldn't — no ESC was connected here.

Safety

  • Propellers OFF. A free-spinning motor gives the ESC everything it
    needs to send real telemetry; you don't need thrust for this test.
  • Mount the motor/FC so it can't walk off the bench, and keep hands and
    loose objects clear before arming.

What you need

  • FC flashedwith this build (see link in comment below)
  • at least one motor output wired to a bidir-DShot-capable ESC (BLHeli32/AM32/Bluejay, DShot telemetry enabled
    on the ESC side), motor attached, no propeller.
  • A way to arm (radio + receiver, or your usual bench-test method).
  • USB cable to the FC, with INAV Configurator installed and connected.

Steps

1. Confirm the ESC is actually reporting telemetry.
Arm briefly at low throttle, open the Configurator's Motors tab or OSD, and
check that RPM/eRPM shows a live non-zero value while the motor spins.
If RPM stays at 0, the ESC isn't sending real telemetry and this test
won't measure what we need — check ESC-side DShot telemetry settings
first.

2. Run A — bidirectional DShot ON (already the default in this build).
a. Arm, hold a steady, moderate throttle (something the motor can sustain
smoothly for a minute — no need to go high) for about 60 seconds to
let telemetry settle into a steady rhythm.
b. While still armed and spinning, go to the cli tab and run:
tasks
Save the output.
c. Repeat step (b) two more times, a few seconds apart, throttle held
steady, so we have 3 samples.
d. Disarm.

3. Run B — bidirectional DShot OFF (comparison baseline).
a. Enter CLI (disarmed) and run:
set dshot_bidir_enabled = OFF save
b. Repeat step 2 exactly (arm, same throttle, same duration, 3 tasks
samples while spinning).
c. Disarm.

4. Restore the setting so the board is back the way it started:

set dshot_bidir_enabled = ON
save

What to send back

  • All 6 tasks outputs (3 from Run A, 3 from Run B), labeled ON/OFF.
  • The throttle % or stick position you held, and roughly how long each run
    spun before sampling.
  • ESC make/firmware (e.g. "BLHeli32 32.10" or "AM32 2.x") and the DShot
    protocol/rate in use (e.g. DSHOT600).
  • Confirmation that RPM telemetry showed live non-zero values during Run A
    (from step 1/2).

That's enough for us to compare the PID task's avg/max time and load
between the two runs under real telemetry traffic.

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.
@sensei-hacker

sensei-hacker commented Aug 20, 2026

Copy link
Copy Markdown
Member

I have made four commits fixing four different kinds of compile failures on this.
Now we're back to the earlier compile failure on ITC RAM.

I would love to be able to include this in INAV 10, but worry about the correctness and quality when there are multiple different types of errors caught just by trying to build it.

@sensei-hacker

sensei-hacker commented Aug 20, 2026

Copy link
Copy Markdown
Member

The current compile failures on all the F722 targets are because this will not fit in ITCM on F722 and it's single-caller within the scheduler() FAST_CODE call chain.

@mart1npetroff

Copy link
Copy Markdown

Memory issue is resolved with this PR: halfmanbear#1

Probably auto transition + this one merged together into base branch will need more memory optimisations.
But I need at least one of them merged first to review if there will be more memory issues.

fix: keep DShot telemetry decode out of ITCM
@github-actions

Copy link
Copy Markdown

RAM / Flash usage vs. base branch — commit c4bc589

Target Flash Δ RAM Δ
MATEKF405 ⚠️ +2580 B (+0.39%) +868 B (+0.60%)
MATEKF722 ⚠️ +3392 B (+0.73%) +880 B (+0.70%)
MATEKF765 ⚠️ +3740 B (+0.55%) +1200 B (+0.80%)
MATEKH743 ⚠️ +3684 B (+0.51%) +1152 B (+0.75%)

@github-actions

Copy link
Copy Markdown

Test firmware build ready — commit c4bc589

Download firmware for PR #11605

244 targets built. Find your board's .hex file by name on that page (e.g. MATEKF405SE.hex). Files are individually downloadable — no GitHub login required.

Development build for testing only. Use Full Chip Erase when flashing.

@HishamGhosheh

Copy link
Copy Markdown
Contributor

Hi, I'm trying to test this but I'm not sure how.
Test build is this (it has USE_ESC_SENSOR):
https://github.com/iNavFlight/pr-test-builds/releases/download/pr-11605/inav_9.1.0_JHEMCUF405_ci-20260822-c4bc589.hex

I had to enable dshot_bidir_enabled manually as it was originally off.
I cannot see RPM and there's no RPM related OSD element in the configurator. I built and tried the v10 of the configurator as well.

Screenshot 2026-08-22 131459 Screenshot 2026-08-22 232923

My FC does support bidirectional dshot when it runs Ardupilot so it's not a hardware issue

@sensei-hacker

Copy link
Copy Markdown
Member

bidirectional DShot + EDT telemetry is a great addition. I pointed another tool at it and I ran into a few things I wasn't sure about and wanted to check with you rather than assume they're bugs, since I may be missing context.

  1. dataAge never increments for the new DShot-bidir path
    In esc_sensor.c, escSensorUpdate() returns early when escSensorPort is NULL, so escSensorIncreaseDataAge() is never reached on this path, and escSensorSetDshotData() only ever sets dataAge = 0. If an ESC signal is lost mid-flight
    after at least one good packet, would the last known values keep being reported as fresh indefinitely (rather than aging out), including on the voltage/current failsafe path?

  2. Capture timer period on HAL/AT32F43x targets
    pwmDshotSetDirectionInput() (pwm_output.c) doesn't appear to widen the timer's autoreload/period before switching to capture mode on HAL (F7/H7/G4) or AT32F43x — it looks like it stays at DSHOT_MOTOR_BITLENGTH-1. The StdPeriph/F4
    branch explicitly sets ARR to 0xffff first. Could the counter wrap mid-frame during telemetry capture on the non-F4 branches? If so it'd affect whether bidir telemetry actually decodes on those targets.

  3. Capture DMA enabled without clearing the prior transfer-complete flag (HAL and AT32 branches)
    Same function — the HAL branch (and similarly the AT32 branch) re-enables the capture DMA stream without clearing the stale TC/HT/TE flags left from the just-completed output transfer, while the StdPeriph branch does this
    explicitly with a comment. Since this is only called right after a TC-complete IRQ, is the flag guaranteed still set at that point, and if so, could that cause a spurious immediate transfer-complete on the new capture stream?

  4. eRPM decode can exceed uint16_t range
    dshotDecodeErpmTelemetryValue() can legitimately return up to ~600000 for small value, but it's stored into telemetryData[], a uint16_t array element, with no clamping. For value <= 9 this truncates. Is this reachable in practice (e.g. very high-KV/low-pole setups), or is value bounded elsewhere such that it can't get that low?

@error414

error414 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

I tested it on my plane.

FC: MATEKH743-WLITE
ESC: Heewing FX-25A Q-H-40 - Bluejay, 0.21.0, 48kHz

Motor will not start at all. Motors beep after connecting battery but will not start, same for both configuratuion
dshot_bidir_enabled = ON
dshot_bidir_enabled = OFF

Connecting from https://esc-configurator.com/ works and it's possible to load ESC configuration

image image

Tested on T1, VTOL configuration.
https://www.dropbox.com/scl/fi/nks2rv2arg3c5v8seweu9/INAV_9.1.0_cli_T1_20260828_191016.txt?rlkey=7ppf1w6ei7oovl8nov5stmmy2&st=2qaqpzji&dl=0

BTW: I don't have prepared test FC for ESC testing, I have to prepare first.

# tasks
Task list         rate/hz  max/us  avg/us maxload avgload     total/ms
 0 -       SYSTEM       9      13       1    0.5%    0.5%     37970
 1 -          PID    1996      68      45   14.0%    9.4%     16596
 2 -         GYRO    3984      15       9    6.4%    4.0%      6434
 3 -           RX      49      17      11    0.5%    0.5%       214
 4 -       SERIAL      99   43233270      13 428009.8%    0.6%     43519
 5 -      BATTERY      49       9       7    0.5%    0.5%        60
 6 -  TEMPERATURE      99       7       1    0.5%    0.5%        14
 7 -       BEEPER      99       6       1    0.5%    0.5%        17
 8 -          GPS      49      33       6    0.6%    0.5%        44
10 -         BARO      62     210     207    1.8%    1.7%      2363
11 -         ADSB       2       2       0    0.5%    0.5%         0
12 -        PITOT      49       3       1    0.5%    0.5%         5
15 -    TELEMETRY     499     145       1    7.7%    0.5%       541
18 -          OSD     250      19       1    0.9%    0.5%        63
19 -          CMS      49       2       1    0.5%    0.5%         3
22 -      VTXCTRL       4       3       1    0.5%    0.5%         1
23 -  PROGRAMMING      10       9       4    0.5%    0.5%        10
25 -          AUX      99       6       2    0.5%    0.5%        25
26 - SPORT MASTER     499       4       1    0.6%    0.5%        30
27 -  CRSF SENSOR     100       2       0    0.5%    0.5%         5
29 -       GIMBAL      49       2       0    0.5%    0.5%         2
30 -  HEADTRACKER      49       5       0    0.5%    0.5%         2
33 -     DRONECAN     499       7       1    0.8%    0.5%        74
Task check function             8       1                        33
Total (excluding SERIAL)                    39.8%   24.6%

@error414

Copy link
Copy Markdown
Contributor

confirmed, in MATEKH743-WLITE (S1 / timer3) is dshot broken. I tried S3/ Timer 5 , same issue

Maintanence-10.x
there is nice dshot frame, which reacts nicely to Motor Test Mode slider in configurator
image

fork/halfmanbear/master
frame is valid but all of the time inav sends value 1999
image

but if I used Motor Test Mode slider and moved slider to 43% power, frame has changed
image

@error414

Copy link
Copy Markdown
Contributor

Here is some suggestion what could be fixed in dshot bidir. There are three fixes (mainly for H7)

  1. fix invertion, bidir dshot is inverted, see info https://betaflight.com/docs/development/API/Dshot#bidirectional-dshot
  2. fix reinicialize DMA stream for reading, mainly for H7 and F7 CPUs
  3. magic fix from betaflight for H7 (have no idea how it works)

My is probably only for DMA version of dshot, I did not tested DMAR on F4 CPU.

It's in 95% code generated by AI. But I tried to understand what is happening there. But for me how bidir-dshot works is something between magic and miracle.
halfmanbear#2

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.

7 participants