dshot_frame/
lib.rs

1//! Support for the DShot ESC protocol
2//!
3//! DShot has two-byte frames, where the first 11 bits are the throttle speed, bit 12 is a
4//! telemetry request flag, and the last four bits are a checksum.
5//!
6//! Throttle values below 48 are reserved for special commands.
7//!
8//! It is transmitted over the wire at a fixed speed, with ones and zeroes both being pulses, but
9//! ones being twice as long as zeroes.
10//!
11//! ## Usage
12//!
13//! This example is adapted from an embassy-stm32 codebase:
14//!
15//! ```ignore
16//! let mut pwm = SimplePwm::new(
17//!     timer,
18//!     Some(PwmPin::new_ch1(pin, OutputType::PushPull)),
19//!     None,
20//!     None,
21//!     None,
22//!     Hertz(150_000),
23//!     CountingMode::EdgeAlignedUp,
24//! );
25//! let max_duty_cycle = pwm.get_max_duty() as u16;
26//!
27//! let frame = Frame::<NormalDshot>::new(1000, false).unwrap();
28//! pwm.waveform_up(&mut dma, Ch1, &frame.duty_cycles()).await;
29//! // Pull the line low after sending a frame.
30//! pwm.set_duty(channel, 0);
31//! pwm.enable(channel);
32//! ```
33
34#![no_std]
35
36/// Defines the behavior of a DShot protocol variant.
37pub trait DshotProtocol {
38    /// Computes the 4-bit CRC for the upper 12 bits of the frame.
39    fn compute_crc(value: u16) -> u16;
40
41    /// Returns `true` if the signal is inverted (bidirectional mode).
42    fn is_inverted() -> bool;
43
44    fn get_translated_throttle(speed: u16) -> u16;
45}
46
47/// Standard (non-inverted) DShot protocol.
48#[derive(Debug, Clone, Copy)]
49pub struct NormalDshot;
50
51impl DshotProtocol for NormalDshot {
52    fn compute_crc(value: u16) -> u16 {
53        (value ^ (value >> 4) ^ (value >> 8)) & 0x0F
54    }
55
56    fn is_inverted() -> bool {
57        false
58    }
59
60    fn get_translated_throttle(speed: u16) -> u16 {
61        (speed + 48) << 5
62    }
63}
64
65/// Bidirectional (inverted) DShot protocol.
66#[derive(Debug, Clone, Copy)]
67pub struct BidirectionalDshot;
68
69impl DshotProtocol for BidirectionalDshot {
70    fn compute_crc(value: u16) -> u16 {
71        (!(value ^ (value >> 4) ^ (value >> 8))) & 0x0F
72    }
73
74    fn is_inverted() -> bool {
75        true
76    }
77
78    fn get_translated_throttle(speed: u16) -> u16 {
79        let mask = 0b111_1111_1111;
80        (!(speed + 48) & mask) << 5
81    }
82}
83
84/// A DShot frame parameterized by its protocol variant.
85#[derive(Copy, Clone, Debug)]
86pub struct Frame<P: DshotProtocol = NormalDshot> {
87    inner: u16,
88    _protocol: core::marker::PhantomData<P>,
89}
90
91impl<P: DshotProtocol> Frame<P> {
92    /// Creates a new frame with the given speed (0-1999) and telemetry request.
93    ///
94    /// Returns [`None`] if the speed is out of bounds.
95    ///
96    /// ```
97    /// # use dshot_frame::*;
98    /// assert_eq!(Frame::<NormalDshot>::new(1000, false).unwrap().speed(), 1000);
99    /// ```
100    pub fn new(speed: u16, request_telemetry: bool) -> Option<Self> {
101        if speed >= 2000 {
102            return None;
103        }
104
105        let translated_throttle = P::get_translated_throttle(speed);
106        let mut frame = Self {
107            inner: translated_throttle,
108            _protocol: core::marker::PhantomData,
109        };
110        if request_telemetry {
111            frame.inner |= 0x10;
112        }
113        frame.compute_crc();
114        Some(frame)
115    }
116
117    /// Creates a new frame with the given [`Command`] and telemetry request.
118    pub fn command(command: Command, request_telemetry: bool) -> Self {
119        let mut frame = Self {
120            inner: (command as u16) << 5,
121            _protocol: core::marker::PhantomData,
122        };
123        if request_telemetry {
124            frame.inner |= 0x10;
125        }
126        frame.compute_crc();
127        frame
128    }
129
130    /// Returns the speed value (0-1999).
131    pub fn speed(&self) -> u16 {
132        (self.inner >> 5) - 48
133    }
134
135    /// Returns whether telemetry is enabled.
136    pub fn telemetry_enabled(&self) -> bool {
137        self.inner & 0x10 != 0
138    }
139
140    /// Returns the CRC checksum.
141    pub fn crc(&self) -> u16 {
142        self.inner & 0x0F
143    }
144
145    /// Computes the CRC based on the first 12 bits and ORs it in.
146    fn compute_crc(&mut self) {
147        let value = self.inner >> 4;
148        let crc = P::compute_crc(value);
149        self.inner = (self.inner & !0x0F) | crc;
150    }
151
152    /// Returns the raw [`u16`].
153    pub fn inner(&self) -> u16 {
154        self.inner
155    }
156
157    /// Returns an array of duty cycles for use in PWM DMA.
158    ///
159    /// This contains an extra element that is always zero to ensure the PWM output gets pulled low
160    /// at the end of the sequence. It can be sliced off if not needed.
161    pub fn duty_cycles(&self, max_duty_cycle: u16) -> [u16; 17] {
162        let mut value = self.inner;
163        let mut rv = [max_duty_cycle * 3 / 4; 17];
164        for item in rv.iter_mut() {
165            let bit = value & 0x8000;
166            if bit == 0 {
167                *item = max_duty_cycle * 3 / 8;
168            }
169            value <<= 1;
170        }
171        rv[16] = 0;
172        rv
173    }
174}
175
176// Type aliases for convenience
177pub type NormalFrame = Frame<NormalDshot>;
178pub type BidirectionalFrame = Frame<BidirectionalDshot>;
179
180/// Fixed commands that occupy the lower 48 speed values.
181///
182/// Some commands need to be sent multiple times to be acted upon to prevent accidental bit-flips
183/// wreaking havoc.
184#[derive(Copy, Clone, Debug)]
185pub enum Command {
186    MotorStop = 0,
187    /// Wait at least 260ms before next command.
188    Beep1,
189    /// Wait at least 260ms before next command.
190    Beep2,
191    /// Wait at least 260ms before next command.
192    Beep3,
193    /// Wait at least 260ms before next command.
194    Beep4,
195    /// Wait at least 260ms before next command.
196    Beep5,
197    /// Wait at least 12ms before next command.
198    ESCInfo,
199    /// Needs 6 transmissions.
200    SpinDirection1,
201    /// Needs 6 transmissions.
202    SpinDirection2,
203    /// Needs 6 transmissions.
204    ThreeDModeOn,
205    /// Needs 6 transmissions.
206    ThreeDModeOff,
207    SettingsRequest,
208    /// Needs 6 transmissions. Wait at least 35ms before next command.
209    SettingsSave,
210    /// Needs 6 transmissions.
211    ExtendedTelemetryEnable,
212    /// Needs 6 transmissions.
213    ExtendedTelemetryDisable,
214
215    // 15-19 are unassigned.
216    /// Needs 6 transmissions.
217    SpinDirectionNormal = 20,
218    /// Needs 6 transmissions.
219    SpinDirectonReversed,
220    Led0On,
221    Led1On,
222    Led2On,
223    Led3On,
224    Led0Off,
225    Led1Off,
226    Led2Off,
227    Led3Off,
228    AudioStreamModeToggle,
229    SilentModeToggle,
230    /// Needs 6 transmissions. Enables individual signal line commands.
231    SignalLineTelemetryEnable,
232    /// Needs 6 transmissions. Disables individual signal line commands.
233    SignalLineTelemetryDisable,
234    /// Needs 6 transmissions. Enables individual signal line commands.
235    SignalLineContinuousERPMTelemetry,
236    /// Needs 6 transmissions. Enables individual signal line commands.
237    SignalLineContinuousERPMPeriodTelemetry,
238
239    // 36-41 are unassigned.
240    /// 1ºC per LSB.
241    SignalLineTemperatureTelemetry = 42,
242    /// 10mV per LSB, 40.95V max.
243    SignalLineVoltageTelemetry,
244    /// 100mA per LSB, 409.5A max.
245    SignalLineCurrentTelemetry,
246    /// 10mAh per LSB, 40.95Ah max.
247    SignalLineConsumptionTelemetry,
248    /// 100erpm per LSB, 409500erpm max.
249    SignalLineERPMTelemetry,
250    /// 16us per LSB, 65520us max.
251    SignalLineERPMPeriodTelemetry,
252}
253
254/// eRPM telemetry frame as sent by ESC in bidirectional DShot mode.
255///
256/// Despite being used in bidirectional mode, this frame is **not inverted**,
257/// and its CRC is computed using the standard (non-inverted) DShot algorithm.
258///
259/// Format (16 bits): `[3-bit shift][9-bit period_base][4-bit CRC]`
260#[derive(Copy, Clone, Debug, PartialEq)]
261pub struct ErpmTelemetry {
262    /// Raw 3-bit shift value (0–7)
263    pub shift: u8,
264    /// Raw 9-bit period base (0–511)
265    pub period_base: u16,
266    /// Raw 4-bit CRC (for debugging)
267    pub crc: u8,
268}
269
270impl ErpmTelemetry {
271    /// Attempts to parse a 16-bit raw telemetry value.
272    ///
273    /// Returns `None` if the CRC is invalid.
274    pub fn try_from_raw(raw: u16) -> Option<Self> {
275        let payload = raw >> 4; // upper 12 bits
276        let received_crc = (raw & 0x0F) as u8;
277        let expected_crc = NormalDshot::compute_crc(payload) as u8;
278
279        if received_crc != expected_crc {
280            return None;
281        }
282
283        let shift = ((payload >> 9) & 0x07) as u8;
284        let period_base = payload & 0x1FF;
285
286        Some(Self {
287            shift,
288            period_base,
289            crc: received_crc,
290        })
291    }
292
293    /// Returns the period in microseconds.
294    ///
295    /// If `period_base` is zero, the period is considered infinite (motor stopped).
296    pub fn period_us(&self) -> Option<u32> {
297        if self.period_base == 0 {
298            None // motor stopped
299        } else {
300            let period = (self.period_base as u32) << self.shift;
301            if period == 0 {
302                None
303            } else {
304                Some(period)
305            }
306        }
307    }
308
309    /// Returns the electrical RPM (eRPM).
310    ///
311    /// Computed as `60_000_000 / period_us`.
312    /// Returns `0` if the motor is stopped (`period_base == 0`).
313    pub fn erpm(&self) -> u32 {
314        match self.period_us() {
315            Some(period) if period > 0 => 60_000_000 / period,
316            _ => 0,
317        }
318    }
319
320    /// Returns the raw 16-bit value (for logging or retransmission).
321    pub fn to_raw(&self) -> u16 {
322        let payload = ((self.shift as u16) << 9) | (self.period_base & 0x1FF);
323        let crc = self.crc as u16;
324        (payload << 4) | crc
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    const MAX_DUTY_CYCLE: u16 = 100;
333    const ZERO: u16 = 37;
334    const ONE: u16 = 75;
335
336    #[test]
337    fn duty_cycles_works() {
338        let frame = NormalFrame::new(999, false).unwrap();
339        assert_eq!(
340            frame.duty_cycles(MAX_DUTY_CYCLE),
341            [
342                ONE, ZERO, ZERO, ZERO, ZERO, ZERO, ONE, ZERO, ONE, ONE, ONE, ZERO, ZERO, ONE, ZERO,
343                ZERO, 0
344            ]
345        );
346    }
347
348    #[test]
349    fn duty_cycles_at_zero() {
350        let frame = NormalFrame::command(Command::MotorStop, false);
351        assert_eq!(
352            frame.duty_cycles(MAX_DUTY_CYCLE),
353            [
354                ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO,
355                ZERO, ZERO, 0
356            ]
357        );
358    }
359
360    #[test]
361    fn frame_constructs_correctly() {
362        let frame = NormalFrame::new(998, false).unwrap();
363        assert_eq!(frame.speed(), 998);
364        assert!(!frame.telemetry_enabled());
365        assert_eq!(frame.crc(), 0x06);
366    }
367
368    #[test]
369    fn frame_constructs_correctly_with_telemetry() {
370        let frame = NormalFrame::new(998, true).unwrap();
371        assert_eq!(frame.speed(), 998);
372        assert!(frame.telemetry_enabled());
373        assert_eq!(frame.crc(), 0x07);
374    }
375
376    #[test]
377    fn frame_constructs_correctly_off_centre() {
378        let frame = NormalFrame::new(50, false).unwrap();
379        assert_eq!(frame.speed(), 50);
380    }
381
382    #[test]
383    fn frame_rejects_invalid_speed_values() {
384        assert!(NormalFrame::new(2000, false).is_none())
385    }
386
387    #[test]
388    fn bidirectional_throttle_works() {
389        let thr = BidirectionalDshot::get_translated_throttle(999);
390        assert_eq!(thr, 0b011_1110_1000_00000)
391    }
392}
393