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
parent
9f4d5ff71f
commit
940c3ccd35
|
@ -20,7 +20,6 @@ extensions = [
|
|||
"sphinx.ext.todo",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinxcontrib.wavedrom",
|
||||
"symbolator_sphinx",
|
||||
"adi_links",
|
||||
"adi_hdl_parser"
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}.
|
||||
|
||||
|
|
|
@ -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
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
@ -7,5 +7,3 @@ aiohttp
|
|||
aiodns
|
||||
sphinxcontrib-wavedrom
|
||||
sphinxcontrib-svg2pdfconverter
|
||||
https://github.com/hdl/pyhdlparser/tarball/master
|
||||
https://github.com/hdl/symbolator/tarball/master
|
||||
|
|
|
@ -145,3 +145,11 @@ td.description {
|
|||
.default .pre {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#hdl-component-diagram {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#hdl-component-diagram svg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
Loading…
Reference in New Issue