VCD to wavedrom JSON/SVG converter
Signed-off-by: Wojciech Gryncewicz <wgryncewicz@antmicro.com>
This commit is contained in:
parent
09197ef30e
commit
c7c5dd8a50
|
@ -1,4 +1,5 @@
|
|||
flake8
|
||||
wavedrom
|
||||
|
||||
# rst_include tool as GitHub doesn't support `.. include::` when rendering
|
||||
# previews.
|
||||
|
|
|
@ -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())
|
||||
|
Loading…
Reference in New Issue