Using QSPI XIP split image
The QSPI XIP split image feature lets you gain more flash storage space for applications by splitting application code into two parts:
Code that runs on the internal flash memory.
Code that runs on supported external flash memory over the Quad Serial Peripheral Interface (QSPI) using Execute in Place (XIP).
This feature is supported on nRF52840 and nRF5340.
Caution
On the nRF52840, do not relocate interrupts to the QSPI XIP flash. Doing so can lock up or brick the device by making the debug access port inaccessible.
The QSPI XIP split images are supported in MCUboot, which allows for updating them over-the-air.
For the nRF5340 DK and Nordic Thingy:53, you can also check out the SMP Server with external XIP sample, which demonstrates this feature.
Requirements
To use this feature, meet the following requirements:
Board based on nRF52840 or nRF5340, with file structure compatible with Hardware model v2 (HWMv2)
External QSPI flash chip with supported commands connected to QSPI pins
QSPI flash chip in always-on mode (meaning, no DPM or low-power modes)
MCUboot configuration in the Swap using move mode (
MCUBOOT_SWAP_USING_MOVE), the Upgrade only mode (MCUBOOT_OVERWRITE_ONLY), or in the direct-XIP modeMemory layout defined by devicetree-based partitioning
Linker file with QSPI XIP flash offset and size
Caution
Currently, MCUboot images cannot be linked together. This means that both parts of the firmware update must be loaded properly before an update is initiated. If one part fails to update, the image on the device will be unbootable and that will likely brick the device.
Network core image updates
The QSPI XIP split image feature supports network core image updates on nRF5340 devices if MCUboot is configured for using either the swap-using-move or the upgrade-only mode.
Create the memory layout with devicetree files
Create the following files:
A project
cpuappboard devicetree overlay that defines the partition nodes used on the device (boot_partition,slot0_partition, externalslot1_partitionthroughslot5_partition, and related nodes). The following files from the SMP Server with external XIP sample provide an example of the devicetree memory layouts for nRF5340:/* * Copyright (c) 2026 Nordic Semiconductor ASA * * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause * * Memory map for QSPI XIP split image with network core update. * External slots: slot1 = app secondary, slot3 = netcore secondary, * slot4/5 = QSPI XIP primary/secondary. */ /delete-node/ &boot_partition; /delete-node/ &slot0_partition; /delete-node/ &slot1_partition; /delete-node/ &storage_partition; &flash0 { partitions { boot_partition: partition@0 { compatible = "zephyr,mapped-partition"; label = "mcuboot"; reg = <0x00000000 DT_SIZE_K(64)>; }; slot0_partition: partition@10000 { compatible = "zephyr,mapped-partition"; label = "image-0"; reg = <0x00010000 0x000d4000>; }; storage_partition: partition@f0000 { compatible = "zephyr,mapped-partition"; label = "storage"; reg = <0x000f0000 DT_SIZE_K(64)>; }; }; }; &mx25r64 { status = "okay"; partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; slot1_partition: partition@0 { label = "image-1"; reg = <0x00000000 0x000d4000>; }; slot3_partition: partition@d4000 { label = "image-3"; reg = <0x000d4000 DT_SIZE_K(256)>; }; slot4_partition: partition@120000 { label = "image-4"; reg = <0x00120000 DT_SIZE_K(256)>; }; slot5_partition: partition@160000 { label = "image-5"; reg = <0x00160000 DT_SIZE_K(256)>; }; }; };
/* * Copyright (c) 2026 Nordic Semiconductor ASA * * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause * * Memory map for QSPI XIP split image (overwrite-only, no network core). * External slots: slot1 = app secondary, slot2/3 = QSPI XIP primary/secondary. */ /delete-node/ &boot_partition; /delete-node/ &slot0_partition; /delete-node/ &slot1_partition; /delete-node/ &storage_partition; &flash0 { partitions { boot_partition: partition@0 { compatible = "zephyr,mapped-partition"; label = "mcuboot"; reg = <0x00000000 DT_SIZE_K(64)>; }; slot0_partition: partition@10000 { compatible = "zephyr,mapped-partition"; label = "image-0"; reg = <0x00010000 0x000d4000>; }; storage_partition: partition@f0000 { compatible = "zephyr,mapped-partition"; label = "storage"; reg = <0x000f0000 DT_SIZE_K(64)>; }; }; }; &mx25r64 { status = "okay"; partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; slot1_partition: partition@0 { label = "image-1"; reg = <0x00000000 0x000d4000>; }; slot3_partition: partition@d4000 { label = "image-3"; reg = <0x000d4000 DT_SIZE_K(256)>; }; slot2_partition: partition@120000 { label = "image-2"; reg = <0x00120000 DT_SIZE_K(256)>; }; }; };
/* * Copyright (c) 2026 Nordic Semiconductor ASA * * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause * * Memory map for QSPI XIP split image in direct-XIP mode (no network core). * Internal slot1 holds the internal direct-XIP variant; external slot2/3 hold * the QSPI XIP primary/secondary images. */ /delete-node/ &boot_partition; /delete-node/ &slot0_partition; /delete-node/ &slot1_partition; /delete-node/ &storage_partition; &flash0 { partitions { boot_partition: partition@0 { compatible = "zephyr,mapped-partition"; label = "mcuboot"; reg = <0x00000000 DT_SIZE_K(64)>; }; slot0_partition: partition@10000 { compatible = "zephyr,mapped-partition"; label = "image-0"; reg = <0x00010000 0x0006a000>; }; slot1_partition: partition@7a000 { compatible = "zephyr,mapped-partition"; label = "image-1"; reg = <0x0007a000 0x0006a000>; }; storage_partition: partition@f0000 { compatible = "zephyr,mapped-partition"; label = "storage"; reg = <0x000f0000 DT_SIZE_K(64)>; }; }; }; &mx25r64 { status = "okay"; partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; slot3_partition: partition@d4000 { label = "image-3"; reg = <0x000d4000 DT_SIZE_K(256)>; }; slot2_partition: partition@120000 { label = "image-2"; reg = <0x00120000 DT_SIZE_K(256)>; }; }; };
#include "../dts/nrf5340_extxip_sram_tfm_ns.dtsi" /delete-node/ &boot_partition; /delete-node/ &slot0_partition; /delete-node/ &slot1_partition; /delete-node/ &storage_partition; &flash0 { partitions { boot_partition: partition@0 { compatible = "zephyr,mapped-partition"; label = "mcuboot"; reg = <0x00000000 DT_SIZE_K(64)>; }; slot0_partition: partition@10000 { compatible = "zephyr,mapped-partition"; label = "image-0"; reg = <0x00010000 0x000d4000>; ranges = <0x0 0x00010000 0x000d4000>; #address-cells = <1>; #size-cells = <1>; slot0_s_partition: partition@0 { compatible = "zephyr,mapped-partition"; label = "image-0-secure"; reg = <0x00000000 0x00040000>; }; slot0_ns_partition: partition@40000 { compatible = "zephyr,mapped-partition"; label = "image-0-nonsecure"; reg = <0x00040000 0x00094000>; }; }; tfm_ps_partition: partition@f0000 { compatible = "zephyr,mapped-partition"; label = "tfm-ps"; reg = <0x000f0000 0x00004000>; }; tfm_its_partition: partition@f4000 { compatible = "zephyr,mapped-partition"; label = "tfm-its"; reg = <0x000f4000 0x00002000>; }; tfm_otp_partition: partition@f6000 { compatible = "zephyr,mapped-partition"; label = "tfm-otp"; reg = <0x000f6000 0x00002000>; }; storage_partition: partition@f8000 { compatible = "zephyr,mapped-partition"; label = "storage"; reg = <0x000f8000 0x00008000>; }; }; }; &mx25r64 { status = "okay"; partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; slot1_partition: partition@0 { label = "image-1"; reg = <0x00000000 0x000d4000>; }; slot3_partition: partition@d4000 { label = "image-3"; reg = <0x000d4000 DT_SIZE_K(256)>; }; slot4_partition: partition@120000 { label = "image-4"; reg = <0x00120000 DT_SIZE_K(256)>; }; slot5_partition: partition@160000 { label = "image-5"; reg = <0x00160000 DT_SIZE_K(256)>; }; }; }; / { /delete-node/ nordic-tz-secure; /delete-node/ nordic-tz-nonsecure; nordic-tz-secure { compatible = "nordic,tz-secure"; code-partitions = <&slot0_s_partition>; data-partitions = <&tfm_ps_partition>, <&tfm_its_partition>, <&tfm_otp_partition>; }; nordic-tz-nonsecure { compatible = "nordic,tz-nonsecure"; code-partitions = <&slot0_ns_partition>; data-partitions = <&storage_partition>; }; };
The following include defines SRAM partitioning required for the secure/non-secure setup:
/* * Copyright (c) 2026 Nordic Semiconductor ASA * * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause * * SRAM layout for TF-M non-secure ext XIP (static DTS, no Partition Manager). * Aligns with pm_static_nrf5340dk_nrf5340_cpuapp_ns.yml (NCSDK-33694): * PCD (sram0_dfu_shared) 0x20000000–0x20002000, * TF-M secure 0x20002000–0x20010000, * NS app 0x20010000–0x20070000, IPC 0x20070000–0x20080000. * * sram0_dfu_shared uses the default node in nrf5340_shared_sram_partition.dtsi * (reg = <0x0 0x2000> under sram0 @ 0x20000000). */ #include <mem.h> &sram0_image { reg = <0x0 DT_SIZE_K(512)>; ranges = <0x0 0x0 DT_SIZE_K(512)>; }; /* MCUboot RAM from 0x20002000, * size 0x7e000 via sram0_offset_for_dfu (flash_sim.overlay zephyr,sram). */ &sram0_offset_for_dfu { reg = <0x2000 0x7e000>; ranges = <0x0 0x2000 0x7e000>; }; /* TF-M RAM */ &sram0_s { reg = <0x2000 (DT_SIZE_K(64) - 0x2000)>; }; /* NS RAM (Application + IPC) */ &sram0_ns { reg = <0x10000 DT_SIZE_K(448)>; ranges = <0x0 0x10000 DT_SIZE_K(448)>; }; /* Application RAM */ &sram0_ns_app { reg = <0x0 DT_SIZE_K(384)>; };
An MCUboot board overlay file to set the same partition layout and
zephyr,code-partitionto theboot_partitionpartition node:#include "../../../boards/nrf5340dk_nrf5340_cpuapp.overlay" / { chosen { zephyr,code-partition = &boot_partition; }; };
You can either place this file as a board overlay in the
sysbuild/mcuboot/boardsfolder of the application or name itapp.overlayand place it in thesysbuild/mcubootfolder so that it applies to all board targets.A devicetree overlay for b0n to define
cpunetpartition nodes (includingb0n_partitionands0_partition):/* * Copyright (c) 2026 Nordic Semiconductor ASA * * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause */ / { chosen { zephyr,code-partition = &b0n_partition; }; }; /delete-node/ &boot_partition; /delete-node/ &slot0_partition; /delete-node/ &slot1_partition; /delete-node/ &storage_partition; &flash1 { partitions { b0n_partition: partition@0 { compatible = "zephyr,mapped-partition"; label = "b0n"; reg = <0x0 0x8580>; }; bl_storage: provision_partition: partition@8580 { compatible = "zephyr,mapped-partition"; label = "b0-provision-data"; reg = <0x8580 0x280>; }; s0_partition: partition@8800 { compatible = "zephyr,mapped-partition"; label = "image-0"; reg = <0x8800 DT_SIZE_K(222)>; }; }; };
Create a linker file
Create a linker file that uses devicetree and Kconfig information for defining memory regions on the external QSPI flash device.
The following code example from the SMP Server with external XIP sample shows how to set up a linker file for direct-XIP mode, with the direct-XIP secondary image configuration in the CONFIG_NCS_IS_VARIANT_IMAGE section.
You can omit this section if you are using other MCUboot operating modes.
Place this file in the application directory with a name similar to linker_arm_extxip.ld.
/*
* Copyright (c) 2022 Carlo Caione <ccaione@baylibre.com>
* Copyright (c) 2024-2026 Nordic Semiconductor
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
/**
* @file
* @brief External QSPI flash Linker command/script file
*
* Linker script for moving desired .text and .data to the external
* memory space.
*/
#include <zephyr/linker/sections.h>
#include <zephyr/devicetree.h>
#include <zephyr/linker/linker-defs.h>
#include <zephyr/linker/linker-tool.h>
/* Let SystemInit() be called in place of z_arm_platform_init() by default. */
PROVIDE(z_arm_platform_init = SystemInit);
/*
* Select the MCUboot slot for the QSPI XIP image based on the assigned image
* number and whether this is a variant (secondary slot) build.
*/
#if CONFIG_MCUBOOT_QSPI_XIP_IMAGE_NUMBER == 1
#if CONFIG_NCS_IS_VARIANT_IMAGE
#define QSPI_XIP_SLOT_NODE DT_NODELABEL(slot3_partition)
#else
#define QSPI_XIP_SLOT_NODE DT_NODELABEL(slot2_partition)
#endif
#elif CONFIG_MCUBOOT_QSPI_XIP_IMAGE_NUMBER == 2
#if CONFIG_NCS_IS_VARIANT_IMAGE
#define QSPI_XIP_SLOT_NODE DT_NODELABEL(slot5_partition)
#else
#define QSPI_XIP_SLOT_NODE DT_NODELABEL(slot4_partition)
#endif
#else
#error "Unsupported CONFIG_MCUBOOT_QSPI_XIP_IMAGE_NUMBER for linker_arm_extxip.ld"
#endif
#define QSPI_XIP_MM_BASE DT_REG_ADDR_BY_NAME(DT_NODELABEL(qspi), qspi_mm)
/*
* ROM_START_OFFSET is 0 for TF-M NS builds (header lives at slot0 start only).
* The QSPI XIP part is still signed with --pad-header, so reserve header space
* at the start of the external slot.
*/
#if CONFIG_BUILD_WITH_TFM
#define EXTFLASH_IMAGE_OFFSET CONFIG_TFM_MCUBOOT_HEADER_SIZE
#else
#define EXTFLASH_IMAGE_OFFSET CONFIG_ROM_START_OFFSET
#endif
#define EXTFLASH_ADDR (QSPI_XIP_MM_BASE + DT_REG_ADDR(QSPI_XIP_SLOT_NODE) + EXTFLASH_IMAGE_OFFSET)
#define EXTFLASH_SIZE (DT_REG_SIZE(QSPI_XIP_SLOT_NODE) - EXTFLASH_IMAGE_OFFSET)
MEMORY
{
EXTFLASH (wx) : ORIGIN = EXTFLASH_ADDR, LENGTH = EXTFLASH_SIZE
}
#include <zephyr/arch/arm/cortex_m/scripts/linker.ld>
Set additional Kconfig options
Set additional Kconfig option in the application’s prj.conf to allow it to execute code from XIP:
# Update the filename here if you have given it a different name
CONFIG_CUSTOM_LINKER_SCRIPT="linker_arm_extxip.ld"
Configure sysbuild
Configure Sysbuild to enable the required Kconfig options for this functionality:
Create a
sysbuild.conffile in the application directory withs the following options, depending on the required functionality:SB_CONFIG_BOOTLOADER_MCUBOOT=y SB_CONFIG_NETCORE_APP_UPDATE=y SB_CONFIG_SECURE_BOOT_NETCORE=y SB_CONFIG_QSPI_XIP_SPLIT_IMAGE=y SB_CONFIG_MCUBOOT_NRF53_MULTI_IMAGE_UPDATE=y SB_CONFIG_MCUBOOT_USE_ALL_AVAILABLE_RAM=y # This will enable the hci_ipc image for the network core, change to the desired image SB_CONFIG_NETCORE_HCI_IPC=y
SB_CONFIG_BOOTLOADER_MCUBOOT=y SB_CONFIG_QSPI_XIP_SPLIT_IMAGE=y SB_CONFIG_NETCORE_NONE=y SB_CONFIG_SECURE_BOOT_NETCORE=n
SB_CONFIG_BOOTLOADER_MCUBOOT=y SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP=y SB_CONFIG_QSPI_XIP_SPLIT_IMAGE=y SB_CONFIG_NETCORE_NONE=y SB_CONFIG_SECURE_BOOT_NETCORE=n
Relocate code to QSPI flash
Relocate the code to run from the QSPI flash inside the applications CMakeLists.txt file using the zephyr_code_relocate function.
This can be used to relocate files and libraries.
zephyr_code_relocate(FILES src/complex_sensor_calculcation_code.c LOCATION EXTFLASH_TEXT NOCOPY)
zephyr_code_relocate(FILES src/complex_sensor_calculcation_code.c LOCATION RAM_DATA)
Note
This assumes that the application has a src/complex_sensor_calculcation_code.c` file.
zephyr_code_relocate(LIBRARY mcumgr_mgmt LOCATION EXTFLASH_TEXT NOCOPY)
zephyr_code_relocate(LIBRARY mcumgr_mgmt LOCATION RAM_DATA)
Note
The library name comes from the Zephyr code.
Set QSPI flash initialization priority
Set the initialization priority of the QSPI flash so that it is configured before any code on the device runs, otherwise the application operation is going to be undefined.
There is no built-in check for this setting.
Therefore, you must manually set the priority based on the relocated code.
For example, if you relocate the shell subsystem, its initialization priority must be higher than the priority of QSPI, and the value of CONFIG_SHELL_BACKEND_SERIAL_INIT_PRIORITY must be greater than CONFIG_NORDIC_QSPI_NOR_INIT_PRIORITY.
Note
Not all libraries or subsystems can be relocated.
Any code that runs early in the boot process or uses SYS_INIT() with a non-configurable init priority cannot be relocated.
Library and subsystem code needs to be manually checked before relocating it.
Programming with the QSPI XIP split image
Programming of the application is supported using the standard procedure. The standard procedure programs the firmware using the default nRF Util configuration, which, for QSPI, is the PP4IO mode.
Programming using a different SPI mode
If you are using a different SPI mode on the QSPI interface, such as DSPI, you must use a custom qspi_nrfutil.json file.
The following is an example for the Thingy:53, which supports DSPI and PP:
{
"firmware_config": {
"peripheral": "QSPI"
},
"pins": {
"sck": 17,
"csn": 18,
"io0": 13,
"io1": 14,
"io2": 15,
"io3": 16
},
"flash_size": 8388608,
"sck_frequency": 8000000,
"address_mode": "MODE24BIT",
"readoc": "READ2IO",
"writeoc": "PP",
"pp_size": "PPSIZE256",
"sck_delay": 128,
"rx_delay": 2,
"page_size": 4096
}
To use this file when programming, add the following lines to the application’s CMakeLists.txt file before the find_package() line:
macro(app_set_runner_args)
# Replace with the filename of your json file
board_runner_args(nrfutil "--ext-mem-config-file=${CMAKE_CURRENT_SOURCE_DIR}/qspi_nrfutil.json")
endmacro()
This will enable programming the target board successfully when using west flash.
Firmware updates
The build system will automatically generate the signed update files that can be loaded to a device to run the newer version of the application.
The dfu_application.zip file also contains these files and you can use the nRF Connect for Mobile application (available for Android and iOS) to upload the firmware over Bluetooth® Low Energy.
File descriptions
The following table lists the files generated when building a QSPI XIP split-image application.
In some file names, <application> is the name of the application and <kernel_name> is the value of CONFIG_KERNEL_BIN_NAME.
File name and location |
Description |
|---|---|
|
Initial HEX output file, unsigned, containing internal and external QSPI flash data. |
|
Initial HEX output file, unsigned, containing internal flash data. |
|
Initial HEX output file, unsigned, containing external QSPI flash data. |
|
Signed internal flash data HEX file. |
|
Signed external QSPI flash data HEX file. |
|
Signed internal and external QSPI flash data HEX file. |
|
Signed internal flash data binary file (for firmware updates). |
|
Signed external QSPI flash data binary file (for firmware updates). |
|
Initial HEX output file, unsigned, containing internal and external QSPI flash data for direct-XIP mode secondary slot image. |
|
Initial HEX output file, unsigned, containing internal flash data for direct-XIP mode secondary slot image. |
|
Initial HEX output file, unsigned, containing external QSPI flash data for direct-XIP mode secondary slot image. |
|
Signed internal flash data HEX file for direct-XIP mode secondary slot image. |
|
Signed external QSPI flash data HEX file for direct-XIP mode secondary slot image. |
|
Signed internal and external QSPI flash data HEX file for direct-XIP mode secondary slot image. |
|
Signed internal flash data binary file (for firmware updates) for direct-XIP mode secondary slot image. |
|
Signed external QSPI flash data binary file (for firmware updates) for direct-XIP mode secondary slot image. |
|
Merged HEX file containing all images, includes both internal and external QSPI flash data for all images. |
|
Created if |