Migrating Qualcomm Dragonwing / Ventuno Q to NVMe: Safe Kernel Handoff Explained

The Qualcomm Dragonwing (Arduino Ventuno Q) development platform is a capable ARM64 single-board computer, but its default 64 GB eMMC (typically ~55 GB usable) quickly becomes an I/O bottleneck under real-world workloads like compilation, Docker container orchestration, and on-device model inference.

While adding an M.2 NVMe SSD offers a significant leap in throughput and capacity, naively cloning the drive as you would on x86 will fail or render the board unbootable.

This post explains the mechanics of Qualcomm’s boot chain, why traditional cloning breaks, and how to execute a zero-risk migration using a Safe Kernel Handoff Architecture.


1. The Challenge with Qualcomm ARM64 Storage

On x86 platforms, cloning an OS to an NVMe drive is trivial: clone the disk, update the EFI boot order in BIOS, and reboot.

Qualcomm platforms differ fundamentally:

  • Complex Partition Tables: The eMMC layout contains over 70 distinct partitions holding low-level vendor firmware (tz, hyp, rpm, xbl), TrustZone components, modem binaries, and Device Tree Blobs (DTBs).
  • Firmware Coupling: The early-stage bootloader and UEFI firmware are hardcoded to initialize and query the eMMC controller.
  • Factory Calibration Data (/var/persist): Partition 70 (mmcblk0p70) contains persistent factory calibration parameters, including Wi-Fi/Bluetooth MAC addresses and power tables. Overwriting or disassociating this partition breaks wireless connectivity.

Attempting to wipe the eMMC or duplicate the entire 70-partition structure to NVMe will prevent the board from completing early-stage boot.


2. The Solution: Safe Kernel Handoff Architecture

Instead of replacing the eMMC entirely, we separate the boot loader role from the root filesystem role:

+-----------------------------------------------------------------+
|                       Qualcomm eMMC (Boot)                      |
|                                                                 |
|   [Power On] -> [UEFI Firmware] -> [GRUB] -> [Linux Kernel]     |
|                                                     |           |
+-----------------------------------------------------|-----------+
                                                      | Hands off
                                                      v Root (/)
+-----------------------------------------------------------------+
|                        M.2 NVMe SSD (Root)                      |
|                                                                 |
|   [initramfs] -> [Pivots root to NVMe UUID] -> [Userspace /]    |
|                                                                 |
+-----------------------------------------------------------------+
  1. eMMC stays the boot orchestrator: The platform initializes hardware, executes UEFI, loads GRUB, and pulls the kernel image (vmlinuz) and initrd directly from eMMC.
  2. Initramfs shifts the root to NVMe: When the kernel boots, the initramfs loads the NVMe driver, detects the M.2 SSD, mounts it by UUID as root (/), and launches userspace (systemd) from high-speed storage.
  3. Critical eMMC partitions stay connected: /boot/efi and /var/persist remain mounted from their original eMMC partitions via filesystem labels.
  4. Instant Rollback: Because the original root partition (/dev/mmcblk0p71) remains intact, reverting to eMMC requires only a single line edit in GRUB.

3. Step-by-Step Migration Breakdown

Step 1: Pre-Flight Environment Validation

Before modifying partitions, verify disk boundaries and runtime safety constraints:

CURRENT_ROOT=$(findmnt -n -o SOURCE /)
if [[ "$CURRENT_ROOT" == *"nvme"* ]]; then
  echo "[-] ERROR: System is already running from an NVMe drive!" >&2
  exit 1
fi

The script performs three checks:

  • Ensures root execution (EUID -eq 0).
  • Confirms target NVMe capacity exceeds current root utilization (df -k vs. blockdev --getsize64).
  • Establishes an EXIT ERR INT TERM trap to cleanly unmount temporary working directories if aborted.

Step 2: Driver Injection into Initramfs

Checking the Dragonwing kernel configuration (/boot/config-$(uname -r)):

CONFIG_PCIE_QCOM=y
CONFIG_BLK_DEV_NVME=m
CONFIG_NVME_CORE=m
  • The Qualcomm PCIe controller (CONFIG_PCIE_QCOM) is built directly into the kernel (=y), meaning the PCIe bus initializes automatically during early boot.
  • The NVMe driver (CONFIG_BLK_DEV_NVME) is modular (=m).

If nvme and nvme_core are not present in the initramfs image, the kernel will fail to detect the drive before the root pivot, dropping into a BusyBox rescue shell.

MODULES_TO_ADD=("nvme" "nvme_core")
for mod in "${MODULES_TO_ADD[@]}"; do
  if ! grep -qs "^${mod}$" /etc/initramfs-tools/modules; then
    echo "$mod" >> /etc/initramfs-tools/modules
  fi
done
update-initramfs -u -k all

Step 3: Partitioning with Flash Alignment (1 MiB)

Automated partitioning scripts often specify 0% as the partition start offset:

# Antipattern:
parted -s "$NVME_DEV" mkpart primary ext4 0% 100%

On NVMe media, 0% places the first partition at logical sector 34. This violates the 4K physical page boundary and creates performance-degrading read-modify-write cycles.

Instead, align explicitly to 1MiB (sector 2048):

wipefs -a "$NVME_DEV"
parted -s "$NVME_DEV" mklabel gpt
parted -s -a optimal "$NVME_DEV" mkpart "$PART_LABEL" ext4 1MiB 100%
udevadm settle
mkfs.ext4 -F -L "$PART_LABEL" "$NVME_PART"

Step 4: Live Rootfs Replication via Non-Recursive Bind Mount

Cloning a running Linux install must omit virtual pseudofilesystems (/proc, /sys, /dev), temporary runtimes (/run), and mounted eMMC sub-partitions (/boot/efi, /var/persist).

We achieve this with a non-recursive bind mount combined with filesystem-isolated rsync:

mkdir -p "$MNT_SSD" "$MNT_CUR"
mount "$NVME_PART" "$MNT_SSD"
mount --bind / "$MNT_CUR"

rsync -axHAWX --numeric-ids --info=progress2 "$MNT_CUR/" "$MNT_SSD/"
  • mount --bind (Non-Recursive): Sub-mount points (/proc, /sys, /boot/efi, /var/persist) appear only as empty directory stubs inside $MNT_CUR.
  • -x (--one-file-system): Prevents rsync from crossing filesystem boundaries.
  • -aHAWX: Retains ownership, hard links, symlinks, POSIX ACLs, and extended attributes (file capabilities and SELinux contexts).
  • --numeric-ids: Prevents userspace translation mismatches during file copying.

After synchronization, we recreate clean mount points on the target filesystem:

mkdir -p "$MNT_SSD"/{dev,proc,sys,run,tmp,mnt,media,boot/efi,var/persist}
chmod 1777 "$MNT_SSD/tmp"

Step 5: Updating /etc/fstab on the Target SSD

The source /etc/fstab on eMMC uses label-based referencing:

LABEL=writable    /              ext4    discard,errors=remount-ro  0  1
LABEL=system-boot /boot/efi/     vfat    defaults                   0  1
LABEL=persist     /var/persist   ext4    defaults                   0  2

On the SSD, we rewrite / to target the new NVMe UUID while leaving /boot/efi and /var/persist pointed to the eMMC labels:

sed -i "s|^LABEL=writable.*|UUID=${NEW_UUID}   /   ext4   discard,errors=remount-ro,noatime   0   1|" "$MNT_SSD/etc/fstab"

This ensures that upon booting into NVMe, hardware calibration data and the UEFI partition remain mapped to their original eMMC locations.


Step 6: GRUB Configuration & Drop-In Priority Order

In /etc/default/grub.d/, Dragonwing provides hardware configurations:

  • 99-dragonwing-defaults.cfg: Defines console=ttyMSM0,115200n8, earlycon, and pcie_pme=nomsi.
  • 99-dragonwing-disable-lvds.cfg: Sets panel options.

Caution: Setting GRUB_CMDLINE_LINUX="..." directly overwrites these parameters. Losing pcie_pme=nomsi can trigger PCIe MSI interrupt storms that stall Qualcomm root complexes.

To handle this cleanly:

  1. Name the configuration file zz-nvme-root.cfg so it evaluates after the 99-*.cfg files in ASCII order.
  2. Append to the variable rather than reassigning it:
cat << 'EOF' > /etc/default/grub.d/zz-nvme-root.cfg
# Dragonwing NVMe root handoff
GRUB_CMDLINE_LINUX="${GRUB_CMDLINE_LINUX} root=UUID=__TARGET_UUID__ rootdelay=2"
EOF

sed -i "s|__TARGET_UUID__|${NEW_UUID}|" /etc/default/grub.d/zz-nvme-root.cfg
update-grub

Why multiple root= entries resolve correctly

The generated command line in /boot/grub/grub.cfg will look similar to:

linux /boot/vmlinuz-... root=UUID=<eMMC-UUID> ro console=ttyMSM0... root=UUID=<NVME-UUID> rootdelay=2

In Linux and Debian/Ubuntu initramfs-tools, command line parameters are evaluated sequentially from left to right. When multiple root= targets are passed, the last entry takes precedence. rootdelay=2 provides an extra 2-second buffer for the PCIe controller and NVMe namespace to finish initialization before the root pivot.


4. Understanding GRUB Warnings during update-grub

During update-grub, the console may display repeated warnings like:

grub-probe: warning: Discarding improperly nested partition (hostdisk//dev/mmcblk0,gpt68,gpt1).

This is expected behavior on Qualcomm platforms:

  • The eMMC contains over 70 partitions. Partition 68 (mmcblk0p68) is a container partition wrapping nested sub-partitions for internal baseband/modem images.
  • When grub-probe inspects block devices, it flags the nested sub-tables and discards them from OS detection.
  • As long as the command outputs done with an exit code of 0, the boot configuration has been written successfully.

5. Verification & Rollback Procedure

Reboot the board once the script finishes:

sudo reboot

Verifying the Migration

Run:

findmnt /

You should see:

TARGET SOURCE         FSTYPE OPTIONS
/      /dev/nvme0n1p1 ext4   rw,noatime,discard,errors=remount-ro

Run df -h / to confirm the root filesystem reflects your SSD’s total capacity.

Emergency Rollback (eMMC Fallback)

Because /dev/mmcblk0p71 was never modified or erased, you can revert instantly:

  • From the OS:
    sudo rm /etc/default/grub.d/zz-nvme-root.cfg
    sudo update-grub
    sudo reboot
    
  • From the GRUB Menu (Serial/Display): Interrupt the 3-second GRUB boot counter, press e to edit the kernel line, delete root=UUID=... rootdelay=2, and press Ctrl+X to boot directly into eMMC.

6. Long-Term Maintenance: Kernel Updates

Because the initial kernel binary (/boot/vmlinuz) is loaded from the eMMC by GRUB, standard kernel updates require awareness:

  • On the NVMe system, /boot/efi maps back to the eMMC EFI partition via LABEL=system-boot.
  • However, /boot itself resides on the NVMe filesystem. If apt upgrade installs a new kernel image, it writes the new files to the NVMe drive’s /boot/ path.
  • Because the eMMC’s /boot holds the kernel that GRUB loads, you should periodically sync the updated kernel and initramfs binaries back to the eMMC partition when performing major kernel updates:
# Mount eMMC root to sync kernel binaries after an upgrade
sudo mkdir -p /mnt/emmc
sudo mount /dev/mmcblk0p71 /mnt/emmc
sudo rsync -av /boot/ /mnt/emmc/boot/
sudo umount /mnt/emmc

Appendix A: Hardware Context & I/O Constraints

When selecting external storage and peripherals for the Dragonwing / Ventuno Q platform, keep these hardware characteristics in mind:

USB-C Speed vs. DisplayPort Alt-Mode

The onboard USB-C port is managed by a Cypress CYPD6129 PD controller and a Texas Instruments TMUXHS4446RETT multiplexer. The high-speed differential pairs are shared dynamically:

ModeSuperSpeed Lanes Used ByEffective USB Data Speed
Data Only (No External Display)USB 3.0 Controller5 Gbps (USB 3.0 / 3.2 Gen 1x1)
DisplayPort Alt-Mode ActiveDisplayPort Video Output480 Mbps (USB 2.0 HighSpeed fallback)

If you rely on external high-speed storage over USB-C while running a display via DP Alt-Mode, data rates will drop to USB 2.0 speeds. Moving your primary operating system to an internal M.2 NVMe SSD keeps high-speed storage independent of display state.


Appendix B: The Complete Migration Script

#!/usr/bin/env bash
set -euo pipefail

# --- CONFIGURATION TARGETS ---
NVME_DEV="/dev/nvme0n1"
NVME_PART="/dev/nvme0n1p1"
PART_LABEL="nvme_root"
MNT_SSD="/mnt/ssd_root"
MNT_CUR="/mnt/current_root"

echo "=========================================================="
echo " Qualcomm Dragonwing / Ventuno Q: NVMe Rootfs Migration"
echo " (Safe Kernel Handoff Architecture)"
echo "=========================================================="

# 1. PRE-FLIGHT CHECKS
if [ "$EUID" -ne 0 ]; then
  echo "[-] ERROR: This script must be run as root (use sudo)." >&2
  exit 1
fi

CURRENT_ROOT=$(findmnt -n -o SOURCE /)
if [[ "$CURRENT_ROOT" == *"nvme"* ]]; then
  echo "[-] ERROR: Your system is ALREADY running from an NVMe drive ($CURRENT_ROOT)!" >&2
  exit 1
fi

if [ ! -b "$NVME_DEV" ]; then
  echo "[-] ERROR: Target SSD '$NVME_DEV' not found! Check your M.2 installation." >&2
  exit 1
fi

ROOT_USED_KB=$(df -k --output=used / | tail -n 1 | tr -d ' ')
DEV_SIZE_BYTES=$(blockdev --getsize64 "$NVME_DEV")
DEV_SIZE_KB=$((DEV_SIZE_BYTES / 1024))

if [ "$DEV_SIZE_KB" -le "$ROOT_USED_KB" ]; then
  echo "[-] ERROR: Target SSD is too small ($((DEV_SIZE_KB / 1024 / 1024)) GB) for current root usage ($((ROOT_USED_KB / 1024 / 1024)) GB)!" >&2
  exit 1
fi

echo "[+] Current Root is on: $CURRENT_ROOT"
echo "[+] Target NVMe Drive is: $NVME_DEV ($((DEV_SIZE_BYTES / 1024 / 1024 / 1024)) GB)"
echo ""
read -r -p "WARNING: ALL DATA ON $NVME_DEV WILL BE ERASED. Continue? (y/N): " CONFIRM
if [[ ! "$CONFIRM" =~ ^[yY]$ ]]; then
  echo "[-] Aborted by user."
  exit 0
fi

# Cleanup trap for safe interruption
cleanup() {
  echo "[*] Cleaning up temporary mounts..."
  umount "$MNT_CUR" 2>/dev/null || true
  umount "$MNT_SSD" 2>/dev/null || true
  rmdir "$MNT_CUR" 2>/dev/null || true
  rmdir "$MNT_SSD" 2>/dev/null || true
}
trap cleanup EXIT ERR INT TERM

# 2. ENSURE INITRAMFS HAS NVME MODULES
echo ""
echo "[1/5] Ensuring NVMe driver is in initramfs..."
mkdir -p /etc/initramfs-tools

MODULES_TO_ADD=("nvme" "nvme_core")
for mod in "${MODULES_TO_ADD[@]}"; do
  if ! grep -qs "^${mod}$" /etc/initramfs-tools/modules; then
    echo "$mod" >> /etc/initramfs-tools/modules
    echo "[+] Added '$mod' to /etc/initramfs-tools/modules"
  fi
done
update-initramfs -u -k all

# 3. PARTITION & FORMAT NVME SSD
echo ""
echo "[2/5] Partitioning and formatting $NVME_DEV..."

swapoff -a 2>/dev/null || true
umount "${NVME_DEV}"* 2>/dev/null || true

wipefs -a "$NVME_DEV"
parted -s "$NVME_DEV" mklabel gpt
parted -s -a optimal "$NVME_DEV" mkpart "$PART_LABEL" ext4 1MiB 100%
udevadm settle
partprobe "$NVME_DEV" || true
sleep 1

mkfs.ext4 -F -L "$PART_LABEL" "$NVME_PART"
udevadm settle

NEW_UUID=$(blkid -s UUID -o value "$NVME_PART")
if [ -z "$NEW_UUID" ]; then
  echo "[-] ERROR: Failed to obtain UUID for $NVME_PART." >&2
  exit 1
fi
echo "[+] Formatted $NVME_PART with UUID: $NEW_UUID"

# 4. CLONE ROOTFS TO SSD
echo ""
echo "[3/5] Cloning rootfs to NVMe SSD..."
mkdir -p "$MNT_SSD" "$MNT_CUR"
mount "$NVME_PART" "$MNT_SSD"
mount --bind / "$MNT_CUR"

rsync -axHAWX --numeric-ids --info=progress2 "$MNT_CUR/" "$MNT_SSD/"

mkdir -p "$MNT_SSD"/{dev,proc,sys,run,tmp,mnt,media,boot/efi,var/persist}
chmod 1777 "$MNT_SSD/tmp"

umount "$MNT_CUR"
rmdir "$MNT_CUR"

# 5. UPDATE FSTAB ON SSD
echo ""
echo "[4/5] Updating /etc/fstab on the SSD..."

sed -i "s|^LABEL=writable.*|UUID=${NEW_UUID}   /   ext4   discard,errors=remount-ro,noatime   0   1|" "$MNT_SSD/etc/fstab"

echo "[+] New /etc/fstab contents on SSD:"
cat "$MNT_SSD/etc/fstab"

umount "$MNT_SSD"
rmdir "$MNT_SSD"

# 6. CONFIGURE GRUB ON eMMC FOR KERNEL HANDOFF
echo ""
echo "[5/5] Configuring GRUB kernel handoff on eMMC..."
mkdir -p /etc/default/grub.d

cat << 'EOF' > /etc/default/grub.d/zz-nvme-root.cfg
# Dragonwing NVMe root handoff
GRUB_CMDLINE_LINUX="${GRUB_CMDLINE_LINUX} root=UUID=__TARGET_UUID__ rootdelay=2"
EOF

sed -i "s|__TARGET_UUID__|${NEW_UUID}|" /etc/default/grub.d/zz-nvme-root.cfg

echo "[+] Generated /etc/default/grub.d/zz-nvme-root.cfg:"
cat /etc/default/grub.d/zz-nvme-root.cfg

echo ""
echo "[*] Updating GRUB..."
update-grub

sync
trap - EXIT ERR INT TERM

echo ""
echo "=========================================================="
echo " [SUCCESS] Migration complete!"
echo " The EFI stub remains safe on eMMC, and the kernel"
echo " will hand over rootfs to your NVMe SSD on boot."
echo " Reboot now to boot from your NVMe SSD."
echo "=========================================================="