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    SpinDirectonReversed,
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    pub shift: u8,
253    /// Raw 9-bit period base (0–511)
254    pub period_base: u16,
255    /// Raw 4-bit CRC (for debugging)
256    pub crc: u8,
257}
258
259impl ErpmTelemetry {
260    /// Attempts to parse a 16-bit raw telemetry value.
261    ///
262    /// Returns `None` if the CRC is invalid.
263    pub fn try_from_raw(raw: u16) -> Option<Self> {
264        let payload = raw >> 4; // upper 12 bits
265        let received_crc = (raw & 0x0F) as u8;
266        let expected_crc = NormalDshot::compute_crc(payload) as u8;
267
268        if received_crc != expected_crc {
269            return None;
270        }
271
272        let shift = ((payload >> 9) & 0x07) as u8;
273        let period_base = payload & 0x1FF;
274
275        Some(Self {
276            shift,
277            period_base,
278            crc: received_crc,
279        })
280    }
281
282    /// Returns the period in microseconds.
283    ///
284    /// If `period_base` is zero, the period is considered infinite (motor stopped).
285    pub fn period_us(&self) -> Option<u32> {
286        if self.period_base == 0 {
287            None // motor stopped
288        } else {
289            let period = (self.period_base as u32) << self.shift;
290            if period == 0 {
291                None
292            } else {
293                Some(period)
294            }
295        }
296    }
297
298    /// Returns the electrical RPM (eRPM).
299    ///
300    /// Computed as `60_000_000 / period_us`.
301    /// Returns `0` if the motor is stopped (`period_base == 0`).
302    pub fn erpm(&self) -> u32 {
303        match self.period_us() {
304            Some(period) if period > 0 => 60_000_000 / period,
305            _ => 0,
306        }
307    }
308
309    /// Returns the raw 16-bit value (for logging or retransmission).
310    pub fn to_raw(&self) -> u16 {
311        let payload = ((self.shift as u16) << 9) | (self.period_base & 0x1FF);
312        let crc = self.crc as u16;
313        (payload << 4) | crc
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    const MAX_DUTY_CYCLE: u16 = 100;
322    const ZERO: u16 = MAX_DUTY_CYCLE / 3;
323    const ONE: u16 = ZERO * 2;
324
325    #[test]
326    fn duty_cycles_works() {
327        let frame = NormalFrame::new(999, false).unwrap();
328        assert_eq!(
329            frame.duty_cycles(MAX_DUTY_CYCLE),
330            [
331                ONE, ZERO, ZERO, ZERO, ZERO, ZERO, ONE, ZERO, ONE, ONE, ONE, ZERO, ZERO, ONE, ZERO,
332                ZERO, 0
333            ]
334        );
335    }
336
337    #[test]
338    fn duty_cycles_at_zero() {
339        let frame = NormalFrame::command(Command::MotorStop, false);
340        assert_eq!(
341            frame.duty_cycles(MAX_DUTY_CYCLE),
342            [
343                ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO,
344                ZERO, ZERO, 0
345            ]
346        );
347    }
348
349    #[test]
350    fn frame_constructs_correctly() {
351        let frame = NormalFrame::new(998, false).unwrap();
352        assert_eq!(frame.speed(), 998);
353        assert!(!frame.telemetry_enabled());
354        assert_eq!(frame.crc(), 0x06);
355    }
356
357    #[test]
358    fn frame_constructs_correctly_with_telemetry() {
359        let frame = NormalFrame::new(998, true).unwrap();
360        assert_eq!(frame.speed(), 998);
361        assert!(frame.telemetry_enabled());
362        assert_eq!(frame.crc(), 0x07);
363    }
364
365    #[test]
366    fn frame_constructs_correctly_off_centre() {
367        let frame = NormalFrame::new(50, false).unwrap();
368        assert_eq!(frame.speed(), 50);
369    }
370
371    #[test]
372    fn frame_rejects_invalid_speed_values() {
373        assert!(NormalFrame::new(2000, false).is_none())
374    }
375
376    #[test]
377    fn bidir_duty_cycles_works() {
378        let frame = BidirectionalFrame::new(998, false).unwrap();
379        assert_eq!(
380            frame.duty_cycles(MAX_DUTY_CYCLE),
381            [
382                ONE, ZERO, ZERO, ZERO, ZERO, ZERO, ONE, ZERO, ONE, ONE, ZERO, ZERO, ONE, ZERO,
383                ZERO, ONE, 0
384            ]
385        );
386    }
387}