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::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// TODO Bidirectional DShot
35
36#![no_std]
37
38/// A frame of two bytes that get send over the wire.
39#[derive(Copy, Clone, Debug)]
40pub struct Frame {
41    inner: u16,
42}
43
44impl Frame {
45    /// Creates a new frame with the given speed (0-1999) and telemetry request.
46    ///
47    /// Returns [`None`] if the speed is out of bounds.
48    ///
49    /// ```
50    /// # use dshot_frame::*;
51    /// assert_eq!(Frame::new(1000, false).unwrap().speed(), 1000);
52    /// ```
53    pub fn new(speed: u16, request_telemetry: bool) -> Option<Self> {
54        if speed >= 2000 {
55            return None;
56        }
57
58        let translated_throttle = (speed + 48) << 5;
59        let mut frame = Self {
60            inner: translated_throttle,
61        };
62        if request_telemetry {
63            frame.inner |= 0x10;
64        }
65        frame.compute_crc();
66        Some(frame)
67    }
68
69    /// Creates a new frame with the given [`Command`] and telemetry request.
70    pub fn command(command: Command, request_telemetry: bool) -> Self {
71        let mut frame = Self {
72            inner: (command as u16) << 5,
73        };
74        if request_telemetry {
75            frame.inner |= 0x10;
76        }
77        frame.compute_crc();
78        frame
79    }
80
81    /// Returns the speed value (0-1999).
82    pub fn speed(&self) -> u16 {
83        (self.inner >> 5) - 48
84    }
85
86    /// Returns whether telemetry is enabled.
87    pub fn telemetry_enabled(&self) -> bool {
88        self.inner & 0x10 != 0
89    }
90
91    /// Returns the CRC checksum.
92    pub fn crc(&self) -> u16 {
93        self.inner & 0x0F
94    }
95
96    /// Computes the CRC based on the first 12 bits and ORs it in.
97    fn compute_crc(&mut self) {
98        let value = self.inner >> 4;
99        let crc = (value ^ (value >> 4) ^ (value >> 8)) & 0x0F;
100        self.inner |= crc;
101    }
102
103    /// Returns the raw [`u16`].
104    pub fn inner(&self) -> u16 {
105        self.inner
106    }
107
108    /// Returns an array of duty cycles for use in PWM DMA.
109    ///
110    /// This contains an extra element that is always zero to ensure the PWM output gets pulled low
111    /// at the end of the sequence. It can be sliced off if not needed.
112    pub fn duty_cycles(&self, max_duty_cycle: u16) -> [u16; 17] {
113        let mut value = self.inner;
114        let mut rv = [max_duty_cycle * 3 / 4; 17];
115        for item in rv.iter_mut() {
116            let bit = value & 0x8000;
117            if bit == 0 {
118                *item = max_duty_cycle * 3 / 8;
119            }
120            value <<= 1;
121        }
122        rv[16] = 0;
123        rv
124    }
125}
126
127/// Fixed commands that occupy the lower 48 speed values.
128///
129/// Some commands need to be sent multiple times to be acted upon to prevent accidental bit-flips
130/// wreaking havoc.
131#[derive(Copy, Clone, Debug)]
132pub enum Command {
133    MotorStop = 0,
134    /// Wait at least 260ms before next command.
135    Beep1,
136    /// Wait at least 260ms before next command.
137    Beep2,
138    /// Wait at least 260ms before next command.
139    Beep3,
140    /// Wait at least 260ms before next command.
141    Beep4,
142    /// Wait at least 260ms before next command.
143    Beep5,
144    /// Wait at least 12ms before next command.
145    ESCInfo,
146    /// Needs 6 transmissions.
147    SpinDirection1,
148    /// Needs 6 transmissions.
149    SpinDirection2,
150    /// Needs 6 transmissions.
151    ThreeDModeOn,
152    /// Needs 6 transmissions.
153    ThreeDModeOff,
154    SettingsRequest,
155    /// Needs 6 transmissions. Wait at least 35ms before next command.
156    SettingsSave,
157    /// Needs 6 transmissions.
158    ExtendedTelemetryEnable,
159    /// Needs 6 transmissions.
160    ExtendedTelemetryDisable,
161
162    // 15-19 are unassigned.
163    /// Needs 6 transmissions.
164    SpinDirectionNormal = 20,
165    /// Needs 6 transmissions.
166    SpinDirectonReversed,
167    Led0On,
168    Led1On,
169    Led2On,
170    Led3On,
171    Led0Off,
172    Led1Off,
173    Led2Off,
174    Led3Off,
175    AudioStreamModeToggle,
176    SilentModeToggle,
177    /// Needs 6 transmissions. Enables individual signal line commands.
178    SignalLineTelemetryEnable,
179    /// Needs 6 transmissions. Disables individual signal line commands.
180    SignalLineTelemetryDisable,
181    /// Needs 6 transmissions. Enables individual signal line commands.
182    SignalLineContinuousERPMTelemetry,
183    /// Needs 6 transmissions. Enables individual signal line commands.
184    SignalLineContinuousERPMPeriodTelemetry,
185
186    // 36-41 are unassigned.
187    /// 1ÂșC per LSB.
188    SignalLineTemperatureTelemetry = 42,
189    /// 10mV per LSB, 40.95V max.
190    SignalLineVoltageTelemetry,
191    /// 100mA per LSB, 409.5A max.
192    SignalLineCurrentTelemetry,
193    /// 10mAh per LSB, 40.95Ah max.
194    SignalLineConsumptionTelemetry,
195    /// 100erpm per LSB, 409500erpm max.
196    SignalLineERPMTelemetry,
197    /// 16us per LSB, 65520us max.
198    SignalLineERPMPeriodTelemetry,
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    const MAX_DUTY_CYCLE: u16 = 100;
206    const ZERO: u16 = 37;
207    const ONE: u16 = 75;
208
209    #[test]
210    fn duty_cycles_works() {
211        let frame = Frame::new(999, false).unwrap();
212        assert_eq!(
213            frame.duty_cycles(MAX_DUTY_CYCLE),
214            [
215                ONE, ZERO, ZERO, ZERO, ZERO, ZERO, ONE, ZERO, ONE, ONE, ONE, ZERO, ZERO, ONE, ZERO,
216                ZERO, 0
217            ]
218        );
219    }
220
221    #[test]
222    fn duty_cycles_at_zero() {
223        let frame = Frame::command(Command::MotorStop, false);
224        assert_eq!(
225            frame.duty_cycles(MAX_DUTY_CYCLE),
226            [
227                ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO,
228                ZERO, ZERO, 0
229            ]
230        );
231    }
232
233    #[test]
234    fn frame_constructs_correctly() {
235        let frame = Frame::new(998, false).unwrap();
236        assert_eq!(frame.speed(), 998);
237        assert!(!frame.telemetry_enabled());
238        assert_eq!(frame.crc(), 0x06);
239    }
240
241    #[test]
242    fn frame_constructs_correctly_with_telemetry() {
243        let frame = Frame::new(998, true).unwrap();
244        assert_eq!(frame.speed(), 998);
245        assert!(frame.telemetry_enabled());
246        assert_eq!(frame.crc(), 0x07);
247    }
248
249    #[test]
250    fn frame_constructs_correctly_off_centre() {
251        let frame = Frame::new(50, false).unwrap();
252        assert_eq!(frame.speed(), 50);
253    }
254
255    #[test]
256    fn frame_rejects_invalid_speed_values() {
257        assert!(Frame::new(2000, false).is_none())
258    }
259}