
One of Zephyr's nicer features is the ability to change the memory layout of your application without needing to dive into the murky world of linker scripts. This allows us to resize and shift bootloader, application, and configuration regions around as the application requirements dictate. There are three common reasons you might need to update the default layout provided by Zephyr.
- Your application is too large to fit both application image slots in internal ROM, so you want to move the second image slot to an external flash chip.
- You either don’t need a, or need a larger, settings partition. In the former case you can recover extra ROM for your application image slots, and in the later you need to shrink the application image slots to prevent overlaps.
- You are defining your own board, so you are the one that needs to provide the defaults.
The purpose of this guide is to provide a summary of how Zephyr uses devicetree to define memory regions, and the process to follow when updating them.
⚠️ Modifying bootloader and application ROM regions is only possible before a device has been deployed, since over-the-air upgrades only update the application image. The bootloader and application need to have a shared expectation of memory addresses, otherwise the bootloader will configure the wrong value into the VTOR register and jump to the wrong address when attempting to start the application.
Devicetree Partitions
Before defining the steps to update the layouts, lets look at the default memory layout for a Nordic Semiconductor nRF52840DK to familiarize ourselves with the format (source).
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 0x0000C000>;
};
slot0_partition: partition@c000 {
label = "image-0";
reg = <0x0000C000 0x00076000>;
};
slot1_partition: partition@82000 {
label = "image-1";
reg = <0x00082000 0x00076000>;
};
storage_partition: partition@f8000 {
label = "storage";
reg = <0x000f8000 0x00008000>;
};
};
};
For this board, the onboard flash (the flash0 node) is being split into 4 separate memory regions. This is done with a special devicetree node of compatible type fixed-partitions. The #address-cells and #size-cells properties can be ignored for our purposes, they are both always <1> for memory partitions. Following the fixed-paritions boilerplate, we have our 4 memory regions; the bootloader, two equally sized application image slots, and the trailing storage partition. Each memory region follows the same structure:
partition_nodelabel: partition@start_offset_1 {
label = "partition-name";
reg = <start_offset_2 partition_size>;
};
| Device | Description |
|---|---|
partition_nodelabel | This field needs to be globally unique, and MCUBoot in particular has expectations for the names. Code can get a reference to the partition through DT_FIXED_PARTITION_ID(partition_nodelabel). |
partition | By convention this field is named partition, but in practice it can be anything. |
start_offset_1 | The byte offset of the start of the memory region, in hexadecimal format. This value must not include a leading 0x or leading 0’s, and by convention uses lower case hex (e.g. @c000, not @C000). |
"partition-name" | An optional string that identifies the partition. This can also be used to retrieve a reference to the partition with DT_NODE_BY_FIXED_PARTITION_LABEL(partition_name), but its usage is discouraged (use DT_FIXED_PARTITION_ID). |
start_offset_2 | This value must be the same numerical value as start_offset_1, but is formatted as an uint32_t hexadecimal number with leading 0x and 0’s. The convention is uppercase letters. (e.g. 0x0000C000). |
partition_size | The size of the memory region in bytes, in the same numerical format as start_offset_2. |
Device Specific Knowledge
Before proceeding to dividing up our memory space, there are two pieces of information that need to be known for each flash device we are using:
- Total flash size in bytes
- Minimum partition unit
The first point is self-explanatory, we need to know how much space we have to allocate. The minimum partition unit is needed because flash memory typically can’t be erased on arbitrary byte boundaries, they have some minimum erase unit. Because we want memory partitions to appear independent, we don’t want an erase on a memory address in one memory partition to accidentally erase data on a second partition.
The total flash size is typically a headline item on a datasheet (1 MB for the nRF52840), while the minimum erase unit can usually be found in the FLASH peripheral sections (4 KB here) (source).

⚠️ For applications utilizing a Memory Protection Unit (MPU), the addressable resolution of the MPU must also be taken into account for the minimum partition unit.
Partition Packing
Generally speaking, we want to be efficient with our ROM. Leaving regions of memory unallocated, as shown in the center layout below, is generally undesirable. Additionally, most applications will want equally sized image-0 and image-1 partitions, since future images will be limited to the smaller of the two partitions.

This leads to a few simple rules that should be obeyed when dividing up your memory space.
- The offset of the first partition should be 0
- The offset of a partition should equal the (offset + size) of the previous partition
- The (offset + size) of the last partition should equal the total size of the memory space
- The size of each partition MUST be a multiple of the minimum partition unit
Examples
The following examples are all built and run on Zephyr v4.2.1 using the hello_world sample. We build the application using sysbuild with -DSB_CONFIG_BOOTLOADER_MCUBOOT=y so that MCUboot is automatically built at the same time.
Default build
west build -p -b nrf52840dk/nrf52840 zephyr/samples/hello_world/ --sysbuild -- -DSB_CONFIG_BOOTLOADER_MCUBOOT=y
Building the application with its default memory layout shows the (mostly) expected result. The 48 kB (0xc000) bootloader FLASH region matches our expectations, however the application image FLASH region of 474800 bytes (0x73eb0) is slightly smaller than the 472 kB (0x76000) size specified in devicetree.
# MCUBoot
Memory region Used Size Region Size %age Used
FLASH: 32660 B 48 KB 66.45%
RAM: 22464 B 256 KB 8.57%
IDT_LIST: 0 GB 32 KB 0.00%
# Application
Memory region Used Size Region Size %age Used
FLASH: 19228 B 474800 B 4.05%
RAM: 4480 B 256 KB 1.71%
IDT_LIST: 0 GB 32 KB 0.00%
This is because the bootloader needs to store swap status and metadata at the end of an image. The amount of space needed is taken care of automatically by the build system with CONFIG_ROM_END_OFFSET. If we examine the value of this kconfig in build/hello_world/zephyr/include/generated/zephyr/autoconf.h, we can see #define CONFIG_ROM_END_OFFSET 0x2150, and 0x76000 - 0x2150 == 474800.
Expanding application FLASH
The first example is expanding the FLASH space available to the application by removing the storage partition. Since this region is 32 kB, we should be able to expand the two application regions by 16 kB each. To calculate the new values for each of our regions:
- The bootloader partition is unchanged, 48 kB
- The total remaining memory is 976 kB (1024 - 48)
- An even split for application images would be 488 kB (976 / 2)
slot0_partitionis therefore 488 kBslot1_partitionis therefore 488 kB
- To double check:
- Validate that 48 + 488 + 488 == 1024
- Validate that each partition is a multiple of 4kB
Translating this into the devicetree format from above:
/* zephyr/samples/hello_world/boards/nrf52840dk_nrf52840.overlay */
/* Delete the original partitions */
/delete-node/ &slot0_partition;
/delete-node/ &slot1_partition;
/delete-node/ &storage_partition;
&flash0 {
partitions {
/* The two application partitions, expanded */
slot0_partition: partition@c000 {
label = "image-0";
/* reg = <48kB 488kB> */
reg = <0x0000C000 0x0007A000>;
};
slot1_partition: partition@86000{
label = "image-1";
/* reg = <540kB 488kB> */
reg = <0x00086000 0x0007A000>;
};
};
};
To provide the build system our new memory layouts, we need to create a new file in the application folder. In this case, zephyr/samples/hello_world/boards/nrf52840dk_nrf52840.overlay. The build system will automatically pick up the changes for the application image, but we need to manually specify it for the MCUboot image.
west build -p -b nrf52840dk/nrf52840 zephyr/samples/hello_world/ --sysbuild -- -DSB_CONFIG_BOOTLOADER_MCUBOOT=y -Dmcuboot_EXTRA_DTC_OVERLAY_FILE=$ZEPHYR_BASE/samples/hello_world/boards/nrf52840dk_nrf52840.overlay
Running the build shows us our updated FLASH regions, with the application FLASH region having increased by 16 kB compared to the baseline.
# MCUBoot
Memory region Used Size Region Size %age Used
FLASH: 32660 B 48 KB 66.45%
RAM: 22464 B 256 KB 8.57%
IDT_LIST: 0 GB 32 KB 0.00%
# Application
Memory region Used Size Region Size %age Used
FLASH: 19228 B 491184 B 3.91%
RAM: 4480 B 256 KB 1.71%
IDT_LIST: 0 GB 32 KB 0.00%
Shrinking application FLASH
The next example moves in the other direction, shrinking application FLASH, to either add a new memory region or expand the settings partition. In this example we will add a new 64 kB partition (file_partition) after the second image partition.
- The bootloader partition is unchanged, 48 kB
- The storage partition is unchanged, 32 kB
- We are adding a new 64 kB partition
- The total remaining memory is 880 kB (1024 - 48 - 32 - 64)
- An even split for application images would be 440 kB (880 / 2)
- slot0_partition is therefore 440 kB
- slot1_partition is therefore 440 kB
- To double check:
- Validate that 48 + 440 + 440 + 64 + 32 == 1024
- Validate that each partition is a multiple of 4kB
Again translating our new memory layout into devicetree:
/* zephyr/samples/hello_world/boards/nrf52840dk_nrf52840.overlay */
/* Delete the original partitions */
/delete-node/ &slot0_partition;
/delete-node/ &slot1_partition;
&flash0 {
partitions {
/* The two application partitions, shrunk */
slot0_partition: partition@c000 {
label = "image-0";
/* reg = <48kB 440kB> */
reg = <0x0000C000 0x0006E000>;
};
slot1_partition: partition@7a000 {
label = "image-1";
/* reg = <488kB 440kB> */
reg = <0x0007A000 0x0006E000>;
};
/* Our new partition */
file_partition: partition@e8000 {
label = "files";
/* reg = <928kB 64kB> */
reg = <0x000E8000 0x00010000>;
};
};
};
After running the same build step from above, we can see our new smaller application FLASH region:
# MCUBoot
Memory region Used Size Region Size %age Used
FLASH: 32660 B 48 KB 66.45%
RAM: 22464 B 256 KB 8.57%
IDT_LIST: 0 GB 32 KB 0.00%
# Application
Memory region Used Size Region Size %age Used
FLASH: 19228 B 442032 B 4.35%
RAM: 4480 B 256 KB 1.71%
IDT_LIST: 0 GB 32 KB 0.00%
Moving the secondary image to external flash
Moving the secondary image to external flash is simpler in terms of the calculations, but requires additional command line options since now MCUboot requires drivers to be enabled for the external flash memory. The slot0_partition is simply updated to be the combined size of the original partitions, while slot1_partition is re-added on the external flash chip node. The size of slot1_partition is the same size as slot0_partition.
- The bootloader partition is unchanged, 48 kB
- The storage partition is unchanged, 32 kB
- The total remaining memory is 944 kB (1024 - 48 - 32)
slot0_partitionis allocated the entire 944 kBslot1_partitionon the external flash, 944 kB
/* zephyr/samples/hello_world/boards/nrf52840dk_nrf52840.overlay */
/* Delete the original partitions */
/delete-node/ &slot0_partition;
/delete-node/ &slot1_partition;
&flash0 {
partitions {
/* Our new, much larger, slot0_partition */
slot0_partition: partition@c000 {
label = "image-0";
/* reg = <48kB 944kB> */
reg = <0x0000C000 0x000EC000>;
};
};
};
/* External flash chip on the nRF52840DK QSPI bus */
&mx25r64 {
partitions {
/* The base board files do not define any partitions on this flash
* chip, so we need to add the boilerplate.
*/
compatible = "fixed-partitions";
#address-cells = < 0x1 >;
#size-cells = < 0x1 >;
/* slot1_partition moved to external flash */
slot1_partition: partition@0 {
label = "image-1";
/* reg = <0kB 936kB> */
reg = < 0x00000000 0x000EA000 >;
};
};
};
In order to build this updated configuration, we also need to enable the QSPI driver in MCUboot. MCUboot has an existing Kconfig file for this, which we can reuse.
west build -p -b nrf52840dk/nrf52840 zephyr/samples/hello_world/ --sysbuild -- -DSB_CONFIG_BOOTLOADER_MCUBOOT=y -Dmcuboot_EXTRA_CONF_FILE=$ZEPHYR_BASE/../bootloader/mcuboot/boot/zephyr/boards/nrf52840dk_qspi_nor.conf -Dmcuboot_EXTRA_DTC_OVERLAY_FILE=$ZEPHYR_BASE/samples/hello_world/boards/nrf52840dk_nrf52840.overlay
With this command, our primary application image has now significantly expanded, while still retaining the ability to upgrade after deployment. The MCUboot resource usage has also increased slightly due to the additional drivers.
# MCUBoot
Memory region Used Size Region Size %age Used
FLASH: 36396 B 48 KB 74.05%
RAM: 24384 B 256 KB 9.30%
IDT_LIST: 0 GB 32 KB 0.00%
# Application
Memory region Used Size Region Size %age Used
FLASH: 19180 B 936 KB 2.00%
RAM: 5696 B 256 KB 2.17%
IDT_LIST: 0 GB 32 KB 0.00%
Summary
The provided examples give a foundation on the steps to take when resizing Zephyr devicetree partitions. The same process can be followed regardless of the microcontroller or board, as long as you can find the total flash memory size and the page erase size.