Streamlining Emulation in U-Boot: A Kconfig Cleanup 🧹

In the world of software development, consistency is key. A recent update to U-Boot Concept takes a solid step in that direction by restructuring how it handles emulation targets. This change makes life easier for developers working across different processor architectures.

Previously there were inconsistencies in the configuration system (Kconfig). For example, enabling QEMU emulation for ARM systems used the ARCH_QEMU symbol, while x86 systems used VENDOR_EMULATION for a similar purpose. This could create confusion and added complexity when managing board configurations.

To resolve this, a new, architecture-neutral symbol, MACH_QEMU, has been introduced. This single, unified option replaces the separate symbols for both ARM and x86 emulation targets.

This small but important merge tidies up the codebase, creating a more consistent and intuitive developer experience. It also sets the stage for future work, with the potential to extend this unified approach to other architectures. It’s a great example of the continuous effort to keep U-Boot clean, efficient, and easy to maintain for everyone involved.




Filesystems in U-Boot

U-Boot supports a fairly wide variety of filesystems, including ext4, ubifs, fat, exfat, zfs, btrfs. These are an important part of bootloader functionality, since reading files from bare partitions or disk offsets is neither scalable nor convenient.

The filesystem API is functional but could use an overhaul. The main interface is in fs/fs.c, which looks like this:

struct fstype_info {
	int fstype;
	char *name;
	/*
	 * Is it legal to pass NULL as .probe()'s  fs_dev_desc parameter? This
	 * should be false in most cases. For "virtual" filesystems which
	 * aren't based on a U-Boot block device (e.g. sandbox), this can be
	 * set to true. This should also be true for the dummy entry at the end
	 * of fstypes[], since that is essentially a "virtual" (non-existent)
	 * filesystem.
	 */
	bool null_dev_desc_ok;
	int (*probe)(struct blk_desc *fs_dev_desc,
		     struct disk_partition *fs_partition);
	int (*ls)(const char *dirname);
	int (*exists)(const char *filename);
	int (*size)(const char *filename, loff_t *size);
	int (*read)(const char *filename, void *buf, loff_t offset,
		    loff_t len, loff_t *actread);
	int (*write)(const char *filename, void *buf, loff_t offset,
		     loff_t len, loff_t *actwrite);
	void (*close)(void);
	int (*uuid)(char *uuid_str);
	/*
	 * Open a directory stream.  On success return 0 and directory
	 * stream pointer via 'dirsp'.  On error, return -errno.  See
	 * fs_opendir().
	 */
	int (*opendir)(const char *filename, struct fs_dir_stream **dirsp);
	/*
	 * Read next entry from directory stream.  On success return 0
	 * and directory entry pointer via 'dentp'.  On error return
	 * -errno.  See fs_readdir().
	 */
	int (*readdir)(struct fs_dir_stream *dirs, struct fs_dirent **dentp);
	/* see fs_closedir() */
	void (*closedir)(struct fs_dir_stream *dirs);
	int (*unlink)(const char *filename);
	int (*mkdir)(const char *dirname);
	int (*ln)(const char *filename, const char *target);
	int (*rename)(const char *old_path, const char *new_path);
};

At first glance this seems like a reasonable API. But where is the filesystem specified? The API seems to assume that this is already present somehow.

In fact there is a pair of separate functions responsible for selecting which filesystem the API acts on:

int fs_set_blk_dev(const char *ifname, const char *dev_part_str, int fstype)
int fs_set_blk_dev_with_part(struct blk_desc *desc, int part)

When you want to access a file, call either of these functions. It sets three ‘global’ variables, fs_dev_desc, fs_dev_part and fs_type . After each operation, a call to fs_close() resets things. This means you must select the block device before each operation. For example, see this code in bootmeth-uclass.c:

	if (IS_ENABLED(CONFIG_BOOTSTD_FULL) && bflow->fs_type)
		fs_set_type(bflow->fs_type);

	ret = fs_size(path, &size);
	log_debug("   %s - err=%d\n", path, ret);

	/* Sadly FS closes the file after fs_size() so we must redo this */
	ret2 = bootmeth_setup_fs(bflow, desc);
	if (ret2)
		return log_msg_ret("fs", ret2);

It is a bit clumsy. Obviously this interface is not set up to support caching. In fact the filesystem is mounted afresh each time it is accessed. In a bootloader this is normally not too much of a problem. Since the OS and associated files are normally packaged in a FIT, a single read is enough to obtain everything that is needed. But if multiple directories need to be searched to find that FIT, or if there are multiple files to read, the repeated mounting does slow things down.

If you have sharp eyes you might have seen another problem. The two functions above assume that they are dealing with a block device. In fact, struct blk_desc is the uclass-private data for a block device. What about when the filesystem is on the network? Also, with sandbox it is possible to access host files:

=> ls hostfs 0 /tmp/gimp
DIR    1044480 ..
DIR       4096 .
DIR       4096 2.10
=> 

Clearly, the files on the hostsystem are not accessed at the block level. How does that work?

The key to this is null_dev_desc_ok , which is true for the hostfs filesystem. There is a special case in the code to handle this.

int blk_get_device_part_str(const char *ifname, const char *dev_part_str,
			     struct blk_desc **desc,
			     struct disk_partition *info, int allow_whole_dev)
{
...
#if IS_ENABLED(CONFIG_SANDBOX) || IS_ENABLED(CONFIG_SEMIHOSTING)
	/*
	 * Special-case a pseudo block device "hostfs", to allow access to the
	 * host's own filesystem.
	 */
	if (!strcmp(ifname, "hostfs")) {
		strcpy((char *)info->type, BOOT_PART_TYPE);
		strcpy((char *)info->name, "Host filesystem");

		return 0;
	}
#endif

It isn’t great. I’ve been looking at virtio-fs lately, which also doesn’t use a block device.

There are other things that could be improved, too:

  • Filesystems must be specified explicitly by their device and partition number. It would be nice to have a unified ‘VFS’ like Linux (and Barebox) so filesystems could be mounted within a unified space.
  • Files cannot be accessed from a device, nor is there any way to maintain a reference to a file you are working with
  • Reading a file must done all at once, in most cases. It would be nice to have an interface to open, read and close the file.

Instead of adding yet more special cases, it may be time to overhaul the code a little.




Verified Boot for Embedded on RK3399

VBE has been a long-running project to create a smaller and faster alternative to EFI. It was originally introduced as a concept in 2022, along with a sandbox implementation and a simple firmware updater for fwupd.

In the intervening period an enormous about of effort has gone into getting this landed in U-Boot for a real board. This has resulted in 10 additional series, on top of the sandbox work:

  • A – Various MMC and SPL tweaks (14 patches, series)
  • B – binman: Enhance FIT support for firmware (20 patches, series)
  • C – binman: More patches to support VBE (15 patches, series)
  • D – A collection of minor tweaks in MMC and elsewhere (18 patches, series)
  • E – SPL improvements and other tweaks (19 patches, series)
  • F – VBE implementation itself, with SPL ‘relocating jump’ (22 patches, series)
  • G – VBE ‘ABrec’ implementation in TPL/SPL/VPL (19 patches, series)
  • H – xPL-stack cleanup (4 patches, series)
  • I – Convert rockchip to use Binman templates (7 patches, series), kindly taken over and landed by Jonas Karlman
  • J – Implementation for RK3399 (25 patches, series)

That’s a total of 163 patches!

The Firefly RK3399 board was chosen, since it has (just) enough SRAM and is fully open source.

The final series has not yet landed in the main tree and it is unclear whether it will. For now I have put it in the Concept tree. You can see a video of it booting below:

I have been thinking about why this took so long to (almost) land. Here is my list, roughly in order from most important to least:

  1. Each series had to land before the next could be sent, with it taking at least one release cycle (3 months) to land each one
  2. Some of the new features were difficult to implement, particularly the relocating SPL jump and the new Binman features
  3. Many of the patches seemed aimless or irrelevant when sent, since they had no useful purpose before VBE could fully land. This created resistance in review
  4. On the other hand, sending too many patches at once would cause people to ignore the series

Overall it was a very difficult process, even for someone who knows U-Boot well. It concerns me that it has become considerably harder to introduce major new things in U-Boot, compared to the days of sandbox or driver model. I don’t have much of a comparison with other firmware projects, but I’m interested in hearing other people’s point of view. Please add a comment if you have thoughts on this.

Anyway, I am pleased to be done with it. The only thing missing at present is ‘ABrec’ updates in fwupd. It should be fairly easy to do, but for the signature checking. Since fwupd has its own implementation of libfdt, that might be non-trivial.

More information on VBE:




Booting into Linux in 100ms

A few weeks ago I took a look at Qboot, a minimal x86 firmware for QEMU which can boot in milliseconds. Qboot was written by Paolo Bonzini and dates from 2015 and there is an LWN article with the original announcement.

I tried it on my machine and it booted in QEMU (with kvm) in about 20ms, from entering Qboot to entering Linux. Very impressive! I was intrigued as to what makes it so fast.

There is another repo, qemu-boot-time by Stefan Garzarella, which provides an easy means to benchmark the boot. It uses perf events in Linux to detect the start and end of Qboot.

Using x86 post codes, I added the same to U-Boot. Initially the boot time was 2.9 seconds! Terrible. Here is a script which works on my machine and measures the time taken for U-Boot boot, using the qemu-x86_64 target:

It turned out that almost two of the seconds were the U-Boot boot delay. Another 800ms was the PXE menu delay. With those removed the time dropped to 210ms, which is not bad.

Using CONFIG_NO_NET dropping CONFIG_VIDEO each shaved off about 50ms. I then tried passing the kernel and initrd through QEMU using the QFW interface. It only saved 15ms but it is something.

I figured that command-line processing would be quite slow. With CONFIG_CMDLINE disabled another 5ms was saved. A final 7ms came from disabling filesystems and EFI loader. Small gains.

In the end, my result is about 83ms (in bold below):

$ ./contrib/qemu-boot-timer.sh
starting perf
building U-Boot
running U-Boot in QEMU
waiting for a bit
qemu-system-x86_64: terminating on signal 15 from pid 2775874 (bash)
parsing perf results
1) pid 2779434
qemu_init_end: 51.924873
u_boot_start: 51.962744 (+0.037871)
u_boot_do_boot: 134.781048 (+82.818304)

One final note: the qemu-x86_64 target actually boots by starting SPL in 16-bit mode and then moving to 64-bit mode to start U-Boot proper. This was partly to avoid calling 16-bit video ROMs from 64-bit code. Now that bochs is used for the display, it should be easy enough to drop SPL for this target. I’m not sure how much time that would save.

Note: Some final hacks are tagged here.




Booting Up the U-Boot Blog!

Welcome to the new official blog for Das U-Boot! For over two decades, the U-Boot mailing list has been the vibrant hub of our development discussions. While the mailing list remains our primary channel for patches and technical conversations, this blog will serve as a new platform to share deeper insights, project news, and community stories in a more accessible format.

As of mid-2025, U-Boot is more critical to the embedded ecosystem than ever. Our development is thriving, with robust support spanning a vast array of architectures from ARM and x86 to the rapidly expanding world of RISC-V. U-Boot provides a flexible, powerful foundation for countless products in the industrial, automotive and consumer-electronics spaces, incorporating modern standards like FIT, EFI and Firmware Handoff to meet today’s demanding security and interoperability requirements.

This blog is a community platform, and we need your voice to make it a success. We invite developers, maintainers, and users to contribute posts. Have you recently ported U-Boot to a novel board? Do you have a technical deep-dive on a specific subsystem you’d like to share? Or perhaps a case study on how U-Boot solved a unique challenge in your project?

If you have an idea for a post, please email the admin to get a login for this site. We’re excited to build this resource with you.

Happy hacking!