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.viewcode",
"sphinxcontrib.wavedrom",
"symbolator_sphinx",
"adi_links",
"adi_hdl_parser"
]

View File

@ -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
@ -534,10 +535,9 @@ class directive_parameters(directive_base):
node = node_div()
if 'path' in self.options:
if 'path' not in self.options:
self.options['path'] = env.docname.replace('/index', '')
lib_name = self.options['path']
else:
lib_name = env.docname.replace('/index', '')
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)

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
================================================================================
.. 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.

View File

@ -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.

View File

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

View File

@ -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.

View File

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

View File

@ -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}.

View File

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

View File

@ -7,5 +7,3 @@ aiohttp
aiodns
sphinxcontrib-wavedrom
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 {
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
Symbolator directive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`Symbolator <https://kevinpt.github.io/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: <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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~