Multi-image builds

The firmware programmed to a device can be composed of either one application or several separate images. In the latter case, one of the images (the parent image) requires one or more other images (the child images) to be present. The child image then chain-loads, or boots, the parent image, which could also be a child image to another parent image, and boots that one.

The most common use cases for builds composed of multiple images are applications that require a bootloader to be present or applications for multi-core CPUs.

What image files are

The image file can refer to an executable, a program, or an ELF file. As one of the last build steps, the linker processes all object files by locating code, data, and symbols in sections in the final ELF file. The linker replaces all symbol references to code and data with addresses. A symbol table is created which maps addresses to symbol names, which is used by debuggers. When an ELF file is converted into another format, such as HEX or binary, the symbol table is lost.

Depending on the application and the SoC, you can use one or several images.

Using multiple images has the following advantages:

  • You can run the linker multiple times and partition the final firmware into several regions. This partitioning is often useful for bootloaders.

  • Since there is a symbol table for each image, the same symbol names can exist multiple times in the final firmware. This is useful for bootloader images, which can require their own copy of a library that the application uses, but in a different version or configuration.

  • In multi-core builds, the build configuration of a child image in a separate core can be made known to the parent image.

The following table lists build files that can be generated as output when building firmware for supported build targets. The table includes files for single-core and multi-core programming scenarios for both Visual Studio Code and command line building methods. Which files you are going to use depends on the application configuration and not directly on the type of SoC you are using. The following scenarios are possible:

  • Single-image - Only one firmware image file is generated for a single core.

  • Multi-image - Two or more firmware image files are generated for a single core. You can read more about this scenario in Multi-image builds.

  • Multi-core - Two or more firmware image files are generated for two or more cores.

File

Description

Programming scenario

zephyr.hex

Default full image. In a multi-image build, several zephyr.hex files are generated, one for each image.

  • Programming build targets with NSPE or single-image.

  • Testing DFU procedure with nrfjprog (programming directly to device).

merged.hex

The result of merging all zephyr.hex files for all images for a core in a multi-image build. Used by Nordic Semiconductor’s build targets in single-core multi-image builds. In multi-core builds, several merged_<image_name>.hex fields are generated, where <image-name> indicates the core.

  • Programming multi-core application.

  • Testing DFU procedure with nrfjprog (programming directly to device).

merged_domain.hex

The result of merging all merged.hex files for all cores or processing environments (merged.hex for the application core and merged.hex or zephyr.hex for the network core).

  • Programming SPE-only and multi-core build targets.

  • Testing DFU procedure with nrfjprog (programming directly to device).

tfm_s.hex

Secure firmware image created by the TF-M build system in the background of the Zephyr build. It is used together with the zephyr.hex file, which is intended for the Non-Secure Processing Environment (NSPE). Located in build/tfm/bin.

Programming SPE-only and multi-core build targets.

app_update.bin

Application core update file used to create dfu_application.zip for multi-core DFU. Can also be used standalone for a single-image DFU. Contains the signed version of the application. This file is transferred in the real-life update procedure, as opposed to HEX files that are transferred with nrfjprog when emulating an update procedure. Compatible with MCUboot.

DFU process for single-image build targets and the application core of the multi-core build targets.

app_signed.hex

HEX file variant of the app_update.bin file. Can also be used standalone for a single-image DFU. Contains the signed version of the application. Compatible with MCUboot.

Programming single-image build targets and the application core of the multi-core build targets.

net_core_app_update.bin

Network core update file used to create dfu_application.zip. This file is transferred in the real-life update procedure, as opposed to HEX files that are transferred with nrfjprog when emulating an update procedure.

DFU process for the network core of multi-core build targets.

dfu_application.zip

Zip file containing both the MCUboot-compatible update image for one or more cores and a manifest describing its contents.

DFU process for both single-core and multi-core applications.

matter.ota

Matter-specific OTA image that contains a Matter-compliant header and a DFU multi-image package that bundles user-selected firmware images.

DFU over Matter for both single-core and multi-core applications.

<file_name>.zigbee

Zigbee-specific OTA image that contains the Zigbee application with the Zigbee OTA header used for providing information about the image to the OTA server. The <file_name> includes manufacturer’s code, image type, file version, and comment (customizable by user, sample name by default). For example: 127F-0141-01020003-light_switch.zigbee.

DFU over Zigbee for both single-core and multi-core applications in the nRF Connect SDK v2.0.0 and later.

For more information on other build output files refer to Build and configuration system page.

When to use multiple images

In the nRF Connect SDK, multiple images are required in the following scenarios:

First-stage and second-stage bootloaders

The first-stage bootloader establishes a root of trust by verifying the next step in the boot sequence. This first-stage bootloader is immutable, which means it cannot be updated or deleted. If a second-stage bootloader is present, then the first-stage bootloader is responsible for booting and updating the second-stage bootloader, which in turn is responsible for booting and updating the application. As such, the first-stage bootloader, the second-stage bootloader, and the application must be located in different images. In this scenario, the application is the parent image and the bootloaders are two separate child images.

See Secure bootloader chain and Adding a bootloader chain for more information.

nRF5340 development kit support

The nRF5340 development kit (DK) contains two separate processors: a network core and an application core. When programming an application for the nRF5340 DK, the application must be divided into at least two images, one for each core.

See Working with nRF5340 DK for more information.

nRF5340 Audio development kit support

The nRF5340 Audio development kit (DK) is based on the nRF5340 development kit and also contains two separate processors. When programming an application for the nRF5340 Audio DK, the application core image is built from a combination of different configuration files. The network core image is programmed with an application-specific precompiled Bluetooth Low Energy Controller binary file that contains the LE Audio Controller Subsystem for nRF53.

See the nRF5340 Audio application documentation for more information.

Default configuration

The nRF Connect SDK samples are set up to build all related images as one solution, starting from the parent image. This is referred to as a multi-image build.

When building the parent image, you can configure how the child image should be handled:

  • Build the child image from the source and include it with the parent image. This is the default setting.

  • Use a prebuilt HEX file of the child image and include it with the parent image.

  • Ignore the child image.

When building the child image from the source or using a prebuilt HEX file, the build system merges the HEX files of both the parent and child image, so that they can be programmed in one single step. This means that you can enable and integrate an additional image just by using the default configuration.

To change the default configuration and configure how a child image is handled, locate the BUILD_STRATEGY configuration options for the child image in the parent image configuration. For example, to use a prebuilt HEX file of the MCUboot instead of building it, select CONFIG_MCUBOOT_BUILD_STRATEGY_USE_HEX_FILE instead of the default CONFIG_MCUBOOT_BUILD_STRATEGY_FROM_SOURCE, and specify the HEX file in CONFIG_MCUBOOT_HEX_FILE. To ignore an MCUboot child image, select CONFIG_MCUBOOT_BUILD_STRATEGY_SKIP_BUILD instead of CONFIG_MCUBOOT_BUILD_STRATEGY_FROM_SOURCE.

Defining and enabling a child image

You can enable existing child images in the nRF Connect SDK by enabling the respective modules in the parent image and selecting the desired build strategy.

To turn an application that you have implemented into a child image that can be included in a parent image, you must update the build scripts to enable the child image and add the required configuration options. You should also know how image-specific variables are disambiguated and what targets of the child images are available.

Updating the build scripts

To make it possible to enable a child image from a parent image, you must include the child image in the build script. If you need to perform this operation out-of-tree (that is, without modifying NCS code), or from the top-level CMakeLists.txt in your sample, see Adding a child image using Zephyr modules.

To do so, place the code from the following example in the CMake tree that is conditional on a configuration option. In the nRF Connect SDK, the code is included in the CMakeLists.txt file for the samples, and in the MCUboot repository.

if (CONFIG_SECURE_BOOT)
  add_child_image(
    NAME b0
    SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/bootloader
    )
endif()

if (CONFIG_BOOTLOADER_MCUBOOT)
   add_child_image(
     NAME mcuboot
     SOURCE_DIR ${MCUBOOT_DIR}/boot/zephyr
     )
endif()

In this code, add_child_image registers the child image with the given name and file path and executes the build scripts of the child image. Note that both the child image’s application build scripts and the core build scripts are executed. The core build scripts might use a different configuration and possibly different devicetree settings.

If you have to execute a child image on a different core, you must specify the namespace for the child image as domain when adding the child image. See the following example:

add_child_image(
   NAME hci_rpmsg
   SOURCE_DIR ${ZEPHYR_BASE}/samples/bluetooth/hci_rpmsg
   DOMAIN CPUNET
   )

A domain is well-defined if there is the CONFIG_DOMAIN_${DOMAIN}_BOARD configuration option in Kconfig.

Adding a child image using Zephyr modules

Any call to add_child_image must be done after nrf/cmake/extensions.cmake is invoked, but before multi_image.cmake is invoked. In some scenarios, this is not possible without modifying the NCS build code, for example, from top-level sample files and project CMakeLists.txt files.

To avoid this issue, use the Modules mechanism provided by the Zephyr build system. The following example shows how to add the required module from a top-level sample CMakeLists.txt.

cmake_minimum_required(VERSION 3.20.0)

set(ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_LIST_DIR})

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(app)

target_sources(app PRIVATE src/main.c)

A zephyr/module.yml file is needed at the base of the added module. The following example specifies only the path to the CMakeLists.txt of the new module. See Modules (External projects) for more details.

build:
  cmake: aci

The CMakeLists.txt located in the directory pointed to by zephyr/module.yml will be invoked when add_child_image can be invoked.

  add_child_image(
    NAME cpunet
    SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/../cpunet
    DOMAIN CPUNET
    BOARD ${CONFIG_DOMAIN_CPUNET_BOARD}
    )

Adding configuration options

When enabling a child image, you must select the build strategy to define how the image should be included. The following three options are available:

  • <IMAGE_NAME>_BUILD_STRATEGY_FROM_SOURCE - Build the child image from source along with the parent image.

  • <IMAGE_NAME>_BUILD_STRATEGY_USE_HEX_FILE - Merge the specified HEX file of the child image with the parent image, using <IMAGE_NAME>_HEX_FILE to specify the HEX file.

  • <IMAGE_NAME>_BUILD_STRATEGY_SKIP_BUILD - Ignore the child image when building and build only the parent image.

Note

Child images that are built with the build strategy <IMAGE_NAME>_BUILD_STRATEGY_SKIP_BUILD or <IMAGE_NAME>_BUILD_STRATEGY_USE_HEX_FILE must define a static partition.

Add these configuration options to the Kconfig file of your child image, replacing <IMAGE_NAME> with the uppercase name of your child image that is specified in add_child_image. To do this, include the Kconfig.template.build_strategy template as follows:

module=MCUBOOT
source "${ZEPHYR_NRF_MODULE_DIR}/subsys/partition_manager/Kconfig.template.build_strategy"

Image-specific variables

The child and parent images are executed in different CMake processes and thus have different namespaces.

Variables in the parent image are not propagated to the child image, with the following exceptions:

  • Any variable named <IMAGE_NAME>_VARIABLEONE in a parent image is propagated to the child image named <IMAGE_NAME> as VARIABLEONE.

  • CMake build settings, such as BOARD_DIR, build type, toolchain info, partition manager info, and similar are always passed to child images.

With these two mechanisms, you can set variables in child images from either parent images or the command line, and you can also set variables globally across all images. For example, to change the VARIABLEONE variable for the childimageone child image and the parent image, specify the CMake command as follows:

 cmake -Dchildimageone_VARIABLEONE=value -DVARIABLEONE=value

You can extend the CMake command used to create the child images by adding flags to the CMake variable EXTRA_MULTI_IMAGE_CMAKE_ARGS. For example, add --trace-expand to that variable to output more debug information.

With west, you can pass these configuration variables into CMake by using the -- separator:

west build -b nrf52840dk_nrf52840 zephyr/samples/hello_world -- \
-Dmcuboot_CONF_FILE=prj_a.conf \
-DCONF_FILE=app_prj.conf

You can make a project pass Kconfig configuration files, fragments, and devicetree overlays to child images by placing them in the child_image folder in the application source directory. The listing below describes how to leverage this functionality, where ACI_NAME is the name of the child image to which the configuration will be applied.

    # It is possible for a sample to use a custom set of Kconfig fragments for a
    # child image, or to append additional Kconfig fragments to the child image.
    # Note that <ACI_NAME> in this context is the name of the child image as
    # passed to the 'add_child_image' function.
    #
    # <child-sample> DIRECTORY
    # | - prj.conf (A)
    # | - prj_<buildtype>.conf (B)
    # | - boards DIRECTORY
    # | | - <board>.conf (C)
    # | | - <board>_<buildtype>.conf (D)


    # <current-sample> DIRECTORY
    # | - prj.conf
    # | - prj_<buildtype>.conf
    # | - child_image DIRECTORY
    #     |-- <ACI_NAME>.conf (I)                 Fragment, used together with (A) and (C)
    #     |-- <ACI_NAME>_<buildtype>.conf (J)     Fragment, used together with (B) and (D)
    #     |-- <ACI_NAME>.overlay                  If present, will be merged with BOARD.dts
    #     |-- <ACI_NAME> DIRECTORY
    #         |-- boards DIRECTORY
    #         |   |-- <board>.conf (E)            If present, use instead of (C), requires (G).
    #         |   |-- <board>_<buildtype>.conf (F)     If present, use instead of (D), requires (H).
    #         |   |-- <board>.overlay             If present, will be merged with BOARD.dts
    #         |   |-- <board>_<revision>.overlay  If present, will be merged with BOARD.dts
    #         |-- prj.conf (G)                    If present, use instead of (A)
    #         |                                   Note that (C) is ignored if this is present.
    #         |                                   Use (E) instead.
    #         |-- prj_<buildtype>.conf (H)        If present, used instead of (B) when user
    #         |                                   specify `-DCONF_FILE=prj_<buildtype>.conf for
    #         |                                   parent image. Note that any (C) is ignored
    #         |                                   if this is present. Use (F) instead.
    #         |-- <board>.overlay                 If present, will be merged with BOARD.dts
    #         |-- <board>_<revision>.overlay      If present, will be merged with BOARD.dts
    #
    # Note: The folder `child_image/<ACI_NAME>` is only need when configurations
    #       files must be used instead of the child image default configs.
    #       The append a child image default config, place the additional settings
    #       in `child_image/<ACI_NAME>.conf`.

Variables in child images

It is possible to provide configuration settings for child images, either as individual settings or using Kconfig fragments. Each child image is referenced using its image name.

The following example sets the configuration option CONFIG_VARIABLEONE=val in the child image childimageone:

 cmake -Dchildimageone_CONFIG_VARIABLEONE=val[...]

You can add a Kconfig fragment to the child image default configuration in a similar way. The following example adds an extra Kconfig fragment extrafragment.conf to childimageone:

 cmake -Dchildimageone_OVERLAY_CONFIG=extrafragment.conf[...]

It is also possible to provide a custom configuration file as a replacement for the default Kconfig file for the child image. The following example uses the custom configuration file myfile.conf when building childimageone:

 cmake -Dchildimageone_CONF_FILE=myfile.conf[...]

If your application includes multiple child images, then you can combine all the above as follows:

  • Setting CONFIG_VARIABLEONE=val in the main application.

  • Adding a Kconfig fragment extrafragment.conf to the childimageone child image, using -Dchildimageone_OVERLAY_CONFIG=extrafragment.conf.

  • Using myfile.conf as configuration for the quz child image, using -Dquz_CONF_FILE=myfile.conf.

     cmake -DCONFIG_VARIABLEONE=val -Dchildimageone_OVERLAY_CONFIG=extrafragment.conf-Dquz_CONF_FILE=myfile.conf[...]

See Secure bootloader chain for more details.

Note

The build system grabs the Kconfig fragment or configuration file specified in a CMake argument relative to that image’s application directory. For example, the build system uses nrf/samples/bootloader/my-fragment.conf when building with the -Db0_OVERLAY_CONFIG=my-fragment.conf option, whereas -DOVERLAY_CONFIG=my-fragment.conf grabs the fragment from the main application’s directory, such as zephyr/samples/hello_world/my-fragment.conf.

You can also merge multiple fragments into the overall configuration for an image by giving a list of Kconfig fragments as a string, separated using ;. The following example shows how to combine abc.conf, Kconfig fragment of the childimageone child image, with the extrafragment.conf fragment:

 cmake -Dchildimageone_OVERLAY_CONFIG='extrafragment.conf;abc.conf'

When the build system finds the fragment, it outputs their merge during the CMake build output as follows:

...
Merged configuration 'extrafragment.conf'
Merged configuration 'abc.conf'
...

Child image devicetree overlays

You can provide devicetree overlays for a child image using *.overlay files.

The following example sets the devicetree overlay extra.overlay to childimageone:

cmake -Dchildimageone_DTC_OVERLAY_FILE='extra.overlay'

The build system does also automatically apply any devicetree overlay located in the child_image folder and named as follows (where ACI_NAME is the name of the child image):

  • child_image/<ACI_NAME>.overlay

  • child_image/<ACI_NAME>/<board>.overlay

  • child_image/<ACI_NAME>/<board>_<revision>.overlay

  • child_image/<ACI_NAME>/boards/<board>.overlay

  • child_image/<ACI_NAME>/boards/<board>_<revision>.overlay

Note

The build system grabs the devicetree overlay files specified in a CMake argument relative to that image’s application directory. For example, the build system uses nrf/samples/bootloader/my-dts.overlay when building with the -Db0_DTC_OVERLAY_FILE=my-dts.overlay option, whereas -DDTC_OVERLAY_FILE=my-dts.overlay grabs the fragment from the main application’s directory, such as zephyr/samples/hello_world/my-dts.overlay.

Child image targets

You can indirectly invoke a selection of child image targets from the parent image. Currently, the child targets that can be invoked from the parent targets are menuconfig, guiconfig, and any targets listed in EXTRA_KCONFIG_TARGETS.

To disambiguate targets, use the same prefix convention used for variables. For example, to run menuconfig, invoke the menuconfig target to configure the parent image and mcuboot_menuconfig to configure the MCUboot child image.

You can also invoke any child target directly from its build directory. Child build directories are located at the root of the parent’s build directory.

Controlling the build process

The child image is built using CMake’s build command cmake --build. This mechanism allows additional control of the build process through CMake.

CMake options

The following CMake options are propagated from the CMake command of the parent image to the CMake command of the child image:

  • CMAKE_BUILD_TYPE

  • CMAKE_VERBOSE_MAKEFILE

You can add other CMake options to a specific child image in the same way as you can set Image-specific variables. For example, add -Dmcuboot_CMAKE_VERBOSE_MAKEFILE to the parent’s CMake command to build the mcuboot child image with verbose output.

To enable additional debug information for the multi-image build command, set the CMake option MULTI_IMAGE_DEBUG_MAKEFILE to the desired debug mode. For example, add -DMULTI_IMAGE_DEBUG_MAKEFILE=explain to log the reasons why a command was executed.

See Providing CMake options for instructions on how to specify these CMake options for the build.

CMake environment variables

Unlike CMake options, CMake environment variables allow you to control the build process without re-invoking CMake.

You can use the CMake environment variables VERBOSE and CMAKE_BUILD_PARALLEL_LEVEL to control the verbosity and the number of parallel jobs for a build:

When using the command line or Visual Studio Code terminal window, you must set them before invoking west. They apply to both the parent and child images. For example, to build with verbose output and one parallel job, use the following command, where build_target is the target for the development kit for which you are building:

west build -b build_target -- -DCMAKE_VERBOSE_MAKEFILE=1 -DCMAKE_BUILD_PARALLEL_LEVEL=1

Memory placement

In a multi-image build, all images must be placed in memory so that they do not overlap. The flash memory start address for each image must be specified by, for example, CONFIG_FLASH_LOAD_OFFSET.

Hardcoding the image locations like this works fine for simple use cases like a bootloader that prepares a device, where there are no further requirements on where in memory each image must be placed. However, more advanced use cases require a memory layout where images are located in a specific order relative to one another.

The nRF Connect SDK provides a Python tool that allows you to specify this kind of relative placement or even a static placement based on start addresses and sizes for the different images.

See Partition Manager for more information about how to set up your partitions and configure your build system.