Skip to main content

dshot_codec/
dshot_command_frame.rs

1use core::ops::Deref;
2
3use super::DshotCommand;
4
5/// `DshotCommandFrame`: transmitted from the Flight Controller(FC) to the ESC.
6///
7/// Whenever the FC wants the motor to spin, beep, or change direction, it transmits a 16-bit `DshotCommandFrame`.<br>
8/// Bits 0–10 (11 bits): Throttle value or Command, values 48 to 2047 for motor speed (throttle), values 1 to 47 for commands.<br>
9/// Bit 11     (1 bit):  Telemetry Request Flag.<br>
10/// Bits 12–15 (4 bits): Checksum (technically a 4-bit Longitudinal Redundancy Check (LRC)).<br>
11///
12/// | Operational Aspect     | Unidirectional (Throttle)           | Unidirectional (Commands)                 | Bidirectional (Throttle & Commands)                |
13/// | :--------------------- | :---------------------------------- | :---------------------------------------- | :------------------------------------------------- |
14/// | **Telemetry Bit**      | **`false`** (Set to `0`)            | **`true`** (Set to `1`)                   | **`true`** (Set to `1`)                            |
15/// | **XOR Checksum Mode**  | **Standard**                        | **Bitwise Inverted**                      | **Bitwise Inverted**                               |
16/// | **ESC Action**         | Executes throttle<br>Remains silent | Executes command<br>Returns a ghost reply | Executes command<br>Returns a telemetry frame      |
17/// | **FC Pin Mode**        | Permanent **Output**                | Permanent **Output**                      | Flips from **Output to Input** right after TX      |
18/// | **Repetition Gate**    | Streams continuously                | **Must repeat ~10 times** to execute      | Commands **must repeat ~10 times** to execute      |
19/// | **FC Software Action** | Fire-and-forget stream              | Fire-and-forget stream                    | Transmits, then pauses ~30µs to capture `GcrFrame` |
20#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, PartialOrd, Ord)]
21pub struct DshotCommandFrame(u16);
22
23impl TryFrom<u16> for DshotCommandFrame {
24    type Error = u16;
25
26    #[inline]
27    fn try_from(value: u16) -> Result<Self, u16> {
28        if value <= Self::MAX_RAW_VALUE {
29            Ok(DshotCommandFrame::encode_raw(value, DshotCommandFrame::NO_TELEMETRY))
30        } else {
31            Err(value)
32        }
33    }
34}
35
36impl From<DshotCommandFrame> for u16 {
37    #[inline]
38    fn from(frame: DshotCommandFrame) -> Self {
39        frame.raw()
40    }
41}
42
43impl Deref for DshotCommandFrame {
44    type Target = u16;
45
46    #[inline]
47    fn deref(&self) -> &Self::Target {
48        &self.0
49    }
50}
51impl DshotCommandFrame {
52    pub const NO_TELEMETRY: bool = false;
53    pub const WITH_TELEMETRY: bool = true;
54
55    pub const UNI_DIRECTIONAL: bool = false;
56    pub const BI_DIRECTIONAL: bool = true;
57
58    // `Dshot` command/throttle payload is 11 bits (0 to 2047)
59    pub const MAX_RAW_VALUE: u16 = 2047;
60    pub const THROTTLE_OFFSET: u16 = 48;
61    pub const THROTTLE_MIN: u16 = 48;
62    pub const THROTTLE_MAX: u16 = 2047;
63
64    const TELEMETRY_BIT: u16 = 0x10;
65    const CHECKSUM_BITS: u16 = 0x0F;
66
67    // 4-bit to 5-bit GCR translation table.
68    pub(crate) const NIBBLE_TO_QUINTET: [u8; 16] =
69        [0x19, 0x1B, 0x12, 0x13, 0x1D, 0x15, 0x16, 0x17, 0x1A, 0x09, 0x0A, 0x0B, 0x1E, 0x0D, 0x0E, 0x0F];
70
71    #[inline]
72    #[must_use]
73    pub const fn new(value: u16) -> Self {
74        Self::encode_raw(value, Self::NO_TELEMETRY)
75    }
76
77    #[inline]
78    #[must_use]
79    pub const fn from_raw(value: u16) -> Self {
80        Self(value)
81    }
82
83    #[inline]
84    #[must_use]
85    pub const fn from_command(command: DshotCommand) -> Self {
86        // commands set the telemetry bit whether in unidirectional or bidirectional mode.
87        Self::encode_raw(command as u16, Self::WITH_TELEMETRY)
88    }
89
90    #[inline]
91    #[must_use]
92    pub const fn raw(self) -> u16 {
93        self.0
94    }
95
96    /// Extracts the original 11-bit command/throttle value from the processed 16-bit frame.
97    #[inline]
98    #[must_use]
99    pub const fn value(self) -> u16 {
100        self.0 >> 5
101    }
102
103    #[inline]
104    #[must_use]
105    pub const fn is_telemetry_enabled(self) -> bool {
106        (self.0 & Self::TELEMETRY_BIT) != 0
107    }
108
109    #[inline]
110    #[must_use]
111    pub const fn checksum(self) -> u16 {
112        self.0 & Self::CHECKSUM_BITS
113    }
114
115    /// Calculates the standard 4-bit XOR outbound checksum.
116    #[inline]
117    #[must_use]
118    pub const fn calculate_checksum(frame_raw: u16) -> u16 {
119        (frame_raw ^ (frame_raw >> 4) ^ (frame_raw >> 8)) & 0x0F
120    }
121
122    /// Assembles an 11-bit command and a telemetry flag into a complete 16-bit `Dshot` transmission word.
123    #[must_use]
124    pub const fn encode_raw(value: u16, with_telemetry: bool) -> Self {
125        // Clamp input value to prevent register overflow corruption
126        let value = if value > Self::MAX_RAW_VALUE { Self::MAX_RAW_VALUE } else { value };
127
128        // Shift left by 1 and inject the telemetry selection bit
129        let frame_raw = if with_telemetry { (value << 1) | 0x01 } else { value << 1 };
130
131        // Calculate the base XOR checksum
132        let mut checksum = Self::calculate_checksum(frame_raw);
133
134        // Both Unidirectional and Bidirectional DShot require the checksum to be inverted when the telemetry bit is set.
135        if with_telemetry {
136            checksum = (!checksum) & 0x0F;
137        }
138        Self((frame_raw << 4) | checksum)
139    }
140
141    /// Converts a throttle scale `[0.0, 1.0]` directly to the `Dshot` frame range `[48, 2047]`.
142    /// In unidirectional mode the telemetry bit is not set and the checksum is not inverted.
143    #[must_use]
144    pub fn from_throttle_unidirectional(throttle: f32) -> Self {
145        #[allow(unused)]
146        use num_traits::float::FloatCore;
147
148        // Clamp throttle to prevent out-of-bounds calculations
149        let throttle = throttle.clamp(0.0, 1.0);
150
151        // Scale linearly across the available 1999 active throttle steps
152        let range = f32::from(Self::THROTTLE_MAX - Self::THROTTLE_MIN);
153        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
154        let dshot_value = (throttle * range).round() as u16 + Self::THROTTLE_MIN;
155
156        // For unidirectional frames the telemetry bit IS NOT set.
157        Self::encode_raw(dshot_value, Self::NO_TELEMETRY)
158    }
159
160    /// Converts a throttle scale `[0.0, 1.0]` directly to the `Dshot` frame range `[48, 2047]`.
161    /// In bidirectional mode the telemetry bit is not set and the checksum is not inverted.
162    #[must_use]
163    pub fn from_throttle_bidirectional(throttle: f32) -> Self {
164        #[allow(unused)]
165        use num_traits::float::FloatCore;
166
167        // Clamp throttle to prevent out-of-bounds calculations
168        let throttle = throttle.clamp(0.0, 1.0);
169
170        // Scale linearly across the available 1999 active throttle steps
171        let range = f32::from(Self::THROTTLE_MAX - Self::THROTTLE_MIN);
172        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
173        let dshot_value = (throttle * range).round() as u16 + Self::THROTTLE_MIN;
174
175        // For bidirectional frames the telemetry bit IS set.
176        Self::encode_raw(dshot_value, Self::WITH_TELEMETRY)
177    }
178
179    #[inline]
180    #[must_use]
181    pub fn from_throttle(throttle: f32, bidirectional: bool) -> Self {
182        if bidirectional {
183            Self::from_throttle_bidirectional(throttle)
184        } else {
185            Self::from_throttle_unidirectional(throttle)
186        }
187    }
188}
189
190impl DshotCommandFrame {
191    // see [DSHOT - the missing Handbook](https://brushlesswhoop.com/dshot-and-bidirectional-dshot/)
192    // for a good description of these conversions
193    #[inline]
194    #[must_use]
195    pub fn to_gcr20(self) -> u32 {
196        let value = self.0;
197        let mut ret = u32::from(Self::NIBBLE_TO_QUINTET[(value & 0x0F) as usize]);
198        ret |= u32::from(Self::NIBBLE_TO_QUINTET[((value >> 4) & 0x0F) as usize]) << 5;
199        ret |= u32::from(Self::NIBBLE_TO_QUINTET[((value >> 8) & 0x0F) as usize]) << 10;
200        ret |= u32::from(Self::NIBBLE_TO_QUINTET[((value >> 12) & 0x0F) as usize]) << 15;
201        ret
202    }
203
204    /// Map the GCR to a 21-bit NRZI value, this new value starts with a 0 and the rest of the bits are set by the following two rules:
205    ///    1. If the current input bit in GCR data is a 1 then the output bit is the inverse of the previous output bit
206    ///    2. If the current input bit in GCR data is a 0 then the output bit is the same as the previous output
207    #[must_use]
208    pub fn gcr20_to_nrzi21(input: u32) -> u32 {
209        let mut ret = 0;
210        let mut prev_gcr_bit = 0;
211        let mut mask = 1 << 19;
212
213        while mask != 0 {
214            ret <<= 1;
215            let input_bit = u32::from((input & mask) != 0);
216            let gcr_bit = input_bit ^ prev_gcr_bit;
217            prev_gcr_bit = gcr_bit;
218            ret |= gcr_bit;
219            mask >>= 1;
220        }
221        ret
222    }
223
224    #[must_use]
225    pub fn gcr_encode(self) -> u32 {
226        let gcr20 = self.to_gcr20();
227        Self::gcr20_to_nrzi21(gcr20)
228    }
229}
230
231#[cfg(test)]
232mod test_traits {
233    use super::*;
234
235    fn is_full<T: Sized + Send + Sync + Unpin + Copy + Clone + Default + PartialEq>() {}
236
237    #[test]
238    fn normal_types() {
239        is_full::<DshotCommandFrame>();
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn checksum() {
249        assert_eq!(DshotCommandFrame::calculate_checksum(0b_1000_0010_1100), 0b_0000_0000_0110,);
250    }
251    #[test]
252    fn throttle_unidirectional() {
253        let frame = DshotCommandFrame::from_throttle_unidirectional(0.0);
254        assert_eq!(48, frame.value());
255        assert!(!frame.is_telemetry_enabled());
256
257        let frame = DshotCommandFrame::from_throttle_unidirectional(0.25);
258        assert_eq!(548, frame.value());
259        assert!(!frame.is_telemetry_enabled());
260
261        let frame = DshotCommandFrame::from_throttle_unidirectional(0.50);
262        assert_eq!(1048, frame.value());
263        assert!(!frame.is_telemetry_enabled());
264
265        let frame = DshotCommandFrame::from_throttle_unidirectional(0.75);
266        assert_eq!(1547, frame.value());
267        assert!(!frame.is_telemetry_enabled());
268
269        let frame = DshotCommandFrame::from_throttle_unidirectional(1.00);
270        assert_eq!(2047, frame.value());
271        assert!(!frame.is_telemetry_enabled());
272    }
273    #[test]
274    fn throttle_bidirectional() {
275        let frame = DshotCommandFrame::from_throttle_bidirectional(0.0);
276        assert_eq!(48, frame.value());
277        assert!(frame.is_telemetry_enabled());
278
279        let frame = DshotCommandFrame::from_throttle_bidirectional(0.25);
280        assert_eq!(548, frame.value());
281        assert!(frame.is_telemetry_enabled());
282
283        let frame = DshotCommandFrame::from_throttle_bidirectional(0.50);
284        assert_eq!(1048, frame.value());
285        assert!(frame.is_telemetry_enabled());
286
287        let frame = DshotCommandFrame::from_throttle_bidirectional(0.75);
288        assert_eq!(1547, frame.value());
289        assert!(frame.is_telemetry_enabled());
290
291        let frame = DshotCommandFrame::from_throttle_bidirectional(1.00);
292        assert_eq!(2047, frame.value());
293        assert!(frame.is_telemetry_enabled());
294    }
295    #[test]
296    fn throttle() {
297        let frame = DshotCommandFrame::from_throttle(0.25, DshotCommandFrame::UNI_DIRECTIONAL);
298        assert_eq!(548, frame.value());
299        assert!(!frame.is_telemetry_enabled());
300
301        let frame = DshotCommandFrame::from_throttle(0.75, DshotCommandFrame::UNI_DIRECTIONAL);
302        assert_eq!(1547, frame.value());
303        assert!(!frame.is_telemetry_enabled());
304
305        let frame = DshotCommandFrame::from_throttle(0.25, DshotCommandFrame::BI_DIRECTIONAL);
306        assert_eq!(548, frame.value());
307        assert!(frame.is_telemetry_enabled());
308
309        let frame = DshotCommandFrame::from_throttle(0.75, DshotCommandFrame::BI_DIRECTIONAL);
310        assert_eq!(1547, frame.value());
311        assert!(frame.is_telemetry_enabled());
312    }
313    #[rustfmt::skip]
314    #[test]
315    fn commands() {
316        let frame = DshotCommandFrame::from_command(DshotCommand::Beep1);
317        assert_eq!(0b_0000_0000_0011_1100, frame.raw());
318
319        let frame = DshotCommandFrame::from_command(DshotCommand::SignalLineErpmTelemetry);
320        assert_eq!(0b_0000_0101_1101_0111, frame.raw());
321
322        let frame = DshotCommandFrame::from_command(DshotCommand::SignalLineErpmPeriodTelemetry);
323        assert_eq!(0b_0000_0101_1111_0101, frame.raw());
324    }
325}
326#[cfg(test)]
327mod command_frame_tests {
328    #![allow(clippy::unwrap_used)]
329    use super::*;
330
331    #[test]
332    fn test_encode_raw_no_telemetry() {
333        // Use a standard throttle value: 1000
334        // 1. Shift left by 1 for telemetry bit (0): 1000 << 1 = 2000 (0x7D0)
335        // 2. Calculate checksum:
336        //    nibble0 = 0x0, nibble1 = 0xD, nibble2 = 0x7
337        //    0x0 ^ 0xD ^ 0x7 = 0xA
338        // 3. Shift payload left by 4 and add checksum: (2000 << 4) | 0xA = 32010 (0x7D0A)
339        let frame = DshotCommandFrame::encode_raw(1000, DshotCommandFrame::NO_TELEMETRY);
340
341        assert_eq!(frame.raw(), 0x7D0A);
342        assert_eq!(frame.value(), 1000);
343        assert!(!frame.is_telemetry_enabled());
344        assert_eq!(frame.checksum(), 0x0A);
345    }
346
347    #[test]
348    fn test_encode_raw_with_telemetry() {
349        // Use the same throttle value (1000) but enable bidirectional telemetry
350        // 1. Shift left by 1 and inject telemetry bit (1): (1000 << 1) | 1 = 2001 (0x7D1)
351        // 2. Calculate checksum:
352        //    nibble0 = 0x1, nibble1 = 0xD, nibble2 = 0x7
353        //    0x1 ^ 0xD ^ 0x7 = 0xB
354        // 3. Because with_telemetry is true, encode_raw bitwise inverts this checksum.
355        //    !0xB & 0x0F = !0b1011 & 0x0F = 0b0100 = 0x4
356        // 4. Shift payload left by 4 and add checksum: (2001 << 4) | 0x4 = 32016 + 4 = 32020 (0x7D14)
357        let frame = DshotCommandFrame::encode_raw(1000, DshotCommandFrame::WITH_TELEMETRY);
358
359        assert_eq!(frame.raw(), 0x7D14);
360        assert_eq!(frame.value(), 1000);
361        assert!(frame.is_telemetry_enabled());
362        assert_eq!(frame.checksum(), 0x04);
363    }
364
365    #[test]
366    fn test_try_from_valid_and_invalid() {
367        let valid_res = DshotCommandFrame::try_from(0);
368        assert!(valid_res.is_ok());
369        assert_eq!(valid_res.unwrap().value(), 0);
370
371        let valid_res = DshotCommandFrame::try_from(1);
372        assert!(valid_res.is_ok());
373        assert_eq!(valid_res.unwrap().value(), 1);
374
375        // Max valid raw value is 2047
376        let valid_res = DshotCommandFrame::try_from(2047);
377        assert!(valid_res.is_ok());
378        assert_eq!(valid_res.unwrap().value(), 2047);
379
380        // Over the limit should return the error value
381        let invalid_res = DshotCommandFrame::try_from(2048);
382        assert!(invalid_res.is_err());
383        assert_eq!(invalid_res.unwrap_err(), 2048);
384    }
385
386    #[test]
387    fn from_command() {
388        // Using MotorStop (value 0)
389        let frame = DshotCommandFrame::from_command(DshotCommand::MotorStop);
390        assert_eq!(frame.value(), 0);
391        assert!(frame.is_telemetry_enabled());
392
393        // Using BeepTone1 (value 1)
394        let frame_telemetry = DshotCommandFrame::from_command(DshotCommand::Beep1);
395        assert_eq!(frame_telemetry.value(), 1);
396        assert!(frame_telemetry.is_telemetry_enabled());
397    }
398
399    #[test]
400    fn from_throttle_scaling() {
401        // 1. Minimum Active Throttle (0.0) -> Should map to THROTTLE_MIN (48)
402        let frame_min = DshotCommandFrame::from_throttle_unidirectional(0.0);
403        assert_eq!(frame_min.value(), 48);
404        let frame_min = DshotCommandFrame::from_throttle_bidirectional(0.0);
405        assert_eq!(frame_min.value(), 48);
406
407        // 2. Maximum Active Throttle (1.0) -> Should map to THROTTLE_MAX (2047)
408        let frame_max = DshotCommandFrame::from_throttle_unidirectional(1.0);
409        assert_eq!(frame_max.value(), 2047);
410        let frame_max = DshotCommandFrame::from_throttle_bidirectional(1.0);
411        assert_eq!(frame_max.value(), 2047);
412
413        // 3. Midpoint Throttle (0.5) -> (2047 - 48) * 0.5 = 999.5 -> rounded to 1000 -> + 48 = 1048
414        let frame_midpoint = DshotCommandFrame::from_throttle_unidirectional(0.5);
415        assert_eq!(frame_midpoint.value(), 1048);
416        let frame_midpoint = DshotCommandFrame::from_throttle_bidirectional(0.5);
417        assert_eq!(frame_midpoint.value(), 1048);
418    }
419
420    #[test]
421    fn from_throttle_clamping() {
422        // Negative inputs should be clamped safely to 0.0 -> evaluating to 48
423        let frame_neg = DshotCommandFrame::from_throttle_unidirectional(-0.25);
424        assert_eq!(frame_neg.value(), 48);
425
426        // Over-unity inputs should be clamped safely to 1.0 -> evaluating to 2047
427        let frame_over = DshotCommandFrame::from_throttle_unidirectional(1.5);
428        assert_eq!(frame_over.value(), 2047);
429    }
430
431    #[test]
432    fn input_value_clamping_protection() {
433        // If an absolute rogue value over 2047 bypasses validation into encode_raw directly,
434        // the constructor must clamp it to 2047 instead of shifting out-of-bounds junk bits
435        let frame = DshotCommandFrame::encode_raw(9999, DshotCommandFrame::NO_TELEMETRY);
436        assert_eq!(frame.value(), 2047);
437    }
438}