diff --git a/CODEOWNERS b/CODEOWNERS index 35a831db3df..65e2447209e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -498,6 +498,7 @@ /samples/dect/dect_phy/dect_shell/ @nrfconnect/ncs-modem-tre /samples/dect/dect_phy/hello_dect/ @nrfconnect/ncs-modem /samples/dfu/ab/ @nrfconnect/ncs-eris +/samples/dfu/ab_split/ @nrfconnect/ncs-eris /samples/dfu/dfu_target/ @nrfconnect/ncs-eris /samples/dfu/dfu_multi_image/ @nrfconnect/ncs-eris /samples/dfu/fw_loader/ @nrfconnect/ncs-eris diff --git a/cmake/sysbuild/image_signing.cmake b/cmake/sysbuild/image_signing.cmake index c3e0013c23a..de833a9e812 100644 --- a/cmake/sysbuild/image_signing.cmake +++ b/cmake/sysbuild/image_signing.cmake @@ -143,6 +143,10 @@ function(zephyr_mcuboot_tasks) set(imgtool_extra ${imgtool_extra} --cid "${CONFIG_MCUBOOT_IMGTOOL_UUID_CID_NAME}") endif() + if(CONFIG_NCS_MCUBOOT_IMGTOOL_APPEND_MANIFEST) + set(imgtool_extra ${imgtool_extra} --manifest "manifest.yaml") + endif() + set(imgtool_args ${imgtool_extra}) # Extensionless prefix of any output file. diff --git a/cmake/sysbuild/mcuboot_manifest.cmake b/cmake/sysbuild/mcuboot_manifest.cmake new file mode 100644 index 00000000000..d1630f12ea0 --- /dev/null +++ b/cmake/sysbuild/mcuboot_manifest.cmake @@ -0,0 +1,88 @@ +# +# Copyright (c) 2025 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +include(${ZEPHYR_NRF_MODULE_DIR}/cmake/sysbuild/bootloader_dts_utils.cmake) + +yaml_create(NAME mcuboot_manifest) +yaml_set(NAME mcuboot_manifest KEY format VALUE "1") +yaml_set(NAME mcuboot_manifest KEY images LIST) +set(manifest_path "manifest.yaml") +set(manifest_img_slot_0 "${DEFAULT_IMAGE}") + +yaml_create(NAME mcuboot_secondary_manifest) +yaml_set(NAME mcuboot_secondary_manifest KEY format VALUE "1") +yaml_set(NAME mcuboot_secondary_manifest KEY images LIST) +set(manifest_secondary_path "manifest_secondary.yaml") +set(manifest_img_slot_1 "mcuboot_secondary_app") + +# There is no need to generate a manifest if there is only a single (merged) image. +if(NOT SB_CONFIG_MCUBOOT_SIGN_MERGED_BINARY) + sysbuild_get(manifest_img IMAGE mcuboot VAR CONFIG_MCUBOOT_MANIFEST_IMAGE_INDEX KCONFIG) + math(EXPR manifest_slot_0 "${manifest_img} * 2") + math(EXPR manifest_slot_1 "${manifest_img} * 2 + 1") + dt_partition_addr(slot0_addr LABEL "slot${manifest_slot_0}_partition" TARGET mcuboot ABSOLUTE REQUIRED) + dt_partition_addr(slot1_addr LABEL "slot${manifest_slot_1}_partition" TARGET mcuboot ABSOLUTE REQUIRED) + + UpdateableImage_Get(images GROUP "DEFAULT") + foreach(image ${images}) + sysbuild_get(BINARY_DIR IMAGE ${image} VAR APPLICATION_BINARY_DIR CACHE) + sysbuild_get(BINARY_BIN_FILE IMAGE ${image} VAR CONFIG_KERNEL_BIN_NAME KCONFIG) + dt_chosen(code_flash TARGET ${image} PROPERTY "zephyr,code-partition") + dt_partition_addr(code_addr PATH "${code_flash}" TARGET ${image} ABSOLUTE REQUIRED) + + if("${code_addr}" STREQUAL "${slot0_addr}") + cmake_path(APPEND BINARY_DIR "zephyr" "manifest.yaml" OUTPUT_VARIABLE manifest_path) + set(manifest_img_slot_0 "${image}") + continue() + endif() + + if(NOT "${SB_CONFIG_SIGNATURE_TYPE}" STREQUAL "NONE") + cmake_path(APPEND BINARY_DIR "zephyr" "${BINARY_BIN_FILE}.signed.bin" OUTPUT_VARIABLE image_path) + else() + cmake_path(APPEND BINARY_DIR "zephyr" "${BINARY_BIN_FILE}.bin" OUTPUT_VARIABLE image_path) + endif() + + yaml_set(NAME mcuboot_manifest KEY images APPEND LIST MAP "path: ${image_path}, name: ${image}") + endforeach() + + foreach(image ${images}) + if("${image}" STREQUAL "${manifest_img_slot_0}") + continue() + endif() + add_dependencies("${manifest_img_slot_0}" "${image}") + endforeach() + + UpdateableImage_Get(variants GROUP "VARIANT") + foreach(image ${variants}) + sysbuild_get(BINARY_DIR IMAGE ${image} VAR APPLICATION_BINARY_DIR CACHE) + sysbuild_get(BINARY_BIN_FILE IMAGE ${image} VAR CONFIG_KERNEL_BIN_NAME KCONFIG) + dt_chosen(code_flash TARGET ${image} PROPERTY "zephyr,code-partition") + dt_partition_addr(code_addr PATH "${code_flash}" TARGET ${image} ABSOLUTE REQUIRED) + + if("${code_addr}" STREQUAL "${slot1_addr}") + cmake_path(APPEND BINARY_DIR "zephyr" "manifest.yaml" OUTPUT_VARIABLE manifest_secondary_path) + set(manifest_img_slot_1 "${image}") + continue() + endif() + + if(NOT "${SB_CONFIG_SIGNATURE_TYPE}" STREQUAL "NONE") + cmake_path(APPEND BINARY_DIR "zephyr" "${BINARY_BIN_FILE}.signed.bin" OUTPUT_VARIABLE image_path) + else() + cmake_path(APPEND BINARY_DIR "zephyr" "${BINARY_BIN_FILE}.bin" OUTPUT_VARIABLE image_path) + endif() + + yaml_set(NAME mcuboot_secondary_manifest KEY images APPEND LIST MAP "path: ${image_path}, name: ${image}") + endforeach() + + foreach(image ${variants}) + if("${image}" STREQUAL "${manifest_img_slot_1}") + continue() + endif() + add_dependencies("${manifest_img_slot_1}" "${image}") + endforeach() +endif() + +yaml_save(NAME mcuboot_manifest FILE "${manifest_path}") +yaml_save(NAME mcuboot_secondary_manifest FILE "${manifest_secondary_path}") diff --git a/samples/dfu/ab_split/CMakeLists.txt b/samples/dfu/ab_split/CMakeLists.txt new file mode 100644 index 00000000000..6fe4740f3a5 --- /dev/null +++ b/samples/dfu/ab_split/CMakeLists.txt @@ -0,0 +1,25 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ab_split) + +target_sources(app PRIVATE + src/main.c + src/ab_utils.c +) + +target_include_directories(app PRIVATE + ${ZEPHYR_MCUBOOT_MODULE_DIR}/boot/bootutil/include + ${ZEPHYR_MCUBOOT_MODULE_DIR}/boot/zephyr/include + ${ZEPHYR_BASE}/samples/subsys/mgmt/mcumgr/smp_svr/src +) + +target_sources_ifdef(CONFIG_MCUMGR_TRANSPORT_BT app PRIVATE + ${ZEPHYR_BASE}/samples/subsys/mgmt/mcumgr/smp_svr/src/bluetooth.c +) diff --git a/samples/dfu/ab_split/Kconfig b/samples/dfu/ab_split/Kconfig new file mode 100644 index 00000000000..98160fae0f9 --- /dev/null +++ b/samples/dfu/ab_split/Kconfig @@ -0,0 +1,14 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +config N_BLINKS + int "Number of fast blinks" + default 1 + +config EMULATE_APP_HEALTH_CHECK_FAILURE + bool "Blocks confirmation of being healthy after the update" + +source "Kconfig.zephyr" diff --git a/samples/dfu/ab_split/README.rst b/samples/dfu/ab_split/README.rst new file mode 100644 index 00000000000..01da0a0de5b --- /dev/null +++ b/samples/dfu/ab_split/README.rst @@ -0,0 +1,203 @@ +.. _ab_split_sample: + +A/B with MCUboot and separated slots +#################################### + +.. contents:: + :local: + :depth: 2 + +The A/B with MCUboot and separated slots sample demonstrates how to configure the application for updates using the A/B method using MCUboot. +This sample is a variant of the :ref:`A/B sample `, where the application and radio images are not merged, but reside in separate MCUboot slots. +This split increases the number of memory areas that must be individually protected from accidental writes. +It also requires additional care when preparing updates to ensure that only a compatible set of slots is booted. +The additional dependency check during the boot process increases the time to boot the system. + +It also includes an example to perform a device health check before confirming the image after the update. +You can update the sample using the Simple Management Protocol (SMP) with UART or Bluetooth® Low Energy. + +To prevent the build system from merging slots, the sysbuild :kconfig:option:`SB_CONFIG_MCUBOOT_SIGN_MERGED_BINARY` option is disabled. +To enable manifest-based dependency management, the :kconfig:option:`SB_CONFIG_MCUBOOT_MANIFEST_UPDATES=y` option is enabled in the :file:`sysbuild.conf` file. + +Requirements +************ + +The sample supports the following development kits: + +.. table-from-sample-yaml:: + +You need the nRF Device Manager app for update over Bluetooth® Low Energy: + +* `nRF Device Manager mobile app for Android`_ +* `nRF Device Manager mobile app for iOS`_ + + +Overview +******** + +This sample demonstrates a firmware update using the A/B method. +This method allows you to store two copies of the application in non-volatile memory (NVM). +You can switch between these copies without performing a swap. +This solution significantly reduces the time during the update in which the device is unavailable. +The switch between images can be triggered by the application or, for example, by a hardware button. + +This sample implements an SMP server. +SMP is a basic transfer encoding used with the MCUmgr management protocol. +For more information about MCUmgr and SMP, see :ref:`device_mgmt`. + +The sample supports the following MCUmgr transports by default: + +* Bluetooth +* Serial (UART) + +A/B functionality +================= + +When you use the A/B configuration with separated slots, the device provides two slots for each set of application and radio firmware: slot A and slot B. +The slots are equivalent, and the device can boot from either of them. +With MCUboot, this is achieved by using the Direct XIP feature. +By design, slot A of the application image boots slot A of the radio image. +This design implies that verifying the image pairs correctly requires a manifest-based dependency. +There can be only one image that includes the manifest TLV. +Its index is configured using :kconfig:option:`SB_CONFIG_MCUBOOT_MANIFEST_IMAGE_INDEX`. +By default, the application image index (``0``) is selected. + +In this document, the following conventions are followed: + +* The application image index (``0``) is referred to as the *manifest image*. +* The following names refer to the same images and are used interchangeably throughout the documentation: + + * *Slot 0*, *primary slot*, and *slot A* + * *Slot 1*, *secondary slot*, and *slot B* + +This configuration allows a background update of the non-active slot while the application runs from the active slot. +After the update is complete, the device can quickly switch to the updated slot on the next reboot. + +The following conditions decide which slot is considered *active* and is booted on the next reboot: + +1. If one of the slots of the manifest image contains a valid image, it is marked as valid only if all other images, described by the manifest are present and placed in the same slot as the manifest. +#. If one of the slots of the manifest image is not valid, the other slot is selected as active. +#. If both slots of the manifest image are valid, the slot marked as "preferred" is selected as active. +#. If both slots of the manifest image are valid and none is marked as *preferred*, the slot with the higher version number is selected as active. +#. If none of the above conditions is met, slot A is selected as active. +#. For all other images, the same slot is selected. + +You can set the preferred slot using the ``boot_request_set_preferred_slot`` function. +Currently, this only sets the boot preference for a single reboot. + +Identifying the active slot +--------------------------- + +If the project uses the Partition Manager, the currently running slot can be identified by checking if ``CONFIG_NCS_IS_VARIANT_IMAGE`` is defined. +If it is defined, the application is running from slot B. +Otherwise, it is running from slot A. + +If the project does not use the Partition Manager (a configuration currently supported only on the nRF54H20 SoC), you can identify the currently running slot by comparing the address referenced by ``zephyr,code-partition`` with the specific node addresses defined in the devicetree. +The following node partitions are used by default: + +* ``cpuapp_slot0_partition`` - Application core, slot A +* ``cpuapp_slot1_partition`` - Application core, slot B +* ``cpurad_slot0_partition`` - Radio core, slot A +* ``cpurad_slot1_partition`` - Radio core, slot B + +For example, verifying that the application is running from slot A can be done by using the following macro: + +.. code-block:: c + + #define IS_RUNNING_FROM_SLOT_A \ + (FIXED_PARTITION_NODE_OFFSET(DT_CHOSEN(zephyr_code_partition)) == \ + FIXED_PARTITION_OFFSET(cpuapp_slot0_partition)) + +.. _ab_split_build_files: + +Build files +----------- + +This sample overrides the default build strategy, so application and radio images are built separately. +In this case, you must send the following files to the device when performing an update: + + +* :file:`build/mcuboot_secondary_app/zephyr/zephyr.signed.bin` - Contains the slot B of the application image. + Upload this file to the secondary slot when the device is running from slot A. +* :file:`build/ipc_radio_secondary_app/zephyr/zephyr.signed.bin` - Contains the slot B of the radio image. + Upload this file to the secondary slot when the device is running from slot A. +* :file:`build/ab/zephyr/zephyr.signed.bin` - Contains the slot A of the application image. + Upload this file to the primary slot when the device is running from slot B. +* :file:`build/ipc_radio/zephyr/zephyr.signed.bin` - Contains the slot A of the radio image. + Upload this file to the primary slot when the device is running from slot B. + +User interface +************** + +LED 0: + This LED indicates that the application is running from slot A. + It is controlled as active low. + This means that it turns on once the application is booted and turns off in short intervals to blinks. + The number of short blinks is configurable using the :kconfig:option:`CONFIG_N_BLINKS` Kconfig option. + It remains off when the application is running from slot B. + +LED 1: + This LED indicates that the application is running from slot B. + It is controlled as active low. + This means that it turns on once the application is booted and turns off at short intervals to blinks. + The number of short blinks is configurable using the :kconfig:option:`CONFIG_N_BLINKS` Kconfig option. + It remains off when the application is running from slot A. + +Button 0: + By pressing this button, you select the non-active slot as the preferred slot for the next reboot. + This preference applies only to the next boot and is cleared after the subsequent reset. + +Configuration +************* + +|config| + +Configuration options +===================== + +Check and configure the following configuration options for the sample: + +.. _CONFIG_N_BLINKS_ABSPLIT: + +CONFIG_N_BLINKS - The number of blinks. + This configuration option sets the number of times the LED corresponding to the currently active slot blinks (LED0 for slot A, LED1 for slot B). + The default value of the option is set to ``1``, causing a single blink to indicate *Version 1*. + You can increment this value to represent an update, such as set it to ``2`` to indicate *Version 2*. + +.. _CONFIG_EMULATE_APP_HEALTH_CHECK_FAILURE_AB_SPLIT: + +CONFIG_EMULATE_APP_HEALTH_CHECK_FAILURE - Enables emulation of a broken application that fails the self-test. + This configuration option emulates a broken application that does not pass the self-test. + +Additional configuration +======================== + +Check and configure the :kconfig:option:`CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION` Kconfig option for the MCUboot library. +This configuration option sets the version to pass to imgtool when signing. +To ensure the updated build is preferred after a DFU, set this option to a higher version than the version currently running on the device. + +Building and running +******************** + +.. |sample path| replace:: :file:`samples/dfu/ab_split` + +.. include:: /includes/build_and_run.txt + +Testing +======= + +To perform DFU using the `nRF Connect Device Manager`_ mobile app, complete the following steps: + +.. include:: /app_dev/device_guides/nrf52/fota_update.rst + :start-after: fota_upgrades_over_ble_nrfcdm_common_dfu_steps_start + :end-before: fota_upgrades_over_ble_nrfcdm_common_dfu_steps_end + +Instead of using the :file:`dfu_application.zip` file, you can also send the appropriate binary file directly, as described in :ref:`ab_split_build_files`. +Make sure to select the correct file based on the currently running slot. + +Dependencies +************ + +This sample uses the following |NCS| library: + +* :ref:`MCUboot ` diff --git a/samples/dfu/ab_split/boards/nrf54h20dk_nrf54h20_cpuapp.overlay b/samples/dfu/ab_split/boards/nrf54h20dk_nrf54h20_cpuapp.overlay new file mode 100644 index 00000000000..af0e3a2720c --- /dev/null +++ b/samples/dfu/ab_split/boards/nrf54h20dk_nrf54h20_cpuapp.overlay @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "../sysbuild/nrf54h20dk_nrf54h20_memory_map.dtsi" + +/ { + chosen { + zephyr,boot-mode = &boot_request; + }; +}; diff --git a/samples/dfu/ab_split/prj.conf b/samples/dfu/ab_split/prj.conf new file mode 100644 index 00000000000..f5bce24ff90 --- /dev/null +++ b/samples/dfu/ab_split/prj.conf @@ -0,0 +1,106 @@ +# Enable MCUmgr and dependencies. +CONFIG_NET_BUF=y +CONFIG_ZCBOR=y +CONFIG_CRC=y +CONFIG_MCUMGR=y +CONFIG_STREAM_FLASH=y +CONFIG_FLASH_MAP=y + +# Some command handlers require a large stack. +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2304 +CONFIG_MAIN_STACK_SIZE=2176 + +# Ensure an MCUboot-compatible binary is generated. +CONFIG_BOOTLOADER_MCUBOOT=y + +# Enable flash operations. +CONFIG_FLASH=y + +# Required by the `taskstat` command. +CONFIG_THREAD_MONITOR=y + +# Support for taskstat command +CONFIG_MCUMGR_GRP_OS_TASKSTAT=y + +# Enable statistics and statistic names. +CONFIG_STATS=y +CONFIG_STATS_NAMES=y + +# Enable most core commands. +CONFIG_FLASH=y +CONFIG_IMG_MANAGER=y +CONFIG_MCUMGR_GRP_IMG=y +CONFIG_MCUMGR_GRP_IMG_NRF=y +CONFIG_MCUMGR_GRP_OS=y +CONFIG_MCUMGR_GRP_STAT=y + +# Enable logging +CONFIG_LOG=y +CONFIG_MCUBOOT_UTIL_LOG_LEVEL_WRN=y + +# Disable debug logging +CONFIG_LOG_MAX_LEVEL=3 + +# Enable boot requests through retained memory. +CONFIG_RETAINED_MEM=y +CONFIG_RETENTION=y +CONFIG_NRF_MCUBOOT_BOOT_REQUEST=y + +CONFIG_RETENTION_BOOT_MODE=y +CONFIG_MCUMGR_GRP_OS_RESET_BOOT_MODE=y + +# Enable DK LED/button library +CONFIG_DK_LIBRARY=y + +# Configure bluetooth + +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y + +# Allow for large Bluetooth data packets. +CONFIG_BT_L2CAP_TX_MTU=498 +CONFIG_BT_BUF_ACL_RX_SIZE=502 +CONFIG_BT_BUF_ACL_TX_SIZE=502 + +# Enable the Bluetooth mcumgr transport (unauthenticated). +CONFIG_MCUMGR_TRANSPORT_BT=y +CONFIG_MCUMGR_TRANSPORT_BT_CONN_PARAM_CONTROL=y + +# Enable the Shell mcumgr transport. +CONFIG_BASE64=y +CONFIG_CRC=y +CONFIG_SHELL=y +CONFIG_SHELL_BACKEND_SERIAL=y +CONFIG_MCUMGR_TRANSPORT_SHELL=y + +# Enable the mcumgr Packet Reassembly feature over Bluetooth and its configuration dependencies. +# MCUmgr buffer size is optimized to fit one SMP packet divided into five Bluetooth Write Commands, +# transmitted with the maximum possible MTU value: 498 bytes. +CONFIG_MCUMGR_TRANSPORT_BT_REASSEMBLY=y +CONFIG_MCUMGR_TRANSPORT_NETBUF_SIZE=2475 +CONFIG_MCUMGR_GRP_OS_MCUMGR_PARAMS=y +CONFIG_MCUMGR_TRANSPORT_WORKQUEUE_STACK_SIZE=4608 + +# Enable the LittleFS file system. +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y + +# Enable file system commands +CONFIG_MCUMGR_GRP_FS=y + +# Enable the storage erase command. +CONFIG_MCUMGR_GRP_ZBASIC=y +CONFIG_MCUMGR_GRP_ZBASIC_STORAGE_ERASE=y + +# Disable Bluetooth ping support +CONFIG_BT_CTLR_LE_PING=n + +# Disable shell commands that are not needed +CONFIG_CLOCK_CONTROL_NRF_SHELL=n +CONFIG_DEVICE_SHELL=n +CONFIG_DEVMEM_SHELL=n +CONFIG_FLASH_SHELL=n + +# Read bootloader version and security counters values from the retained RAM +CONFIG_RETENTION_BOOTLOADER_INFO=y +CONFIG_RETENTION_BOOTLOADER_INFO_TYPE_MCUBOOT=y diff --git a/samples/dfu/ab_split/sample.yaml b/samples/dfu/ab_split/sample.yaml new file mode 100644 index 00000000000..4c0b948511d --- /dev/null +++ b/samples/dfu/ab_split/sample.yaml @@ -0,0 +1,18 @@ +sample: + description: AB update sample with separated slots + name: ab_split +common: + sysbuild: true + build_only: true + tags: + - dfu_ab + - ci_samples_dfu + +tests: + sample.dfu.ab_split: + extra_configs: + - CONFIG_MCUMGR_GRP_IMG_ALLOW_CONFIRM_NON_ACTIVE_IMAGE_ANY=y + platform_allow: + - nrf54h20dk/nrf54h20/cpuapp + integration_platforms: + - nrf54h20dk/nrf54h20/cpuapp diff --git a/samples/dfu/ab_split/src/ab_utils.c b/samples/dfu/ab_split/src/ab_utils.c new file mode 100644 index 00000000000..f1011295336 --- /dev/null +++ b/samples/dfu/ab_split/src/ab_utils.c @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(ab_sample); + +#define ACTIVE_IMAGE 0 + +#define CODE_PARTITION DT_CHOSEN(zephyr_code_partition) +#define CODE_PARTITION_OFFSET FIXED_PARTITION_NODE_OFFSET(CODE_PARTITION) + +#define SLOT_A_PARTITION cpuapp_slot0_partition +#define SLOT_B_PARTITION cpuapp_slot1_partition +#define CPURAD_SLOT_A_PARTITION cpurad_slot0_partition +#define CPURAD_SLOT_B_PARTITION cpurad_slot1_partition + +#define SLOT_A_OFFSET FIXED_PARTITION_OFFSET(SLOT_A_PARTITION) +#define SLOT_B_OFFSET FIXED_PARTITION_OFFSET(SLOT_B_PARTITION) +#define SLOT_A_SIZE FIXED_PARTITION_SIZE(SLOT_A_PARTITION) +#define SLOT_B_SIZE FIXED_PARTITION_SIZE(SLOT_B_PARTITION) + +#define SLOT_A_FLASH_AREA_ID FIXED_PARTITION_ID(SLOT_A_PARTITION) +#define SLOT_B_FLASH_AREA_ID FIXED_PARTITION_ID(SLOT_B_PARTITION) +#define CPURAD_SLOT_A_FLASH_AREA_ID FIXED_PARTITION_ID(CPURAD_SLOT_A_PARTITION) +#define CPURAD_SLOT_B_FLASH_AREA_ID FIXED_PARTITION_ID(CPURAD_SLOT_B_PARTITION) + +#define IS_SLOT_A \ + (CODE_PARTITION_OFFSET >= SLOT_A_OFFSET && \ + CODE_PARTITION_OFFSET < SLOT_A_OFFSET + SLOT_A_SIZE) +#define IS_SLOT_B \ + (CODE_PARTITION_OFFSET >= SLOT_B_OFFSET && \ + CODE_PARTITION_OFFSET < SLOT_B_OFFSET + SLOT_B_SIZE) + +#define STATUS_LEDS_THREAD_STACK_SIZE 512 +#define STATUS_LEDS_THREAD_PRIORITY (CONFIG_NUM_PREEMPT_PRIORITIES - 1) +K_THREAD_STACK_DEFINE(status_leds_thread_stack_area, STATUS_LEDS_THREAD_STACK_SIZE); + +enum ab_boot_slot { + SLOT_A = 0, + SLOT_B = 1, + SLOT_INVALID, +}; + +/** @brief Radio firmware self test + * + * @details + * End-device specific self test should be implemented here. + */ +static bool radio_domain_healthy(void) +{ + return bt_is_ready(); +} + +/** @brief Application firmware self test + * + * @details + * End-device specific self test should be implemented here. Enabling + * CONFIG_EMULATE_APP_HEALTH_CHECK_FAILURE allows to emulate a faulty + * firmware, unable to confirm its health, and ultimately to test + * a rollback to previous firmware after the update. + */ +static bool app_domain_healthy(void) +{ + if (IS_ENABLED(CONFIG_EMULATE_APP_HEALTH_CHECK_FAILURE)) { + return false; + } + + return true; +} + +static enum ab_boot_slot active_boot_slot_get(void) +{ + enum ab_boot_slot active_slot = SLOT_INVALID; + + if (IS_SLOT_A) { + active_slot = SLOT_A; + } else if (IS_SLOT_B) { + active_slot = SLOT_B; + } else { + LOG_ERR("Cannot determine current slot"); + } + + return active_slot; +} + +static void device_healthcheck(void) +{ + int err; + char *img_set = NULL; + const struct flash_area *fa; + int area_id = -1; + int cpurad_area_id = -1; + enum ab_boot_slot active_slot = active_boot_slot_get(); + + if (active_slot == SLOT_INVALID) { + return; + } + + /* Confirming only in non-degraded boot states + */ + if (active_slot == SLOT_A) { + img_set = "A"; + area_id = SLOT_A_FLASH_AREA_ID; + cpurad_area_id = CPURAD_SLOT_A_FLASH_AREA_ID; + } else if (active_slot == SLOT_B) { + img_set = "B"; + area_id = SLOT_B_FLASH_AREA_ID; + cpurad_area_id = CPURAD_SLOT_B_FLASH_AREA_ID; + } + + LOG_INF("Testing image set %s...", img_set); + + bool healthy = true; + + if (!radio_domain_healthy()) { + LOG_ERR("Radio domain is NOT healthy"); + healthy = false; + } + + if (!app_domain_healthy()) { + LOG_ERR("App domain is NOT healthy"); + healthy = false; + } + + if (!healthy) { + LOG_ERR("Reboot the device to try to boot from previous firmware"); + return; + } + + LOG_INF("Confirming..."); + + if (flash_area_open(area_id, &fa) != 0) { + LOG_ERR("Cannot open flash area for application slot %s", img_set); + return; + } + + err = boot_set_next(fa, true, true); + + flash_area_close(fa); + if (err == 0) { + LOG_INF("Application confirmed\n"); + } else { + LOG_ERR("Failed to confirm application, err: %d", err); + } + + if (flash_area_open(cpurad_area_id, &fa) != 0) { + LOG_ERR("Cannot open flash area for radio slot %s", img_set); + return; + } + + err = boot_set_next(fa, true, true); + + flash_area_close(fa); + if (err == 0) { + LOG_INF("Radio confirmed\n"); + } else { + LOG_ERR("Failed to confirm radio, err: %d", err); + } +} + +static void select_slot_for_single_boot(enum ab_boot_slot slot) +{ + int err = 0; + char active_slot = (active_boot_slot_get() == SLOT_A) ? 'A' : 'B'; + enum boot_slot new_slot = BOOT_SLOT_NONE; + + if (slot == SLOT_A) { + LOG_INF("Temporarily switching slots (%c -> A)", active_slot); + new_slot = BOOT_SLOT_PRIMARY; + } else if (slot == SLOT_B) { + LOG_INF("Temporarily switching slots (%c -> B)", active_slot); + new_slot = BOOT_SLOT_SECONDARY; + } else { + LOG_ERR("Cannot determine active slot, cannot toggle"); + return; + } + + err = boot_request_set_preferred_slot(ACTIVE_IMAGE, new_slot); + + if (err == 0) { + LOG_INF("Slot toggled, restart the device to enforce"); + } else { + LOG_ERR("Failed to toggle slots, err: %d", err); + } +} + +static void boot_state_report(void) +{ + enum ab_boot_slot active_slot = active_boot_slot_get(); + + if (active_slot == SLOT_A) { + LOG_INF("Booted from slot A"); + } else if (active_slot == SLOT_B) { + LOG_INF("Booted from slot B"); + } else { + LOG_INF("Cannot determine active slot"); + } +} + +static void button_handler(uint32_t button_state, uint32_t has_changed) +{ + if ((has_changed & DK_BTN1_MSK) && (button_state & DK_BTN1_MSK)) { + select_slot_for_single_boot(SLOT_A); + } else if ((has_changed & DK_BTN2_MSK) && (button_state & DK_BTN2_MSK)) { + select_slot_for_single_boot(SLOT_B); + } +} + +struct k_thread status_leds_thread_data; + +static void status_leds_thread_entry_point(void *p1, void *p2, void *p3) +{ + int blinking_led = DK_LED1; + enum ab_boot_slot active_slot = active_boot_slot_get(); + + if (active_slot == SLOT_A) { + blinking_led = DK_LED1; + } else if (active_slot == SLOT_B) { + blinking_led = DK_LED2; + } else { + return; + } + + while (1) { + for (int i = 0; i < CONFIG_N_BLINKS; i++) { + dk_set_led_off(blinking_led); + k_msleep(250); + dk_set_led_on(blinking_led); + k_msleep(250); + } + + k_msleep(5000); + } +} + +void ab_actions_perform(void) +{ + int ret; + + boot_state_report(); + + ret = dk_leds_init(); + if (ret) { + LOG_ERR("Cannot init LEDs (err: %d)", ret); + } + + ret = dk_buttons_init(button_handler); + if (ret) { + LOG_ERR("Cannot init buttons (err: %d)", ret); + } + + k_thread_create(&status_leds_thread_data, status_leds_thread_stack_area, + K_THREAD_STACK_SIZEOF(status_leds_thread_stack_area), + status_leds_thread_entry_point, + NULL, NULL, NULL, + STATUS_LEDS_THREAD_PRIORITY, 0, K_NO_WAIT); + + device_healthcheck(); +} diff --git a/samples/dfu/ab_split/src/ab_utils.h b/samples/dfu/ab_split/src/ab_utils.h new file mode 100644 index 00000000000..abaabbda163 --- /dev/null +++ b/samples/dfu/ab_split/src/ab_utils.h @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +void ab_actions_perform(void); diff --git a/samples/dfu/ab_split/src/main.c b/samples/dfu/ab_split/src/main.c new file mode 100644 index 00000000000..aad56902ffd --- /dev/null +++ b/samples/dfu/ab_split/src/main.c @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2012-2014 Wind River Systems, Inc. + * Copyright (c) 2020 Prevas A/S + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include "ab_utils.h" + +#ifdef CONFIG_RETENTION_BOOTLOADER_INFO +#include +#include +#endif + +#define LOG_LEVEL LOG_LEVEL_DBG +#include +LOG_MODULE_REGISTER(ab_sample); + +#ifdef CONFIG_RETENTION_BOOTLOADER_INFO +static void blinfo_bootloader_version(void) +{ + struct image_version version = {0x00}; + + int ret = blinfo_lookup(BLINFO_BOOTLOADER_VERSION, (char *)&version, + sizeof(struct image_version)); + + if (ret < 0) { + LOG_INF("blinfo_lookup error: %d", ret); + } else { + LOG_INF("Bootloader version: %d.%d.%d+%d", version.iv_major, + version.iv_minor, version.iv_revision, version.iv_build_num); + } +} + +#ifdef CONFIG_MCUBOOT_HARDWARE_DOWNGRADE_PREVENTION +static void blinfo_security_counter(void) +{ + uint32_t counter = 0; + + int ret = blinfo_lookup(BLINFO_SECURITY_COUNTER_IMAGE_0, (char *)&counter, + sizeof(counter)); + + if (ret < 0) { + LOG_INF("blinfo_lookup error: %d", ret); + } else { + LOG_INF("Security counter: %d", counter); + } +} +#endif /* CONFIG_MCUBOOT_HARDWARE_DOWNGRADE_PREVENTION */ +#endif /* CONFIG_RETENTION_BOOTLOADER_INFO */ + +int main(void) +{ +#ifdef CONFIG_RETENTION_BOOTLOADER_INFO + blinfo_bootloader_version(); +#ifdef CONFIG_MCUBOOT_HARDWARE_DOWNGRADE_PREVENTION + blinfo_security_counter(); +#endif /* CONFIG_MCUBOOT_HARDWARE_DOWNGRADE_PREVENTION */ +#endif + +#ifdef CONFIG_MCUMGR_TRANSPORT_BT + start_smp_bluetooth_adverts(); +#endif + + /* Give BLE a moment to start up */ + k_sleep(K_MSEC(1000)); + + ab_actions_perform(); + + /* using __TIME__ ensure that a new binary will be built on every + * compile which is convenient when testing firmware upgrade. + */ + LOG_INF("build time: " __DATE__ " " __TIME__); + + /* The system work queue handles all incoming mcumgr requests. Let the + * main thread idle while the mcumgr server runs. + */ + while (1) { + k_sleep(K_MSEC(1000)); + } + + return 0; +} diff --git a/samples/dfu/ab_split/sysbuild.conf b/samples/dfu/ab_split/sysbuild.conf new file mode 100644 index 00000000000..6cb102f8437 --- /dev/null +++ b/samples/dfu/ab_split/sysbuild.conf @@ -0,0 +1,15 @@ +# Enable MCUboot bootloader support +SB_CONFIG_BOOTLOADER_MCUBOOT=y + +# Enable radiocore +SB_CONFIG_NETCORE_IPC_RADIO=y +SB_CONFIG_NETCORE_IPC_RADIO_BT_HCI_IPC=y + +# Enable direct XIP with revert support +SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP_WITH_REVERT=y + +# Disable merging of slots +SB_CONFIG_MCUBOOT_SIGN_MERGED_BINARY=n + +# Enable manifest-based updates +SB_CONFIG_MCUBOOT_MANIFEST_UPDATES=y diff --git a/samples/dfu/ab_split/sysbuild/mcuboot/boards/nrf54h20dk_nrf54h20_cpuapp.overlay b/samples/dfu/ab_split/sysbuild/mcuboot/boards/nrf54h20dk_nrf54h20_cpuapp.overlay new file mode 100644 index 00000000000..34ce5398224 --- /dev/null +++ b/samples/dfu/ab_split/sysbuild/mcuboot/boards/nrf54h20dk_nrf54h20_cpuapp.overlay @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + + #include "../../nrf54h20dk_nrf54h20_memory_map.dtsi" + +/ { + chosen { + zephyr,code-partition = &boot_partition; + }; +}; diff --git a/samples/dfu/ab_split/sysbuild/mcuboot/prj.conf b/samples/dfu/ab_split/sysbuild/mcuboot/prj.conf new file mode 100644 index 00000000000..d0db88e2c8d --- /dev/null +++ b/samples/dfu/ab_split/sysbuild/mcuboot/prj.conf @@ -0,0 +1,37 @@ +# Enable boot requests through retained memory. +CONFIG_RETAINED_MEM=y +CONFIG_RETENTION=y +CONFIG_NRF_MCUBOOT_BOOT_REQUEST=y + +CONFIG_NRF_SECURITY=y +CONFIG_MULTITHREADING=y + +# Configuration below is copied from mcuboot/boot/zephyr/prj.conf, as creating +# the sysbuild/mcuboot directory inside a sample removes the default configuration. + +CONFIG_PM=n + +CONFIG_MAIN_STACK_SIZE=10240 + +CONFIG_BOOT_SWAP_SAVE_ENCTLV=n +CONFIG_BOOT_ENCRYPT_IMAGE=n + +CONFIG_BOOT_UPGRADE_ONLY=n +CONFIG_BOOT_BOOTSTRAP=n + +CONFIG_FLASH=y + +CONFIG_LOG=y +CONFIG_LOG_MODE_MINIMAL=y +CONFIG_LOG_DEFAULT_LEVEL=0 +CONFIG_MCUBOOT_LOG_LEVEL_INF=y +CONFIG_CBPRINTF_NANO=y +CONFIG_PICOLIBC=y +CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE=0 + +CONFIG_NCS_APPLICATION_BOOT_BANNER_STRING="MCUboot" + +# Expose bootloader version and security counter through retention +CONFIG_BOOT_SHARE_DATA=y +CONFIG_BOOT_SHARE_DATA_BOOTINFO=y +CONFIG_BOOT_SHARE_BACKEND_RETENTION=y diff --git a/samples/dfu/ab_split/sysbuild/nrf54h20dk_nrf54h20_memory_map.dtsi b/samples/dfu/ab_split/sysbuild/nrf54h20dk_nrf54h20_memory_map.dtsi new file mode 100644 index 00000000000..07406e34302 --- /dev/null +++ b/samples/dfu/ab_split/sysbuild/nrf54h20dk_nrf54h20_memory_map.dtsi @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/ { + chosen { + nrf,bootloader-request = &boot_request; + zephyr,bootloader-info = &boot_info0; + }; +}; + +/ { + reserved-memory { + cpuapp_retained_mem: memory@e1ad000 { + compatible = "zephyr,memory-region"; + reg = <0xe1ad000 DT_SIZE_K(4)>; + zephyr,memory-region = "RetainedNvm"; + status = "okay"; + + retainedmem { + compatible = "zephyr,retained-ram"; + status = "okay"; + #address-cells = <1>; + #size-cells = <1>; + + boot_request: boot_request@0 { + compatible = "zephyr,retention"; + status = "okay"; + reg = <0x0 16>; + prefix = [0B 01]; + checksum = <4>; + }; + }; + }; + }; + + sram@22007FC0 { + compatible = "zephyr,memory-region", "mmio-sram"; + reg = <0x22007FC0 0x40>; + zephyr,memory-region = "RetainedMem"; + status = "okay"; + + retainedmem { + compatible = "zephyr,retained-ram"; + status = "okay"; + #address-cells = <1>; + #size-cells = <1>; + + boot_info0: boot_info@0 { + compatible = "zephyr,retention"; + status = "okay"; + reg = <0x0 0x40>; + }; + }; + }; +}; + +&cpuapp_ram0 { + reg = <0x22000000 0x7FC0>; +}; diff --git a/subsys/bootloader/Kconfig b/subsys/bootloader/Kconfig index 3257653f533..ba6c91380e9 100644 --- a/subsys/bootloader/Kconfig +++ b/subsys/bootloader/Kconfig @@ -167,4 +167,26 @@ config NCS_MCUBOOT_BOOTLOADER_SIGN_MERGED_BINARY help This is a Kconfig which is informative only, the value should not be changed. +config NCS_MCUBOOT_MANIFEST_UPDATES + bool "Manifest-based updates (informative only, do not change)" + help + When enabled, supports transactional updates using manifests. + This is a Kconfig which is informative only, the value should not be changed. + +if NCS_MCUBOOT_MANIFEST_UPDATES + +config NCS_MCUBOOT_IMGTOOL_APPEND_MANIFEST + bool "MCUboot append manifest (informative only, do not change)" + help + Append common manifest while signing the image. + This is a Kconfig which is informative only, the value should not be changed. + +config NCS_MCUBOOT_MANIFEST_IMAGE_INDEX + int "Index of the image that must include manifest (informative only, do not change)" + help + Specifies the index of the image that must include the manifest. + This is a Kconfig which is informative only, the value should not be changed. + +endif # NCS_MCUBOOT_MANIFEST_UPDATES + endmenu diff --git a/subsys/mgmt/mcumgr/grp/img_mgmt/src/img_mgmt.c b/subsys/mgmt/mcumgr/grp/img_mgmt/src/img_mgmt.c index c87a5498b26..46b39bb4479 100644 --- a/subsys/mgmt/mcumgr/grp/img_mgmt/src/img_mgmt.c +++ b/subsys/mgmt/mcumgr/grp/img_mgmt/src/img_mgmt.c @@ -31,6 +31,11 @@ #include #endif +#ifdef CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES +#include +#include +#endif + #ifdef CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS #include #include @@ -274,6 +279,124 @@ int img_mgmt_active_image(void) return ACTIVE_IMAGE_IS; } +#ifdef CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES +/** + * Checks whether the manifest of the image in the specified slot can be used for booting. + * + * @param slot The slot to check the manifest for. + * + * @return true if the manifest can be used, false otherwise. + */ +static bool boot_check_manifest(enum boot_slot slot) +{ + struct image_header hdr; + struct image_tlv tlv; + size_t data_off; + size_t data_end; + bool manifest_found; + uint8_t erased_val; + uint32_t erased_val_32; + struct mcuboot_manifest tmp_manifest; + uint8_t hash[IMAGE_HASH_SIZE]; + int rc; + int image_slot = CONFIG_NCS_MCUBOOT_MANIFEST_IMAGE_INDEX * BOOT_SLOT_COUNT + slot; + + rc = img_mgmt_erased_val(image_slot, &erased_val); + if (rc != 0) { + return false; + } + + rc = img_mgmt_read(image_slot, + boot_get_image_start_offset(img_mgmt_flash_area_id(image_slot)), + &hdr, sizeof(hdr)); + if (rc != 0) { + return false; + } + + erased_val_32 = ERASED_VAL_32(erased_val); + if (hdr.ih_magic != IMAGE_MAGIC) { + return false; + } + + /* Read the image's TLVs. We try to find manifest only inside the protected TLVs. + * If the manifest is missing, the image is considered invalid. + */ + data_off = hdr.ih_hdr_size + hdr.ih_img_size + + boot_get_image_start_offset(img_mgmt_flash_area_id(image_slot)); + + rc = img_mgmt_find_tlvs(image_slot, &data_off, &data_end, IMAGE_TLV_PROT_INFO_MAGIC); + if (rc != 0) { + return false; + } + + manifest_found = false; + while (data_off + sizeof(tlv) <= data_end) { + rc = img_mgmt_read(image_slot, data_off, &tlv, sizeof(tlv)); + if (rc != 0) { + return false; + } + if (tlv.it_type == 0xff && tlv.it_len == 0xffff) { + return false; + } + if ((tlv.it_type != IMAGE_TLV_MANIFEST) || + (tlv.it_len != sizeof(struct mcuboot_manifest))) { + /* Non-manifest TLV. Skip it. */ + data_off += sizeof(tlv) + tlv.it_len; + continue; + } + + if (manifest_found) { + /* More than one manifest. */ + return false; + } + manifest_found = true; + + data_off += sizeof(tlv); + if (data_off + sizeof(struct mcuboot_manifest) > data_end) { + return false; + } + rc = img_mgmt_read(image_slot, data_off, &tmp_manifest, + sizeof(struct mcuboot_manifest)); + if (rc != 0) { + return false; + } + } + + if (!manifest_found) { + return false; + } + + for (size_t i = 0; i < BOOT_IMAGE_NUMBER; i++) { + if (i == CONFIG_NCS_MCUBOOT_MANIFEST_IMAGE_INDEX) { + continue; + } + + rc = img_mgmt_read_info(i * BOOT_SLOT_COUNT + slot, NULL, hash, NULL); + if ((rc == 0) && bootutil_verify_manifest_image_hash(&tmp_manifest, hash, i)) { + /* Hash matches */ + continue; + } + +#if !defined(CONFIG_MCUBOOT_BOOTLOADER_MODE_DIRECT_XIP) && \ + !defined(CONFIG_MCUBOOT_BOOTLOADER_MODE_DIRECT_XIP_WITH_REVERT) && \ + !defined(CONFIG_MCUBOOT_BOOTLOADER_MODE_RAM_LOAD) + /* In SWAP modes, the image may be placed in either primary or secondary slot. */ + rc = img_mgmt_read_info(i * BOOT_SLOT_COUNT + ((slot + 1) % BOOT_SLOT_COUNT), NULL, + hash, NULL); + if ((rc == 0) && bootutil_verify_manifest_image_hash(&tmp_manifest, hash, i)) { + /* Hash matches */ + continue; + } +#endif + + LOG_ERR("Manifest hash does not match image %d hash", i); + return false; + } + + return true; +} +#endif /* CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES */ + /* * Reads the version and build hash from the specified image slot. */ @@ -317,6 +440,12 @@ int img_mgmt_read_info(int image_slot, struct image_version *ver, uint8_t *hash, if (flags != NULL) { *flags = hdr.ih_flags; +#ifdef CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES + /* Mark slot as unbootable if manifest is not satisfied. */ + if (!boot_check_manifest(image_slot % SLOTS_PER_IMAGE)) { + *flags |= IMAGE_F_NON_BOOTABLE; + } +#endif } /* Read the image's TLVs. We first try to find the protected TLVs, if the protected diff --git a/subsys/mgmt/mcumgr/grp/img_mgmt/src/img_mgmt_state.c b/subsys/mgmt/mcumgr/grp/img_mgmt/src/img_mgmt_state.c index ae9e18685b8..5642e80cdaa 100644 --- a/subsys/mgmt/mcumgr/grp/img_mgmt/src/img_mgmt_state.c +++ b/subsys/mgmt/mcumgr/grp/img_mgmt/src/img_mgmt_state.c @@ -131,6 +131,20 @@ img_mgmt_state_flags(int query_slot) int image = query_slot / 2; /* We support max 2 images for now */ int active_slot = img_mgmt_active_slot(image); +#ifdef CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES + /* If manifest-based updates are used, overwrite the image and slot with + * the manifest image. + */ + image = CONFIG_NCS_MCUBOOT_MANIFEST_IMAGE_INDEX; + if (query_slot == active_slot) { + active_slot = img_mgmt_active_slot(image); + query_slot = active_slot; + } else { + active_slot = img_mgmt_active_slot(image); + query_slot = img_mgmt_get_opposite_slot(active_slot); + } +#endif /* CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES */ + /* In case when MCUboot is configured for FW loader/updater mode, slots * can be either active or non-active. There is no concept of pending * or confirmed slots. @@ -258,18 +272,24 @@ int img_mgmt_get_next_boot_slot(int image, enum img_mgmt_next_boot_type *type) { struct image_version aver; struct image_version over; - int active_slot = img_mgmt_active_slot(image); - int other_slot = img_mgmt_get_opposite_slot(active_slot); #if defined(CONFIG_MCUBOOT_BOOTLOADER_MODE_DIRECT_XIP_WITH_REVERT) int active_slot_state; int other_slot_state; #endif /* defined(CONFIG_MCUBOOT_BOOTLOADER_MODE_DIRECT_XIP_WITH_REVERT) */ enum img_mgmt_next_boot_type lt = NEXT_BOOT_TYPE_NORMAL; - int return_slot = active_slot; +#ifdef CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES + /* If manifest-based updates are used, only the manifest image is considered. */ + int query_image = image; + image = CONFIG_NCS_MCUBOOT_MANIFEST_IMAGE_INDEX; +#endif /* CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES */ + + int active_slot = img_mgmt_active_slot(image); + int other_slot = img_mgmt_get_opposite_slot(active_slot); int rcs = img_mgmt_read_info(other_slot, &over, NULL, NULL); int rca = img_mgmt_read_info(active_slot, &aver, NULL, NULL); + int return_slot = active_slot; #if defined(CONFIG_MCUBOOT_BOOTLOADER_MODE_DIRECT_XIP_WITH_REVERT) active_slot_state = read_directxip_state(active_slot); @@ -364,6 +384,14 @@ int img_mgmt_get_next_boot_slot(int image, enum img_mgmt_next_boot_type *type) *type = lt; } +#ifdef CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES + if (return_slot != active_slot) { + return_slot = img_mgmt_get_opposite_slot(img_mgmt_active_slot(query_image)); + } else { + return_slot = img_mgmt_active_slot(query_image); + } +#endif /* CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES */ + return return_slot; } #endif /* defined(CONFIG_MCUBOOT_BOOTLOADER_MODE_DIRECT_XIP) || \ diff --git a/sysbuild/CMakeLists.txt b/sysbuild/CMakeLists.txt index 2549986f532..4f1791f37a7 100644 --- a/sysbuild/CMakeLists.txt +++ b/sysbuild/CMakeLists.txt @@ -172,6 +172,7 @@ function(${SYSBUILD_CURRENT_MODULE_NAME}_pre_cmake) set(dfu_slots_output_names Application;Network;Wifi\ patches;QSPI\ XIP;MCUboot\ b0\ update) set(dfu_slots_output_text "-- Sysbuild assigned MCUboot image IDs:\n") set(dfu_image_index -1) + set(manifest_image_target "none") list(LENGTH dfu_slots_sysbuild_kconfigs test_things_size) math(EXPR test_things_size "${test_things_size} - 1") @@ -193,6 +194,22 @@ function(${SYSBUILD_CURRENT_MODULE_NAME}_pre_cmake) set_config_int(${image} ${current_application_kconfig} ${value}) endforeach() + if(SB_CONFIG_MCUBOOT_MANIFEST_UPDATES) + if(${value} EQUAL ${SB_CONFIG_MCUBOOT_MANIFEST_IMAGE_INDEX}) + if("${current_application_kconfig}" STREQUAL "CONFIG_MCUBOOT_APPLICATION_IMAGE_NUMBER") + set(manifest_image_target ${DEFAULT_IMAGE}) + elseif("${current_application_kconfig}" STREQUAL "CONFIG_MCUBOOT_NETWORK_CORE_IMAGE_NUMBER") + set(manifest_image_target ${SB_CONFIG_NETCORE_IMAGE_NAME}) + else() + # Unsupported indexes: + # - CONFIG_MCUBOOT_WIFI_PATCHES_IMAGE_NUMBER + # - CONFIG_MCUBOOT_QSPI_XIP_IMAGE_NUMBER + # - CONFIG_MCUBOOT_MCUBOOT_IMAGE_NUMBER + message(FATAL_ERROR "MCUboot manifest image can only be assigned to application or network core images") + endif() + endif() + endif() + set(${current_cache_name} ${value} CACHE INTERNAL "" FORCE) endforeach() @@ -256,6 +273,25 @@ function(${SYSBUILD_CURRENT_MODULE_NAME}_pre_cmake) set_config_bool(mcuboot CONFIG_BOOT_IMG_HASH_ALG_SHA512 y) endif() + if(SB_CONFIG_MCUBOOT_MANIFEST_UPDATES) + set_config_bool(mcuboot CONFIG_MCUBOOT_MANIFEST_UPDATES y) + set_config_int(mcuboot CONFIG_MCUBOOT_MANIFEST_IMAGE_INDEX + ${SB_CONFIG_MCUBOOT_MANIFEST_IMAGE_INDEX}) + + if("${manifest_image_target}" STREQUAL "none") + message(FATAL_ERROR "No manifest image target, cannot append manifest to image") + endif() + set_config_bool(${manifest_image_target} CONFIG_NCS_MCUBOOT_IMGTOOL_APPEND_MANIFEST y) + if(SB_CONFIG_MCUBOOT_BUILD_DIRECT_XIP_VARIANT) + if("${manifest_image_target}" STREQUAL "${DEFAULT_IMAGE}") + set_config_bool(mcuboot_secondary_app CONFIG_NCS_MCUBOOT_IMGTOOL_APPEND_MANIFEST y) + else() + set_config_bool(${manifest_image_target}_secondary_app + CONFIG_NCS_MCUBOOT_IMGTOOL_APPEND_MANIFEST y) + endif() + endif() + endif() + # Apply configuration to application foreach(image ${updateable_images}) foreach(mode ${application_mcuboot_modes}) @@ -266,6 +302,11 @@ function(${SYSBUILD_CURRENT_MODULE_NAME}_pre_cmake) endif() endforeach() + if(SB_CONFIG_MCUBOOT_MANIFEST_UPDATES) + set_config_bool(${image} CONFIG_NCS_MCUBOOT_MANIFEST_UPDATES y) + set_config_int(${image} CONFIG_NCS_MCUBOOT_MANIFEST_IMAGE_INDEX ${SB_CONFIG_MCUBOOT_MANIFEST_IMAGE_INDEX}) + endif() + if(SB_CONFIG_BOOT_SIGNATURE_TYPE_ED25519) set_config_bool(${image} CONFIG_MCUBOOT_BOOTLOADER_SIGNATURE_TYPE_ED25519 y) endif() @@ -385,7 +426,8 @@ function(${SYSBUILD_CURRENT_MODULE_NAME}_pre_cmake) OR SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP_WITH_REVERT OR SB_CONFIG_MCUBOOT_COMPRESSED_IMAGE_SUPPORT OR (SB_CONFIG_SOC_SERIES_NRF54LX AND SB_CONFIG_BOOT_ENCRYPTION) - OR SB_CONFIG_MCUBOOT_HARDWARE_DOWNGRADE_PREVENTION) + OR SB_CONFIG_MCUBOOT_HARDWARE_DOWNGRADE_PREVENTION + OR SB_CONFIG_MCUBOOT_MANIFEST_UPDATES) # Use NCS signing script with support for PM or direct XIP (NCS specific features) if(SB_CONFIG_QSPI_XIP_SPLIT_IMAGE) set(${DEFAULT_IMAGE}_SIGNING_SCRIPT "${ZEPHYR_NRF_MODULE_DIR}/cmake/sysbuild/image_signing_split.cmake" CACHE INTERNAL "MCUboot signing script" FORCE) @@ -888,6 +930,10 @@ function(${SYSBUILD_CURRENT_MODULE_NAME}_post_cmake) include(${ZEPHYR_NRF_MODULE_DIR}/cmake/sysbuild/mcuboot_nrf54h20.cmake) endif() + if(SB_CONFIG_BOOTLOADER_MCUBOOT AND SB_CONFIG_MCUBOOT_MANIFEST_UPDATES) + include(${ZEPHYR_NRF_MODULE_DIR}/cmake/sysbuild/mcuboot_manifest.cmake) + endif() + if(SB_CONFIG_DFU_ZIP) if(SB_CONFIG_BOOTLOADER_MCUBOOT) include(${ZEPHYR_NRF_MODULE_DIR}/cmake/sysbuild/zip.cmake) diff --git a/sysbuild/Kconfig.mcuboot b/sysbuild/Kconfig.mcuboot index 57bb78e1078..896d3706506 100644 --- a/sysbuild/Kconfig.mcuboot +++ b/sysbuild/Kconfig.mcuboot @@ -23,6 +23,32 @@ config MCUBOOT_IMAGES_ROM_END_OFFSET_AUTO (e.g. "smp_svr;ipc_radio", "mcuboot_secondary_app"). If empty the default set of updateable images will be affected. +config MCUBOOT_MANIFEST_UPDATES + bool "Manifest-based updates" + depends on SOC_NRF54H20 + depends on (MCUBOOT_UPDATEABLE_IMAGES > 1) && (MCUBOOT_MODE_DIRECT_XIP || MCUBOOT_MODE_DIRECT_XIP_WITH_REVERT) + help + When enabled, supports transactional updates using manifests. + This allows multiple images to be updated atomically. The manifest + is a separate TLV which contains a list of images to update and + their expected hash values. The manifest TLV is a part of an image + that is signed to prevent tampering. + The manifest must be transferred as part of the image with index 0. + It can be a dedicated image, or part of an existing image. + If the second option is selected, all updates must contain an update + for image 0. + +if MCUBOOT_MANIFEST_UPDATES + +config MCUBOOT_MANIFEST_IMAGE_INDEX + int "Index of the image that must include manifest" + default 0 + range 0 MCUBOOT_UPDATEABLE_IMAGES + help + Specifies the index of the image that must include the manifest. + +endif # MCUBOOT_MANIFEST_UPDATES + config MCUBOOT_BUILD_DIRECT_XIP_VARIANT bool "Build DirectXIP variant image" depends on MCUBOOT_MODE_DIRECT_XIP || MCUBOOT_MODE_DIRECT_XIP_WITH_REVERT diff --git a/west.yml b/west.yml index 2849215c088..226d8507b69 100644 --- a/west.yml +++ b/west.yml @@ -126,7 +126,7 @@ manifest: compare-by-default: true - name: mcuboot repo-path: sdk-mcuboot - revision: 3839107e52c7228eba123129a3806fb3391781d6 + revision: 9ba25b87567fceaea2e290fbda6be30ebc00625c path: bootloader/mcuboot - name: qcbor url: https://github.com/laurencelundblade/QCBOR