diff --git a/cmake/sysbuild/image_signing.cmake b/cmake/sysbuild/image_signing.cmake index c3e0013c23ae..88298d5907ac 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 000000000000..c0559875ba2a --- /dev/null +++ b/cmake/sysbuild/mcuboot_manifest.cmake @@ -0,0 +1,89 @@ +# +# 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") + +# Since the default (merged) image is excluded from the manifest, because it contains the manifest +# itself, there is no need to construct the manifest when building a merged image. +if(NOT SB_CONFIG_MCUBOOT_SIGN_MERGED_BINARY) + sysbuild_get(manifest_img IMAGE mcuboot VAR CONFIG_MCUBOOT_MANIFEST_IMAGE_NUMBER 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 000000000000..8c773b7d17d1 --- /dev/null +++ b/samples/dfu/ab_split/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# 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) +target_sources(app PRIVATE 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 000000000000..98160fae0f97 --- /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 000000000000..5983e84568ac --- /dev/null +++ b/samples/dfu/ab_split/README.rst @@ -0,0 +1,185 @@ +.. _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 demand on the number of memory areas that should be individually locked from accidental writes as well as requires additional care when preparing updates, so only a compatible set of slots are 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/mcuboot/prj.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 firmware update using the A/B method. +This method allows two copies of the application in the NVM memory. +It is possible to switch between these copies without performing a swap, which significantly reduces time of device's unavailability during the update. +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 the A/B with separated slots functionality is used, the device has two slots for each application and radio firmwares: slot A and slot B. +The slots are equivalent, and the device can boot from either of them. +By design, the slot A of the application image boots the slot A of the radio image, so there is a manifest-based dependency required to correctly verify the correctness of the image pairs. +In the case of MCUboot, this is achieved by using the Direct XIP feature. +Thus, note that the terms slot 0, primary slot, slot A and slot 1, secondary slot, slot B are used interchangeably throughout the documentation. +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 will be booted (active) on the next reboot: + +1. If one of the slots contains a valid image, it is marked as valid only if the same slot of the other image is also valid. +#. If one of the slots is not valid, the other slot is selected as active. +#. If both slots are valid, the slot marked as "preferred" is selected as active. +#. If both slots 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. + +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 only supported on the nRF54H20), the currently running slot can be identified by comparing the address pointed `zephyr,code-partition` to specific node addresses defined in the device tree. +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, the following files should be sent to the device when performing an update: + +* :file:`build/mcuboot_secondary_app/zephyr/zephyr.signed.bin` - Contains the slot B of the application image. + This file should be uploaded 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. + This file should be uploaded 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. + This file should be uploaded 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. + This file should be uploaded 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, meaning it will turn on once the application is booted and blinks (turns off) in short intervals. + The number of short blinks is configurable using the :kconfig:option:`CONFIG_N_BLINKS` Kconfig option. + It will remain off if 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, meaning it will turn on once the application is booted and blinks (turns off) in short intervals. + The number of short blinks is configurable using the :kconfig:option:`CONFIG_N_BLINKS` Kconfig option. + It will remain off if the application is running from slot A. + +Button 0: + By pressing this button, the non-active slot will be selected as the preferred slot on 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 option for the sample: + +.. _CONFIG_N_BLINKS: + +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: + +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` library Kconfig option specific to 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` + +.. 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_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 000000000000..af0e3a2720c9 --- /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 000000000000..8a3e61b2d71d --- /dev/null +++ b/samples/dfu/ab_split/prj.conf @@ -0,0 +1,102 @@ +# 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_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 +CONFIG_BT_CTLR_DATA_LENGTH_MAX=251 + +# 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 diff --git a/samples/dfu/ab_split/sample.yaml b/samples/dfu/ab_split/sample.yaml new file mode 100644 index 000000000000..51ffd4ebec15 --- /dev/null +++ b/samples/dfu/ab_split/sample.yaml @@ -0,0 +1,17 @@ +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: + sysbuild: true + 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 000000000000..61f6dd3bebd3 --- /dev/null +++ b/samples/dfu/ab_split/src/ab_utils.c @@ -0,0 +1,261 @@ +/* + * 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_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) +#define IS_SLOT_B (CODE_PARTITION_OFFSET == SLOT_B_OFFSET) + +#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 toggle_slot_for_single_boot(void) +{ + int err = 0; + enum ab_boot_slot active_slot = active_boot_slot_get(); + enum boot_slot new_slot = BOOT_SLOT_NONE; + + if (active_slot == SLOT_A) { + LOG_INF("Temporarily switching slots (A -> B)"); + new_slot = BOOT_SLOT_SECONDARY; + } else if (active_slot == SLOT_B) { + LOG_INF("Temporarily switching slots (B -> A)"); + new_slot = BOOT_SLOT_PRIMARY; + } 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)) { + toggle_slot_for_single_boot(); + } +} + +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 000000000000..abaabbda163a --- /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 000000000000..1343bdbf530c --- /dev/null +++ b/samples/dfu/ab_split/src/main.c @@ -0,0 +1,41 @@ +/* + * 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" + +#define LOG_LEVEL LOG_LEVEL_DBG +#include +LOG_MODULE_REGISTER(ab_sample); + +int main(void) +{ +#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 000000000000..6cb102f84375 --- /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 000000000000..34ce53982244 --- /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 000000000000..d55fc624072a --- /dev/null +++ b/samples/dfu/ab_split/sysbuild/mcuboot/prj.conf @@ -0,0 +1,34 @@ +# 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" + 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 000000000000..b3b43b05fac1 --- /dev/null +++ b/samples/dfu/ab_split/sysbuild/nrf54h20dk_nrf54h20_memory_map.dtsi @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/ { + chosen { + nrf,bootloader-request = &boot_request; + }; +}; + +/ { + reserved-memory { + cpuapp_retained_mem: memory@e1ad000 { + compatible = "zephyr,memory-region"; + reg = <0xe1ad000 DT_SIZE_K(4)>; + zephyr,memory-region = "RetainedMem"; + 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>; + }; + }; + }; + }; +}; diff --git a/subsys/bootloader/Kconfig b/subsys/bootloader/Kconfig index 1cb4dde61090..382794e3cebf 100644 --- a/subsys/bootloader/Kconfig +++ b/subsys/bootloader/Kconfig @@ -182,4 +182,10 @@ 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_IMGTOOL_APPEND_MANIFEST + bool "MCUboot append manifest" + help + Append common manifest while signing the image. + This is a Kconfig which is informative only, the value should not be changed. + endmenu diff --git a/sysbuild/CMakeLists.txt b/sysbuild/CMakeLists.txt index 0ddb99d7b344..978072138917 100644 --- a/sysbuild/CMakeLists.txt +++ b/sysbuild/CMakeLists.txt @@ -229,6 +229,14 @@ 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_bool(${DEFAULT_IMAGE} CONFIG_NCS_MCUBOOT_IMGTOOL_APPEND_MANIFEST y) + if(SB_CONFIG_MCUBOOT_BUILD_DIRECT_XIP_VARIANT) + set_config_bool(mcuboot_secondary_app CONFIG_NCS_MCUBOOT_IMGTOOL_APPEND_MANIFEST y) + endif() + endif() + # Apply configuration to application foreach(image ${updateable_images}) foreach(mode ${application_mcuboot_modes}) @@ -331,7 +339,9 @@ function(${SYSBUILD_CURRENT_MODULE_NAME}_pre_cmake) set_config_bool(mcuboot CONFIG_BOOT_FIH_PROFILE_DEFAULT_LOW y) endif() - if(SB_CONFIG_PARTITION_MANAGER OR SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP OR SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP_WITH_REVERT OR SB_CONFIG_MCUBOOT_COMPRESSED_IMAGE_SUPPORT) + if(SB_CONFIG_PARTITION_MANAGER OR SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP + OR SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP_WITH_REVERT OR SB_CONFIG_MCUBOOT_COMPRESSED_IMAGE_SUPPORT + 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) @@ -798,6 +808,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 f13ccef4a9b5..000086bbe115 100644 --- a/sysbuild/Kconfig.mcuboot +++ b/sysbuild/Kconfig.mcuboot @@ -22,6 +22,19 @@ 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 "Enable transactional updates" + help + If y, enables support for 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. + 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 5a981d5e8523..4ebfaa3fa46c 100644 --- a/west.yml +++ b/west.yml @@ -127,7 +127,7 @@ manifest: compare-by-default: true - name: mcuboot repo-path: sdk-mcuboot - revision: 697ca33a67777cf55045d1b33ababc9511eaa227 + revision: pull/560/head path: bootloader/mcuboot - name: qcbor url: https://github.com/laurencelundblade/QCBOR