#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright 2020 The SkyWater PDK Authors. # # Use of this source code is governed by the Apache 2.0 # license that can be found in the LICENSE file or at # https://www.apache.org/licenses/LICENSE-2.0 # # SPDX-License-Identifier: Apache-2.0 ''' VCD waveform to wawedrom script/SVG conversion script. ''' from __future__ import print_function import os import sys import argparse import pathlib import wavedrom from contextlib import contextmanager wavedrom_template ="""\ {{ signal: [ {signals} ]}}""" signal_template = " {{ name: \"{name}\", {fill}wave: '{wave}' }}" def eprint(*args, **kwargs): ''' Print to stderr ''' print(*args, file=sys.stderr, **kwargs) @contextmanager def file_or_stdout(file): ''' Open file or stdout if file is None ''' if file is None: yield sys.stdout else: with file.open('w') as out_file: yield out_file def readVCD (file): ''' Parses VCD file. Args: file - path to a VCD file [pathlib.Path] Returns: vcd - dictionary containing vcd sections [dict] ''' eprint() eprint(file.name) assert file.is_file(), file vcd = {} with file.open('r') as f: currtag = 'body' for line in f: # regular line if not line.startswith('$'): vcd[currtag] = vcd.setdefault(currtag, '') + line continue # tag, other than end if not line.startswith('$end'): currtag = line.partition(' ')[0].lstrip('$').rstrip() vcd[currtag] = vcd.setdefault(currtag, '') + line.partition(' ')[2].rpartition('$')[0] # line ends with end tag if not vcd[currtag].endswith('\n'): vcd[currtag] += '\n' if line.split()[-1]=='$end': currtag = 'body' vcd[currtag] = '' if 'var' not in vcd or 'dumpvars' not in vcd: raise SyntaxError("Invalid VCD file format") return vcd def parsetowavedrom (file, savetofile = False): ''' Reads and simplifies VCD waveform Generates wavedrom notation. Args: file - path to a VCD file [pathlib.Path] ''' varsubst = {} # var substitution reg = [] # list of signals wire = [] # list of signals (wire class) wave = {} # waveform event = [] # event timings vcd = readVCD (file) # parse vars for line in vcd['var'].split('\n'): line = line.strip().split() if len(line)<4: if len(line): print (f"Warning: malformed var definition {' '.join(line)}") continue if line[1]!='1': print (f"Warning: bus in vars (unsupported) {' '.join(line)}") if line[0]=='reg': reg.append(line[3]) varsubst[line[2]] = line[3] if line[0]=='wire': wire.append(line[3]) varsubst[line[2]] = line[3] # set initial states event.append(0) #default for v in reg+wire: wave[v] = ['x'] #defined for line in vcd['dumpvars'].split('\n'): if len(line)>=2: wave[ varsubst[line[1]] ] = [line[0]] # parse wave body for line in vcd['body'].split('\n'): #timestamp line if line.startswith('#'): line = line.strip().lstrip('#') if not line.isnumeric(): raise SyntaxError("Invalid VCD timestamp") event.append(int(line)) for v in wave.keys(): wave[v].append('.') # state change line else : if len(line)>=2: wave [ varsubst[line[1]] ][-1] = line[0] # TODO: add "double interval support" signals = [] for v in wave.keys(): fill = ' ' * (max( [len(s) for s in wave.keys()] ) - len(v)) wavestr = ''.join(wave[v]) signals.append( signal_template.format( name = v, wave = wavestr, fill = fill ) ) signals = ',\n'.join(signals) wavedrom = wavedrom_template.format ( signals = signals ) outfile = file.with_suffix(".wdr.json") if savetofile else None with file_or_stdout(outfile) as f: f.write(wavedrom) return wavedrom def quoted_strings_wavedrom (wdr) : ''' Convert wavedrom script to more restrictive version of JSON with quoted keywords ''' wdr = wdr.replace(' signal:',' "signal":') wdr = wdr.replace(' name:',' "name":') wdr = wdr.replace(' wave:',' "wave":') wdr = wdr.replace("'",'"') return wdr def main(): ''' Converts VCD waveform to wavedrom format''' output_txt = 'output:\n stdout or [vcdname].wdr.json file and/or [vcdname].svg file' allcellpath = '../../../libraries/*/latest/cells/*/*.vcd' parser = argparse.ArgumentParser( description = main.__doc__, epilog = output_txt, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( "--all_libs", help="process all in "+allcellpath, action="store_true") parser.add_argument( "-w", "--wavedrom", help="generate wavedrom .wdr.json file", action="store_true") parser.add_argument( "-s", "--savesvg", help="generate .svg image", action="store_true") parser.add_argument( "infile", help="VCD waveform file", type=pathlib.Path, nargs="*") args = parser.parse_args() if args.all_libs: path = pathlib.Path(allcellpath).expanduser() parts = path.parts[1:] if path.is_absolute() else path.parts paths = pathlib.Path(path.root).glob(str(pathlib.Path("").joinpath(*parts))) args.infile = list(paths) infile = [d.resolve() for d in args.infile if d.is_file()] errors = 0 for f in infile: try: wdr = parsetowavedrom(f, args.wavedrom) if args.savesvg: svg = wavedrom.render( quoted_strings_wavedrom(wdr) ) outfile = f.with_suffix(".svg") svg.saveas(outfile) except KeyboardInterrupt: sys.exit(1) except (AssertionError, FileNotFoundError, ChildProcessError) as ex: eprint (f'Error: {type(ex).__name__}') eprint (f'{ex.args}') errors +=1 eprint (f'\n{len(infile)} files processed, {errors} errors.') return 0 if errors else 1 if __name__ == "__main__": sys.exit(main())