diff --git a/boards/nordic/nrf54h20dk/nrf54h20dk_nrf54h20-memory_map.dtsi b/boards/nordic/nrf54h20dk/nrf54h20dk_nrf54h20-memory_map.dtsi index b6a53955c60..e14426c59fc 100644 --- a/boards/nordic/nrf54h20dk/nrf54h20dk_nrf54h20-memory_map.dtsi +++ b/boards/nordic/nrf54h20dk/nrf54h20dk_nrf54h20-memory_map.dtsi @@ -166,5 +166,13 @@ periphconf_partition: partition@1ae000 { reg = <0x1ae000 DT_SIZE_K(8)>; }; + + secondary_partition: partition@1b0000 { + reg = <0x1b0000 DT_SIZE_K(64)>; + }; + + secondary_periphconf_partition: partition@1c0000 { + reg = <0x1c0000 DT_SIZE_K(8)>; + }; }; }; diff --git a/scripts/ci/check_compliance.py b/scripts/ci/check_compliance.py index b0f4ccebb1e..251cbc97a58 100755 --- a/scripts/ci/check_compliance.py +++ b/scripts/ci/check_compliance.py @@ -1231,6 +1231,9 @@ def check_no_undef_outside_kconfig(self, kconf): "FOO_LOG_LEVEL", "FOO_SETTING_1", "FOO_SETTING_2", + "GEN_UICR_GENERATE_PERIPHCONF", # Used in specialized build tool, not part of main Kconfig + "GEN_UICR_SECONDARY", # Used in specialized build tool, not part of main Kconfig + "GEN_UICR_SECONDARY_GENERATE_PERIPHCONF", # Used in specialized build tool, not part of main Kconfig "HEAP_MEM_POOL_ADD_SIZE_", # Used as an option matching prefix "HUGETLBFS", # Linux, in boards/xtensa/intel_adsp_cavs25/doc "IAR_BUFFERED_WRITE", diff --git a/scripts/west_commands/runners/nrf_common.py b/scripts/west_commands/runners/nrf_common.py index a7401084ef3..8a69c65bb2b 100644 --- a/scripts/west_commands/runners/nrf_common.py +++ b/scripts/west_commands/runners/nrf_common.py @@ -433,26 +433,6 @@ def program_hex(self): core='Application', ) - if self.build_conf.getboolean("CONFIG_NRF_HALTIUM_GENERATE_UICR"): - zephyr_build_dir = Path(self.cfg.build_dir) / 'zephyr' - - self.op_program( - str(zephyr_build_dir / 'uicr.hex'), - 'ERASE_NONE', - None, - defer=True, - core='Application', - ) - - if self.build_conf.getboolean("CONFIG_NRF_HALTIUM_UICR_PERIPHCONF"): - self.op_program( - str(zephyr_build_dir / 'periphconf.hex'), - 'ERASE_NONE', - None, - defer=True, - core='Application', - ) - if not self.erase and regtool_generated_uicr: self.exec_op('erase', core=core, kind='uicr') else: diff --git a/soc/nordic/Kconfig.sysbuild b/soc/nordic/Kconfig.sysbuild index a726d20c464..040a04203bf 100644 --- a/soc/nordic/Kconfig.sysbuild +++ b/soc/nordic/Kconfig.sysbuild @@ -4,5 +4,6 @@ config HAS_NORDIC_VPR_LAUNCHER_IMAGE bool +rsource "common/uicr/Kconfig.sysbuild" rsource "common/vpr/Kconfig.sysbuild" orsource "*/Kconfig.sysbuild" diff --git a/soc/nordic/common/CMakeLists.txt b/soc/nordic/common/CMakeLists.txt index 825be4842fb..04f0c1a3219 100644 --- a/soc/nordic/common/CMakeLists.txt +++ b/soc/nordic/common/CMakeLists.txt @@ -3,9 +3,7 @@ add_subdirectory_ifdef(CONFIG_RISCV_CORE_NORDIC_VPR vpr) -if(CONFIG_NRF_PERIPHCONF_SECTION OR CONFIG_NRF_HALTIUM_GENERATE_UICR) - add_subdirectory(uicr) -endif() +add_subdirectory(uicr) # Let SystemInit() be called in place of soc_reset_hook() by default. zephyr_linker_symbol(SYMBOL soc_reset_hook EXPR "@SystemInit@") diff --git a/soc/nordic/common/uicr/CMakeLists.txt b/soc/nordic/common/uicr/CMakeLists.txt index 0bde6b47f57..d350d8d3f58 100644 --- a/soc/nordic/common/uicr/CMakeLists.txt +++ b/soc/nordic/common/uicr/CMakeLists.txt @@ -5,32 +5,26 @@ if(CONFIG_NRF_PERIPHCONF_SECTION) zephyr_linker_sources(SECTIONS uicr.ld) endif() -if(CONFIG_NRF_HALTIUM_GENERATE_UICR) - if(CONFIG_NRF_PERIPHCONF_SECTION) - set(in_periphconf_elf_arg - --in-periphconf-elf $ - ) - endif() - - if(CONFIG_NRF_HALTIUM_UICR_PERIPHCONF) - set(periphconf_hex_file ${PROJECT_BINARY_DIR}/periphconf.hex) - set(out_periphconf_hex_arg - --out-periphconf-hex ${periphconf_hex_file} - ) - list(APPEND optional_byproducts ${periphconf_hex_file}) - endif() - - set(uicr_hex_file ${PROJECT_BINARY_DIR}/uicr.hex) - set_property(GLOBAL APPEND PROPERTY extra_post_build_commands - COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${ZEPHYR_BASE}/scripts/dts/python-devicetree/src - ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/gen_uicr.py - --in-config ${DOTCONFIG} - --in-edt-pickle ${EDT_PICKLE} - ${in_periphconf_elf_arg} - ${out_periphconf_hex_arg} - --out-uicr-hex ${uicr_hex_file} +if(CONFIG_NRF_PERIPHCONF_GENERATE_ENTRIES) + set(periphconf_entries_c_file ${PROJECT_BINARY_DIR}/periphconf_entries_generated.c) + execute_process( + COMMAND + ${CMAKE_COMMAND} -E env ZEPHYR_BASE=${ZEPHYR_BASE} + ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/gen_periphconf_entries.py + --soc ${CONFIG_SOC} + --in-edt-pickle ${EDT_PICKLE} + --out-periphconf-source ${periphconf_entries_c_file} + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + COMMAND_ERROR_IS_FATAL ANY ) - set_property(GLOBAL APPEND PROPERTY extra_post_build_byproducts - ${uicr_hex_file} ${optional_byproducts} + zephyr_sources(${periphconf_entries_c_file}) + message(STATUS "Generated ${periphconf_entries_c_file} from ${EDT_PICKLE}") +endif() + +if(CONFIG_NRF_PERIPHCONF_SECTION AND NOT SYSBUILD) + message(WARNING "CONFIG_NRF_PERIPHCONF_SECTION is enabled, but Sysbuild is not being used. " + "The global peripheral configuration will not be applied unless artifacts " + "are generated manually/externally. To enable automatic generation, build with " + "Sysbuild and ensure that SB_CONFIG_NRF_HALTIUM_GENERATE_UICR=y." ) endif() diff --git a/soc/nordic/common/uicr/Kconfig b/soc/nordic/common/uicr/Kconfig index f132510a7a5..73b5e5cf2d1 100644 --- a/soc/nordic/common/uicr/Kconfig +++ b/soc/nordic/common/uicr/Kconfig @@ -1,31 +1,20 @@ # Copyright (c) 2025 Nordic Semiconductor ASA # SPDX-License-Identifier: Apache-2.0 -config NRF_HALTIUM_GENERATE_UICR - bool "Generate UICR file" - depends on SOC_NRF54H20_CPUAPP - default y - help - Generate UICR HEX file. - -if NRF_HALTIUM_GENERATE_UICR - -config NRF_HALTIUM_UICR_PERIPHCONF - bool "Initialize global domain peripherals" - default y - help - Generates a blob containing static global domain peripheral initialization - values extracted from the build artifacts, and configures UICR.PERIPHCONF - to point at the blob. The initialization values are then loaded ahead of - ahead of the application boot. - -endif - config NRF_PERIPHCONF_SECTION bool "Populate global peripheral initialization section" - default y if SOC_NRF54H20_CPUAPP + default y if SOC_NRF54H20_CPUAPP || SOC_NRF54H20_CPURAD depends on LINKER_DEVNULL_SUPPORT imply LINKER_DEVNULL_MEMORY help Include static global domain peripheral initialization values from the build in a dedicated section in the devnull region. + +config NRF_PERIPHCONF_GENERATE_ENTRIES + bool "Generate PERIPHCONF entries source file" + default y if SOC_NRF54H20_CPUAPP || SOC_NRF54H20_CPURAD + depends on NRF_PERIPHCONF_SECTION + depends on NRF_PLATFORM_HALTIUM + help + Generate a C file containing PERIPHCONF entries based on the + device configuration in the devicetree. diff --git a/soc/nordic/common/uicr/Kconfig.sysbuild b/soc/nordic/common/uicr/Kconfig.sysbuild new file mode 100644 index 00000000000..eb885beaaaf --- /dev/null +++ b/soc/nordic/common/uicr/Kconfig.sysbuild @@ -0,0 +1,9 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +config NRF_HALTIUM_GENERATE_UICR + bool "Generate UICR file" + depends on SOC_SERIES_NRF54HX + default y + help + Generate UICR HEX file. diff --git a/soc/nordic/common/uicr/gen_periphconf_entries.py b/soc/nordic/common/uicr/gen_periphconf_entries.py new file mode 100644 index 00000000000..e70e56acc1f --- /dev/null +++ b/soc/nordic/common/uicr/gen_periphconf_entries.py @@ -0,0 +1,658 @@ +""" +Copyright (c) 2025 Nordic Semiconductor ASA +SPDX-License-Identifier: Apache-2.0 +""" + +from __future__ import annotations + +import argparse +import enum +import os +import pickle +import sys +from pathlib import Path +from typing import Any + +try: + ZEPHYR_BASE = Path(os.environ["ZEPHYR_BASE"]).resolve() +except KeyError: + sys.exit("Set the environment variable 'ZEPHYR_BASE' to point to the zephyr root directory") + +# Add packages that are located in zephyr itself to the python path so we can import them below +sys.path.insert(0, str(ZEPHYR_BASE / "scripts/dts/python-devicetree/src")) +sys.path.insert(0, str(ZEPHYR_BASE / "soc/nordic/common/uicr")) + +from periphconf.builder import ( + Ctrlsel, + FixedPPIMap, + GpiosProp, + Node, + NrfCompChannel, + NrfFun, + NrfPsel, + NrfSaadcChannel, + PeriphconfBuilder, + ProcessorId, + SocLookupTables, + dt_processor_id, +) + +# These peripherals are special cases that don't fit the general rules we use to generate +# PERIPHCONF entries based on the devicetree. +NODELABEL_TO_KWARGS = { + # Channel links on this node do not result in PPIB connections, because the *-links properties + # are ambiguous. Instead, the PPIB connections are configured by the DPPICs connected to this. + "dppic130": {"add_ppib_channel_links": False}, + # The interrupts for this node are not managed in IRQMAP. + "canpll": {"has_irq_mapping": False}, +} + + +def get_additional_node_kwargs(node: Node) -> dict[str, Any]: + additional_kwargs = {} + for label in node.labels: + additional_kwargs.update(NODELABEL_TO_KWARGS.get(label, {})) + return additional_kwargs + + +class Soc(enum.Enum): + """Names of SoCs supported by this script""" + + NRF54H20 = "nrf54h20" + NRF9280 = "nrf9280" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + allow_abbrev=False, + description=( + "Generate a C source file containing entries for the PERIPHCONF blob based on the " + "device configuration in devicetree." + ), + ) + parser.add_argument( + "-v", + "--verbose", + default=0, + action="count", + help="Print verbose output such as debug information.", + ) + parser.add_argument( + "--soc", + required=True, + choices=[soc.value for soc in Soc], + help=( + "SoC to generate PERIPHCONF macros for. " + "Used to look up soc specific hardware information" + ), + ) + parser.add_argument( + "--in-edt-pickle", + type=argparse.FileType("rb"), + required=True, + help="Path to the pickled edtlib.EDT object with devicetree contents.", + ) + parser.add_argument( + "--out-periphconf-source", + type=argparse.FileType("w", encoding="utf-8"), + required=True, + help="Path to write the generated PERIPHCONF C source file to.", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + dt = pickle.load(args.in_edt_pickle) + processor = dt_processor_id(dt) + lookup_tables = lookup_tables_get(Soc(args.soc)) + builder = PeriphconfBuilder(dt, lookup_tables) + + # Application local peripherals + if processor == ProcessorId.APPLICATION: + for node in dt.label2node["cpuapp_peripherals"].children.values(): + builder.add_local_peripheral_cfg(node, **get_additional_node_kwargs(node)) + + # Radio local peripherals + if processor == ProcessorId.RADIOCORE: + for node in dt.label2node["cpurad_peripherals"].children.values(): + builder.add_local_peripheral_cfg(node, **get_additional_node_kwargs(node)) + + # Global domain peripherals + for node in dt.label2node["global_peripherals"].children.values(): + builder.add_global_peripheral_cfg(node, **get_additional_node_kwargs(node)) + + # Add pins referenced by 'gpios' properties on non-peripheral nodes, for example + # buttons and leds. We only add SPU configurations for these and not CTRLSEL, + # to avoid false CTRLSEL conflicts for things like PWM leds. + for node in dt.nodes: + builder.add_gpio_spu_permissions(node) + + script_name = Path(__file__).resolve().relative_to(ZEPHYR_BASE) + generated_by = f"Generated by {script_name}" + + generated_source = builder.build_generated_source(generated_by) + args.out_periphconf_source.write(generated_source) + + +def lookup_tables_get(soc: Soc) -> SocLookupTables: + if soc == Soc.NRF54H20: + ctrlsel_lookup = { + # CAN120 + 0x5F8D_8000: { + # P2 + NrfPsel(fun=NrfFun.CAN_TX, port=2, pin=9): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.CAN_RX, port=2, pin=8): Ctrlsel.CAN_PWM_I3C, + # P9 + NrfPsel(fun=NrfFun.CAN_TX, port=9, pin=5): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.CAN_RX, port=9, pin=4): Ctrlsel.CAN, + }, + # PWM120 + 0x5F8E_4000: { + # P2 + NrfPsel(fun=NrfFun.PWM_OUT0, port=2, pin=4): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT1, port=2, pin=5): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT2, port=2, pin=6): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT3, port=2, pin=7): Ctrlsel.CAN_PWM_I3C, + # P6 + NrfPsel(fun=NrfFun.PWM_OUT0, port=6, pin=6): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT1, port=6, pin=7): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT2, port=6, pin=8): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT3, port=6, pin=9): Ctrlsel.CAN_PWM_I3C, + # P7 + NrfPsel(fun=NrfFun.PWM_OUT0, port=7, pin=0): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT1, port=7, pin=1): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT2, port=7, pin=6): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT3, port=7, pin=7): Ctrlsel.CAN_PWM_I3C, + }, + # PWM130 + 0x5F9A_4000: { + # P9 + NrfPsel(fun=NrfFun.PWM_OUT0, port=9, pin=2): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT1, port=9, pin=3): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT2, port=9, pin=4): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT3, port=9, pin=5): Ctrlsel.CAN_PWM_I3C, + }, + # SPIM130/SPIS130/TWIM130/TWIS130/UARTE130 + 0x5F9A_5000: { + # SPIM mappings + NrfPsel(fun=NrfFun.SPIM_MOSI, port=9, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=9, pin=2): Ctrlsel.SERIAL0, + GpiosProp(name="cs-gpios", port=9, pin=3): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=9, pin=4): Ctrlsel.SERIAL0, + # SPIS mappings + NrfPsel(fun=NrfFun.SPIS_MISO, port=9, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIS_MOSI, port=9, pin=2): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIS_CSN, port=9, pin=3): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIS_SCK, port=9, pin=4): Ctrlsel.SERIAL0, + # TWIM mappings + NrfPsel(fun=NrfFun.TWIM_SDA, port=9, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.TWIM_SCL, port=9, pin=4): Ctrlsel.SERIAL0, + # TWIS mappings + NrfPsel(fun=NrfFun.TWIS_SDA, port=9, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.TWIS_SCL, port=9, pin=4): Ctrlsel.SERIAL0, + # UARTÈ mappings + NrfPsel(fun=NrfFun.UART_TX, port=9, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RX, port=9, pin=4): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.UART_CTS, port=9, pin=2): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RTS, port=9, pin=3): Ctrlsel.SERIAL0, + }, + # SPIM131/SPIS131/TWIM131/TWIS131/UARTE131 + 0x5F9A_6000: { + # SPIM mappings + NrfPsel(fun=NrfFun.SPIM_MOSI, port=9, pin=0): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.SPIM_MISO, port=9, pin=2): Ctrlsel.CAN_TDM_SERIAL2, + GpiosProp(name="cs-gpios", port=9, pin=3): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.SPIM_SCK, port=9, pin=1): Ctrlsel.CAN_TDM_SERIAL2, + # SPIS mappings + NrfPsel(fun=NrfFun.SPIS_MISO, port=9, pin=0): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.SPIS_MOSI, port=9, pin=2): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.SPIS_CSN, port=9, pin=3): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.SPIS_SCK, port=9, pin=1): Ctrlsel.CAN_TDM_SERIAL2, + # TWIM mappings + NrfPsel(fun=NrfFun.TWIM_SDA, port=9, pin=0): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TWIM_SCL, port=9, pin=1): Ctrlsel.CAN_TDM_SERIAL2, + # TWIS mappings + NrfPsel(fun=NrfFun.TWIS_SDA, port=9, pin=0): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TWIS_SCL, port=9, pin=1): Ctrlsel.CAN_TDM_SERIAL2, + # UARTÈ mappings + NrfPsel(fun=NrfFun.UART_TX, port=9, pin=0): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.UART_RX, port=9, pin=1): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_CTS, port=9, pin=2): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.UART_RTS, port=9, pin=3): Ctrlsel.CAN_TDM_SERIAL2, + }, + # VPR121 (FLPR) + 0x5F8D_4000: { + # P1 + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=8): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=9): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=10): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=11): Ctrlsel.VPR_GRC, + # P2 + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=0): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=1): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=2): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=3): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=4): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=5): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=6): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=7): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=8): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=9): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=10): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=11): Ctrlsel.VPR_GRC, + # P6 + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=0): Ctrlsel.VPR_GRC, + # (pin 1-2 are not connected with VIO) + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=3): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=4): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=5): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=6): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=7): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=8): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=9): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=10): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=11): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=12): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=6, pin=13): Ctrlsel.VPR_GRC, + # P7 + NrfPsel(fun=NrfFun.IGNORE, port=7, pin=0): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=7, pin=1): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=7, pin=2): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=7, pin=3): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=7, pin=4): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=7, pin=5): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=7, pin=6): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=7, pin=7): Ctrlsel.VPR_GRC, + # P9 + NrfPsel(fun=NrfFun.IGNORE, port=9, pin=0): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=9, pin=1): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=9, pin=2): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=9, pin=3): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=9, pin=4): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=9, pin=5): Ctrlsel.VPR_GRC, + }, + # SPIS120 + 0x5F8E_5000: { + NrfPsel(fun=NrfFun.SPIS_MISO, port=6, pin=3): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIS_MOSI, port=6, pin=4): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIS_CSN, port=6, pin=9): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIS_SCK, port=6, pin=0): Ctrlsel.SERIAL0, + }, + # SPIM120/UARTE120 + 0x5F8E_6000: { + # SPIM P6 mappings + NrfPsel(fun=NrfFun.SPIM_MOSI, port=6, pin=8): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=6, pin=7): Ctrlsel.SERIAL0, + GpiosProp(name="cs-gpios", port=6, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=6, pin=1): Ctrlsel.SERIAL0, + # SPIM P7 mappings + NrfPsel(fun=NrfFun.SPIM_MOSI, port=7, pin=7): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=7, pin=6): Ctrlsel.SERIAL0, + GpiosProp(name="cs-gpios", port=7, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=7, pin=3): Ctrlsel.SERIAL0, + # SPIM P2 mappings + NrfPsel(fun=NrfFun.SPIM_MOSI, port=2, pin=6): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=2, pin=5): Ctrlsel.SERIAL0, + GpiosProp(name="cs-gpios", port=2, pin=7): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=2, pin=3): Ctrlsel.SERIAL0, + # UARTÈ P6 mappings + NrfPsel(fun=NrfFun.UART_TX, port=6, pin=8): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_CTS, port=6, pin=7): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RX, port=6, pin=6): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RTS, port=6, pin=5): Ctrlsel.SERIAL0, + # UARTÈ P7 mappings + NrfPsel(fun=NrfFun.UART_TX, port=7, pin=7): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_CTS, port=7, pin=6): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RX, port=7, pin=4): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RTS, port=7, pin=5): Ctrlsel.SERIAL0, + # UARTÈ P2 mappings + NrfPsel(fun=NrfFun.UART_TX, port=2, pin=6): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_CTS, port=2, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RX, port=2, pin=4): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RTS, port=2, pin=7): Ctrlsel.SERIAL0, + }, + # SPIM121 + 0x5F8E_7000: { + # SPIM P6 mappings + NrfPsel(fun=NrfFun.SPIM_MOSI, port=6, pin=13): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=6, pin=12): Ctrlsel.SERIAL0, + GpiosProp(name="cs-gpios", port=6, pin=10): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=6, pin=2): Ctrlsel.SERIAL0, + # SPIM P7 mappings + NrfPsel(fun=NrfFun.SPIM_MOSI, port=7, pin=1): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.SPIS_MISO, port=7, pin=1): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.SPIM_MISO, port=7, pin=0): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.SPIS_MOSI, port=7, pin=0): Ctrlsel.EXMIF_RADIO_SERIAL1, + GpiosProp(name="cs-gpios", port=7, pin=4): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.SPIS_CSN, port=7, pin=4): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.SPIM_SCK, port=7, pin=2): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.SPIS_SCK, port=7, pin=2): Ctrlsel.EXMIF_RADIO_SERIAL1, + # SPIM P2 mappings + NrfPsel(fun=NrfFun.SPIM_MOSI, port=2, pin=11): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=2, pin=10): Ctrlsel.SERIAL0, + GpiosProp(name="cs-gpios", port=2, pin=8): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=2, pin=2): Ctrlsel.SERIAL0, + }, + # EXMIF + 0x5F09_5000: { + NrfPsel(fun=NrfFun.EXMIF_CK, port=6, pin=0): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_RWDS, port=6, pin=2): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_CS0, port=6, pin=3): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_DQ7, port=6, pin=4): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_DQ1, port=6, pin=5): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_DQ6, port=6, pin=6): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_DQ0, port=6, pin=7): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_DQ5, port=6, pin=8): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_DQ3, port=6, pin=9): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_DQ2, port=6, pin=10): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_DQ4, port=6, pin=11): Ctrlsel.EXMIF_RADIO_SERIAL1, + NrfPsel(fun=NrfFun.EXMIF_CS1, port=6, pin=13): Ctrlsel.EXMIF_RADIO_SERIAL1, + }, + # VPR130 (PPR) + 0x5F90_8000: { + # P0 + NrfPsel(fun=NrfFun.IGNORE, port=0, pin=4): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=0, pin=5): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=0, pin=6): Ctrlsel.VPR_GRC, + NrfPsel(fun=NrfFun.IGNORE, port=0, pin=7): Ctrlsel.VPR_GRC, + }, + # TDM130 + 0x5F99_2000: { + # TDM P1 mappings + NrfPsel(fun=NrfFun.TDM_MCK, port=1, pin=2): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SCK_M, port=1, pin=3): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SCK_S, port=1, pin=3): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SDOUT, port=1, pin=4): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SDIN, port=1, pin=5): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_FSYNC_M, port=1, pin=6): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_FSYNC_S, port=1, pin=6): Ctrlsel.CAN_TDM_SERIAL2, + # TDM P2 mappings + NrfPsel(fun=NrfFun.TDM_MCK, port=2, pin=0): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SCK_M, port=2, pin=1): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SCK_S, port=2, pin=1): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SDOUT, port=2, pin=9): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SDIN, port=2, pin=10): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_FSYNC_M, port=2, pin=11): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_FSYNC_S, port=2, pin=11): Ctrlsel.CAN_TDM_SERIAL2, + }, + # TDM131 + 0x5F99_7000: { + # TDM P1 mappings + NrfPsel(fun=NrfFun.TDM_MCK, port=1, pin=0): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SCK_M, port=1, pin=1): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SCK_S, port=1, pin=1): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SDOUT, port=1, pin=9): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SDIN, port=1, pin=10): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_FSYNC_M, port=1, pin=11): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_FSYNC_S, port=1, pin=11): Ctrlsel.CAN_TDM_SERIAL2, + # TDM P2 mappings + NrfPsel(fun=NrfFun.TDM_MCK, port=2, pin=2): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SCK_M, port=2, pin=3): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SCK_S, port=2, pin=3): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SDOUT, port=2, pin=4): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_SDIN, port=2, pin=6): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_FSYNC_M, port=2, pin=7): Ctrlsel.CAN_TDM_SERIAL2, + NrfPsel(fun=NrfFun.TDM_FSYNC_S, port=2, pin=7): Ctrlsel.CAN_TDM_SERIAL2, + }, + # GPIOTE0 (RAD) + 0x5302_7000: { + # P1 + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=4): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=5): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=6): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=7): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=8): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=9): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=10): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=1, pin=11): Ctrlsel.CAN, + # P2 + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=0): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=1): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=2): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=3): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=4): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=5): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=6): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=7): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=8): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=9): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=10): Ctrlsel.CAN, + NrfPsel(fun=NrfFun.IGNORE, port=2, pin=11): Ctrlsel.CAN, + }, + } + elif soc == Soc.NRF9280: + ctrlsel_lookup = { + # PWM120 + 0x5F8E_4000: { + # P2 + NrfPsel(fun=NrfFun.PWM_OUT0, port=2, pin=0): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT1, port=2, pin=1): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT2, port=2, pin=2): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT3, port=2, pin=3): Ctrlsel.CAN_PWM_I3C, + # P6 + NrfPsel(fun=NrfFun.PWM_OUT0, port=6, pin=0): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT0, port=6, pin=6): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT1, port=6, pin=1): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT1, port=6, pin=7): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT2, port=6, pin=2): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT2, port=6, pin=8): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT3, port=6, pin=3): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT3, port=6, pin=9): Ctrlsel.CAN_PWM_I3C, + }, + # SPIM120/UARTE120 + 0x5F8E_6000: { + # SPIM P2 mappings + GpiosProp(name="cs-gpios", port=2, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=2, pin=3): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MOSI, port=2, pin=4): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=2, pin=0): Ctrlsel.SERIAL0, + # SPIM P6 mappings + GpiosProp(name="cs-gpios", port=6, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=6, pin=7): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MOSI, port=6, pin=8): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=6, pin=1): Ctrlsel.SERIAL0, + # UARTE P2 mappings + NrfPsel(fun=NrfFun.UART_CTS, port=2, pin=3): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RTS, port=2, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RX, port=2, pin=2): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_TX, port=2, pin=4): Ctrlsel.SERIAL0, + # UARTE P6 mappings + NrfPsel(fun=NrfFun.UART_CTS, port=6, pin=7): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RTS, port=6, pin=5): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RX, port=6, pin=6): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_TX, port=6, pin=8): Ctrlsel.SERIAL0, + }, + # SPIM121 + 0x5F8E_7000: { + # P2 + GpiosProp(name="cs-gpios", port=2, pin=6): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=2, pin=8): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MOSI, port=2, pin=9): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=2, pin=1): Ctrlsel.SERIAL0, + # P6 + GpiosProp(name="cs-gpios", port=6, pin=10): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=6, pin=12): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MOSI, port=6, pin=13): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=6, pin=2): Ctrlsel.SERIAL0, + }, + # PWM130 + 0x5F9A_4000: { + NrfPsel(fun=NrfFun.PWM_OUT0, port=9, pin=2): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT1, port=9, pin=3): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT2, port=9, pin=4): Ctrlsel.CAN_PWM_I3C, + NrfPsel(fun=NrfFun.PWM_OUT3, port=9, pin=5): Ctrlsel.CAN_PWM_I3C, + }, + # SPIM130/SPIS130/TWIM130/TWIS130/UARTE130 + 0x5F9A_5000: { + # SPIM mappings + GpiosProp(name="cs-gpios", port=9, pin=1): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MISO, port=9, pin=2): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_MOSI, port=9, pin=3): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIM_SCK, port=9, pin=0): Ctrlsel.SERIAL0, + # SPIS mappings + NrfPsel(fun=NrfFun.SPIS_CSN, port=9, pin=1): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIS_MISO, port=9, pin=3): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIS_MOSI, port=9, pin=2): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.SPIS_SCK, port=9, pin=0): Ctrlsel.SERIAL0, + # TWIM mappings + NrfPsel(fun=NrfFun.TWIM_SCL, port=9, pin=0): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.TWIM_SDA, port=9, pin=3): Ctrlsel.SERIAL0, + # UARTE mappings + NrfPsel(fun=NrfFun.UART_CTS, port=9, pin=2): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RTS, port=9, pin=1): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_RX, port=9, pin=4): Ctrlsel.SERIAL0, + NrfPsel(fun=NrfFun.UART_TX, port=9, pin=3): Ctrlsel.SERIAL0, + }, + } + else: + raise NotImplementedError(f"No CTRLSEL table exists for soc {soc}") + + # Entries below this are common to all supported socs at the time of writing. + + adc_channel_pin_lookup = { + # SAADC + 0x5F98_2000: { + NrfSaadcChannel.AIN0: (1, 0), + NrfSaadcChannel.AIN1: (1, 1), + NrfSaadcChannel.AIN2: (1, 2), + NrfSaadcChannel.AIN3: (1, 3), + NrfSaadcChannel.AIN4: (1, 4), + NrfSaadcChannel.AIN5: (1, 5), + NrfSaadcChannel.AIN6: (1, 6), + NrfSaadcChannel.AIN7: (1, 7), + NrfSaadcChannel.AIN8: (9, 0), + NrfSaadcChannel.AIN9: (9, 1), + NrfSaadcChannel.AIN10: (9, 2), + NrfSaadcChannel.AIN11: (9, 3), + NrfSaadcChannel.AIN12: (9, 4), + NrfSaadcChannel.AIN13: (9, 5), + } + } + comp_channel_pin_lookup = { + # COMP/LPCOMP + 0x5F98_3000: { + NrfCompChannel.AIN0: (1, 0), + NrfCompChannel.AIN1: (1, 1), + NrfCompChannel.AIN2: (1, 2), + NrfCompChannel.AIN3: (1, 3), + NrfCompChannel.AIN4: (1, 4), + NrfCompChannel.AIN5: (1, 5), + NrfCompChannel.AIN6: (1, 6), + NrfCompChannel.AIN7: (1, 7), + NrfCompChannel.AIN8: (9, 0), + NrfCompChannel.AIN9: (9, 1), + } + } + spu_instances = [ + ("SPU110", 0x5F08_0000), + ("SPU111", 0x5F09_0000), + ("SPU120", 0x5F8C_0000), + ("SPU121", 0x5F8D_0000), + ("SPU122", 0x5F8E_0000), + ("SPU130", 0x5F90_0000), + ("SPU131", 0x5F92_0000), + ("SPU132", 0x5F98_0000), + ("SPU133", 0x5F99_0000), + ("SPU134", 0x5F9A_0000), + ("SPU135", 0x5F9B_0000), + ("SPU136", 0x5F9C_0000), + ("SPU137", 0x5F9D_0000), + ] + dppics = { + "DPPIC120": 0x5F8E_1000, + "DPPIC130": 0x5F92_2000, + "DPPIC131": 0x5F98_1000, + "DPPIC132": 0x5F99_1000, + "DPPIC133": 0x5F9A_1000, + "DPPIC134": 0x5F9B_1000, + "DPPIC135": 0x5F9C_1000, + "DPPIC136": 0x5F9D_1000, + } + ppib_instances = [ + ("PPIB110", 0x5F09_8000), + ("PPIB120", 0x5F8E_E000), + ("PPIB121", 0x5F8E_F000), + ("PPIB130", 0x5F92_5000), + ("PPIB131", 0x5F92_6000), + ("PPIB132", 0x5F98_D000), + ("PPIB133", 0x5F99_D000), + ("PPIB134", 0x5F9A_D000), + ("PPIB135", 0x5F9B_D000), + ("PPIB136", 0x5F9C_D000), + ("PPIB137", 0x5F9D_D000), + ] + ppib_name_to_addr = dict(ppib_instances) + dppic_to_ppib_connections = { + dppics["DPPIC120"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB121"], + channel_map=range(0, 8), + ), + dppics["DPPIC131"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB132"], + channel_map=range(0, 8), + ), + dppics["DPPIC132"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB133"], + channel_map=range(0, 8), + ), + dppics["DPPIC133"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB134"], + channel_map=range(0, 8), + ), + dppics["DPPIC134"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB135"], + channel_map=range(0, 8), + ), + dppics["DPPIC135"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB136"], + channel_map=range(0, 8), + ), + dppics["DPPIC136"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB137"], + channel_map=range(0, 8), + ), + } + ppib_to_ppib_connections = { + ppib_name_to_addr["PPIB132"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB130"], + channel_map=range(0, 8), + ), + ppib_name_to_addr["PPIB133"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB130"], + channel_map=range(8, 16), + ), + ppib_name_to_addr["PPIB134"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB130"], + channel_map=range(16, 24), + ), + ppib_name_to_addr["PPIB135"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB130"], + channel_map=range(24, 32), + ), + ppib_name_to_addr["PPIB136"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB131"], + channel_map=range(0, 8), + ), + ppib_name_to_addr["PPIB137"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB131"], + channel_map=range(8, 16), + ), + ppib_name_to_addr["PPIB121"]: FixedPPIMap( + connected_to=ppib_name_to_addr["PPIB131"], + channel_map=range(16, 24), + ), + } + return SocLookupTables( + ctrlsel_lookup=ctrlsel_lookup, + adc_channel_pin_lookup=adc_channel_pin_lookup, + comp_channel_pin_lookup=comp_channel_pin_lookup, + dppic_to_ppib_connections=dppic_to_ppib_connections, + ppib_to_ppib_connections=ppib_to_ppib_connections, + spu_instances=spu_instances, + ppib_instances=ppib_instances, + ) + + +if __name__ == "__main__": + main() diff --git a/soc/nordic/common/uicr/gen_uicr.py b/soc/nordic/common/uicr/gen_uicr.py index 5888786e928..ce69cd7e80a 100644 --- a/soc/nordic/common/uicr/gen_uicr.py +++ b/soc/nordic/common/uicr/gen_uicr.py @@ -7,11 +7,7 @@ import argparse import ctypes as c -import math -import pickle -import re import sys -from collections import defaultdict from itertools import groupby from elftools.elf.elffile import ELFFile @@ -25,11 +21,6 @@ # Must match the name used in the linker script. PERIPHCONF_SECTION = "uicr_periphconf_entry" -# Expected nodelabel of the UICR devicetree node, used to extract its location from the devicetree. -UICR_NODELABEL = "uicr" -# Nodelabel of the PERIPHCONF devicetree node, used to extract its location from the devicetree. -PERIPHCONF_NODELABEL = "periphconf_partition" - # Common values for representing enabled/disabled in the UICR format. ENABLED_VALUE = 0xFFFF_FFFF DISABLED_VALUE = 0xBD23_28A8 @@ -75,23 +66,38 @@ class Protectedmem(c.LittleEndianStructure): ] -class Recovery(c.LittleEndianStructure): +class Wdtstart(c.LittleEndianStructure): _pack_ = 1 _fields_ = [ ("ENABLE", c.c_uint32), - ("PROCESSOR", c.c_uint32), - ("INITSVTOR", c.c_uint32), - ("SIZE4KB", c.c_uint32), + ("INSTANCE", c.c_uint32), + ("CRV", c.c_uint32), + ] + + +class SecurestorageCrypto(c.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("APPLICATIONSIZE1KB", c.c_uint32), + ("RADIOCORESIZE1KB", c.c_uint32), ] -class Its(c.LittleEndianStructure): +class SecurestorageIts(c.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("APPLICATIONSIZE1KB", c.c_uint32), + ("RADIOCORESIZE1KB", c.c_uint32), + ] + + +class Securestorage(c.LittleEndianStructure): _pack_ = 1 _fields_ = [ ("ENABLE", c.c_uint32), ("ADDRESS", c.c_uint32), - ("APPLICATIONSIZE", c.c_uint32), - ("RADIOCORESIZE", c.c_uint32), + ("CRYPTO", SecurestorageCrypto), + ("ITS", SecurestorageIts), ] @@ -113,6 +119,64 @@ class Mpcconf(c.LittleEndianStructure): ] +class SecondaryTrigger(c.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("ENABLE", c.c_uint32), + ("RESETREAS", c.c_uint32), + ("RESERVED", c.c_uint32), + ] + + +class SecondaryProtectedmem(c.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("ENABLE", c.c_uint32), + ("SIZE4KB", c.c_uint32), + ] + + +class SecondaryWdtstart(c.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("ENABLE", c.c_uint32), + ("INSTANCE", c.c_uint32), + ("CRV", c.c_uint32), + ] + + +class SecondaryPeriphconf(c.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("ENABLE", c.c_uint32), + ("ADDRESS", c.c_uint32), + ("MAXCOUNT", c.c_uint32), + ] + + +class SecondaryMpcconf(c.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("ENABLE", c.c_uint32), + ("ADDRESS", c.c_uint32), + ("MAXCOUNT", c.c_uint32), + ] + + +class Secondary(c.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("ENABLE", c.c_uint32), + ("PROCESSOR", c.c_uint32), + ("TRIGGER", SecondaryTrigger), + ("ADDRESS", c.c_uint32), + ("PROTECTEDMEM", SecondaryProtectedmem), + ("WDTSTART", SecondaryWdtstart), + ("PERIPHCONF", SecondaryPeriphconf), + ("MPCCONF", SecondaryMpcconf), + ] + + class Uicr(c.LittleEndianStructure): _pack_ = 1 _fields_ = [ @@ -123,11 +187,14 @@ class Uicr(c.LittleEndianStructure): ("APPROTECT", Approtect), ("ERASEPROTECT", c.c_uint32), ("PROTECTEDMEM", Protectedmem), - ("RECOVERY", Recovery), - ("ITS", Its), - ("RESERVED2", c.c_uint32 * 7), + ("WDTSTART", Wdtstart), + ("RESERVED2", c.c_uint32), + ("SECURESTORAGE", Securestorage), + ("RESERVED3", c.c_uint32 * 5), ("PERIPHCONF", Periphconf), ("MPCCONF", Mpcconf), + ("SECONDARY", Secondary), + ("PADDING", c.c_uint32 * 15), ] @@ -141,18 +208,6 @@ def main() -> None: "peripherals, and to protect the device in various ways." ), ) - parser.add_argument( - "--in-config", - required=True, - type=argparse.FileType("r"), - help="Path to the .config file from the application build", - ) - parser.add_argument( - "--in-edt-pickle", - required=True, - type=argparse.FileType("rb"), - help="Path to the edt.pickle file from the application build", - ) parser.add_argument( "--in-periphconf-elf", dest="in_periphconf_elfs", @@ -165,70 +220,204 @@ def main() -> None: "by ascending address and cleared of duplicate entries." ), ) + parser.add_argument( + "--out-merged-hex", + required=True, + type=argparse.FileType("w", encoding="utf-8"), + help="Path to write the merged UICR+PERIPHCONF HEX file to", + ) parser.add_argument( "--out-uicr-hex", required=True, type=argparse.FileType("w", encoding="utf-8"), - help="Path to write the generated UICR HEX file to", + help="Path to write the UICR-only HEX file to", ) parser.add_argument( "--out-periphconf-hex", + type=argparse.FileType("w", encoding="utf-8"), + help="Path to write the PERIPHCONF-only HEX file to", + ) + parser.add_argument( + "--periphconf-address", + default=None, + type=lambda s: int(s, 0), + help="Absolute flash address of the PERIPHCONF partition (decimal or 0x-prefixed hex)", + ) + parser.add_argument( + "--periphconf-size", + default=None, + type=lambda s: int(s, 0), + help="Size in bytes of the PERIPHCONF partition (decimal or 0x-prefixed hex)", + ) + parser.add_argument( + "--uicr-address", + required=True, + type=lambda s: int(s, 0), + help="Absolute flash address of the UICR region (decimal or 0x-prefixed hex)", + ) + parser.add_argument( + "--secondary", + action="store_true", + help="Enable secondary firmware support in UICR", + ) + parser.add_argument( + "--secondary-address", + default=None, + type=lambda s: int(s, 0), + help="Absolute flash address of the secondary firmware (decimal or 0x-prefixed hex)", + ) + parser.add_argument( + "--secondary-periphconf-address", + default=None, + type=lambda s: int(s, 0), + help=( + "Absolute flash address of the secondary PERIPHCONF partition " + "(decimal or 0x-prefixed hex)" + ), + ) + parser.add_argument( + "--secondary-periphconf-size", default=None, + type=lambda s: int(s, 0), + help="Size in bytes of the secondary PERIPHCONF partition (decimal or 0x-prefixed hex)", + ) + parser.add_argument( + "--in-secondary-periphconf-elf", + dest="in_secondary_periphconf_elfs", + default=[], + action="append", + type=argparse.FileType("rb"), + help=( + "Path to an ELF file to extract secondary PERIPHCONF data from. " + "Can be provided multiple times. The secondary PERIPHCONF data from each ELF file " + "is combined in a single list which is sorted by ascending address and cleared " + "of duplicate entries." + ), + ) + parser.add_argument( + "--out-secondary-periphconf-hex", type=argparse.FileType("w", encoding="utf-8"), - help="Path to write the generated PERIPHCONF HEX file to", + help="Path to write the secondary PERIPHCONF-only HEX file to", ) args = parser.parse_args() try: + # Validate argument dependencies + if args.out_periphconf_hex: + if args.periphconf_address is None: + raise ScriptError( + "--periphconf-address is required when --out-periphconf-hex is used" + ) + if args.periphconf_size is None: + raise ScriptError("--periphconf-size is required when --out-periphconf-hex is used") + + # Validate secondary argument dependencies + if args.secondary and args.secondary_address is None: + raise ScriptError("--secondary-address is required when --secondary is used") + + if args.out_secondary_periphconf_hex: + if args.secondary_periphconf_address is None: + raise ScriptError( + "--secondary-periphconf-address is required when " + "--out-secondary-periphconf-hex is used" + ) + if args.secondary_periphconf_size is None: + raise ScriptError( + "--secondary-periphconf-size is required when " + "--out-secondary-periphconf-hex is used" + ) + init_values = DISABLED_VALUE.to_bytes(4, "little") * (c.sizeof(Uicr) // 4) uicr = Uicr.from_buffer_copy(init_values) uicr.VERSION.MAJOR = UICR_FORMAT_VERSION_MAJOR uicr.VERSION.MINOR = UICR_FORMAT_VERSION_MINOR - kconfig_str = args.in_config.read() - kconfig = parse_kconfig(kconfig_str) + # Process periphconf data first and configure UICR completely before creating hex objects + periphconf_hex = IntelHex() + secondary_periphconf_hex = IntelHex() - edt = pickle.load(args.in_edt_pickle) + if args.out_periphconf_hex: + periphconf_combined = extract_and_combine_periphconfs(args.in_periphconf_elfs) - try: - periphconf_partition = edt.label2node[PERIPHCONF_NODELABEL] - except LookupError as e: - raise ScriptError( - "Failed to find a PERIPHCONF partition in the devicetree. " - f"Expected a DT node with label '{PERIPHCONF_NODELABEL}'." - ) from e + padding_len = args.periphconf_size - len(periphconf_combined) + periphconf_final = periphconf_combined + bytes([0xFF for _ in range(padding_len)]) - flash_base_address = periphconf_partition.flash_controller.regs[0].addr - periphconf_address = flash_base_address + periphconf_partition.regs[0].addr - periphconf_size = periphconf_partition.regs[0].size + # Add periphconf data to periphconf hex object + periphconf_hex.frombytes(periphconf_final, offset=args.periphconf_address) - periphconf_combined = extract_and_combine_periphconfs(args.in_periphconf_elfs) - padding_len = periphconf_size - len(periphconf_combined) - periphconf_final = periphconf_combined + bytes([0xFF for _ in range(padding_len)]) - - if kconfig.get("CONFIG_NRF_HALTIUM_UICR_PERIPHCONF") == "y": + # Configure UICR with periphconf settings uicr.PERIPHCONF.ENABLE = ENABLED_VALUE - uicr.PERIPHCONF.ADDRESS = periphconf_address - uicr.PERIPHCONF.MAXCOUNT = math.floor(periphconf_size / 8) + uicr.PERIPHCONF.ADDRESS = args.periphconf_address + + # MAXCOUNT is given in number of 8-byte peripheral + # configuration entries and periphconf_size is given in + # bytes. When setting MAXCOUNT based on the + # periphconf_size we must first assert that + # periphconf_size has not been misconfigured. + if args.periphconf_size % 8 != 0: + raise ScriptError( + f"args.periphconf_size was {args.periphconf_size}, but must be divisible by 8" + ) + + uicr.PERIPHCONF.MAXCOUNT = args.periphconf_size // 8 - try: - uicr_node = edt.label2node[UICR_NODELABEL] - except LookupError as e: - raise ScriptError( - "Failed to find UICR node in the devicetree. " - f"Expected a DT node with label '{UICR_NODELABEL}'." - ) from e + # Handle secondary firmware configuration + if args.secondary: + uicr.SECONDARY.ENABLE = ENABLED_VALUE + uicr.SECONDARY.ADDRESS = args.secondary_address + # Handle secondary periphconf if provided + if args.out_secondary_periphconf_hex: + secondary_periphconf_combined = extract_and_combine_periphconfs( + args.in_secondary_periphconf_elfs + ) + + padding_len = args.secondary_periphconf_size - len(secondary_periphconf_combined) + secondary_periphconf_final = secondary_periphconf_combined + bytes( + [0xFF for _ in range(padding_len)] + ) + + # Add secondary periphconf data to secondary periphconf hex object + secondary_periphconf_hex.frombytes( + secondary_periphconf_final, offset=args.secondary_periphconf_address + ) + + # Configure UICR with secondary periphconf settings + uicr.SECONDARY.PERIPHCONF.ENABLE = ENABLED_VALUE + uicr.SECONDARY.PERIPHCONF.ADDRESS = args.secondary_periphconf_address + + # MAXCOUNT is given in number of 8-byte peripheral + # configuration entries and secondary_periphconf_size is given in + # bytes. When setting MAXCOUNT based on the + # secondary_periphconf_size we must first assert that + # secondary_periphconf_size has not been misconfigured. + if args.secondary_periphconf_size % 8 != 0: + raise ScriptError( + f"args.secondary_periphconf_size was {args.secondary_periphconf_size}, " + f"but must be divisible by 8" + ) + + uicr.SECONDARY.PERIPHCONF.MAXCOUNT = args.secondary_periphconf_size // 8 + + # Create UICR hex object with final UICR data uicr_hex = IntelHex() - uicr_hex.frombytes(bytes(uicr), offset=uicr_node.regs[0].addr) + uicr_hex.frombytes(bytes(uicr), offset=args.uicr_address) - uicr_hex.write_hex_file(args.out_uicr_hex) + # Create merged hex by combining UICR and periphconf hex objects + merged_hex = IntelHex() + merged_hex.fromdict(uicr_hex.todict()) - if args.out_periphconf_hex is not None: - periphconf_hex = IntelHex() - periphconf_hex.frombytes(periphconf_final, offset=periphconf_address) + if args.out_periphconf_hex: periphconf_hex.write_hex_file(args.out_periphconf_hex) + merged_hex.fromdict(periphconf_hex.todict()) + + if args.out_secondary_periphconf_hex: + secondary_periphconf_hex.write_hex_file(args.out_secondary_periphconf_hex) + merged_hex.fromdict(secondary_periphconf_hex.todict()) + + merged_hex.write_hex_file(args.out_merged_hex) + uicr_hex.write_hex_file(args.out_uicr_hex) except ScriptError as e: print(f"Error: {e!s}") @@ -237,6 +426,7 @@ def main() -> None: def extract_and_combine_periphconfs(elf_files: list[argparse.FileType]) -> bytes: combined_periphconf = [] + ipcmap_index = 0 for in_file in elf_files: elf = ELFFile(in_file) @@ -247,6 +437,7 @@ def extract_and_combine_periphconfs(elf_files: list[argparse.FileType]) -> bytes conf_section_data = conf_section.data() num_entries = len(conf_section_data) // PERIPHCONF_ENTRY_SIZE periphconf = (PeriphconfEntry * num_entries).from_buffer_copy(conf_section_data) + ipcmap_index = adjust_ipcmap_entries(periphconf, offset_index=ipcmap_index) combined_periphconf.extend(periphconf) combined_periphconf.sort(key=lambda e: e.regptr) @@ -270,15 +461,35 @@ def extract_and_combine_periphconfs(elf_files: list[argparse.FileType]) -> bytes return bytes(final_periphconf) -def parse_kconfig(content: str) -> dict[str, str | None]: - result = defaultdict(None) - match_iter = re.finditer( - r"^(?P(SB_)?CONFIG_[^=\s]+)=(?P[^\s#])+$", content, re.MULTILINE - ) - for match in match_iter: - result[match["config"]] = match["value"] - - return result +# This workaround is currently needed to avoid conflicts in IPCMAP whenever more than +# one image uses IPCMAP, because at the moment each image has no way of knowing which +# IPCMAP channel indices it should use for the configuration it generates locally. +# +# What the workaround does is adjust all IPCMAP entries found in the periphconf by the +# given index offset. +# +# The workaround assumes that IPCMAP entries are allocated sequentially starting from 0 +# in each image, it will probably not work for arbitrary IPCMAP entries. +def adjust_ipcmap_entries(periphconf: c.Array[PeriphconfEntry], offset_index: int) -> int: + max_ipcmap_index = offset_index + + for entry in sorted(periphconf, key=lambda e: e.regptr): + if IPCMAP_CHANNEL_START_ADDR <= entry.regptr < IPCMAP_CHANNEL_END_ADDR: + entry.regptr += offset_index * IPCMAP_CHANNEL_SIZE + entry_ipcmap_index = (entry.regptr - IPCMAP_CHANNEL_START_ADDR) // IPCMAP_CHANNEL_SIZE + max_ipcmap_index = max(max_ipcmap_index, entry_ipcmap_index) + + return max_ipcmap_index + 1 + + +# Size of each IPCMAP.CHANNEL[i] +IPCMAP_CHANNEL_SIZE = 8 +# Number of entries in IPCMAP.CHANNEL +IPCMAP_CHANNEL_COUNT = 16 +# Address of IPCMAP.CHANNEL[0] +IPCMAP_CHANNEL_START_ADDR = 0x5F92_3000 + 256 * 4 +# Address of IPCMAP.CHANNEL[channel count] + 1 +IPCMAP_CHANNEL_END_ADDR = IPCMAP_CHANNEL_START_ADDR + IPCMAP_CHANNEL_SIZE * IPCMAP_CHANNEL_COUNT if __name__ == "__main__": diff --git a/soc/nordic/common/uicr/gen_uicr/CMakeLists.txt b/soc/nordic/common/uicr/gen_uicr/CMakeLists.txt new file mode 100644 index 00000000000..1163ff8e21f --- /dev/null +++ b/soc/nordic/common/uicr/gen_uicr/CMakeLists.txt @@ -0,0 +1,146 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The code in this CMakeLists.txt constructs the arguments for gen_uicr.py +# and creates a flashable zephyr.hex file containing UICR data (no C code compiled) +# + +cmake_minimum_required(VERSION 3.20.0) + +# Instead of adding all of Zephyr we add just the subset that is +# required to generate uicr.hex. +# +# The generation of uicr.hex is configured by this image, so we +# include modules from zephyr_default up until kconfig. + +find_package(Zephyr + COMPONENTS zephyr_default:kconfig + REQUIRED HINTS $ENV{ZEPHYR_BASE} + ) + +project(uicr) + +# Function to parse a Kconfig value from a .config file +function(parse_kconfig_value config_file config_name output_var) + file(STRINGS ${config_file} config_lines ENCODING "UTF-8") + foreach(line ${config_lines}) + if("${line}" MATCHES "^${config_name}=\"(.*)\"$") + set(${output_var} "${CMAKE_MATCH_1}" PARENT_SCOPE) + return() + endif() + endforeach() +endfunction() + +# Function to compute partition absolute address and size from devicetree +function(compute_partition_address_and_size partition_nodelabel output_address_var output_size_var) + dt_nodelabel(partition_path NODELABEL ${partition_nodelabel} REQUIRED) + dt_reg_addr(partition_offset PATH ${partition_path} REQUIRED) + dt_reg_size(partition_size PATH ${partition_path} REQUIRED) + + # Calculate absolute partition address + math(EXPR partition_address "${CONFIG_FLASH_BASE_ADDRESS} + ${partition_offset}" OUTPUT_FORMAT HEXADECIMAL) + + # Set output variables in parent scope + set(${output_address_var} ${partition_address} PARENT_SCOPE) + set(${output_size_var} ${partition_size} PARENT_SCOPE) +endfunction() + +# Use CMAKE_VERBOSE_MAKEFILE to silence an unused-variable warning. +if(CMAKE_VERBOSE_MAKEFILE) +endif() + +set(periphconf_args) +set(periphconf_elfs) +set(merged_hex_file ${APPLICATION_BINARY_DIR}/zephyr/${CONFIG_KERNEL_BIN_NAME}.hex) +set(secondary_periphconf_elfs) +set(uicr_hex_file ${APPLICATION_BINARY_DIR}/zephyr/uicr.hex) +set(periphconf_hex_file ${APPLICATION_BINARY_DIR}/zephyr/periphconf.hex) +set(secondary_periphconf_hex_file ${APPLICATION_BINARY_DIR}/zephyr/secondary_periphconf.hex) + +# Get UICR absolute address from this image's devicetree +dt_nodelabel(uicr_path NODELABEL "uicr" REQUIRED) +dt_reg_addr(UICR_ADDRESS PATH ${uicr_path} REQUIRED) + +if(CONFIG_GEN_UICR_GENERATE_PERIPHCONF) + # gen_uicr.py parses all zephyr.elf files. To find these files (which + # have not been built yet) we scan sibling build directories for + # zephyr.dts + get_filename_component(SYSBUILD_DIR ${APPLICATION_BINARY_DIR} DIRECTORY) + file(GLOB _siblings LIST_DIRECTORIES true "${SYSBUILD_DIR}/*") + foreach(_dir ${_siblings}) + get_filename_component(_name ${_dir} NAME) + if(_name STREQUAL "uicr") + # This image is an exception to the rule. It has a zephyr.dts, but + # no zephyr.elf + continue() + endif() + + if(EXISTS ${_dir}/zephyr/zephyr.dts) + # Read CONFIG_KERNEL_BIN_NAME from the sibling's .config file + parse_kconfig_value(${_dir}/zephyr/.config CONFIG_KERNEL_BIN_NAME kernel_bin_name) + set(kernel_elf_path ${_dir}/zephyr/${kernel_bin_name}.elf) + + # Check if this is secondary firmware by looking for the marker file + if(EXISTS ${_dir}/is_secondary_firmware.txt) + list(APPEND secondary_periphconf_elfs ${kernel_elf_path}) + else() + list(APPEND periphconf_elfs ${kernel_elf_path}) + endif() + endif() + endforeach() + + # Compute PERIPHCONF absolute address and size from this image's devicetree + compute_partition_address_and_size("periphconf_partition" PERIPHCONF_ADDRESS PERIPHCONF_SIZE) + + # Set up periphconf arguments for gen_uicr.py + list(APPEND periphconf_args --periphconf-address ${PERIPHCONF_ADDRESS}) + list(APPEND periphconf_args --periphconf-size ${PERIPHCONF_SIZE}) + list(APPEND periphconf_args --out-periphconf-hex ${periphconf_hex_file}) + + foreach(elf ${periphconf_elfs}) + list(APPEND periphconf_args --in-periphconf-elf ${elf}) + endforeach() +endif(CONFIG_GEN_UICR_GENERATE_PERIPHCONF) + +if(CONFIG_GEN_UICR_SECONDARY) + set(secondary_args --secondary) + + # Compute SECONDARY partition absolute address from this image's devicetree + compute_partition_address_and_size("secondary_partition" SECONDARY_ADDRESS SECONDARY_SIZE) + + list(APPEND secondary_args + --secondary-address ${SECONDARY_ADDRESS} + ) + + if(CONFIG_GEN_UICR_SECONDARY_GENERATE_PERIPHCONF) + # Compute SECONDARY_PERIPHCONF absolute address and size from this image's devicetree + compute_partition_address_and_size("secondary_periphconf_partition" SECONDARY_PERIPHCONF_ADDRESS SECONDARY_PERIPHCONF_SIZE) + + list(APPEND secondary_args --secondary-periphconf-address ${SECONDARY_PERIPHCONF_ADDRESS}) + list(APPEND secondary_args --secondary-periphconf-size ${SECONDARY_PERIPHCONF_SIZE}) + list(APPEND secondary_args --out-secondary-periphconf-hex ${secondary_periphconf_hex_file}) + + foreach(elf ${secondary_periphconf_elfs}) + list(APPEND secondary_args --in-secondary-periphconf-elf ${elf}) + endforeach() + endif() +endif() + +# Generate hex files (merged, uicr-only, periphconf-only, and secondary-periphconf-only) +add_custom_command( + OUTPUT ${merged_hex_file} ${uicr_hex_file} ${periphconf_hex_file} ${secondary_periphconf_hex_file} + COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${ZEPHYR_BASE}/scripts/dts/python-devicetree/src + ${PYTHON_EXECUTABLE} ${ZEPHYR_BASE}/soc/nordic/common/uicr/gen_uicr.py + --uicr-address ${UICR_ADDRESS} + --out-merged-hex ${merged_hex_file} + --out-uicr-hex ${uicr_hex_file} + ${periphconf_args} + ${secondary_args} + DEPENDS ${periphconf_elfs} ${secondary_periphconf_elfs} + WORKING_DIRECTORY ${APPLICATION_BINARY_DIR} + COMMENT "Using gen_uicr.py to generate ${merged_hex_file}, ${uicr_hex_file}, ${periphconf_hex_file}, and ${secondary_periphconf_hex_file} from ${periphconf_elfs} ${secondary_periphconf_elfs}" +) + +# Add zephyr subdirectory to handle flash configuration with correct paths +add_subdirectory(zephyr) + +add_custom_target(gen_uicr ALL DEPENDS ${merged_hex_file} ${uicr_hex_file} ${periphconf_hex_file} ${secondary_periphconf_hex_file}) diff --git a/soc/nordic/common/uicr/gen_uicr/Kconfig b/soc/nordic/common/uicr/gen_uicr/Kconfig new file mode 100644 index 00000000000..41d31db6464 --- /dev/null +++ b/soc/nordic/common/uicr/gen_uicr/Kconfig @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +menu "UICR generator options" + +config GEN_UICR_GENERATE_PERIPHCONF + bool "Generate PERIPHCONF hex alongside UICR" + default y + help + When enabled, the UICR generator will populate the + periphconf_partition partition. + +config GEN_UICR_SECONDARY + bool "Enable UICR.SECONDARY.ENABLE" + +config GEN_UICR_SECONDARY_GENERATE_PERIPHCONF + bool "Generate SECONDARY.PERIPHCONF hex alongside UICR" + default y + depends on GEN_UICR_SECONDARY + help + When enabled, the UICR generator will populate the + secondary_periphconf_partition partition. + +endmenu + +source "Kconfig.zephyr" diff --git a/soc/nordic/common/uicr/gen_uicr/prj.conf b/soc/nordic/common/uicr/gen_uicr/prj.conf new file mode 100644 index 00000000000..b2a4ba59104 --- /dev/null +++ b/soc/nordic/common/uicr/gen_uicr/prj.conf @@ -0,0 +1 @@ +# nothing here diff --git a/soc/nordic/common/uicr/gen_uicr/zephyr/CMakeLists.txt b/soc/nordic/common/uicr/gen_uicr/zephyr/CMakeLists.txt new file mode 100644 index 00000000000..1d020664016 --- /dev/null +++ b/soc/nordic/common/uicr/gen_uicr/zephyr/CMakeLists.txt @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Flash configuration for UICR domain +# This subdirectory ensures runners.yaml is generated in the correct location + +# Manually include board configuration to enable automatic runners.yaml generation +include(${BOARD_DIR}/board.cmake OPTIONAL) + +# Create the runners_yaml_props_target that flash system expects +add_custom_target(runners_yaml_props_target) + +# Set hex_file property to point to zephyr.hex in this directory +set_target_properties(runners_yaml_props_target PROPERTIES + hex_file "zephyr.hex" +) + +# Override the runners.yaml path to use CMAKE_CURRENT_BINARY_DIR instead of PROJECT_BINARY_DIR +# This ensures runners.yaml is generated at build/uicr/zephyr/ where west expects it +set(PROJECT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}) + +# Include flash support to automatically generate runners.yaml +include(${ZEPHYR_BASE}/cmake/flash/CMakeLists.txt) diff --git a/soc/nordic/common/uicr/periphconf/__init__.py b/soc/nordic/common/uicr/periphconf/__init__.py new file mode 100644 index 00000000000..fe8f6fbacec --- /dev/null +++ b/soc/nordic/common/uicr/periphconf/__init__.py @@ -0,0 +1,6 @@ +""" +Copyright (c) 2025 Nordic Semiconductor ASA +SPDX-License-Identifier: Apache-2.0 +""" + +# empty diff --git a/soc/nordic/common/uicr/periphconf/builder.py b/soc/nordic/common/uicr/periphconf/builder.py new file mode 100644 index 00000000000..98c2bdfb0a0 --- /dev/null +++ b/soc/nordic/common/uicr/periphconf/builder.py @@ -0,0 +1,1460 @@ +""" +Copyright (c) 2025 Nordic Semiconductor ASA +SPDX-License-Identifier: Apache-2.0 +""" + +from __future__ import annotations + +import enum +import logging +import re +from collections.abc import Iterable, Iterator, Mapping +from dataclasses import dataclass +from functools import cached_property +from itertools import islice +from typing import ( + Any, + Literal, + NamedTuple, +) + +# edtlib classes used in this module. +# Defined this way as placeholders so type checking for these could be added later. +EDT = Any +Node = Any +PinCtrl = Any +Property = Any + +# Status values that indicate ownership either by the processor or a child processor +STATUS_OWNED = ("okay", "reserved") + +# Compatibles of global peripherals that should not be assigned to current core. +SKIP_SPU_PERIPH_PERM_COMPATS = { + "nordic,nrf-bellboard-tx", + "nordic,nrf-bellboard-rx", + "nordic,nrf-clic", + "nordic,nrf-dppic-global", + "nordic,nrf-gpio", + "nordic,nrf-gpiote", + "nordic,nrf-grtc", + "nordic,nrf-ipct-global", + "nordic,nrf-temp", + "nordic,nrf-vevif-task-tx", + "nordic,nrf-vevif-task-rx", +} + +# Compatibles of global peripherals that should be assigned to the current core but do not have DMA +NO_DMA_COMPATS = { + "nordic,nrf-auxpll", + "nordic,nrf-comp", + "nordic,nrf-egu", + "nordic,nrf-exmif", + "nordic,nrf-lpcomp", + "nordic,nrf-qdec", + "nordic,nrf-timer", + "nordic,nrf-rtc", + "nordic,nrf-wdt", + # VPRs have a PERM DMASEC setting, but PPR applications currently do not work as intended when + # setting it to secure. + "nordic,nrf-vpr-coprocessor", +} + + +class BuilderError(RuntimeError): + """Generic runtime error raised by the PeriphconfBuilder""" + + ... + + +class BadDevicetreeError(BuilderError): + """Error raised when the devicetree is misconfigured + or if assumptions about the devicetree are not met. + """ + + ... + + +def _init_logger() -> logging.Logger: + formatter = logging.Formatter("{message}", style="{") + handler = logging.StreamHandler() + handler.setFormatter(formatter) + + logger = logging.getLogger("periphconf") + logger.setLevel(logging.ERROR) + logger.addHandler(handler) + + return logger + + +# Logger for the script +log = _init_logger() + + +class PeriphconfBuilder: + def __init__( + self, + dt: EDT, + lookup_tables: SocLookupTables, + ) -> None: + """Builder class used to generate a PERIPHCONF C source file based on the devicetree. + + :param dt: Devicetree object. + :param lookup_tables: Lookup table object containing soc-specific information. + """ + + self._dt = dt + self._hw_tables = lookup_tables + self._processor_id = dt_processor_id(dt) + self._owner_id = self._processor_id.default_owner_id + + self._macros = [] + self._ipcmap_idx = 0 + self._generated_ipcmaps = set() + + self._nodelabel_lookup = { + dt_reg_addr(node): node.labels[0] for node in dt.nodes if node.regs and node.labels + } + + def build_generated_source(self, header_line: str | None = None) -> str: + """Generate a C source file containing all the macros generated through + calls to the API. + """ + source_lines = [] + + if header_line: + source_lines.extend([f"/* {header_line} */", ""]) + + source_lines.extend( + [ + "#include ", + "#include ", + "", + ] + ) + + for macro in sorted(self._macros): + source_lines.append(macro.c_render(self._nodelabel_lookup)) + + return "\n".join(source_lines) + + def add_local_peripheral_cfg( + self, + node_or_nodelabel: Node | str, + /, + ) -> None: + """Generate macros for populating PERIPHCONF based on the properties of a local domain + devicetree node. + + :param node_or_nodelabel: node or label of the node to process. + """ + self._add_peripheral_cfg(node_or_nodelabel, is_global=False, add_gpios=False) + + def add_global_peripheral_cfg( + self, + node_or_nodelabel: Node | str, + /, + *, + has_irq_mapping: bool = True, + add_ppib_channel_links: bool = True, + ) -> None: + """Generate macros for populating PERIPHCONF based on the properties of the given + devicetree node located in a global domain. + + :param node_or_nodelabel: node or label of the node to process. + :param has_irq_mapping: configure IRQMAP to map the peripheral IRQs to the processor the + devicetree belongs to, if interrupts are specified on the node. + :param add_ppib_channel_links: add PPIB connections based on the channel link properties of + the node. + """ + self._add_peripheral_cfg( + node_or_nodelabel, + is_global=True, + has_irq_mapping=has_irq_mapping, + add_ppib_channel_links=add_ppib_channel_links, + ) + + def _add_peripheral_cfg( + self, + node_or_nodelabel: Node | str, + is_global: bool = True, + has_irq_mapping: bool = True, + add_ppib_channel_links: bool = True, + add_gpios: bool = True, + ) -> None: + if isinstance(node_or_nodelabel, str): + node = self._dt.label2node[node_or_nodelabel] + else: + node = node_or_nodelabel + + if node.status not in STATUS_OWNED: + return + + if is_global: + has_skip_spu_periph_perm_compat = any( + compat in SKIP_SPU_PERIPH_PERM_COMPATS for compat in node.compats + ) + if not has_skip_spu_periph_perm_compat: + has_no_dma_compat = any(compat in NO_DMA_COMPATS for compat in node.compats) + self._add_global_peripheral_spu_permissions(node, has_dma=not has_no_dma_compat) + if has_irq_mapping: + self._add_global_peripheral_irq_mapping(node) + + if "nordic,nrf-gpiote" in node.compats: + self._add_nrf_gpiote_spu_permissions(node) + + if "nordic,nrf-dppic-global" in node.compats: + self._add_nrf_dppic_spu_permissions(node) + if add_ppib_channel_links: + self._add_nrf_dppic_ppib_links(node) + + if "nordic,nrf-ipct-global" in node.compats: + self._add_nrf_ipct_global_spu_permissions(node) + self._add_nrf_ipct_ipcmap_channel_links(node) + + if "nordic,nrf-grtc" in node.compats: + self._add_nrf_grtc_spu_permissions(node) + + if "nordic,nrf-saadc" in node.compats: + self._add_nrf_saadc_channel_pin_spu_permissions(node) + + if "nordic,nrf-comp" in node.compats or "nordic,nrf-lpcomp" in node.compats: + self._add_nrf_comp_lpcomp_channel_pin_spu_permissions(node) + + self._add_peripheral_pinctrls_spu_permissions_and_ctrlsel(node) + + if "nordic,nrf-ipct-local" in node.compats: + self._add_nrf_ipct_ipcmap_channel_links(node) + + if add_gpios: + self._add_peripheral_gpios_spu_permissions_and_ctrlsel(node) + + def add_gpio_spu_permissions(self, node_or_nodelabel: Node | str, /) -> None: + """Generate macros for populating SPU FEATURE.GPIO[n].PIN[m] registers based on + gpios properties on the given devicetree node. + + CTRLSEL is not set for these properties; it is assumed that the default CTRLSEL + value (GPIO) is the correct CTRLSEL for these pins. + + :param node_or_nodelabel: node or label of the node to process. + """ + if isinstance(node_or_nodelabel, str): + node = self._dt.label2node[node_or_nodelabel] + else: + node = node_or_nodelabel + + if node.status not in STATUS_OWNED: + return + + self._add_peripheral_gpios_spu_permissions_and_ctrlsel(node, skip_ctrlsel=True) + + def _add_global_peripheral_spu_permissions( + self, + node: Node, + has_dma: bool = True, + ) -> None: + """Adds an SPU PERIPH[n].PERM entry for the peripheral described by the node.""" + addr = dt_reg_addr(node) + secure = dt_node_is_secure(node) + if has_dma: + dma_secure = secure + else: + # If there is no DMA then the DMASEC field is not writable and reads as zero. + # Set to zero to avoid readback errors. + dma_secure = False + periph_label = node.labels[0] + + spu_address = get_spu_addr_for_periph(addr) + spu_name = self._hw_tables.spu_addr_to_name[spu_address] + parsed_addr = Address(addr) + periph_slave_index = parsed_addr.slave_index + + self._macros.append( + MacroCall( + "UICR_SPU_PERIPH_PERM_SET", + [ + Address(spu_address), + periph_slave_index, + secure, + dma_secure, + self._owner_id.c_enum, + ], + comment=f"{spu_name}: {periph_label} permissions", + ) + ) + + def _add_global_peripheral_irq_mapping(self, node: Node) -> None: + """Adds an IRQMAP[n].SINK entry for each interrupt on the + peripheral described by the node. + """ + periph_identifier = dt_node_identifier(node) + get_irqn_from_dt = bool(node.labels) + + for i, interrupt in enumerate(node.interrupts): + interrupt_processors = set() + interrupt_ctrl = interrupt.controller + + interrupt_processors.update(dt_node_processors_from_labels(interrupt_ctrl)) + + if not interrupt_processors: + interrupt_processors.update(dt_node_processors_from_labels(node)) + + if not interrupt_processors: + raise BuilderError( + f"No unique processor ID could be found based on interrupt controllers " + f"or nodelabels for peripheral node {node.path}" + ) + if len(interrupt_processors) > 1: + raise BuilderError( + f"Peripheral node {node.path} corresponds to multiple processors " + f"({interrupt_processors}), which is not supported." + ) + + irq_processor = next(iter(interrupt_processors)) + + if get_irqn_from_dt: + macro_irqn = f"DT_IRQN_BY_IDX(DT_NODELABEL({node.labels[0]}), {i})" + else: + macro_irqn = str(interrupt.data["irq"]) + + self._macros.append( + MacroCall( + "UICR_IRQMAP_IRQ_SINK_SET", + [ + macro_irqn, + irq_processor.c_enum, + ], + comment=f"{periph_identifier} IRQ => {irq_processor.name}", + ) + ) + + def _add_nrf_gpiote_spu_permissions(self, node: Node) -> None: + """Adds SPU FEATURE.GPIOTE[n].CH[m] entries for configured channels on the + GPIOTE peripheral described by the node. + """ + addr = dt_reg_addr(node) + instance_name = dt_node_identifier(node) + spu_address = get_spu_addr_for_periph(addr) + spu_name = self._hw_tables.spu_addr_to_name[spu_address] + + for num, secure in dt_split_channels_get(node): + self._macros.append( + MacroCall( + "UICR_SPU_FEATURE_GPIOTE_CH_SET", + [ + Address(spu_address), + 0, + num, + secure, + self._owner_id.c_enum, + ], + comment=f"{spu_name}: {instance_name} ch. {num} permissions", + ) + ) + + def _add_nrf_dppic_spu_permissions(self, node: Node) -> None: + """Adds SPU FEATURE.DPPIC[n].CH[m] and SPU FEATURE.DPPIC[n].CHG[m] entries for + configured channels and channel groups on the DPPIC peripheral described by the node. + """ + addr = dt_reg_addr(node) + channels = dt_split_channels_get(node) + channel_groups = dt_split_channels_get( + node, + owned_name="owned-channel-groups", + nonsecure_name="nonsecure-channel-groups", + ) + + instance_name = dt_node_identifier(node) + spu_address = get_spu_addr_for_periph(addr) + spu_name = self._hw_tables.spu_addr_to_name[spu_address] + + for num, secure in channels: + self._macros.append( + MacroCall( + "UICR_SPU_FEATURE_DPPIC_CH_SET", + [ + Address(spu_address), + num, + secure, + self._owner_id.c_enum, + ], + comment=f"{spu_name}: {instance_name} ch. {num} permissions", + ) + ) + + for num, secure in channel_groups: + self._macros.append( + MacroCall( + "UICR_SPU_FEATURE_DPPIC_CHG_SET", + [ + Address(spu_address), + num, + secure, + self._owner_id.c_enum, + ], + comment=f"{spu_name}: {instance_name} ch. group {num} permissions", + ) + ) + + def _add_nrf_dppic_ppib_links(self, node: Node) -> None: + """Adds PPIB SUBSCRIBE_SEND[n] and PPIB PUBLISH_RECEIVE[n] entries for + configured "source" and "sink" channels on the DPPIC peripheral described by the node. + """ + addr = dt_reg_addr(node, secure=True) + source_channels = dt_array_prop(node, "source-channels", []) + sink_channels = dt_array_prop(node, "sink-channels", []) + + for num in source_channels: + self._link_dppi_channels(addr, num, direction="source") + + for num in sink_channels: + self._link_dppi_channels(addr, num, direction="sink") + + def _link_dppi_channels( + self, + dppic_addr: int, + channel_num: int, + direction: Literal["source"] | Literal["sink"], + ) -> None: + local_ppib_addr, local_ppib_ch_map = self._hw_tables.dppic_to_ppib_connections[dppic_addr] + local_ppib_ch = local_ppib_ch_map[channel_num] + + remote_ppib_addr, remote_ppib_ch_map = self._hw_tables.ppib_to_ppib_connections[ + local_ppib_addr + ] + remote_ppib_ch = remote_ppib_ch_map[local_ppib_ch] + + if direction == "source": + sub_ppib_addr, sub_ppib_ch = local_ppib_addr, local_ppib_ch + pub_ppib_addr, pub_ppib_ch = remote_ppib_addr, remote_ppib_ch + else: + sub_ppib_addr, sub_ppib_ch = remote_ppib_addr, remote_ppib_ch + pub_ppib_addr, pub_ppib_ch = local_ppib_addr, local_ppib_ch + + sub_ppib_name = self._hw_tables.ppib_addr_to_name[sub_ppib_addr] + pub_ppib_name = self._hw_tables.ppib_addr_to_name[pub_ppib_addr] + + self._macros.append( + MacroCall( + "UICR_PPIB_SUBSCRIBE_SEND_ENABLE", + [ + Address(sub_ppib_addr), + sub_ppib_ch, + ], + comment=( + f"SUB: {sub_ppib_name} ch. {sub_ppib_ch} => {pub_ppib_name} ch. {pub_ppib_ch}" + ), + ) + ) + self._macros.append( + MacroCall( + "UICR_PPIB_PUBLISH_RECEIVE_ENABLE", + [ + Address(pub_ppib_addr), + pub_ppib_ch, + ], + comment=( + f"PUB: {sub_ppib_name} ch. {sub_ppib_ch} => {pub_ppib_name} ch. {pub_ppib_ch}" + ), + ) + ) + + def _add_nrf_ipct_global_spu_permissions(self, node: Node) -> None: + """Adds SPU FEATURE.IPCT[n].CH[m] entries for configured channels + on the IPCT peripheral described by the node. + """ + addr = dt_reg_addr(node) + instance_name = dt_node_identifier(node) + spu_address = get_spu_addr_for_periph(addr) + spu_name = self._hw_tables.spu_addr_to_name[spu_address] + + for num, secure in dt_split_channels_get(node): + self._macros.append( + MacroCall( + "UICR_SPU_FEATURE_IPCT_CH_SET", + [ + Address(spu_address), + num, + secure, + self._owner_id.c_enum, + ], + comment=f"{spu_name}: {instance_name} ch. {num} permissions", + ) + ) + + def _add_nrf_ipct_ipcmap_channel_links(self, node: Node) -> None: + """Adds IPCMAP CHANNEL[n].SOURCE and CHANNEL[n].SINK entries for "sink" and "source" + channels on the IPCT peripheral described by the node. + """ + source_channel_links = dt_array_prop(node, "source-channel-links", []) + if len(source_channel_links) % 3 != 0: + raise BadDevicetreeError() + + sink_channel_links = dt_array_prop(node, "sink-channel-links", []) + if len(sink_channel_links) % 3 != 0: + raise BadDevicetreeError() + + node_domain_raw = dt_prop(node, "global-domain-id", None) + if node_domain_raw is None: + addr = dt_reg_addr(node) + try: + node_domain = Address(addr).domain + except ValueError as e: + raise BadDevicetreeError( + f"Failed to determine domain ID for address 0x{addr:08x} " + f"specified on node {node.path}" + ) from e + else: + node_domain = DomainId(node_domain_raw) + + for source_ch, sink_domain_raw, sink_ch in batched(source_channel_links, 3): + sink_domain = DomainId(sink_domain_raw) + self._link_ipct_channel(node_domain, source_ch, sink_domain, sink_ch) + + for sink_ch, source_domain_raw, source_ch in batched(sink_channel_links, 3): + source_domain = DomainId(source_domain_raw) + self._link_ipct_channel(source_domain, source_ch, node_domain, sink_ch) + + def _link_ipct_channel( + self, + source_domain: DomainId, + source_ch: int, + sink_domain: DomainId, + sink_ch: int, + ) -> None: + # Setting "source-channel-links" on one end and "sink-channel-links" on the other + # leads to duplicated entries without this check. + link_args = ( + source_domain.c_enum, + source_ch, + sink_domain.c_enum, + sink_ch, + ) + if link_args in self._generated_ipcmaps: + log.debug(f"Skip duplicate IPCMAP entry: {link_args}") + return + self._generated_ipcmaps.add(link_args) + + self._macros.append( + MacroCall( + "UICR_IPCMAP_CHANNEL_CFG", + [self._ipcmap_idx, *link_args], + comment=( + f"{source_domain.name} IPCT ch. {source_ch} => " + f"{sink_domain.name} IPCT ch. {sink_ch}" + ), + ) + ) + self._ipcmap_idx += 1 + + def _add_nrf_grtc_spu_permissions(self, node: Node) -> None: + """Adds SPU FEATURE.GRTC[n].CC[m] entries for configured channels + on the GRTC peripheral described by the node. + """ + grtc_addr = dt_reg_addr(node) + spu_address = get_spu_addr_for_periph(grtc_addr) + spu_name = self._hw_tables.spu_addr_to_name[spu_address] + + for num, secure in dt_split_channels_get(node): + self._macros.append( + MacroCall( + "UICR_SPU_FEATURE_GRTC_CC_SET", + [ + Address(spu_address), + num, + secure, + self._owner_id.c_enum, + ], + comment=f"{spu_name}: GRTC CC{num} permissions", + ) + ) + + def _add_peripheral_gpios_spu_permissions_and_ctrlsel( + self, + node: Node, + skip_ctrlsel: bool = False, + ) -> None: + """Adds SPU FEATURE.GPIO[n].PIN[m] and GPIO PIN_CNF[n] entries for + pins used in phandle-array properties called 'gpios' or ending in '-gpios' + found on the peripheral node. + """ + for name in node.props: + if not re.fullmatch(r"^(.+-)?gpios$", name): + continue + + if node.props[name].type != "phandle-array": + log.debug(f"skipping *-gpios prop {name} in {node.path} (not a phandle-array)") + continue + + for entry in dt_array_prop(node, name): + gpio_node = entry.controller + if "nordic,nrf-gpio" not in gpio_node.compats: + continue + + port = gpio_node.props["port"].val + num = entry.data["pin"] + secure = dt_node_is_secure(gpio_node) + if not skip_ctrlsel: + ctrlsel = self._hw_tables.lookup_ctrlsel_for_property( + node.props[name], (port, num) + ) + else: + ctrlsel = None + self._configure_gpio_pin(entry.controller, num, secure, ctrlsel) + + def _add_peripheral_pinctrls_spu_permissions_and_ctrlsel(self, node: Node) -> None: + """Adds SPU FEATURE.GPIO[n].PIN[m] and GPIO PIN_CNF[n] entries for + pins used in pinctrl properties referenced by the peripheral node. + """ + secure = dt_node_is_secure(node) + for pinctrl in node.pinctrls: + for config_node in pinctrl.conf_nodes: + for group_node in config_node.children.values(): + for i, psel_val in enumerate(dt_array_prop(group_node, "psels")): + psel = NrfPsel.from_raw(psel_val) + + if psel.is_disconnected(): + # Pin is unused and should be ignored + continue + + gpio_node = find_gpio_node_by_port( + node.edt, + psel.port, + err_suffix=f" (referenced by {group_node.path}:psels[{i}])", + ) + num = psel.pin + ctrlsel = self._hw_tables.lookup_ctrlsel_for_pinctrl(pinctrl, psel) + self._configure_gpio_pin(gpio_node, num, secure, ctrlsel) + + def _add_nrf_saadc_channel_pin_spu_permissions(self, node: Node) -> None: + """Adds SPU FEATURE.GPIO[n].PIN[m] and GPIO PIN_CNF[n] entries for + pins corresponding to the channel properties on the ADC peripheral node. + """ + saadc_addr = dt_reg_addr(node, secure=True) + if saadc_addr not in self._hw_tables.adc_channel_pin_lookup: + return + + channel_pins = self._hw_tables.adc_channel_pin_lookup[saadc_addr] + secure = dt_node_is_secure(node) + + for name, child in node.children.items(): + if not name.startswith("channel"): + continue + + for prop_name in ["zephyr,input-positive", "zephyr,input-negative"]: + prop_val = dt_prop(child, prop_name, None) + if prop_val in channel_pins: + port, num = channel_pins[prop_val] + gpio_node = find_gpio_node_by_port(node.edt, port) + self._configure_gpio_pin(gpio_node, num, secure, CTRLSEL_DEFAULT) + + def _add_nrf_comp_lpcomp_channel_pin_spu_permissions(self, node: Node) -> None: + """Adds SPU FEATURE.GPIO[n].PIN[m] and GPIO PIN_CNF[n] entries for + pins corresponding to the channel properties on the COMP/LPCOMP peripheral node. + """ + comp_addr = dt_reg_addr(node, secure=True) + if comp_addr not in self._hw_tables.comp_channel_pin_lookup: + return + + channel_pins = self._hw_tables.comp_channel_pin_lookup[comp_addr] + secure = dt_node_is_secure(node) + + for prop_name in ["psel", "extrefsel"]: + prop_val = dt_prop(node, prop_name, None) + if prop_val in channel_pins: + port, num = channel_pins[prop_val] + gpio_node = find_gpio_node_by_port(node.edt, port) + self._configure_gpio_pin(gpio_node, num, secure, CTRLSEL_DEFAULT) + + def _configure_gpio_pin( + self, + gpio_node: Node, + num: int, + secure: bool, + ctrlsel: int | None, + ) -> None: + gpio_addr = dt_reg_addr(gpio_node) + gpio_port = dt_prop(gpio_node, "port") + spu_address = get_spu_addr_for_periph(gpio_addr) + spu_name = self._hw_tables.spu_addr_to_name[spu_address] + + self._macros.append( + MacroCall( + "UICR_SPU_FEATURE_GPIO_PIN_SET", + [ + Address(spu_address), + gpio_port, + num, + secure, + self._owner_id.c_enum, + ], + comment=f"{spu_name}: P{gpio_port}.{num} permissions", + ) + ) + + if ctrlsel is not None: + ctrlsel_int = int(ctrlsel) + self._macros.append( + MacroCall( + "UICR_GPIO_PIN_CNF_CTRLSEL_SET", + [ + Address(gpio_addr), + num, + ctrlsel_int, + ], + comment=f"P{gpio_port}.{num} CTRLSEL = {ctrlsel_int}", + ) + ) + + +def find_gpio_node_by_port(dt: EDT, port: int, err_suffix: str = "") -> Node: + """Find the GPIO node in the devicetree with the given port.""" + for gpio_node in dt.compat2nodes["nordic,nrf-gpio"]: + if gpio_node.props["port"].val == port: + return gpio_node + raise BadDevicetreeError(f"Failed to find Nordic GPIO node with port {port}{err_suffix}") + + +@dataclass(order=True) +class MacroCall: + name: str + args: list + comment: str | None = None + + def c_render(self, nodelabel_lookup: dict[int, str]) -> str: + """Render the macro as C code""" + str_args = [] + for arg in self.args: + if isinstance(arg, Address): + if int(arg) in nodelabel_lookup: + str_args.append(c_dt_reg_addr_by_label(nodelabel_lookup[int(arg)])) + else: + str_args.append(c_hex_addr(int(arg))) + elif isinstance(arg, bool): + str_args.append(c_bool(arg)) + elif hasattr(arg, "c_enum"): + str_args.append(arg.c_enum) + else: + str_args.append(str(arg)) + + comment = f"/* {self.comment} */\n" if self.comment else "" + return f"{comment}{self.name}({', '.join(str_args)});" + + +def c_hex_addr(address: int) -> str: + """Format address as a C 32-bit hex literal.""" + return f"0x{address:08x}UL" + + +def c_bool(value: bool) -> str: + """Format value as a C bool literal.""" + return "true" if value else "false" + + +def c_dt_reg_addr_by_label(nodelabel: str) -> str: + """Format a peripheral address using devicetree macros to get the address based on node label""" + return f"DT_REG_ADDR(DT_NODELABEL({nodelabel}))" + + +# Equivalent to itertools.batched(), using the recipe provided in the docs. +def batched(iterable: Iterable, n: int, *, strict: bool = False) -> Iterator: + if n < 1: + raise ValueError("n must be at least one") + iterator = iter(iterable) + while batch := tuple(islice(iterator, n)): + if strict and len(batch) != n: + raise ValueError("batched(): incomplete batch") + yield batch + + +# Sentinel used to represent no provided default value in the functions below +class NoDefault: ... + + +NO_DEFAULT = NoDefault + + +def dt_reg_addr( + node: Node, + index: int = 0, + secure: bool = False, + default: int | type[NoDefault] = NO_DEFAULT, +) -> int: + """Get a register address and property identifier for a node.""" + try: + addr = node.regs[index].addr + except IndexError: + if isinstance(default, int): + addr = default + else: + raise + if secure: + return int(Address(addr).as_secure()) + return addr + + +def dt_reg_size(node: Node, index: int = 0) -> int: + """Get a register size and property identifier for a node.""" + return node.regs[index].size + + +def dt_prop(node: Node, name: str, default: Any = NO_DEFAULT) -> Any: + """Get the property value and identfier of a property. + Optionally returns a default value. + """ + try: + prop = node.props[name] + except KeyError: + if default != NO_DEFAULT: + return default + raise + + return prop.val + + +def dt_node_identifier(node: Node, default_to_path: bool = True) -> str: + """Get a string that identifies the node. + Returns the first nodelabel if it exists, otherwise defaults to the node path. + If default_to_path is False, exits with an error instead of defaulting. + """ + if node.labels: + return node.labels[0] + elif default_to_path: + return node.path + raise BuilderError(f"Expected a nodelabel on {node}, but the node has no label") + + +def dt_array_prop( + node: Node, + name: str, + default: list[Any] | type[NO_DEFAULT] = NO_DEFAULT, +) -> list[Any]: + """Get the member values and identifiers of an array property. + Optionally returns a default value. + """ + try: + prop = node.props[name] + except KeyError: + if not isinstance(default, type): + return list(default) + raise + + return list(prop.val) + + +def dt_node_processors_from_labels(node: Node) -> list[ProcessorId]: + """Deduce a processor ID from a list of devicetree nodelabels.""" + substring_processor = {cpu.zephyr_name: cpu for cpu in ProcessorId.__members__.values()} + processors = set() + for substring, processor_id in substring_processor.items(): + if any(substring in label for label in node.labels): + processors.add(processor_id) + return list(processors) + + +def dt_split_channels_get( + node: Node, + owned_name: str = "owned-channels", + nonsecure_name: str = "nonsecure-channels", +) -> list[tuple[int, bool]]: + """Parse 'split channels' properties.""" + owned = [] + owned.extend(dt_array_prop(node, owned_name, default=[])) + owned.extend(dt_array_prop(node, f"child-{owned_name}", default=[])) + + sec_lookup = {} + if nonsecure_name in node.props: + nonsecure = dt_array_prop(node, nonsecure_name) + sec_lookup.update(dict.fromkeys(nonsecure, False)) + + default_sec = dt_node_is_secure(node) + channels = [] + for ch in owned: + sec = sec_lookup.setdefault(ch, default_sec) + channels.append((ch, sec)) + + return channels + + +def dt_node_is_secure(node: Node) -> bool: + if node.bus_node is not None and node.bus_node.regs: + addr = dt_reg_addr(node.bus_node) + elif node.regs: + addr = dt_reg_addr(node) + else: + raise ValueError( + f"Failed to determine security of {node.path} " + "from the address of its bus node or itself" + ) + return Address(addr).security + + +def dt_processor_id(devicetree: EDT) -> ProcessorId: + """Get processor information from a domain's devicetree.""" + cpus = [ + node + for node in devicetree.get_node("/cpus").children.values() + if node.name.startswith("cpu@") + ] + if len(cpus) != 1: + raise RuntimeError( + f"Expected exactly 1 'cpu' node, but devicetree contained {len(cpus)} nodes" + ) + + try: + return ProcessorId(cpus[0].regs[0].addr) + except Exception as e: + raise RuntimeError( + f"Devicetree 'cpu' node has invalid Processor ID {cpus[0].regs[0].addr}" + ) from e + + +@dataclass(frozen=True) +class NrfPsel: + """Decoded NRF_PSEL values.""" + + fun: NrfFun + port: int + pin: int + + @classmethod + def from_raw(cls, psel_value: int) -> NrfPsel: + """Decode a raw NRF_PSEL encoded int value to its individual parts.""" + port, pin = divmod(psel_value & (~NRF_PSEL_FUN_MASK), NRF_PSEL_GPIO_PIN_COUNT) + fun = (psel_value & NRF_PSEL_FUN_MASK) >> NRF_PSEL_FUN_POS + return NrfPsel(fun=NrfFun(fun), port=port, pin=pin) + + def is_disconnected(self) -> bool: + """True if the value represents a disconnected pin""" + return (self.port * NRF_PSEL_GPIO_PIN_COUNT + self.pin) == NRF_PSEL_PIN_MASK + + +# # Bit position of the function bits in the pinctrl pin value encoded from NRF_PSEL() +NRF_PSEL_FUN_POS = 24 +# # Mask for the function bits in the pinctrl pin value encoded from NRF_PSEL() +NRF_PSEL_FUN_MASK = 0xFF << NRF_PSEL_FUN_POS +# Number of pins per port used in NRF_PSEL() +NRF_PSEL_GPIO_PIN_COUNT = 32 +# Mask for the port, pin bits in the pinctrl pin value encoded from NRF_PSEL() +NRF_PSEL_PIN_MASK = 0x1FF + + +@dataclass(frozen=True) +class GpiosProp: + """CTRLSEL lookup table entry for special *-gpios properties used in some peripheral + bindings, which are in some cases used instead of pinctrl. + """ + + name: str + port: int + pin: int + + +@enum.unique +class Ctrlsel(int, enum.Enum): + """ + Enumeration of GPIO.PIN_CNF[n].CTRLSEL values. + The list here may not be exhaustive. + """ + + GPIO = 0 + VPR_GRC = 1 + CAN_PWM_I3C = 2 + SERIAL0 = 3 + EXMIF_RADIO_SERIAL1 = 4 + CAN_TDM_SERIAL2 = 5 + CAN = 6 + TND = 7 + + +# Default CTRLSEL value indicating that CTRLSEL should not be used +CTRLSEL_DEFAULT = Ctrlsel.GPIO + + +class NrfFun(int, enum.Enum): + """Pin functions used with pinctrl, see include/zephyr/dt-bindings/pinctrl/nrf-pinctrl.h + Only the functions relevant for CTRLSEL deduction have been included. + """ + + UART_TX = 0 + UART_RX = 1 + UART_RTS = 2 + UART_CTS = 3 + SPIM_SCK = 4 + SPIM_MOSI = 5 + SPIM_MISO = 6 + SPIS_SCK = 7 + SPIS_MOSI = 8 + SPIS_MISO = 9 + SPIS_CSN = 10 + TWIM_SCL = 11 + TWIM_SDA = 12 + PWM_OUT0 = 22 + PWM_OUT1 = 23 + PWM_OUT2 = 24 + PWM_OUT3 = 25 + EXMIF_CK = 35 + EXMIF_DQ0 = 36 + EXMIF_DQ1 = 37 + EXMIF_DQ2 = 38 + EXMIF_DQ3 = 39 + EXMIF_DQ4 = 40 + EXMIF_DQ5 = 41 + EXMIF_DQ6 = 42 + EXMIF_DQ7 = 43 + EXMIF_CS0 = 44 + EXMIF_CS1 = 45 + CAN_TX = 46 + CAN_RX = 47 + TWIS_SCL = 48 + TWIS_SDA = 49 + EXMIF_RWDS = 50 + GRTC_CLKOUT_FAST = 55 + GRTC_CLKOUT_32K = 56 + TDM_SCK_M = 71 + TDM_SCK_S = 72 + TDM_FSYNC_M = 73 + TDM_FSYNC_S = 74 + TDM_SDIN = 75 + TDM_SDOUT = 76 + TDM_MCK = 77 + + # Value used to ignore the function field and only check (port, pin) + IGNORE = -1 + # Fallback for unknown NrfFun values, assumes that pin CTRLSEL should be set to GPIO + ASSUMED_GPIO = -2 + + @classmethod + def _missing_(cls, value: Any) -> NrfFun: + return cls.ASSUMED_GPIO + + +class NrfSaadcChannel(int, enum.Enum): + """Identifiers representing SAADC channels. + See include/zephyr/dt-bindings/adc/nrf-saadc-haltium.h. + """ + + AIN0 = 1 + AIN1 = 2 + AIN2 = 3 + AIN3 = 4 + AIN4 = 5 + AIN5 = 6 + AIN6 = 7 + AIN7 = 8 + AIN8 = 9 + AIN9 = 10 + AIN10 = 11 + AIN11 = 12 + AIN12 = 13 + AIN13 = 14 + + +class NrfCompChannel(str, enum.Enum): + """Identifiers representing COMP/LPCOMP channels. + See dts/bindings/comparator/nordic,nrf-comp.yaml. + """ + + AIN0 = "AIN0" + AIN1 = "AIN1" + AIN2 = "AIN2" + AIN3 = "AIN3" + AIN4 = "AIN4" + AIN5 = "AIN5" + AIN6 = "AIN6" + AIN7 = "AIN7" + AIN8 = "AIN8" + AIN9 = "AIN9" + + +class FixedPPIMap(NamedTuple): + """A DPPIC-PPIB or PPIB-PPIB channel mapping. + These connections are fixed in the hardware. + """ + + connected_to: int + channel_map: range + + +@dataclass +class SocLookupTables: + """Container for various hardware info needed to generate PERIPHCONF.""" + + ctrlsel_lookup: dict[int, dict[NrfPsel | GpiosProp, Ctrlsel]] + adc_channel_pin_lookup: dict[int, dict[NrfSaadcChannel, tuple[int, int]]] + comp_channel_pin_lookup: dict[int, dict[NrfCompChannel, tuple[int, int]]] + dppic_to_ppib_connections: dict[int, FixedPPIMap] + ppib_to_ppib_connections: dict[int, FixedPPIMap] + spu_instances: list[tuple[str, int]] + ppib_instances: list[tuple[str, int]] + + @cached_property + def spu_addr_to_name(self) -> Mapping[int, str]: + return {addr: name for name, addr in self.spu_instances} + + @cached_property + def ppib_addr_to_name(self) -> Mapping[int, str]: + return {addr: name for name, addr in self.ppib_instances} + + def lookup_ctrlsel_for_property(self, prop: Property, psel: tuple[int, int]) -> Ctrlsel | None: + """Find the appopriate CTRLSEL value for a given gpios property.""" + if not prop.node.regs: + # Only nodes with registers can be looked up + return None + + periph_addr = dt_reg_addr(prop.node, secure=True) + gpios_prop = GpiosProp(name=prop.name, port=psel[0], pin=psel[1]) + return self._lookup_ctrlsel(periph_addr, gpios_prop) + + def lookup_ctrlsel_for_pinctrl(self, prop: PinCtrl, psel: NrfPsel) -> Ctrlsel | None: + """Find the appopriate CTRLSEL value for a given pinctrl.""" + if psel.fun == NrfFun.ASSUMED_GPIO: + # We map unsupported values to GPIO CTRLSEL + return CTRLSEL_DEFAULT + + periph_addr = dt_reg_addr(prop.node, secure=True) + return self._lookup_ctrlsel(periph_addr, psel) + + def _lookup_ctrlsel( + self, + periph_addr: int, + prop_or_psel: NrfPsel | GpiosProp, + ) -> Ctrlsel | None: + ctrlsel = None + + if periph_addr in self.ctrlsel_lookup: + ident_lut = self.ctrlsel_lookup[periph_addr] + if prop_or_psel in ident_lut: + ctrlsel = ident_lut[prop_or_psel] + elif isinstance(prop_or_psel, NrfPsel): + # Check if this entry is enumerated with "ignored" function + sub_entry_no_fun = NrfPsel( + fun=NrfFun.IGNORE, port=prop_or_psel.port, pin=prop_or_psel.pin + ) + ctrlsel = ident_lut.get(sub_entry_no_fun, None) + + log.debug(f"periph_addr=0x{periph_addr:09_x}, {prop_or_psel=} -> {ctrlsel=}") + + return ctrlsel + + +def get_spu_addr_for_periph(periph_addr: int) -> int: + """Get the address of the SPU instance governing the permissions for the peripheral + at the given address. + """ + address = Address(periph_addr) + + # The common rule is that the SPU instance sits at the start of the address space for + # the bus of the peripheral. + address.security = True + address.slave_index = 0 + address.address_space = 0 + + # However, some buses are special due to having > 16 slaves and need special handling. + address.bus = SPU_ADDRESS_BUS_REMAPPING.get(address.bus, address.bus) + + return int(address) + + +SPU_ADDRESS_BUS_REMAPPING = { + # Both of these bus IDs represent APB32 and should use the same SPU instance with bus ID 146. + 147: 146, +} + + +@enum.unique +class DomainId(enum.IntEnum): + """Domain IDs.""" + + RESERVED = 0 + SECURE = 1 + APPLICATION = 2 + RADIOCORE = 3 + CELLCORE = 4 + GLOBALFAST = 12 + GLOBALSLOW = 13 + GLOBAL_ = 14 + GLOBAL = 15 + + @property + def c_enum(self) -> str: + return f"NRF_DOMAIN_{self.name.upper()}" + + +@enum.unique +class OwnerId(enum.IntEnum): + """Owner IDs.""" + + NONE = 0 + SECURE = 1 + APPLICATION = 2 + RADIOCORE = 3 + CELL = 4 + SYSCTRL = 8 + + @property + def c_enum(self) -> str: + return f"NRF_OWNER_{self.name.upper()}" + + +@enum.unique +class ProcessorId(enum.IntEnum): + """Processor IDs.""" + + SECURE = 1 + APPLICATION = 2 + RADIOCORE = 3 + CELLCORE = 4 + SYSCTRL = 12 + PPR = 13 + FLPR = 14 + + @property + def zephyr_name(self) -> str: + """Name used in zephyr to denote the processor with this ID.""" + match self: + case ProcessorId.SECURE: + return "cpusec" + case ProcessorId.APPLICATION: + return "cpuapp" + case ProcessorId.RADIOCORE: + return "cpurad" + case ProcessorId.CELLCORE: + return "cpucell" + case ProcessorId.SYSCTRL: + return "cpusys" + case ProcessorId.PPR: + return "cpuppr" + case ProcessorId.FLPR: + return "cpuflpr" + + @property + def default_owner_id(self) -> OwnerId: + """Default owner ID associated with this ID (the ID used by accesses from the processor).""" + match self: + case ProcessorId.SECURE: + return OwnerId.SECURE + case ProcessorId.APPLICATION: + return OwnerId.APPLICATION + case ProcessorId.RADIOCORE: + return OwnerId.RADIOCORE + case ProcessorId.CELLCORE: + return OwnerId.CELL + case ProcessorId.SYSCTRL: + return OwnerId.SYSCTRL + case ProcessorId.PPR: + return OwnerId.APPLICATION + case ProcessorId.FLPR: + return OwnerId.APPLICATION + + @property + def c_enum(self) -> str: + return f"NRF_PROCESSOR_{self.name.upper()}" + + +@enum.unique +class AddressRegion(enum.IntEnum): + """Address regions, defined by Address Format of the data sheet.""" + + PROGRAM = 0 + DATA = 1 + PERIPHERAL = 2 + EXT_XIP = 3 + EXT_XIP_ENCRYPTED = 4 + STM = 5 + CPU = 7 + + @classmethod + def from_address(cls, address: int) -> AddressRegion: + """Get the address region of an address.""" + return Address(address).region + + +# Regions that have domain ID and security fields +HAS_DOMAIN_SECURITY = [ + AddressRegion.PROGRAM, + AddressRegion.DATA, + AddressRegion.PERIPHERAL, + AddressRegion.STM, +] + +# Regions that have the peripheral address format +HAS_PERIPH_BITS = [ + AddressRegion.PERIPHERAL, + AddressRegion.STM, +] + + +class Address: + """Helper for working with addresses.""" + + def __init__(self, value: int | Address = 0) -> None: + self._val = int(value) + + def __repr__(self) -> str: + if self.region in HAS_DOMAIN_SECURITY: + domain_sec_str = ( + f", domain={self.domain.name} ({int(self.domain)}), security={self.security}" + ) + else: + domain_sec_str = "" + + if self.region in HAS_PERIPH_BITS: + periph_bits_str = ( + f", bus={self.bus} (0b{self.bus:09_b}), " + f"slave_index={self.slave_index} (0b{self.slave_index:09_b})" + ) + else: + periph_bits_str = "" + + field_str = ( + f"region={self.region.name} ({int(self.region)}){domain_sec_str}{periph_bits_str}, " + f"address_space=0x{self.address_space:_x}" + ) + + return f"{type(self).__name__}({field_str})" + + def __str__(self) -> str: + return repr(self) + + def as_secure(self) -> Address: + addr = Address(self) + addr.security = True + return addr + + @property + def region(self) -> AddressRegion: + """Address region.""" + return AddressRegion(get_field(self._val, ADDRESS_REGION_POS, ADDRESS_REGION_MASK)) + + @region.setter + def region(self, new: int) -> None: + self._val = update_field(self._val, new, ADDRESS_REGION_POS, ADDRESS_REGION_MASK) + + @property + def security(self) -> bool: + """Address security (only present in some regions).""" + self._check_has_security() + return bool(get_field(self._val, ADDRESS_SECURITY_POS, ADDRESS_SECURITY_MASK)) + + @security.setter + def security(self, new: bool) -> None: + self._check_has_security() + self._val = update_field(self._val, int(new), ADDRESS_SECURITY_POS, ADDRESS_SECURITY_MASK) + + def _check_has_security(self) -> None: + self._check_region_has_field(HAS_DOMAIN_SECURITY, "security bit") + + @property + def domain(self) -> DomainId: + """Address domain ID (only present in some regions).""" + self._check_has_domain_id() + return DomainId(get_field(self._val, ADDRESS_DOMAIN_POS, ADDRESS_DOMAIN_MASK)) + + @domain.setter + def domain(self, new: DomainId | int) -> None: + self._check_has_domain_id() + self._val = update_field(self._val, new, ADDRESS_DOMAIN_POS, ADDRESS_DOMAIN_MASK) + + def _check_has_domain_id(self) -> None: + self._check_region_has_field(HAS_DOMAIN_SECURITY, "domain ID") + + @property + def bus(self) -> int: + """Bus ID (only present in some regions).""" + self._check_has_bus() + return get_field(self._val, ADDRESS_BUS_POS, ADDRESS_BUS_MASK) + + @bus.setter + def bus(self, new: int) -> None: + self._check_has_bus() + self._val = update_field(self._val, new, ADDRESS_BUS_POS, ADDRESS_BUS_MASK) + + def _check_has_bus(self) -> None: + self._check_region_has_field(HAS_PERIPH_BITS, "Peripheral/APB bus number") + + @property + def slave_index(self) -> int: + """Slave index (only present in some regions).""" + self._check_has_slave_index() + return get_field(self._val, ADDRESS_SLAVE_POS, ADDRESS_SLAVE_MASK) + + @slave_index.setter + def slave_index(self, new: int) -> None: + self._check_has_slave_index() + self._val = update_field(self._val, new, ADDRESS_SLAVE_POS, ADDRESS_SLAVE_MASK) + + def _check_has_slave_index(self) -> None: + self._check_region_has_field(HAS_PERIPH_BITS, "Peripheral/APB slave index") + + @property + def address_space(self) -> int: + """Internal address space address (semantics depend on the region).""" + match self.region: + case AddressRegion.PROGRAM | AddressRegion.DATA: + return get_field(self._val, ADDRESS_SPACE_POS, ADDRESS_PROGRAM_DATA_SPACE_MASK) + case AddressRegion.PERIPHERAL | AddressRegion.STM: + return get_field(self._val, ADDRESS_SPACE_POS, ADDRESS_PERIPHERAL_SPACE_MASK) + case _: + return get_field(self._val, ADDRESS_SPACE_POS, ADDRESS_DEFAULT_SPACE_MASK) + + @address_space.setter + def address_space(self, new: int) -> None: + match self.region: + case AddressRegion.PROGRAM | AddressRegion.DATA: + self._val = update_field( + self._val, new, ADDRESS_SPACE_POS, ADDRESS_PROGRAM_DATA_SPACE_MASK + ) + case AddressRegion.PERIPHERAL | AddressRegion.STM: + self._val = update_field( + self._val, new, ADDRESS_SPACE_POS, ADDRESS_PERIPHERAL_SPACE_MASK + ) + case _: + self._val = update_field( + self._val, new, ADDRESS_SPACE_POS, ADDRESS_DEFAULT_SPACE_MASK + ) + + def _check_region_has_field(self, valid_regions: list[AddressRegion], field_name: str) -> None: + if self.region not in valid_regions: + raise ValueError(f"{field_name} is not defined for address region {self.region.name}") + + def __eq__(self, other) -> bool: + return int(self) == int(other) + + def __lt__(self, other: Address | int) -> bool: + return int(self) < int(other) + + def __int__(self) -> int: + return self._val + + +ADDRESS_REGION_POS = 29 +ADDRESS_REGION_MASK = 0x7 << ADDRESS_REGION_POS +ADDRESS_SECURITY_POS = 28 +ADDRESS_SECURITY_MASK = 0x1 << ADDRESS_SECURITY_POS +ADDRESS_DOMAIN_POS = 24 +ADDRESS_DOMAIN_MASK = 0xF << ADDRESS_DOMAIN_POS +ADDRESS_BUS_POS = 16 +ADDRESS_BUS_MASK = 0xFF << ADDRESS_BUS_POS +ADDRESS_SLAVE_POS = 12 +ADDRESS_SLAVE_MASK = 0xF << ADDRESS_SLAVE_POS +ADDRESS_PERIPHID_POS = 12 +ADDRESS_PERIPHID_MASK = 0x7FF << ADDRESS_PERIPHID_POS +ADDRESS_SPACE_POS = 0 +ADDRESS_PROGRAM_DATA_SPACE_MASK = 0xFF_FFFF +ADDRESS_PERIPHERAL_SPACE_MASK = 0xFFF +ADDRESS_DEFAULT_SPACE_MASK = 0x1FFF_FFFF + + +def peripheral_id_get(periph_address: int) -> int: + """Get the peripheral ID of a peripheral address.""" + return get_field(periph_address, ADDRESS_PERIPHID_POS, ADDRESS_PERIPHID_MASK) + + +def get_field(value: int, field_pos: int, field_mask: int) -> int: + """Get the value of a field in a bitfield.""" + return (value & field_mask) >> field_pos + + +def update_field(value: int, field_new: int, field_pos: int, field_mask: int) -> int: + """Update a field in a bitfield.""" + return (value & ~field_mask) | ((field_new << field_pos) & field_mask) diff --git a/soc/nordic/common/uicr/sysbuild.cmake b/soc/nordic/common/uicr/sysbuild.cmake new file mode 100644 index 00000000000..9cafd261036 --- /dev/null +++ b/soc/nordic/common/uicr/sysbuild.cmake @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Add UICR generator as a utility image +ExternalZephyrProject_Add( + APPLICATION uicr + SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/gen_uicr +) + +# Ensure UICR is configured and built after the default image so EDT/ELFs exist. +sysbuild_add_dependencies(CONFIGURE uicr ${DEFAULT_IMAGE}) + +add_dependencies(uicr ${DEFAULT_IMAGE}) +if(DEFINED image) + add_dependencies(uicr ${image}) +endif() diff --git a/soc/nordic/ironside/CMakeLists.txt b/soc/nordic/ironside/CMakeLists.txt index 98e721541d7..ea7f68207a8 100644 --- a/soc/nordic/ironside/CMakeLists.txt +++ b/soc/nordic/ironside/CMakeLists.txt @@ -6,6 +6,7 @@ zephyr_library() zephyr_library_sources_ifdef(CONFIG_NRF_IRONSIDE_CALL call.c) zephyr_library_sources_ifdef(CONFIG_NRF_IRONSIDE_BOOT_REPORT boot_report.c) zephyr_library_sources_ifdef(CONFIG_NRF_IRONSIDE_CPUCONF_SERVICE cpuconf.c) +zephyr_library_sources_ifdef(CONFIG_NRF_IRONSIDE_BOOTMODE_SERVICE bootmode.c) zephyr_library_sources_ifdef(CONFIG_NRF_IRONSIDE_TDD_SERVICE tdd.c) zephyr_library_sources_ifdef(CONFIG_NRF_IRONSIDE_UPDATE_SERVICE update.c) zephyr_library_sources_ifdef(CONFIG_NRF_IRONSIDE_DVFS_SERVICE dvfs.c) diff --git a/soc/nordic/ironside/Kconfig b/soc/nordic/ironside/Kconfig index 3136e3bd5f4..ce4878b7b18 100644 --- a/soc/nordic/ironside/Kconfig +++ b/soc/nordic/ironside/Kconfig @@ -56,6 +56,12 @@ config NRF_IRONSIDE_BOOT_REPORT help Support for parsing the Boot Report populated by Nordic IronSide firmware. +config NRF_IRONSIDE_BOOTMODE_SERVICE + bool "IronSide boot mode service" + select NRF_IRONSIDE_CALL + help + Service used to reboot into secondary firmware boot mode. + config NRF_IRONSIDE_DVFS_SERVICE bool "IronSide DVFS service" depends on SOC_NRF54H20_CPUAPP diff --git a/soc/nordic/ironside/bootmode.c b/soc/nordic/ironside/bootmode.c new file mode 100644 index 00000000000..92ca5e31265 --- /dev/null +++ b/soc/nordic/ironside/bootmode.c @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include + +#define BOOT_MODE_SECONDARY (0x1) + +BUILD_ASSERT(IRONSIDE_BOOTMODE_SERVICE_NUM_ARGS <= NRF_IRONSIDE_CALL_NUM_ARGS); + +int ironside_bootmode_secondary_reboot(const uint8_t *msg, size_t msg_size) +{ + int err; + struct ironside_call_buf *buf; + uint8_t *buf_msg; + + if (msg_size > IRONSIDE_BOOTMODE_SERVICE_MSG_MAX_SIZE) { + return -IRONSIDE_BOOTMODE_ERROR_MESSAGE_TOO_LARGE; + } + + buf = ironside_call_alloc(); + + buf->id = IRONSIDE_CALL_ID_BOOTMODE_SERVICE_V1; + + buf->args[IRONSIDE_BOOTMODE_SERVICE_MODE_IDX] = BOOT_MODE_SECONDARY; + + buf_msg = (uint8_t *)&buf->args[IRONSIDE_BOOTMODE_SERVICE_MSG_0_IDX]; + + memset(buf_msg, 0, IRONSIDE_BOOTMODE_SERVICE_MSG_MAX_SIZE); + + if (msg_size > 0) { + memcpy(buf_msg, msg, msg_size); + } + + ironside_call_dispatch(buf); + + if (buf->status == IRONSIDE_CALL_STATUS_RSP_SUCCESS) { + err = buf->args[IRONSIDE_BOOTMODE_SERVICE_RETCODE_IDX]; + } else { + err = buf->status; + } + + ironside_call_release(buf); + + return err; +} diff --git a/soc/nordic/ironside/include/nrf_ironside/bootmode.h b/soc/nordic/ironside/include/nrf_ironside/bootmode.h new file mode 100644 index 00000000000..4da46f33dd6 --- /dev/null +++ b/soc/nordic/ironside/include/nrf_ironside/bootmode.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_SOC_NORDIC_IRONSIDE_INCLUDE_NRF_IRONSIDE_BOOTMODE_H_ +#define ZEPHYR_SOC_NORDIC_IRONSIDE_INCLUDE_NRF_IRONSIDE_BOOTMODE_H_ + +#include +#include +#include + +/** + * @name Boot mode service error codes. + * @{ + */ + +/** Invalid/unsupported boot mode transition. */ +#define IRONSIDE_BOOTMODE_ERROR_UNSUPPORTED_MODE (1) +/** Failed to reboot into the boot mode due to other activity preventing a reset. */ +#define IRONSIDE_BOOTMODE_ERROR_BUSY (2) +/** The boot message is too large to fit in the buffer. */ +#define IRONSIDE_BOOTMODE_ERROR_MESSAGE_TOO_LARGE (3) + +/** + * @} + */ + +/* IronSide call identifiers with implicit versions. */ +#define IRONSIDE_CALL_ID_BOOTMODE_SERVICE_V1 5 + +enum { + IRONSIDE_BOOTMODE_SERVICE_MODE_IDX, + IRONSIDE_BOOTMODE_SERVICE_MSG_0_IDX, + IRONSIDE_BOOTMODE_SERVICE_MSG_1_IDX, + IRONSIDE_BOOTMODE_SERVICE_MSG_2_IDX, + IRONSIDE_BOOTMODE_SERVICE_MSG_3_IDX, + /* The last enum value is reserved for the number of arguments */ + IRONSIDE_BOOTMODE_SERVICE_NUM_ARGS, +}; + +/* Maximum size of the message parameter. */ +#define IRONSIDE_BOOTMODE_SERVICE_MSG_MAX_SIZE (4 * sizeof(uint32_t)) + +/* Index of the return code within the service buffer. */ +#define IRONSIDE_BOOTMODE_SERVICE_RETCODE_IDX (0) + +/** + * @brief Request a reboot into the secondary firmware boot mode. + * + * This invokes the IronSide SE boot mode service to restart the system into the secondary boot + * mode. In this mode, the secondary configuration defined in UICR is applied instead of the + * primary one. The system immediately reboots without a reply if the request succeeds. + * + * The given message data is passed to the boot report of the CPU booted in the secondary boot mode. + * + * @note This function does not return if the request is successful. + * @note The device will boot into the secondary firmware instead of primary firmware. + * @note The request does not fail if the secondary firmware is not defined. + * + * @param msg A message that can be placed in the cpu's boot report. + * @param msg_size Size of the message in bytes. + * + * @retval 0 on success. + * @retval -IRONSIDE_BOOTMODE_ERROR_UNSUPPORTED_MODE if the secondary boot mode is unsupported. + * @retval -IRONSIDE_BOOTMODE_ERROR_BUSY if the reboot was blocked. + * @retval -IRONSIDE_BOOTMODE_ERROR_MESSAGE_TOO_LARGE if msg_size is greater than + * IRONSIDE_BOOTMODE_SERVICE_MSG_MAX_SIZE. + * @retval Positive error status if reported by IronSide call (see error codes in @ref call.h). + */ +int ironside_bootmode_secondary_reboot(const uint8_t *msg, size_t msg_size); + +#endif /* ZEPHYR_SOC_NORDIC_IRONSIDE_INCLUDE_NRF_IRONSIDE_BOOTMODE_H_ */ diff --git a/soc/nordic/ironside/include/nrf_ironside/cpuconf.h b/soc/nordic/ironside/include/nrf_ironside/cpuconf.h index 49e562b1369..b112396704a 100644 --- a/soc/nordic/ironside/include/nrf_ironside/cpuconf.h +++ b/soc/nordic/ironside/include/nrf_ironside/cpuconf.h @@ -63,10 +63,10 @@ BUILD_ASSERT(IRONSIDE_CPUCONF_NUM_ARGS <= NRF_IRONSIDE_CALL_NUM_ARGS); * If the given msg_size is less than that, the remaining bytes are set to zero. * * @retval 0 on success or if the CPU has already booted. - * @retval Positive non-0 error status if reported by IronSide call. - * @retval -IRONSIDE_CPUCONF_ERROR_WRONG_CPU if cpu is unrecognized + * @retval -IRONSIDE_CPUCONF_ERROR_WRONG_CPU if cpu is unrecognized. * @retval -IRONSIDE_CPUCONF_ERROR_MESSAGE_TOO_LARGE if msg_size is greater than * IRONSIDE_CPUCONF_SERVICE_MSG_MAX_SIZE. + * @retval Positive error status if reported by IronSide call (see error codes in @ref call.h). */ int ironside_cpuconf(NRF_PROCESSORID_Type cpu, const void *vector_table, bool cpu_wait, const uint8_t *msg, size_t msg_size); diff --git a/soc/nordic/ironside/include/nrf_ironside/dvfs.h b/soc/nordic/ironside/include/nrf_ironside/dvfs.h index f48c8a6344a..c47cc43a859 100644 --- a/soc/nordic/ironside/include/nrf_ironside/dvfs.h +++ b/soc/nordic/ironside/include/nrf_ironside/dvfs.h @@ -68,7 +68,19 @@ enum ironside_dvfs_oppoint { * specified value. It will block until the change is applied. * * @param dvfs_oppoint The new DVFS oppoint to set. - * @return int 0 on success, negative error code on failure. + * + * @retval 0 on success. + * @retval -IRONSIDE_DVFS_ERROR_WRONG_OPPOINT if the requested DVFS oppoint is not allowed. + * @retval -IRONSIDE_DVFS_ERROR_BUSY if waiting for mutex lock timed out, or hardware is busy. + * @retval -IRONSIDE_DVFS_ERROR_OPPOINT_DATA if there is configuration error in the DVFS service. + * @retval -IRONSIDE_DVFS_ERROR_PERMISSION if the caller does not have permission to change the DVFS + * oppoint. + * @retval -IRONSIDE_DVFS_ERROR_NO_CHANGE_NEEDED if the requested DVFS oppoint is already set. + * @retval -IRONSIDE_DVFS_ERROR_TIMEOUT if the operation timed out, possibly due to a hardware + * issue. + * @retval -IRONSIDE_DVFS_ERROR_ISR_NOT_ALLOWED if the DVFS oppoint change operation is not allowed + * in the ISR context. + * @retval Positive error status if reported by IronSide call (see error codes in @ref call.h). */ int ironside_dvfs_change_oppoint(enum ironside_dvfs_oppoint dvfs_oppoint); diff --git a/soc/nordic/ironside/include/nrf_ironside/tdd.h b/soc/nordic/ironside/include/nrf_ironside/tdd.h index d065b1619a6..adfb1c53a64 100644 --- a/soc/nordic/ironside/include/nrf_ironside/tdd.h +++ b/soc/nordic/ironside/include/nrf_ironside/tdd.h @@ -31,7 +31,8 @@ enum ironside_se_tdd_config { * @param config The configuration to be applied. * * @retval 0 on success. - * @retval -IRONSIDE_SE_TDD_ERROR_EINVAL on invalid argument. + * @retval -IRONSIDE_SE_TDD_SERVICE_ERROR_INVALID_CONFIG if the configuration is invalid. + * @retval Positive error status if reported by IronSide call (see error codes in @ref call.h). */ int ironside_se_tdd_configure(const enum ironside_se_tdd_config config); diff --git a/soc/nordic/ironside/include/nrf_ironside/update.h b/soc/nordic/ironside/include/nrf_ironside/update.h index c0f021a91ed..b01b5df6dad 100644 --- a/soc/nordic/ironside/include/nrf_ironside/update.h +++ b/soc/nordic/ironside/include/nrf_ironside/update.h @@ -61,10 +61,10 @@ struct ironside_update_blob { * * @param update Pointer to update blob * + * @retval 0 on a successful request (although the update itself may still fail). * @retval -IRONSIDE_UPDATE_ERROR_NOT_PERMITTED if missing access to the update candidate. * @retval -IRONSIDE_UPDATE_ERROR_SICR_WRITE_FAILED if writing update parameters to SICR failed. - * @returns Positive non-0 error status if reported by IronSide call. - * @returns 0 on a successful request (although the update itself may still fail). + * @retval Positive error status if reported by IronSide call (see error codes in @ref call.h). * */ int ironside_update(const struct ironside_update_blob *update); diff --git a/soc/nordic/sysbuild.cmake b/soc/nordic/sysbuild.cmake index 03db1a5ad18..631c79d57fc 100644 --- a/soc/nordic/sysbuild.cmake +++ b/soc/nordic/sysbuild.cmake @@ -29,3 +29,7 @@ if(SB_CONFIG_VPR_LAUNCHER) sysbuild_cache_set(VAR ${image}_SNIPPET APPEND REMOVE_DUPLICATES ${launcher_snippet}) endif() + +if(SB_CONFIG_NRF_HALTIUM_GENERATE_UICR) + include(${CMAKE_CURRENT_LIST_DIR}/common/uicr/sysbuild.cmake) +endif() diff --git a/tests/drivers/comparator/gpio_loopback/testcase.yaml b/tests/drivers/comparator/gpio_loopback/testcase.yaml index bf0e7d127a0..ad25bd224b0 100644 --- a/tests/drivers/comparator/gpio_loopback/testcase.yaml +++ b/tests/drivers/comparator/gpio_loopback/testcase.yaml @@ -18,6 +18,7 @@ tests: - frdm_ke15z drivers.comparator.gpio_loopback.nrf_comp: extra_args: + - SNIPPET_ROOT="." - SNIPPET="gpio_loopback_nrf_comp" platform_allow: - nrf5340dk/nrf5340/cpuapp @@ -27,6 +28,7 @@ tests: - ophelia4ev/nrf54l15/cpuapp drivers.comparator.gpio_loopback.nrf_lpcomp: extra_args: + - SNIPPET_ROOT="." - SNIPPET="gpio_loopback_nrf_lpcomp" platform_allow: - nrf5340dk/nrf5340/cpuapp