def __exit__(self, *args): if not self._run_called: warnings.warn("Simulation created, but not run", UserWarning) if self._vcd_writer: vcd_timestamp = (self._timestamp + self._delta) / self._epsilon self._vcd_writer.close(vcd_timestamp) if self._vcd_file and self._gtkw_file: gtkw_save = GTKWSave(self._gtkw_file) if hasattr(self._vcd_file, "name"): gtkw_save.dumpfile(self._vcd_file.name) if hasattr(self._vcd_file, "tell"): gtkw_save.dumpfile_size(self._vcd_file.tell()) gtkw_save.treeopen("top") gtkw_save.zoom_markers( math.log(self._epsilon / self._fastest_clock) - 14) def add_trace(signal, **kwargs): signal_slot = self._signal_slots[signal] if self._vcd_names[signal_slot] is not None: if len(signal) > 1 and not signal.decoder: suffix = "[{}:0]".format(len(signal) - 1) else: suffix = "" gtkw_save.trace(self._vcd_names[signal_slot] + suffix, **kwargs) for domain, cd in self._domains.items(): with gtkw_save.group("d.{}".format(domain)): if cd.rst is not None: add_trace(cd.rst) add_trace(cd.clk) for signal in self._traces: add_trace(signal) if self._vcd_file: self._vcd_file.close() if self._gtkw_file: self._gtkw_file.close()
def pre_init(cls, env): # Compose a GTKWave save file that lays-out the various VCD signals in # a meaningful manner. This must be done at pre-init time to allow # sim.gtkw.live to work. analog_kwargs = {'datafmt': 'dec', 'color': 'cycle', 'extraflags': ['analog_step']} with open(env.config['sim.gtkw.file'], 'w') as gtkw_file: gtkw = GTKWSave(gtkw_file) gtkw.dumpfile(env.config['sim.vcd.dump_file'], abspath=False) gtkw.treeopen('grocery') gtkw.signals_width(300) gtkw.trace('customers.active', **analog_kwargs) for i in range(env.config['grocery.num_lanes']): with gtkw.group('Lane{}'.format(i)): scope = 'grocery.lane{}'.format(i) gtkw.trace(scope + '.customer_queue', **analog_kwargs) gtkw.trace(scope + '.feed_belt', **analog_kwargs) gtkw.trace(scope + '.bag_area', **analog_kwargs) gtkw.trace(scope + '.baggers', **analog_kwargs)
def __exit__(self, *args): if not self._run_called: warnings.warn("Simulation created, but not run", UserWarning) if self._vcd_writer: vcd_timestamp = (self._timestamp + self._delta) / self._epsilon self._vcd_writer.close(vcd_timestamp) if self._vcd_file and self._gtkw_file: gtkw_save = GTKWSave(self._gtkw_file) if hasattr(self._vcd_file, "name"): gtkw_save.dumpfile(self._vcd_file.name) if hasattr(self._vcd_file, "tell"): gtkw_save.dumpfile_size(self._vcd_file.tell()) gtkw_save.treeopen("top") gtkw_save.zoom_markers(math.log(self._epsilon / self._fastest_clock) - 14) def add_trace(signal, **kwargs): signal_slot = self._signal_slots[signal] if self._vcd_names[signal_slot] is not None: if len(signal) > 1: suffix = "[{}:0]".format(len(signal) - 1) else: suffix = "" gtkw_save.trace(self._vcd_names[signal_slot] + suffix, **kwargs) for domain, cd in self._domains.items(): with gtkw_save.group("d.{}".format(domain)): if cd.rst is not None: add_trace(cd.rst) add_trace(cd.clk) for signal in self._traces: add_trace(signal) if self._vcd_file: self._vcd_file.close() if self._gtkw_file: self._gtkw_file.close()
class GTKWSave: """Generator of pretty GTKWave savefiles from SoC signals Usage example: ``` builder = Builder(soc, **builder_kwargs) vns = builder.build(run=False, **build_kwargs) with GTKWSave(vns, savefile=savefile, dumpfile=dumpfile) as gtkw: gtkw.clocks() gtkw.fsm_states(soc) gtkw.add(soc.bus.slaves["main_ram"]) ``` """ def __init__(self, vns: Namespace, savefile: str, dumpfile: str, filtersdir: str = None, treeopen: bool = True, prefix: str = "TOP.sim."): """Crate savefile generator for the namespace. `prefix` is prepended to all signal names and defaults to the one used by Litex simulator. """ self.vns = vns # Namespace output of Builder.build, required to resolve signal names self.prefix = prefix self.savefile = savefile self.dumpfile = dumpfile self.filtersdir = filtersdir self.treeopen = treeopen if self.filtersdir is None: self.filtersdir = os.path.dirname(self.savefile) def __enter__(self): # pyvcd: https://pyvcd.readthedocs.io/en/latest/vcd.gtkw.html from vcd.gtkw import GTKWSave self.file = open(self.savefile, "w") self.gtkw = GTKWSave(self.file) self.gtkw.dumpfile(self.dumpfile) if self.treeopen: modules = self.prefix.rstrip(".").split(".") for i in range(len(modules)): if modules[i]: self.gtkw.treeopen(".".join(modules[:i + 1])) self.gtkw.sst_expanded(True) return self def __exit__(self, type, value, traceback): self.file.close() print("\nGenerated GTKWave save file at: {}\n".format(self.savefile)) def name(self, sig: Signal) -> str: bits = "" if len(sig) > 1: bits = "[{}:0]".format(len(sig) - 1) return self.vns.get_name(sig) + bits def signal(self, signal: Signal): self.gtkw.trace(self.prefix + self.name(signal)) def common_prefix(self, names: Sequence[str]) -> str: prefix = os.path.commonprefix(names) last_underscore = prefix.rfind("_") return prefix[:last_underscore + 1] def group(self, signals: Sequence[Signal], group_name: str = None, alias: bool = True, closed: bool = True, mappers: Optional[Sequence[SigMapper]] = None, translation_files: Optional[Sequence[str]] = None, **kwargs): mappers = mappers or [] translation_files = translation_files or {} if len(signals) == 1: return self.signal(signals[0]) names = [self.name(s) for s in signals] common = self.common_prefix(names) make_alias = (lambda n: n[len(common):]) if alias else (lambda n: n) sigs = [ SigTrace(name=n, signal=s, alias=make_alias(n)) for i, (s, n) in enumerate(zip(signals, names)) ] for sig, file in zip(sigs, translation_files): sig.filter_file = file for mapper in mappers: sigs = list(mapper(sigs)) with self.gtkw.group(group_name or common.strip("_"), closed=closed): for s in sigs: self.gtkw.trace(self.prefix + s.name, alias=s.alias, color=s.color, translate_filter_file=s.filter_file, **kwargs) def by_regex(self, regex: Regex, **kwargs): pattern = re.compile(regex) signals = list( filter(lambda sig: pattern.search(self.vns.pnd[sig]), self.vns.pnd.keys())) assert len(signals) > 0, "No match found for {}".format(regex) return self.group(signals, **kwargs) def clocks(self, **kwargs): clks = [cd.clk for cd in self.vns.clock_domains] self.group(clks, group_name="clocks", alias=False, closed=False, **kwargs) def add(self, obj: Any, no_defaults=False, **kwargs): # TODO: add automatic default handlers for Litex types (e.g. WishBone, AXI, ...) def default_mappers(types, mappers): if not no_defaults and isinstance(obj, types): kwargs["mappers"] = DEFAULT_ENDPOINT_MAPPERS + kwargs.get( "mappers", []) if isinstance(obj, Record): # automatic settings for supported Record types default_mappers(stream.Endpoint, DEFAULT_ENDPOINT_MAPPERS) self.group([s for s, _ in obj.iter_flat()], **kwargs) elif isinstance(obj, Signal): self.signal(obj) elif self._is_module_with_attrs(obj, ["sink", "source"], types=stream.Endpoint, required=any): self._add_groupped_attrs( obj, ["sink", "source"], **kwargs) # recurse to Record->Endpoint handler else: raise NotImplementedError(type(obj), obj) def _add_groupped_attrs(self, obj, attrs, **kwargs): # add given attributes of an object in an encapsulating group, with attribute names as subgroup names with self.gtkw.group(kwargs["group_name"], closed=kwargs.get("closed", True)): for attr in attrs: if hasattr(obj, attr): new_kwargs = kwargs.copy() new_kwargs["group_name"] = attr self.add(getattr(obj, attr), **new_kwargs) def make_fsm_state_translation(self, fsm: FSM) -> str: # generate filter file from vcd.gtkw import make_translation_filter translations = list(fsm.decoding.items()) filename = "filter__{}.txt".format( self._strip_bits(self.name(fsm.state))) filepath = os.path.join(self.filtersdir, filename) with open(filepath, 'w') as f: f.write(make_translation_filter(translations, size=len(fsm.state))) return filepath def iter_submodules(self, parent: Module) -> Generator[Module, None, None]: for name, module in getattr(parent, "_submodules", []): yield module yield from self.iter_submodules(module) def make_fsm_state_alias(self, state: Signal): # Try to improve state name, as the defaults are usually hard to decipher. # This will make sure to include the name of the module that has the FSM, # but still there are some issues, e.g. we always add number to all names. alias = "" for name, num in reversed(state.backtrace): if alias.startswith(name): continue if name == "subfragments": break alias = "{}{}_{}".format(name, num, alias) return alias.strip("_") def fsm_states(self, soc: Module, alias: bool = True, **kwargs): fsms = list( filter(lambda module: isinstance(module, FSM), self.iter_submodules(soc))) states = [fsm.state for fsm in fsms] files = [self.make_fsm_state_translation(fsm) for fsm in fsms] if alias: aliases = { state: self.make_fsm_state_alias(state) for state in states } def add_alias(sig): sig.alias = aliases.get(sig.signal, None) return sig kwargs["mappers"] = [lambda sigs: map(add_alias, sigs) ] + kwargs.get("mappers", []) self.group(states, group_name="FSM states", translation_files=files, **kwargs) @staticmethod def _strip_bits(name: str) -> str: if name.endswith("]") and "[" in name: name = name[:name.rfind("[")] return name @staticmethod def _is_module_with_attrs(obj, attrs, types, required) -> bool: if not isinstance(obj, Module): return False available = map( lambda a: hasattr(obj, a) and isinstance(getattr(obj, a), types), attrs) return required(available)