diff --git a/boards/index.rst b/boards/index.rst index 0643a355e59..9b83276abbf 100644 --- a/boards/index.rst +++ b/boards/index.rst @@ -24,6 +24,10 @@ this page `. single field, selecting multiple options (such as two architectures) will show boards matching **either** option. + * The list of supported hardware features for each board is automatically generated using + information from the Devicetree. It may not be reflecting the full list of supported features + since some of them may not be enabled by default. + * Can't find your exact board? Don't worry! If a similar board with the same or a closely related MCU exists, you can use it as a :ref:`starting point ` for adding support for your own board. diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index bcabb520a79..83b45a3a2a7 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -16,6 +16,7 @@ set(SPHINXOPTS "-j auto -W --keep-going -T" CACHE STRING "Default Sphinx Options set(SPHINXOPTS_EXTRA "" CACHE STRING "Extra Sphinx Options (added to defaults)") set(LATEXMKOPTS "-halt-on-error -no-shell-escape" CACHE STRING "Default latexmk options") set(DT_TURBO_MODE OFF CACHE BOOL "Enable DT turbo mode") +set(HW_FEATURES_TURBO_MODE OFF CACHE BOOL "Enable HW features turbo mode") set(DOC_TAG "development" CACHE STRING "Documentation tag") set(DTS_ROOTS "${ZEPHYR_BASE}" CACHE STRING "DT bindings root folders") @@ -149,6 +150,16 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${GEN_DEVICETREE_ #------------------------------------------------------------------------------- # html +set(SPHINX_TAGS "${DOC_TAG}") +if(HW_FEATURES_TURBO_MODE) + list(APPEND SPHINX_TAGS "hw_features_turbo") +endif() + +set(SPHINX_TAGS_ARGS "") +foreach(tag ${SPHINX_TAGS}) + list(APPEND SPHINX_TAGS_ARGS "-t" "${tag}") +endforeach() + add_doc_target( html COMMAND ${CMAKE_COMMAND} -E env ${SPHINX_ENV} OUTPUT_DIR=${DOCS_HTML_DIR} @@ -157,7 +168,7 @@ add_doc_target( -c ${DOCS_CFG_DIR} -d ${DOCS_DOCTREE_DIR} -w ${DOCS_BUILD_DIR}/html.log - -t ${DOC_TAG} + ${SPHINX_TAGS_ARGS} ${SPHINXOPTS} ${SPHINXOPTS_EXTRA} ${DOCS_SRC_DIR} @@ -187,7 +198,7 @@ add_doc_target( -c ${DOCS_CFG_DIR} -d ${DOCS_DOCTREE_DIR} -w ${DOCS_BUILD_DIR}/html.log - -t ${DOC_TAG} + ${SPHINX_TAGS_ARGS} ${SPHINXOPTS} ${SPHINXOPTS_EXTRA} ${DOCS_SRC_DIR} @@ -214,7 +225,7 @@ add_doc_target( -c ${DOCS_CFG_DIR} -d ${DOCS_DOCTREE_DIR} -w ${DOCS_BUILD_DIR}/latex.log - -t ${DOC_TAG} + ${SPHINX_TAGS_ARGS} -t convertimages ${SPHINXOPTS} ${SPHINXOPTS_EXTRA} @@ -266,7 +277,7 @@ add_doc_target( -c ${DOCS_CFG_DIR} -d ${DOCS_DOCTREE_DIR} -w ${DOCS_BUILD_DIR}/linkcheck.log - -t ${DOC_TAG} + ${SPHINX_TAGS_ARGS} ${SPHINXOPTS} ${SPHINXOPTS_EXTRA} ${DOCS_SRC_DIR} diff --git a/doc/Makefile b/doc/Makefile index e1dd82d59cb..4ce22f280f4 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -8,6 +8,7 @@ SPHINXOPTS ?= -j auto -W --keep-going -T SPHINXOPTS_EXTRA ?= LATEXMKOPTS ?= -halt-on-error -no-shell-escape DT_TURBO_MODE ?= 0 +HW_FEATURES_TURBO_MODE ?= 0 # ------------------------------------------------------------------------------ # Documentation targets @@ -15,10 +16,10 @@ DT_TURBO_MODE ?= 0 .PHONY: configure clean html html-fast html-live html-live-fast latex pdf doxygen html-fast: - ${MAKE} html DT_TURBO_MODE=1 + ${MAKE} html DT_TURBO_MODE=1 HW_FEATURES_TURBO_MODE=1 html-live-fast: - ${MAKE} html-live DT_TURBO_MODE=1 + ${MAKE} html-live DT_TURBO_MODE=1 HW_FEATURES_TURBO_MODE=1 html html-live latex pdf linkcheck doxygen: configure cmake --build ${BUILDDIR} --target $@ @@ -32,7 +33,8 @@ configure: -DSPHINXOPTS="${SPHINXOPTS}" \ -DSPHINXOPTS_EXTRA="${SPHINXOPTS_EXTRA}" \ -DLATEXMKOPTS="${LATEXMKOPTS}" \ - -DDT_TURBO_MODE=${DT_TURBO_MODE} + -DDT_TURBO_MODE=${DT_TURBO_MODE} \ + -DHW_FEATURES_TURBO_MODE=${HW_FEATURES_TURBO_MODE} clean: cmake --build ${BUILDDIR} --target clean diff --git a/doc/_extensions/zephyr/domain/__init__.py b/doc/_extensions/zephyr/domain/__init__.py index 5cdb7213ca9..bc8957de01e 100644 --- a/doc/_extensions/zephyr/domain/__init__.py +++ b/doc/_extensions/zephyr/domain/__init__.py @@ -2,7 +2,7 @@ Zephyr Extension ################ -Copyright (c) 2023 The Linux Foundation +Copyright (c) 2023-2025 The Linux Foundation SPDX-License-Identifier: Apache-2.0 This extension adds a new ``zephyr`` domain for handling the documentation of various entities @@ -708,6 +708,7 @@ class BoardCatalogDirective(SphinxDirective): "boards": domain_data["boards"], "vendors": domain_data["vendors"], "socs": domain_data["socs"], + "hw_features_present": self.env.app.config.zephyr_generate_hw_features, }, ) return [nodes.raw("", rendered, format="html")] @@ -954,7 +955,7 @@ def install_static_assets_as_needed( def load_board_catalog_into_domain(app: Sphinx) -> None: - board_catalog = get_catalog() + board_catalog = get_catalog(generate_hw_features=app.config.zephyr_generate_hw_features) app.env.domaindata["zephyr"]["boards"] = board_catalog["boards"] app.env.domaindata["zephyr"]["vendors"] = board_catalog["vendors"] app.env.domaindata["zephyr"]["socs"] = board_catalog["socs"] @@ -962,6 +963,7 @@ def load_board_catalog_into_domain(app: Sphinx) -> None: def setup(app): app.add_config_value("zephyr_breathe_insert_related_samples", False, "env") + app.add_config_value("zephyr_generate_hw_features", False, "env") app.add_domain(ZephyrDomain) diff --git a/doc/_extensions/zephyr/domain/static/css/board-catalog.css b/doc/_extensions/zephyr/domain/static/css/board-catalog.css index 9e5e33f7e7f..36768a1be4e 100644 --- a/doc/_extensions/zephyr/domain/static/css/board-catalog.css +++ b/doc/_extensions/zephyr/domain/static/css/board-catalog.css @@ -1,5 +1,5 @@ /** - * Copyright (c) 2024, The Linux Foundation. + * Copyright (c) 2024-2025, The Linux Foundation. * SPDX-License-Identifier: Apache-2.0 */ @@ -82,6 +82,49 @@ white-space: nowrap; } +.tag-container { + display: flex; + flex-wrap: wrap; + border: 1px solid #ccc; + border-radius: 50px; + padding: 5px 18px; +} + +.tag-container:focus-within { + border-color: var(--input-focus-border-color); +} + +.tag { + background-color: var(--admonition-note-background-color); + color: var(--admonition-note-color); + padding: 2px 12px 4px 16px; + border-radius: 30px; + display: inline-flex; + align-items: center; + cursor: pointer; + font-size: 14px; + margin-right: 8px; +} + +.tag:hover { + background-color: #0056b3; +} + +.tag::after { + content: '\00D7'; /* multiplication sign */ + margin-left: 8px; + font-size: 12px; + cursor: pointer; +} + +.filter-form input.tag-input { + flex: 1; + border: none; + padding: 5px; + outline: none; + background-color: transparent; +} + #catalog { display: flex; flex-wrap: wrap; diff --git a/doc/_extensions/zephyr/domain/static/js/board-catalog.js b/doc/_extensions/zephyr/domain/static/js/board-catalog.js index 6ac7ae946b8..0e8c2767c7f 100644 --- a/doc/_extensions/zephyr/domain/static/js/board-catalog.js +++ b/doc/_extensions/zephyr/domain/static/js/board-catalog.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2024, The Linux Foundation. + * Copyright (c) 2024-2025, The Linux Foundation. * SPDX-License-Identifier: Apache-2.0 */ @@ -29,6 +29,29 @@ function populateFormFromURL() { } }); + // Restore supported features from URL + if (hashParams.has("features")) { + const features = hashParams.get("features").split(","); + setTimeout(() => { + features.forEach(feature => { + const tagContainer = document.getElementById('tag-container'); + const tagInput = document.getElementById('tag-input'); + + const tagElement = document.createElement('span'); + tagElement.classList.add('tag'); + tagElement.textContent = feature; + tagElement.onclick = () => { + const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent); + selectedTags.splice(selectedTags.indexOf(feature), 1); + tagElement.remove(); + filterBoards(); + }; + tagContainer.insertBefore(tagElement, tagInput); + }); + filterBoards(); + }, 0); + } + filterBoards(); } @@ -47,6 +70,10 @@ function updateURL() { } }); + // Add supported features to URL + const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent); + selectedTags.length ? hashParams.set("features", selectedTags.join(",")) : hashParams.delete("features"); + window.history.replaceState({}, "", `#${hashParams.toString()}`); } @@ -84,6 +111,81 @@ function fillSocSocSelect(families, series = undefined, selectOnFill = false) { }); } +function setupHWCapabilitiesField() { + let selectedTags = []; + + const tagContainer = document.getElementById('tag-container'); + const tagInput = document.getElementById('tag-input'); + const datalist = document.getElementById('tag-list'); + + const tagCounts = Array.from(document.querySelectorAll('.board-card')).reduce((acc, board) => { + board.getAttribute('data-supported-features').split(' ').forEach(tag => { + acc[tag] = (acc[tag] || 0) + 1; + }); + return acc; + }, {}); + + const allTags = Object.keys(tagCounts).sort(); + + function addTag(tag) { + if (selectedTags.includes(tag) || tag === "" || !allTags.includes(tag)) return; + selectedTags.push(tag); + + const tagElement = document.createElement('span'); + tagElement.classList.add('tag'); + tagElement.textContent = tag; + tagElement.onclick = () => removeTag(tag); + tagContainer.insertBefore(tagElement, tagInput); + + tagInput.value = ''; + updateDatalist(); + } + + function removeTag(tag) { + selectedTags = selectedTags.filter(t => t !== tag); + document.querySelectorAll('.tag').forEach(el => { + if (el.textContent.includes(tag)) el.remove(); + }); + updateDatalist(); + } + + function updateDatalist() { + datalist.innerHTML = ''; + const filteredTags = allTags.filter(tag => !selectedTags.includes(tag)); + + filteredTags.forEach(tag => { + const option = document.createElement('option'); + option.value = tag; + datalist.appendChild(option); + }); + + filterBoards(); + } + + tagInput.addEventListener('input', () => { + if (allTags.includes(tagInput.value)) { + addTag(tagInput.value); + } + }); + + // Add tag when pressing the Enter key + tagInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && allTags.includes(tagInput.value)) { + addTag(tagInput.value); + e.preventDefault(); + } + }); + + // Delete tag when pressing the Backspace key + tagInput.addEventListener('keydown', (e) => { + if (e.key === 'Backspace' && tagInput.value === '' && selectedTags.length > 0) { + removeTag(selectedTags[selectedTags.length - 1]); + } + }); + + updateDatalist(); +} + document.addEventListener("DOMContentLoaded", function () { const form = document.querySelector(".filter-form"); @@ -101,9 +203,10 @@ document.addEventListener("DOMContentLoaded", function () { fillSocFamilySelect(); fillSocSeriesSelect(); fillSocSocSelect(); - populateFormFromURL(); + setupHWCapabilitiesField(); + socFamilySelect = document.getElementById("family"); socFamilySelect.addEventListener("change", () => { const selectedFamilies = [...socFamilySelect.selectedOptions].map(({ value }) => value); @@ -142,6 +245,11 @@ function resetForm() { fillSocFamilySelect(); fillSocSeriesSelect(); fillSocSocSelect(); + + // Clear supported features + document.querySelectorAll('.tag').forEach(tag => tag.remove()); + document.getElementById('tag-input').value = ''; + filterBoards(); } @@ -160,8 +268,10 @@ function filterBoards() { const vendorSelect = document.getElementById("vendor").value; const socSocSelect = document.getElementById("soc"); + const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent); + const resetFiltersBtn = document.getElementById("reset-filters"); - if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length) { + if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length || selectedTags.length) { resetFiltersBtn.classList.remove("btn-disabled"); } else { resetFiltersBtn.classList.add("btn-disabled"); @@ -174,6 +284,7 @@ function filterBoards() { const boardArchs = board.getAttribute("data-arch").split(" "); const boardVendor = board.getAttribute("data-vendor"); const boardSocs = board.getAttribute("data-socs").split(" "); + const boardSupportedFeatures = board.getAttribute("data-supported-features").split(" "); let matches = true; @@ -183,7 +294,8 @@ function filterBoards() { !(nameInput && !boardName.includes(nameInput)) && !(archSelect && !boardArchs.includes(archSelect)) && !(vendorSelect && boardVendor !== vendorSelect) && - (selectedSocs.length === 0 || selectedSocs.some((soc) => boardSocs.includes(soc))); + (selectedSocs.length === 0 || selectedSocs.some((soc) => boardSocs.includes(soc))) && + (selectedTags.length === 0 || selectedTags.every((tag) => boardSupportedFeatures.includes(tag))); board.classList.toggle("hidden", !matches); }); diff --git a/doc/_extensions/zephyr/domain/templates/board-card.html b/doc/_extensions/zephyr/domain/templates/board-card.html index 4c16a3c0cc5..efc83cdcdab 100644 --- a/doc/_extensions/zephyr/domain/templates/board-card.html +++ b/doc/_extensions/zephyr/domain/templates/board-card.html @@ -1,5 +1,5 @@ {# - Copyright (c) 2024, The Linux Foundation. + Copyright (c) 2024-2025, The Linux Foundation. SPDX-License-Identifier: Apache-2.0 #} @@ -15,7 +15,7 @@ data-arch="{{ board.archs | join(" ") }}" data-vendor="{{ board.vendor }}" data-socs="{{ board.socs | join(" ") }}" - tabindex="0"> + data-supported-features="{{ board.supported_features | join(" ") }}" tabindex="0">
{{ vendors[board.vendor] }}
{% if board.image -%} A picture of the {{ board.full_name }} board +
+ +
+ + +
+
+
diff --git a/doc/_scripts/gen_boards_catalog.py b/doc/_scripts/gen_boards_catalog.py index db17c67314f..19011ac4114 100644 --- a/doc/_scripts/gen_boards_catalog.py +++ b/doc/_scripts/gen_boards_catalog.py @@ -1,7 +1,11 @@ -# Copyright (c) 2024 The Linux Foundation +# Copyright (c) 2024-2025 The Linux Foundation # SPDX-License-Identifier: Apache-2.0 import logging +import os +import pickle +import subprocess +import sys from collections import namedtuple from pathlib import Path @@ -12,6 +16,8 @@ import zephyr_module from gen_devicetree_rest import VndLookup ZEPHYR_BASE = Path(__file__).parents[2] +ZEPHYR_BINDINGS = ZEPHYR_BASE / "dts/bindings" +EDT_PICKLE_PATH = "zephyr/edt.pickle" logger = logging.getLogger(__name__) @@ -38,6 +44,7 @@ def guess_image(board_or_shield): return (img_file.relative_to(ZEPHYR_BASE)).as_posix() if img_file else None + def guess_doc_page(board_or_shield): patterns = [ "doc/index.{ext}", @@ -51,7 +58,92 @@ def guess_doc_page(board_or_shield): return doc_file -def get_catalog(): +def gather_board_devicetrees(twister_out_dir): + """Gather EDT objects for each board from twister output directory. + + Args: + twister_out_dir: Path object pointing to twister output directory + + Returns: + A dictionary mapping board names to a dictionary of board targets and their EDT objects. + The structure is: {board_name: {board_target: edt_object}} + """ + board_devicetrees = {} + + if not twister_out_dir.exists(): + return board_devicetrees + + # Find all build_info.yml files in twister-out + build_info_files = list(twister_out_dir.glob("*/**/build_info.yml")) + + for build_info_file in build_info_files: + # Look for corresponding zephyr.dts + edt_pickle_file = build_info_file.parent / EDT_PICKLE_PATH + if not edt_pickle_file.exists(): + continue + + try: + with open(build_info_file) as f: + build_info = yaml.safe_load(f) + board_info = build_info.get('cmake', {}).get('board', {}) + board_name = board_info.get('name') + qualifier = board_info.get('qualifiers', '') + revision = board_info.get('revision', '') + + board_target = board_name + if qualifier: + board_target = f"{board_name}/{qualifier}" + if revision: + board_target = f"{board_target}@{revision}" + + with open(edt_pickle_file, 'rb') as f: + edt = pickle.load(f) + board_devicetrees.setdefault(board_name, {})[board_target] = edt + + except Exception as e: + logger.error(f"Error processing build info file {build_info_file}: {e}") + + return board_devicetrees + + +def run_twister_cmake_only(outdir): + """Run twister in cmake-only mode to generate build info files. + + Args: + outdir: Directory where twister should output its files + """ + twister_cmd = [ + sys.executable, + f"{ZEPHYR_BASE}/scripts/twister", + "-T", "samples/hello_world/", + "--all", + "-M", + "--keep-artifacts", "zephyr/edt.pickle", + "--cmake-only", + "--outdir", str(outdir), + ] + + minimal_env = { + 'PATH': os.environ.get('PATH', ''), + 'ZEPHYR_BASE': str(ZEPHYR_BASE), + 'HOME': os.environ.get('HOME', ''), + 'PYTHONPATH': os.environ.get('PYTHONPATH', '') + } + + try: + subprocess.run(twister_cmd, check=True, cwd=ZEPHYR_BASE, env=minimal_env) + except subprocess.CalledProcessError as e: + logger.warning(f"Failed to run Twister, list of hw features might be incomplete.\n{e}") + + +def get_catalog(generate_hw_features=False): + """Get the board catalog. + + Args: + generate_hw_features: If True, run twister to generate hardware features information. + """ + import tempfile + vnd_lookup = VndLookup(ZEPHYR_BASE / "dts/bindings/vendor-prefixes.txt", []) module_settings = { @@ -78,6 +170,15 @@ def get_catalog(): boards = list_boards.find_v2_boards(args_find_boards) systems = list_hardware.find_v2_systems(args_find_boards) board_catalog = {} + board_devicetrees = {} + + if generate_hw_features: + logger.info("Running twister in cmake-only mode to get Devicetree files for all boards") + with tempfile.TemporaryDirectory() as tmp_dir: + run_twister_cmake_only(tmp_dir) + board_devicetrees = gather_board_devicetrees(Path(tmp_dir)) + else: + logger.info("Skipping generation of supported hardware features.") for board in boards.values(): # We could use board.vendor but it is often incorrect. Instead, deduce vendor from @@ -91,6 +192,40 @@ def get_catalog(): vendor = folder.name break + socs = {soc.name for soc in board.socs} + full_name = board.full_name or board.name + 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 + if node.status == "okay" and node.matching_compat is not None + ] + + target_features = {} + for node in okay_nodes: + binding_path = Path(node.binding_path) + binding_type = ( + binding_path.relative_to(ZEPHYR_BINDINGS).parts[0] + if binding_path.is_relative_to(ZEPHYR_BINDINGS) + else "misc" + ) + target_features.setdefault(binding_type, set()).add(node.matching_compat) + + + # 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) + # Grab all the twister files for this board and use them to figure out all the archs it # supports. archs = set() @@ -103,10 +238,6 @@ def get_catalog(): except Exception as e: logger.error(f"Error parsing twister file {twister_file}: {e}") - socs = {soc.name for soc in board.socs} - full_name = board.full_name or board.name - doc_page = guess_doc_page(board) - board_catalog[board.name] = { "name": board.name, "full_name": full_name, @@ -114,6 +245,8 @@ def get_catalog(): "vendor": vendor, "archs": list(archs), "socs": list(socs), + "supported_features": supported_features, + "targets": list(targets), "image": guess_image(board), } diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css index 586450fc649..5671d6a0520 100644 --- a/doc/_static/css/custom.css +++ b/doc/_static/css/custom.css @@ -1,6 +1,7 @@ /** * Copyright (c) 2019-2020, Juan Linietsky, Ariel Manzur and the Godot community * Copyright (c) 2021, Teslabs Engineering S.L. + * Copyright (c) 2023-2025, The Linux Foundation. * SPDX-License-Identifier: CC-BY-3.0 * * Various tweaks to the Read the Docs theme to better conform with Zephyr's diff --git a/doc/conf.py b/doc/conf.py index a9646be01e5..a54fc5f22f1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -325,6 +325,7 @@ external_content_keep = [ # -- Options for zephyr.domain -------------------------------------------- zephyr_breathe_insert_related_samples = True +zephyr_generate_hw_features = not tags.has("hw_features_turbo") # pylint: disable=undefined-variable # noqa: F821 # -- Options for sphinx.ext.graphviz -------------------------------------- diff --git a/doc/contribute/documentation/generation.rst b/doc/contribute/documentation/generation.rst index 7c88ccd1beb..cad9201b113 100644 --- a/doc/contribute/documentation/generation.rst +++ b/doc/contribute/documentation/generation.rst @@ -253,11 +253,19 @@ To enable this mode, set the following option when invoking cmake:: -DDT_TURBO_MODE=1 -or invoke make with the following target:: +Another step that typically takes a long time is the generation of the list of +supported features for each board. This can be disabled by setting the following +option when invoking cmake:: + + -DHW_FEATURES_TURBO_MODE=1 + +Invoking :command:`make` with the following target will build the documentation +without either of the aforementioned features:: cd ~/zephyrproject/zephyr/doc - # To generate HTML output without detailed Kconfig + # To generate HTML output without detailed Devicetree bindings documentation + # and supported features index make html-fast Viewing generated documentation locally