diff --git a/docs/conf.py b/docs/conf.py index c08410e5d..6a9cb98eb 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,6 @@ extensions = [ "sphinx.ext.todo", "sphinx.ext.viewcode", "sphinxcontrib.wavedrom", - "symbolator_sphinx", "adi_links", "adi_hdl_parser" ] diff --git a/docs/extensions/adi_hdl_parser.py b/docs/extensions/adi_hdl_parser.py index f5df14794..f97f29a6c 100644 --- a/docs/extensions/adi_hdl_parser.py +++ b/docs/extensions/adi_hdl_parser.py @@ -13,6 +13,7 @@ from sphinx.util.nodes import nested_parse_with_titles from sphinx.util import logging from lxml import etree from adi_hdl_static import hdl_strings +from adi_hdl_render import hdl_component from uuid import uuid4 from hashlib import sha1 @@ -343,7 +344,7 @@ class directive_interfaces(directive_base): if tag not in bs and tag not in pr: logger.warning(f"Signal {tag} defined in the directive does not exist in the IP-XACT (component.xml)!") - return subnode + return subnode def run(self): env = self.state.document.settings.env @@ -414,7 +415,7 @@ class directive_regmap(directive_base): subnode += section if 'no-type-info' in self.options: - return subnode + return subnode tgroup = nodes.tgroup(cols=3) for _ in range(3): @@ -438,7 +439,7 @@ class directive_regmap(directive_base): tgroup += tbody section += table - return subnode + return subnode def run(self): env = self.state.document.settings.env @@ -534,10 +535,9 @@ class directive_parameters(directive_base): node = node_div() - if 'path' in self.options: - lib_name = self.options['path'] - else: - lib_name = env.docname.replace('/index', '') + if 'path' not in self.options: + self.options['path'] = env.docname.replace('/index', '') + lib_name = self.options['path'] subnode = nodes.section(ids=["hdl-parameters"]) if lib_name in env.component: @@ -549,6 +549,47 @@ class directive_parameters(directive_base): return [ node ] +class directive_component_diagram(directive_base): + option_spec = {'path': directives.unchanged} + required_arguments = 0 + optional_arguments = 0 + + def missing_diagram(self): + svg_raw = hdl_component.render_placeholder(self.options['path']) + + svg = nodes.raw('', svg_raw, format='html') + return [ svg ] + + def diagram(self): + name = hdl_component.get_name(self.options['path']) + path = '_build/managed' + f = open(os.path.join(path, name)) + svg_raw = f.read() + + svg = nodes.raw('', svg_raw, format='html') + return [ svg ] + + def run(self): + env = self.state.document.settings.env + self.current_doc = env.doc2path(env.docname) + + node = node_div() + + if 'path' not in self.options: + self.options['path'] = env.docname.replace('/index', '') + lib_name = self.options['path'] + + subnode = nodes.section(ids=["hdl-component-diagram"]) + if lib_name in env.component: + subnode += self.diagram() + else: + subnode += self.missing_diagram() + + node += subnode + + return [ node ] + + def parse_hdl_component(path, ctime): component = { 'bus_interface':{}, @@ -651,8 +692,16 @@ def parse_hdl_component(path, ctime): dm[signal_name].append(bus_name[0:bus_name.find('_signal_reset')]) continue + if get(bus_interface, 'slave') is not None: + bus_role = 'slave' + elif get(bus_interface, 'master') is not None: + bus_role = 'master' + else: + bus_role = None + bs[bus_name] = { 'name': sattrib(get(bus_interface, 'busType'), 'name'), + 'role': bus_role, 'dependency': get_dependency(bus_interface, 'busInterface'), 'port_map': {} } @@ -747,6 +796,7 @@ def manage_hdl_components(env, docnames, libraries): pass else: cp[lib] = parse_hdl_component(f, ctime) + hdl_component.render(env, lib, cp[lib]) docnames.append(doc) # From https://github.com/tfcollins/vger/blob/main/vger/hdl_reg_map.py @@ -923,6 +973,7 @@ def manage_hdl_artifacts(app, env, docnames): def setup(app): app.add_directive('collapsible', directive_collapsible) app.add_directive('hdl-parameters', directive_parameters) + app.add_directive('hdl-component-diagram', directive_component_diagram) app.add_directive('hdl-interfaces', directive_interfaces) app.add_directive('hdl-regmap', directive_regmap) diff --git a/docs/extensions/adi_hdl_render.py b/docs/extensions/adi_hdl_render.py new file mode 100644 index 000000000..da6d7eeba --- /dev/null +++ b/docs/extensions/adi_hdl_render.py @@ -0,0 +1,230 @@ +import os.path +from lxml import etree + +font_size = 16 +margin = 18 +line_length = 16 +text_vertical_margin = 8 +color_main = '#0067b9' +color_bg1 = '#c4e5ff' +color_bg2 = '#ebf6ff' +stroke_width = 3 + +class hdl_component(): + + @staticmethod + def get_name(lib_name): + return f"{lib_name.replace('/','-')}.svg" + + @staticmethod + def render(env, lib_name, item): + #ports, bus_interface + dest_dir = os.path.join(env.srcdir, '_build/managed') + dest_file = os.path.join(dest_dir, hdl_component.get_name(lib_name)) + + def make_style(parent): + style = etree.SubElement(parent, 'style').text = """ + a { + text-decoration: none; + } + """ + def make_gradient(parent): + defs = etree.SubElement(parent, 'defs') + gradient = etree.SubElement(defs, 'linearGradient', attrib={ + 'id':'ip_background', + 'x1':'0', 'x2':'1', 'y1':'0', 'y2':'1' + }) + etree.SubElement(gradient, 'stop', attrib={'offset':'0%', 'stop-color':color_bg1}) + etree.SubElement(gradient, 'stop', attrib={'offset':'100%', 'stop-color':color_bg2}) + + def symbol_bus(parent): + rect_height = font_size + one_fifth_x = line_length/5 + one_fifth_y = rect_height/5 + x = 0; y = 0 + pattern = [[0,1,0,1,0],[0,0,0,0,0],[0,1,0,1,0],[0,0,0,0,0],[0,1,0,1,0]] + etree.SubElement(parent, "rect", attrib={ + 'x':'0', 'y':'0', + 'width':str(line_length), 'height':str(font_size), + 'fill':color_bg1, + }) + for i in pattern: + for j in i: + if j: + etree.SubElement(parent, "rect", attrib={ + 'x':str(x), 'y':str(y), + 'width':str(one_fifth_x), 'height':str(one_fifth_y), + 'fill':color_main, + }) + x = x+one_fifth_x + x = 0 + y = y+one_fifth_y + def symbol_port(parent): + etree.SubElement(parent, "line", attrib={ + 'stroke':'black', + 'stroke-width':str(stroke_width), + 'x1':'0', 'y1':str(font_size/2), + 'x2':str(line_length), 'y2':str(font_size/2) + }) + + def create_text(items, side): + y_pos = margin*4 + if side == 'out': + text_anchor = 'end' + x_pos = margin*4 + aux_width + line_x1 = x_pos+(margin) + line_x2 = x_pos+(margin+line_length) + x_pos_group = x_pos+margin + scale_group = 'scale(1,1)' + else: + text_anchor = 'start' + x_pos = margin*3 + line_x1 = x_pos-(margin+line_length) + line_x2 = x_pos-(margin) + x_pos_group = x_pos-margin + scale_group = 'scale(-1,1)' + + for elem in items: + if elem[1] == 'bus': + link_anchor = f"#bus-interface-{elem[0]}" + else: + link_anchor = "#ports" + link = etree.SubElement(root, "a", attrib={ + 'href':link_anchor, + }) + etree.SubElement(link, "text", attrib={ + 'style':f"font: {font_size}px sans-serif", + 'text-anchor':text_anchor, + 'dominant-baseline':'middle', + 'x':str(x_pos), 'y':str(y_pos) + }).text = elem[0] + group = etree.SubElement(root, "g", attrib={ + 'transform':f"translate({x_pos_group},{y_pos-font_size/2}) {scale_group}"} + ) + if elem[1] == 'bus': + symbol_bus(group) + else: + symbol_port(group) + y_pos += font_size+text_vertical_margin + + ins = [] + outs = [] + for key in item['bus_interface']: + if item['bus_interface'][key]['role'] == 'master': + outs.append((key, 'bus')) + else: + ins.append((key, 'bus')) + for key in item['ports']: + if item['ports'][key]['direction'] == 'out': + outs.append((key, 'port')) + else: + ins.append((key, 'port')) + + max_len_in = 0 + for elem in ins: + max_len_in = len(elem[0]) if len(elem[0]) > max_len_in else max_len_in + + max_len_out = 0 + for elem in outs: + max_len_out = len(elem[0]) if len(elem[0]) > max_len_out else max_len_out + + aux_width = (max_len_in+max_len_out)*font_size*.6 + + num_outs = len(outs) + num_ins = len(ins) + max_num = max(num_outs, num_ins) + + root = etree.Element('svg', xmlns="http://www.w3.org/2000/svg") + + make_style(root) + make_gradient(root) + + ip_width = aux_width + margin*3 + ip_height = max_num*(font_size+text_vertical_margin) + margin*2 + etree.SubElement(root, "rect", attrib={ + 'x':str(margin*2), 'y':str(margin*2), + 'width':str(ip_width), 'height':str(ip_height), + 'rx':str(margin), + 'fill':'url(#ip_background)' + }) + + create_text(ins,'in') + create_text(outs,'out') + + viewbox_x = margin*7 + aux_width + viewbox_y = margin*7 + max_num*(font_size+text_vertical_margin) + root.set('viewBox', f"0 0 {viewbox_x} {viewbox_y}") + root.set('width', str(viewbox_x)) + root.set('height', str(viewbox_y)) + + etree.SubElement(root, "rect", attrib={ + 'x':str(margin*2), 'y':str(margin*2), + 'width':str(ip_width), 'height':str(ip_height), + 'rx':str(margin), + 'fill':'none', + 'stroke':color_main, + 'stroke-width':str(stroke_width) + }) + + ipname_y = viewbox_y-font_size-margin + ipname_x = viewbox_x/2 + etree.SubElement(root, "text", attrib={ + 'style':f"font: {font_size}px sans-serif", + 'fill': color_main, + 'text-anchor':'middle', + 'dominant-baseline':'middle', + 'x':str(ipname_x), 'y':str(ipname_y) + }).text = lib_name[lib_name.rfind('/')+1:] + + tree = etree.ElementTree(root) + + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + tree.write(dest_file) + + @staticmethod + def render_placeholder(lib_name): + root = etree.Element('svg', xmlns="http://www.w3.org/2000/svg") + + def text_element(text, fs, x, y, font='sans-serif'): + etree.SubElement(root, "text", attrib={ + 'style':f"font: {font_size*fs}px {font}", + 'fill': '#666', + 'text-anchor':'middle', + 'dominant-baseline':'middle', + 'x':str(x), 'y':str(y) + }).text = text + + ip_width = font_size*30 + ip_height = font_size*15 + etree.SubElement(root, "rect", attrib={ + 'x':str(margin*2), 'y':str(margin*2), + 'width':str(ip_width), 'height':str(ip_height), + 'rx':str(margin), + 'stroke':'#666', + 'fill':'none', + 'stroke-width':str(stroke_width), + 'stroke-dasharray':'8 16', + 'stroke-linecap':'round' + }) + + viewbox_x = ip_width + 4*margin + viewbox_y = ip_height + 4*margin + root.set('viewBox', f"0 0 {viewbox_x} {viewbox_y}") + root.set('width', str(viewbox_x)) + root.set('height', str(viewbox_y)) + + text_y = viewbox_y/2.5 + text_x = viewbox_x/2 + text_element("🧐", 4, text_x, text_y-text_vertical_margin*2) + text_element(f"{lib_name[lib_name.rfind('/')+1:]} IP-XACT not found.", + 1, text_x, text_y+font_size*2+text_vertical_margin) + text_element("Generate it and the documentation:", + .75, text_x, text_y+font_size*3+text_vertical_margin*2.5) + text_element(f"(cd {lib_name}; make)", + .75, text_x, text_y+font_size*4+text_vertical_margin*3, 'monospace') + text_element("(cd docs; make html)", + .75, text_x, text_y+font_size*5+text_vertical_margin*3.5, 'monospace') + + tree = etree.ElementTree(root) + return etree.tostring(tree, encoding="utf-8", method="xml").decode("utf-8") diff --git a/docs/library/axi_dmac/index.rst b/docs/library/axi_dmac/index.rst index d28dc5bae..071980dc5 100644 --- a/docs/library/axi_dmac/index.rst +++ b/docs/library/axi_dmac/index.rst @@ -3,8 +3,7 @@ High-Speed DMA Controller ================================================================================ -.. symbolator:: ../../../library/axi_dmac/axi_dmac.v - :caption: axi_dmac +.. hdl-component-diagram:: The AXI DMAC is a high-speed, high-throughput, general purpose DMA controller intended to be used to transfer data between system memory and other peripherals @@ -71,7 +70,6 @@ Configuration Parameters -------------------------------------------------------------------------------- .. hdl-parameters:: - :path: library/axi_dmac * - ID - Instance identification number. diff --git a/docs/library/spi_engine/axi_spi_engine.rst b/docs/library/spi_engine/axi_spi_engine.rst index 527c670bd..67a5456f8 100644 --- a/docs/library/spi_engine/axi_spi_engine.rst +++ b/docs/library/spi_engine/axi_spi_engine.rst @@ -3,8 +3,7 @@ AXI SPI Engine Module ================================================================================ -.. symbolator:: ../../../library/spi_engine/axi_spi_engine/axi_spi_engine.v - :caption: axi_spi_engine +.. hdl-component-diagram:: The AXI SPI Engine peripheral allows asynchronous interrupt-driven memory-mapped access to a SPI Engine Control Interface. diff --git a/docs/library/spi_engine/spi_engine_execution.rst b/docs/library/spi_engine/spi_engine_execution.rst index 6ef0fe884..cfc40d6af 100644 --- a/docs/library/spi_engine/spi_engine_execution.rst +++ b/docs/library/spi_engine/spi_engine_execution.rst @@ -3,8 +3,7 @@ SPI Engine Execution Module ================================================================================ -.. symbolator:: ../../../library/spi_engine/spi_engine_execution/spi_engine_execution.v - :caption: spi_engine_execution +.. hdl-component-diagram:: The SPI Engine Execution peripheral forms the heart of the SPI Engine framework. It is responsible for handling a SPI Engine control stream and translates it diff --git a/docs/library/spi_engine/spi_engine_interconnect.rst b/docs/library/spi_engine/spi_engine_interconnect.rst index bd31706ad..ff6d52f89 100644 --- a/docs/library/spi_engine/spi_engine_interconnect.rst +++ b/docs/library/spi_engine/spi_engine_interconnect.rst @@ -3,8 +3,7 @@ SPI Engine Interconnect Module ================================================================================ -.. symbolator:: ../../../library/spi_engine/spi_engine_interconnect/spi_engine_interconnect.v - :caption: axi_spi_engine +.. hdl-component-diagram:: The SPI Engine Interconnect module allows connecting multiple :ref:`spi_engine control-interface` masters to a single @@ -38,7 +37,6 @@ Configuration Parameters -------------------------------------------------------------------------------- .. hdl-parameters:: - :path: library/spi_engine/spi_engine_interconnect * - DATA_WIDTH - Data width of the parallel SDI/SDO data interfaces. diff --git a/docs/library/spi_engine/spi_engine_offload.rst b/docs/library/spi_engine/spi_engine_offload.rst index 966fcea35..a5772e89f 100644 --- a/docs/library/spi_engine/spi_engine_offload.rst +++ b/docs/library/spi_engine/spi_engine_offload.rst @@ -3,7 +3,7 @@ SPI Engine Offload Module ================================================================================ -.. symbolator:: ../../../library/spi_engine/spi_engine_offload/spi_engine_offload.v +.. hdl-component-diagram:: The SPI Engine Offload peripheral allows to store a SPI Engine command and SDO data stream in a RAM or ROM module. The command stream is executed when the diff --git a/docs/library/template_framework/template_module.rst b/docs/library/template_framework/template_module.rst index 56d5da509..aa38e0e1f 100644 --- a/docs/library/template_framework/template_module.rst +++ b/docs/library/template_framework/template_module.rst @@ -3,8 +3,8 @@ Template Module ================================================================================ -.. symbolator:: ../../../library/spi_engine/spi_engine_execution/spi_engine_execution.v - :caption: spi_engine_execution +.. hdl-component-diagram:: + :path: library/spi_engine/spi_engine_execution The {module name} is responsible for {brief description}. diff --git a/docs/library/template_ip/index.rst b/docs/library/template_ip/index.rst index 95fb6ca4e..e3d30c9c8 100644 --- a/docs/library/template_ip/index.rst +++ b/docs/library/template_ip/index.rst @@ -5,8 +5,8 @@ IP Template ================================================================================ -.. symbolator:: ../../../library/spi_engine/spi_engine_execution/spi_engine_execution.v - :caption: spi_engine_execution +.. hdl-component-diagram:: + :path: library/spi_engine/spi_engine_execution Features -------------------------------------------------------------------------------- diff --git a/docs/requirements.txt b/docs/requirements.txt index 1e2a4a33a..809a44fe8 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -7,5 +7,3 @@ aiohttp aiodns sphinxcontrib-wavedrom sphinxcontrib-svg2pdfconverter -https://github.com/hdl/pyhdlparser/tarball/master -https://github.com/hdl/symbolator/tarball/master diff --git a/docs/sources/custom.css b/docs/sources/custom.css index f127e42b4..dd26bff01 100755 --- a/docs/sources/custom.css +++ b/docs/sources/custom.css @@ -145,3 +145,11 @@ td.description { .default .pre { white-space: pre; } + +#hdl-component-diagram { + text-align: center; +} + +#hdl-component-diagram svg { + max-width: 100%; +} diff --git a/docs/user_guide/docs_guidelines.rst b/docs/user_guide/docs_guidelines.rst index 55d10b276..cbe2334e3 100644 --- a/docs/user_guide/docs_guidelines.rst +++ b/docs/user_guide/docs_guidelines.rst @@ -241,12 +241,6 @@ do: pip install -r requirements.txt -Symbolator directive -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -`Symbolator `_ is a tool to generate -component diagrams. - Custom directives and roles -------------------------------------------------------------------------------- @@ -450,6 +444,39 @@ You can provide description to a port or a bus, but not for a bus port. The ``:path:`` option is optional, and should **not** be included if the documentation file path matches the *component.xml* hierarchically. +HDL component diagram directive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The HDL component diagram directive gets information parsed from *component.xml* +library and generates a component diagram for the IP with buses and ports +information. + +.. note:: + + The *component.xml* files are generated by Vivado during the library build + and not by the documentation tooling. + +The directive syntax is: + +.. code:: rst + + .. hdl-component-diagram:: + :path: + +For example: + +.. code:: rst + + .. hdl-component-diagram:: + :path: library/spi_engine/spi_engine_interconnect + +The ``:path:`` option is optional, and should **not** be included if the +documentation file path matches the *component.xml* hierarchically. + +.. note:: + + This directive replaces the deprecated ``symbolator`` directive. + HDL regmap directive ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~