docs: Add component diagram generator

Replaces Symbolator with custom component diagram generator for more
reliable diagrams.
It uses the IP-XACT file, if it is not found, a placeholder is added
instead.

Signed-off-by: Jorge Marques <jorge.marques@analog.com>
main
Jorge Marques 2023-12-04 19:57:35 -03:00 committed by Jorge Marques
parent 9f4d5ff71f
commit 940c3ccd35
13 changed files with 338 additions and 31 deletions

View File

@ -20,7 +20,6 @@ extensions = [
"sphinx.ext.todo", "sphinx.ext.todo",
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
"sphinxcontrib.wavedrom", "sphinxcontrib.wavedrom",
"symbolator_sphinx",
"adi_links", "adi_links",
"adi_hdl_parser" "adi_hdl_parser"
] ]

View File

@ -13,6 +13,7 @@ from sphinx.util.nodes import nested_parse_with_titles
from sphinx.util import logging from sphinx.util import logging
from lxml import etree from lxml import etree
from adi_hdl_static import hdl_strings from adi_hdl_static import hdl_strings
from adi_hdl_render import hdl_component
from uuid import uuid4 from uuid import uuid4
from hashlib import sha1 from hashlib import sha1
@ -343,7 +344,7 @@ class directive_interfaces(directive_base):
if tag not in bs and tag not in pr: 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)!") 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): def run(self):
env = self.state.document.settings.env env = self.state.document.settings.env
@ -414,7 +415,7 @@ class directive_regmap(directive_base):
subnode += section subnode += section
if 'no-type-info' in self.options: if 'no-type-info' in self.options:
return subnode return subnode
tgroup = nodes.tgroup(cols=3) tgroup = nodes.tgroup(cols=3)
for _ in range(3): for _ in range(3):
@ -438,7 +439,7 @@ class directive_regmap(directive_base):
tgroup += tbody tgroup += tbody
section += table section += table
return subnode return subnode
def run(self): def run(self):
env = self.state.document.settings.env env = self.state.document.settings.env
@ -534,10 +535,9 @@ class directive_parameters(directive_base):
node = node_div() node = node_div()
if 'path' in self.options: if 'path' not in self.options:
lib_name = self.options['path'] self.options['path'] = env.docname.replace('/index', '')
else: lib_name = self.options['path']
lib_name = env.docname.replace('/index', '')
subnode = nodes.section(ids=["hdl-parameters"]) subnode = nodes.section(ids=["hdl-parameters"])
if lib_name in env.component: if lib_name in env.component:
@ -549,6 +549,47 @@ class directive_parameters(directive_base):
return [ node ] 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): def parse_hdl_component(path, ctime):
component = { component = {
'bus_interface':{}, 'bus_interface':{},
@ -651,8 +692,16 @@ def parse_hdl_component(path, ctime):
dm[signal_name].append(bus_name[0:bus_name.find('_signal_reset')]) dm[signal_name].append(bus_name[0:bus_name.find('_signal_reset')])
continue 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] = { bs[bus_name] = {
'name': sattrib(get(bus_interface, 'busType'), 'name'), 'name': sattrib(get(bus_interface, 'busType'), 'name'),
'role': bus_role,
'dependency': get_dependency(bus_interface, 'busInterface'), 'dependency': get_dependency(bus_interface, 'busInterface'),
'port_map': {} 'port_map': {}
} }
@ -747,6 +796,7 @@ def manage_hdl_components(env, docnames, libraries):
pass pass
else: else:
cp[lib] = parse_hdl_component(f, ctime) cp[lib] = parse_hdl_component(f, ctime)
hdl_component.render(env, lib, cp[lib])
docnames.append(doc) docnames.append(doc)
# From https://github.com/tfcollins/vger/blob/main/vger/hdl_reg_map.py # 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): def setup(app):
app.add_directive('collapsible', directive_collapsible) app.add_directive('collapsible', directive_collapsible)
app.add_directive('hdl-parameters', directive_parameters) 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-interfaces', directive_interfaces)
app.add_directive('hdl-regmap', directive_regmap) app.add_directive('hdl-regmap', directive_regmap)

View File

@ -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")

View File

@ -3,8 +3,7 @@
High-Speed DMA Controller High-Speed DMA Controller
================================================================================ ================================================================================
.. symbolator:: ../../../library/axi_dmac/axi_dmac.v .. hdl-component-diagram::
:caption: axi_dmac
The AXI DMAC is a high-speed, high-throughput, general purpose DMA controller 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 intended to be used to transfer data between system memory and other peripherals
@ -71,7 +70,6 @@ Configuration Parameters
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
.. hdl-parameters:: .. hdl-parameters::
:path: library/axi_dmac
* - ID * - ID
- Instance identification number. - Instance identification number.

View File

@ -3,8 +3,7 @@
AXI SPI Engine Module AXI SPI Engine Module
================================================================================ ================================================================================
.. symbolator:: ../../../library/spi_engine/axi_spi_engine/axi_spi_engine.v .. hdl-component-diagram::
:caption: axi_spi_engine
The AXI SPI Engine peripheral allows asynchronous interrupt-driven memory-mapped The AXI SPI Engine peripheral allows asynchronous interrupt-driven memory-mapped
access to a SPI Engine Control Interface. access to a SPI Engine Control Interface.

View File

@ -3,8 +3,7 @@
SPI Engine Execution Module SPI Engine Execution Module
================================================================================ ================================================================================
.. symbolator:: ../../../library/spi_engine/spi_engine_execution/spi_engine_execution.v .. hdl-component-diagram::
:caption: spi_engine_execution
The SPI Engine Execution peripheral forms the heart of the SPI Engine framework. 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 It is responsible for handling a SPI Engine control stream and translates it

View File

@ -3,8 +3,7 @@
SPI Engine Interconnect Module SPI Engine Interconnect Module
================================================================================ ================================================================================
.. symbolator:: ../../../library/spi_engine/spi_engine_interconnect/spi_engine_interconnect.v .. hdl-component-diagram::
:caption: axi_spi_engine
The SPI Engine Interconnect module allows connecting multiple The SPI Engine Interconnect module allows connecting multiple
:ref:`spi_engine control-interface` masters to a single :ref:`spi_engine control-interface` masters to a single
@ -38,7 +37,6 @@ Configuration Parameters
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
.. hdl-parameters:: .. hdl-parameters::
:path: library/spi_engine/spi_engine_interconnect
* - DATA_WIDTH * - DATA_WIDTH
- Data width of the parallel SDI/SDO data interfaces. - Data width of the parallel SDI/SDO data interfaces.

View File

@ -3,7 +3,7 @@
SPI Engine Offload Module 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 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 data stream in a RAM or ROM module. The command stream is executed when the

View File

@ -3,8 +3,8 @@
Template Module Template Module
================================================================================ ================================================================================
.. symbolator:: ../../../library/spi_engine/spi_engine_execution/spi_engine_execution.v .. hdl-component-diagram::
:caption: spi_engine_execution :path: library/spi_engine/spi_engine_execution
The {module name} is responsible for {brief description}. The {module name} is responsible for {brief description}.

View File

@ -5,8 +5,8 @@
IP Template IP Template
================================================================================ ================================================================================
.. symbolator:: ../../../library/spi_engine/spi_engine_execution/spi_engine_execution.v .. hdl-component-diagram::
:caption: spi_engine_execution :path: library/spi_engine/spi_engine_execution
Features Features
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

@ -7,5 +7,3 @@ aiohttp
aiodns aiodns
sphinxcontrib-wavedrom sphinxcontrib-wavedrom
sphinxcontrib-svg2pdfconverter sphinxcontrib-svg2pdfconverter
https://github.com/hdl/pyhdlparser/tarball/master
https://github.com/hdl/symbolator/tarball/master

View File

@ -145,3 +145,11 @@ td.description {
.default .pre { .default .pre {
white-space: pre; white-space: pre;
} }
#hdl-component-diagram {
text-align: center;
}
#hdl-component-diagram svg {
max-width: 100%;
}

View File

@ -241,12 +241,6 @@ do:
pip install -r requirements.txt pip install -r requirements.txt
Symbolator directive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`Symbolator <https://kevinpt.github.io/symbolator/>`_ is a tool to generate
component diagrams.
Custom directives and roles 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 The ``:path:`` option is optional, and should **not** be included if the
documentation file path matches the *component.xml* hierarchically. 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: <ip_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 HDL regmap directive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~