Mark Hayter | 14336e7 | 2019-12-29 16:21:34 -0800 | [diff] [blame] | 1 | # Copyright lowRISC contributors. |
| 2 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| 3 | # SPDX-License-Identifier: Apache-2.0 |
| 4 | # |
Scott Johnson | fe79c4b | 2020-07-08 10:31:08 -0700 | [diff] [blame] | 5 | # Convert I2C host format to SVG |
Mark Hayter | 14336e7 | 2019-12-29 16:21:34 -0800 | [diff] [blame] | 6 | |
| 7 | import logging as log |
| 8 | from collections import namedtuple |
| 9 | |
| 10 | import i2csvg.i2csvg_data as sdata |
| 11 | |
| 12 | |
| 13 | # validating version of int(x, 0) |
| 14 | # returns int value, error flag |
| 15 | # if error flag is True value will be zero |
| 16 | def check_int(x, err_prefix): |
| 17 | if isinstance(x, int): |
| 18 | return x, False |
| 19 | if len(x) == 0: |
| 20 | log.error(err_prefix + " Zero length string") |
| 21 | return 0, True |
| 22 | if x[0] == '0' and len(x) > 2: |
| 23 | if x[1] in 'bB': |
| 24 | validch = '01' |
| 25 | elif x[1] in 'oO': |
| 26 | validch = '01234567' |
| 27 | elif x[1] in 'xX': |
| 28 | validch = '0123456789abcdefABCDEF' |
| 29 | else: |
| 30 | log.error(err_prefix + |
| 31 | ": int must start digit, 0b, 0B, 0o, 0O, 0x or 0X") |
| 32 | return 0, True |
| 33 | for c in x[2:]: |
| 34 | if not c in validch: |
| 35 | log.error(err_prefix + ": Bad character " + c + " in " + x) |
| 36 | return 0, True |
| 37 | else: |
| 38 | if not x.isdecimal(): |
| 39 | log.error(err_prefix + ": Number not valid int " + x) |
| 40 | return 0, 1 |
| 41 | return int(x, 0), False |
| 42 | |
| 43 | |
| 44 | def check_single(line, char, fullline, err): |
| 45 | ''' Check for character char in input line |
| 46 | Return True if there is one (or more) |
| 47 | Propagate err or force to err True if more than one char |
| 48 | ''' |
| 49 | res = False |
| 50 | if char in line: |
| 51 | res = True |
| 52 | if line.count(char) > 1: |
| 53 | log.warning('Multiple ' + char + ' in line ' + fullline) |
| 54 | err = True |
| 55 | return res, err |
| 56 | |
| 57 | |
| 58 | I2cOp = namedtuple('I2cOp', |
| 59 | 'read rcont start stop nackok mvalue adr size fbyte tag') |
| 60 | |
| 61 | |
| 62 | def check_and_size(iic, line): |
| 63 | ''' Check I2C Op for validity and return size in bits |
| 64 | ''' |
| 65 | err = False |
| 66 | if iic.start and iic.read: |
| 67 | log.error('Start and Read found in ' + line) |
| 68 | err = True |
| 69 | if iic.rcont and not iic.read: |
| 70 | log.error('RCont without Read found in ' + line) |
| 71 | err = True |
| 72 | |
| 73 | # documentation says R+C and P is not permitted, but I think it |
| 74 | # is needed for protocols where the last read data is ACKed? |
| 75 | # (these don't match I2C or SMBus spec but exist in the wild) |
| 76 | |
| 77 | size = 0 |
| 78 | if iic.start: |
| 79 | size += 1 |
| 80 | if iic.stop: |
| 81 | size += 1 |
| 82 | if iic.read: |
| 83 | # multi is D0, D1, ..., Dn-1 so 3 byte/acks and a 1 bit squiggle |
| 84 | # regular read is one byte/ack per |
| 85 | size += 9 * 3 + 1 if iic.mvalue else 9 * iic.fbyte |
| 86 | else: |
| 87 | # write data is one byte/ack |
| 88 | size += 9 |
| 89 | # rcont, nackok just affect the polarity of the final ack bit |
| 90 | # adr just affects how the write data is drawn |
| 91 | return size, err |
| 92 | |
| 93 | |
| 94 | def parse_i2c_fifodata(line): |
| 95 | ''' Parse input line of I2C FDATA fifo and convert to internal type |
| 96 | Line is usually 0x + the hex value written to the register |
| 97 | But could be binary (0b), octal (0o) or decimal |
| 98 | ''' |
| 99 | fifodata, err = check_int(line, 'FIFO value') |
| 100 | # bit values here must match the register definition! |
| 101 | ress = (fifodata & 0x0100) != 0 |
| 102 | resp = (fifodata & 0x0200) != 0 |
| 103 | resr = (fifodata & 0x0400) != 0 |
| 104 | resc = (fifodata & 0x0800) != 0 |
| 105 | resn = (fifodata & 0x1000) != 0 |
| 106 | resb = fifodata & 0xff |
| 107 | resm = False # only used in descriptive case |
| 108 | resa = False # only used in descriptive case |
| 109 | |
| 110 | tmpr = I2cOp(resr, resc, ress, resp, resn, resm, resa, 0, resb, None) |
| 111 | size, serr = check_and_size(tmpr, line) |
| 112 | if serr: |
| 113 | err = True |
| 114 | return I2cOp(resr, resc, ress, resp, resn, resm, resa, size, resb, |
| 115 | None), err |
| 116 | |
| 117 | |
| 118 | def parse_i2c_code(line): |
| 119 | ''' Parse input line of I2C FDATA fifo and convert to internal type |
| 120 | Line is coded with flags and an 8-bit data value |
| 121 | S - Start flag, P - stop flag, |
| 122 | R - read flag, C - continue read flag, N - NackOk flag |
| 123 | followed by the data byte |
Scott Johnson | fe79c4b | 2020-07-08 10:31:08 -0700 | [diff] [blame] | 124 | Special cases: |
Mark Hayter | 14336e7 | 2019-12-29 16:21:34 -0800 | [diff] [blame] | 125 | M - indicates multiple bytes instead of data byte |
| 126 | A - followed by 0 or 1 address/direction or 2 address/data |
| 127 | Data value in quotes is a tag |
| 128 | ''' |
| 129 | resr, resc, ress, resp, resn = False, False, False, False, False |
| 130 | resm, resa, resb = False, False, 0 |
| 131 | |
| 132 | err = False |
| 133 | firstval = 0 |
| 134 | for i in line: |
| 135 | if i.isdigit() or i == "'": |
| 136 | break |
| 137 | firstval += 1 |
| 138 | # Will only check the flags section, so no concern about hex digits |
| 139 | ress, err = check_single(line[:firstval], 'S', line, err) |
| 140 | resp, err = check_single(line[:firstval], 'P', line, err) |
| 141 | resr, err = check_single(line[:firstval], 'R', line, err) |
| 142 | resc, err = check_single(line[:firstval], 'C', line, err) |
| 143 | resn, err = check_single(line[:firstval], 'N', line, err) |
| 144 | # these two are formally part of the value but parse like flags |
| 145 | resm, err = check_single(line[:firstval], 'M', line, err) |
| 146 | resa, err = check_single(line[:firstval], 'A', line, err) |
| 147 | |
| 148 | if firstval == len(line): |
| 149 | if not resm: |
| 150 | err = True |
| 151 | log.error('No value found in ' + line) |
| 152 | rest = None |
| 153 | else: |
| 154 | if resm: |
| 155 | err = True |
| 156 | log.error('Found M and value in ' + line) |
| 157 | rest = None |
| 158 | resb = 0 |
| 159 | elif line[firstval] == "'": |
| 160 | rest = line[firstval + 1:-1] |
| 161 | resb = 0 |
| 162 | else: |
| 163 | rest = None |
| 164 | resb, verr = check_int(line[firstval:], |
| 165 | 'Value in ' + line + ' ' + str(firstval)) |
| 166 | if verr: |
| 167 | err = True |
| 168 | if resb < 0 or resb > 255 or (resa and resb > 2): |
| 169 | log.error('Value out of range in ' + line) |
| 170 | resb = 0 |
| 171 | err = True |
| 172 | |
| 173 | tmpr = I2cOp(resr, resc, ress, resp, resn, resm, resa, 0, resb, rest) |
| 174 | size, serr = check_and_size(tmpr, line) |
| 175 | if serr: |
| 176 | err = True |
| 177 | return I2cOp(resr, resc, ress, resp, resn, resm, resa, size, resb, |
| 178 | rest), err |
| 179 | |
| 180 | |
| 181 | def parse_file(infile, fifodata=False, prefix=None): |
| 182 | ''' Parse a file of I2C data |
| 183 | fifodata indicates if the data is a dump from writes to FDATA fifo |
| 184 | prefix is a prefix on valid lines and will be stripped |
| 185 | lines without the prefix are ignored |
| 186 | Returns list of I2cOps or str (for titles) |
| 187 | ''' |
| 188 | transaction = [] |
| 189 | errors = 0 |
| 190 | firstline = True |
| 191 | |
| 192 | for line in infile: |
| 193 | if prefix: |
| 194 | if not line.startswith(prefix): |
| 195 | continue |
| 196 | line = line[len(prefix):] |
| 197 | |
| 198 | if len(line) == 0 or line.isspace() or line[0] == '#': |
| 199 | continue |
| 200 | line = line.lstrip().rstrip() |
| 201 | if line[0] == 'T': |
| 202 | transaction.append(line[1:].lstrip()) |
| 203 | continue |
| 204 | schar = ',' |
| 205 | if fifodata and not ',' in line: |
| 206 | # fifodata could also be whitespace spearated |
| 207 | schar = None |
| 208 | |
| 209 | for sline in line.split(sep=schar): |
| 210 | if fifodata: |
| 211 | t, err = parse_i2c_fifodata(sline) |
| 212 | else: |
| 213 | t, err = parse_i2c_code(sline) |
| 214 | if err: |
| 215 | errors += 1 |
| 216 | else: |
| 217 | transaction.append(t) |
| 218 | if errors > 0: |
| 219 | log.error('Found ' + str(errors) + ' errors in input') |
| 220 | return transaction |
| 221 | |
| 222 | |
| 223 | def output_debug(outfile, t, term): |
| 224 | for tr in t: |
| 225 | outfile.write(str(tr) + term) |
| 226 | |
| 227 | |
| 228 | def text_element(tr, term, titles): |
| 229 | if isinstance(tr, str): |
| 230 | if titles: |
| 231 | return 'T ' + tr + term |
| 232 | return '' |
| 233 | flags = 'S' if tr.start else '.' |
| 234 | flags += 'P' if tr.stop else '.' |
| 235 | flags += 'R' if tr.read else '.' |
| 236 | flags += 'C' if tr.rcont else '.' |
| 237 | flags += 'N' if tr.nackok else '.' |
| 238 | |
| 239 | # mvalue and adr are only for drawing, but can propagate in value |
| 240 | if tr.adr: |
| 241 | val = 'A' + str(tr.fbyte) |
| 242 | else: |
| 243 | if tr.tag: |
| 244 | val = "'" + tr.tag + "'" |
| 245 | else: |
| 246 | val = 'M' if tr.mvalue else hex(tr.fbyte) |
| 247 | return flags + ' ' + val + term |
| 248 | |
| 249 | |
| 250 | def output_text(outfile, transactions, term, titles=True): |
| 251 | for tr in transactions: |
| 252 | text = text_element(tr, term, titles) |
| 253 | if text: |
| 254 | outfile.write(text) |
| 255 | |
| 256 | |
| 257 | # use will place a defined group at the given x,y |
| 258 | def svg_use(item, x, y): |
| 259 | return ' <use href="#' + item + '" x="' + str(x) + \ |
| 260 | '" y="' + str(y) + '" />\n' |
| 261 | |
| 262 | |
| 263 | # a byte write is a byte of data from the host and an ack from the device |
| 264 | def svg_wrbyte(xpos, ypos, nok, label): |
| 265 | rtext = svg_use('hbyte', xpos, ypos) |
| 266 | rtext += ' <text x="' + str(xpos + (sdata.bytew / 2)) |
| 267 | rtext += '" y="' + str(ypos + sdata.txty) + '">\n' |
| 268 | rtext += label |
| 269 | rtext += '</text>\n' |
| 270 | xpos += sdata.bytew |
| 271 | if nok: |
| 272 | rtext += svg_use('norackd', xpos, ypos) |
| 273 | else: |
| 274 | rtext += svg_use('ackd', xpos, ypos) |
| 275 | xpos += sdata.bitw |
| 276 | return rtext, xpos |
| 277 | |
| 278 | |
| 279 | # a byte read is a byte of data from the device and an ack/nack from the host |
| 280 | def svg_rdbyte(xpos, ypos, ack, label): |
| 281 | rtext = svg_use('dbyte', xpos, ypos) |
| 282 | rtext += ' <text x="' + str(xpos + (sdata.bytew / 2)) |
| 283 | rtext += '" y="' + str(ypos + sdata.txty) + '">\n' |
| 284 | rtext += label |
| 285 | rtext += '</text>\n' |
| 286 | xpos += sdata.bytew |
| 287 | rtext += svg_use(ack, xpos, ypos) |
| 288 | xpos += sdata.bitw |
| 289 | return rtext, xpos |
| 290 | |
| 291 | |
| 292 | def svg_element(tr, xpos, ypos): |
| 293 | etext = '' |
| 294 | if tr.start: |
| 295 | etext += svg_use('start', xpos, ypos) |
| 296 | xpos += sdata.bitw |
| 297 | |
| 298 | if tr.read and not tr.mvalue: |
| 299 | for n in range(0, 1 if tr.tag else tr.fbyte): |
| 300 | acktype = 'ackh' if (n < tr.fbyte - 1) or tr.rcont else 'nackh' |
| 301 | t, xpos = svg_rdbyte(xpos, ypos, acktype, |
| 302 | tr.tag if tr.tag else 'D' + str(n + 1)) |
| 303 | etext += t |
| 304 | if xpos > sdata.wrap and (n < tr.fbyte - 1): |
| 305 | xpos = sdata.cindent |
| 306 | ypos += sdata.linesep |
| 307 | elif tr.read and tr.mvalue: |
| 308 | # need space to draw three byte+ack and a break squiggle |
| 309 | if (xpos + (sdata.bytew + sdata.bitw) * 3 + sdata.bitw) > sdata.wrap: |
| 310 | xpos = sdata.cindent |
| 311 | ypos += sdata.linesep |
| 312 | t, xpos = svg_rdbyte(xpos, ypos, 'ackh', 'Data1') |
| 313 | etext += t |
| 314 | t, xpos = svg_rdbyte(xpos, ypos, 'ackh', 'Data2') |
| 315 | etext += t |
| 316 | etext += svg_use('skip', xpos, ypos) |
| 317 | xpos += sdata.bitw |
| 318 | t, xpos = svg_rdbyte(xpos, ypos, 'nackh', 'DataN') |
| 319 | etext += t |
| 320 | |
| 321 | elif tr.adr: |
| 322 | etext += svg_use('adr' + str(tr.fbyte), xpos, ypos) |
| 323 | xpos += sdata.bytew |
| 324 | etext += svg_use('ackd', xpos, ypos) |
| 325 | xpos += sdata.bitw |
| 326 | elif tr.mvalue: |
| 327 | # need space to draw three byte+ack and a break squiggle |
| 328 | if (xpos + (sdata.bytew + sdata.bitw) * 3 + sdata.bitw) > sdata.wrap: |
| 329 | xpos = sdata.cindent |
| 330 | ypos += sdata.linesep |
| 331 | t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, 'Data1') |
| 332 | etext += t |
| 333 | t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, 'Data2') |
| 334 | etext += t |
| 335 | etext += svg_use('skip', xpos, ypos) |
| 336 | xpos += sdata.bitw |
| 337 | t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, 'DataN') |
| 338 | etext += t |
| 339 | |
| 340 | elif tr.start: # and not tr.adr by position in elif |
| 341 | etext += svg_use('adr' + str(tr.fbyte & 1), xpos, ypos) |
| 342 | etext += ' <text x="' + str(xpos + 115) |
| 343 | etext += '" y="' + str(ypos + sdata.txty) + '">' + hex(tr.fbyte >> 1) |
| 344 | etext += '</text>\n' |
| 345 | xpos += sdata.bytew |
| 346 | etext += svg_use('ackd', xpos, ypos) |
| 347 | xpos += sdata.bitw |
| 348 | |
| 349 | else: |
| 350 | t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, |
| 351 | tr.tag if tr.tag else hex(tr.fbyte)) |
| 352 | etext += t |
| 353 | |
| 354 | if tr.stop: |
| 355 | etext += svg_use('pstop', xpos, ypos) |
| 356 | xpos += sdata.bitw |
| 357 | |
| 358 | return etext, xpos, ypos |
| 359 | |
| 360 | |
| 361 | # since they are referenced by href name the style and defs only |
| 362 | # go in the first svg in a file |
| 363 | first_svg = True |
| 364 | |
| 365 | |
| 366 | def out_svg(outfile, svg, ypos, svgtext): |
| 367 | global first_svg |
| 368 | outfile.write('<svg\n' + sdata.svgtag_consts) |
| 369 | outfile.write('viewBox="0 0 ' + str(sdata.svgw) + ' ' + |
| 370 | str(ypos + sdata.linesep + 8) + '">\n') |
| 371 | if (first_svg): |
| 372 | outfile.write(sdata.svgstyle + sdata.svg_defs) |
| 373 | first_svg = False |
| 374 | outfile.write(svg) |
| 375 | if svgtext: |
| 376 | outfile.write('<text x="10" y="' + str(ypos + sdata.linesep + 3)) |
| 377 | outfile.write('" class="tt">' + svgtext[:-2] + '</text>\n') |
| 378 | outfile.write('</svg>\n') |
| 379 | |
| 380 | |
| 381 | def output_svg(outfile, transactions, title): |
| 382 | xpos = 0 |
| 383 | ypos = 0 |
| 384 | svg = '' |
| 385 | svgtext = '' |
| 386 | for tr in transactions: |
| 387 | if isinstance(tr, str): |
| 388 | if svg: |
| 389 | out_svg(outfile, svg, ypos, svgtext) |
| 390 | if title: |
| 391 | outfile.write('<h2>' + tr + '</h2>\n') |
| 392 | xpos = 0 |
| 393 | ypos = 0 |
| 394 | svg = '' |
| 395 | svgtext = '' |
| 396 | continue |
| 397 | if xpos > sdata.wrap: |
| 398 | xpos = sdata.cindent |
| 399 | ypos += sdata.linesep |
| 400 | trsvg, xpos, ypos = svg_element(tr, xpos, ypos) |
| 401 | svgtext += text_element(tr, ', ', False) |
| 402 | svg += trsvg |
| 403 | |
| 404 | out_svg(outfile, svg, ypos, svgtext) |