"""GTKWave save file generator.
This module provides tools for generating GTKWave save files.
`GTKWave`__ is an application for viewing VCD data. When opening a VCD file with
GTKWave, by default, no VCD variables (signals) are displayed. It is thus useful to have
an accompanying "save" file which configures various aspects of how GTKWave shows the
VCD data, including which variables are displayed, variable aliases, color information,
and more.
__ http://gtkwave.sourceforge.net
"""
import datetime
import math
import os
import time
import warnings
from contextlib import contextmanager
from enum import Enum, Flag, auto
from typing import IO, Any, Dict, Generator, List, Optional, Sequence, Tuple, Union
[docs]class GTKWFlag(Flag):
"""These are the valid GTKWave trace flags."""
highlight = auto()
"Highlight the trace item"
hex = auto() #: Hexadecimal data value representation
dec = auto() #: Decimal data value representation
bin = auto() #: Binary data value representation
oct = auto() #: Octal data value representation
rjustify = auto()
"Right-justify signal name/alias"
invert = auto()
reverse = auto()
exclude = auto()
blank = auto() #: Used for blank, label, and/or analog height
signed = auto()
"Signed (2's compliment) data representation"
ascii = auto()
"ASCII character representation"
collapsed = auto() #: Used for closed groups
ftranslated = auto() #: Trace translated with filter file
ptranslated = auto() #: Trace translated with filter process
analog_step = auto() #: Show trace as discrete analog steps
analog_interpolated = auto() #: Show trace as analog with interpolation
analog_blank_stretch = auto() #: Used to extend height of analog data
real = auto() #: Real (floating point) data value representation
analog_fullscale = auto() #: Analog data scaled using full simulation time
zerofill = auto()
onefill = auto()
closed = auto()
grp_begin = auto()
"Begin a group of signals"
grp_end = auto()
"End a group of signals"
bingray = auto()
graybin = auto()
real2bits = auto()
ttranslated = auto()
popcnt = auto()
"Show the population count, i.e. the number of set bits"
fpdecshift = auto()
[docs]class GTKWColor(Enum):
"""The colors used by GTKWave.
The `cycle` color is special and indicates the GTKWave should cycle through this
list of colors, starting from the last selected color.
"""
cycle = -1
"Cycle between colors"
normal = 0
"Default color"
red = 1
orange = 2
yellow = 3
green = 4
blue = 5
indigo = 6
violet = 7
def _cycle(self):
return GTKWColor((self.value + 1) % 8)
[docs]class GTKWSave:
"""Write GTKWave save files.
This class provides methods for writing the various pieces of a GTKWave save file. A
GTKWave save file compliments a VCD dump file with dump file specific configuration
GTKWave uses to display the dump file.
A GTKWave save file is line-oriented ASCII text. Each line consists of a single
configuration directive. All directives are optional.
Some directives, such as :meth:`dumpfile()`, are for general GTKWave configuration.
These general directives may be added anywhere in the save file and in any order
relative to other directives. Directives may also be duplicated--the last one added
will be used by GTKWave.
The :meth:`trace()`, :meth:`trace_bits()`, :meth:`group()`, and :meth:`blank`
directives add signals to the "Signals" list which are traced in the "Waves" frame.
The order in which these signal traces are added determines the order in GTKWave.
"""
def __init__(self, savefile: IO[str]) -> None:
self.file = savefile
self.path = getattr(savefile, 'name', None)
self._flags = GTKWFlag(0)
self._color_stack = [GTKWColor.normal]
self._filter_files: List[str] = []
self._filter_procs: List[str] = []
def _p(self, *args: object, **kwargs) -> None:
print(*args, file=self.file, **kwargs)
def _set_flags(self, flags: GTKWFlag) -> None:
if flags != self._flags:
self._p(f'@{flags.value:x}')
self._flags = flags
def _set_color(self, color: Optional[Union[GTKWColor, str, int]]) -> None:
if color is not None:
if not isinstance(color, GTKWColor):
warnings.warn(
'Using str and int for colors is deprecated. '
'Use vcd.gtkw.GTKWColor instead.',
DeprecationWarning,
stacklevel=2,
)
if isinstance(color, str):
color = GTKWColor.__members__[color]
else:
assert isinstance(color, int)
color = GTKWColor(color)
assert isinstance(color, GTKWColor)
if color == GTKWColor.cycle:
self._color_stack[-1] = self._color_stack[-1]._cycle()
else:
self._color_stack[-1] = color
self._p(f'[color] {self._color_stack[-1].value}')
def _set_translate_filter_file(self, filter_path: Optional[str]) -> None:
if filter_path:
try:
filter_id = 1 + self._filter_files.index(filter_path)
except ValueError:
self._filter_files.append(filter_path)
filter_id = len(self._filter_files)
self._p(f'^{filter_id} {filter_path}')
def _set_translate_filter_proc(self, proc_path: Optional[str]) -> None:
if proc_path:
try:
filter_id = 1 + self._filter_procs.index(proc_path)
except ValueError:
self._filter_procs.append(proc_path)
filter_id = len(self._filter_procs)
self._p(f'^>{filter_id} {proc_path}')
[docs] def dumpfile(self, dump_path: str, abspath: bool = True) -> None:
"""Add VCD dump file path to save file.
The `[dumpfile]` must be in the save file in order to only have to specify the
save file on the `gtkwave` command line. I.e.:
$ gtkwave my.gtkw
If the `[dumpfile]` is not present in the save file, both the dump and save
files must be specified to `gtkwave`:
$ gtkwave my.vcd my.gtkw
:param dump_path: path to VCD dump file or None to produce special "(null)"
value in the save file.
:param bool abspath: convert *dump_path* to an absolute path.
"""
if dump_path is None:
self._p('[dumpfile] (null)')
else:
if abspath:
dump_path = os.path.abspath(dump_path)
self._p(f'[dumpfile] "{dump_path}"')
[docs] def dumpfile_mtime(
self,
mtime: Optional[Union[float, time.struct_time, datetime.datetime]] = None,
dump_path: Optional[str] = None,
) -> None:
"""Add dump file modification time to save file.
Configuring the dump file's modification time is optional.
"""
time_format = '%a %b %d %H:%M:%S %Y'
if mtime is None:
assert isinstance(dump_path, str)
mtime = os.stat(dump_path).st_mtime
if isinstance(mtime, float):
mtime = time.gmtime(mtime)
if isinstance(mtime, time.struct_time):
mtime_str = time.strftime(time_format, mtime)
elif isinstance(mtime, datetime.datetime):
mtime_str = mtime.strftime(time_format)
else:
raise TypeError(f'Invalid mtime type ({type(mtime)})')
self._p(f'[dumpfile_mtime] "{mtime_str}"')
[docs] def dumpfile_size(
self, size: Optional[int] = None, dump_path: Optional[str] = None
) -> None:
"""Add dump file size annotation to save file.
Configuring the dump file's size is optional.
"""
if size is None:
assert isinstance(dump_path, str)
size = os.stat(dump_path).st_size
self._p(f'[dumpfile_size] {size}')
[docs] def savefile(self, save_path: Optional[str] = None, abspath: bool = True) -> None:
"""Add the path of the save file to the save file.
With no parameters, the output file's name will be used.
Configuring the `[savefile]` is optional.
:param save_path: path to this save file. None will use the output file's path.
:param bool abspath: determines whether to make the path absolute.
"""
if save_path is None and self.path is None:
self._p('[savefile] (null)')
else:
if save_path is None:
save_path = self.path
if abspath and save_path is not None:
save_path = os.path.abspath(save_path)
self._p(f'[savefile] "{save_path}"')
[docs] def timestart(self, timestamp: int = 0) -> None:
"""Add simulation start time to the save file."""
self._p(f'[timestart] {timestamp}')
[docs] def zoom_markers(
self, zoom: float = 0.0, marker: int = -1, **kwargs: Dict[str, int]
) -> None:
"""Set zoom, primary marker, and markers 'a' - 'z'."""
self._p(
f'*{zoom:.6f} {marker}',
*[kwargs.get(k, -1) for k in 'abcdefghijklmnopqrstuvwxyz'],
)
[docs] def size(self, width: int, height: int) -> None:
"""Set GTKWave window size."""
self._p(f'[size] {width} {height}')
[docs] def pos(self, x: int = -1, y: int = -1) -> None:
"""Set GTKWave window position."""
self._p(f'[pos] {x} {y}')
[docs] def treeopen(self, tree: str) -> None:
"""Start with *tree* open in Signal Search Tree (SST).
GTKWave specifies tree paths with a trailing '.'. The trailing '.' will
automatically be added if it is omitted in the *tree* parameter.
:param str tree: scope/path/tree to be opened in GTKWave's SST frame.
"""
if tree[-1] == '.':
self._p(f'[treeopen] {tree}')
else:
self._p(f'[treeopen] {tree}.')
[docs] def signals_width(self, width: int) -> None:
"""Set width of Signals frame."""
self._p(f'[signals_width] {width}')
[docs] def sst_expanded(self, is_expanded: bool) -> None:
"""Set whether Signal Search Tree (SST) frame is expanded."""
self._p(f'[sst_expanded] {int(bool(is_expanded))}')
[docs] def pattern_trace(self, is_enabled: bool) -> None:
"""Enable/disable pattern trace."""
self._p(f'[pattern_trace] {int(bool(is_enabled))}')
[docs] @contextmanager
def group(
self, name: str, closed: bool = False, highlight: bool = False
) -> Generator[None, None, None]:
"""Contextmanager helper for :meth:`begin_group` and :meth:`end_group`.
This context manager starts a new group of signal traces and ends the group when
leaving the `with` block. E.g.:
>>> import io
>>> gtkw = GTKWSave(io.StringIO())
>>> with gtkw.group('mygroup'):
... gtkw.trace('a.b.x')
... gtkw.trace('a.b.y')
... gtkw.trace('a.b.z')
:param str name: the name/label of the trace group.
:param bool closed: group should be closed at GTKWave startup.
:param bool highlight: group should be highlighted at GTKWave startup.
"""
self.begin_group(name, closed, highlight)
try:
yield None
finally:
self.end_group(name, closed, highlight)
[docs] def begin_group(
self, name: str, closed: bool = False, highlight: bool = False
) -> None:
"""Begin a new signal trace group.
Consider using :meth:`group()` instead of :meth:`begin_group()` and
:meth:`end_group()`.
:param str name: the name/label of the trace group.
:param bool closed: group should be closed at GTKWave startup.
:param bool highlight: group should be highlighted at GTKWave startup.
"""
flags = GTKWFlag.grp_begin | GTKWFlag.blank
if closed:
flags |= GTKWFlag.closed
if highlight:
flags |= GTKWFlag.highlight
self._set_flags(flags)
self._p(f'-{name}')
self._color_stack.append(GTKWColor.normal)
[docs] def end_group(
self, name: str, closed: bool = False, highlight: bool = False
) -> None:
"""End a signal trace group.
This call must match with a prior call to :meth:`begin_group(). Consider using
:meth:`group()` instead of :meth:`begin_group()` and :meth:`end_group()`.
:param str name: the name/label of the trace group.
:param bool closed: group should be closed at GTKWave startup.
:param bool highlight: group should be highlighted at GTKWave startup.
"""
flags = GTKWFlag.grp_end | GTKWFlag.blank
if closed:
flags |= GTKWFlag.closed | GTKWFlag.collapsed
if highlight:
flags |= GTKWFlag.highlight
self._set_flags(flags)
self._p(f'-{name}')
self._color_stack.pop(-1)
[docs] def blank(
self, label: str = '', analog_extend: bool = False, highlight: bool = False
) -> None:
"""Add blank or label to trace signals list.
:param str label: Optional label for the blank.
:param bool analog_extend: extend the height of an immediately preceding analog
trace signal.
:param bool highlight: blank should be highlighted at GTKWave startup.
"""
flags = GTKWFlag.blank
if analog_extend:
flags |= GTKWFlag.analog_blank_stretch
if highlight:
flags |= GTKWFlag.highlight
self._set_flags(flags)
self._p(f'-{label}')
[docs] def trace(
self,
name: str,
alias: Optional[str] = None,
color: Optional[Union[GTKWColor, str, int]] = None,
datafmt: str = 'hex',
highlight: bool = False,
rjustify: bool = True,
extraflags: Union[GTKWFlag, Optional[Sequence[str]]] = GTKWFlag(0),
translate_filter_file: Optional[str] = None,
translate_filter_proc: Optional[str] = None,
) -> None:
"""Add signal trace to save file.
:param str name: fully-qualified name of signal to trace.
:param str alias: optional alias to display instead of the *name*.
:param GTKWColor color: optional color to use for the signal's trace.
:param str datafmt: the format used for data display. Must be one of 'hex',
'dec', 'bin', 'oct', 'ascii', 'real', or 'signed'.
:param bool highlight: trace should be highlighted at GTKWave startup.
:param bool rjustify: trace name/alias should be right-justified.
:param GTKWFlag extraflags: extra flags to apply to the trace.
:param str translate_filter_file: path to translate filter file.
:param str translate_filter_proc: path to translate filter executable.
.. Note::
GTKWave versions <= 3.3.64 require vector signal names to have a bit range
suffix. For example, an 8-bit vector variable "module.myint" would be known
by GTKWave as "module.myint[7:0]".
GTKWave versions after 3.3.64 do not use bit-range suffixes.
"""
if datafmt not in ['hex', 'dec', 'bin', 'oct', 'ascii', 'real', 'signed']:
raise ValueError(f'Invalid datafmt ({datafmt})')
flags = GTKWFlag.__members__[datafmt]
if isinstance(extraflags, GTKWFlag):
flags |= extraflags
else:
warnings.warn(
'Using Optional[Sequence[str]] for extraflags is deprecated. '
'Use vcd.gtkw.GTKWFlag instead.',
DeprecationWarning,
)
if extraflags is not None:
for flag in GTKWFlag:
if flag.name in extraflags:
flags |= flag
if highlight:
flags |= GTKWFlag.highlight
if rjustify:
flags |= GTKWFlag.rjustify
if translate_filter_file:
flags |= GTKWFlag.ftranslated
if translate_filter_proc:
flags |= GTKWFlag.ptranslated
self._set_flags(flags)
self._set_color(color)
self._set_translate_filter_file(translate_filter_file)
self._set_translate_filter_proc(translate_filter_proc)
if alias:
self._p(f'+{{{alias}}} ', end='')
self._p(name)
[docs] @contextmanager
def trace_bits(
self,
name: str,
alias: Optional[str] = None,
color: Optional[Union[str, int]] = None,
datafmt: str = 'hex',
highlight: bool = False,
rjustify: bool = True,
extraflags: Union[GTKWFlag, Optional[Sequence[str]]] = GTKWFlag(0),
translate_filter_file: Optional[str] = None,
translate_filter_proc: Optional[str] = None,
) -> Generator[None, None, None]:
"""Contextmanager for tracing bits of a vector signal.
This allows each individual bit of a vector signal to have its own trace (and
trace configuration).
>>> import io
>>> gtkw = GTKWSave(io.StringIO())
>>> name = 'mod.myint'
>>> with gtkw.trace_bits(name):
... gtkw.trace_bit(0, name)
... gtkw.trace_bit(1, name)
... gtkw.trace_bit(2, name)
... gtkw.trace_bit(3, name, 'special', color=GTKWColor.yellow)
:param str name: fully-qualified name of the vector variable to trace.
:param str alias: optional alias to display instead of *name*.
:param int color: optional trace color.
:param str datafmt: format for data display.
:param bool highlight: trace should be highlighted at GTKWave startup.
:param bool rjustify: trace name/alias should be right-justified.
:param GTKWFlag extraflags: extra flags to apply to the trace.
:param str translate_filter_file: path to translate filter file.
:param str translate_filter_proc: path to translate filter executable.
"""
self.trace(
name,
alias,
color,
datafmt,
highlight,
rjustify,
extraflags,
translate_filter_file,
translate_filter_proc,
)
flags = GTKWFlag.bin
if isinstance(extraflags, GTKWFlag):
flags |= extraflags
else:
warnings.warn(
'Using Optional[Sequence[str]] for extraflags is deprecated. '
'Use vcd.gtkw.GTKWFlag instead.',
DeprecationWarning,
)
if extraflags is not None:
for flag in GTKWFlag:
if flag.name in extraflags:
flags |= flag
if highlight:
flags |= GTKWFlag.highlight
if rjustify:
flags |= GTKWFlag.rjustify
self._set_flags(flags)
try:
yield None
finally:
flags = GTKWFlag.blank | GTKWFlag.grp_end | GTKWFlag.collapsed
if highlight:
flags |= GTKWFlag.highlight
self._set_flags(flags)
self._p('-group_end')
[docs] def trace_bit(
self,
index: int,
name: str,
alias: Optional[str] = None,
color: Optional[Union[GTKWColor, str, int]] = None,
) -> None:
"""Trace individual bit of vector signal.
This is meant for use in conjunction with :meth:`trace_bits()`.
:param int index: index of bit
:param str name: name of parent vector signal.
:param str alias: optional alias to display for bit.
:param int color: optional color for bit's trace.
"""
self._set_color(color)
if alias:
self._p(f'+{{{alias}}} ', end='')
self._p(f'({index}){name}')
TranslationType = Union[
Tuple[Union[int, str], str], Tuple[Union[int, str], str, Union[str, int]]
]
[docs]def make_translation_filter(
translations: Sequence[Tuple[Any, ...]],
datafmt: str = 'hex',
size: Optional[int] = None,
) -> str:
"""Create translation filter.
The returned translation filter string that can be written to a translation filter
file usable by GTKWave.
:param translations:
Sequence of 2-tuples `(value, alias)` or 3-tuples `(value, alias, color)`.
:param str datafmt:
Format to apply to the translation values. This *datafmt* must match the
*datafmt* used with :meth:`GTKWSave.trace()`, otherwise these translations will
not be matched by GTKWave.
:returns: Translation filter string suitable for writing to a translation filter
file.
"""
if datafmt == "hex":
assert isinstance(size, int)
value_format = f'0{int(math.ceil(size / 4))}x'
elif datafmt == 'oct':
assert isinstance(size, int)
value_format = f'0{int(math.ceil(size / 3))}o'
elif datafmt in ['dec', 'signed']:
value_format = 'd'
elif datafmt == 'bin':
assert isinstance(size, int)
value_format = f'0{size}b'
elif datafmt == 'real':
value_format = '.16g'
elif datafmt == 'ascii':
value_format = ""
ascii_translations = []
for translation in translations:
value = translation[0]
rest = list(translation[1:])
if isinstance(value, int):
value = bytes((value,)).decode('ascii')
elif not isinstance(value, str):
raise TypeError(f'Invalid type ({type(value)}) for ascii translation')
elif len(value) != 1:
raise ValueError(f'Invalid ascii string "{value}"')
ascii_translations.append(tuple([value] + rest))
translations = ascii_translations
else:
raise ValueError(f'invalid datafmt ({datafmt})')
lines = []
for translation in translations:
if len(translation) == 2:
value, label = translation
color = None
else:
value, label, color = translation
if datafmt in ['hex', 'oct', 'bin']:
assert isinstance(size, int)
max_val = 1 << size
if -value > (max_val >> 1) or value >= max_val:
raise ValueError(f'Value ({value}) not representable in {size} bits')
if value < 0:
# Two's compliment treatment
value += 1 << size
value_str = format(value, value_format)
if color is None:
lines.append(f'{value_str} {label}')
else:
lines.append(f'{value_str} ?{color}?{label}')
return '\n'.join(lines)
[docs]def decode_flags(flags: Union[str, int]) -> List[str]:
"""Decode hexadecimal flags from GTKWave save file into flag names.
This is useful for understanding what, for example "@802022" means when inspecting a
GTKWave save file.
:param flags: Hexadecimal flags from GTKWave save file; either as an integer or
string with hexadecimal characters.
:returns: List of flag names
"""
if isinstance(flags, str):
decoded = GTKWFlag(int(flags.lstrip('@'), 16))
else:
decoded = GTKWFlag(flags)
return [str(flag.name) for flag in GTKWFlag if flag & decoded]
[docs]def spawn_gtkwave_interactive(
dump_path: str, save_path: str, quiet: bool = False
) -> None: # pragma: no cover
"""Spawn gtkwave process in interactive mode.
A process pipeline is constructed such that the contents of the VCD dump file at
*dump_path* are displayed interactively as the dump file is being written (i.e. with
:class:`~vcd.writer.VCDWriter`.
The process pipeline built is approximately equivalent to::
$ tail -f dump_path | shmidcat | gtkwave -vI save_path
The ``tail``, ``shmidcat``, and ``gtkwave`` executables must be found in ``$PATH``.
.. Warning::
This function does not work on Windows.
.. Note::
A child python process of the caller will remain running until the GTKWave
window is closed. This process ensures that the various other child processes
are properly reaped.
:param str dump_path: path to VCD dump file. The dump file must exist, but be empty.
:param str save_path: path to GTKWave save file. The save file will be read
immediately by GTKWave and thus must be completely written.
:param bool quiet: quiet GTKWave's output by closing its `stdout` and `stderr` file
descriptors.
"""
import signal
stdin_fd, stdout_fd, stderr_fd = 0, 1, 2
if not os.fork():
shmidcat_rd_fd, tail_wr_fd = os.pipe()
tail_pid = os.fork()
if not tail_pid:
os.close(shmidcat_rd_fd)
os.dup2(tail_wr_fd, stdout_fd)
os.execlp('tail', 'tail', '-n', '+0', '-f', dump_path)
os.close(tail_wr_fd)
gtkwave_rd_fd, shmidcat_wr_fd = os.pipe()
shmidcat_pid = os.fork()
if not shmidcat_pid:
os.close(gtkwave_rd_fd)
os.dup2(shmidcat_rd_fd, stdin_fd)
os.dup2(shmidcat_wr_fd, stdout_fd)
os.execlp('shmidcat', 'shmidcat')
os.close(shmidcat_rd_fd)
os.close(shmidcat_wr_fd)
gtkwave_pid = os.fork()
if not gtkwave_pid:
os.dup2(gtkwave_rd_fd, stdin_fd)
if quiet:
devnull = open(os.devnull, 'w')
os.dup2(devnull.fileno(), stdout_fd)
os.dup2(devnull.fileno(), stderr_fd)
os.execlp('gtkwave', 'gtkwave', '--vcd', '--interactive', save_path)
# The first forked process exists to do this cleanup...
os.waitpid(gtkwave_pid, 0)
os.kill(tail_pid, signal.SIGTERM)
os.kill(shmidcat_pid, signal.SIGTERM)
os._exit(0)