QEMU SPI NOR flash with SWUpdate recovery
- 04 May, 2025

In the QEMU Custom SPI peripheral post the support for SPI controller for Cubieboard was introduced. A custom SPI peripheral was developed to demonstrate basic functionality.
QEMU comes with out-of-the-box support for several different SPI peripherals, one of which is the SPI NOR flash memory, modeled after M25P80 chip, but supports different variants, from various manufacturers and memory sizes.
The NOR memory can be combined with the SWUpdate for Cubieboard Yocto setup and Yocto Initramfs, to introduce a recovery option in case the eMMC memory gets corrupted.
In this post the steps for making a SWUpdate recovery initramfs image with Yocto and loading it from SPI NOR flash memory attached to QEMU Cubieboard will be shown. Once loaded, the initramfs image will format and partition the eMMC memory and start SWUpdate installation with a file from a USB drive.
Info
In the post terms eMMC and SD will be used interchangeably, as from the end-user perspective the eMMC can be viewed as a non-removable SD card. The eMMC-specific partitions, like boot partitions, are not supported by QEMU, so they will not be covered in this post.
The following items will be covered
Tip
All of the changes that will be described, plus a few more which are required for this to work, are available in the
scarthgap
branch of
meta-mistra repository in
Github.
Yocto recovery image
In order to implement the recovery image, the initramfs image used in the initramfs post needs to be extended with several utilities.
SWupdate support
The SWupdate support needs to be added, but in this case there is no need for the web server support. Instead, the client utility can be added since the installation will be started using an update file stored on the USB drive.
Therefore, the following two items should be added to the image definition
PACKAGE_INSTALL += "\
swupdate \
swupdate-client \
"
Bootloader upgrade
Since the recovery image is supposed to completely recover the eMMC, the update image should be extended to also write the bootloader to the appropriate location in the eMMC memory.
The SWupdate sw-description
is updated to also hold information on the bootloader and the bootscript, so those files
can be deployed with every upgrade.
images:(
...,
{
filename = "u-boot-sunxi-with-spl.bin";
sha256 = "$swupdate_get_sha256(u-boot-sunxi-with-spl.bin)";
device = "/dev/mmcblk0";
offset = "8K";
});
files: ({
filename = "boot.scr";
path = "/boot.scr";
device = "/dev/mmcblk0p1";
filesystem = "vfat";
sha256 = "$swupdate_get_sha256(boot.scr)";
});
Note
From the description, the U-Boot binary will be written to the eMMC at 8kB offset, and the boot.scr
file will be
copied to the first partition of the eMMC, which is FAT formatted.
This also means that the recipe for the update file generation needs to be extended to depend on these files being
present, so following lines must be updated in the update-image.bb
recipe
IMAGE_DEPENDS = "\
virtual/bootloader \
mistra-swupdate \
"
SWUPDATE_IMAGES = "\
${SPL_BINARY} \
boot.scr \
mistra-swupdate-cubieboard-ng.rootfs \
"
Deploying U-Boot environment
In order for SWUpdate to work, it expects certain information to be provided by U-Boot. That information is passed through the U-Boot environment.
The default configuration for Cubieboard is to have environment stored in the /boot
partition of the eMMC. Since eMMC
should be clean in the scenario that is considered for this post, then the U-Boot environment will not be present.
In order to circumvent this, we can deploy the default U-Boot environment in the initramfs and use it before running the update command.
This is done by adding a ROOTFS_POSTPROCESS_COMMAND
in the initramfs image recipe. The command will copy the initial
environment into a file in the /etc/u-boot-initial-env
, which is the default location libubootenv tools (fw_printenv
and fw_setenv
) use.
add_uboot_initial_env() {
install -d ${IMAGE_ROOTFS}${sysconfdir}
install -m 0644 ${DEPLOY_DIR_IMAGE}/u-boot-initial-env ${IMAGE_ROOTFS}${sysconfdir}
}
ROOTFS_POSTPROCESS_COMMAND += "add_uboot_initial_env; "
Disk management utilities
Once the initramfs is started, the eMMC memory needs to be partitioned and formatted.
The eMMC will be partitioned using sfdisk
and mke2fs
will be used to format the ext4 partitions.
Tip
There is no need to format all partitions, only the boot and data, since those will not be handled by the update process.
The sfdisk
and mkfs
packages are added in the image recipe using
PACKAGE_INSTALL += "\
util-linux-sfdisk \
e2fsprogs-mke2fs \
"
Partitioning eMMC in the initramfs init script will be performed as
sfdisk /dev/mmcblk0 <<EOF
unit: sectors
sector-size: 512
start= 4096, size= 81920, type=c, bootable
start= 86016, size= 681574, type=L
start= 770048, size= 681574, type=L
start= 1454080, size= , type=L
EOF
The values for the start and size sectors for partitions are dumped from the eMMC image that was generated with the Yocto build system, so the partitioning of eMMC from initramfs applies the exact same configuration.
After the eMMC is partitioned, formatting of the boot and data partitions is done as
mkfs.vfat /dev/mmcblk0p1
mkfs.ext4 -F -E lazy_itable_init /dev/mmcblk0p4
Tip
The -E lazy_itable_init
is used to speed up the formatting process by deferring the initialization of the inodes to
when the filesystem is first mounted.
Init script updates
While most of the configuration is kept from the Yocto Initramfs, a new init script is created to fulfill the desired scenario requirements.
The script performs several actions
- Partition and format the eMMC according to the steps shown above
- Create
uboot.env
file in the/boot/
partition to hold the U-Boot environment, needed for SWUpdate to function properly - Start SWUpdate main process
- Mount USB drive which holds the update file
- Run the update using
swupdate-client
After swupdate-client
is done with the update, a reboot will be performed.
FitImage
One of the requirements set at the top is that the SPI NOR memory is used to store the recovery image. This means that the Linux kernel binary, device tree and the bundled initramfs should be stored there and fetched via U-Boot at boot time.
U-Boot has command sf read
that can be used to read desired amount of bytes from SPI NOR memory into the RAM memory.
However, in order to be able to invoke that command, the U-Boot somehow needs to know how many bytes should be loaded.
Therefore, we need to find a way to obtain that information for both kernel image (with bundled initramfs) and device
tree blob.
In order to simplify things, we will use fitImage format to bundle all three items into one binary file. This way we will have to get information on size and load only one item instead of three. The additional benefit will be that the fitImage header will hold the size of the complete bundle, so we will be able to determine the size of the binary at runtime.
The fitImage generation requires creating a fitImage description, and then running mkimage
command on it. The
description is in the form of device tree description, containing information about the binaries that need to be
included, as well as configurations that are supported.
Tip
Yocto supports auto-generation of the fitImage description file (.its). It is enough to define
KERNEL_CLASSES += "kernel-fitimage"
KERNEL_IMAGETYPES += "fitImage"
in one of the config files (MACHINE in our case).
The fitImage description for our use-case (auto-generated by Yocto) is
/dts-v1/;
/ {
description = "Kernel fitImage for Mistra Recovery/6.6.28/cubieboard-ng";
#address-cells = <1>;
images {
kernel-1 {
description = "Linux kernel";
data = /incbin/("linux.bin");
type = "kernel";
arch = "arm";
os = "linux";
compression = "none";
load = <0x40008000>;
entry = <0x40008000>;
hash-1 {
algo = "sha256";
};
};
fdt-sun4i-a10-cubieboard.dtb {
description = "Flattened Device Tree blob";
data = /incbin/("arch/arm/boot/dts/allwinner/sun4i-a10-cubieboard.dtb");
type = "flat_dt";
arch = "arm";
compression = "none";
hash-1 {
algo = "sha256";
};
};
};
configurations {
default = "conf-sun4i-a10-cubieboard.dtb";
conf-sun4i-a10-cubieboard.dtb {
description = "1 Linux kernel, FDT blob";
kernel = "kernel-1";
fdt = "fdt-sun4i-a10-cubieboard.dtb";
hash-1 {
algo = "sha256";
};
};
};
};
Running the mkimage
command will generate the following output, indicating that the inidividual binaries were included
FIT description: Kernel fitImage for Mistra Recovery/6.6.28/cubieboard-ng
Created: Wed Apr 17 09:19:38 2024
Image 0 (kernel-1)
Description: Linux kernel
Created: Wed Apr 17 09:19:38 2024
Type: Kernel Image
Compression: uncompressed
Data Size: 5343968 Bytes = 5218.72 KiB = 5.10 MiB
Architecture: ARM
OS: Linux
Load Address: 0x40008000
Entry Point: 0x40008000
Hash algo: sha256
Hash value: 76b08f9eca78d0a8f8985d85a900dcad012206232a73323149ce9f1d44b3f0cf
Image 1 (fdt-sun4i-a10-cubieboard.dtb)
Description: Flattened Device Tree blob
Created: Wed Apr 17 09:19:38 2024
Type: Flat Device Tree
Compression: uncompressed
Data Size: 23462 Bytes = 22.91 KiB = 0.02 MiB
Architecture: ARM
Hash algo: sha256
Hash value: 42e9f15c500146fb23e7fac3f9e46fee9d67b5326c367eaac80a2813e4fdb68c
Default Configuration: 'conf-sun4i-a10-cubieboard.dtb'
Configuration 0 (conf-sun4i-a10-cubieboard.dtb)
Description: 1 Linux kernel, FDT blob
Kernel: kernel-1
FDT: fdt-sun4i-a10-cubieboard.dtb
Hash algo: sha256
Hash value: unavailable
Determining size
The fitImage header is essentially the Flattened Device Tree (FDT) header, used by U-Boot to get information on loading binaries. The header structure is
struct fdt_header {
uint32_t magic;
uint32_t totalsize;
uint32_t off_dt_struct;
uint32_t off_dt_strings;
uint32_t off_mem_rsvmap;
uint32_t version;
uint32_t last_comp_version;
uint32_t boot_cpuid_phys;
uint32_t size_dt_strings;
uint32_t size_dt_struct;
};
From the structure definition, the size of the image is the second word, or 4 bytes from the start of the image.
To check if the value is there and that it can be used, the size of the fitImage that is generated for this post is
10676900 bytes, or 0xA2EAA4 in hex format. Checking the 2nd word of the binary with xxd
returns
xxd -s 4 -p fitImage--6.6.28-r0-cubieboard-ng-20250501190148.bin | head -c 8
00a2eaa4
Adding SPI NOR flash to Cubieboard
In order to add the SPI NOR flash memory to Cubiebboard in QEMU, some code changes are required. The patch with the code changes can be used on QEMU v10.0.0.
Info
The memory that is chosen is IS25WP128 since it has size of 16MB, which is enough to fit the fitImage (10MB).
What the patch does is:
- instantiate the SPI NOR memory
- connect it to MTD interface (important for later)
- connects the CS (Chip select) pin of the memory with the chip select pin 0 (CS0) of the SPI controller
With these changes, the QEMU would create an SPI NOR memory on startup and we could write and read back the written contents.
However, the idea in this post is to put the fitImage with recovery image in this memory, so the QEMU invocation command will have an addition, which will connect a binary file on the host, representing the flash memory, with the MTD interface component generated in the code. That way, every read and write associated with the emulated SPI NOR flash memory will be done with that binary file on the host.
When the file is generated, we should fill it with ones (0xFFs), since that would correspond to an erased flash memory. That can be achieved by using
dd if=/dev/zero ibs=1 count=$((16*1024*1024)) | LC_ALL=C tr '\000' '\377' >flash.bin
In order to start QEMU with host-backed flash memory storage, following command can be used
qemu-system-arm \
-M cubieboard \
-m 1G \
-kernel ${UBOOT} \
-nographic \
-drive if=mtd,file=flash.bin,format=raw
Info
The ${UBOOT}
is the path to the U-Boot binary compiled with the CONFIG_OF_EMBED=y
option, since that is required
in order to run U-Boot directly in QEMU.
Accessing from U-Boot
Before the memory can be accessed from U-Boot (or Linux kernel, but that is not the focus of this post):
- configuration update is needed, in order to integrate drivers for SPI flash and ISSI memory,
- a device tree update is needed, since U-Boot (Linux kernel) get information about available SPI devices from it.
Following config options need to be added to the U-Boot configuration
CONFIG_SPI=y
CONFIG_SPI_FLASH=y
CONFIG_SPI_FLASH_ISSI=y
CONFIG_CMD_SF=y
CONFIG_CMD_SF_TEST=y
The following addition is needed in the spi0 definition of the sun4i-a10-cubieboard.dts description (patched file)
flash: m25p80@0 {
compatible = "issi,is25wp128", "jedec,spi-nor";
spi-max-frequency = <20000000>;
reg = <0>;
};
Once U-Boot is started, we can check that the SPI NOR is available and do several tests.
First we can probe that the memory is recognized using
=> sf probe
SF: Detected is25wp128 with page size 256 Bytes, erase size 4 KiB, total 16 MiB
We can also try to write some content and the read it back. Since the sf write
and sf read
commands can only move
data from RAM to flash and back, first we need to populate RAM with some data.
First we write one word (0x12345678) to an address in RAM (0x40000000)
=> mw 40000000 12345678
Then write the value from that address in RAM (0x40000000) to start of flash memory (0x0). The 4
in the command is
number of bytes to transfer (one word)
=> sf write 40000000 0 4
Reading the value from memory to RAM (different location now, 0x40001000) and printing it is done in the following way
=> sf read 40001000 0 4
=> md 40001000 1
40001000: 12345678 xV4.
which confirms that the value is written.
We can also check on the host that the value is written to the flash.bin
using
xxd -p flash.bin | head -c 4
78563412
This shows that the data is written, but the representation of bytes in the NOR flash has different endianess.
Info
This will be important later when we need to read the size of the fitImage, since we will have to change endianess on the fly.
One more test that can be performed is the built-in U-Boot NOR memory test (enabled with CONFIG_CMD_SF_TEST=y
).
Command accepts offset and length of bytes to test, so running test across the whole memory would be invoked using
=> sf test 0 1000000
SPI flash test:
0 erase: 1 ticks, 16384000 KiB/s 131072.000 Mbps
1 check: 5354 ticks, 3060 KiB/s 24.480 Mbps
2 write: 9475 ticks, 1729 KiB/s 13.832 Mbps
3 read: 5284 ticks, 3100 KiB/s 24.800 Mbps
Test passed
0 erase: 1 ticks, 16384000 KiB/s 131072.000 Mbps
1 check: 5354 ticks, 3060 KiB/s 24.480 Mbps
2 write: 9475 ticks, 1729 KiB/s 13.832 Mbps
3 read: 5284 ticks, 3100 KiB/s 24.800 Mbps
Loading to flash memory
Besides loading the Flash memory with all ones, we can load a custom value into the memory.
For instance, load other binary files using dd
. Let’s create a helper file with the 0x12345678
contents
echo -ne '\x78\x56\x34\x12' > helper.bin
Then, we can write contents of helper.bin
to the beginning of flash.bin
using
dd if=helper.bin of=flash.bin conv=notrunc
Tip
The conv=notrunc
is important, since otherwise the whole file will be truncated to the size of the helper.bin
Now, when the first 4 bytes are read from the SPI NOR flash in QEMU, it will show
=> sf read 40000000 0 4
=> md 40000000 1
40000000: 12345678 xV4.
Testing
The following test scenario will be used:
- QEMU will be started with U-Boot as kernel parameter, with flash memory loaded with fitImage, USB drive with SWUpdate update file and an empty SD card image attached to it.
- fitImage initramfs will partition and format the eMMC/SD card, install update from USB and reboot; after reboot the QEMU can be closed in order to start it again from the SD card
- QEMU will be started with SD card parameter, to demonstrate that the eMMC/SD card has been created properly.
Preparation
The preparation for testing will involve several steps.
First, the needed images should be built using Yocto. Then, those images should be put into appropriate locations so they can be used.
Building images
The recovery image is built using
. ./setup-environment build_recovery
DISTRO=mistra-recovery MACHINE=cubieboard-ng bitbake mistra-swupdate-recovery
The bootloader that will be used to run the loading from SPI NOR flash is built using
. ./setup-environment build_recovery
DISTRO=mistra-recovery MACHINE=cubieboard-ng bitbake virtual/bootloader
The SWUpdate update image is built using
. ./setup-environment build_fb
DISTRO=mistra-framebuffer MACHINE=cubieboard-ng bitbake update-image
Note
The instructions include setting up different build directories since different DISTRO
settings are used for these
builds.
Once all images are built, preparation of other binary files can be done.
Info
The output files can be found in the tmp/deploy/images/cubieboard-ng/
directories under corresponding build
directories.
Preparing USB image
The USB drive should have one FAT partition and the update image should be copied to it.
The following commands create a 1GB image for the USB drive and one FAT partition on it.
dd if=/dev/zero of=usb.img bs=1M count=1024
sfdisk usb.img <<EOF
,,c
EOF
The kpartx
utility can be used to make the partition available to the system, so it can be formatted.
sudo kpartx -av ./usb.img
add map loop0p1 (252:0): 0 2095104 linear 7:0 2048
Note
loopXpY
can differ, use the correct ones.Based on the output, the partition to use is /dev/mapper/loop0p1
, so we can format it using
sudo mkfs.vfat /dev/mapper/loop0p1
In order to copy the files, partition needs to be mounted
sudo mkdir -p /run/mount/usb
sudo mount /dev/mapper/loop0p1 /run/mount/usb
sudo cp update-image-cubieboard-ng.rootfs.swu /run/mount/usb
Now that the file is in place, cleanup can be performed
sudo umount /run/mount/usb
sudo kpartx -d ./usb.img
Preparing SPI NOR image
The SPI NOR image should be created as it was done initially, by writing all FF’s to it
dd if=/dev/zero ibs=1 count=$((16*1024*1024)) | LC_ALL=C tr '\000' '\377' >flash.bin
After that, the fitImage binary should be copied directly
dd if=fitImage-cubieboard-ng.bin of=flash.bin conv=notrunc
Preparing SD card image
The SD card/eMMC image should be created empty, since the recovery image should partition and format it later. A 1GB
image is enough and it can be created using qemu-img
tool
qemu-img create sd.img 1G
Tip
The build directory of QEMU should be added to the PATH
in order to have the qemu-img
tool available
Recovery run
After all images are prepared, the QEMU can be started with
qemu-system-arm \
-M cubieboard \
-m 1G \
-kernel /work/cubieboard_ng-mistra-linux-gnueabi/u-boot/2024.01/build/u-boot \
-usb \
-device usb-storage,bus=usb-bus.1,drive=stick \
-drive if=none,id=stick,file=usb.img \
-sd sd.img \
-drive if=mtd,file=flash.bin,format=raw \
-nographic
U-Boot 2024.01-g (Jan 08 2024 - 15:37:48 +0000) Allwinner Technology
CPU: Allwinner A10 (SUN4I)
Model: Cubietech Cubieboard
DRAM: 1 GiB
Core: 76 devices, 26 uclasses, devicetree: embed
WDT: Not starting watchdog@1c20c90
MMC: mmc@1c0f000: 0
Loading Environment from FAT... Unable to use mmc 0:0...
Unknown monitor
Unknown monitor
In: serial,usbkbd
Out: serial,vidconsole
Err: serial,vidconsole
Net:
Error: ethernet@1c0b000 No valid MAC address found.
No ethernet found.
starting USB...
Bus usb@1c14000: USB EHCI 1.00
Bus usb@1c14400: USB OHCI 1.0
Bus usb@1c1c000: USB EHCI 1.00
Bus usb@1c1c400: USB OHCI 1.0
scanning bus usb@1c14000 for devices... 1 USB Device(s) found
scanning bus usb@1c14400 for devices... 1 USB Device(s) found
scanning bus usb@1c1c000 for devices... 2 USB Device(s) found
scanning bus usb@1c1c400 for devices... 1 USB Device(s) found
scanning usb for storage devices... 1 Storage Device(s) found
Hit any key to stop autoboot: 0
Note
The U-Boot ELF binary is located in the work directory, so path from the repo root would be
build_recovery/tmp/work/cubieboard_ng-mistra-linux-gnueabi/u-boot/2024.01/build/u-boot
Using U-Boot prompt we need to load the fitImage from the SPI NOR flash memory. But before we can do it, we need to get information on the fitImage size from the header.
The simplest way is to load the first 8 bytes into RAM, and then reorder bytes in order to get the correct value.
=> sf probe
SF: Detected is25wp128 with page size 256 Bytes, erase size 4 KiB, total 16 MiB
=> sf read 40000000 0 8
device 0 offset 0x0, size 0x8
SF: 8 bytes @ 0x0 Read: OK
=> setexpr.b fit_size_msb *40000004
=> setexpr.b fit_size_3 *40000005
=> setexpr.b fit_size_2 *40000006
=> setexpr.b fit_size_lsb *40000007
=> setenv fit_size ${fit_size_msb}${fit_size_3}${fit_size_2}${fit_size_lsb}
Now we can load the fitImage into RAM with
=> sf read ${loadaddr} 0 ${fit_size}
device 0 offset 0x0, size 0xa2eaa4
SF: 10676900 bytes @ 0x0 Read: OK
The image can now be executed using
=> bootm
The kernel will now start and perform the scenario as we defined it.
After the update is done, the system will reboot.
Warning
After the system reboots, QEMU will start running the U-Boot passed via -kernel
again. In order to validate that the
U-Boot is properly installed on the SD card/eMMC image, current ran can be exited using Ctrl+A X
eMMC installed run
Now that the recovery is complete, QEMU can be started in the following manner in order to boot from SD card/eMMC
qemu-system-arm \
-M cubieboard \
-m 1G \
-net nic \
-net tap,ifname=qemu-tap0,script=no,downscript=no \
-usb \
-sd sd.img \
-nographic
Mistra FrameBuffer 4.0 cubieboard-ng ttyS0
cubieboard-ng login:
Summary
This was a rather complex post, combining use of USB, SPI NOR and SD card to implement recovery procedure for Cubieboard QEMU.
The SPI NOR and USB can be an interesting addition to the interfaces, and various scenarios can be tested.