Skip to content

Commit 5f4aeaf

Browse files
Add initial POC telemetry decoding (#4)
* add gcr lookup table * refactor, add skeleton state implementation * add telem variables * return bool from dshot_cmd decode * move all code related to dshot_cmd inside the state add telem state add switching to telem state * add telem annotations * store dshot kbaud make dshot period lookup dynamic * add rough received bit handling no error handling etc, no limit on # of bits (crashes) * tidyup * transition back to cmd state after 21 bits * remove previously remove var * fix high bit * use bitwise operators for bit add skeleton functions * default to bidir * add annotation types * add start/end for telem packets move debugging to annotation * add force edt option * fix gcr table * update annotations * initial attempt at xoring and gcr * checksums match, send it * tidyup * tidyup * add crc handling * cleanup * cleanup
1 parent 7145f2a commit 5f4aeaf

1 file changed

Lines changed: 224 additions & 59 deletions

File tree

pd.py

Lines changed: 224 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,38 @@
2222

2323
import sigrokdecode as srd
2424
from functools import reduce
25-
26-
25+
from enum import Enum
26+
27+
gcr_tables = {
28+
"0b11001": 0x0,
29+
"0b11011": 0x1,
30+
"0b10010": 0x2,
31+
"0b10011": 0x3,
32+
"0b11101": 0x4,
33+
"0b10101": 0x5,
34+
"0b10110": 0x6,
35+
"0b10111": 0x7,
36+
"0b11010": 0x8,
37+
"0b1001": 0x9,
38+
"0b1010": 0xa,
39+
"0b1011": 0xb,
40+
"0b11110": 0xc,
41+
"0b1101": 0xd,
42+
"0b1110": 0xe,
43+
"0b1111": 0xf
44+
}
2745

2846
class SamplerateError(Exception):
2947
pass
3048

49+
class State(Enum):
50+
CMD = 1,
51+
TELEM = 2
52+
53+
class State_Telem(Enum):
54+
START = 1,
55+
RECV = 2
56+
3157
class Decoder(srd.Decoder):
3258
api_version = 3
3359
id = 'dshot'
@@ -44,65 +70,83 @@ class Decoder(srd.Decoder):
4470

4571
options = (
4672
{'id': 'dshot_rate', 'desc': 'DShot Rate', 'default': '150','values': ('150', '300','600','1200')},
47-
{ 'id': 'bidir', 'desc': 'Bidirectional DShot','default': 'False', 'values': ('True', 'False')},
73+
{ 'id': 'bidir', 'desc': 'Bidirectional DShot','default': 'True', 'values': ('True', 'False')},
4874
{ 'id': 'log', 'desc': 'Write log file','default': 'no', 'values': ('yes', 'no')},
75+
{'id': 'edt_force', 'desc': 'Force EDT as telem type', 'default': 'no', 'values': ('True', 'False')},
4976
)
5077
annotations = (
5178
('bit', 'Bit'),
5279
('cmd', 'Command'),
5380
('throttle', 'Throttle'),
5481
('checksum', 'CRC'),
5582
('errors', 'Errors'),
83+
('telem_bit', 'Telem Bit'),
84+
('telem_erpm', 'Telem ERPM'),
85+
('telem_edt', 'Telem EDT'),
86+
('telem_errors', 'Telem Errors'),
87+
('telem_error2', 'Telem Errors2'),
5688

5789
)
5890
annotation_rows = (
5991
('bits', 'Bits', (0,)),
6092
('dshot_data', 'DShot Data', (1,2,3)),
6193
('dshot_errors', 'Dshot Errors', (4,)),
94+
('telem_bits', 'Telem Bits', (5,)),
95+
('dshot_telem_erpm', 'Dshot Telem ERPM', (6,)),
96+
('dshot_telem_edt', 'Dshot Telem', (7,)),
97+
('dshot_telem_errors', 'Dshot Errors', (8,)),
98+
('dshot_telem_errors2', 'Dshot Errors', (9,)),
6299
)
63100

64-
dshot_period_lookup = {'150': 6.67e-6, '300': 3.33e-6,'600':1.67e-6,'1200':0.83e-6}
101+
#dshot_period_lookup = {'150': 6.67e-6, '300': 3.33e-6,'600':1.67e-6,'1200':0.83e-6}
102+
65103

66104
def __init__(self):
67105
self.reset()
68106

69107
def reset(self):
108+
self.state = State.CMD
70109
self.samplerate = None
71-
# self.oldpin = None
72-
# self.ss_packet = None
73-
# self.ss = None
74-
# self.es = None
75-
# self.bits = []
110+
111+
self.debug = False
112+
76113
self.inreset = False
77114
self.bidirectional = False
115+
self.dshot_kbaud = 300e3
78116
self.dshot_period = 3.33e-6
79117
self.actual_period = None
80118
self.halfbitwidth = None
81119
self.currbit_ss = None
82120
self.currbit_es = None
83-
self.samples_toreset = None
121+
self.samples_after_motorcmd = None
84122
self.samples_pp = None
85-
123+
124+
self.telem_start = None
125+
self.state_telem = State_Telem.START
126+
self.telem_baudrate_midpoint = 0
127+
self.edt_force = False
86128

87129
def start(self):
88130
self.bidirectional = True if self.options['bidir'] == 'True' else False
89-
self.dshot_period = self.dshot_period_lookup[self.options['dshot_rate']]
131+
self.edt_force = True if self.options['edt_force'] == 'True' else False
132+
self.dshot_kbaud = int(self.options['dshot_rate'])*1000
133+
self.dshot_period = 1/self.dshot_kbaud
90134
self.samples_pp = int(self.samplerate*self.dshot_period)
91-
self.samples_toreset = self.samples_pp*3
92-
# self.halfbitwidth = int((self.samplerate / self.dshot_period) / 2.0)
93-
#print("start period",self.dshot_period)
135+
self.samples_after_motorcmd = self.samples_pp * 3
136+
self.samples_after_telempkt = self.samples_pp * 3
137+
94138
self.out_ann = self.register(srd.OUTPUT_ANN)
139+
self.telem_baudrate_midpoint = int((self.samplerate / (self.dshot_kbaud*(5/4))) / 2.0)
140+
if self.debug:
141+
print("telem_midpoint",self.telem_baudrate_midpoint)
95142

96143
def metadata(self, key, value):
97144
if key == srd.SRD_CONF_SAMPLERATE:
98145
self.samplerate = value
99146

100-
def handle_bits(self, results):
147+
def handle_bits_dshot(self, results):
101148
#ss, es, bit
102-
#print(results)
103149
bits = [result[2] for result in results]
104-
# print(bits)
105-
106150

107151
if len(bits) == 16:
108152
dshot_value = int(reduce(lambda a, b: (a << 1) | b, bits[:11]))
@@ -138,18 +182,11 @@ def handle_bits(self, results):
138182
if not crc_ok:
139183
self.put(crc_startsample, results[15][1], self.out_ann,
140184
[4, ['CRC INVALID']])
141-
142-
143-
self.bits = []
144-
self.ss_packet = None
185+
return True
145186
else:
146-
return
147-
# self.put(results[0][0], results[-1::1][1], self.out_ann,
148-
# [1, ['ERROR: INVALID PACKET LENGTH', 'ERR', 'E']])
149-
150-
151-
def handle_bit(self, ss, es, nb_ss):
187+
return False
152188

189+
def handle_bit_dshot(self, ss, es, nb_ss):
153190
period = nb_ss - ss
154191
duty = es - ss
155192
# Ideal duty for T0H: 33%, T1H: 66%.
@@ -159,43 +196,171 @@ def handle_bit(self, ss, es, nb_ss):
159196
[0, ['%d' % bit_]])
160197
return [ss,nb_ss,bit_]
161198

162-
199+
def handle_telem_bit(self,matched):
200+
# None to raise exception if no match
201+
result = None
202+
203+
# Low
204+
if matched == (True, False):
205+
# 0 value
206+
result = 0
207+
208+
# High
209+
elif matched == (False, True):
210+
# 1 value
211+
result = 1
212+
return result
213+
214+
def process_telem_erpm(self,packet,start,end):
215+
# Raw packet
216+
self.put(start,
217+
end, self.out_ann,
218+
[6, ['%23s' % bin(packet)]])
219+
# XOR with next?
220+
packet &= 0x0FFFFF
221+
packet = (packet^(packet>>1))
222+
self.put(start,
223+
end, self.out_ann,
224+
[7, ['%23s' % bin(packet)]])
225+
# Undo GCR
226+
output = 0b0
227+
228+
nibbles = 4
229+
bitmask = 0b11111 << ((nibbles-1)*5)
230+
231+
for n in range(nibbles):
232+
gcr_n = bitmask & packet
233+
ungcr = gcr_tables[bin(gcr_n >> (nibbles - (n + 1)) * 5)]
234+
output = (output << 4) | ungcr
235+
bitmask = (bitmask >> 5)
236+
237+
# Compare CRC
238+
crc_received = output & 0xF
239+
output = (output >> 4) & 0xFFF
240+
crc_calc = ~((output ^ (output >> 4) ^ (output >> 8))) & 0x0F
241+
242+
243+
244+
self.put(end - ((self.telem_baudrate_midpoint * 2)*4),
245+
end, self.out_ann,
246+
[7, ['%23s' % ("RX CRC: "+hex(crc_received)+" Calc CRC: "+hex(crc_calc))]])
247+
if crc_calc != crc_received:
248+
self.put(end - ((self.telem_baudrate_midpoint * 2) * 4),
249+
end, self.out_ann,
250+
[8, ['%23s' % ("CRC ERROR!")]])
251+
# The upper 12 bit contain the eperiod (1/erps) in the following bitwise encoding:
252+
#
253+
# e e e m m m m m m m m m
254+
#
255+
# The 9 bit value M needs to shifted left E times to get the period in micro seconds.
256+
# This gives a range of 1 us to 65408 us. Which translates to a min e-frequency of 15.29 hz or for 14 pole motors 3.82 hz.
257+
return
258+
def process_telem_edt(self,packet,start,end):
259+
260+
return
261+
def process_telem(self,packet,start,end):
262+
if self.edt_force:
263+
self.process_telem_edt(packet,start,end)
264+
else:
265+
self.process_telem_erpm(packet,start,end)
266+
267+
return
268+
269+
270+
271+
163272
def decode(self):
164273
if not self.samplerate:
165274
raise SamplerateError('Cannot decode without samplerate.')
166275

167276
results = []
277+
telem = 0b0
278+
tlm_start = 0
168279
while True:
169-
if not self.bidirectional:
170-
pins = self.wait([{0: 'r'},{0: 'f'},{'skip':self.samples_toreset}])
171-
else:
172-
pins = self.wait([{0: 'f'},{0: 'r'},{'skip':self.samples_toreset}])
173-
174-
if self.currbit_ss and self.currbit_es and self.matched[2]:
175-
# Assume end of packet if have seen start and end of a potential bit but no further change within 3 periods
176-
# TODO: Confirm wait period this works with spec
177-
results += [self.handle_bit(self.currbit_ss,self.currbit_es,(self.currbit_ss+self.samples_pp))]
178-
self.currbit_ss = None
179-
self.currbit_es = None
180-
181-
# Pass results to decoder
182-
self.handle_bits(results)
183-
results = []
184-
185280

186-
if self.matched[0] and not self.currbit_ss and not self.currbit_es:
187-
# Start of bit
188-
self.currbit_ss = self.samplenum
189-
elif self.matched[1] and self.currbit_ss and not self.currbit_es:
190-
# End of bit
191-
self.currbit_es = self.samplenum
192-
elif self.matched[0] and self.currbit_es and self.currbit_ss:
193-
# Have complete bit, can handle bit now
194-
result = [self.handle_bit(self.currbit_ss,self.currbit_es,self.samplenum)]
195-
# print(result)
196-
results += result
197-
self.currbit_ss = self.samplenum
198-
self.currbit_es = None
281+
match self.state:
282+
case State.CMD:
283+
if not self.bidirectional:
284+
pins = self.wait([{0: 'r'}, {0: 'f'}, {'skip': self.samples_after_motorcmd}])
285+
else:
286+
pins = self.wait([{0: 'f'}, {0: 'r'}, {'skip': self.samples_after_motorcmd}])
287+
#TODO: Increase skip to maximum time for effiency
288+
#TODO: Mark any changes in this time as errors? Option to reduce load?
289+
290+
if self.currbit_ss and self.currbit_es and self.matched[2]:
291+
# Assume end of packet if have seen start and end of a potential bit but no further change within 3 periods
292+
# TODO: Confirm wait period this works with spec
293+
results += [self.handle_bit_dshot(self.currbit_ss, self.currbit_es,
294+
(self.currbit_ss + self.samples_pp))]
295+
self.currbit_ss = None
296+
self.currbit_es = None
297+
298+
# Pass results to decoder
299+
result = self.handle_bits_dshot(results)
300+
if result and self.bidirectional:
301+
self.state = State.TELEM
302+
results = []
303+
304+
if self.matched[0] and not self.currbit_ss and not self.currbit_es:
305+
# Start of bit
306+
self.currbit_ss = self.samplenum
307+
elif self.matched[1] and self.currbit_ss and not self.currbit_es:
308+
# End of bit
309+
self.currbit_es = self.samplenum
310+
elif self.matched[0] and self.currbit_es and self.currbit_ss:
311+
# Have complete bit, can handle bit now
312+
result = [self.handle_bit_dshot(self.currbit_ss, self.currbit_es, self.samplenum)]
313+
# print(result)
314+
results += result
315+
self.currbit_ss = self.samplenum
316+
self.currbit_es = None
317+
case State.TELEM:
318+
match self.state_telem:
319+
case State_Telem.START:
320+
# First wait for falling edge (idle high)
321+
pins = self.wait([{0: 'f'}])
322+
# Save start pulse
323+
tlm_start = self.samplenum
324+
# Switch to receiving state
325+
self.state_telem = State_Telem.RECV
326+
# TODO: Check if still low after 1/8 bitlength for error det?
327+
case State_Telem.RECV:
328+
# First conditions skips half bit width and matches low
329+
# Second condition skips half bit width and matches high
330+
pins = self.wait([{0: 'l', 'skip': self.telem_baudrate_midpoint},
331+
{0: 'h', 'skip': self.telem_baudrate_midpoint}])
332+
333+
# Append next bit
334+
curr_bit = self.handle_telem_bit(self.matched)
335+
self.put(self.samplenum - self.telem_baudrate_midpoint,
336+
self.samplenum + self.telem_baudrate_midpoint,
337+
self.out_ann,
338+
[5, ['%04d' % curr_bit]])
339+
telem = telem | curr_bit
340+
341+
# Skip half bitwidth to end of bit
342+
pins = self.wait([{'skip': self.telem_baudrate_midpoint}])
343+
344+
if telem.bit_length() >= 20-1:
345+
self.process_telem(telem,tlm_start,self.samplenum)
346+
347+
# Reset
348+
telem = 0b0
349+
self.state_telem = State_Telem.START
350+
self.state = State.CMD
351+
else:
352+
# Shift for next bit
353+
telem = telem << 1
354+
# If not mark as error
355+
356+
# Then skip x samples and sample
357+
# Repeat for 21 bits (TBC)
358+
359+
#TODO: What happens if it gets stuck in the wrong state?
360+
361+
362+
363+
199364

200365

201366

0 commit comments

Comments
 (0)