| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| # |
| # Convert I2C host format to SVG |
| |
| import logging as log |
| from collections import namedtuple |
| |
| import i2csvg.i2csvg_data as sdata |
| |
| |
| # validating version of int(x, 0) |
| # returns int value, error flag |
| # if error flag is True value will be zero |
| def check_int(x, err_prefix): |
| if isinstance(x, int): |
| return x, False |
| if len(x) == 0: |
| log.error(err_prefix + " Zero length string") |
| return 0, True |
| if x[0] == '0' and len(x) > 2: |
| if x[1] in 'bB': |
| validch = '01' |
| elif x[1] in 'oO': |
| validch = '01234567' |
| elif x[1] in 'xX': |
| validch = '0123456789abcdefABCDEF' |
| else: |
| log.error(err_prefix + |
| ": int must start digit, 0b, 0B, 0o, 0O, 0x or 0X") |
| return 0, True |
| for c in x[2:]: |
| if not c in validch: |
| log.error(err_prefix + ": Bad character " + c + " in " + x) |
| return 0, True |
| else: |
| if not x.isdecimal(): |
| log.error(err_prefix + ": Number not valid int " + x) |
| return 0, 1 |
| return int(x, 0), False |
| |
| |
| def check_single(line, char, fullline, err): |
| ''' Check for character char in input line |
| Return True if there is one (or more) |
| Propagate err or force to err True if more than one char |
| ''' |
| res = False |
| if char in line: |
| res = True |
| if line.count(char) > 1: |
| log.warning('Multiple ' + char + ' in line ' + fullline) |
| err = True |
| return res, err |
| |
| |
| I2cOp = namedtuple('I2cOp', |
| 'read rcont start stop nackok mvalue adr size fbyte tag') |
| |
| |
| def check_and_size(iic, line): |
| ''' Check I2C Op for validity and return size in bits |
| ''' |
| err = False |
| if iic.start and iic.read: |
| log.error('Start and Read found in ' + line) |
| err = True |
| if iic.rcont and not iic.read: |
| log.error('RCont without Read found in ' + line) |
| err = True |
| |
| # documentation says R+C and P is not permitted, but I think it |
| # is needed for protocols where the last read data is ACKed? |
| # (these don't match I2C or SMBus spec but exist in the wild) |
| |
| size = 0 |
| if iic.start: |
| size += 1 |
| if iic.stop: |
| size += 1 |
| if iic.read: |
| # multi is D0, D1, ..., Dn-1 so 3 byte/acks and a 1 bit squiggle |
| # regular read is one byte/ack per |
| size += 9 * 3 + 1 if iic.mvalue else 9 * iic.fbyte |
| else: |
| # write data is one byte/ack |
| size += 9 |
| # rcont, nackok just affect the polarity of the final ack bit |
| # adr just affects how the write data is drawn |
| return size, err |
| |
| |
| def parse_i2c_fifodata(line): |
| ''' Parse input line of I2C FDATA fifo and convert to internal type |
| Line is usually 0x + the hex value written to the register |
| But could be binary (0b), octal (0o) or decimal |
| ''' |
| fifodata, err = check_int(line, 'FIFO value') |
| # bit values here must match the register definition! |
| ress = (fifodata & 0x0100) != 0 |
| resp = (fifodata & 0x0200) != 0 |
| resr = (fifodata & 0x0400) != 0 |
| resc = (fifodata & 0x0800) != 0 |
| resn = (fifodata & 0x1000) != 0 |
| resb = fifodata & 0xff |
| resm = False # only used in descriptive case |
| resa = False # only used in descriptive case |
| |
| tmpr = I2cOp(resr, resc, ress, resp, resn, resm, resa, 0, resb, None) |
| size, serr = check_and_size(tmpr, line) |
| if serr: |
| err = True |
| return I2cOp(resr, resc, ress, resp, resn, resm, resa, size, resb, |
| None), err |
| |
| |
| def parse_i2c_code(line): |
| ''' Parse input line of I2C FDATA fifo and convert to internal type |
| Line is coded with flags and an 8-bit data value |
| S - Start flag, P - stop flag, |
| R - read flag, C - continue read flag, N - NackOk flag |
| followed by the data byte |
| Special cases: |
| M - indicates multiple bytes instead of data byte |
| A - followed by 0 or 1 address/direction or 2 address/data |
| Data value in quotes is a tag |
| ''' |
| resr, resc, ress, resp, resn = False, False, False, False, False |
| resm, resa, resb = False, False, 0 |
| |
| err = False |
| firstval = 0 |
| for i in line: |
| if i.isdigit() or i == "'": |
| break |
| firstval += 1 |
| # Will only check the flags section, so no concern about hex digits |
| ress, err = check_single(line[:firstval], 'S', line, err) |
| resp, err = check_single(line[:firstval], 'P', line, err) |
| resr, err = check_single(line[:firstval], 'R', line, err) |
| resc, err = check_single(line[:firstval], 'C', line, err) |
| resn, err = check_single(line[:firstval], 'N', line, err) |
| # these two are formally part of the value but parse like flags |
| resm, err = check_single(line[:firstval], 'M', line, err) |
| resa, err = check_single(line[:firstval], 'A', line, err) |
| |
| if firstval == len(line): |
| if not resm: |
| err = True |
| log.error('No value found in ' + line) |
| rest = None |
| else: |
| if resm: |
| err = True |
| log.error('Found M and value in ' + line) |
| rest = None |
| resb = 0 |
| elif line[firstval] == "'": |
| rest = line[firstval + 1:-1] |
| resb = 0 |
| else: |
| rest = None |
| resb, verr = check_int(line[firstval:], |
| 'Value in ' + line + ' ' + str(firstval)) |
| if verr: |
| err = True |
| if resb < 0 or resb > 255 or (resa and resb > 2): |
| log.error('Value out of range in ' + line) |
| resb = 0 |
| err = True |
| |
| tmpr = I2cOp(resr, resc, ress, resp, resn, resm, resa, 0, resb, rest) |
| size, serr = check_and_size(tmpr, line) |
| if serr: |
| err = True |
| return I2cOp(resr, resc, ress, resp, resn, resm, resa, size, resb, |
| rest), err |
| |
| |
| def parse_file(infile, fifodata=False, prefix=None): |
| ''' Parse a file of I2C data |
| fifodata indicates if the data is a dump from writes to FDATA fifo |
| prefix is a prefix on valid lines and will be stripped |
| lines without the prefix are ignored |
| Returns list of I2cOps or str (for titles) |
| ''' |
| transaction = [] |
| errors = 0 |
| firstline = True |
| |
| for line in infile: |
| if prefix: |
| if not line.startswith(prefix): |
| continue |
| line = line[len(prefix):] |
| |
| if len(line) == 0 or line.isspace() or line[0] == '#': |
| continue |
| line = line.lstrip().rstrip() |
| if line[0] == 'T': |
| transaction.append(line[1:].lstrip()) |
| continue |
| schar = ',' |
| if fifodata and not ',' in line: |
| # fifodata could also be whitespace spearated |
| schar = None |
| |
| for sline in line.split(sep=schar): |
| if fifodata: |
| t, err = parse_i2c_fifodata(sline) |
| else: |
| t, err = parse_i2c_code(sline) |
| if err: |
| errors += 1 |
| else: |
| transaction.append(t) |
| if errors > 0: |
| log.error('Found ' + str(errors) + ' errors in input') |
| return transaction |
| |
| |
| def output_debug(outfile, t, term): |
| for tr in t: |
| outfile.write(str(tr) + term) |
| |
| |
| def text_element(tr, term, titles): |
| if isinstance(tr, str): |
| if titles: |
| return 'T ' + tr + term |
| return '' |
| flags = 'S' if tr.start else '.' |
| flags += 'P' if tr.stop else '.' |
| flags += 'R' if tr.read else '.' |
| flags += 'C' if tr.rcont else '.' |
| flags += 'N' if tr.nackok else '.' |
| |
| # mvalue and adr are only for drawing, but can propagate in value |
| if tr.adr: |
| val = 'A' + str(tr.fbyte) |
| else: |
| if tr.tag: |
| val = "'" + tr.tag + "'" |
| else: |
| val = 'M' if tr.mvalue else hex(tr.fbyte) |
| return flags + ' ' + val + term |
| |
| |
| def output_text(outfile, transactions, term, titles=True): |
| for tr in transactions: |
| text = text_element(tr, term, titles) |
| if text: |
| outfile.write(text) |
| |
| |
| # use will place a defined group at the given x,y |
| def svg_use(item, x, y): |
| return ' <use href="#' + item + '" x="' + str(x) + \ |
| '" y="' + str(y) + '" />\n' |
| |
| |
| # a byte write is a byte of data from the host and an ack from the device |
| def svg_wrbyte(xpos, ypos, nok, label): |
| rtext = svg_use('hbyte', xpos, ypos) |
| rtext += ' <text x="' + str(xpos + (sdata.bytew / 2)) |
| rtext += '" y="' + str(ypos + sdata.txty) + '">\n' |
| rtext += label |
| rtext += '</text>\n' |
| xpos += sdata.bytew |
| if nok: |
| rtext += svg_use('norackd', xpos, ypos) |
| else: |
| rtext += svg_use('ackd', xpos, ypos) |
| xpos += sdata.bitw |
| return rtext, xpos |
| |
| |
| # a byte read is a byte of data from the device and an ack/nack from the host |
| def svg_rdbyte(xpos, ypos, ack, label): |
| rtext = svg_use('dbyte', xpos, ypos) |
| rtext += ' <text x="' + str(xpos + (sdata.bytew / 2)) |
| rtext += '" y="' + str(ypos + sdata.txty) + '">\n' |
| rtext += label |
| rtext += '</text>\n' |
| xpos += sdata.bytew |
| rtext += svg_use(ack, xpos, ypos) |
| xpos += sdata.bitw |
| return rtext, xpos |
| |
| |
| def svg_element(tr, xpos, ypos): |
| etext = '' |
| if tr.start: |
| etext += svg_use('start', xpos, ypos) |
| xpos += sdata.bitw |
| |
| if tr.read and not tr.mvalue: |
| for n in range(0, 1 if tr.tag else tr.fbyte): |
| acktype = 'ackh' if (n < tr.fbyte - 1) or tr.rcont else 'nackh' |
| t, xpos = svg_rdbyte(xpos, ypos, acktype, |
| tr.tag if tr.tag else 'D' + str(n + 1)) |
| etext += t |
| if xpos > sdata.wrap and (n < tr.fbyte - 1): |
| xpos = sdata.cindent |
| ypos += sdata.linesep |
| elif tr.read and tr.mvalue: |
| # need space to draw three byte+ack and a break squiggle |
| if (xpos + (sdata.bytew + sdata.bitw) * 3 + sdata.bitw) > sdata.wrap: |
| xpos = sdata.cindent |
| ypos += sdata.linesep |
| t, xpos = svg_rdbyte(xpos, ypos, 'ackh', 'Data1') |
| etext += t |
| t, xpos = svg_rdbyte(xpos, ypos, 'ackh', 'Data2') |
| etext += t |
| etext += svg_use('skip', xpos, ypos) |
| xpos += sdata.bitw |
| t, xpos = svg_rdbyte(xpos, ypos, 'nackh', 'DataN') |
| etext += t |
| |
| elif tr.adr: |
| etext += svg_use('adr' + str(tr.fbyte), xpos, ypos) |
| xpos += sdata.bytew |
| etext += svg_use('ackd', xpos, ypos) |
| xpos += sdata.bitw |
| elif tr.mvalue: |
| # need space to draw three byte+ack and a break squiggle |
| if (xpos + (sdata.bytew + sdata.bitw) * 3 + sdata.bitw) > sdata.wrap: |
| xpos = sdata.cindent |
| ypos += sdata.linesep |
| t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, 'Data1') |
| etext += t |
| t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, 'Data2') |
| etext += t |
| etext += svg_use('skip', xpos, ypos) |
| xpos += sdata.bitw |
| t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, 'DataN') |
| etext += t |
| |
| elif tr.start: # and not tr.adr by position in elif |
| etext += svg_use('adr' + str(tr.fbyte & 1), xpos, ypos) |
| etext += ' <text x="' + str(xpos + 115) |
| etext += '" y="' + str(ypos + sdata.txty) + '">' + hex(tr.fbyte >> 1) |
| etext += '</text>\n' |
| xpos += sdata.bytew |
| etext += svg_use('ackd', xpos, ypos) |
| xpos += sdata.bitw |
| |
| else: |
| t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, |
| tr.tag if tr.tag else hex(tr.fbyte)) |
| etext += t |
| |
| if tr.stop: |
| etext += svg_use('pstop', xpos, ypos) |
| xpos += sdata.bitw |
| |
| return etext, xpos, ypos |
| |
| |
| # since they are referenced by href name the style and defs only |
| # go in the first svg in a file |
| first_svg = True |
| |
| |
| def out_svg(outfile, svg, ypos, svgtext): |
| global first_svg |
| outfile.write('<svg\n' + sdata.svgtag_consts) |
| outfile.write('viewBox="0 0 ' + str(sdata.svgw) + ' ' + |
| str(ypos + sdata.linesep + 8) + '">\n') |
| if (first_svg): |
| outfile.write(sdata.svgstyle + sdata.svg_defs) |
| first_svg = False |
| outfile.write(svg) |
| if svgtext: |
| outfile.write('<text x="10" y="' + str(ypos + sdata.linesep + 3)) |
| outfile.write('" class="tt">' + svgtext[:-2] + '</text>\n') |
| outfile.write('</svg>\n') |
| |
| |
| def output_svg(outfile, transactions, title): |
| xpos = 0 |
| ypos = 0 |
| svg = '' |
| svgtext = '' |
| for tr in transactions: |
| if isinstance(tr, str): |
| if svg: |
| out_svg(outfile, svg, ypos, svgtext) |
| if title: |
| outfile.write('<h2>' + tr + '</h2>\n') |
| xpos = 0 |
| ypos = 0 |
| svg = '' |
| svgtext = '' |
| continue |
| if xpos > sdata.wrap: |
| xpos = sdata.cindent |
| ypos += sdata.linesep |
| trsvg, xpos, ypos = svg_element(tr, xpos, ypos) |
| svgtext += text_element(tr, ', ', False) |
| svg += trsvg |
| |
| out_svg(outfile, svg, ypos, svgtext) |