From 7c3bc0b09f9d18393c936692d30a97f8c36a8036 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Tue, 4 Feb 2020 20:14:41 +0800 Subject: [PATCH 1/3] 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 000000000..6262c575e --- /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 000000000..7ef618871 --- /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 000000000..63380b3d6 --- /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 000000000..de83e0739 --- /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 000000000..0ef15e9a4 --- /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 Date: Tue, 4 Feb 2020 20:34:10 +0800 Subject: [PATCH 2/3] soc: doc: use sphinx toctree as it was intended The sphinx toctree was behaving oddly, and so previously we were ignoring it completely. This patch causes it to be used correctly, which removes the need for double-including various sections. Signed-off-by: Sean Cross --- litex/soc/doc/__init__.py | 163 ++++++++++++++++++++++++-------------- 1 file changed, 102 insertions(+), 61 deletions(-) diff --git a/litex/soc/doc/__init__.py b/litex/soc/doc/__init__.py index 6262c575e..0a8c15dbb 100644 --- a/litex/soc/doc/__init__.py +++ b/litex/soc/doc/__init__.py @@ -10,7 +10,7 @@ from .csr import DocumentedCSRRegion from .module import gather_submodules, ModuleNotDocumented, DocumentedModule, DocumentedInterrupts from .rst import reflow -sphinx_configuration = """ +default_sphinx_configuration = """ project = '{}' copyright = '{}, {}' author = '{}' @@ -26,6 +26,7 @@ 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 @@ -34,13 +35,17 @@ def sub_csr_bit_range(busword, csr, offset): 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(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 @@ -48,11 +53,16 @@ def print_svd_register(csr, csr_address, description, length, 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( + ' {}'.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 @@ -67,12 +77,14 @@ def print_svd_register(csr, csr_address, description, length, svd): 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(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()): @@ -85,9 +97,11 @@ def generate_svd(soc, buildpath, vendor="litex", name="soc", filename=None, desc 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)) + 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)) + documented_regions.append(DocumentedCSRRegion( + csr_region, csr_data_width=soc.csr_data_width)) if filename is None: filename = name + ".svd" @@ -98,7 +112,8 @@ def generate_svd(soc, buildpath, vendor="litex", name="soc", filename=None, desc print(' {}'.format(vendor), file=svd) print(' {}'.format(name.upper()), file=svd) if description is not None: - print(' '.format(reflow(description)), file=svd) + print( + ' '.format(reflow(description)), file=svd) print('', file=svd) print(' 8', file=svd) print(' 32', file=svd) @@ -113,10 +128,13 @@ def generate_svd(soc, buildpath, vendor="litex", name="soc", filename=None, desc 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) + 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(' '.format( + reflow(region.sections[0].body())), file=svd) print(' ', file=svd) for csr in region.csrs: description = None @@ -125,42 +143,53 @@ def generate_svd(soc, buildpath, vendor="litex", name="soc", filename=None, desc 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 + (start, length, name) = sub_csr_bit_range( + region.busword, csr, i) if length > 0: - bits_str = "Bits {}-{} of `{}`.".format(start, start+length, csr.name) + bits_str = "Bits {}-{} of `{}`.".format( + start, start+length, csr.name) else: - bits_str = "Bit {} of `{}`.".format(start, csr.name) + 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) + 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) + 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) + 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) + 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( + ' 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( + ' {}'.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): + author="Anonymous", sphinx_extensions=[], quiet=False, note_pulses=False, + from_scratch=True): """Possible extra extensions: [ 'm2r', @@ -177,13 +206,17 @@ def generate_docs(soc, base_dir, project_name="LiteX SoC Project", # 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) + # Create the sphinx configuration file if the user has requested, + # or if it doesn't exist already. + if from_scratch or not os.path.isfile(base_dir + "conf.py"): + 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(default_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)) @@ -200,13 +233,16 @@ def generate_docs(soc, base_dir, project_name="LiteX SoC Project", 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)) + regions.append((region_name, region.origin, + region.busword, region.obj)) + for csr_region in regions: module = None if hasattr(soc, csr_region[0]): @@ -214,9 +250,11 @@ def generate_docs(soc, base_dir, project_name="LiteX SoC Project", seen_modules.add(module) submodules = gather_submodules(module) - documented_region = DocumentedCSRRegion(csr_region, module, submodules, csr_data_width=soc.csr_data_width) + 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_region.document_interrupt( + soc, submodules, interrupts[documented_region.name]) documented_regions.append(documented_region) # Document any modules that are not CSRs. @@ -231,38 +269,41 @@ def generate_docs(soc, base_dir, project_name="LiteX SoC Project", except ModuleNotDocumented: pass - with open(base_dir + "index.rst", "w", encoding="utf-8") as index: - print(""" + # Create index.rst containing links to all of the generated files. + # If the user has set `from_scratch=False`, then skip this step. + if from_scratch or not os.path.isfile(base_dir + "index.rst"): + 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(""" + 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(""" +.. toctree:: + :maxdepth: 1 +""", file=index) + for module in additional_modules: + print(" {}".format(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(""" +.. toctree:: + :maxdepth: 1 +""", file=index) + for region in documented_regions: + print(" {}".format(region.name), file=index) + + print(""" Indices and tables -================== +------------------ * :ref:`genindex` * :ref:`modindex` @@ -279,10 +320,10 @@ Indices and tables with open(base_dir + region.name + ".rst", "w", encoding="utf-8") as outfile: region.print_region(outfile, base_dir, note_pulses) + # Copy over wavedrom javascript and configuration files 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()) From 58598d4fda74d038a351271768fa7f34cfa6bca2 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Tue, 4 Feb 2020 23:49:08 +0800 Subject: [PATCH 3/3] integration: svd: move svd generation to `export` It was suggested that we should move svd generation into `export`, alongside the rest of the generators such as csv, json, and h. This performs this move, while keeping a compatible `generate_svd()` function inside `soc/doc/`. Signed-off-by: Sean Cross --- litex/soc/doc/__init__.py | 166 ++------------------------------ litex/soc/integration/export.py | 152 +++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 159 deletions(-) diff --git a/litex/soc/doc/__init__.py b/litex/soc/doc/__init__.py index 0a8c15dbb..38c753c54 100644 --- a/litex/soc/doc/__init__.py +++ b/litex/soc/doc/__init__.py @@ -6,6 +6,7 @@ import pathlib import datetime from litex.soc.interconnect.csr import _CompoundCSR +from litex.soc.integration import export from .csr import DocumentedCSRRegion from .module import gather_submodules, ModuleNotDocumented, DocumentedModule, DocumentedInterrupts from .rst import reflow @@ -26,165 +27,12 @@ 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)) - +def generate_svd(soc, buildpath, filename=None, name="soc", **kwargs): if filename is None: filename = name + ".svd" + kwargs["name"] = name 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) - 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) + svd.write(export.get_svd(soc, **kwargs)) def generate_docs(soc, base_dir, project_name="LiteX SoC Project", @@ -282,7 +130,7 @@ Documentation for {} if len(additional_modules) > 0: print(""" Modules -------- +======= .. toctree:: :maxdepth: 1 @@ -293,7 +141,7 @@ Modules if len(documented_regions) > 0: print(""" Register Groups ---------------- +=============== .. toctree:: :maxdepth: 1 @@ -303,7 +151,7 @@ Register Groups print(""" Indices and tables ------------------- +================== * :ref:`genindex` * :ref:`modindex` diff --git a/litex/soc/integration/export.py b/litex/soc/integration/export.py index d38c32d65..c91f53524 100644 --- a/litex/soc/integration/export.py +++ b/litex/soc/integration/export.py @@ -23,6 +23,11 @@ from litex.soc.interconnect.csr import CSRStatus from litex.build.tools import generated_banner +from litex.soc.doc.rst import reflow +from litex.soc.doc.module import gather_submodules, ModuleNotDocumented, DocumentedModule, DocumentedInterrupts +from litex.soc.doc.csr import DocumentedCSRRegion +from litex.soc.interconnect.csr import _CompoundCSR + # CPU files ---------------------------------------------------------------------------------------- def get_cpu_mak(cpu, compile_software): @@ -273,3 +278,150 @@ def get_csr_csv(csr_regions={}, constants={}, mem_regions={}): d["memories"][name]["type"], ) return r + +# SVD Export -------------------------------------------------------------------------------------- + +def get_svd(soc, vendor="litex", name="soc", description=None): + 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): + svd.append(' ') + svd.append(' {}'.format(csr.short_numbered_name)) + if description is not None: + svd.append(' '.format(description)) + svd.append(' 0x{:04x}'.format(csr_address)) + svd.append(' 0x{:02x}'.format(csr.reset_value)) + svd.append(' {}'.format(length)) + svd.append(' {}'.format(csr.access)) + csr_address = csr_address + 4 + svd.append(' ') + if hasattr(csr, "fields") and len(csr.fields) > 0: + for field in csr.fields: + svd.append(' ') + svd.append(' {}'.format(field.name)) + svd.append(' {}'.format(field.offset + + field.size - 1)) + svd.append(' [{}:{}]'.format( + field.offset + field.size - 1, field.offset)) + svd.append(' {}'.format(field.offset)) + svd.append(' '.format( + reflow(field.description))) + svd.append(' ') + 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" + svd.append(' ') + svd.append(' {}'.format(field_name)) + svd.append(' {}'.format(field_size - 1)) + svd.append(' [{}:{}]'.format(field_size - 1, 0)) + svd.append(' {}'.format(0)) + svd.append(' ') + svd.append(' ') + svd.append(' ') + + 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)) + + svd = [] + svd.append('') + svd.append('') + svd.append('') + svd.append(' {}'.format(vendor)) + svd.append(' {}'.format(name.upper())) + if description is not None: + svd.append(' '.format(reflow(description))) + svd.append('') + svd.append(' 8') + svd.append(' 32') + svd.append(' 32') + svd.append(' read-write') + svd.append(' 0x00000000') + svd.append(' 0xFFFFFFFF') + svd.append('') + svd.append(' ') + + for region in documented_regions: + csr_address = 0 + svd.append(' ') + svd.append(' {}'.format(region.name.upper())) + svd.append(' 0x{:08X}'.format(region.origin)) + svd.append(' {}'.format(region.name.upper())) + if len(region.sections) > 0: + svd.append(' '.format( + reflow(region.sections[0].body()))) + svd.append(' ') + 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) + 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 + svd.append(' ') + svd.append(' ') + svd.append(' 0') + svd.append(' 0x{:x}'.format(csr_address)) + svd.append(' registers') + svd.append(' ') + if region.name in interrupts: + svd.append(' ') + svd.append(' {}'.format(region.name)) + svd.append(' {}'.format(interrupts[region.name])) + svd.append(' ') + svd.append(' ') + svd.append(' ') + svd.append('') + return "\n".join(svd)