Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0e30739
Add bidirectional DShot telemetry and RPM gyro filtering
halfmanbear May 26, 2026
c9aa345
Merge upstream/maintenance-10.x to resolve conflicts with PR #11605
sensei-hacker Aug 3, 2026
fe94094
Fix unused-function CI failure on F4 StdPeriph targets
sensei-hacker Aug 20, 2026
0dcf861
Fix implicit-function-declaration on F7/H7 DMA request calls
sensei-hacker Aug 20, 2026
61fcc05
docs: regenerate Settings.md for PR #11605's new DShot telemetry sett…
sensei-hacker Aug 20, 2026
6d26d2a
Fix implicit-function-declaration for loadDmaBufferDshot on DMAR targets
sensei-hacker Aug 20, 2026
3b2a74d
fix: keep DShot telemetry decode out of ITCM
mart1npetroff Aug 20, 2026
c4bc589
Merge pull request #1 from mart1npetroff/fix/halfmanbear-itcm-overflow
halfmanbear Aug 22, 2026
ac6c735
BidirDshot - fix for H7, invertion of timer, reset DMA for reading te…
error414 Aug 29, 2026
abc7374
BidirDshot - hold the line to avoid double beeping durring FC bootup
error414 Sep 5, 2026
4499455
BidirDshot - skip circular DMA keepalive during EEPROM writes
error414 Sep 8, 2026
016f3c6
Merge branch 'maintenance-10.x' into master
sensei-hacker Sep 10, 2026
e6dcaf2
Merge pull request #2 from error414/error414/feat/bidir-dshot-fix
halfmanbear Sep 10, 2026
0a611ac
Merge branch 'fork/halfmanbear/master' into error414/feature/bidir-dshot
error414 Sep 21, 2026
57434f3
Bidir DSHOT - USE_DSHOT_BIDIR (H7), bidir_dshot.c, review fixes
error414 Sep 21, 2026
59a7161
DSHOT BIDIR - keep pin hight
error414 Sep 24, 2026
331cc72
Merge branch 'maintenance-10.x' into error414/feature/bidir-dshot
error414 Sep 24, 2026
1385fdd
DSHOT BIDIR - allow for all target without DMAR
error414 Sep 24, 2026
d94f0fc
DSHOT BIDIR - fix 1. SRXL2 telemetry disappears from consumers
error414 Sep 24, 2026
d9df0b4
DSHOT BIDIR - 2. AT32 motors never receive usable telemetry
error414 Sep 24, 2026
cb54594
DSHOT BIDIR - 5. Rp2350 accepts telemetry it cannot use - disable bi…
error414 Sep 24, 2026
4ec7ad6
DSHOT BIDIR - small fixes for RPM filters
error414 Sep 26, 2026
fdd4052
DSHOT BIDIR - add bidir dshot rpm to MSP
error414 Sep 28, 2026
df0bd31
Merge branch 'maintenance-10.x' into error414/feature/bidir-dshot
error414 Sep 28, 2026
61637a5
DSHOT BIDIR - fix DMA library clearing flag
error414 Sep 28, 2026
20b0c54
DSHOT BIDIR - add counters of frames for bidir and for aurt telemetry
error414 Sep 29, 2026
020f8f9
DSHOT BIDIR - add counters to MSP for configurator
error414 Sep 29, 2026
535673d
DSHOT BIDIR - adjust error counter
error414 Sep 30, 2026
5ef40b9
DSHOT BIDIR - pull up durring eeprom saving instead of dire driving pin
error414 Oct 1, 2026
7e2742d
DSHOT BIDIR - disable bidir for F7, not enought memory so far
error414 Oct 2, 2026
fc84c11
DSHOT BIDIR - optimize memory
error414 Oct 2, 2026
7086adc
Merge branch 'maintenance-10.x' into error414/feature/bidir-dshot
error414 Oct 5, 2026
d6ed722
DSHOT BIDIR - fix EDT, send EDT ON after arm
error414 Oct 6, 2026
4a7d4e2
DSHOT BIDIR - try to make space for F722
error414 Oct 6, 2026
44d28e4
mixer: dynamic idle from bidirectional DShot RPM, ported from Betaflight
MrScothh Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/Blackbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ Available debug modes include:
- `GPS` - GPS debugging
- `ALTITUDE` - Altitude estimation debugging
- `MAG` - Raw, uncalibrated magnetometer samples (useful for external compass calibration)
- `RPM_FILTER` - Per-motor RPM as seen by the RPM filter, motors 1-8 in `debug[0]`-`debug[7]` (needs the RPM filter enabled, so bidirectional DShot or ESC telemetry)
- `DYN_IDLE` - Dynamic idle (`dynamic_idle_min_rpm`): P, I and D in `debug[0]`-`debug[2]` (0.0001 of the motor range), the slowest motor's frequency in `debug[3]` (0.1 Hz)
- And 20+ other modes for specific subsystems

To use debug mode logging:
Expand Down
72 changes: 72 additions & 0 deletions docs/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,8 @@ Defines debug values exposed in debug variables (developer / debugging setting)
| ESC | |
| FW_TURN | |
| MAG | |
| RPM_FILTER | |
| DYN_IDLE | |

---

Expand Down Expand Up @@ -944,6 +946,26 @@ Sets the DShot beeper tone

---

### dshot_bidir_enabled

Enable bidirectional DShot telemetry on motor outputs. Required for RPM filtering without a separate ESC telemetry UART

| Default | Min | Max |
| --- | --- | --- |
| OFF | OFF | ON |

---

### dshot_edt_enabled

Enable extended DShot telemetry (temperature, voltage, current in addition to eRPM). The ESC is asked for it on every arm, so the values appear after the first arm

| Default | Min | Max |
| --- | --- | --- |
| OFF | OFF | ON |

---

### dterm_lpf2_hz

Dterm pre-differentiation LPF cutoff (Hz). Filters gyro before differentiation to reduce noise amplification. Higher = less delay, more noise. 0 = disabled. Values around 200-250Hz can add smoothing with small delay.
Expand Down Expand Up @@ -1029,6 +1051,56 @@ Q factor for dynamic notches

---

### dynamic_idle_d_gain

D gain of `dynamic_idle_min_rpm`, on how fast the slowest motor slows down

| Default | Min | Max |
| --- | --- | --- |
| 50 | 0 | 250 |

---

### dynamic_idle_i_gain

I gain of `dynamic_idle_min_rpm`: it rises fast and falls slowly

| Default | Min | Max |
| --- | --- | --- |
| 50 | 1 | 250 |

---

### dynamic_idle_max_increase

The most `dynamic_idle_min_rpm` may raise the motors' low end, in tenths of a percent of the motor range: 150 is 15 %

| Default | Min | Max |
| --- | --- | --- |
| 150 | 10 | 255 |

---

### dynamic_idle_min_rpm

Multirotor only, ported from Betaflight, needs `dshot_bidir_enabled`. The slowest speed the motors are kept at once the throttle has been raised [RPM]; the low end of the motor output then moves with it, starting from the lowest DShot value instead of `throttle_idle`. Until the throttle has passed `airmode_throttle_threshold` once after arming the motors go no higher than `throttle_idle`. Betaflight's `dyn_idle_min_rpm` is in hundreds of RPM: 35 there is 3500 here. It starts once at least 80% of every motor's replies decode and falls back to `throttle_idle` below 50%. 0 disables it.

| Default | Min | Max |
| --- | --- | --- |
| 0 | 0 | 20000 |

---

### dynamic_idle_p_gain

P gain of `dynamic_idle_min_rpm`, on how far the slowest motor is below it

| Default | Min | Max |
| --- | --- | --- |
| 50 | 1 | 250 |

---

### enable_broken_o4_workaround

DJI O4 release firmware has a broken MSP DisplayPort implementation. This enables a workaround to restore ARM detection.
Expand Down
6 changes: 4 additions & 2 deletions docs/development/msp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4180,16 +4180,18 @@ When the MSP JSON specification changes, bump `msp_messages.json` version:
**Notes:** Requires `USE_PROGRAMMING_FRAMEWORK`. Fixed 8-byte reply carrying one 64-bit mask as two `uint32_t` halves, low half first. Only the first `MIN(MAX_LOGIC_CONDITIONS, 64)` bits are evaluated. A condition counts as configured when any of `enabled`, `activatorId` (default -1), `operation`, `operandA.type`, `operandA.value`, `operandB.type`, `operandB.value` or `flags` differs from its default.

## <a id="msp2_inav_esc_rpm"></a>`MSP2_INAV_ESC_RPM (8256 / 0x2040)`
**Description:** Retrieves the RPM reported by each ESC via telemetry.
**Description:** Retrieves the RPM and telemetry link quality reported by each ESC.

**Request Payload:** **None**

**Reply Payload:**
|Field|C Type|Size (Bytes)|Units|Description|
|---|---|---|---|---|
| `escRpm` | `uint32_t` | 4 | RPM | RPM reported by the ESC |
| `frameSuccess` | `uint8_t` | 1 | % | Share of telemetry replies decoded in the last closed window. 255 when no reply was expected in that window |
| `frameTotal` | `uint16_t` | 2 | frames | Telemetry replies expected in the last closed window, missing ones included |

**Notes:** Requires `USE_ESC_SENSOR`. Payload size depends on the number of detected motors with telemetry.
**Notes:** Requires `USE_ESC_SENSOR` or `USE_DSHOT_BIDIR`. 7 bytes per motor, `getMotorCount()` entries. Source follows the RPM filter: bidirectional DSHOT when `isDshotTelemetryActive()`, serial ESC telemetry otherwise. The frame statistics come from fixed back-to-back windows of `ESC_FRAME_WINDOW_MS` (1 s), so they change once per window. A reply counts as missing when the ESC stays silent, fails CRC/GCR decoding or (serial) times out. Sources without frame counting (SRXL2) always report 255 / 0.

## <a id="msp2_inav_esc_telem"></a>`MSP2_INAV_ESC_TELEM (8257 / 0x2041)`
**Description:** Retrieves the full telemetry data structure reported by each ESC.
Expand Down
16 changes: 14 additions & 2 deletions docs/development/msp/msp_messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -9847,13 +9847,25 @@
"ctype": "uint32_t",
"desc": "RPM reported by the ESC",
"units": "RPM"
},
{
"name": "frameSuccess",
"ctype": "uint8_t",
"desc": "Share of telemetry replies decoded in the last closed window. 255 when no reply was expected in that window",
"units": "%"
},
{
"name": "frameTotal",
"ctype": "uint16_t",
"desc": "Telemetry replies expected in the last closed window, missing ones included",
"units": "frames"
}
],
"repeating": "getMotorCount()"
},
"variable_len": "getMotorCount()",
"notes": "Requires `USE_ESC_SENSOR`. Payload size depends on the number of detected motors with telemetry.",
"description": "Retrieves the RPM reported by each ESC via telemetry."
"notes": "Requires `USE_ESC_SENSOR` or `USE_DSHOT_BIDIR`. 7 bytes per motor, `getMotorCount()` entries. Source follows the RPM filter: bidirectional DSHOT when `isDshotTelemetryActive()`, serial ESC telemetry otherwise. The frame statistics come from fixed back-to-back windows of `ESC_FRAME_WINDOW_MS` (1 s), so they change once per window. A reply counts as missing when the ESC stays silent, fails CRC/GCR decoding or (serial) times out. Sources without frame counting (SRXL2) always report 255 / 0.",
"description": "Retrieves the RPM and telemetry link quality reported by each ESC."
},
"MSP2_INAV_ESC_TELEM": {
"code": 8257,
Expand Down
2 changes: 2 additions & 0 deletions src/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ main_sources(COMMON_SRC
drivers/pwm_esc_detect.h
drivers/pwm_mapping.c
drivers/pwm_mapping.h
drivers/bidir_dshot.c
drivers/bidir_dshot.h
drivers/pwm_output.c
drivers/pwm_output.h
drivers/pinio.c
Expand Down
8 changes: 5 additions & 3 deletions src/main/blackbox/blackbox.c
Original file line number Diff line number Diff line change
Expand Up @@ -1573,8 +1573,10 @@ static void loadSlowState(blackboxSlowState_t *slow)

#ifdef USE_ESC_SENSOR
escSensorData_t * escSensor = escSensorGetData();
slow->escRPM = escSensor->rpm;
slow->escTemperature = escSensor->temperature;
if (escSensor) {
slow->escRPM = escSensor->rpm;
slow->escTemperature = escSensor->temperature;
}
#endif

#ifdef USE_DRONECAN
Expand All @@ -1600,7 +1602,7 @@ static bool writeSlowFrameIfNeeded(bool allowPeriodicWrite)
if (shouldWrite) {
loadSlowState(&slowHistory);
} else {
blackboxSlowState_t newSlowState;
blackboxSlowState_t newSlowState = {0};

loadSlowState(&newSlowState);

Expand Down
2 changes: 2 additions & 0 deletions src/main/build/debug.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ typedef enum {
DEBUG_ESC,
DEBUG_FW_TURN,
DEBUG_MAG,
DEBUG_RPM_FILTER,
DEBUG_DYN_IDLE,
DEBUG_COUNT // also update debugModeNames in cli.c
} debugType_e;

Expand Down
166 changes: 166 additions & 0 deletions src/main/drivers/bidir_dshot.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* This file is part of Betaflight.
*
* Betaflight is free software. You can redistribute this software
* and/or modify this software under the terms of the GNU General
* Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later
* version.
*
* Betaflight is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software.
*
* If not, see <http://www.gnu.org/licenses/>.
*/

#include <math.h>

#include "platform.h"
#include "drivers/bidir_dshot.h"

// Bidirectional DSHOT telemetry only: eRPM / EDT decoding, RPM state for the RPM filter and
// the ESC sensor. Frame generation, DMA and DSHOT commands for plain DSHOT live in
// pwm_output.c under USE_DSHOT. Every user of this API is guarded by USE_DSHOT_BIDIR.
//
// Values are published as decoded, unfiltered: frames arrive at the DSHOT frame rate, which
// follows the scheduler's idle time rather than any fixed period, so smoothing belongs to the
// consumer that samples them at a known rate (rpmFilterUpdateTask()).

#ifdef USE_DSHOT_BIDIR

#include "flight/mixer.h"

// Only eRPM, temperature, voltage and current are kept; the EDT debug and status frames
// have no consumer and just mark their type as seen
#define DSHOT_TELEMETRY_STORED_TYPE_COUNT (DSHOT_TELEMETRY_TYPE_CURRENT + 1)

typedef struct {
uint16_t telemetryData[DSHOT_TELEMETRY_STORED_TYPE_COUNT];
uint8_t telemetryTypes; // bit per dshotTelemetryType_e seen since boot
} dshotTelemetryMotorState_t;

bool useDshotTelemetry = false;

static dshotTelemetryMotorState_t motorTelemetry[MAX_SUPPORTED_MOTORS];
static float erpmToHz;

static uint32_t dshotDecodeErpmTelemetryValue(uint16_t value)
{
if (value == 0x0fff) {
return 0;
}

value = (value & 0x01ff) << ((value & 0xfe00) >> 9);
if (!value) {
return DSHOT_TELEMETRY_INVALID;
}

return (1000000 * 60 / 100 + value / 2) / value;
}

static void dshotDecodeTelemetryValue(uint16_t value, uint8_t motorIndex, uint32_t *decoded, dshotTelemetryType_e *type)
{
const bool edtEnabled = motorConfig()->useDshotEdt || (motorTelemetry[motorIndex].telemetryTypes & DSHOT_EXTENDED_TELEMETRY_MASK) != 0;
const unsigned telemetryType = (value & 0x0f00) >> 8;
const bool isErpm = !edtEnabled || (telemetryType & 0x01) || (telemetryType == 0);

if (isErpm) {
*decoded = dshotDecodeErpmTelemetryValue(value);
*type = DSHOT_TELEMETRY_TYPE_ERPM;
} else {
// EDT frame types 2, 4, ... 14 are the enum values 1..7 in order
*type = telemetryType >> 1;
*decoded = value & 0x00ff;
}
}

bool isDshotTelemetryActive(void)
{
return useDshotTelemetry;
}

// useDshotTelemetry itself is set by the motor driver (pwmMotorPreconfigure()), which
// knows whether the outputs were configured for DSHOT at all
void initDshotTelemetry(void)
{
if (!useDshotTelemetry) {
return;
}

erpmToHz = ERPM_PER_LSB / 60.0f / (motorConfig()->motorPoleCount / 2.0f);
}

uint16_t dshotProcessPacket(uint16_t rawValue, uint8_t motorIndex)
{
if (!useDshotTelemetry || motorIndex >= MAX_SUPPORTED_MOTORS) {
return rawValue;
}

// As in Betaflight, a window without a reply is not counted: an ESC may skip replies
// when busy, so only replies that were at least partly captured count
if (rawValue == DSHOT_TELEMETRY_NOEDGE) {
return rawValue;
}

escFrameCounter_t *frames = escSensorFrameCounter(motorIndex);
frames->total++;

if (rawValue == DSHOT_TELEMETRY_INVALID) {
return rawValue;
}

dshotTelemetryType_e type;
uint32_t decoded;
dshotDecodeTelemetryValue(rawValue, motorIndex, &decoded, &type);
if (decoded == DSHOT_TELEMETRY_INVALID) {
return DSHOT_TELEMETRY_INVALID;
}

frames->valid++;

motorTelemetry[motorIndex].telemetryTypes |= (1 << type);
if (type < DSHOT_TELEMETRY_STORED_TYPE_COUNT) {
motorTelemetry[motorIndex].telemetryData[type] = decoded;
}

return rawValue;
}

float getMotorFrequencyHz(uint8_t motorIndex)
{
return motorTelemetry[motorIndex].telemetryData[DSHOT_TELEMETRY_TYPE_ERPM] * erpmToHz;
}

float getDshotRpm(uint8_t motorIndex)
{
return getMotorFrequencyHz(motorIndex) * 60.0f;
}

bool getDshotEscSensorData(escSensorData_t *data, uint8_t motorIndex)
{
if (!useDshotTelemetry || motorIndex >= MAX_SUPPORTED_MOTORS) {
return false;
}

const dshotTelemetryMotorState_t *state = &motorTelemetry[motorIndex];
if ((state->telemetryTypes & (1 << DSHOT_TELEMETRY_TYPE_ERPM)) == 0) {
return false;
}

// escSensorData_t units: rpm (mechanical), degrees C, 0.01 V, 0.01 A. EDT reports
// the temperature in degrees C, the voltage in 0.25 V and the current in 1 A steps.
data->rpm = lrintf(getDshotRpm(motorIndex));
data->temperature = (state->telemetryTypes & (1 << DSHOT_TELEMETRY_TYPE_TEMPERATURE)) ? state->telemetryData[DSHOT_TELEMETRY_TYPE_TEMPERATURE] : 0;
data->voltage = (state->telemetryTypes & (1 << DSHOT_TELEMETRY_TYPE_VOLTAGE)) ? state->telemetryData[DSHOT_TELEMETRY_TYPE_VOLTAGE] * 25 : 0;
data->current = (state->telemetryTypes & (1 << DSHOT_TELEMETRY_TYPE_CURRENT)) ? state->telemetryData[DSHOT_TELEMETRY_TYPE_CURRENT] * 100 : 0;

return true;
}

#endif
Loading
Loading