Multi-image builds

In many cases, the firmware that is programmed to a device consists of not only one application, but several separate images, where 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 in turn might be a child image to another parent image and boot that one. The most common use cases for builds consisting of multiple images are applications that require a bootloader to be present, or applications for multi-core CPUs.

When to use multiple images

An image (also referred to as executable, program, or elf file) consists of pieces of code and data that are identified by image-unique names recorded in a single symbol table. The symbol table exists as metadata in a .elf or .exe file and is not included when the image is converted to a HEX file for programming. Instead, the code and data are placed at addresses by a linker. This linking process is what distinguishes images from object files (which do not require linking). Therefore, to determine if you have zero, one, or more images, count the number of times the linker runs.

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 might required 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.

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

nRF9160 SPU configuration

The nRF9160 SiP application MCU is divided into a secure and a non-secure domain. The code in the secure domain can configure the System Protection Unit (SPU) to allow non-secure access to the CPU resources that are required by the application, and then jump to the code in the non-secure domain. Therefore, the nRF9160 samples (the parent image) require the Secure Partition Manager (the child image) to be programmed in addition to the actual application.

See nRF9160 DK and Working with nRF9160 DK for more information.

MCUboot bootloader

The MCUboot bootloader establishes a root of trust by verifying the next step in the boot sequence. This first-stage bootloader is immutable, which means it must never be updated or deleted. However, it allows to update the application, and therefore MCUboot and the application must be located in different images. In this scenario, the application is the parent image and MCUboot is the child image.

See MCUboot for more information. The MCUboot bootloader is used in the nRF9160: HTTP application update sample.

nRF5340 support

nRF5340 contains two separate processors: a network core and an application core. When programming applications to the nRF5340 DK, they must be divided into at least two images, one for each core. See Working with nRF53 Series 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 multi-image build.

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

  • Build the child image from 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 source or using a prebuilt HEX file, the build system merges the HEX files of the parent and child image together so that they can easily be programmed in one single step. This means that you might enable and integrate an additional image without even realizing it, 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 respective child image in the parent image configuration. For example, to use a prebuilt HEX file of the Secure Partition Manager instead of building it, select CONFIG_SPM_BUILD_STRATEGY_USE_HEX_FILE instead of the default CONFIG_SPM_BUILD_STRATEGY_FROM_SOURCE, and specify the HEX file in CONFIG_SPM_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 make it possible 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.

This code should be put in place in the cmake tree that is conditional on a configuration option for having the parent image use the child image. In the nRF Connect SDK, the code is included in the CMakeLists.txt file for the samples, and in the MCUboot repository.

See the following example code:

if (CONFIG_SPM)
  add_child_image(
    NAME spm
    SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/spm
    )
endif()

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 a child image is to be executed on a different core, you must specify the name space for the child image as domain when adding the child image. For example:

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

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

Adding configuration options

When enabling a child image, you select the build strategy, thus how the image is included. The three options are:

  • Build the child image from source along with the parent image - IMAGENAME_BUILD_STRATEGY_FROM_SOURCE

  • Merge the specified HEX file of the child image with the parent image - IMAGENAME_BUILD_STRATEGY_USE_HEX_FILE, and IMAGENAME_HEX_FILE to specify the HEX file

  • Ignore the child image when building and build only the parent image - IMAGENAME_BUILD_STRATEGY_SKIP_BUILD

Note

Child images that are built with the build strategy IMAGENAME_BUILD_STRATEGY_SKIP_BUILD or IMAGENAME_BUILD_STRATEGY_USE_HEX_FILE must define a static partition.

You must add these four configuration options to the Kconfig file for your child image, replacing IMAGENAME with the (uppercase) name of your child image (as specified in add_child_image).

This can be done by including the Kconfig.template.build_strategy template, as shown below.

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

Image-specific variables

The child image and parent image 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 IMAGENAME_FOO in a parent image is propagated to the child image named IMAGENAME as FOO.

  • Variables that are in the list SHARED_MULTI_IMAGE_VARIABLES are propagated to all child images.

With these two mechanisms, it is possible to set variables in child images from either parent images or the command line, and it is possible to set variables globally across all images. For example, to change the CONF_FILE variable for the MCUboot image and the parent image, specify the CMake command as follows:

cmake -Dmcuboot_CONF_FILE=prj_a.conf -DCONF_FILE=app_prj.conf

You can extend the CMake command that is 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.

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, the same prefix convention is used as for variables. This means that to run menuconfig, for example, you 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 SEGGER Embedded Studio, you must set these environment variables before starting SES, and they will apply only to the build of the child images. On the command line, you must set them before invoking west, and they will apply to both the parent image and the child images. For example, to build with verbose output and one parallel job, use the following commands (where board_name is the name of the board for which you are building):

  • Linux/macOS:

    $ VERBOSE=True CMAKE_BUILD_PARALLEL_LEVEL=1 west build -b board_name
  • Windows:

    > set VERBOSE=True && set CMAKE_BUILD_PARALLEL_LEVEL=1 && west build -b board_name

Memory placement

In a multi-image build, all images must be placed in memory so that they do not overlap. The flash 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 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.