Skip to main content
Terminal window icon with prompt and cursor RJSTONE.net

Back to all posts

Experimenting With Das U-Boot Development Using WSL

Published on by Robert Stone · 17 min read

What This Is About

I have this Pinebook Pro and want to build u-boot firmware for it. But I’d rather cross-compile and test it out in qemu, rather than have to fiddle with booting the real hardware every time.

So I want to set up a cross-compilation environment for aarch64 (arm64) that will let me do that (even if I can’t build for every possible hardware target with every possible feature).

I’m going to try to do this with WSL in Windows, even though I’m not sure this is really the best option. (A dev container or VM might be more suitable.)

What I’m Using

Windows Subsystem for Linux v2 with Ubuntu 22. I think this is just the default Linux distribution for WSL these days.

> hostnamectl  status
...
         Chassis: container
...
  Virtualization: wsl
Operating System: Ubuntu 22.04.5 LTS
          Kernel: Linux 6.6.87.1-microsoft-standard-WSL2
    Architecture: x86-64

> wsl.exe --version
WSL version: 2.5.7.0
Kernel version: 6.6.87.1-1
WSLg version: 1.0.66
...
Windows version: 10.0.26100.4351

What I’m Doing

I’ll try to get all these steps in the right order according to how I’m doing things.

Add arm64 to dpkg

It gets annoying, but some things use the name arm64 to identify the ARM 64-bit architecture target, and other things use aarch64. For dpkg we need:

> dpkg --add-architecture arm64
> dpkg --print-foreign-architectures
arm64

Most other things will refer to ARM architecture as aarch64 (or the same with 32 for 32 bit).

I’m not sure how much this will help, but I figure I might as well tell dpkg that it’s allowed to install packages for the target platform just in case it comes in handy.

Installing Necessary Packages

Here’s what I’m installing.

sudo apt update

sudo apt-get install bc bison build-essential coccinelle \
  device-tree-compiler dfu-util efitools flex gdisk graphviz imagemagick \
  libgnutls28-dev libncurses-dev gcc-aarch64-linux-gnu \
  libpython3-dev libsdl2-dev libssl-dev lz4 lzma lzma-alone openssl \
  pkg-config python3 python3-asteval python3-coverage python3-filelock \
  python3-pkg-resources python3-pycryptodome python3-pyelftools \
  python3-pytest python3-pytest-xdist python3-sphinxcontrib.apidoc \
  python3-sphinx-rtd-theme python3-subunit python3-testtools \
  python3-venv swig uuid-dev python3-setuptools

This is the list of dependencies in the u-boot docs minue the package libguestfs-tools because it depends on mdadm which just hangs in WSL in it’s install hook script. I’m guessing it wants to add a kernel module that doesn’t work in WSL or something. If preparation to work around this if needed (probably not needed for what I’m doing):

sudo apt-add-repository -s
cd <the same directory where I cloned u-boot>
ls .
  u-boot/
sudo apt-get source libguestfs-tools
ls .
  libguestfs-1.46.2                 libguestfs_1.46.2-10ubuntu3.debian.tar.xz
  libguestfs_1.46.2-10ubuntu3.dsc   libguestfs_1.46.2.orig.tar.gz
  u-boot

But when dealing with WSL, be prepared for some debian packages to not “just work”, especially when they are things that would expect to be able to add kernel modules.

Additionally you may need or want these:

sudo apt-get install arm-trusted-firmware-tools bsdutils bsdextrautils \
  bzip2 clang gdb-multiarch gdbserver git \
  qemu qemu-system qemu-user qemu-utils xz-utils

The only one absolutely needed there is git, if it’s not already installed..

Clone u-boot Repo

git clone https://source.denx.de/u-boot/u-boot.git
git checkout v2025.07-rc5

Right now, I figure that v2025.07-rc5 ought to be very recent without being too unstable. (Usually rc5 or rc6 is the last rc before release, but the release for v2025.07 isn’t tagged yet.)

Set Env Variables

This is the main one that needs to be set:

> export CROSS_COMPILE=aarch64-linux-gnu-

(Notice the aarch64 this time.)

Or don’t set the environment variable and invoke make with it set like this every time when needed:

> CROSS_COMPILE=aarch64-linux-gnu- make [target_name]

Supplying this as a make variable also works:

> make CROSS_COMPILE=aarch64-linux-gnu- [target_name]

Optional things to set if you want to try using clang:

export HOSTCC=clang
export CC=clang

You can also use a specific output directory by adding O= as in O=/tmp/build/artifacts as an argument to the make command.

Use V=1 (or a larger number) for more verbose make output.

Also, run make help in the u-boot top level directory to get some text output about what targets are available and what they’re for.

Cleaning

  • make clean - removes most things but doesn’t delete .config
  • make mrproper - same as clean plus removes .config and some caches
  • make distclean - absolute maximum clean

Running some level of clean is important if you change any configuration. If only the .config is changed then regular clean should be good enough. If you change O= then mrproper will be needed.

Test Build

Try this first.

Use unset CROSS_COMPILE if set as an env var, because the sandbox tests just use native compilation. (Optionally add the O= or V= to the below if you want.)

> make mrproper
> make qtest
....

This will generate a .config using sandbox_defconfig from the configs/ directory, then run a bunch of tests.

Don’t be surprised if some fail though because you may still be lacking some dependencies, and this tries to test every feature used on any platform. So whatever failures happen may not even be relevant for your target hardware.

Build a Boot Firmware for Real Hardware (Pinebook Pro)

Before I start I really need to read the device-specific docs. Even with the docs it can be difficult to know what they’re actually talking about, because unfortunately whoever is usually writing this stuff seems to be writing it for an audience who already knows everything.

But enough complaining, here are the main ones for this Pinebook Pro:

The generic architecture doc isn’t too useful for me yet, but the second one has build instructions.

“Pinebook Pro” isn’t listed, but I know it’s using a rk3399 SoC and I know there’s a pinebook-pro-rk3399_defconfig in configs/ so whatever build instructions are helpful for rk3399 should be helpful to me.

TF-A (trusted-firmware-a)

There’s nothing in the above doc to tell you what this is or why you need it, but at least there are some build instructions, which is a big time saver.

From within my working base directory, which contains my u-boot directory:

git clone https://github.com/TrustedFirmware-A/trusted-firmware-a.git
cd trusted-firmware-a
make realclean
make CROSS_COMPILE=aarch64-linux-gnu- PLAT=rk3399
........build messages spew........
cd ..

And thankfully that works like a charn. I now have trusted-firmware-a/build/rk3399/release/bl31/bl31.elf which I will soon need.

What BL31.elf Actually Is

Simple answer: This is some stock ARM BSP (Board Support Package) firmware for the ARM IP (chip design) used by Rockchip on this particular model of chip (rk3399). The design won’t work without it, and it’s not worth rewriting a different version of it if the source code is available, so people just use this.

More complicated answer: This is a particularly critical part of the firmware that executes at a maximum “super supervisor” privilege level (EL3 or Exception Level 3, which is above the hypervisor level EL2) and acts as a firewall between what the documentation calls the “normal world” and the “secure world”.

Therefore it makes sense to use the smallest most solid computer program possible to control access to these sorts of functions and restrict them appropriately. Without a solid security mechanism, “rootkit” software could make itself invisible then do anything to anything by having the most unrestricted access to everything’s state.

The name “trusted” indicates that it occupies a priviledged position where it must be considered secure by the rest of the design. That doesn’t mean it’s automatically trustworthy, but if this is compromised then everything is. So it has to be trusted by design, and actually being trustworthy would be its main design objective.

To prevent tampering, ideally this especially (like the rest of the firmware) should be verified stating with a hardware root of trust. That is, it should only be loaded and executed by a SoC if it has a digital signature that should only be verifiable if the firmware image has been approved by the owner, manufacturer, etc., as indicated by a one-time-programmable CA certificate in the SoC.

But the Pinebook Pro doesn’t come with these features enabled/configured, and I have read that this particular Rockchip design (rk3399) might not be able to verify firmware signatures properly or something. (It’s one of the things I want to get to the bottom of eventually.)

Anyway, more information on the BL31 functions are here:

So, unlike BL1 (which is permanently burned into the chip) any firmware needs to include something for the BL31 functions and it might as well be the official firmware (which you can build yourself to help ensure that it isn’t “rootkitted”).

make the Config (.config)

First everything needs to be clean, then make something_defconfig where something_defconfig is one of the device default config files in configs/.

> make distclean
....
> make -j8 pinebook-pro-rk3399_defconfig
... blabla make output blabla....
#
# configuration written to .config
#

(-j8 is optional and means parallel processing, with 8 processes.)

In my case, the defconfig file is pinebook-pro-rk3399_defconfig.

That will create .config in the current directory (u-boot top level) based on the default configuration for the Pinebook Pro.

Tamper With, uh, I mean Improve, the Config

If you ran make help you might have noticed this.

> cd u-boot/
> make help
...
Configuration targets:
  config          - Update current config utilising a line-oriented program
  nconfig         - Update current config utilising a ncurses menu based program
  menuconfig      - Update current config utilising a menu based program
  xconfig         - Update current config utilising a Qt based front-end
  gconfig         - Update current config utilising a GTK+ based front-end
.....blabla...

These launch different editors for the .config file to change the options from the defaults that were copied from the _defconfig file for your device.

In my opinion, the best one to use is nconfig because it provides easy access to all the options. menuconfig is essentially just a color version of nconfig with less convenient keymappings, but it’s almost the same.

I don’t want to bother to install the dependencies needed to compile the GUI equivalents, so I’m ignoring those. config is very inconvenient because it will just ask you to confirm pretty much all settings, one after the other, and there are zillions of options.

So unless xconfig or gconfig just instantly work in your environment, pretty much every option besides nconfig or menuconfig is a waste of time.

Some usage notes:

nconfig

Use F2 to get some explanation of a the currently selected setting, hit F6 to save the config, and you can search for settings with F8. Esc also works as F5 “Back”.

Use ? for the same function as F2, and Esc does not work as “Back”.

What to Not Change

Two main bits of advice:

  • A bunch of settings are characteristics of the target device that can’t be changed unless you want to see it lock up or never do anything to begin with. These are usually pretty obvious like “Enable ARM Architecture” and “Support Rockchip RK3399” for example, but some are more obscure. Try to use some common sense.

  • Some settings might normally be perfectly fine to enable, but the feature is broken for your particular device, or there’s a dependent feature that doesn’t automatically get enabled and you’re not sure what it is. So you can end up with some seemingly valid configurations that prevent the firmware from building, or that just lock up after boothing. For this reason it’s not a good idea to change too many things before testing, otherwise it may be difficult to figure out what caused a problem.

  • If there’s a build error, check any related settings that you changed to see if it’s caused by that and a missing dependency.

So just keep in mind that not all combinations of all options are guaranteed to work even when the options seem fine. Not every feature is “there yet” on every platform.

Don’t expect any of the config to be idiot-proof.

Use U-Boot TPL or Vendor “TPL”?

Just “one more thing” for the Nth time…

There are still two environment variables I need to set for the Pinebook Pro before starting the build.

One is optional if you don’t mind “suspend to RAM” (sleep) not working properly on the Pinebook Pro. But if you do want this working:

cd ..
git clone https://github.com/rockchip-linux/rkbin
cd u-boot

This will clone the closed-source firmware driver binaries from Rockchip, one of which is rkbin/bin/rk33/px30_ddr_333MHz_v1.16.bin.

To use this as part of the image at the end of the build, we need to set ROCKCHIP_TPL to the correct file.

To explain this and the boot process a little better, these are the different firmware images that get executed at boot:

  1. BROM (BL1) - Boot ROM, can’t be changed, burned into the chip, is the first thing to start executing. This initializes the SoC (System on a Chip), (or at least things that need immediate setup like clocks, etc).
  2. TPL (BL2) - Tertiary Program Loader (u-boot terminology) - this initializes DRAM and returns control to the BROM. Not much else happens here. (u-boot can be configured to try adding a few extra things here when it builds this part, but not much makes sense to add.)
  3. SPL (BL2) - Secondary Program Loader (u-boot term) - Loaded and executed by BROM after the TPL returns. Now that RAM should all be working, this loads up the remaining firmware, that being TF-A and u-boot, then executes TF-A. (In some cases this can contain extra stuff beyond the norm.)
  4. TF-A (BL31) - The Trusted Firmware A for the BL31 phase (3.1) which runs at EL3 (Exception Level 3, highest privilege). Once it’s done initializing it drops the EL to EL2 or EL1 and then executes u-boot. It then stays resident, and remains dormant until something asks it to do something requiring EL3 super powers.
  5. u-boot (BL33) - Finally, after all that rigamarole, u-boot gets to start at EL2 or EL1. Only here do we actually get to what you think of as a normal OS boot loader. Everything else is really Firmware (what’s contained in the “BIOS” flash chip on a regular PC.) Once done with it’s stuff, it drops to EL1 and loads an OS kernel to execute at EL1. (EL2 is the hypervisor level for virtualization, so this could be used if virtualization is in play.)

So basically the vendor image px30_ddr_333MHz_v1.16.bin is a complete replacement for what u-boot would ordinary supply as “TPL”.

So even though I’d like source code for this, I think I’ll just use the vendor’s TPL for now and see how it works for “sleep mode”. At least I was able to build the TP-A image from source.

cd u-boot
export ROCKCHIP_TPL=../rkbin/bin/rk33/px30_ddr_333MHz_v1.16.bin
export BL31=../trusted-firmware-a/build/rk3399/release/bl31/bl31.elf

Finally… Run the Build

After all that trouble, I’m ready to actually build the whole image.

make CROSS_COMPILE=aarch64-linux-gnu- -j8

Woohoo! Success! Worked on the first try. Probably only worked smoothly because I’m going through the process slowly enough to write all of this up.

Pick Through The Artifacts

The successful build process left us with lots of stuff, including:

> ls -l *.img *.bin *.itb
-rw-r--r-- 1 rjstone rjstone  397312 Jun 30 02:44 idbloader-spi.img
-rw-r--r-- 1 rjstone rjstone  198656 Jun 30 02:44 idbloader.img
-rw-r--r-- 1 rjstone rjstone 1230697 Jun 30 02:44 simple-bin-spi.fit.itb
-rw-r--r-- 1 rjstone rjstone 1230730 Jun 30 02:44 simple-bin.fit.itb
-rw-r--r-- 1 rjstone rjstone 1068616 Jun 30 02:44 u-boot-dtb.bin
-rw-r--r-- 1 rjstone rjstone 1069612 Jun 30 02:44 u-boot-dtb.img
-rwxr-xr-x 1 rjstone rjstone  970856 Jun 30 02:44 u-boot-nodtb.bin
-rw-r--r-- 1 rjstone rjstone 2149888 Jun 30 02:44 u-boot-rockchip-spi.bin
-rw-r--r-- 1 rjstone rjstone 9588224 Jun 30 02:44 u-boot-rockchip.bin
-rw-r--r-- 1 rjstone rjstone 1068616 Jun 30 02:44 u-boot.bin
-rw-r--r-- 1 rjstone rjstone 1069612 Jun 30 02:44 u-boot.img
-rw-r--r-- 1 rjstone rjstone 1232384 Jun 30 02:44 u-boot.itb

I know I’m supposed to be able to flash idbloader.img and u-boot.bin, or just u-boot-rockchip.bin which combines the two, but what is all this other stuff? Tune in for a future episode this season where I will figure that out.

Flashing/Writing The Boot Loader

Warning: Don’t just assume the layout of a firmware/boot loader image you found and just write it using the offsets shown below, even if the filenames look similar. Similar images that come with different OS distributions may contain other stuff, like disk partition information, intended to be written from offset 0 because it includes a MBR and partition table. Make sure you know what a file contains and what offset it should be written at before writing it. Otherwise you end up with non-working binary at the entry points.

Block-Oriented Boot Devices and Images

If we use the single image file then we need to write it to the “disk” image or device like this:

sudo dd if=u-boot-rockchip.bin of=<file or /dev device> seek=64
sync

You may also want conv=fsync,notrunc if writing to a file.

The seek=64 will seek forward 64 * 512B blocks in the output, or 32,768 bytes from the beginning.

If we need to write the seperate .img and .idb files, it should be done like this:

sudo dd if=/boot/idbloader.img conv=notrunc seek=64    of=<dev or disk img>
sudo dd if=/boot/u-boot.itb    conv=notrunc seek=16384 of=<dev or disk img>

The single image ends up putting these two at the same offsets when the whole thing is written starting from seek=64:

> hexdump  u-boot-rockchip.bin -s 0x30690  | head -10
0030690 0074 6976 2d6e 7573 7070 796c 0000 0000
00306a0 0000 0000 0000 0000 0000 0000 0000 0000
*
0030800 ffff ffff ffff ffff ffff ffff ffff ffff
*
07f8000 0dd0 edfe 0000 000c 0000 3800 0000 c80a
07f8010 0000 2800 0000 1100 0000 0200 0000 0000
07f8020 0000 c800 0000 900a 0000 0000 0000 0000

So idbloader.img (the first part) ends at about 0x3069A (in this particular build), then is filled with all 1’s (0xFF) until 0x7F800, then the u-boot.itb part starts. (All 1’s is the state of erased flash ram, and using that removes the need to write to a block after erasing it.)

So that just saves some trouble, but it’s the same thing in offset terms.

Writing to SPI Flash and “Not Disk” MTD Flash

Don’t Do It

At least on the Pinebook Pro with it’s hard-coded boot order.

The first reason why anyone who’s a “noob” to this kind of thing (including me) shouldn’t do this on a Pinebook Pro is that SPI has the highest boot priority, and if it’s programmed with something that looks like it can boot, but instead hangs, then the PBP is hard bricked and can’t be unbricked without disabling the SPI flash chip somehow. This normally requires a soldering iron, since there’s no switch for it like there is with the eMMC.

SDcards Are Convenient

When testing out bootloaders on actual hardware, for the Pinebook Pro the easiest option is to erase the SPI (if there’s anything there, it’s blank from the factory), erase the firmware areas of the internal eMMC, and just always boot from SDCard while you’re tinkering.

That’s because the SDcard can be easily removed and reflashed, but it’s less convenient to disble the eMMC (internal switch) and even more inconvenient to disable the SPI flash (need soldering iron).

How It’s Done, Approximately

I don’t really want to provide directions for doing this mostly because I haven’t done it myself, but unlike eMMC and sdcard devices, things like SPI flash chips are usually programmed with specialized utilities like Linux MTD Utilities.

The reason for this is that “MTD” flash devices are “raw” and “dumb” flash chips that don’t do any automatic mapping of blocks for wear balancing, or automatic detection and re/unmapping of bad blocks. Software has to do that when programming them.

SDcards, USB flash drives, etc, all contain controllers that do this kind of stuff to create a translation layer in hardware that looks like a reliable block device. Under the hood, physical blocks are being remapped all over the place internally to avoid burning out specific physical blocks when the same logical blocks keep getting overwritten.

So trying to just dd to one of these devices would be a bad idea. If nothing else, bad blocks wouldn’t be detected or dealt with.

Pinebook Pro SPI Info

Here are some links:

SPI Images

You will also notice that there are separate images built for SPI flash, like u-boot-rockchip-spi.bin, which you might also notice is much smaller than u-boot-rockchip.bin. That’s because it doesn’t have the same huge 0xFF hole in the middle as the offsets are not the same for booting from SPI flash.

There’s a bit of a hole in that one, but not in the same location or as large:

0060680 6f74 2d72 616d 2d78 696d 7263 766f 6c6f
0060690 0074 6976 2d6e 7573 7070 796c 0000 0000
00606a0 0000 0000 0000 0000 0000 0000 0000 0000
*
0061000 ffff ffff ffff ffff ffff ffff ffff ffff
*
00e0000 0dd0 edfe 0000 000c 0000 3800 0000 b00a
00e0010 0000 2800 0000 1100 0000 0200 0000 0000
00e0020 0000 bf00 0000 780a 0000 0000 0000 0000

Finishing Up

That’s it for this episode.

In the next episode, I’ll go over booting u-boot in qemu (though it won’t be exactly the same pinebook pro rockchip image).