Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"peakrdl-html ~= 2.11",
"sphinx >= 1.8",
"myst-parser >= 1.0",
"sphinxcontrib-yowasp-wavedrom",
]

authors = [
Expand Down
10 changes: 10 additions & 0 deletions src/sphinx_peakrdl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
from typing import TYPE_CHECKING

from .__about__ import __version__
Expand All @@ -10,14 +11,23 @@
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata


def _setup_static_path(app: "Sphinx") -> None:
static_dir = str(Path(__file__).parent / "_static")
if static_dir not in app.config.html_static_path:
app.config.html_static_path.append(static_dir)


def setup(app: "Sphinx") -> "ExtensionMetadata":
config.setup_config(app)

app.connect("config-inited", config.elaborate_config_callback)
app.connect("env-before-read-docs", build.compile_input_callback)
app.connect("html-collect-pages", html.write_html_callback)
app.connect("builder-inited", _setup_static_path)

app.add_domain(PeakRDLDomain)
app.add_css_file("peakrdl.css")

return {
'version': __version__,
Expand Down
3 changes: 3 additions & 0 deletions src/sphinx_peakrdl/_static/peakrdl.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.peakrdl-bitfield {
margin-bottom: 1.5em;
}
1 change: 1 addition & 0 deletions src/sphinx_peakrdl/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def setup_config(app: "Sphinx") -> None:

# Inline doc settings
app.add_config_value("peakrdl_doc_wrap_section", True, "env", [bool])
app.add_config_value("peakrdl_doc_field_sections", False, "env", [bool])


def elaborate_config_callback(app: "Sphinx", cfg: "Config") -> None:
Expand Down
86 changes: 68 additions & 18 deletions src/sphinx_peakrdl/directives/docnode.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
from typing import Sequence, Optional, List, Tuple

import json

from sphinx.domains import Domain
from sphinx.util.docutils import SphinxDirective
from sphinx.util import logging
from sphinx import addnodes

from docutils import nodes
from docutils.parsers.rst import directives
from docutils.statemachine import StringList

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

from ..utils import lookup_rdl_node, FieldList, Table, alpha_from_int
from ..wavedrom import register_to_wavedrom

from ..markdown.render import render_to_docutils

Expand Down Expand Up @@ -200,7 +204,8 @@ def make_rdl_node_doc(self, rdl_node: Node) -> Sequence[nodes.Element]:
ref_id = rdl_node.get_path(array_suffix="", empty_array_suffix="")
heading = nodes.section()
heading.attributes["ids"] = [ref_id]
heading.append(nodes.title(text=rdl_node.get_property("name")))
display_name = rdl_node.get_property("name") or rdl_node.inst_name
heading.append(nodes.title(text=display_name))
heading.extend(doc_nodes)
doc_nodes = [heading]

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


def _build_wavedrom_node(self, rdl_node: RegNode) -> nodes.container:
"""Build a WaveDrom bitfield diagram via nested RST parsing."""
wavedrom_dict = register_to_wavedrom(rdl_node)
wavedrom_json = json.dumps(wavedrom_dict)

rst = StringList()
path = rdl_node.get_path(array_suffix="", empty_array_suffix="")
image_name = f"regblock_{path.replace('.', '_')}"
rst.append(f".. wavedrom:: {image_name}", "<peakrdl>")
rst.append("", "<peakrdl>")
for line in wavedrom_json.splitlines():
rst.append(f" {line}", "<peakrdl>")
rst.append("", "<peakrdl>")

container = nodes.container(classes=["peakrdl-bitfield"])
self.state.nested_parse(rst, 0, container)
return container

def _build_field_def_list(self, rdl_node: RegNode) -> list[nodes.Element]:
"""Build field descriptions as a definition list (compact style)."""
def_list = nodes.definition_list()
for field in reversed(rdl_node.fields()):
desc = field.get_property("desc")
if not desc:
continue

dli = nodes.definition_list_item()
def_list.append(dli)

dl_term = nodes.term(text=field.inst_name)
dl_def = nodes.definition()
dl_def_p = self.get_rdl_desc(field)
dl_def.append(dl_def_p)

dli.append(dl_term)
dli.append(dl_def)
return [def_list]

def _build_field_sections(self, rdl_node: RegNode) -> list[nodes.Element]:
"""Build field descriptions as individual sections with headings."""
sections: list[nodes.Element] = []
for field in reversed(rdl_node.fields()):
desc = field.get_property("desc")
if not desc:
continue

path = field.get_path(array_suffix="", empty_array_suffix="")
section_id = nodes.make_id(path)
section = nodes.section(ids=[section_id])
section += nodes.title(text=field.inst_name)
section += self.get_rdl_desc(field)
sections.append(section)
return sections

def make_rdl_reg_doc(self, rdl_node: RegNode) -> Sequence[nodes.Element]:
# Info Field List Header
fl = self.get_info_header(rdl_node)

# Description
desc_paragraph = self.get_rdl_desc(rdl_node)

# WaveDrom bitfield diagram
wavedrom_node = self._build_wavedrom_node(rdl_node)

# Field Table
table = Table(["Bits", "Identifier", "Access", "Reset", "Name"])
for field in reversed(rdl_node.fields()):
Expand Down Expand Up @@ -254,24 +316,12 @@ def make_rdl_reg_doc(self, rdl_node: RegNode) -> Sequence[nodes.Element]:
])

# Field descriptions
def_list = nodes.definition_list()
for field in reversed(rdl_node.fields()):
desc = field.get_property("desc")
if not desc:
continue

dli = nodes.definition_list_item()
def_list.append(dli)

dl_term = nodes.term(text = field.inst_name)
dl_def = nodes.definition()
dl_def_p = self.get_rdl_desc(field)
dl_def.append(dl_def_p)

dli.append(dl_term)
dli.append(dl_def)
if self.config.peakrdl_doc_field_sections:
field_descs = self._build_field_sections(rdl_node)
else:
field_descs = self._build_field_def_list(rdl_node)

return [fl, desc_paragraph, table.as_node(), def_list]
return [fl, desc_paragraph, wavedrom_node, table.as_node(), *field_descs]


def make_rdl_grouplike_doc(self, rdl_node: AddressableNode) -> Sequence[nodes.Element]:
Expand Down
100 changes: 100 additions & 0 deletions src/sphinx_peakrdl/wavedrom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Register to WaveDrom JSON conversion.

Works directly with systemrdl-compiler RegNode/FieldNode — no additional
dependencies beyond systemrdl-compiler itself.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING

from systemrdl.rdltypes import AccessType

if TYPE_CHECKING:
from systemrdl.node import FieldNode, RegNode


def register_to_wavedrom(rdl_node: RegNode) -> dict:
"""Convert a RegNode to a WaveDrom bitfield JSON dict.

Returns a dict ready for ``json.dumps()``, in the format
``{"reg": [...], "config": {"lanes": N, ...}}``.

Fields and gaps are emitted LSB-first (bit 0 = first element),
which matches WaveDrom's bit numbering convention.
"""
regwidth = rdl_node.get_property("regwidth")
accesswidth = rdl_node.get_property("accesswidth")

# Sort fields by lsb ascending (LSB-first for WaveDrom)
fields = sorted(rdl_node.fields(), key=lambda f: f.lsb)

# Single-pass: walk fields LSB-first, emitting gap entries for holes
reg_entries: list[dict] = []
pos = 0
for field in fields:
if field.lsb > pos:
# Gap between current position and this field
reg_entries.append(_gap_entry(field.lsb - pos))
reg_entries.append(_field_entry(field))
pos = field.msb + 1

# Trailing gap to fill out the register width
if pos < regwidth:
reg_entries.append(_gap_entry(regwidth - pos))

# Compute vspace for rotated labels on narrow fields
max_rotated_len = max(
(len(f.inst_name) for f in fields if f.width <= 2), default=0
)
vspace = max(80, max_rotated_len * 8 + 80) if max_rotated_len else 80

lanes = regwidth // accesswidth
config: dict = {"lanes": lanes, "hspace": 888, "vspace": vspace}

return {"reg": reg_entries, "config": config}


def _field_entry(field: FieldNode) -> dict:
"""Convert a single FieldNode to a WaveDrom reg entry."""
sw = field.get_property("sw")
access_str = _access_to_str(sw)
entry: dict = {
"name": field.inst_name,
"bits": field.width,
"attr": [access_str],
"type": _access_to_type(sw),
}
if field.width <= 2:
entry["rotate"] = -90
return entry


def _gap_entry(width: int) -> dict:
"""Create a WaveDrom reserved/gap entry."""
return {"bits": width, "name": "", "type": 5}


def _access_to_str(sw: AccessType) -> str:
"""Map AccessType to a short display string."""
return {
AccessType.rw: "rw",
AccessType.r: "ro",
AccessType.w: "wo",
AccessType.rw1: "rw1",
AccessType.w1: "w1",
AccessType.na: "na",
}.get(sw, "?")


def _access_to_type(sw: AccessType) -> int:
"""Map AccessType to WaveDrom type (colour index)."""
return {
AccessType.rw: 0,
AccessType.r: 2,
AccessType.w: 4,
AccessType.rw1: 0,
AccessType.w1: 4,
AccessType.na: 5,
}.get(sw, 0)