From 7c3bc0b09f9d18393c936692d30a97f8c36a8036 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Tue, 4 Feb 2020 20:14:41 +0800 Subject: [PATCH] litex-doc: initial merge of lxsocdoc lxsocdoc enables automatic documentation of litex projects, including automatic generation of SVD files. This merges the existing lxsocdoc distribution into the `soc/doc` directory. Signed-off-by: Sean Cross --- litex/soc/doc/__init__.py | 288 +++++++++++++++++++ litex/soc/doc/csr.py | 471 +++++++++++++++++++++++++++++++ litex/soc/doc/module.py | 120 ++++++++ litex/soc/doc/rst.py | 169 +++++++++++ litex/soc/doc/static/WaveDrom.js | 3 + litex/soc/doc/static/default.js | 3 + 6 files changed, 1054 insertions(+) create mode 100644 litex/soc/doc/__init__.py create mode 100644 litex/soc/doc/csr.py create mode 100644 litex/soc/doc/module.py create mode 100644 litex/soc/doc/rst.py create mode 100644 litex/soc/doc/static/WaveDrom.js create mode 100644 litex/soc/doc/static/default.js diff --git a/litex/soc/doc/__init__.py b/litex/soc/doc/__init__.py new file mode 100644 index 00000000..6262c575 --- /dev/null +++ b/litex/soc/doc/__init__.py @@ -0,0 +1,288 @@ +# This file is Copyright (c) 2020 Sean Cross +# License: BSD + +import os +import pathlib +import datetime + +from litex.soc.interconnect.csr import _CompoundCSR +from .csr import DocumentedCSRRegion +from .module import gather_submodules, ModuleNotDocumented, DocumentedModule, DocumentedInterrupts +from .rst import reflow + +sphinx_configuration = """ +project = '{}' +copyright = '{}, {}' +author = '{}' +extensions = [ + 'sphinx.ext.autosectionlabel', + 'sphinxcontrib.wavedrom',{} +] +templates_path = ['_templates'] +exclude_patterns = [] +offline_skin_js_path = "https://wavedrom.com/skins/default.js" +offline_wavedrom_js_path = "https://wavedrom.com/WaveDrom.js" +html_theme = 'alabaster' +html_static_path = ['_static'] +""" + +def sub_csr_bit_range(busword, csr, offset): + nwords = (csr.size + busword - 1)//busword + i = nwords - offset - 1 + nbits = min(csr.size - i*busword, busword) - 1 + name = (csr.name + str(i) if nwords > 1 else csr.name).upper() + origin = i*busword + return (origin, nbits, name) + +def print_svd_register(csr, csr_address, description, length, svd): + print(' ', file=svd) + print(' {}'.format(csr.short_numbered_name), file=svd) + if description is not None: + print(' '.format(description), file=svd) + print(' 0x{:04x}'.format(csr_address), file=svd) + print(' 0x{:02x}'.format(csr.reset_value), file=svd) + print(' {}'.format(length), file=svd) + print(' {}'.format(csr.access), file=svd) + csr_address = csr_address + 4 + print(' ', file=svd) + if hasattr(csr, "fields") and len(csr.fields) > 0: + for field in csr.fields: + print(' ', file=svd) + print(' {}'.format(field.name), file=svd) + print(' {}'.format(field.offset + field.size - 1), file=svd) + print(' [{}:{}]'.format(field.offset + field.size - 1, field.offset), file=svd) + print(' {}'.format(field.offset), file=svd) + print(' '.format(reflow(field.description)), file=svd) + print(' ', file=svd) + else: + field_size = csr.size + field_name = csr.short_name.lower() + # Strip off "ev_" from eventmanager fields + if field_name == "ev_enable": + field_name = "enable" + elif field_name == "ev_pending": + field_name = "pending" + elif field_name == "ev_status": + field_name = "status" + print(' ', file=svd) + print(' {}'.format(field_name), file=svd) + print(' {}'.format(field_size - 1), file=svd) + print(' [{}:{}]'.format(field_size - 1, 0), file=svd) + print(' {}'.format(0), file=svd) + print(' ', file=svd) + print(' ', file=svd) + print(' ', file=svd) + +def generate_svd(soc, buildpath, vendor="litex", name="soc", filename=None, description=None): + interrupts = {} + for csr, irq in sorted(soc.soc_interrupt_map.items()): + interrupts[csr] = irq + + documented_regions = [] + + raw_regions = [] + if hasattr(soc, "get_csr_regions"): + raw_regions = soc.get_csr_regions() + else: + for region_name, region in soc.csr_regions.items(): + raw_regions.append((region_name, region.origin, region.busword, region.obj)) + for csr_region in raw_regions: + documented_regions.append(DocumentedCSRRegion(csr_region, csr_data_width=soc.csr_data_width)) + + if filename is None: + filename = name + ".svd" + with open(buildpath + "/" + filename, "w", encoding="utf-8") as svd: + print('', file=svd) + print('', file=svd) + print('', file=svd) + print(' {}'.format(vendor), file=svd) + print(' {}'.format(name.upper()), file=svd) + if description is not None: + print(' '.format(reflow(description)), file=svd) + print('', file=svd) + print(' 8', file=svd) + print(' 32', file=svd) + print(' 32', file=svd) + print(' read-write', file=svd) + print(' 0x00000000', file=svd) + print(' 0xFFFFFFFF', file=svd) + print('', file=svd) + print(' ', file=svd) + + for region in documented_regions: + csr_address = 0 + print(' ', file=svd) + print(' {}'.format(region.name.upper()), file=svd) + print(' 0x{:08X}'.format(region.origin), file=svd) + print(' {}'.format(region.name.upper()), file=svd) + if len(region.sections) > 0: + print(' '.format(reflow(region.sections[0].body())), file=svd) + print(' ', file=svd) + for csr in region.csrs: + description = None + if hasattr(csr, "description"): + description = csr.description + if isinstance(csr, _CompoundCSR) and len(csr.simple_csrs) > 1: + is_first = True + for i in range(len(csr.simple_csrs)): + (start, length, name) = sub_csr_bit_range(region.busword, csr, i) + sub_name = csr.name.upper() + "_" + name + if length > 0: + bits_str = "Bits {}-{} of `{}`.".format(start, start+length, csr.name) + else: + bits_str = "Bit {} of `{}`.".format(start, csr.name) + if is_first: + if description is not None: + print_svd_register(csr.simple_csrs[i], csr_address, bits_str + " " + description, length, svd) + else: + print_svd_register(csr.simple_csrs[i], csr_address, bits_str, length, svd) + is_first = False + else: + print_svd_register(csr.simple_csrs[i], csr_address, bits_str, length, svd) + csr_address = csr_address + 4 + else: + length = ((csr.size + region.busword - 1)//region.busword) * region.busword + print_svd_register(csr, csr_address, description, length, svd) + csr_address = csr_address + 4 + print(' ', file=svd) + print(' ', file=svd) + print(' 0', file=svd) + print(' 0x{:x}'.format(csr_address), file=svd) + print(' registers', file=svd) + print(' ', file=svd) + if region.name in interrupts: + print(' ', file=svd) + print(' {}'.format(region.name), file=svd) + print(' {}'.format(interrupts[region.name]), file=svd) + print(' ', file=svd) + print(' ', file=svd) + print(' ', file=svd) + print('', file=svd) + +def generate_docs(soc, base_dir, project_name="LiteX SoC Project", + author="Anonymous", sphinx_extensions=[], quiet=False, note_pulses=False): + """Possible extra extensions: + [ + 'm2r', + 'recommonmark', + 'sphinx_rtd_theme', + 'sphinx_autodoc_typehints', + ] + """ + + # Ensure the target directory is a full path + if base_dir[-1] != '/': + base_dir = base_dir + '/' + + # Ensure the output directory exists + pathlib.Path(base_dir + "/_static").mkdir(parents=True, exist_ok=True) + + # Create various Sphinx plumbing + with open(base_dir + "conf.py", "w", encoding="utf-8") as conf: + year = datetime.datetime.now().year + sphinx_ext_str = "" + for ext in sphinx_extensions: + sphinx_ext_str += "\n \"{}\",".format(ext) + print(sphinx_configuration.format(project_name, year, author, author, sphinx_ext_str), file=conf) + if not quiet: + print("Generate the documentation by running `sphinx-build -M html {} {}_build`".format(base_dir, base_dir)) + + # Gather all interrupts so we can easily map IRQ numbers to CSR sections + interrupts = {} + for csr, irq in sorted(soc.soc_interrupt_map.items()): + interrupts[csr] = irq + + # Convert each CSR region into a DocumentedCSRRegion. + # This process will also expand each CSR into a DocumentedCSR, + # which means that CompoundCSRs (such as CSRStorage and CSRStatus) + # that are larger than the buswidth will be turned into multiple + # DocumentedCSRs. + documented_regions = [] + seen_modules = set() + regions = [] + # Previously, litex contained a function to gather csr regions. + if hasattr(soc, "get_csr_regions"): + regions = soc.get_csr_regions() + else: + # Now we just access the regions directly. + for region_name, region in soc.csr_regions.items(): + regions.append((region_name, region.origin, region.busword, region.obj)) + for csr_region in regions: + module = None + if hasattr(soc, csr_region[0]): + module = getattr(soc, csr_region[0]) + seen_modules.add(module) + submodules = gather_submodules(module) + + documented_region = DocumentedCSRRegion(csr_region, module, submodules, csr_data_width=soc.csr_data_width) + if documented_region.name in interrupts: + documented_region.document_interrupt(soc, submodules, interrupts[documented_region.name]) + documented_regions.append(documented_region) + + # Document any modules that are not CSRs. + # TODO: Add memory maps here. + additional_modules = [ + DocumentedInterrupts(interrupts), + ] + for (mod_name, mod) in soc._submodules: + if mod not in seen_modules: + try: + additional_modules.append(DocumentedModule(mod_name, mod)) + except ModuleNotDocumented: + pass + + with open(base_dir + "index.rst", "w", encoding="utf-8") as index: + print(""" +Documentation for {} +{} + +.. toctree:: + :hidden: +""".format(project_name, "="*len("Documentation for " + project_name)), file=index) + for module in additional_modules: + print(" {}".format(module.name), file=index) + for region in documented_regions: + print(" {}".format(region.name), file=index) + + if len(additional_modules) > 0: + print(""" +Modules +======= +""", file=index) + for module in additional_modules: + print("* :doc:`{} <{}>`".format(module.name.upper(), module.name), file=index) + + if len(documented_regions) > 0: + print(""" +Register Groups +=============== +""", file=index) + for region in documented_regions: + print("* :doc:`{} <{}>`".format(region.name.upper(), region.name), file=index) + + print(""" +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` +""", file=index) + + # Create a Region file for each of the documented CSR regions. + for region in documented_regions: + with open(base_dir + region.name + ".rst", "w", encoding="utf-8") as outfile: + region.print_region(outfile, base_dir, note_pulses) + + # Create a Region file for each additional non-CSR module + for region in additional_modules: + with open(base_dir + region.name + ".rst", "w", encoding="utf-8") as outfile: + region.print_region(outfile, base_dir, note_pulses) + + with open(os.path.dirname(__file__) + "/static/WaveDrom.js", "r") as wd_in: + with open(base_dir + "/_static/WaveDrom.js", "w") as wd_out: + wd_out.write(wd_in.read()) + + with open(os.path.dirname(__file__) + "/static/default.js", "r") as wd_in: + with open(base_dir + "/_static/default.js", "w") as wd_out: + wd_out.write(wd_in.read()) diff --git a/litex/soc/doc/csr.py b/litex/soc/doc/csr.py new file mode 100644 index 00000000..7ef61887 --- /dev/null +++ b/litex/soc/doc/csr.py @@ -0,0 +1,471 @@ +# This file is Copyright (c) 2020 Sean Cross +# License: BSD + +from migen import * +from migen.util.misc import xdir +from migen.fhdl.specials import Memory + +from litex.soc.integration.doc import ModuleDoc +from litex.soc.interconnect.csr_bus import SRAM +from litex.soc.interconnect.csr import _CompoundCSR, CSRStatus, CSRStorage, CSRField, _CSRBase +from litex.soc.interconnect.csr_eventmanager import _EventSource, SharedIRQ, EventManager, EventSourceLevel, EventSourceProcess, EventSourcePulse + +import textwrap + +from .rst import print_table, reflow + +class DocumentedCSRField: + def __init__(self, field): + self.name = field.name + self.size = field.size + self.offset = field.offset + self.reset_value = field.reset.value + self.description = field.description + self.access = field.access + self.pulse = field.pulse + self.values = field.values + + # If this is part of a sub-CSR, this value will be different + self.start = None + +class DocumentedCSR: + def trim(self, docstring): + if docstring is not None: + return reflow(docstring) + return None + + def __init__(self, name, address, short_numbered_name="", short_name="", reset=0, offset=0, size=8, description=None, access="read-write", fields=[]): + self.name = name + self.short_name = short_name + self.short_numbered_name = short_numbered_name + self.address = address + self.offset = offset + self.size = size + if size == 0: + print("!!! Warning: creating CSR of size 0 {}".format(name)) + self.description = self.trim(description) + self.reset_value = reset + self.fields = fields + self.access = access + for f in self.fields: + f.description = self.trim(f.description) + +class DocumentedCSRRegion: + def __init__(self, csr_region, module=None, submodules=[], csr_data_width=8): + (self.name, self.origin, self.busword, self.raw_csrs) = csr_region + self.current_address = self.origin + self.sections = [] + self.csrs = [] + self.csr_data_width = csr_data_width + + # If the section has extra documentation, gather it. + if isinstance(module, ModuleDoc): + self.sections.append(module) + if module is not None and hasattr(module, "get_module_documentation"): + docs = module.get_module_documentation() + for doc in docs: + self.sections.append(doc) + + if isinstance(self.raw_csrs, SRAM): + print("{}@{:x}: Found SRAM: {}".format(self.name, self.origin, self.raw_csrs)) + elif isinstance(self.raw_csrs, list): + for csr in self.raw_csrs: + if isinstance(csr, _CSRBase): + self.document_csr(csr) + elif isinstance(csr, SRAM): + print("{}: Found SRAM in the list: {}".format(self.name, csr)) + else: + print("{}: Unknown module: {}".format(self.name, csr)) + elif isinstance(self.raw_csrs, Memory): + self.csrs.append(DocumentedCSR( + self.name.upper(), self.origin, short_numbered_name=self.name.upper(), short_name=self.name.upper(), reset=0, size=self.raw_csrs.width, + description="{} x {}-bit memory".format(self.raw_csrs.width, self.raw_csrs.depth) + )) + print("{}@{:x}: Found memory that's {} x {} (but memories aren't documented yet)".format(self.name, self.origin, self.raw_csrs.width, self.raw_csrs.depth)) + else: + print("{}@{:x}: Unexpected item on the CSR bus: {}".format(self.name, self.origin, self.raw_csrs)) + + def bit_range(self, start, end, empty_if_zero=False): + end -= 1 + if start == end: + if empty_if_zero: + return "" + return "[{}]".format(start) + else: + return "[{}:{}]".format(end, start) + + def document_interrupt(self, soc, submodules, irq): + managers = submodules["event_managers"] + for m in managers: + sources_u = [y for x, y in xdir(m, True) if isinstance(y, _EventSource)] + sources = sorted(sources_u, key=lambda x: x.duid) + + def source_description(src): + if hasattr(src, "name") and src.name is not None: + base_text = "`1` if a `{}` event occurred. ".format(src.name) + else: + base_text = "`1` if a this particular event occurred. " + if hasattr(src, "description") and src.description is not None: + return src.description + elif isinstance(src, EventSourceLevel): + return base_text + "This Event is **level triggered** when the signal is **high**." + elif isinstance(src, EventSourcePulse): + return base_text + "This Event is triggered on a **rising** edge." + elif isinstance(src, EventSourceProcess): + return base_text + "This Event is triggered on a **falling** edge." + else: + return base_text + "This Event uses an unknown method of triggering." + + # Patch the DocumentedCSR to add our own Description, if one doesn't exist. + for dcsr in self.csrs: + short_name = dcsr.short_name.upper() + if short_name == m.status.name.upper(): + if dcsr.fields is None or len(dcsr.fields) == 0: + fields = [] + for i, source in enumerate(sources): + if hasattr(source, "name") and source.name is not None: + fields.append(DocumentedCSRField(CSRField(source.name, offset=i, description="Level of the `{}` event".format(source.name)))) + else: + fields.append(DocumentedCSRField(CSRField("event{}".format(i), offset=i, description="Level of the `event{}` event".format(i)))) + dcsr.fields = fields + if dcsr.description is None: + dcsr.description = "This register contains the current raw level of the Event trigger. Writes to this register have no effect." + elif short_name == m.pending.name.upper(): + if dcsr.fields is None or len(dcsr.fields) == 0: + fields = [] + for i, source in enumerate(sources): + if hasattr(source, "name") and source.name is not None: + fields.append(DocumentedCSRField(CSRField(source.name, offset=i, description=source_description(source)))) + else: + fields.append(DocumentedCSRField(CSRField("event{}".format(i), offset=i, description=source_description(source)))) + dcsr.fields = fields + if dcsr.description is None: + dcsr.description = "When an Event occurs, the corresponding bit will be set in this register. To clear the Event, set the corresponding bit in this register." + elif short_name == m.enable.name.upper(): + if dcsr.fields is None or len(dcsr.fields) == 0: + fields = [] + for i, source in enumerate(sources): + if hasattr(source, "name") and source.name is not None: + fields.append(DocumentedCSRField(CSRField(source.name, offset=i, description="Write a `1` to enable the `{}` Event".format(source.name)))) + else: + fields.append(DocumentedCSRField(CSRField("event{}".format(i), offset=i, description="Write a `1` to enable the `{}` Event".format(i)))) + dcsr.fields = fields + if dcsr.description is None: + dcsr.description = "This register enables the corresponding Events. Write a `0` to this register to disable individual events." + + def sub_csr_bit_range(self, csr, offset): + nwords = (csr.size + self.busword - 1)//self.busword + i = nwords - offset - 1 + nbits = min(csr.size - i*self.busword, self.busword) - 1 + name = (csr.name + str(i) if nwords > 1 else csr.name).upper() + origin = i*self.busword + return (origin, nbits, name) + + def split_fields(self, fields, start, end): + """Split `fields` into a sub-list that only contains the fields + between `start` and `end`. + This means that sometimes registers will get truncated. For example, + if we're going from [8:15] and we have a register that spans [7:15], + the bottom bit will be cut off. To account for this, we set the `.start` + property of the resulting split field to `1`, the `.offset` to `0`, and the + `.size` to 7. + """ + split_f = [] + for field in fields: + if field.offset > end: + continue + if field.offset + field.size < start: + continue + new_field = DocumentedCSRField(field) + + new_field.offset -= start + if new_field.offset < 0: + underflow_amount = -new_field.offset + new_field.offset = 0 + new_field.size -= underflow_amount + new_field.start = underflow_amount + # If it extends past the range, clamp the size to the range + if new_field.offset + new_field.size > (end - start): + new_field.size = (end - start) - new_field.offset + 1 + if new_field.start is None: + new_field.start = 0 + split_f.append(new_field) + return split_f + + def print_reg(self, reg, stream): + print("", file=stream) + print(" .. wavedrom::", file=stream) + print(" :caption: {}".format(reg.name), file=stream) + print("", file=stream) + print(" {", file=stream) + print(" \"reg\": [", file=stream) + if len(reg.fields) > 0: + bit_offset = 0 + for field in reg.fields: + field_name = field.name + attr_str = "" + if field.reset_value != 0: + attr_str = "\"attr\": '" + str(field.reset_value) + "', " + type_str = "" + if field.pulse: + type_str = "\"type\": 4, " + if hasattr(field, "start") and field.start is not None: + field_name = "{}{}".format(field.name, self.bit_range(field.start, field.size + field.start, empty_if_zero=True)) + term="," + if bit_offset != field.offset: + print(" {\"bits\": " + str(field.offset - bit_offset) + "},", file=stream) + if field.offset + field.size == self.busword: + term="" + print(" {\"name\": \"" + field_name + "\", " + type_str + attr_str + "\"bits\": " + str(field.size) + "}" + term, file=stream) + bit_offset = field.offset + field.size + if bit_offset != self.busword: + print(" {\"bits\": " + str(self.busword - bit_offset) + "}", file=stream) + else: + term="" + if reg.size != self.csr_data_width: + term="," + attr_str = "" + if reg.reset_value != 0: + attr_str = "\"attr\": 'reset: " + str(reg.reset_value) + "', " + print(" {\"name\": \"" + reg.short_name.lower() + self.bit_range(reg.offset, reg.offset + reg.size, empty_if_zero=True) + "\", " + attr_str + "\"bits\": " + str(reg.size) + "}" + term, file=stream) + if reg.size != self.csr_data_width: + print(" {\"bits\": " + str(self.csr_data_width - reg.size) + "},", file=stream) + print(" ], \"config\": {\"hspace\": 400, \"bits\": " + str(self.busword) + ", \"lanes\": 1 }, \"options\": {\"hspace\": 400, \"bits\": " + str(self.busword) + ", \"lanes\": 1}", file=stream) + print(" }", file=stream) + print("", file=stream) + + def get_csr_reset(self, csr): + reset = 0 + if hasattr(csr, "fields"): + for f in csr.fields.fields: + reset = reset | (f.reset_value << f.offset) + elif hasattr(csr, "storage"): + reset = int(csr.storage.reset.value) + elif hasattr(csr, "status"): + reset = int(csr.status.reset.value) + return reset + + def get_csr_size(self, csr): + nbits = 0 + if hasattr(csr, "fields"): + for f in csr.fields.fields: + nbits = max(nbits, f.size + f.offset) + elif hasattr(csr, "storage"): + nbits = int(csr.storage.nbits) + elif hasattr(csr, "status"): + nbits = int(csr.status.nbits) + elif hasattr(csr ,"r"): + nbits = int(csr.r.nbits) + elif hasattr(csr, "value"): + nbits = int(csr.value.nbits) + else: + raise ValueError("Internal error: can't determine CSR size of {}".format(csr)) + return nbits + + def document_csr(self, csr): + """Generates one or more DocumentedCSR, which will get appended + to self.csrs""" + fields = [] + description = None + atomic_write = False + full_name = self.name.upper() + "_" + csr.name.upper() + reset = 0 + if isinstance(csr, CSRStatus): + access = "read-only" + else: + access = "read-write" + + if hasattr(csr, "fields"): + fields = csr.fields.fields + if hasattr(csr, "description"): + description = csr.description + if hasattr(csr, "atomic_write"): + atomic_write = csr.atomic_write + size = self.get_csr_size(csr) + reset = self.get_csr_reset(csr) + + # If the CSR is composed of multiple sub-CSRs, document each + # one individually. + if isinstance(csr, _CompoundCSR) and len(csr.simple_csrs) > 1: + for i in range(len(csr.simple_csrs)): + (start, length, name) = self.sub_csr_bit_range(csr, i) + sub_name = self.name.upper() + "_" + name + bits_str = "Bits {}-{} of `{}`.".format(start, start+length, full_name) + if atomic_write: + if i == (range(len(csr.simple_csrs))-1): + bits_str += "Writing this register triggers an update of " + full_name + else: + bits_str += "The value won't take effect until `" + full_name + "0` is written." + if i == 0: + d = description + if description is None: + d = bits_str + else: + d = bits_str + " " + reflow(d) + self.csrs.append(DocumentedCSR( + sub_name, self.current_address, short_numbered_name=name.upper(), short_name=csr.name.upper(), reset=(reset>>start)&((2**length)-1), + offset=start, size=self.csr_data_width, + description=d, fields=self.split_fields(fields, start, start + length), access=access + )) + else: + self.csrs.append(DocumentedCSR( + sub_name, self.current_address, short_numbered_name=name.upper(), short_name=csr.name.upper(), reset=(reset>>start)&((2**length)-1), + offset=start, size=self.csr_data_width, + description=bits_str, fields=self.split_fields(fields, start, start + length), access=access + )) + self.current_address += 4 + else: + self.csrs.append(DocumentedCSR( + full_name, self.current_address, short_numbered_name=csr.name.upper(), short_name=csr.name.upper(), reset=reset, size=size, + description=description, fields=fields, access=access + )) + self.current_address += 4 + + def make_value_table(self, values): + ret = "" + max_value_width=len("Value") + max_description_width=len("Description") + for v in values: + (value, name, description) = (None, None, None) + if len(v) == 2: + (value, description) = v + elif len(v) == 3: + (value, name, description) = v + else: + raise ValueError("Unexpected length of CSRField's value tuple") + + # Ensure the value is a string + if not isinstance(value, str): + value = "{}".format(value) + + max_value_width = max(max_value_width, len(value)) + for d in description.splitlines(): + max_description_width = max(max_description_width, len(d)) + ret += "\n" + ret += "+-" + "-"*max_value_width + "-+-" + "-"*max_description_width + "-+\n" + ret += "| " + "Value".ljust(max_value_width) + " | " + "Description".ljust(max_description_width) + " |\n" + ret += "+=" + "="*max_value_width + "=+=" + "="*max_description_width + "=+\n" + for v in values: + (value, name, description) = (None, None, None) + if len(v) == 2: + (value, description) = v + elif len(v) == 3: + (value, name, description) = v + else: + raise ValueError("Unexpected length of CSRField's value tuple") + + # Ensure the value is a string + if not isinstance(value, str): + value = "{}".format(value) + + value = value.ljust(max_value_width) + first_line = True + for d in description.splitlines(): + if first_line: + ret += "| {} | {} |\n".format(value, d.ljust(max_description_width)) + first_line = False + else: + ret += "| {} | {} |\n".format(" ".ljust(max_value_width), d.ljust(max_description_width)) + ret += "+-" + "-"*max_value_width + "-+-" + "-"*max_description_width + "-+\n" + return ret + + def print_region(self, stream, base_dir, note_pulses): + title = "{}".format(self.name.upper()) + print(title, file=stream) + print("=" * len(title), file=stream) + print("", file=stream) + + for section in self.sections: + title = textwrap.dedent(section.title()) + body = textwrap.dedent(section.body()) + print("{}".format(title), file=stream) + print("-" * len(title), file=stream) + + if section.format() == "rst": + print(body, file=stream) + elif section.format() == "md": + filename = section.path() + if filename is not None: + print(".. mdinclude:: " + filename, file=stream) + else: + temp_filename = self.name + '-' + str(hash(title)) + "." + section.format() + with open(base_dir + "/" + temp_filename, "w") as cache: + print(body, file=cache) + print(".. mdinclude:: " + temp_filename, file=stream) + print("", file=stream) + + if len(self.csrs) > 0: + title = "Register Listing for {}".format(self.name.upper()) + print(title, file=stream) + print("-" * len(title), file=stream) + + csr_table = [["Register", "Address"]] + for csr in self.csrs: + csr_table.append([":ref:`{} <{}>`".format(csr.name, csr.name), ":ref:`0x{:08x} <{}>`".format(csr.address, csr.name)]) + print_table(csr_table, stream) + + for csr in self.csrs: + print("{}".format(csr.name), file=stream) + print("^" * len(csr.name), file=stream) + print("", file=stream) + print("`Address: 0x{:08x} + 0x{:x} = 0x{:08x}`".format(self.origin, csr.address - self.origin, csr.address), file=stream) + print("", file=stream) + if csr.description is not None: + print(textwrap.indent(csr.description, prefix=" "), file=stream) + self.print_reg(csr, stream) + if len(csr.fields) > 0: + max_field_width=len("Field") + max_name_width=len("Name") + max_description_width=len("Description") + value_tables = {} + + for f in csr.fields: + field = self.bit_range(f.offset, f.offset + f.size) + max_field_width = max(max_field_width, len(field)) + + name = f.name + if hasattr(f, "start") and f.start is not None: + name = "{}{}".format(f.name, self.bit_range(f.start, f.size + f.start)) + max_name_width = max(max_name_width, len(name)) + + description = f.description + if description is None: + description = "" + if note_pulses and f.pulse: + description = description + "\n\nWriting a 1 to this bit triggers the function." + for d in description.splitlines(): + max_description_width = max(max_description_width, len(d)) + if f.values is not None: + value_tables[f.name] = self.make_value_table(f.values) + for d in value_tables[f.name].splitlines(): + max_description_width = max(max_description_width, len(d)) + print("", file=stream) + print("+-" + "-"*max_field_width + "-+-" + "-"*max_name_width + "-+-" + "-"*max_description_width + "-+", file=stream) + print("| " + "Field".ljust(max_field_width) + " | " + "Name".ljust(max_name_width) + " | " + "Description".ljust(max_description_width) + " |", file=stream) + print("+=" + "="*max_field_width + "=+=" + "="*max_name_width + "=+=" + "="*max_description_width + "=+", file=stream) + for f in csr.fields: + field = self.bit_range(f.offset, f.offset + f.size).ljust(max_field_width) + + name = f.name.upper() + if hasattr(f, "start") and f.start is not None: + name = "{}{}".format(f.name.upper(), self.bit_range(f.start, f.size + f.start)) + name = name.ljust(max_name_width) + + description = f.description + if description is None: + description = "" + if note_pulses and f.pulse: + description = description + "\n\nWriting a 1 to this bit triggers the function." + + if f.name in value_tables: + description += "\n" + value_tables[f.name] + + first_line = True + for d in description.splitlines(): + if first_line: + print("| {} | {} | {} |".format(field, name, d.ljust(max_description_width)), file=stream) + first_line = False + else: + print("| {} | {} | {} |".format(" ".ljust(max_field_width), " ".ljust(max_name_width), d.ljust(max_description_width)), file=stream) + print("+-" + "-"*max_field_width + "-+-" + "-"*max_name_width + "-+-" + "-"*max_description_width + "-+", file=stream) + print("", file=stream) diff --git a/litex/soc/doc/module.py b/litex/soc/doc/module.py new file mode 100644 index 00000000..63380b3d --- /dev/null +++ b/litex/soc/doc/module.py @@ -0,0 +1,120 @@ +# This file is Copyright (c) 2020 Sean Cross +# License: BSD + +from migen.fhdl.module import DUID +from migen.util.misc import xdir + +from litex.soc.interconnect.csr_eventmanager import EventManager +from litex.soc.integration.doc import ModuleDoc + +import textwrap + +from .rst import print_table, print_rst + +def gather_submodules_inner(module, depth, seen_modules, submodules): + if module is None: + return submodules + if depth == 0: + if isinstance(module, ModuleDoc): + # print("{} is an instance of ModuleDoc".format(module)) + submodules["module_doc"].append(module) + for k,v in module._submodules: + # print("{}Submodule {} {}".format(" "*(depth*4), k, v)) + if v not in seen_modules: + seen_modules.add(v) + if isinstance(v, EventManager): + # print("{}{} appears to be an EventManager".format(" "*(depth*4), k)) + submodules["event_managers"].append(v) + + if isinstance(v, ModuleDoc): + submodules["module_doc"].append(v) + + gather_submodules_inner(v, depth + 1, seen_modules, submodules) + return submodules + +def gather_submodules(module): + depth = 0 + seen_modules = set() + submodules = { + "event_managers": [], + "module_doc": [], + } + + return gather_submodules_inner(module, depth, seen_modules, submodules) + +class ModuleNotDocumented(Exception): + """Indicates a Module has no documentation or sub-documentation""" + pass + +class DocumentedModule: + """Multi-section Documentation of a Module""" + + def __init__(self, name, module, has_documentation=False): + self.name = name + self.sections = [] + + if isinstance(module, ModuleDoc): + has_documentation = True + self.sections.append(module) + + if hasattr(module, "get_module_documentation"): + for doc in module.get_module_documentation(): + has_documentation = True + self.sections.append(doc) + + if not has_documentation: + raise ModuleNotDocumented() + + def print_region(self, stream, base_dir, note_pulses=False): + title = "{}".format(self.name.upper()) + print(title, file=stream) + print("=" * len(title), file=stream) + print("", file=stream) + + for section in self.sections: + title = textwrap.dedent(section.title()) + body = textwrap.dedent(section.body()) + print("{}".format(title), file=stream) + print("-" * len(title), file=stream) + print(textwrap.dedent(body), file=stream) + print("", file=stream) + +class DocumentedInterrupts(DocumentedModule): + """A :obj:`DocumentedModule` that automatically documents interrupts in an SoC + + This creates a :obj:`DocumentedModule` object that prints out the contents + of the interrupt map of an SoC. + """ + def __init__(self, interrupts): + DocumentedModule.__init__(self, "interrupts", None, has_documentation=True) + + self.irq_table = [["Interrupt", "Module"]] + for module_name, irq_no in interrupts.items(): + self.irq_table.append([str(irq_no), ":doc:`{} <{}>`".format(module_name.upper(), module_name)]) + + def print_region(self, stream, base_dir, note_pulses=False): + title = "Interrupt Controller" + print(title, file=stream) + print("=" * len(title), file=stream) + print("", file=stream) + + print_rst(stream, + """ + This device has an ``EventManager``-based interrupt + system. Individual modules generate `events` which are wired + into a central interrupt controller. + + When an interrupt occurs, you should look the interrupt number up + in the CPU-specific interrupt table and then call the relevant + module. + """) + + section_title = "Assigned Interrupts" + print("{}".format(section_title), file=stream) + print("-" * len(section_title), file=stream) + print("", file=stream) + + print("The following interrupts are assigned on this system:", file=stream) + print_table(self.irq_table, stream) + + diff --git a/litex/soc/doc/rst.py b/litex/soc/doc/rst.py new file mode 100644 index 00000000..de83e073 --- /dev/null +++ b/litex/soc/doc/rst.py @@ -0,0 +1,169 @@ +# This file is Copyright (c) 2020 Sean Cross +# License: BSD + +import textwrap + +def make_table(t): + """Make a reStructured Text Table + + Returns + ------- + + A string containing a reStructured Text table. + """ + column_widths = [] + + table = "\n" + if len(t) <= 0: + return table + + # Figure out how wide to make each column + for col in t[0]: + column_widths.append(0) + + for row in t: + for i, column in enumerate(row): + column_widths[i] = max(column_widths[i], len(column)) + + # Print out header + header = t.pop(0) + table += "+" + for i, column in enumerate(header): + table += "-" + "-"*column_widths[i] + table += "-+" + table += "\n" + + table += "|" + for i, column in enumerate(header): + table += " " + column.ljust(column_widths[i]) + " |" + table += "\n" + + table += "+" + for i, column in enumerate(header): + table += "=" + "="*column_widths[i] + table += "=+" + table += "\n" + + for row in t: + table += "|" + for i, column in enumerate(row): + table += " " + column.ljust(column_widths[i]) + " |" + table += "\n" + + table += "+" + for i, column in enumerate(row): + table += "-" + "-"*column_widths[i] + table += "-+" + table += "\n" + table += "\n" + + return table + +def print_table(table, stream): + """Print a reStructured Text table + + Arguments + --------- + + table (:obj:`list` of :obj:`list`s): A list of rows in the table. + Each row has several columns. The first row is the table header. + + stream (:obj:`io`): Destination output file. + """ + column_widths = [] + + print("", file=stream) + if len(table) <= 0: + return + + # Figure out how wide to make each column + for col in table[0]: + column_widths.append(0) + + for row in table: + for i, column in enumerate(row): + column_widths[i] = max(column_widths[i], len(column)) + + # Print out header + header = table.pop(0) + print("+", file=stream, end="") + for i, column in enumerate(header): + print("-" + "-"*column_widths[i], file=stream, end="") + print("-+", file=stream, end="") + print("", file=stream) + + print("|", file=stream, end="") + for i, column in enumerate(header): + print(" " + column.ljust(column_widths[i]) + " |", file=stream, end="") + print("", file=stream) + + print("+", file=stream, end="") + for i, column in enumerate(header): + print("=" + "="*column_widths[i], file=stream, end="") + print("=+", file=stream, end="") + print("", file=stream) + + for row in table: + print("|", file=stream, end="") + for i, column in enumerate(row): + print(" " + column.ljust(column_widths[i]) + " |", file=stream, end="") + print("", file=stream) + + print("+", file=stream, end="") + for i, column in enumerate(row): + print("-" + "-"*column_widths[i], file=stream, end="") + print("-+", file=stream, end="") + print("", file=stream) + print("", file=stream) + +def pad_first_line_if_necessary(s): + if not isinstance(s, str): + return s + lines = s.split("\n") + + # If there aren't at least two lines, don't do anything + if len(lines) < 2: + return s + + # If the first line is blank, don't do anything + if lines[0].strip() == "": + return s + + # If the pading on line 1 is greater than line 2, pad line 1 + # and return the result + line_0_padding = len(lines[0]) - len(lines[0].lstrip(' ')) + line_1_padding = len(lines[1]) - len(lines[1].lstrip(' ')) + if (line_1_padding > 0) and (line_1_padding > line_0_padding): + lines[0] = " " * (line_1_padding - line_0_padding) + lines[0] + return "\n".join(lines) + return s + +def reflow(s, width=80): + """Reflow the jagged text that gets generated as part + of this Python comment. + + In this comment, the first line would be indented relative + to the rest. Additionally, the width of this block would + be limited to the original text width. + + To reflow text, break it along \n\n, then dedent and reflow + each line individually. + + Finally, append it to a new string to be returned. + """ + if not isinstance(s, str): + return s + out = [] + s = pad_first_line_if_necessary(s) + for piece in textwrap.dedent(s).split("\n\n"): + trimmed_piece = textwrap.fill(textwrap.dedent(piece).strip(), width=width) + out.append(trimmed_piece) + return "\n\n".join(out) + +def _reflow(s, width=80): + return reflow(s, width) + +def print_rst(stream, s, reflow=True): + """Print a given string to the given stream. Ensure it is reflowed.""" + print(_reflow(s), file=stream) + print("", file=stream) diff --git a/litex/soc/doc/static/WaveDrom.js b/litex/soc/doc/static/WaveDrom.js new file mode 100644 index 00000000..0ef15e9a --- /dev/null +++ b/litex/soc/doc/static/WaveDrom.js @@ -0,0 +1,3 @@ +/*! wavedrom 2019-05-21 */ + +!function o(s,i,c){function l(t,e){if(!i[t]){if(!s[t]){var r="function"==typeof require&&require;if(!e&&r)return r(t,!0);if(u)return u(t,!0);var n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var a=i[t]={exports:{}};s[t][0].call(a.exports,function(e){return l(s[t][1][e]||e)},a,a.exports,o,s,i,c)}return i[t].exports}for(var u="function"==typeof require&&require,e=0;et+u.offsetWidth||e.yr+u.offsetHeight)&&(u.parentNode.removeChild(u),document.body.removeEventListener("mousedown",f,!1))}(l=document.getElementById(c+i)).childNodes[0].addEventListener("contextmenu",function(e){var t,r,n;(u=document.createElement("div")).className="wavedromMenu",u.style.top=e.y+"px",u.style.left=e.x+"px",t=document.createElement("ul"),(r=document.createElement("li")).innerHTML="Save as PNG",t.appendChild(r),(n=document.createElement("li")).innerHTML="Save as SVG",t.appendChild(n),u.appendChild(t),document.body.appendChild(u),r.addEventListener("click",function(){var e,t,r,n,a,o,s;e="",0!==i&&(e+=(t=document.getElementById(c+0)).innerHTML.substring(166,t.innerHTML.indexOf(''))),e=[l.innerHTML.slice(0,166),e,l.innerHTML.slice(166)].join(""),r="data:image/svg+xml;base64,"+btoa(e),(n=new Image).src=r,(a=document.createElement("canvas")).width=n.width,a.height=n.height,a.getContext("2d").drawImage(n,0,0),o=a.toDataURL("image/png"),(s=document.createElement("a")).href=o,s.download="wavedrom.png",s.click(),u.parentNode.removeChild(u),document.body.removeEventListener("mousedown",f,!1)},!1),n.addEventListener("click",function(){var e,t,r,n;e="",0!==i&&(e+=(t=document.getElementById(c+0)).innerHTML.substring(166,t.innerHTML.indexOf(''))),e=[l.innerHTML.slice(0,166),e,l.innerHTML.slice(166)].join(""),r="data:image/svg+xml;base64,"+btoa(e),(n=document.createElement("a")).href=r,n.download="wavedrom.svg",n.click(),u.parentNode.removeChild(u),document.body.removeEventListener("mousedown",f,!1)},!1),u.addEventListener("contextmenu",function(e){e.preventDefault()},!1),document.body.addEventListener("mousedown",f,!1),e.preventDefault()},!1)}},{}],2:[function(e,t,r){"use strict";t.exports=function(e,t,r){var n,a,o=r.x-t.x,s=r.y-t.y,i=(t.x+r.x)/2,c=(t.y+r.y)/2;switch(e.shape){case"-":break;case"~":n="M "+t.x+","+t.y+" c "+.7*o+", 0 "+.3*o+", "+s+" "+o+", "+s;break;case"-~":n="M "+t.x+","+t.y+" c "+.7*o+", 0 "+o+", "+s+" "+o+", "+s,e.label&&(i=t.x+.75*(r.x-t.x));break;case"~-":n="M "+t.x+","+t.y+" c 0, 0 "+.3*o+", "+s+" "+o+", "+s,e.label&&(i=t.x+.25*(r.x-t.x));break;case"-|":n="m "+t.x+","+t.y+" "+o+",0 0,"+s,e.label&&(i=r.x);break;case"|-":n="m "+t.x+","+t.y+" 0,"+s+" "+o+",0",e.label&&(i=t.x);break;case"-|-":n="m "+t.x+","+t.y+" "+o/2+",0 0,"+s+" "+o/2+",0";break;case"->":a="marker-end:url(#arrowhead);stroke:#0041c4;stroke-width:1;fill:none";break;case"~>":a="marker-end:url(#arrowhead);stroke:#0041c4;stroke-width:1;fill:none",n="M "+t.x+","+t.y+" c "+.7*o+", 0 "+.3*o+", "+s+" "+o+", "+s;break;case"-~>":a="marker-end:url(#arrowhead);stroke:#0041c4;stroke-width:1;fill:none",n="M "+t.x+","+t.y+" c "+.7*o+", 0 "+o+", "+s+" "+o+", "+s,e.label&&(i=t.x+.75*(r.x-t.x));break;case"~->":a="marker-end:url(#arrowhead);stroke:#0041c4;stroke-width:1;fill:none",n="M "+t.x+","+t.y+" c 0, 0 "+.3*o+", "+s+" "+o+", "+s,e.label&&(i=t.x+.25*(r.x-t.x));break;case"-|>":a="marker-end:url(#arrowhead);stroke:#0041c4;stroke-width:1;fill:none",n="m "+t.x+","+t.y+" "+o+",0 0,"+s,e.label&&(i=r.x);break;case"|->":a="marker-end:url(#arrowhead);stroke:#0041c4;stroke-width:1;fill:none",n="m "+t.x+","+t.y+" 0,"+s+" "+o+",0",e.label&&(i=t.x);break;case"-|->":a="marker-end:url(#arrowhead);stroke:#0041c4;stroke-width:1;fill:none",n="m "+t.x+","+t.y+" "+o/2+",0 0,"+s+" "+o/2+",0";break;case"<->":a="marker-end:url(#arrowhead);marker-start:url(#arrowtail);stroke:#0041c4;stroke-width:1;fill:none";break;case"<~>":a="marker-end:url(#arrowhead);marker-start:url(#arrowtail);stroke:#0041c4;stroke-width:1;fill:none",n="M "+t.x+","+t.y+" c "+.7*o+", 0 "+.3*o+", "+s+" "+o+", "+s;break;case"<-~>":a="marker-end:url(#arrowhead);marker-start:url(#arrowtail);stroke:#0041c4;stroke-width:1;fill:none",n="M "+t.x+","+t.y+" c "+.7*o+", 0 "+o+", "+s+" "+o+", "+s,e.label&&(i=t.x+.75*(r.x-t.x));break;case"<-|>":a="marker-end:url(#arrowhead);marker-start:url(#arrowtail);stroke:#0041c4;stroke-width:1;fill:none",n="m "+t.x+","+t.y+" "+o+",0 0,"+s,e.label&&(i=r.x);break;case"<-|->":a="marker-end:url(#arrowhead);marker-start:url(#arrowtail);stroke:#0041c4;stroke-width:1;fill:none",n="m "+t.x+","+t.y+" "+o/2+",0 0,"+s+" "+o/2+",0";break;default:a="fill:none;stroke:#F00;stroke-width:1"}return{lx:i,ly:c,d:n,style:a}}},{}],3:[function(e,t,r){t.exports={chars:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34,47,74,74,118,89,25,44,44,52,78,37,44,37,37,74,74,74,74,74,74,74,74,74,74,37,37,78,78,78,74,135,89,89,96,96,89,81,103,96,37,67,89,74,109,96,103,89,103,96,89,81,96,89,127,89,87,81,37,37,37,61,74,44,74,74,67,74,74,37,74,74,30,30,67,30,112,74,74,74,74,44,67,37,74,67,95,66,65,67,44,34,44,78,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,37,43,74,74,74,74,34,74,44,98,49,74,78,0,98,73,53,73,44,44,44,77,71,37,44,44,49,74,111,111,111,81,89,89,89,89,89,89,133,96,89,89,89,89,37,37,37,37,96,96,103,103,103,103,103,78,103,96,96,96,96,87,89,81,74,74,74,74,74,74,118,67,74,74,74,74,36,36,36,36,74,74,74,74,74,74,74,73,81,74,74,74,74,65,74,65,89,74,89,74,89,74,96,67,96,67,96,67,96,67,96,82,96,74,89,74,89,74,89,74,89,74,89,74,103,74,103,74,103,74,103,74,96,74,96,74,37,36,37,36,37,36,37,30,37,36,98,59,67,30,89,67,67,74,30,74,30,74,39,74,44,74,30,96,74,96,74,96,74,80,96,74,103,74,103,74,103,74,133,126,96,44,96,44,96,44,89,67,89,67,89,67,89,67,81,38,81,50,81,37,96,74,96,74,96,74,96,74,96,74,96,74,127,95,87,65,87,81,67,81,67,81,67,30,84,97,91,84,91,84,94,92,73,104,109,91,84,81,84,100,82,76,74,103,91,131,47,40,99,77,37,79,130,100,84,104,114,87,126,101,87,84,93,84,69,84,46,52,82,52,82,114,89,102,96,100,98,91,70,88,88,77,70,85,89,77,67,84,39,65,61,39,189,173,153,111,105,61,123,123,106,89,74,37,30,103,74,96,74,96,74,96,74,96,74,96,74,81,91,81,91,81,130,131,102,84,103,84,87,78,104,81,104,81,88,76,37,189,173,153,103,84,148,90,100,84,89,74,133,118,103,81],other:114}},{}],4:[function(e,t,r){"use strict";var n=e("onml/lib/stringify.js"),a=e("./w3.js");t.exports=function(e){var t=document.createElementNS(a.svg,"g");return t.innerHTML=n(e),t.childNodes[0]}},{"./w3.js":33,"onml/lib/stringify.js":36}],5:[function(e,t,r){"use strict";var n=e("./eva"),a=e("./render-wave-form");t.exports=function(){a(0,n("InputJSON_0"),"WaveDrom_Display_")}},{"./eva":6,"./render-wave-form":30}],6:[function(require,module,exports){"use strict";function eva(id){var TheTextBox,source;function erra(e){return{signal:[{name:["tspan",["tspan",{class:"error h5"},"Error: "],e.message]}]}}if(TheTextBox=document.getElementById(id),TheTextBox.type&&"textarea"===TheTextBox.type)try{source=eval("("+TheTextBox.value+")")}catch(e){return erra(e)}else try{source=eval("("+TheTextBox.innerHTML+")")}catch(e){return erra(e)}if("[object Object]"!==Object.prototype.toString.call(source))return erra({message:'[Semantic]: The root has to be an Object: "{signal:[...]}"'});if(source.signal){if("[object Array]"!==Object.prototype.toString.call(source.signal))return erra({message:'[Semantic]: "signal" object has to be an Array "signal:[]"'})}else if(source.assign){if("[object Array]"!==Object.prototype.toString.call(source.assign))return erra({message:'[Semantic]: "assign" object hasto be an Array "assign:[]"'})}else if(!source.reg)return erra({message:'[Semantic]: "signal:[...]" or "assign:[...]" property is missing inside the root Object'});return source}module.exports=eva},{}],7:[function(e,t,r){"use strict";t.exports=function(e){var t=0,r=0,n=[];return e.forEach(function(e){"vvv-2"===e||"vvv-3"===e||"vvv-4"===e||"vvv-5"===e?r+=1:0!==r&&(n.push(t-(r+1)/2),r=0),t+=1}),0!==r&&n.push(t-(r+1)/2),n}},{}],8:[function(e,t,r){"use strict";t.exports=function(e,t,r){var n,a,o=[];if(4===e.length){for(a=0;a"===o&&(i=!1,o=l.shift()),n=1;"."===l[0]||"|"===l[0];)l.shift(),n+=1;u=i?u.concat(h(a+o,0,n-r.period)):u.concat(h(a+o,t,n))}for(s=0;sdiv.wavedromMenu{position:fixed;border:solid 1pt#CCCCCC;background-color:white;box-shadow:0px 10px 20px #808080;cursor:default;margin:0px;padding:0px;}div.wavedromMenu>ul{margin:0px;padding:0px;}div.wavedromMenu>ul>li{padding:2px 10px;list-style:none;}div.wavedromMenu>ul>li:hover{background-color:#b5d5ff;}'}},{"./append-save-as-dialog":1,"./eva":6,"./render-wave-form":30}],18:[function(e,t,r){"use strict";t.exports=function e(t,r){var n,a,o={},s={x:10};for("string"!=typeof t[0]&&"number"!=typeof t[0]||(a=t[0],s.x=25),r.x+=s.x,n=0;n"===r&&(s=!1,r=a.shift()),o+=s?1:2*t.period,"|"===r&&n.push(["use",{"xlink:href":"#gap",transform:"translate("+t.xs*((o-(s?0:t.period))*t.hscale-t.phase)+")"}]);return n}t.exports=function(e,t,r){var n,a,o=[];if(e)for(n in e)r.period=e[n].period?e[n].period:1,r.phase=(e[n].phase?2*e[n].phase:0)+r.xmin_cfg,a=s(e[n].wave,r),o=o.concat([["g",{id:"wavegap_"+n+"_"+t,transform:"translate(0,"+(r.y0+n*r.yo)+")"}].concat(a)]);return["g",{id:"wavegaps_"+t}].concat(o)}},{}],23:[function(e,t,r){"use strict";var c=e("tspan");t.exports=function(e,r,n){var a,o,s,i=["g"];return e.forEach(function(e,t){i.push(["path",{id:"group_"+t+"_"+r,d:"m "+(e.x+.5)+","+(e.y*n.yo+3.5+n.yh0+n.yh1)+" c -3,0 -5,2 -5,5 l 0,"+(e.height*n.yo-16)+" c 0,3 2,5 5,5",style:"stroke:#0041c4;stroke-width:1;fill:none"}]),void 0!==e.name&&(a=e.x-10,o=n.yo*(e.y+e.height/2)+n.yh0+n.yh1,(s=c.parse(e.name)).unshift("text",{"text-anchor":"middle",class:"info","xml:space":"preserve"}),i.push(["g",{transform:"translate("+a+","+o+")"},["g",{transform:"rotate(270)"},s]]))}),i}},{tspan:37}],24:[function(e,t,r){"use strict";var n=e("tspan"),a=e("./text-width.js");t.exports=function(e,t){var r=a(t,8)+2;return["g",{transform:"translate("+e.x+","+e.y+")"},["rect",{x:-(r>>1),y:-5,width:r,height:10,style:"fill:#FFF;"}],["text",{"text-anchor":"middle",y:3,style:"font-size:8px;"}].concat(n.parse(t))]}},{"./text-width.js":32,tspan:37}],25:[function(e,t,r){"use strict";var s=e("./render-marks"),i=e("./render-arcs"),c=e("./render-gaps");t.exports=function(e,t,r,n,a,o){return[s(t,e,o,a)].concat(r.res).concat([i(n.lanes,e,a,o)]).concat([c(n.lanes,e,o)])}},{"./render-arcs":20,"./render-gaps":22,"./render-marks":26}],26:[function(e,t,r){"use strict";var m=e("tspan");function u(e,t,r){return e[t]&&e[t].text?[["text",{x:e.xmax*e.xs/2,y:r,fill:"#000","text-anchor":"middle","xml:space":"preserve"}].concat(m.parse(e[t].text))]:[]}function f(e,t,r,n,a,o,s){var i,c,l,u,f=1,d=0,h=[];if(void 0===e[t]||void 0===e[t][r])return[];if("string"==typeof(c=e[t][r]))c=c.split(" ");else if("number"==typeof c||"boolean"==typeof c)for(i=Number(c),c=[],u=0;u>o&1,t+n*(a/2-o-.5),r));return s}function n(e,o){var s=o.hspace/o.mod,i=["g",{transform:d(s/2,o.vspace/5)}],c=["g",{transform:d(s/2,o.vspace/2+4)}],l=["g",{transform:d(s/2,o.vspace)}],u=["g",{transform:d(0,o.vspace/4)}];return e.forEach(function(e){var t,r,n,a;if(t=0,r=o.mod-1,n=o.index*o.mod,a=(o.index+1)*o.mod-1,e.lsb/o.mod>>0===o.index)t=e.lsbm,n=e.lsb,e.msb/o.mod>>0===o.index&&(a=e.msb,r=e.msbm);else{if(e.msb/o.mod>>0!==o.index)return;a=e.msb,r=e.msbm}i.push(h(n,s*(o.mod-t-1))),t!==r&&i.push(h(a,s*(o.mod-r-1))),e.name&&c.push(p(e.name,s*(o.mod-(r+t)/2-1),0,s,e.bits)),void 0!==e.name&&void 0===e.type||u.push(["rect",{style:"fill-opacity:0.1"+function(e){var t=f[e];return void 0!==t?";fill:hsl("+t+",100%,50%)":""}(e.type),x:s*(o.mod-r-1),y:0,width:s*(r-t+1),height:o.vspace/2}]),void 0!==e.attr&&l.push(function(n,e,a,t,r){var o=a*(e.mod-(r+t)/2-1);return Array.isArray(n.attr)?n.attr.reduce(function(e,t,r){return null==t?e:e.concat([p(t,o,16*r,a,n.bits)])},["g",{}]):p(n.attr,o,0,a,n.bits)}(e,o,s,t,r))}),["g",u,i,c,l]}function u(e,t){return["g",{transform:d(4.5,(t.lanes-t.index-1)*t.vspace+.5),"text-anchor":"middle","font-size":t.fontsize,"font-family":t.fontfamily||"sans-serif","font-weight":t.fontweight||"normal"},function(e,t){var r=t.hspace,n=t.vspace,a=t.mod,o=["g",{transform:d(0,n/4),stroke:"black","stroke-width":1,"stroke-linecap":"round"}];o.push(c(r)),o.push(l(n/2)),o.push(c(r,0,n/2));for(var s=t.index*t.mod,i=t.mod;i===t.mod||e.some(function(e){return e.lsb===s})?o.push(l(n/2,i*(r/a))):(o.push(l(n/16,i*(r/a))),o.push(l(n/16,i*(r/a),7*n/16))),s++,--i;);return o}(e,t),n(e,t)]}t.exports=function(e,t){(t="object"==typeof t?t:{}).vspace=i(t.vspace,19,80),t.hspace=i(t.hspace,39,800),t.lanes=i(t.lanes,0,1),t.bits=i(t.bits,4,32),t.fontsize=i(t.fontsize,5,14),t.bigendian=t.bigendian||!1;var r,n=16*e.reduce(function(e,t){return Math.max(e,Array.isArray(t.attr)?t.attr.length:0)},0),a=function(e,t){return["svg",{xmlns:"http://www.w3.org/2000/svg",width:e,height:t,viewBox:[0,0,e,t].join(" ")}]}(t.hspace+9,(t.vspace+n)*t.lanes+5),o=0,s=t.bits/t.lanes;for(t.mod=0|s,e.forEach(function(e){e.lsb=o,e.lsbm=o%s,o+=e.bits,e.msb=o-1,e.msbm=e.msb%s}),r=0;r");a+=">"}switch(typeof t){case"string":case"number":case"boolean":return void(o+=t+i)}s=!1,o+=n(t)})?a+"/>"+i:s?a+function(e){var t=e.split("\n"),r=[];return t.forEach(function(e){""!==e.trim()&&r.push(e)}),r.join("\n")}(o)+""+i:a+i+r(o)+""+i}(e)}},{}],37:[function(e,t,r){"use strict";var n=e("./parse"),a=e("./reparse");r.parse=n,r.reparse=a},{"./parse":38,"./reparse":39}],38:[function(e,t,r){"use strict";var s=/||||||||<\/o>|<\/ins>|<\/s>|<\/sub>|<\/sup>|<\/b>|<\/i>|<\/tt>/;function i(r,e){e.add&&e.add.split(";").forEach(function(e){var t=e.split(" ");r[t[0]][t[1]]=!0}),e.del&&e.del.split(";").forEach(function(e){var t=e.split(" ");delete r[t[0]][t[1]]})}var c={"":{add:"text-decoration overline"},"":{del:"text-decoration overline"},"":{add:"text-decoration underline"},"":{del:"text-decoration underline"},"":{add:"text-decoration line-through"},"":{del:"text-decoration line-through"},"":{add:"font-weight bold"},"":{del:"font-weight bold"},"":{add:"font-style italic"},"":{del:"font-style italic"},"":{add:"baseline-shift sub;font-size .7em"},"":{del:"baseline-shift sub;font-size .7em"},"":{add:"baseline-shift super;font-size .7em"},"":{del:"baseline-shift super;font-size .7em"},"":{add:"font-family monospace"},"":{del:"font-family monospace"}};function l(n){return Object.keys(n).reduce(function(e,t){var r=Object.keys(n[t]);return 0