]> sigrok.org Git - libsigrokdecode.git/blame - decoders/usb_power_delivery/pd.py
usb_power_delivery: print more useful stuff
[libsigrokdecode.git] / decoders / usb_power_delivery / pd.py
CommitLineData
ced6589f
VP
1##
2## This file is part of the libsigrokdecode project.
3##
fddb3114 4## Copyright (C) 2015 Google, Inc
4083a379 5## Copyright (C) 2018 Peter Hazenberg <sigrok@haas-en-berg.nl>
ced6589f
VP
6##
7## This program is free software; you can redistribute it and/or modify
8## it under the terms of the GNU General Public License as published by
9## the Free Software Foundation; either version 2 of the License, or
10## (at your option) any later version.
11##
12## This program is distributed in the hope that it will be useful,
13## but WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15## GNU General Public License for more details.
16##
fddb3114 17## You should have received a copy of the GNU General Public License
4539e9ca 18## along with this program; if not, see <http://www.gnu.org/licenses/>.
fddb3114 19##
ced6589f
VP
20
21import sigrokdecode as srd
22import struct
23import zlib # for crc32
24
79065c6f 25# BMC encoding with a 600kHz datarate
ced6589f
VP
26UI_US = 1000000/600000.0
27
28# Threshold to discriminate half-1 from 0 in Binary Mark Conding
29THRESHOLD_US = (UI_US + 2 * UI_US) / 2
30
31# Control Message type
32CTRL_TYPES = {
89b9aaf7
VP
33 0: 'reserved',
34 1: 'GOOD CRC',
35 2: 'GOTO MIN',
36 3: 'ACCEPT',
37 4: 'REJECT',
38 5: 'PING',
39 6: 'PS RDY',
40 7: 'GET SOURCE CAP',
41 8: 'GET SINK CAP',
42 9: 'DR SWAP',
43 10: 'PR SWAP',
44 11: 'VCONN SWAP',
45 12: 'WAIT',
46 13: 'SOFT RESET',
47 14: 'reserved',
48 15: 'reserved'
ced6589f
VP
49}
50
51# Data message type
52DATA_TYPES = {
89b9aaf7
VP
53 1: 'SOURCE CAP',
54 2: 'REQUEST',
55 3: 'BIST',
56 4: 'SINK CAP',
57 15: 'VDM'
ced6589f
VP
58}
59
60# 4b5b encoding of the symbols
61DEC4B5B = [
62 0x10, # Error 00000
63 0x10, # Error 00001
64 0x10, # Error 00010
65 0x10, # Error 00011
66 0x10, # Error 00100
67 0x10, # Error 00101
68 0x13, # Sync-3 00110
69 0x14, # RST-1 00111
70 0x10, # Error 01000
71 0x01, # 1 = 0001 01001
72 0x04, # 4 = 0100 01010
73 0x05, # 5 = 0101 01011
74 0x10, # Error 01100
75 0x16, # EOP 01101
76 0x06, # 6 = 0110 01110
77 0x07, # 7 = 0111 01111
78 0x10, # Error 10000
79 0x12, # Sync-2 10001
80 0x08, # 8 = 1000 10010
81 0x09, # 9 = 1001 10011
82 0x02, # 2 = 0010 10100
83 0x03, # 3 = 0011 10101
84 0x0A, # A = 1010 10110
85 0x0B, # B = 1011 10111
86 0x11, # Sync-1 11000
87 0x15, # RST-2 11001
88 0x0C, # C = 1100 11010
89 0x0D, # D = 1101 11011
90 0x0E, # E = 1110 11100
91 0x0F, # F = 1111 11101
92 0x00, # 0 = 0000 11110
93 0x10, # Error 11111
94]
95SYM_ERR = 0x10
96SYNC1 = 0x11
97SYNC2 = 0x12
98SYNC3 = 0x13
99RST1 = 0x14
100RST2 = 0x15
101EOP = 0x16
102SYNC_CODES = [SYNC1, SYNC2, SYNC3]
103HRST_CODES = [RST1, RST1, RST1, RST2]
104
f30fdbb6
GS
105SOP_SEQUENCES = [
106 (SYNC1, SYNC1, SYNC1, SYNC2),
107 (SYNC1, SYNC1, SYNC3, SYNC3),
108 (SYNC1, SYNC3, SYNC1, SYNC3),
109 (SYNC1, RST2, RST2, SYNC3),
110 (SYNC1, RST2, SYNC3, SYNC2),
111 (RST1, SYNC1, RST1, SYNC3),
112 (RST1, RST1, RST1, RST2),
113]
ced6589f 114START_OF_PACKETS = {
f30fdbb6
GS
115 SOP_SEQUENCES[0]: 'SOP',
116 SOP_SEQUENCES[1]: "SOP'",
117 SOP_SEQUENCES[2]: 'SOP"',
118 SOP_SEQUENCES[3]: "SOP' Debug",
119 SOP_SEQUENCES[4]: 'SOP" Debug',
120 SOP_SEQUENCES[5]: 'Cable Reset',
121 SOP_SEQUENCES[6]: 'Hard Reset',
ced6589f
VP
122}
123
124SYM_NAME = [
125 ['0x0', '0'],
126 ['0x1', '1'],
127 ['0x2', '2'],
128 ['0x3', '3'],
129 ['0x4', '4'],
130 ['0x5', '5'],
131 ['0x6', '6'],
132 ['0x7', '7'],
133 ['0x8', '8'],
134 ['0x9', '9'],
135 ['0xA', 'A'],
136 ['0xB', 'B'],
137 ['0xC', 'C'],
138 ['0xD', 'D'],
139 ['0xE', 'E'],
140 ['0xF', 'F'],
141 ['ERROR', 'X'],
142 ['SYNC-1', 'S1'],
143 ['SYNC-2', 'S2'],
144 ['SYNC-3', 'S3'],
145 ['RST-1', 'R1'],
146 ['RST-2', 'R2'],
147 ['EOP', '#'],
148]
149
150RDO_FLAGS = {
89b9aaf7
VP
151 (1 << 24): 'no_suspend',
152 (1 << 25): 'comm_cap',
153 (1 << 26): 'cap_mismatch',
154 (1 << 27): 'give_back'
ced6589f 155}
4083a379 156
ced6589f 157PDO_FLAGS = {
89b9aaf7
VP
158 (1 << 29): 'dual_role_power',
159 (1 << 28): 'suspend',
160 (1 << 27): 'ext',
161 (1 << 26): 'comm_cap',
162 (1 << 25): 'dual_role_data'
ced6589f
VP
163}
164
165BIST_MODES = {
89b9aaf7
VP
166 0: 'Receiver',
167 1: 'Transmit',
168 2: 'Counters',
169 3: 'Carrier 0',
170 4: 'Carrier 1',
171 5: 'Carrier 2',
172 6: 'Carrier 3',
173 7: 'Eye',
ced6589f
VP
174}
175
176VDM_CMDS = {
89b9aaf7
VP
177 1: 'Disc Ident',
178 2: 'Disc SVID',
179 3: 'Disc Mode',
180 4: 'Enter Mode',
181 5: 'Exit Mode',
182 6: 'Attention',
ced6589f
VP
183 # 16..31: SVID Specific Commands
184 # DisplayPort Commands
89b9aaf7
VP
185 16: 'DP Status',
186 17: 'DP Configure',
ced6589f 187}
89b9aaf7 188VDM_ACK = ['REQ', 'ACK', 'NAK', 'BSY']
ced6589f 189
4083a379
P
190STORED_PDOS = {}
191
669f30f4
UH
192class SamplerateError(Exception):
193 pass
194
ced6589f 195class Decoder(srd.Decoder):
bc6f82bb 196 api_version = 3
ced6589f
VP
197 id = 'usb_power_delivery'
198 name = 'USB PD'
199 longname = 'USB Power Delivery'
200 desc = 'USB Power Delivery protocol.'
201 license = 'gplv2+'
202 inputs = ['logic']
203 outputs = ['usb_pd']
204 channels = (
205 {'id': 'cc', 'name': 'CC', 'desc': 'Control channel'},
206 )
207 options = (
208 {'id': 'fulltext', 'desc': 'full text decoding of the packet',
209 'default': 'no', 'values': ('yes', 'no')},
210 )
211 annotations = (
212 ('type', 'Packet Type'),
213 ('Preamble', 'Preamble'),
214 ('SOP', 'Start of Packet'),
215 ('Head', 'Header'),
216 ('Data', 'Data'),
217 ('CRC', 'Checksum'),
218 ('EOP', 'End Of Packet'),
219 ('Sym', '4b5b symbols'),
220 ('warnings', 'Warnings'),
221 ('src', 'Source Message'),
222 ('snk', 'Sink Message'),
223 ('payload', 'Payload'),
224 ('text', 'Plain text'),
225 )
226 annotation_rows = (
227 ('4B5B', 'symbols', (7, )),
228 ('Phase', 'parts', (1, 2, 3, 4, 5, 6, )),
229 ('payload', 'Payload', (11, )),
230 ('type', 'Type', (0, 9, 10, )),
231 ('warnings', 'Warnings', (8, )),
232 ('text', 'Full text', (12, )),
233 )
234 binary = (
235 ('raw-data', 'RAW binary data'),
236 )
237
238 def get_request(self, rdo):
239 pos = (rdo >> 28) & 7
240 op_ma = ((rdo >> 10) & 0x3ff) * 10
241 max_ma = (rdo & 0x3ff) * 10
89b9aaf7 242 flags = ''
57ba804a 243 for f in sorted(RDO_FLAGS.keys(), reverse = True):
ced6589f 244 if rdo & f:
4083a379
P
245 flags += ' [' + RDO_FLAGS[f] + ']'
246 return '(PDO #%d: %s) %gA (operating) / %gA (max)%s' % (pos, STORED_PDOS[pos], op_ma/1000.0, max_ma/1000.0, flags)
ced6589f 247
4083a379
P
248 def get_source_sink_cap(self, pdo, idx):
249 t1 = (pdo >> 30) & 3
250 if t1 == 0:
251 t_name = 'Fixed'
252 mv = ((pdo >> 10) & 0x3ff) * 0.05
253 ma = ((pdo >> 0) & 0x3ff) * 0.01
254 p = '%gV %gA (%gW)' % (mv, ma, mv*ma)
255 STORED_PDOS[idx] = '%s %gV' % (t_name, mv)
256 elif t1 == 1:
257 t_name = 'Battery'
258 minv = ((pdo >> 10) & 0x3ff) * 0.05
259 maxv = ((pdo >> 20) & 0x3ff) * 0.05
260 mw = ((pdo >> 0) & 0x3ff) * 0.25
261 p = '%g/%gV %gW' % (minv, maxv, mw)
262 STORED_PDOS[idx] = '%s %g/%gV' % (t_name, minv, maxv)
263 elif t1 == 2:
264 t_name = 'Variable'
265 minv = ((pdo >> 10) & 0x3ff) * 0.05
266 maxv = ((pdo >> 20) & 0x3ff) * 0.05
267 ma = ((pdo >> 0) & 0x3ff) * 0.01
268 p = '%g/%gV %gA' % (minv, maxv, ma)
269 STORED_PDOS[idx] = '%s %g/%gV' % (t_name, minv, maxv)
270 elif t1 == 3:
271 t2 = (pdo >> 28) & 3
272 if t2 == 0:
273 t_name = 'Programmable'
274 p = 'TODO: PPS support'
275 else:
276 t_name = 'Reserved APDO: '+bin(t2)
277 p = ''
89b9aaf7 278 flags = ''
57ba804a 279 for f in sorted(PDO_FLAGS.keys(), reverse = True):
ced6589f 280 if pdo & f:
4083a379
P
281 flags += ' [' + PDO_FLAGS[f] + ']'
282 return '[%s] %s%s' % (t_name, p, flags)
ced6589f
VP
283
284 def get_vdm(self, idx, data):
285 if idx == 0: # VDM header
79065c6f
UH
286 vid = data >> 16
287 struct = data & (1 << 15)
89b9aaf7 288 txt = 'VDM'
79065c6f
UH
289 if struct: # Structured VDM
290 cmd = data & 0x1f
291 src = data & (1 << 5)
292 ack = (data >> 6) & 3
293 pos = (data >> 8) & 7
294 ver = (data >> 13) & 3
89b9aaf7
VP
295 txt = VDM_ACK[ack] + ' '
296 txt += VDM_CMDS[cmd] if cmd in VDM_CMDS else 'cmd?'
297 txt += ' pos %d' % (pos) if pos else ' '
79065c6f 298 else: # Unstructured VDM
89b9aaf7
VP
299 txt = 'unstruct [%04x]' % (data & 0x7fff)
300 txt += ' SVID:%04x' % (vid)
ced6589f 301 else: # VDM payload
89b9aaf7 302 txt = 'VDO:%08x' % (data)
ced6589f
VP
303 return txt
304
305 def get_bist(self, idx, data):
306 mode = data >> 28
307 counter = data & 0xffff
89b9aaf7 308 mode_name = BIST_MODES[mode] if mode in BIST_MODES else 'INVALID'
ced6589f 309 if mode == 2:
89b9aaf7 310 mode_name = 'Counter[= %d]' % (counter)
ced6589f 311 # TODO check all 0 bits are 0 / emit warnings
89b9aaf7 312 return 'mode %s' % (mode_name) if idx == 0 else 'invalid BRO'
ced6589f
VP
313
314 def putpayload(self, s0, s1, idx):
315 t = self.head_type()
4083a379 316 txt = '['+str(idx+1)+'] '
ced6589f 317 if t == 2:
4083a379
P
318 txt += self.get_request(self.data[idx])
319 elif t == 1 or t == 4:
320 txt += self.get_source_sink_cap(self.data[idx], idx+1)
ced6589f 321 elif t == 15:
4083a379 322 txt += self.get_vdm(idx, self.data[idx])
ced6589f 323 elif t == 3:
4083a379 324 txt += self.get_bist(idx, self.data[idx])
ced6589f 325 self.putx(s0, s1, [11, [txt, txt]])
89b9aaf7 326 self.text += ' - ' + txt
ced6589f
VP
327
328 def puthead(self):
329 ann_type = 9 if self.head_power_role() else 10
89b9aaf7 330 role = 'SRC' if self.head_power_role() else 'SNK'
ced6589f 331 if self.head_data_role() != self.head_power_role():
89b9aaf7 332 role += '/DFP' if self.head_data_role() else '/UFP'
ced6589f
VP
333 t = self.head_type()
334 if self.head_count() == 0:
335 shortm = CTRL_TYPES[t]
336 else:
89b9aaf7 337 shortm = DATA_TYPES[t] if t in DATA_TYPES else 'DAT???'
ced6589f 338
89b9aaf7 339 longm = '{:s}[{:d}]:{:s}'.format(role, self.head_id(), shortm)
ced6589f
VP
340 self.putx(0, -1, [ann_type, [longm, shortm]])
341 self.text += longm
342
343 def head_id(self):
344 return (self.head >> 9) & 7
345
346 def head_power_role(self):
347 return (self.head >> 8) & 1
348
349 def head_data_role(self):
350 return (self.head >> 5) & 1
351
352 def head_rev(self):
353 return ((self.head >> 6) & 3) + 1
354
355 def head_type(self):
356 return self.head & 0xF
357
358 def head_count(self):
359 return (self.head >> 12) & 7
360
361 def putx(self, s0, s1, data):
362 self.put(self.edges[s0], self.edges[s1], self.out_ann, data)
363
364 def putwarn(self, longm, shortm):
365 self.putx(0, -1, [8, [longm, shortm]])
366
367 def compute_crc32(self):
89b9aaf7 368 bdata = struct.pack('<H'+'I'*len(self.data), self.head & 0xffff,
ced6589f
VP
369 *tuple([d & 0xffffffff for d in self.data]))
370 return zlib.crc32(bdata)
371
372 def rec_sym(self, i, sym):
373 self.putx(i, i+5, [7, SYM_NAME[sym]])
374
375 def get_sym(self, i, rec=True):
376 v = (self.bits[i] | (self.bits[i+1] << 1) | (self.bits[i+2] << 2) |
377 (self.bits[i+3] << 3) | (self.bits[i+4] << 4))
378 sym = DEC4B5B[v]
379 if rec:
380 self.rec_sym(i, sym)
381 return sym
382
383 def get_short(self):
384 i = self.idx
385 # Check it's not a truncated packet
386 if len(self.bits) - i <= 20:
89b9aaf7 387 self.putwarn('Truncated', '!')
ced6589f
VP
388 return 0x0BAD
389 k = [self.get_sym(i), self.get_sym(i+5),
390 self.get_sym(i+10), self.get_sym(i+15)]
391 # TODO check bad symbols
392 val = k[0] | (k[1] << 4) | (k[2] << 8) | (k[3] << 12)
393 self.idx += 20
394 return val
395
396 def get_word(self):
397 lo = self.get_short()
398 hi = self.get_short()
399 return lo | (hi << 16)
400
401 def find_corrupted_sop(self, k):
402 # Start of packet are valid even if they have only 3 correct symbols
403 # out of 4.
f30fdbb6 404 for seq in SOP_SEQUENCES:
ced6589f
VP
405 if [k[i] == seq[i] for i in range(len(k))].count(True) >= 3:
406 return START_OF_PACKETS[seq]
407 return None
408
409 def scan_eop(self):
410 for i in range(len(self.bits) - 19):
411 k = (self.get_sym(i, rec=False), self.get_sym(i+5, rec=False),
412 self.get_sym(i+10, rec=False), self.get_sym(i+15, rec=False))
279331bd 413 sym = START_OF_PACKETS.get(k, None)
ced6589f
VP
414 if not sym:
415 sym = self.find_corrupted_sop(k)
416 # We have an interesting symbol sequence
417 if sym:
418 # annotate the preamble
419 self.putx(0, i, [1, ['Preamble', '...']])
420 # annotate each symbol
421 self.rec_sym(i, k[0])
422 self.rec_sym(i+5, k[1])
423 self.rec_sym(i+10, k[2])
424 self.rec_sym(i+15, k[3])
425 if sym == 'Hard Reset':
89b9aaf7 426 self.text += 'HRST'
ced6589f
VP
427 return -1 # Hard reset
428 elif sym == 'Cable Reset':
89b9aaf7 429 self.text += 'CRST'
ced6589f
VP
430 return -1 # Cable reset
431 else:
432 self.putx(i, i+20, [2, [sym, 'S']])
433 return i+20
434 self.putx(0, len(self.bits), [1, ['Junk???', 'XXX']])
89b9aaf7
VP
435 self.text += 'Junk???'
436 self.putwarn('No start of packet found', 'XXX')
ced6589f
VP
437 return -1 # No Start Of Packet
438
92b7b49f 439 def __init__(self):
10aeb8ea
GS
440 self.reset()
441
442 def reset(self):
ced6589f
VP
443 self.samplerate = None
444 self.idx = 0
445 self.packet_seq = 0
ced6589f 446 self.previous = 0
ced6589f
VP
447 self.startsample = None
448 self.bits = []
449 self.edges = []
450 self.bad = []
451 self.half_one = False
452 self.start_one = 0
453
454 def metadata(self, key, value):
455 if key == srd.SRD_CONF_SAMPLERATE:
456 self.samplerate = value
457 # 0 is 2 UI, space larger than 1.5x 0 is definitely wrong
458 self.maxbit = self.us2samples(3 * UI_US)
459 # duration threshold between half 1 and 0
460 self.threshold = self.us2samples(THRESHOLD_US)
461
462 def start(self):
463 self.out_python = self.register(srd.OUTPUT_PYTHON)
464 self.out_ann = self.register(srd.OUTPUT_ANN)
465 self.out_binary = self.register(srd.OUTPUT_BINARY)
466 self.out_bitrate = self.register(
467 srd.OUTPUT_META,
468 meta=(int, 'Bitrate', 'Bitrate during the packet')
469 )
470
471 def us2samples(self, us):
ced6589f
VP
472 return int(us * self.samplerate / 1000000)
473
474 def decode_packet(self):
475 self.data = []
476 self.idx = 0
89b9aaf7 477 self.text = ''
ced6589f
VP
478
479 if len(self.edges) < 50:
480 return # Not a real PD packet
481
482 self.packet_seq += 1
483 tstamp = float(self.startsample) / self.samplerate
89b9aaf7 484 self.text += '#%-4d (%8.6fms): ' % (self.packet_seq, tstamp*1000)
ced6589f
VP
485
486 self.idx = self.scan_eop()
487 if self.idx < 0:
488 # Full text trace of the issue
489 self.putx(0, self.idx, [12, [self.text, '...']])
490 return # No real packet: ABORT
491
492 # Packet header
493 self.head = self.get_short()
494 self.putx(self.idx-20, self.idx, [3, ['H:%04x' % (self.head), 'HD']])
495 self.puthead()
496
497 # Decode data payload
498 for i in range(self.head_count()):
499 self.data.append(self.get_word())
500 self.putx(self.idx-40, self.idx,
501 [4, ['[%d]%08x' % (i, self.data[i]), 'D%d' % (i)]])
502 self.putpayload(self.idx-40, self.idx, i)
503
504 # CRC check
505 self.crc = self.get_word()
506 ccrc = self.compute_crc32()
507 if self.crc != ccrc:
89b9aaf7 508 self.putwarn('Bad CRC %08x != %08x' % (self.crc, ccrc), 'CRC!')
ced6589f
VP
509 self.putx(self.idx-40, self.idx, [5, ['CRC:%08x' % (self.crc), 'CRC']])
510
511 # End of Packet
512 if len(self.bits) >= self.idx + 5 and self.get_sym(self.idx) == EOP:
513 self.putx(self.idx, self.idx + 5, [6, ['EOP', 'E']])
514 self.idx += 5
515 else:
89b9aaf7 516 self.putwarn('No EOP', 'EOP!')
ced6589f
VP
517 # Full text trace
518 if self.options['fulltext'] == 'yes':
519 self.putx(0, self.idx, [12, [self.text, '...']])
520
521 # Meta data for bitrate
522 ss, es = self.edges[0], self.edges[-1]
523 bitrate = self.samplerate*len(self.bits) / float(es - ss)
524 self.put(es, ss, self.out_bitrate, int(bitrate))
525 # Raw binary data (BMC decoded)
2824e811 526 self.put(es, ss, self.out_binary, [0, bytes(self.bits)])
ced6589f 527
bc6f82bb 528 def decode(self):
033e7d4d 529 if not self.samplerate:
669f30f4 530 raise SamplerateError('Cannot decode without samplerate.')
bc6f82bb
UH
531 while True:
532 self.wait({0: 'e'})
ced6589f
VP
533
534 # First sample of the packet, just record the start date
535 if not self.startsample:
536 self.startsample = self.samplenum
537 self.previous = self.samplenum
538 continue
539
540 diff = self.samplenum - self.previous
541
542 # Large idle: use it as the end of packet
543 if diff > self.maxbit:
544 # the last edge of the packet
545 self.edges.append(self.previous)
546 # Export the packet
547 self.decode_packet()
548 # Reset for next packet
549 self.startsample = self.samplenum
550 self.bits = []
551 self.edges = []
552 self.bad = []
553 self.half_one = False
554 self.start_one = 0
555 else: # add the bit to the packet
556 is_zero = diff > self.threshold
557 if is_zero and not self.half_one:
558 self.bits.append(0)
559 self.edges.append(self.previous)
560 elif not is_zero and self.half_one:
561 self.bits.append(1)
562 self.edges.append(self.start_one)
563 self.half_one = False
564 elif not is_zero and not self.half_one:
565 self.half_one = True
566 self.start_one = self.previous
567 else: # Invalid BMC sequence
568 self.bad.append((self.start_one, self.previous))
569 # TODO try to recover
570 self.bits.append(0)
571 self.edges.append(self.previous)
572 self.half_one = False
573 self.previous = self.samplenum