🚄 100% native Nordic with MCUboot and crashproof, dual-image DFU

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
:grin:

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.

  1. Connect your debugger (e.g., J-Link or another dev board running DAPLink).
  2. 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! :smiling_face_with_sunglasses::selfie:

HTH

GL :slight_smile: PJ :v:

:grin:

YES…

Yes & Yes, this exact architecture will work across the entire XIAO nRF series, but there are a few important nuances depending on which specific board you are holding.

Here is the breakdown of how this native Nordic/MCUboot approach maps to the different hardware variants:

1. XIAO nRF52840 (Standard & Sense)

This is exactly what the code provided above targets.

  • Internal Flash: 1MB (nRF52840).
  • External QSPI Flash: 2MB (P25Q16H).
  • The Nuance: The configuration files (app.overlay and mcuboot.conf) use the nordic,qspi-nor driver. This is critical because the nRF52840 has a dedicated hardware QSPI peripheral block that handles the external flash layout efficiently.

2. The New XIAO nRF54 Series (e.g., nRF54L15 / nRF54LM20A)

If you are moving to the next-generation nRF54 chips that Seeed Studio has been rolling out, this exact architectural strategy is still the absolute gold standard, but the underlying configuration changes slightly:

  • No More MBR: The nRF54 architecture drops the old Nordic Master Boot Record (MBR) entirely. MCUboot sits directly at address 0x00000000 as the primary hardware entry point.
  • Driver Changes: The nRF54 series replaces the older QSPI peripheral with a newer XIP (Execute-in-Place) / SPI execution block. In your mcuboot.conf and app.overlay, you swap out CONFIG_NORDIC_QSPI_NOR=y for the updated nRF54 flash driver architecture specified in the newer nRF Connect SDK (NCS v2.7.0 / v3.0.0+).
  • Memory Real Estate: The nRF54L series typically features tighter or comparable internal execution spaces depending on the sub-variant, making the eviction of the factory UF2 bootloader even more necessary to fit massive stacks like Matter or complex BLE applications.

Summary Checklist for All Boards

No matter which XIAO nRF board you deploy to, the core rules remain identical:

  1. Ditch the Factory UF2: You must use an external SWD programmer (J-Link, DAPLink) to completely wipe the chip and install MCUboot at the base of the flash.
    for all you go it alone , roll your own PCB pronouns :grin: just..
  2. Match the JEDEC ID: If Seeed Studio changes the physical flash vendor on a specific production batch of carrier boards, double-check the jedec-id = [...] block in your DeviceTree overlay to match the exact flash chip on your workbench.

I’ll do a demo of the Expansion board with additional Flash , doing it’s thing…
just like the first one but now a much larger flash chip :+1:

HTH
GL :slight_smile: PJ :v: