Hi there,
So
With the Newest Xiao’s on the street and another on the way , Time has come to give you this…
Don’t Sleep on NRF_SDK
![]()
Going 100% native Nordic with MCUboot and a crashproof, two-slot flash layout is absolutely the right call, especially when you are building complex applications that need bulletproof reliability.
If your target is a rock-solid, dual-image DFU that can survive a power loss mid-update without bricking, you want to split your storage across the internal flash and the external 2MB QSPI flash on the XIAO.
Here is a complete project blueprint, including the necessary configuration files and structural code, to establish a pure Nordic Connect SDK (NCS) / Zephyr environment using MCUboot.
The Storage Strategy
To make this crashproof, we configure a Dual-Slot Storage Layout:
- Slot 0 (Primary Application): Lives entirely inside the internal $1\text{ MB}$ flash. This is where your live code runs.
- Slot 1 (Secondary DFU Image): Lives entirely on the external 2MB P25Q16H QSPI Flash.
- The Swap: MCUboot downloads the update into Slot 1. Upon reboot, it performs a test swap. If the new image fails to boot or crashes, the watchdog timer triggers a reset, and MCUboot automatically swaps the old, working image back into Slot 0.
1. The DeviceTree Overlay (app.overlay)
You need to tell Zephyr exactly where the external QSPI flash lives and define the partitions so MCUboot knows where to look. Create this file in your root project directory:
&qspi {
status = "okay";
pinctrl-0 = <&qspi_default>; // Ensure your board definition maps CS, CLK, and DIO pins
pinctrl-names = "default";
p25q16h: p25q16h@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <104000000>;
jedec-id = [15 60 15]; /* Manufacturer & Device ID for P25Q16H */
size = <16777216>; /* 16 Megabits = 2 Megabytes */
has-dpd;
t-enter-dpd = <3000>;
t-exit-dpd = <30000>;
};
};
/ {
chosen {
/* MCUboot primary slot inside internal flash */
zephyr,code-partition = &slot0_partition;
};
};
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 0x00010000>; /* 64KB for MCUboot */
};
slot0_partition: partition@10000 {
label = "image-0";
reg = <0x00010000 0x000ED000>; /* ~948KB for App Primary Slot */
};
/* Scratch partition if using swap-using-scratch strategy */
scratch_partition: partition@fd000 {
label = "image-scratch";
reg = <0x000FD000 0x00003000>; /* 12KB Scratch storage */
};
};
};
&p25q16h {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
slot1_partition: partition@0 {
label = "image-1";
reg = <0x00000000 0x000ED000>; /* Must exactly match slot0 size */
};
storage_partition: partition@ed000 {
label = "storage";
reg = <0x000ED000 0x00113000>; /* Use the remaining 1.1MB for raw file/NVS data */
};
};
};
2. MCUboot Child Image Config (child_image/mcuboot.conf)
Because MCUboot compiles as an independent bootloader image before your main app, it needs its own config file telling it to include native Nordic drivers for the QSPI peripheral so it can read Slot 1 on boot.
Create a folder named child_image in your project root, and place mcuboot.conf inside it:
# Enable flash drivers within the bootloader context
CONFIG_FLASH=y
# Enable Nordic QSPI hardware driver
CONFIG_NORDIC_QSPI_NOR=y
# Enable MCUboot logging over UART for debugging boot issues
CONFIG_MCUBOOT_SERIAL=y
CONFIG_BOOT_SERIAL_UART=y
# Configure Image Swap behavior (Move-based swap minimizes wear)
CONFIG_BOOT_SWAP_USING_MOVE=y
# Force signature verification on the secondary slot images
CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y
3. Main Application Configuration (prj.conf)
In your primary application configuration, you must enable the Device Management / Simple Management Protocol (SMP) layer so the Nordic DFU apps can pass the binary over the air directly to your code.
# Core OS settings
CONFIG_GPIO=y
CONFIG_WATCHDOG=y
# Enable the bootloader utility integrations in the main app
CONFIG_BOOTLOADER_MCUBOOT=y
CONFIG_MCUBOOT_IMG_MANAGER=y
# OS Management & DFU over BLE features
CONFIG_MCUMGR=y
CONFIG_MCUMGR_TRANSPORT_BLE=y
CONFIG_MCUMGR_GRP_IMG=y
CONFIG_MCUMGR_GRP_OS=y
# Ensure sufficient buffer sizing for incoming firmware chunks
CONFIG_MCUMGR_TRANSPORT_NET_BUF_COUNT=4
CONFIG_MCUMGR_TRANSPORT_NET_BUF_SIZE=256
4. Application Implementation (src/main.c)
Your code needs to initialize the SMP service so it listens for inbound updates, and it must confirm the image once it successfully boots. If your app boots up but fails to call boot_write_img_confirmed(), MCUboot assumes the update was unstable and rolls back to the previous version on the next power cycle.
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/dfu/mcuboot.h>
/* MCUmgr dependencies for native DFU transport */
#include <zephyr/mgmt/mcumgr/mgmt/mgmt.h>
#include <zephyr/mgmt/mcumgr/transport/smp_ble.h>
#include <zephyr/mgmt/mcumgr/grp/img_mgmt/img_mgmt.h>
#include <zephyr/mgmt/mcumgr/grp/os_mgmt/os_mgmt.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
void main(void)
{
LOG_INF("Nordic 100% Native Architecture Booting...");
/* -------------------------------------------------------------------------
* CRASHPROOF SAFETY CHECK: Confirm the Image
* ------------------------------------------------------------------------- */
if (!boot_is_img_confirmed()) {
int err = boot_write_img_confirmed();
if (err == 0) {
LOG_INF("New firmware image verified and confirmed successfully!");
} else {
LOG_ERR("Failed to confirm image (err %d). System will roll back on reset.", err);
}
}
/* Register the core MCUmgr command sets (OS and Image management) */
os_mgmt_register_group();
img_mgmt_register_group();
/* Start the SMP BLE Transport service so the Nordic DFU app can discover it */
int smp_err = smp_ble_init();
if (smp_err) {
LOG_ERR("Failed to initialize SMP BLE transport (err %d)", smp_err);
} else {
LOG_INF("SMP BLE Service online. Awaiting secure DFU connections...");
}
while (1) {
k_sleep(K_MSEC(1000));
}
}
How to Flash this the First Time
Because you are stripping out the Adafruit UF2 bootloader completely to reclaim internal flash real estate, you cannot use USB drag-and-drop anymore. You must use an external SWD programmer connected to the small debugging pads on the underside of the XIAO.
- Connect your debugger (e.g., J-Link or another dev board running DAPLink).
- Compile your project using the standard command line or the VS Code nRF Connect extension extension:
west build -b xiao_ble
Wipe the chip entirely and flash the combined bootloader + application image:
west flash --erase
Once complete, the board will boot directly under the absolute command of MCUboot, completely immune to standard filesystem crashes, with an active SMP DFU service ready to process incoming files directly from the official Nordic nRF Connect Device Manager mobile application! ![]()
![]()
HTH
GL
PJ ![]()