Skip to main content

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
45/// Standard (non-inverted) DShot protocol.
46#[derive(Debug, Clone, Copy)]
47pub struct NormalDshot;
48
49impl DshotProtocol for NormalDshot {
50    fn compute_crc(value: u16) -> u16 {
51        (value ^ (value >> 4) ^ (value >> 8)) & 0x0F
52    }
53
54    fn is_inverted() -> bool {
55        false
56    }
57}
58
59/// Bidirectional (inverted) DShot protocol.
60#[derive(Debug, Clone, Copy)]
61pub struct BidirectionalDshot;
62
63impl DshotProtocol for BidirectionalDshot {
64    fn compute_crc(value: u16) -> u16 {
65        (!(value ^ (value >> 4) ^ (value >> 8))) & 0x0F
66    }
67
68    fn is_inverted() -> bool {
69        true
70    }
71}
72
73/// A DShot frame parameterized by its protocol variant.
74#[derive(Copy, Clone, Debug)]
75pub struct Frame<P: DshotProtocol = NormalDshot> {
76    inner: u16,
77    _protocol: core::marker::PhantomData<P>,
78}
79
80impl<P: DshotProtocol> Frame<P> {
81    /// Creates a new frame with the given speed (0-1999) and telemetry request.
82    ///
83    /// Returns [`None`] if the speed is out of bounds.
84    ///
85    /// ```
86    /// # use dshot_frame::*;
87    /// assert_eq!(Frame::<NormalDshot>::new(1000, false).unwrap().speed(), 1000);
88    /// ```
89    pub fn new(speed: u16, request_telemetry: bool) -> Option<Self> {
90        if speed >= 2000 {
91            return None;
92        }
93
94        let translated_throttle = (speed + 48) << 5;
95        let mut frame = Self {
96            inner: translated_throttle,
97            _protocol: core::marker::PhantomData,
98        };
99        if request_telemetry {
100            frame.inner |= 0x10;
101        }
102        frame.compute_crc();
103        Some(frame)
104    }
105
106    /// Creates a new frame with the given [`Command`] and telemetry request.
107    pub fn command(command: Command, request_telemetry: bool) -> Self {
108        let mut frame = Self {
109            inner: (command as u16) << 5,
110            _protocol: core::marker::PhantomData,
111        };
112        if request_telemetry {
113            frame.inner |= 0x10;
114        }
115        frame.compute_crc();
116        frame
117    }
118
119    /// Returns the speed value (0-1999).
120    pub fn speed(&self) -> u16 {
121        (self.inner >> 5) - 48
122    }
123
124    /// Returns whether telemetry is enabled.
125    pub fn telemetry_enabled(&self) -> bool {
126        self.inner & 0x10 != 0
127    }
128
129    /// Returns the CRC checksum.
130    pub fn crc(&self) -> u16 {
131        self.inner & 0x0F
132    }
133
134    /// Computes the CRC based on the first 12 bits and ORs it in.
135    fn compute_crc(&mut self) {
136        let value = self.inner >> 4;
137        let crc = P::compute_crc(value);
138        self.inner = (self.inner & !0x0F) | crc;
139    }
140
141    /// Returns the raw [`u16`].
142    pub fn inner(&self) -> u16 {
143        self.inner
144    }
145
146    /// Returns an array of duty cycles for use in PWM DMA.
147    ///
148    /// This contains an extra element that is always zero to ensure the PWM output gets pulled low
149    /// at the end of the sequence. It can be sliced off if not needed.
150    pub fn duty_cycles(&self, max_duty_cycle: u16) -> [u16; 17] {
151        let mut value = self.inner;
152        let mut rv = [max_duty_cycle * 2 / 3; 17];
153        for item in rv.iter_mut() {
154            let bit = value & 0x8000;
155            if bit == 0 {
156                *item = max_duty_cycle / 3;
157            }
158            value <<= 1;
159        }
160        rv[16] = 0;
161        rv
162    }
163}
164
165// Type aliases for convenience
166pub type NormalFrame = Frame<NormalDshot>;
167pub type BidirectionalFrame = Frame<BidirectionalDshot>;
168
169/// Fixed commands that occupy the lower 48 speed values.
170///
171/// Some commands need to be sent multiple times to be acted upon to prevent accidental bit-flips
172/// wreaking havoc.
173#[derive(Copy, Clone, Debug)]
174pub enum Command {
175    MotorStop = 0,
176    /// Wait at least 260ms before next command.
177    Beep1,
178    /// Wait at least 260ms before next command.
179    Beep2,
180    /// Wait at least 260ms before next command.
181    Beep3,
182    /// Wait at least 260ms before next command.
183    Beep4,
184    /// Wait at least 260ms before next command.
185    Beep5,
186    /// Wait at least 12ms before next command.
187    ESCInfo,
188    /// Needs 6 transmissions.
189    SpinDirection1,
190    /// Needs 6 transmissions.
191    SpinDirection2,
192    /// Needs 6 transmissions.
193    ThreeDModeOn,
194    /// Needs 6 transmissions.
195    ThreeDModeOff,
196    SettingsRequest,
197    /// Needs 6 transmissions. Wait at least 35ms before next command.
198    SettingsSave,
199    /// Needs 6 transmissions.
200    ExtendedTelemetryEnable,
201    /// Needs 6 transmissions.
202    ExtendedTelemetryDisable,
203
204    // 15-19 are unassigned.
205    /// Needs 6 transmissions.
206    SpinDirectionNormal = 20,
207    /// Needs 6 transmissions.
208    SpinDirectionReversed,
209    Led0On,
210    Led1On,
211    Led2On,
212    Led3On,
213    Led0Off,
214    Led1Off,
215    Led2Off,
216    Led3Off,
217    AudioStreamModeToggle,
218    SilentModeToggle,
219    /// Needs 6 transmissions. Enables individual signal line commands.
220    SignalLineTelemetryEnable,
221    /// Needs 6 transmissions. Disables individual signal line commands.
222    SignalLineTelemetryDisable,
223    /// Needs 6 transmissions. Enables individual signal line commands.
224    SignalLineContinuousERPMTelemetry,
225    /// Needs 6 transmissions. Enables individual signal line commands.
226    SignalLineContinuousERPMPeriodTelemetry,
227
228    // 36-41 are unassigned.
229    /// 1ºC per LSB.
230    SignalLineTemperatureTelemetry = 42,
231    /// 10mV per LSB, 40.95V max.
232    SignalLineVoltageTelemetry,
233    /// 100mA per LSB, 409.5A max.
234    SignalLineCurrentTelemetry,
235    /// 10mAh per LSB, 40.95Ah max.
236    SignalLineConsumptionTelemetry,
237    /// 100erpm per LSB, 409500erpm max.
238    SignalLineERPMTelemetry,
239    /// 16us per LSB, 65520us max.
240    SignalLineERPMPeriodTelemetry,
241}
242
243/// eRPM telemetry frame as sent by ESC in bidirectional DShot mode.
244///
245/// Despite being used in bidirectional mode, this frame is **not inverted**,
246/// and its CRC is computed using the standard (non-inverted) DShot algorithm.
247///
248/// Format (16 bits): `[3-bit shift][9-bit period_base][4-bit CRC]`
249#[derive(Copy, Clone, Debug, PartialEq)]
250pub struct ErpmTelemetry {
251    /// Raw 3-bit shift value (0–7)
252    shift: u8,
253    /// Raw 9-bit period base (0–511)
254    period_base: u16,
255    /// Raw 4-bit CRC (for debugging)
256    crc: u8,
257}
258
259impl ErpmTelemetry {
260    /// Returns the 3-bit shift value (0-7).
261    pub fn shift(&self) -> u8 {
262        self.shift
263    }
264
265    /// Returns the 9-bit period base (0-511).
266    pub fn period_base(&self) -> u16 {
267        self.period_base
268    }
269
270    /// Returns the 4-bit CRC.
271    pub fn crc(&self) -> u8 {
272        self.crc
273    }
274
275    /// Sets the 3-bit shift value (0-7). Recalculates the CRC accordingly.
276    ///
277    /// # Panics
278    ///
279    /// Panics if the shift value is out of bounds (> 7).
280    pub fn set_shift(&mut self, shift: u8) {
281        assert!(shift <= 7, "Shift value needs to be within 0..=7");
282
283        self.shift = shift;
284        self.crc = NormalDshot::compute_crc((self.shift as u16) << 9 | self.period_base) as u8;
285    }
286
287    /// Sets the 9-bit period-base (0-511). Recalculates the CRC accordingly.
288    ///
289    /// # Panics
290    ///
291    /// Panics if the period base is out of bounds (> 511).
292    pub fn set_period_base(&mut self, period_base: u16) {
293        assert!(period_base <= 511, "Period base needs to be within 0..=511");
294
295        self.period_base = period_base;
296        self.crc = NormalDshot::compute_crc((self.shift as u16) << 9 | self.period_base) as u8;
297    }
298
299    /// Attempts to parse a 16-bit raw telemetry value.
300    ///
301    /// Returns `None` if the CRC is invalid.
302    pub fn try_from_raw(raw: u16) -> Option<Self> {
303        let payload = raw >> 4; // upper 12 bits
304        let received_crc = (raw & 0x0F) as u8;
305        let expected_crc = NormalDshot::compute_crc(payload) as u8;
306
307        if received_crc != expected_crc {
308            return None;
309        }
310
311        let shift = ((payload >> 9) & 0x07) as u8;
312        let period_base = payload & 0x1FF;
313
314        Some(Self {
315            shift,
316            period_base,
317            crc: received_crc,
318        })
319    }
320
321    /// Returns the period in microseconds.
322    ///
323    /// If `period_base` is zero, the period is considered infinite (motor stopped).
324    pub fn period_us(&self) -> Option<u32> {
325        if self.period_base == 0 {
326            None // motor stopped
327        } else {
328            let period = (self.period_base as u32) << self.shift;
329            if period == 0 {
330                None
331            } else {
332                Some(period)
333            }
334        }
335    }
336
337    /// Returns the electrical RPM (eRPM).
338    ///
339    /// Computed as `60_000_000 / period_us`.
340    /// Returns `0` if the motor is stopped (`period_base == 0`).
341    pub fn erpm(&self) -> u32 {
342        match self.period_us() {
343            Some(period) if period > 0 => 60_000_000 / period,
344            _ => 0,
345        }
346    }
347
348    /// Returns the raw 16-bit value (for logging or retransmission).
349    pub fn to_raw(&self) -> u16 {
350        let payload = ((self.shift as u16) << 9) | (self.period_base & 0x1FF);
351        let crc = self.crc as u16;
352        (payload << 4) | crc
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    const MAX_DUTY_CYCLE: u16 = 100;
361    const ZERO: u16 = MAX_DUTY_CYCLE / 3;
362    const ONE: u16 = ZERO * 2;
363
364    #[test]
365    fn duty_cycles_works() {
366        let frame = NormalFrame::new(999, false).unwrap();
367        assert_eq!(
368            frame.duty_cycles(MAX_DUTY_CYCLE),
369            [
370                ONE, ZERO, ZERO, ZERO, ZERO, ZERO, ONE, ZERO, ONE, ONE, ONE, ZERO, ZERO, ONE, ZERO,
371                ZERO, 0
372            ]
373        );
374    }
375
376    #[test]
377    fn duty_cycles_at_zero() {
378        let frame = NormalFrame::command(Command::MotorStop, false);
379        assert_eq!(
380            frame.duty_cycles(MAX_DUTY_CYCLE),
381            [
382                ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO,
383                ZERO, ZERO, 0
384            ]
385        );
386    }
387
388    #[test]
389    fn frame_constructs_correctly() {
390        let frame = NormalFrame::new(998, false).unwrap();
391        assert_eq!(frame.speed(), 998);
392        assert!(!frame.telemetry_enabled());
393        assert_eq!(frame.crc(), 0x06);
394    }
395
396    #[test]
397    fn frame_constructs_correctly_with_telemetry() {
398        let frame = NormalFrame::new(998, true).unwrap();
399        assert_eq!(frame.speed(), 998);
400        assert!(frame.telemetry_enabled());
401        assert_eq!(frame.crc(), 0x07);
402    }
403
404    #[test]
405    fn frame_constructs_correctly_off_centre() {
406        let frame = NormalFrame::new(50, false).unwrap();
407        assert_eq!(frame.speed(), 50);
408    }
409
410    #[test]
411    fn frame_rejects_invalid_speed_values() {
412        assert!(NormalFrame::new(2000, false).is_none())
413    }
414
415    #[test]
416    fn telemetry_setters_recompute_crc() {
417        let mut telemetry = ErpmTelemetry::try_from_raw(0x4646).unwrap();
418
419        telemetry.set_shift(5);
420        assert_eq!(
421            telemetry.crc(),
422            NormalDshot::compute_crc((telemetry.shift() as u16) << 9 | telemetry.period_base())
423                as u8
424        );
425
426        telemetry.set_period_base(42);
427        assert_eq!(
428            telemetry.crc(),
429            NormalDshot::compute_crc((telemetry.shift() as u16) << 9 | telemetry.period_base())
430                as u8
431        );
432    }
433
434    #[test]
435    fn telemetry_setters_preserve_round_trip() {
436        let mut telemetry = ErpmTelemetry::try_from_raw(0x4646).unwrap();
437        telemetry.set_shift(5);
438        telemetry.set_period_base(42);
439        assert_eq!(ErpmTelemetry::try_from_raw(telemetry.to_raw()), Some(telemetry));
440    }
441
442    #[test]
443    #[should_panic(expected = "Shift value needs to be within 0..=7")]
444    fn telemetry_set_shift_panics_on_out_of_bounds() {
445        let mut telemetry = ErpmTelemetry::try_from_raw(0x4646).unwrap();
446        telemetry.set_shift(8);
447    }
448
449    #[test]
450    #[should_panic(expected = "Period base needs to be within 0..=511")]
451    fn telemetry_set_period_base_panics_on_out_of_bounds() {
452        let mut telemetry = ErpmTelemetry::try_from_raw(0x4646).unwrap();
453        telemetry.set_period_base(512);
454    }
455
456    #[test]
457    fn telemetry_setters_update_values() {
458        let mut telemetry = ErpmTelemetry::try_from_raw(0x4646).unwrap();
459        telemetry.set_shift(5);
460        assert_eq!(telemetry.shift(), 5);
461        telemetry.set_period_base(42);
462        assert_eq!(telemetry.period_base(), 42);
463    }
464
465    #[test]
466    fn bidir_duty_cycles_works() {
467        let frame = BidirectionalFrame::new(998, false).unwrap();
468        assert_eq!(
469            frame.duty_cycles(MAX_DUTY_CYCLE),
470            [
471                ONE, ZERO, ZERO, ZERO, ZERO, ZERO, ONE, ZERO, ONE, ONE, ZERO, ZERO, ONE, ZERO,
472                ZERO, ONE, 0
473            ]
474        );
475    }
476}