Skip to content

Commit 8b94c0e

Browse files
committed
doc: boards: extensions: Add Sphinx directive for board supported hardware
Introduce a new directive for displaying hardware features supported by a board, using information available in the devicetree. Signed-off-by: Benjamin Cabé <[email protected]>
1 parent 8458702 commit 8b94c0e

File tree

6 files changed

+353
-3
lines changed

6 files changed

+353
-3
lines changed

doc/_extensions/zephyr/domain/__init__.py

+114
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
from zephyr.doxybridge import DoxygenGroupDirective
5454
from zephyr.gh_utils import gh_link_get_url
5555

56+
from .binding_types import BINDING_TYPES
57+
5658
__version__ = "0.2.0"
5759

5860

@@ -67,6 +69,16 @@
6769

6870
logger = logging.getLogger(__name__)
6971

72+
BINDING_TYPE_TO_DOCUTILS_NODE = {}
73+
for key, value in BINDING_TYPES.items():
74+
if isinstance(value, tuple):
75+
# For abbreviations with explanations
76+
abbr, explanation = value
77+
BINDING_TYPE_TO_DOCUTILS_NODE[key] = nodes.abbreviation(abbr, abbr, explanation=explanation)
78+
else:
79+
# For simple text
80+
BINDING_TYPE_TO_DOCUTILS_NODE[key] = nodes.Text(value)
81+
7082

7183
class CodeSampleNode(nodes.Element):
7284
pass
@@ -685,6 +697,7 @@ def run(self):
685697
board_node = BoardNode(id=board_name)
686698
board_node["full_name"] = board["full_name"]
687699
board_node["vendor"] = vendors.get(board["vendor"], board["vendor"])
700+
board_node["supported_features"] = board["supported_features"]
688701
board_node["archs"] = board["archs"]
689702
board_node["socs"] = board["socs"]
690703
board_node["image"] = board["image"]
@@ -716,6 +729,106 @@ def run(self):
716729
return [nodes.paragraph(text="Board catalog is only available in HTML.")]
717730

718731

732+
class BoardSupportedHardwareDirective(SphinxDirective):
733+
"""A directive for showing the supported hardware features of a board."""
734+
735+
has_content = False
736+
required_arguments = 0
737+
optional_arguments = 0
738+
739+
def run(self):
740+
env = self.env
741+
docname = env.docname
742+
743+
matcher = NodeMatcher(BoardNode)
744+
board_nodes = list(self.state.document.traverse(matcher))
745+
if not board_nodes:
746+
logger.warning(
747+
"board-supported-hw directive must be used in a board documentation page.",
748+
location=(docname, self.lineno),
749+
)
750+
return []
751+
752+
board_node = board_nodes[0]
753+
supported_features = board_node["supported_features"]
754+
755+
if not supported_features:
756+
return []
757+
758+
# Create the table structure
759+
table = nodes.table(classes=["colwidths-given"])
760+
tgroup = nodes.tgroup(cols=3)
761+
762+
tgroup += nodes.colspec(colwidth=20, classes=["col-1"])
763+
tgroup += nodes.colspec(colwidth=50)
764+
tgroup += nodes.colspec(colwidth=30)
765+
766+
# Create header row
767+
thead = nodes.thead()
768+
row = nodes.row()
769+
headers = ["Type", "Description", "Compatible"]
770+
for header in headers:
771+
row += nodes.entry("", nodes.paragraph(text=header))
772+
thead += row
773+
tgroup += thead
774+
775+
# Create table body
776+
tbody = nodes.tbody()
777+
778+
def feature_sort_key(feature):
779+
if feature == "cpu":
780+
return (0, feature)
781+
return (1, feature)
782+
783+
sorted_features = sorted(supported_features.keys(), key=feature_sort_key)
784+
785+
for feature in sorted_features:
786+
items = list(supported_features[feature].items())
787+
num_items = len(items)
788+
789+
for i, (key, value) in enumerate(items):
790+
row = nodes.row()
791+
792+
# Add type column only for first row of a feature
793+
if i == 0:
794+
type_entry = nodes.entry(morerows=num_items - 1)
795+
type_entry += nodes.paragraph(
796+
"",
797+
"",
798+
BINDING_TYPE_TO_DOCUTILS_NODE.get(feature, nodes.Text(feature)).deepcopy(),
799+
)
800+
row += type_entry
801+
802+
row += nodes.entry("", nodes.paragraph(text=value))
803+
804+
# Create compatible xref
805+
xref = addnodes.pending_xref(
806+
"",
807+
refdomain="std",
808+
reftype="dtcompatible",
809+
reftarget=key,
810+
refexplicit=False,
811+
refwarn=True,
812+
)
813+
xref += nodes.literal(text=key)
814+
row += nodes.entry("", nodes.paragraph("", "", xref))
815+
816+
tbody += row
817+
818+
tgroup += tbody
819+
table += tgroup
820+
821+
note = nodes.admonition()
822+
note += nodes.title(text="Note")
823+
note["classes"].append("note")
824+
note += nodes.paragraph(
825+
text="The list of supported hardware features was automatically "
826+
"generated using information from the Devicetree."
827+
)
828+
829+
return [table, note]
830+
831+
719832
class ZephyrDomain(Domain):
720833
"""Zephyr domain"""
721834

@@ -734,6 +847,7 @@ class ZephyrDomain(Domain):
734847
"code-sample-category": CodeSampleCategoryDirective,
735848
"board-catalog": BoardCatalogDirective,
736849
"board": BoardDirective,
850+
"board-supported-hw": BoardSupportedHardwareDirective,
737851
}
738852

739853
object_types: dict[str, ObjType] = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""
2+
Zephyr Binding Types
3+
###################
4+
5+
SPDX-FileCopyrightText: Copyright (c) 2025 The Linux Foundation
6+
SPDX-License-Identifier: Apache-2.0
7+
8+
This module contains the mapping of binding types to their human-readable names and descriptions.
9+
Each entry can be either:
10+
- A string representing the human-readable name
11+
- A tuple of (abbreviation, full name) for cases where an abbreviation exists
12+
"""
13+
14+
BINDING_TYPES = {
15+
# zephyr-keep-sorted-start /^ "/
16+
"acpi": ("ACPI", "Advanced Configuration and Power Interface"),
17+
"adc": ("ADC", "Analog to Digital Converter"),
18+
"alh": ("ALH", "Audio Link Hub"),
19+
"arc": "ARC architecture",
20+
"arm": "ARM architecture",
21+
"audio": "Audio",
22+
"auxdisplay": "Auxiliary Display",
23+
"battery": "Battery",
24+
"base": "Base",
25+
"bluetooth": "Bluetooth",
26+
"cache": "Cache",
27+
"can": ("CAN", "Controller Area Network"),
28+
"charger": "Charger",
29+
"clock": "Clock control",
30+
"coredump": "Core dump",
31+
"counter": "Counter",
32+
"cpu": "CPU",
33+
"crypto": "Cryptographic accelerator",
34+
"dac": ("DAC", "Digital to Analog Converter"),
35+
"dai": ("DAI", "Digital Audio Interface"),
36+
"debug": "Debug",
37+
"dfpmcch": "DFPMCCH",
38+
"dfpmccu": "DFPMCCU",
39+
"disk": "Disk",
40+
"display": "Display",
41+
"display/panel": "Display panel",
42+
"dma": ("DMA", "Direct Memory Access"),
43+
"dsa": ("DSA", "Distributed Switch Architecture"),
44+
"edac": ("EDAC", "Error Detection and Correction"),
45+
"espi": ("eSPI", "Enhanced Serial Peripheral Interface"),
46+
"ethernet": "Ethernet",
47+
"firmware": "Firmware",
48+
"flash_controller": "Flash controller",
49+
"fpga": ("FPGA", "Field Programmable Gate Array"),
50+
"fs": "File system",
51+
"fuel-gauge": "Fuel gauge",
52+
"gnss": ("GNSS", "Global Navigation Satellite System"),
53+
"gpio": ("GPIO", "General Purpose Input/Output"),
54+
"haptics": "Haptics",
55+
"hda": ("HDA", "High Definition Audio"),
56+
"hdlc_rcp_if": "IEEE 802.15.4 HDLC RCP interface",
57+
"hwinfo": "Hardware information",
58+
"hwspinlock": "Hardware spinlock",
59+
"i2c": ("I2C", "Inter-Integrated Circuit"),
60+
"i2s": ("I2S", "Inter-IC Sound"),
61+
"i3c": ("I3C", "Improved Inter-Integrated Circuit"),
62+
"ieee802154": "IEEE 802.15.4",
63+
"iio": ("IIO", "Industrial I/O"),
64+
"input": "Input",
65+
"interrupt-controller": "Interrupt controller",
66+
"ipc": ("IPC", "Inter-Processor Communication"),
67+
"ipm": ("IPM", "Inter-Processor Mailbox"),
68+
"kscan": "Keyscan",
69+
"led": ("LED", "Light Emitting Diode"),
70+
"led_strip": ("LED", "Light Emitting Diode"),
71+
"lora": "LoRa",
72+
"mbox": "Mailbox",
73+
"mdio": ("MDIO", "Management Data Input/Output"),
74+
"memory-controllers": "Memory controller",
75+
"memory-window": "Memory window",
76+
"mfd": ("MFD", "Multi-Function Device"),
77+
"mhu": ("MHU", "Mailbox Handling Unit"),
78+
"net": "Networking",
79+
"mipi-dbi": ("MIPI DBI", "Mobile Industry Processor Interface Display Bus Interface"),
80+
"mipi-dsi": ("MIPI DSI", "Mobile Industry Processor Interface Display Serial Interface"),
81+
"misc": "Miscellaneous",
82+
"mm": "Memory management",
83+
"mmc": ("MMC", "MultiMediaCard"),
84+
"mmu_mpu": ("MMU / MPU", "Memory Management Unit / Memory Protection Unit"),
85+
"modem": "Modem",
86+
"mspi": "Multi-bit SPI",
87+
"mtd": ("MTD", "Memory Technology Device"),
88+
"wireless": "Wireless network",
89+
"options": "Options",
90+
"ospi": "Octal SPI",
91+
"pcie": ("PCIe", "Peripheral Component Interconnect Express"),
92+
"peci": ("PECI", "Platform Environment Control Interface"),
93+
"phy": "PHY",
94+
"pinctrl": "Pin control",
95+
"pm_cpu_ops": "Power management CPU operations",
96+
"power": "Power management",
97+
"power-domain": "Power domain",
98+
"ppc": "PPC architecture",
99+
"ps2": ("PS/2", "Personal System/2"),
100+
"pwm": ("PWM", "Pulse Width Modulation"),
101+
"qspi": "Quad SPI",
102+
"regulator": "Regulator",
103+
"reserved-memory": "Reserved memory",
104+
"reset": "Reset controller",
105+
"retained_mem": "Retained memory",
106+
"retention": "Retention",
107+
"riscv": "RISC-V architecture",
108+
"rng": ("RNG", "Random Number Generator"),
109+
"rtc": ("RTC", "Real Time Clock"),
110+
"sd": "SD",
111+
"sdhc": "SDHC",
112+
"sensor": "Sensors",
113+
"serial": "Serial controller",
114+
"shi": ("SHI", "Secure Hardware Interface"),
115+
"sip_svc": ("SIP", "Service in Platform"),
116+
"smbus": ("SMBus", "System Management Bus"),
117+
"sound": "Sound",
118+
"spi": ("SPI", "Serial Peripheral Interface"),
119+
"sram": "SRAM",
120+
"stepper": "Stepper",
121+
"syscon": "System controller",
122+
"tach": "Tachometer",
123+
"tcpc": ("TCPC", "USB Type-C Port Controller"),
124+
"test": "Test",
125+
"timer": "Timer",
126+
"timestamp": "Timestamp",
127+
"usb": "USB",
128+
"usb-c": "USB Type-C",
129+
"uac2": "USB Audio Class 2",
130+
"video": "Video",
131+
"virtualization": "Virtualization",
132+
"w1": "1-Wire",
133+
"watchdog": "Watchdog",
134+
"wifi": "Wi-Fi",
135+
"xen": "Xen",
136+
"xspi": ("XSPI", "Expanded Serial Peripheral Interface"),
137+
# zephyr-keep-sorted-stop
138+
}

doc/_scripts/gen_boards_catalog.py

+61-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,59 @@
2020
logger = logging.getLogger(__name__)
2121

2222

23+
class DeviceTreeUtils:
24+
_compat_description_cache = {}
25+
26+
@classmethod
27+
def get_first_sentence(cls, text):
28+
"""Extract the first sentence from a text block (typically a node description).
29+
30+
Args:
31+
text: The text to extract the first sentence from.
32+
33+
Returns:
34+
The first sentence found in the text, or the entire text if no sentence
35+
boundary is found.
36+
"""
37+
# Split the text into lines
38+
lines = text.splitlines()
39+
40+
# Trim leading and trailing whitespace from each line and ignore completely blank lines
41+
lines = [line.strip() for line in lines]
42+
43+
if not lines:
44+
return ""
45+
46+
# Case 1: Single line followed by blank line(s) or end of text
47+
if len(lines) == 1 or (len(lines) > 1 and lines[1] == ""):
48+
first_line = lines[0]
49+
# Check for the first period
50+
period_index = first_line.find(".")
51+
# If there's a period, return up to the period; otherwise, return the full line
52+
return first_line[: period_index + 1] if period_index != -1 else first_line
53+
54+
# Case 2: Multiple contiguous lines, treat as a block
55+
block = " ".join(lines)
56+
period_index = block.find(".")
57+
# If there's a period, return up to the period; otherwise, return the full block
58+
return block[: period_index + 1] if period_index != -1 else block
59+
60+
@classmethod
61+
def get_cached_description(cls, node):
62+
"""Get the cached description for a devicetree node.
63+
64+
Args:
65+
node: A devicetree node object with matching_compat and description attributes.
66+
67+
Returns:
68+
The cached description for the node's compatible, creating it if needed.
69+
"""
70+
return cls._compat_description_cache.setdefault(
71+
node.matching_compat,
72+
cls.get_first_sentence(node.description)
73+
)
74+
75+
2376
def guess_file_from_patterns(directory, patterns, name, extensions):
2477
for pattern in patterns:
2578
for ext in extensions:
@@ -121,7 +174,10 @@ def run_twister_cmake_only(outdir):
121174
sys.executable,
122175
f"{ZEPHYR_BASE}/scripts/twister",
123176
"-T", "samples/hello_world/",
124-
"--all",
177+
# "--all",
178+
"-p", "wio_terminal",
179+
"-p", "reel_board",
180+
"-p", "native_posix",
125181
"-M",
126182
"--cmake-only",
127183
"-j9",
@@ -219,7 +275,10 @@ def get_catalog(generate_hw_features=False):
219275
for node in okay_nodes:
220276
binding_path = Path(node.binding_path)
221277
binding_type = binding_path.relative_to(ZEPHYR_BASE / "dts/bindings").parts[0]
222-
target_features.setdefault(binding_type, set()).add(node.matching_compat)
278+
description = DeviceTreeUtils.get_cached_description(node)
279+
target_features.setdefault(binding_type, {}).setdefault(
280+
node.matching_compat, description
281+
)
223282

224283

225284
# for now we do the union of all supported features for all of board's targets but

doc/_static/css/custom.css

+10
Original file line numberDiff line numberDiff line change
@@ -1190,3 +1190,13 @@ li>a.code-sample-link.reference.internal.current {
11901190
text-overflow: ellipsis;
11911191
max-width:80%;
11921192
}
1193+
1194+
/* TEMPORARY */
1195+
#supported-features>div>table>tbody>tr>td>p {
1196+
white-space: normal;
1197+
}
1198+
1199+
#supported-features td {
1200+
background-color: var(--table-row-odd-background-color);
1201+
border-left-width: 1px;
1202+
}

0 commit comments

Comments
 (0)