Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cmake/modules/kconfig.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ if(CONFIG_NCS_IS_VARIANT_IMAGE)

# Modify the CONFIG_FLASH_LOAD_OFFSET and CONFIG_FLASH_LOAD_SIZE for both the .config and autoconf.h files.
# If partition manager is not used, these values should be taken from the device tree.
# Additionally, convert primary slot dependencies to secondary slot dependencies.
set(dotconfig_variant_content)
foreach(line IN LISTS dotconfig_content)
if("${line}" MATCHES "^CONFIG_FLASH_LOAD_OFFSET=.*$")
Expand All @@ -49,6 +50,10 @@ if(CONFIG_NCS_IS_VARIANT_IMAGE)
string(REGEX REPLACE "CONFIG_FLASH_LOAD_SIZE=(.*)" "CONFIG_FLASH_LOAD_SIZE=${code_partition_size}" line ${line})
endif()

if("${line}" MATCHES "(--dependencies|-d).*\([0-9, ]+primary[0-9., ]+\)")
string(REGEX REPLACE "primary" "secondary" line ${line})
endif()

list(APPEND dotconfig_variant_content "${line}\n")
endforeach()

Expand All @@ -62,6 +67,10 @@ if(CONFIG_NCS_IS_VARIANT_IMAGE)
string(REGEX REPLACE "#define CONFIG_FLASH_LOAD_SIZE (.*)" "#define CONFIG_FLASH_LOAD_SIZE ${code_partition_size}" line ${line})
endif()

if("${line}" MATCHES "(--dependencies|-d).*\([0-9, ]+primary[0-9., ]+\)")
string(REGEX REPLACE "primary" "secondary" line ${line})
endif()

list(APPEND autoconf_variant_content "${line}\n")
endforeach()

Expand Down
97 changes: 66 additions & 31 deletions cmake/sysbuild/zip.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ function(dfu_app_zip_package)
# Network core
get_property(image_name GLOBAL PROPERTY DOMAIN_APP_CPUNET)
set(net_update_name "${image_name}.bin")
set(secondary_net_update_name "${image_name}_secondary_app.bin")
sysbuild_get(net_core_board IMAGE ${image_name} VAR BOARD CACHE)

if(SB_CONFIG_SECURE_BOOT_NETCORE)
Expand All @@ -204,42 +205,76 @@ function(dfu_app_zip_package)
mcuboot_image_number_to_slot(net_update_slot_primary ${SB_CONFIG_MCUBOOT_NETWORK_CORE_IMAGE_NUMBER} n)
mcuboot_image_number_to_slot(net_update_slot_secondary ${SB_CONFIG_MCUBOOT_NETWORK_CORE_IMAGE_NUMBER} y)

if(SB_CONFIG_PARTITION_MANAGER)
set(net_load_address "$<TARGET_PROPERTY:partition_manager,CPUNET_PM_APP_ADDRESS>")
else()
get_address_from_dt_partition_nodelabel("slot${net_update_slot_primary}_partition"
net_load_address
)
endif()
if(NOT SB_CONFIG_MCUBOOT_BUILD_DIRECT_XIP_VARIANT)
if(SB_CONFIG_PARTITION_MANAGER)
set(net_load_address "$<TARGET_PROPERTY:partition_manager,CPUNET_PM_APP_ADDRESS>")
else()
get_address_from_dt_partition_nodelabel("slot${net_update_slot_primary}_partition"
net_load_address
)
endif()

math(EXPR net_update_slot_primary "${net_update_slot_primary} + 1")
math(EXPR net_update_slot_secondary "${net_update_slot_secondary} + 1")
math(EXPR net_update_slot_primary "${net_update_slot_primary} + 1")
math(EXPR net_update_slot_secondary "${net_update_slot_secondary} + 1")

set(generate_script_app_params
${generate_script_app_params}
"${net_update_name}image_index=${SB_CONFIG_MCUBOOT_NETWORK_CORE_IMAGE_NUMBER}"
"${net_update_name}slot_index_primary=${net_update_slot_primary}"
"${net_update_name}slot_index_secondary=${net_update_slot_secondary}"
"${net_update_name}load_address=${net_load_address}"
"${net_update_name}version=${net_update_version}"
"${net_update_name}board=${net_core_board}"
"${net_update_name}soc=${SB_CONFIG_SOC}"
)

if(SB_CONFIG_SECURE_BOOT_NETCORE)
list(APPEND bin_files "${CMAKE_BINARY_DIR}/signed_by_mcuboot_and_b0_${image_name}.bin")
list(APPEND signed_targets ${image_name}_signed_packaged_target)
else()
sysbuild_get(net_CONFIG_KERNEL_BIN_NAME IMAGE ${image_name} VAR CONFIG_KERNEL_BIN_NAME KCONFIG)
if(SB_CONFIG_BOOT_ENCRYPTION)
list(APPEND bin_files "${CMAKE_BINARY_DIR}/${image_name}/zephyr/${net_CONFIG_KERNEL_BIN_NAME}.signed.encrypted.bin")
else()
list(APPEND bin_files "${CMAKE_BINARY_DIR}/${image_name}/zephyr/${net_CONFIG_KERNEL_BIN_NAME}.signed.bin")
endif()
endif()

list(APPEND zip_names "${net_update_name}")
list(APPEND signed_targets ${image_name}_extra_byproducts)
elseif(NOT SB_CONFIG_PARTITION_MANAGER)
get_address_from_dt_partition_nodelabel("slot${net_update_slot_primary}_partition" primary_net_load_address)
get_address_from_dt_partition_nodelabel("slot${net_update_slot_secondary}_partition" secondary_net_load_address)

set(generate_script_app_params
# Radio in DirectXIP mode
set(generate_script_app_params
${generate_script_app_params}
"${net_update_name}image_index=${SB_CONFIG_MCUBOOT_NETWORK_CORE_IMAGE_NUMBER}"
"${net_update_name}slot_index_primary=${net_update_slot_primary}"
"${net_update_name}slot_index_secondary=${net_update_slot_secondary}"
"${net_update_name}load_address=${net_load_address}"
"${net_update_name}version=${net_update_version}"
"${net_update_name}board=${net_core_board}"
"${net_update_name}soc=${SB_CONFIG_SOC}"
)
"${net_update_name}load_address=${primary_net_load_address}"
"${net_update_name}version_MCUBOOT+XIP=${CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION}"
"${net_update_name}image_index=${SB_CONFIG_MCUBOOT_NETWORK_CORE_IMAGE_NUMBER}"
"${net_update_name}slot=${net_update_slot_primary}"
"${secondary_net_update_name}load_address=${secondary_net_load_address}"
"${secondary_net_update_name}image_index=${SB_CONFIG_MCUBOOT_NETWORK_CORE_IMAGE_NUMBER}"
"${secondary_net_update_name}slot=${net_update_slot_secondary}"
"${secondary_net_update_name}version_MCUBOOT+XIP=${CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION}"
)
list(APPEND bin_files
"${CMAKE_BINARY_DIR}/${image_name}/zephyr/${CONFIG_KERNEL_BIN_NAME}.signed.bin"
"${CMAKE_BINARY_DIR}/${image_name}_secondary_app/zephyr/${CONFIG_KERNEL_BIN_NAME}.signed.bin"
)
set(exclude_files EXCLUDE
${CMAKE_BINARY_DIR}/${image_name}/zephyr/${CONFIG_KERNEL_BIN_NAME}.signed.bin
${CMAKE_BINARY_DIR}/${image_name}_secondary_app/zephyr/${CONFIG_KERNEL_BIN_NAME}.signed.bin
)
set(include_files INCLUDE
${CMAKE_BINARY_DIR}/${image_name}/zephyr/${CONFIG_KERNEL_BIN_NAME}.bin
${CMAKE_BINARY_DIR}/${image_name}_secondary_app/zephyr/${CONFIG_KERNEL_BIN_NAME}.bin
)

if(SB_CONFIG_SECURE_BOOT_NETCORE)
list(APPEND bin_files "${CMAKE_BINARY_DIR}/signed_by_mcuboot_and_b0_${image_name}.bin")
list(APPEND signed_targets ${image_name}_signed_packaged_target)
else()
sysbuild_get(net_CONFIG_KERNEL_BIN_NAME IMAGE ${image_name} VAR CONFIG_KERNEL_BIN_NAME KCONFIG)
if(SB_CONFIG_BOOT_ENCRYPTION)
list(APPEND bin_files "${CMAKE_BINARY_DIR}/${image_name}/zephyr/${net_CONFIG_KERNEL_BIN_NAME}.signed.encrypted.bin")
else()
list(APPEND bin_files "${CMAKE_BINARY_DIR}/${image_name}/zephyr/${net_CONFIG_KERNEL_BIN_NAME}.signed.bin")
endif()
list(APPEND zip_names "${net_update_name};${secondary_net_update_name}")
list(APPEND signed_targets ${image_name}_extra_byproducts)
list(APPEND signed_targets ${image_name}_secondary_app_extra_byproducts)
endif()

list(APPEND zip_names "${net_update_name}")
list(APPEND signed_targets ${image_name}_extra_byproducts)
endif()

if(SB_CONFIG_DFU_ZIP_WIFI_FW_PATCH)
Expand Down
24 changes: 24 additions & 0 deletions samples/dfu/ab_split/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 14 additions & 0 deletions samples/dfu/ab_split/Kconfig
Original file line number Diff line number Diff line change
@@ -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"
194 changes: 194 additions & 0 deletions samples/dfu/ab_split/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
.. _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 <ab_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 slot-based dependency management, the :kconfig:option:`CONFIG_BOOT_VERSION_CMP_USE_SLOT_NUMBER` 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 slot-based dependency required to correctly verify the correctness of the images.
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.

In addition, set the :kconfig:option:`CONFIG_MCUBOOT_EXTRA_IMGTOOL_ARGS` library Kconfig option specific to the MCUboot library to specify the slot-specific image dependencies.
For example, to ensure that there is a valid radio image with version ``1.0.0`` or greater in the radio primary slot set:

.. code-block:: console

CONFIG_MCUBOOT_EXTRA_IMGTOOL_ARGS="--dependencies \"(0,primary,1.0.0)\""

The slot-specific dependency is automatically converted to point to the secondary slot when building the second variant of each image.

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 <mcuboot_index_ncs>`
13 changes: 13 additions & 0 deletions samples/dfu/ab_split/boards/nrf54h20dk_nrf54h20_cpuapp.overlay
Original file line number Diff line number Diff line change
@@ -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;
};
};
Loading
Loading