Skip to content

Commit 22205d0

Browse files
author
Shareef Jalloq
committed
feat: add wavedrom bitfield support
This commit adds support for inline wavedrom bitfield rendering. Each register gains an SVG view of the bit fields. Adds a dependenc on sphinxcontrib-yowasp-wavedrom.
1 parent 3937f53 commit 22205d0

6 files changed

Lines changed: 183 additions & 19 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies = [
1212
"peakrdl-html ~= 2.11",
1313
"sphinx >= 1.8",
1414
"myst-parser >= 1.0",
15+
"sphinxcontrib-yowasp-wavedrom",
1516
]
1617

1718
authors = [

src/sphinx_peakrdl/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from pathlib import Path
12
from typing import TYPE_CHECKING
23

34
from .__about__ import __version__
@@ -10,14 +11,23 @@
1011
from sphinx.application import Sphinx
1112
from sphinx.util.typing import ExtensionMetadata
1213

14+
15+
def _setup_static_path(app: "Sphinx") -> None:
16+
static_dir = str(Path(__file__).parent / "_static")
17+
if static_dir not in app.config.html_static_path:
18+
app.config.html_static_path.append(static_dir)
19+
20+
1321
def setup(app: "Sphinx") -> "ExtensionMetadata":
1422
config.setup_config(app)
1523

1624
app.connect("config-inited", config.elaborate_config_callback)
1725
app.connect("env-before-read-docs", build.compile_input_callback)
1826
app.connect("html-collect-pages", html.write_html_callback)
27+
app.connect("builder-inited", _setup_static_path)
1928

2029
app.add_domain(PeakRDLDomain)
30+
app.add_css_file("peakrdl.css")
2131

2232
return {
2333
'version': __version__,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.peakrdl-bitfield {
2+
margin-bottom: 1.5em;
3+
}

src/sphinx_peakrdl/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def setup_config(app: "Sphinx") -> None:
3232

3333
# Inline doc settings
3434
app.add_config_value("peakrdl_doc_wrap_section", True, "env", [bool])
35+
app.add_config_value("peakrdl_doc_field_sections", False, "env", [bool])
3536

3637

3738
def elaborate_config_callback(app: "Sphinx", cfg: "Config") -> None:

src/sphinx_peakrdl/directives/docnode.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
from typing import Sequence, Optional, List, Tuple
22

3+
import json
4+
35
from sphinx.domains import Domain
46
from sphinx.util.docutils import SphinxDirective
57
from sphinx.util import logging
68
from sphinx import addnodes
79

810
from docutils import nodes
911
from docutils.parsers.rst import directives
12+
from docutils.statemachine import StringList
1013

1114
from systemrdl.node import Node, RegNode, AddressableNode, SignalNode, RootNode
1215
from systemrdl.rdltypes.references import PropertyReference
1316
from systemrdl.source_ref import FileSourceRef, DetailedFileSourceRef
1417

1518
from ..utils import lookup_rdl_node, FieldList, Table, alpha_from_int
19+
from ..wavedrom import register_to_wavedrom
1620

1721
from ..markdown.render import render_to_docutils
1822

@@ -200,8 +204,8 @@ def make_rdl_node_doc(self, rdl_node: Node) -> Sequence[nodes.Element]:
200204
ref_id = rdl_node.get_path(array_suffix="", empty_array_suffix="")
201205
heading = nodes.section()
202206
heading.attributes["ids"] = [ref_id]
203-
# TODO: Include RDL name in title if it was set.
204-
heading.append(nodes.title(text=rdl_node.inst_name))
207+
display_name = rdl_node.get_property("name") or rdl_node.inst_name
208+
heading.append(nodes.title(text=display_name))
205209
heading.extend(doc_nodes)
206210
doc_nodes = [heading]
207211

@@ -210,13 +214,70 @@ def make_rdl_node_doc(self, rdl_node: Node) -> Sequence[nodes.Element]:
210214
return doc_nodes
211215

212216

217+
def _build_wavedrom_node(self, rdl_node: RegNode) -> nodes.container:
218+
"""Build a WaveDrom bitfield diagram via nested RST parsing."""
219+
wavedrom_dict = register_to_wavedrom(rdl_node)
220+
wavedrom_json = json.dumps(wavedrom_dict)
221+
222+
rst = StringList()
223+
path = rdl_node.get_path(array_suffix="", empty_array_suffix="")
224+
image_name = f"regblock_{path.replace('.', '_')}"
225+
rst.append(f".. wavedrom:: {image_name}", "<peakrdl>")
226+
rst.append("", "<peakrdl>")
227+
for line in wavedrom_json.splitlines():
228+
rst.append(f" {line}", "<peakrdl>")
229+
rst.append("", "<peakrdl>")
230+
231+
container = nodes.container(classes=["peakrdl-bitfield"])
232+
self.state.nested_parse(rst, 0, container)
233+
return container
234+
235+
def _build_field_def_list(self, rdl_node: RegNode) -> list[nodes.Element]:
236+
"""Build field descriptions as a definition list (compact style)."""
237+
def_list = nodes.definition_list()
238+
for field in reversed(rdl_node.fields()):
239+
desc = field.get_property("desc")
240+
if not desc:
241+
continue
242+
243+
dli = nodes.definition_list_item()
244+
def_list.append(dli)
245+
246+
dl_term = nodes.term(text=field.inst_name)
247+
dl_def = nodes.definition()
248+
dl_def_p = self.get_rdl_desc(field)
249+
dl_def.append(dl_def_p)
250+
251+
dli.append(dl_term)
252+
dli.append(dl_def)
253+
return [def_list]
254+
255+
def _build_field_sections(self, rdl_node: RegNode) -> list[nodes.Element]:
256+
"""Build field descriptions as individual sections with headings."""
257+
sections: list[nodes.Element] = []
258+
for field in reversed(rdl_node.fields()):
259+
desc = field.get_property("desc")
260+
if not desc:
261+
continue
262+
263+
path = field.get_path(array_suffix="", empty_array_suffix="")
264+
section_id = nodes.make_id(path)
265+
section = nodes.section(ids=[section_id])
266+
section += nodes.title(text=field.inst_name)
267+
section += self.get_rdl_desc(field)
268+
sections.append(section)
269+
return sections
270+
213271
def make_rdl_reg_doc(self, rdl_node: RegNode) -> Sequence[nodes.Element]:
214272
# Info Field List Header
215273
fl = self.get_info_header(rdl_node)
216274

217275
# Description
218276
desc_paragraph = self.get_rdl_desc(rdl_node)
219277

278+
# WaveDrom bitfield diagram
279+
wavedrom_node = self._build_wavedrom_node(rdl_node)
280+
220281
# Field Table
221282
table = Table(["Bits", "Identifier", "Access", "Reset", "Name"])
222283
for field in reversed(rdl_node.fields()):
@@ -255,24 +316,12 @@ def make_rdl_reg_doc(self, rdl_node: RegNode) -> Sequence[nodes.Element]:
255316
])
256317

257318
# Field descriptions
258-
def_list = nodes.definition_list()
259-
for field in reversed(rdl_node.fields()):
260-
desc = field.get_property("desc")
261-
if not desc:
262-
continue
263-
264-
dli = nodes.definition_list_item()
265-
def_list.append(dli)
266-
267-
dl_term = nodes.term(text = field.inst_name)
268-
dl_def = nodes.definition()
269-
dl_def_p = self.get_rdl_desc(field)
270-
dl_def.append(dl_def_p)
271-
272-
dli.append(dl_term)
273-
dli.append(dl_def)
319+
if self.config.peakrdl_doc_field_sections:
320+
field_descs = self._build_field_sections(rdl_node)
321+
else:
322+
field_descs = self._build_field_def_list(rdl_node)
274323

275-
return [fl, desc_paragraph, table.as_node(), def_list]
324+
return [fl, desc_paragraph, wavedrom_node, table.as_node(), *field_descs]
276325

277326

278327
def make_rdl_grouplike_doc(self, rdl_node: AddressableNode) -> Sequence[nodes.Element]:

src/sphinx_peakrdl/wavedrom.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Register to WaveDrom JSON conversion.
2+
3+
Works directly with systemrdl-compiler RegNode/FieldNode — no additional
4+
dependencies beyond systemrdl-compiler itself.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import json
10+
from typing import TYPE_CHECKING
11+
12+
from systemrdl.rdltypes import AccessType
13+
14+
if TYPE_CHECKING:
15+
from systemrdl.node import FieldNode, RegNode
16+
17+
18+
def register_to_wavedrom(rdl_node: RegNode) -> dict:
19+
"""Convert a RegNode to a WaveDrom bitfield JSON dict.
20+
21+
Returns a dict ready for ``json.dumps()``, in the format
22+
``{"reg": [...], "config": {"lanes": N, ...}}``.
23+
24+
Fields and gaps are emitted LSB-first (bit 0 = first element),
25+
which matches WaveDrom's bit numbering convention.
26+
"""
27+
regwidth = rdl_node.get_property("regwidth")
28+
accesswidth = rdl_node.get_property("accesswidth")
29+
30+
# Sort fields by lsb ascending (LSB-first for WaveDrom)
31+
fields = sorted(rdl_node.fields(), key=lambda f: f.lsb)
32+
33+
# Single-pass: walk fields LSB-first, emitting gap entries for holes
34+
reg_entries: list[dict] = []
35+
pos = 0
36+
for field in fields:
37+
if field.lsb > pos:
38+
# Gap between current position and this field
39+
reg_entries.append(_gap_entry(field.lsb - pos))
40+
reg_entries.append(_field_entry(field))
41+
pos = field.msb + 1
42+
43+
# Trailing gap to fill out the register width
44+
if pos < regwidth:
45+
reg_entries.append(_gap_entry(regwidth - pos))
46+
47+
# Compute vspace for rotated labels on narrow fields
48+
max_rotated_len = max(
49+
(len(f.inst_name) for f in fields if f.width <= 2), default=0
50+
)
51+
vspace = max(80, max_rotated_len * 8 + 80) if max_rotated_len else 80
52+
53+
lanes = regwidth // accesswidth
54+
config: dict = {"lanes": lanes, "hspace": 888, "vspace": vspace}
55+
56+
return {"reg": reg_entries, "config": config}
57+
58+
59+
def _field_entry(field: FieldNode) -> dict:
60+
"""Convert a single FieldNode to a WaveDrom reg entry."""
61+
sw = field.get_property("sw")
62+
access_str = _access_to_str(sw)
63+
entry: dict = {
64+
"name": field.inst_name,
65+
"bits": field.width,
66+
"attr": [access_str],
67+
"type": _access_to_type(sw),
68+
}
69+
if field.width <= 2:
70+
entry["rotate"] = -90
71+
return entry
72+
73+
74+
def _gap_entry(width: int) -> dict:
75+
"""Create a WaveDrom reserved/gap entry."""
76+
return {"bits": width, "name": "", "type": 5}
77+
78+
79+
def _access_to_str(sw: AccessType) -> str:
80+
"""Map AccessType to a short display string."""
81+
return {
82+
AccessType.rw: "rw",
83+
AccessType.r: "ro",
84+
AccessType.w: "wo",
85+
AccessType.rw1: "rw1",
86+
AccessType.w1: "w1",
87+
AccessType.na: "na",
88+
}.get(sw, "?")
89+
90+
91+
def _access_to_type(sw: AccessType) -> int:
92+
"""Map AccessType to WaveDrom type (colour index)."""
93+
return {
94+
AccessType.rw: 0,
95+
AccessType.r: 2,
96+
AccessType.w: 4,
97+
AccessType.rw1: 0,
98+
AccessType.w1: 4,
99+
AccessType.na: 5,
100+
}.get(sw, 0)

0 commit comments

Comments
 (0)