1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
use core::ops::Deref;
use super::{DshotError, DshotTelemetryFrame};
/// A captured GCR frame received straight from the PIO FIFO block.
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, PartialOrd, Ord)]
pub struct GcrFrame(u32);
impl From<GcrFrame> for u32 {
#[inline]
fn from(frame: GcrFrame) -> Self {
frame.0
}
}
impl Deref for GcrFrame {
type Target = u32;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl GcrFrame {
// Standard 5-bit GCR to 4-bit Nibble translation map
const INVALID_NIBBLE: u16 = 255;
const QUINTET_TO_NIBBLE: [u16; 32] = [
255, 255, 255, 255, 255, 255, 255, 255, 255, 9, 10, 11, 255, 13, 14, 15, 255, 255, 2, 3, 255, 5, 6, 7, 255, 0,
8, 1, 255, 4, 12, 255,
];
#[inline]
#[must_use]
pub const fn from_raw(raw_pio: u32) -> Self {
// The PIO delivers 20 bits of decoded GCR payload
Self(raw_pio & 0x000F_FFFF)
}
#[inline]
#[must_use]
pub const fn raw_20(self) -> u32 {
self.0
}
/// Decodes the 20-bit GCR stream into a standard 16-bit `Dshot` frame.
/// # Errors
#[inline]
pub fn try_decode(self) -> Result<DshotTelemetryFrame, DshotError> {
let gcr20 = self.0;
// Extract the 5-bit quintets.
// Because the PIO shifts LSB-first, the chronological data order is inverted:
// The first bits received land in the highest positions (bits 15-19).
let quintet3 = (gcr20 >> 15) & 0x1F; // First received (MSB of DShot frame)
let quintet2 = (gcr20 >> 10) & 0x1F;
let quintet1 = (gcr20 >> 5) & 0x1F;
let quintet0 = gcr20 & 0x1F; // Last received (LSB of DShot frame)
let nibble3 = Self::QUINTET_TO_NIBBLE[quintet3 as usize];
let nibble2 = Self::QUINTET_TO_NIBBLE[quintet2 as usize];
let nibble1 = Self::QUINTET_TO_NIBBLE[quintet1 as usize];
let nibble0 = Self::QUINTET_TO_NIBBLE[quintet0 as usize];
// If any translation hits an invalid code pattern (255), drop the packet
if nibble0 == Self::INVALID_NIBBLE
|| nibble1 == Self::INVALID_NIBBLE
|| nibble2 == Self::INVALID_NIBBLE
|| nibble3 == Self::INVALID_NIBBLE
{
return Err(DshotError::InvalidNrziData);
}
// Reconstruct the original 16-bit DShot telemetry word layout
let telemetry_word = nibble0 | (nibble1 << 4) | (nibble2 << 8) | (nibble3 << 12);
let frame = DshotTelemetryFrame::try_from(telemetry_word)?;
if frame.checksum_is_ok() {
Ok(frame)
} else {
Err(DshotError::InvalidChecksum)
}
}
}
#[cfg(test)]
mod test_traits {
use super::*;
fn is_full_eq<T: Sized + Send + Sync + Unpin + Copy + Clone + Default + Eq + PartialEq>() {}
#[test]
fn normal_types() {
is_full_eq::<GcrFrame>();
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
// Helper function to build a raw 20-bit GCR integer from four 5-bit quintets.
// Simulates how the PIO loads them LSB-first into the register buffer.
fn make_raw_pio_gcr(q3: u32, q2: u32, q1: u32, q0: u32) -> u32 {
(q3 << 15) | (q2 << 10) | (q1 << 5) | q0
}
#[test]
fn test_valid_zero_erpm_frame() {
// Payload: 12-bit value = 0x000 (Nibbles: q3=0, q2=0, q1=0) -> GCR: 0x19
// Checksum: !(0 ^ 0 ^ 0) & 0x0F = 0x0F. (Nibble q0=0x0F) -> GCR: 0x0F
let gcr_payload_zero = 0x19; // Decodes to 0x0
let gcr_checksum_zero = 0x0F; // Decodes to 0x0F
let raw_pio = make_raw_pio_gcr(gcr_payload_zero, gcr_payload_zero, gcr_payload_zero, gcr_checksum_zero);
let frame = GcrFrame::from_raw(raw_pio);
let decode_result = frame.try_decode();
assert!(decode_result.is_ok(), "Expected valid zero eRPM frame decoding to succeed");
}
#[test]
fn test_valid_active_erpm_frame() {
// Simulating a moving motor with a 12-bit period telemetry value of 0x55A
// nibble3 = 0x5 -> GCR: 0x15
// nibble2 = 0x5 -> GCR: 0x15
// nibble1 = 0xA -> GCR: 0x0A
// Checksum calculation: !(0x5 ^ 0x5 ^ 0xA) & 0x0F = !0xA & 0x0F = 0x5
// nibble0 = 0x5 -> GCR: 0x15
let gcr_5 = 0x15;
let gcr_a = 0x0A;
let raw_pio = make_raw_pio_gcr(gcr_5, gcr_5, gcr_a, gcr_5);
let frame = GcrFrame::from_raw(raw_pio);
let decode_result = frame.try_decode();
assert!(decode_result.is_ok(), "Expected valid active eRPM frame decoding to succeed");
// If your test harness handles actual structural return verification,
// you can assert the final parsed u16 word evaluates to 0x55A5:
let decoded_frame = decode_result.unwrap();
assert_eq!(decoded_frame.raw_16(), 0x55A5, "Decoded DShot telemetry word layout mismatch");
}
#[test]
fn test_valid_edt_temperature_frame() {
// Let's simulate an EDT Temperature frame.
// Say the 12-bit payload is 0x24E:
// nibble3 = 0x2 (EDT Temperature category) -> GCR: 0x12
// nibble2 = 0x4 (Data high) -> GCR: 0x1D
// nibble1 = 0xE (Data low) -> GCR: 0x0E
// Checksum: !(0x2 ^ 0x4 ^ 0xE) = !0x8 = 0x7 -> GCR: 0x17
let raw_pio = make_raw_pio_gcr(0x12, 0x1D, 0x0E, 0x17);
let frame = GcrFrame::from_raw(raw_pio);
let decode_result = frame.try_decode();
assert!(decode_result.is_ok(), "Expected valid EDT frame to decode successfully");
}
#[test]
fn test_invalid_gcr_sequence() {
// The 5-bit value 0x00 is completely forbidden in GCR (violates run-length constraints)
// and maps to 255 (0xFF) in our lookup array.
let raw_pio = make_raw_pio_gcr(0x00, 0x19, 0x19, 0x19);
let frame = GcrFrame::from_raw(raw_pio);
let decode_result = frame.try_decode();
assert!(
matches!(decode_result, Err(DshotError::InvalidNrziData)),
"Expected failure due to invalid wire patterns"
);
}
#[test]
fn test_corrupted_checksum() {
// Payload: 12-bit value = 0x000 (GCR quintets: 0x19, 0x19, 0x19)
// Correct checksum should decode to 0x0F (GCR: 0x0F)
// Let's corrupt it by sending an incorrect checksum token (GCR: 0x15)
let raw_pio = make_raw_pio_gcr(0x19, 0x19, 0x19, 0x15);
let frame = GcrFrame::from_raw(raw_pio);
let decode_result = frame.try_decode();
assert!(
matches!(decode_result, Err(DshotError::InvalidChecksum)),
"Expected validation rejection due to checksum mismatch"
);
}
}