From 742d1a87d04ad1bfca7439d82e629aaadf2b296a Mon Sep 17 00:00:00 2001 From: Wojciech Gryncewicz Date: Fri, 27 Nov 2020 11:10:20 +0100 Subject: [PATCH 1/5] Added cell VCD waveform generator script --- environment.yml | 3 + .../skywater_pdk/cells/generate/waveform.py | 162 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100755 scripts/python-skywater-pdk/skywater_pdk/cells/generate/waveform.py diff --git a/environment.yml b/environment.yml index ee13527..08e754a 100644 --- a/environment.yml +++ b/environment.yml @@ -20,6 +20,9 @@ channels: dependencies: - python=3.8 - pip +- yosys +- netlistsvg +- verilog # Packages installed from PyPI - pip: - -r file:requirements.txt diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/waveform.py b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/waveform.py new file mode 100755 index 0000000..ec3b865 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/waveform.py @@ -0,0 +1,162 @@ +#!/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 + +''' This is a cell VCD waveform generation script. +''' + +import csv +import json +import os +import sys +import argparse +import pathlib +import glob +import subprocess +import textwrap +import re + + +def write_vcd (cellpath, define_data, use_power_pins=False): + ''' Generates vcd for a given cell. + + Args: + cellpath - path to a cell [str of pathlib.Path] + define_data - cell data from json [dic] + use_power_pins - include power pins toggling in simulation [bool] + ''' + + # collect power port names + pp = [] + for p in define_data['ports']: + if len(p)>2 and p[0]=='power': + pp.append(p[1]) + + # define output file(s) + ppsuffix = '.pp' if use_power_pins else '' + outfile = os.path.join(cellpath, define_data['file_prefix'] + ppsuffix + '.vcd') + vppfile = os.path.join(cellpath, define_data['file_prefix'] + '.vpp.tmp') + tmptestbed = os.path.join(cellpath, define_data['file_prefix'] + '.tb.v.tmp') + + # find and patch Verilog testbed file + testbedfile = os.path.join(cellpath, define_data['file_prefix'] + '.tb.v') + assert os.path.exists(testbedfile), testbedfile + insertppdefine = use_power_pins + prvline='' + with open(tmptestbed,'w') as ttb: + with open(testbedfile,'r') as tbf: + for line in tbf: + # add use_power_pins define + if insertppdefine and line.startswith('`include'): + line = '`define USE_POWER_PINS\n' + line + insertppdefine = False + # add dumpfile define + if prvline.strip(' \n\r')=='begin': + line = line[:-len(line.lstrip())] + \ + '$dumpfile("' + outfile + '");\n' + \ + line[:-len(line.lstrip())] + \ + '$dumpvars(1,top);\n' + \ + line + # remove power pins from reg - optinal, but makes output more readable + if not use_power_pins: + for p in pp: + if re.search( 'reg\s+'+p, line ) is not None or \ + re.search( p+'\s+\=', line ) is not None : + line='' + break + # remove power pins from dut + if not use_power_pins and define_data['file_prefix']+' dut' in line: + for p in pp: + line = line.replace(f'.{p}({p}),','') + line = line.replace(f'.{p}({p}))',')') + prvline = line + ttb.write(line) + + # generate vpp code and vcd recording + if subprocess.call(['iverilog', '-o', vppfile, tmptestbed], cwd=cellpath): + raise ChildProcessError("Icarus Verilog compilation failed") + if subprocess.call(['vvp', vppfile], cwd=cellpath): + raise ChildProcessError("Icarus Verilog runtime failed") + + # remove temporary files + os.remove(tmptestbed) + os.remove(vppfile) + + +def process(cellpath): + ''' Processes cell indicated by path. + Opens cell definiton and calls further processing + + Args: + cellpath - path to a cell [str of pathlib.Path] + ''' + + print() + print(cellpath) + define_json = os.path.join(cellpath, 'definition.json') + if not os.path.exists(define_json): + print("No definition.json in", cellpath) + assert os.path.exists(define_json), define_json + define_data = json.load(open(define_json)) + + if define_data['type'] == 'cell': + write_vcd(cellpath, define_data, use_power_pins = False) + write_vcd(cellpath, define_data, use_power_pins = True) + + return + + +def main(): + ''' Generates VCD waveform for cell.''' + + prereq_txt = '' + output_txt = 'output:\n generates [fullcellname].vcd' + allcellpath = '../../../libraries/*/latest/cells/*' + + parser = argparse.ArgumentParser( + description = main.__doc__, + epilog = prereq_txt +'\n\n'+ output_txt, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + "--all_libs", + help="process all cells in "+allcellpath, + action="store_true") + parser.add_argument( + "cell_dir", + help="path to the cell directory", + 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.cell_dir = list(paths) + + cell_dirs = [d.resolve() for d in args.cell_dir if d.is_dir()] + + errors = 0 + for d in cell_dirs: + try: + process(d) + except KeyboardInterrupt: + sys.exit(1) + except (AssertionError, FileNotFoundError, ChildProcessError) as ex: + print (f'Error: {type(ex).__name__}') + print (f'{ex.args}') + errors +=1 + print (f'\n{len(cell_dirs)} files processed, {errors} errors.') + return 0 if errors else 1 + +if __name__ == "__main__": + sys.exit(main()) + From 09197ef30e02045f7d7fcb5facf180bb3c60d9ab Mon Sep 17 00:00:00 2001 From: Wojciech Gryncewicz Date: Fri, 27 Nov 2020 18:45:39 +0100 Subject: [PATCH 2/5] Changed Icarus verilog package name in environment.yml --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 08e754a..320a1a5 100644 --- a/environment.yml +++ b/environment.yml @@ -22,7 +22,7 @@ dependencies: - pip - yosys - netlistsvg -- verilog +- iverilog # Packages installed from PyPI - pip: - -r file:requirements.txt From c7c5dd8a5080eba3a6b86d5ab34d19ae0a9cfaee Mon Sep 17 00:00:00 2001 From: Wojciech Gryncewicz Date: Mon, 30 Nov 2020 18:40:38 +0100 Subject: [PATCH 3/5] VCD to wavedrom JSON/SVG converter Signed-off-by: Wojciech Gryncewicz --- requirements.txt | 1 + .../cells/generate/vcd2wavedrom.py | 225 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100755 scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py diff --git a/requirements.txt b/requirements.txt index fe8a2c1..41b39cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flake8 +wavedrom # rst_include tool as GitHub doesn't support `.. include::` when rendering # previews. diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py new file mode 100755 index 0000000..69f3ef4 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py @@ -0,0 +1,225 @@ +#!/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()) + From adea4260e7579250f15719e3dd801c3570abf949 Mon Sep 17 00:00:00 2001 From: Wojciech Gryncewicz Date: Tue, 1 Dec 2020 13:44:17 +0100 Subject: [PATCH 4/5] Improved handling of testbenches with infinite clock Signed-off-by: Wojciech Gryncewicz --- .../skywater_pdk/cells/generate/vcd2wavedrom.py | 12 +++++++----- .../skywater_pdk/cells/generate/waveform.py | 9 ++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py index 69f3ef4..ed9b43b 100755 --- a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py +++ b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py @@ -75,8 +75,11 @@ def readVCD (file): currtag = 'body' vcd[currtag] = '' - if 'var' not in vcd or 'dumpvars' not in vcd: - raise SyntaxError("Invalid VCD file format") + if 'var' not in vcd: + raise SyntaxError("No variables recorded in VCD file") + if 'dumpvars' not in vcd: + print ("Warning: intial variable states undefined") + var['dumpvars'] = '' return vcd @@ -213,9 +216,8 @@ def main(): svg.saveas(outfile) except KeyboardInterrupt: sys.exit(1) - except (AssertionError, FileNotFoundError, ChildProcessError) as ex: - eprint (f'Error: {type(ex).__name__}') - eprint (f'{ex.args}') + except (SyntaxError, AssertionError, FileNotFoundError, ChildProcessError) as ex: + eprint (f'{type(ex).__name__}: {", ".join(ex.args)}') errors +=1 eprint (f'\n{len(infile)} files processed, {errors} errors.') return 0 if errors else 1 diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/waveform.py b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/waveform.py index ec3b865..a21d2ad 100755 --- a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/waveform.py +++ b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/waveform.py @@ -49,6 +49,8 @@ def write_vcd (cellpath, define_data, use_power_pins=False): testbedfile = os.path.join(cellpath, define_data['file_prefix'] + '.tb.v') assert os.path.exists(testbedfile), testbedfile insertppdefine = use_power_pins + insertdumpvars = True + insertfinish = True prvline='' with open(tmptestbed,'w') as ttb: with open(testbedfile,'r') as tbf: @@ -58,12 +60,17 @@ def write_vcd (cellpath, define_data, use_power_pins=False): line = '`define USE_POWER_PINS\n' + line insertppdefine = False # add dumpfile define - if prvline.strip(' \n\r')=='begin': + if insertdumpvars and prvline.strip(' \n\r')=='begin': line = line[:-len(line.lstrip())] + \ '$dumpfile("' + outfile + '");\n' + \ line[:-len(line.lstrip())] + \ '$dumpvars(1,top);\n' + \ line + insertdumpvars = False + # add finish command, to stop paraller threads + if insertfinish and line.strip(' \n\r')=='end' and not '$finish' in prvline: + line = prvline[:-len(prvline.lstrip())] + '$finish;\n' + line + insertfinish = False # remove power pins from reg - optinal, but makes output more readable if not use_power_pins: for p in pp: From c5bd980a84ed99d427f08721619727ecff7cfa0e Mon Sep 17 00:00:00 2001 From: Wojciech Gryncewicz Date: Tue, 1 Dec 2020 15:26:39 +0100 Subject: [PATCH 5/5] Added option for reduction of long clock sequences in wavedrom converter Signed-off-by: Wojciech Gryncewicz --- .../cells/generate/vcd2wavedrom.py | 70 ++++++++++++++----- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py index ed9b43b..7e4adf1 100755 --- a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py +++ b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py @@ -18,6 +18,7 @@ import sys import argparse import pathlib import wavedrom +import re from contextlib import contextmanager @@ -44,7 +45,7 @@ def file_or_stdout(file): def readVCD (file): - ''' Parses VCD file. + ''' Parses VCD file. Args: file - path to a VCD file [pathlib.Path] @@ -67,12 +68,12 @@ def readVCD (file): # 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] + vcd[currtag] = vcd.setdefault(currtag, '') + line.partition(' ')[2].rpartition('$')[0] # line ends with end tag if not vcd[currtag].endswith('\n'): - vcd[currtag] += '\n' + vcd[currtag] += '\n' if line.split()[-1]=='$end': - currtag = 'body' + currtag = 'body' vcd[currtag] = '' if 'var' not in vcd: @@ -83,13 +84,39 @@ def readVCD (file): return vcd -def parsetowavedrom (file, savetofile = False): + +def reduce_clock_sequences (wave) : + ''' Remove clock seqnces longer than 2 cycles + not accompanied by other signals changes + + Parameters: + wave - dictionary 'signal'->['list of states'] [dict] + ''' + for v in wave: + sig = wave[v] # analized signal + other = [wave[i] for i in wave if i!=v] # list of other signals + other = [''.join(s) for s in zip(*other)] # list of concatenated states + other = [len(s.replace('.','')) for s in other] # list of state changes count + sig = [s if o==0 else ' ' for s,o in zip(sig,other)] # keep only when no changes in other + sig = "".join(sig) + cuts = [] + for m in re.finditer("(10){2,}",sig): + cuts.append( (m.start()+1, m.end()-1) ) # area to be reduced, leave 1..0 + cuts.reverse() + for cut in cuts: + for v,w in wave.items(): # reduce cuts from all signals + wave[v] = w[ :cut[0]] + w[cut[1]: ] + + return wave + + +def parsetowavedrom (file, savetofile = False, reduce_clock = 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 @@ -117,21 +144,21 @@ def parsetowavedrom (file, savetofile = False): # set initial states event.append(0) - #default + #default for v in reg+wire: wave[v] = ['x'] #defined - for line in vcd['dumpvars'].split('\n'): + for line in vcd['dumpvars'].split('\n'): if len(line)>=2: wave[ varsubst[line[1]] ] = [line[0]] - # parse wave body + # 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") + raise SyntaxError("Invalid VCD timestamp") event.append(int(line)) for v in wave.keys(): wave[v].append('.') @@ -139,11 +166,12 @@ def parsetowavedrom (file, savetofile = False): else : if len(line)>=2: wave [ varsubst[line[1]] ][-1] = line[0] - - # TODO: add "double interval support" + + if reduce_clock: + wave = reduce_clock_sequences(wave) signals = [] - for v in wave.keys(): + 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 ) ) @@ -151,15 +179,18 @@ def parsetowavedrom (file, savetofile = False): wavedrom = wavedrom_template.format ( signals = signals ) - outfile = file.with_suffix(".wdr.json") if savetofile else None + 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 + + Parameters: + wdr - wavedrom script [str] ''' wdr = wdr.replace(' signal:',' "signal":') wdr = wdr.replace(' name:',' "name":') @@ -190,6 +221,11 @@ def main(): "--savesvg", help="generate .svg image", action="store_true") + parser.add_argument( + "-r", + "--reduceclk", + help="reduce clock sequences", + action="store_true") parser.add_argument( "infile", help="VCD waveform file", @@ -209,10 +245,10 @@ def main(): errors = 0 for f in infile: try: - wdr = parsetowavedrom(f, args.wavedrom) + wdr = parsetowavedrom(f, args.wavedrom, args.reduceclk) if args.savesvg: svg = wavedrom.render( quoted_strings_wavedrom(wdr) ) - outfile = f.with_suffix(".svg") + outfile = f.with_suffix(".svg") svg.saveas(outfile) except KeyboardInterrupt: sys.exit(1)