Skip to content

doc: boards: extensions: Add Sphinx directive for board supported hardware #85652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
27 changes: 1 addition & 26 deletions boards/bbc/microbit/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ magnetometer sensors, Bluetooth and USB connectivity, a display consisting of
external battery pack. The device inputs and outputs are through five ring
connectors that are part of the 23-pin edge connector.

* :abbr:`NVIC (Nested Vectored Interrupt Controller)`
* :abbr:`RTC (nRF RTC System Clock)`
* UART
* GPIO
* FLASH
* RADIO (Bluetooth Low Energy)

More information about the board can be found at the `microbit website`_.

Hardware
Expand All @@ -39,25 +32,7 @@ The micro:bit has the following physical features:
Supported Features
==================

The bbc_microbit board configuration supports the following nRF51
hardware features:

+-----------+------------+----------------------+
| Interface | Controller | Driver/Component |
+===========+============+======================+
| NVIC | on-chip | nested vectored |
| | | interrupt controller |
+-----------+------------+----------------------+
| RTC | on-chip | system clock |
+-----------+------------+----------------------+
| UART | on-chip | serial port |
+-----------+------------+----------------------+
| GPIO | on-chip | gpio |
+-----------+------------+----------------------+
| FLASH | on-chip | flash |
+-----------+------------+----------------------+
| RADIO | on-chip | Bluetooth |
+-----------+------------+----------------------+
.. zephyr:board-supported-hw::
Programming and Debugging
*************************
Expand Down
20 changes: 1 addition & 19 deletions boards/bbc/microbit_v2/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,7 @@ The micro:bit-v2 has the following physical features:
Supported Features
==================

The bbc_microbit_v2 board configuration supports the following
hardware features:

+-----------+------------+----------------------+
| Interface | Controller | Driver/Component |
+===========+============+======================+
| NVIC | on-chip | nested vectored |
| | | interrupt controller |
+-----------+------------+----------------------+
| RTC | on-chip | system clock |
+-----------+------------+----------------------+
| UART | on-chip | serial port |
+-----------+------------+----------------------+
| GPIO | on-chip | gpio |
+-----------+------------+----------------------+
| FLASH | on-chip | flash |
+-----------+------------+----------------------+
| RADIO | on-chip | Bluetooth |
+-----------+------------+----------------------+
.. zephyr:board-supported-hw::
Programming and Debugging
*************************
Expand Down
171 changes: 171 additions & 0 deletions doc/_extensions/zephyr/domain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"""

import json
import re
import sys
from collections.abc import Iterator
from os import path
Expand Down Expand Up @@ -65,6 +66,43 @@
TEMPLATES_DIR = Path(__file__).parent / "templates"
RESOURCES_DIR = Path(__file__).parent / "static"

# Load and parse binding types from text file
BINDINGS_TXT_PATH = ZEPHYR_BASE / "dts" / "bindings" / "binding-types.txt"
ACRONYM_PATTERN = re.compile(r'([a-zA-Z0-9-]+)\s*\((.*?)\)')
BINDING_TYPE_TO_DOCUTILS_NODE = {}


def parse_text_with_acronyms(text):
"""Parse text that may contain acronyms into a list of nodes."""
result = nodes.inline()
last_end = 0

for match in ACRONYM_PATTERN.finditer(text):
# Add any text before the acronym
if match.start() > last_end:
result += nodes.Text(text[last_end : match.start()])

# Add the acronym
abbr, explanation = match.groups()
result += nodes.abbreviation(abbr, abbr, explanation=explanation)
last_end = match.end()

# Add any remaining text
if last_end < len(text):
result += nodes.Text(text[last_end:])

return result


with open(BINDINGS_TXT_PATH) as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue

key, value = line.split('\t', 1)
BINDING_TYPE_TO_DOCUTILS_NODE[key] = parse_text_with_acronyms(value)

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -685,6 +723,7 @@ def run(self):
board_node = BoardNode(id=board_name)
board_node["full_name"] = board["full_name"]
board_node["vendor"] = vendors.get(board["vendor"], board["vendor"])
board_node["supported_features"] = board["supported_features"]
board_node["archs"] = board["archs"]
board_node["socs"] = board["socs"]
board_node["image"] = board["image"]
Expand Down Expand Up @@ -716,6 +755,137 @@ def run(self):
return [nodes.paragraph(text="Board catalog is only available in HTML.")]


class BoardSupportedHardwareDirective(SphinxDirective):
"""A directive for showing the supported hardware features of a board."""

has_content = False
required_arguments = 0
optional_arguments = 0

def run(self):
env = self.env
docname = env.docname

matcher = NodeMatcher(BoardNode)
board_nodes = list(self.state.document.traverse(matcher))
if not board_nodes:
logger.warning(
"board-supported-hw directive must be used in a board documentation page.",
location=(docname, self.lineno),
)
return []

board_node = board_nodes[0]
supported_features = board_node["supported_features"]
result_nodes = []

paragraph = nodes.paragraph()
paragraph += nodes.Text("The ")
paragraph += nodes.literal(text=board_node["id"])
paragraph += nodes.Text(" board supports the hardware features listed below.")
result_nodes.append(paragraph)

if not env.app.config.zephyr_generate_hw_features:
note = nodes.admonition()
note += nodes.title(text="Note")
note["classes"].append("warning")
note += nodes.paragraph(
text="The list of supported hardware features was not generated. Run a full "
"documentation build for the required metadata to be available."
)
result_nodes.append(note)
return result_nodes

# Add the note before any tables
note = nodes.admonition()
note += nodes.title(text="Note")
note["classes"].append("note")
note += nodes.paragraph(
text="The tables below were automatically generated using information from the "
"Devicetree. They may not be fully representative of all the hardware features "
"supported by the board."
)
result_nodes.append(note)

for target, features in sorted(supported_features.items()):
if not features:
continue

target_heading = nodes.section(ids=[f"{board_node['id']}-{target}-hw-features"])
heading = nodes.title()
heading += nodes.literal(text=target)
heading += nodes.Text(" target")
target_heading += heading
result_nodes.append(target_heading)

table = nodes.table(classes=["colwidths-given"])
tgroup = nodes.tgroup(cols=3)

tgroup += nodes.colspec(colwidth=20, classes=["col-1"])
tgroup += nodes.colspec(colwidth=50)
tgroup += nodes.colspec(colwidth=30)

thead = nodes.thead()
row = nodes.row()
headers = ["Type", "Description", "Compatible"]
for header in headers:
row += nodes.entry("", nodes.paragraph(text=header))
thead += row
tgroup += thead

tbody = nodes.tbody()

def feature_sort_key(feature):
# Put "CPU" first. Later updates might also give priority to features
# like "sensor"s, for example.
if feature == "cpu":
return (0, feature)
return (1, feature)

sorted_features = sorted(features.keys(), key=feature_sort_key)

for feature in sorted_features:
items = list(features[feature].items())
num_items = len(items)

for i, (key, value) in enumerate(items):
row = nodes.row()

# Add type column only for first row of a feature
if i == 0:
type_entry = nodes.entry(morerows=num_items - 1)
type_entry += nodes.paragraph(
"",
"",
BINDING_TYPE_TO_DOCUTILS_NODE.get(
feature, nodes.Text(feature)
).deepcopy(),
)
row += type_entry

row += nodes.entry("", nodes.paragraph(text=value))

# Create compatible xref
xref = addnodes.pending_xref(
"",
refdomain="std",
reftype="dtcompatible",
reftarget=key,
refexplicit=False,
refwarn=True,
)
xref += nodes.literal(text=key)
row += nodes.entry("", nodes.paragraph("", "", xref))

tbody += row

tgroup += tbody
table += tgroup
result_nodes.append(table)

return result_nodes


class ZephyrDomain(Domain):
"""Zephyr domain"""

Expand All @@ -734,6 +904,7 @@ class ZephyrDomain(Domain):
"code-sample-category": CodeSampleCategoryDirective,
"board-catalog": BoardCatalogDirective,
"board": BoardDirective,
"board-supported-hw": BoardSupportedHardwareDirective,
}

object_types: dict[str, ObjType] = {
Expand Down
12 changes: 11 additions & 1 deletion doc/_extensions/zephyr/domain/templates/board-card.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@
data-arch="{{ board.archs | join(" ") }}"
data-vendor="{{ board.vendor }}"
data-socs="{{ board.socs | join(" ") }}"
data-supported-features="{{ board.supported_features | join(" ") }}" tabindex="0">
data-supported-features="
{%- set feature_types = [] -%}
{%- for target_features in board.supported_features.values() -%}
{%- for feature_type in target_features.keys() -%}
{%- if feature_type not in feature_types -%}
{%- set _ = feature_types.append(feature_type) -%}
{%- endif -%}
{%- endfor -%}
{%- endfor -%}
{{- feature_types|join(' ') -}}
" tabindex="0">
<div class="vendor">{{ vendors[board.vendor] }}</div>
{% if board.image -%}
<img alt="A picture of the {{ board.full_name }} board"
Expand Down
69 changes: 59 additions & 10 deletions doc/_scripts/gen_boards_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,59 @@
logger = logging.getLogger(__name__)


class DeviceTreeUtils:
_compat_description_cache = {}

@classmethod
def get_first_sentence(cls, text):
"""Extract the first sentence from a text block (typically a node description).

Args:
text: The text to extract the first sentence from.

Returns:
The first sentence found in the text, or the entire text if no sentence
boundary is found.
"""
# Split the text into lines
lines = text.splitlines()

# Trim leading and trailing whitespace from each line and ignore completely blank lines
lines = [line.strip() for line in lines]

if not lines:
return ""

# Case 1: Single line followed by blank line(s) or end of text
if len(lines) == 1 or (len(lines) > 1 and lines[1] == ""):
first_line = lines[0]
# Check for the first period
period_index = first_line.find(".")
# If there's a period, return up to the period; otherwise, return the full line
return first_line[: period_index + 1] if period_index != -1 else first_line

# Case 2: Multiple contiguous lines, treat as a block
block = " ".join(lines)
period_index = block.find(".")
# If there's a period, return up to the period; otherwise, return the full block
return block[: period_index + 1] if period_index != -1 else block

@classmethod
def get_cached_description(cls, node):
"""Get the cached description for a devicetree node.

Args:
node: A devicetree node object with matching_compat and description attributes.

Returns:
The cached description for the node's compatible, creating it if needed.
"""
return cls._compat_description_cache.setdefault(
node.matching_compat,
cls.get_first_sentence(node.description)
)


def guess_file_from_patterns(directory, patterns, name, extensions):
for pattern in patterns:
for ext in extensions:
Expand Down Expand Up @@ -197,13 +250,10 @@ def get_catalog(generate_hw_features=False):
doc_page = guess_doc_page(board)

supported_features = {}
targets = set()

# Use pre-gathered build info and DTS files
if board.name in board_devicetrees:
for board_target, edt in board_devicetrees[board.name].items():
targets.add(board_target)

okay_nodes = [
node
for node in edt.nodes
Expand All @@ -218,13 +268,13 @@ def get_catalog(generate_hw_features=False):
if binding_path.is_relative_to(ZEPHYR_BINDINGS)
else "misc"
)
target_features.setdefault(binding_type, set()).add(node.matching_compat)

description = DeviceTreeUtils.get_cached_description(node)
target_features.setdefault(binding_type, {}).setdefault(
node.matching_compat, description
)

# for now we do the union of all supported features for all of board's targets but
# in the future it's likely the catalog will be organized so that the list of
# supported features is also available per target.
supported_features.update(target_features)
# Store features for this specific target
supported_features[board_target] = target_features

# Grab all the twister files for this board and use them to figure out all the archs it
# supports.
Expand All @@ -246,7 +296,6 @@ def get_catalog(generate_hw_features=False):
"archs": list(archs),
"socs": list(socs),
"supported_features": supported_features,
"targets": list(targets),
"image": guess_image(board),
}

Expand Down
Loading
Loading