Preparing and running QEMU ARM for Cubieboard (Part 1)
- March 24, 2024
This is part 1 of QEMU Board Emulation series
In this post I will cover the following things
At the end of this post we will have a working QEMU, and starting point for U-Boot and Linux kernel which can be used for further work. A simple init ramdsik will be used instead of root filesystem, so the Linux kernel does not panic on boot. In the next post a real root filesystem will be used.
The Cubieboard will be used as the target board.
Before we start with the development, prerequisites need to be installed. I am using Ubuntu 22.04 as the host system, so if different system is used it is possible that some additional packages from prerequisites need to be installed.
# Installing prerequisites
sudo apt -y install git libglib2.0-dev libfdt-dev libpixman-1-dev \
zlib1g-dev libnfs-dev libiscsi-dev git-email libaio-dev \
libbluetooth-dev libbrlapi-dev libbz2-dev libcap-dev \
libcap-ng-dev libcurl4-gnutls-dev libgtk-3-dev libibverbs-dev \
libjpeg8-dev libncurses5-dev libnuma-dev librbd-dev \
librdmacm-dev libsasl2-dev libsdl2-dev libseccomp-dev \
libsnappy-dev libssh2-1-dev libvde-dev libvdeplug-dev \
libxen-dev liblzo2-dev valgrind xfslibs-dev kpartx libssl-dev \
net-tools python3-sphinx python3-sphinx-rtd-theme libsdl2-image-dev \
flex bison libgmp3-dev libmpc-dev device-tree-compiler u-boot-tools \
bc git libncurses5-dev lzop make tftpd-hpa uml-utilities \
nfs-kernel-server swig ninja-build libusb-1.0-0-dev python3-venv \
python3-setuptools python3-dev fdisk
The development will be done in the $HOME/work
directory, and that directory will be refered to as $PROJ_DIR
.
Building QEMU
The latest stable version at the time of writing is 8.2.2.
Downloading QEMU source code
The QEMU emulator source code can be obtained as an archive from here or as a git repository from github.
If archive is used, then QEMU can be downloaded and prepared using
# archive
wget -c https://download.qemu.org/qemu-8.2.2.tar.xz
tar xf qemu-8.2.2.tar.xz && mv qemu-8.2.2 qemu
cd qemu
If github is used, then QEMU can be downloaded and prepared using
# github
git clone https://github.com/qemu/qemu.git
cd qemu
git checkout v8.2.2 -b devel
git submodule init
git submodule update --recursive
Configuring and building QEMU
Before building QEMU, it needs to be configured. In this process various options can be selected. For these posts following configuration command will be used
# configuration
mkdir -p build && cd build
../configure --target-list=arm-softmmu \
--enable-sdl \
--enable-tools \
--enable-fdt \
--enable-libnfs
SDL is selected as GUI backend, QEMU tools for handling image and network will be compiled, device tree support and NFS support will also be included.
After the configuration step is done, code can be compiled using
# building
make -j
The output files will be in ./arm-softmmu
directory, where the most import one is qemu-system-arm
. The tools, like
qemu-img
, will be in the ./
directory.
In order to keep all of the details in one place, all relevant paths and exports will be saved to a file called env.sh
in the root of the project.
So, after compiling the QEMU the $PROJ_DIR/env.sh
should look like
# Environment file
PROJ_DIR=$HOME/work
export PATH=$PROJ_DIR/qemu/build/arm-softmmu:$PROJ_DIR/qemu/build:$PATH
Running QEMU
QEMU has many options which can be selected at runtime. Some of them are shown in the following table
switch | description | example value |
---|---|---|
-M |
select machine that will be emulated | cubieboard , sabrelite |
-m |
set amount of RAM memory | 512M , 1G |
-kernel |
executable file that will be loaded | u-boot or kernel ELF file |
-drive |
specify storage drive to be used | file=sd.img,format=raw,if=sd |
-device |
specify device to be allocated | loader,file=zImage,addr=0x62000000,force-raw |
-net |
ethernet network | nic , tap,ifname=tap0,script=no |
-nographic |
disable display window | N/A |
-serial |
set how serial interface is connected | stdio , pty |
The list of all switches can be obtained with
qemu-system-arm --help
and available values for a specific switch using
qemu-system-arm <switch> ?
Before using QEMU we need an executable file to run, so we will proceed to obtaining toolchain and building U-Boot.
Getting toolchain
The cross-compilation toolchain for ARM architecture can obtained in various ways: get a prebuilt from ARM/Linaro or Bootlin, or build a custom toolchain using crosstool-NG.
In this post we will be using a prebuilt toolchain from ARM. It can be downloaded using
# Download toolchain
wget -c https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x86_64-arm-none-linux-gnueabihf.tar.xz
mkdir toolchain
tar xf arm-gnu-toolchain-13.2.rel1-x86_64-arm-none-linux-gnueabihf.tar.xz -C toolchain --strip-components=1
Update the $PROJ_DIR/env.sh
file so it looks like
# Environment file
PROJ_DIR=$HOME/work
export PATH=$PROJ_DIR/toolchain/bin:$PROJ_DIR/qemu/build/arm-softmmu:$PROJ_DIR/qemu/build:$PATH
This way, before cross-compiling any part of the code it is enough just to source the $PROJ_DIR/env.sh
script.
Building U-Boot
Even though QEMU can be used without the bootloader, where Linux kernel image, device tree blob and kernel command line are passed, in this blog series the goal is to emulate also the boot process. We will use U-Boot as bootloader, but Barebox can also be used.
Downloading U-Boot source code
As with QEMU, the source code can be obtained in the form of an archive or from a github repository.
If archive is used, then U-Boot source code can be downloaded and prepared using
# archive
wget -c https://ftp.denx.de/pub/u-boot/u-boot-2024.01.tar.bz2
tar xf u-boot-2024.01.tar.bz2 && mv u-boot-2024.01 u-boot
cd u-boot
If github is used, then U-Boot source code can be downloaded and prepared using
# github
git clone https://github.com/u-boot/u-boot.git
cd u-boot
git checkout v2024.01 -b devel
Configuring and building U-Boot
Before compiling U-Boot the environment script that was created needs to be sourced in order to add toolchain
executables to the $PATH
# sourcing environment script
source $PROJ_DIR/env.sh
The configuration for Cubieboard is done using the following command
# Configure U-Boot
make CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_cubieboard Cubieboard_defconfig
If additional adjustment needs to be made, it can be done using the menuconfig
command as
# Configure U-Boot
make CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_cubieboard menuconfig
In our case, since we are using just raw U-Boot, we need to embed the device tree file with the U-Boot image. The device tree file holds hardware description information, so without it U-Boot will not start.
In order to do it, CONFIG_OF_EMBED
setting needs to be applied
Device Tree Control -> Provider of DTB for DT control -> Embedded DTB for DT control
Once configuration is done, the build is started using
# Build U-Boot
make CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_cubieboard -j
After the build is completed, in the $PROJ_DIR/u-boot/build_cubieboard
directory there will be a file called u-boot
which will be run inside QEMU. For simpler handling, the path to this file can be added into the environment file, so
next time it is sourced we will be able to access u-boot
executable from anywhere.
After exporting this path the $PROJ_DIR/env.sh
should look like
# Environment file
PROJ_DIR=$HOME/work
export PATH=$PROJ_DIR/toolchain/bin:$PROJ_DIR/qemu/build/arm-softmmu:$PROJ_DIR/qemu/build:$PATH
export UBOOT=$PROJ_DIR/u-boot/build_cubieboard/u-boot
Running U-Boot inside QEMU
In order to run U-Boot in QEMU the u-boot
ELF file needs to be passed with the -kernel
switch. Since at this moment
only U-Boot is ready, we will be able to enter U-Boot and look around the provided console interface.
The command that can be used to run U-Boot inside QEMU is (do not forget to source the $PROJ_DIR/env.sh
file
beforehand)
# Run U-Boot in QEMU
qemu-system-arm -M cubieboard -kernel $UBOOT -serial mon:stdio
U-Boot 2024.01 (Mar 23 2024 - 20:40:51 +0100) Allwinner Technology
CPU: Allwinner A10 (SUN4I)
Model: Cubietech Cubieboard
DRAM: 1 GiB
Core: 74 devices, 24 uclasses, devicetree: embed
WDT: Not starting watchdog@1c20c90
MMC: mmc@1c0f000: 0
Loading Environment from FAT... Card did not respond to voltage select! : -110
** Bad device specification mmc 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 0.00
Bus usb@1c14400: USB OHCI 0.0
Bus usb@1c1c000: USB EHCI 0.00
Bus usb@1c1c400: USB OHCI 0.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... 1 USB Device(s) found
scanning bus usb@1c1c400 for devices... 1 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
After testing, exit QEMU with Ctrl+a
,x
.
Building Linux kernel
Latest stable kernel at the time of writing is 6.8.1
Downloading source code
The code can be obtained from git server or from an archive.
If archive is used, then Linux source code can be downloaded and prepared using
# archive
wget -c https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.8.1.tar.xz
tar xf linux-6.8.1.tar.xz && mv linux-6.8.1 linux
cd linux
If git server is used, then Linux source code can be downloaded and prepared using
# git server
git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
cd linux
git checkout v6.8.1 -b devel
Configuring and building
Before compiling Linux, the environment script that was created needs to be sourced in order to add toolchain
executables to the $PATH
# sourcing environment script
source $PROJ_DIR/env.sh
The configuration for Cubieboard is done using the following command
# Configure Linux kernel - multi_v7
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_cubieboard multi_v7_defconfig
The multi_v7_defconfig
is a universal configuration for many ARMv7 based boards, where actual configuration is done
based on the Device Tree.
If additional adjustment needs to be made, it can be done using the menuconfig
command as
# Configure Linux - manual
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_cubieboard menuconfig
Once configuration is done, the build is started using
# Build Linux kernel
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_cubieboard -j
After the build is completed, two files will be needed for running in the QEMU:
- file
zImage
in the$PROJ_DIR/linux/build_cubieboard/arch/arm/boot
directory - compressed Linux kernel image file - file
sun4i-a10-cubieboard.dtb
in the$PROJ_DIR/linux/build_cubieboard/arch/arm/boot/dts/allwinner
directory - compiled device tree file with hardware description used by Linux kernel to set up hardware.
For simpler handling, the path to these file can be added into the environment file. After exporting these paths the
$PROJ_DIR/env.sh
should look like
# Environment file
PROJ_DIR=/home/user/qemu_devel
export PATH=$PROJ_DIR/toolchain/bin:$PROJ_DIR/qemu/build/arm-softmmu:$PROJ_DIR/qemu/build:$PATH
export UBOOT=$PROJ_DIR/u-boot/build_cubieboard/u-boot
export ZIMAGE=$PROJ_DIR/linux/build_cubieboard/arch/arm/boot/zImage
export DTB=$PROJ_DIR/linux/build_cubieboard/arch/arm/boot/dts/allwinner/sun4i-a10-cubieboard.dtb
Running U-Boot and Linux inside QEMU
The idea is to use U-Boot to start the Linux kernel, the same way it would have been done on the real board. Since we will not handle flash, SD card interface or network intferace in this post, we will use the ‘loader’ feature of the QEMU to place the Linux kernel image and Device Tree file at the appropriate addresses in memory, as if the U-Boot code had already copied them from some of the possible bootable locations. In some of the other posts other methods will be covered.
The command that can be used to run U-Boot inside QEMU, with loading Linux kernel image and device tree blob at the
appropriate addresses is (do not forget to source the $PROJ_DIR/env.sh
file beforehand)
# Run U-Boot and Linux kernel in QEMU
qemu-system-arm -M cubieboard -m 1G -kernel $UBOOT -nographic \
-device loader,file=$ZIMAGE,addr=0x42000000,force-raw=on \
-device loader,file=$DTB,addr=0x43000000,force-raw=on
U-Boot 2024.01 (Mar 23 2024 - 20:40:51 +0100) Allwinner Technology
CPU: Allwinner A10 (SUN4I)
Model: Cubietech Cubieboard
DRAM: 1 GiB
Core: 74 devices, 24 uclasses, devicetree: embed
WDT: Not starting watchdog@1c20c90
MMC: mmc@1c0f000: 0
Loading Environment from FAT... Card did not respond to voltage select! : -110
** Bad device specification mmc 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 0.00
Bus usb@1c14400: USB OHCI 0.0
Bus usb@1c1c000: USB EHCI 0.00
Bus usb@1c1c400: USB OHCI 0.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... 1 USB Device(s) found
scanning bus usb@1c1c400 for devices... 1 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
U-Boot needs to be stopped and then following commands need to be entered so Linux kernel is started with the device tree.
# Run Linux kernel from U-Boot
bootz 42000000 - 43000000
Kernel image @ 0x42000000 [ 0x000000 - 0xa7b200 ]
## Flattened Device Tree blob at 43000000
Booting using the fdt blob at 0x43000000
Working FDT set to 43000000
Loading Device Tree to 49ff7000, end 49fffb4d ... OK
Working FDT set to 49ff7000
DE is present but not probed
Starting kernel ...
[ 0.000000] Booting Linux on physical CPU 0x0
[ 0.000000] Linux version 6.8.1 (straxy@DESKTOP-CTRN0PE) (arm-none-linux-gnueabihf-gcc (Arm GNU Toolchain 13.2.rel1 (Build arm-13.7)) 13.2.1 20231009, GNU ld (Arm GNU Toolchain 13.2.rel1 (Build arm-13.7)) 2.41.0.20231009) #1 SMP Sat Mar 23 21:21:00 CET 2024
[snip]
[ 1.709098] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
The first command sets boot arguments that are passed to the Linux kernel. In this case, the only thing that needs to be
configured is the device that is used for serial console, and that is the ttyAMA0
, or the UART0 port.
The second command start the boot of the Linux kernel, where parameters that are passed are address of zImage
kernel
image, address of init ramdisk (in this case -
since it is not used) and the address where device tree blob is placed.
After executing these commands, the Linux kernel will panic since there is no root filesystem, which is expected.
Simple “ramdisk”
What is actually a root filesystem? It is a set of files and directories organized in a certain way. Those files and directories can be on a physical medium (HDD, SD card, eMMC, Flash memory), a remote location (NFS boot), but also can be executed from RAM in case the init ramdisk/ramfs is used.
In all cases, the kernel is looking for an ‘init’ file which is the first one that is executed. So, by supplying an ‘init’ file we can give the kernel a reason not to panic.
The simplest way to create this ‘init’ file, without building a full-blown root filesystem, is to create a ‘Hello, world!’ application and link it statically. The application should write directly to registers (we will use UART peripheral so we can get some messages) and will be used only for the demonstration.
Hello, world
A classic ‘Hello, world!’ C application can be used:
/* hello.c */
#include <stdio.h>
void main()
{
printf("Hello, world!\n");
while(1);
}
The code can be compiled into a static binary using:
# Compile 'Hello, world!'
arm-none-linux-gnueabihf-gcc -static hello.c -o hello
The init ramdisk that can be used with U-Boot can be created using:
# Create ramdisk
echo hello | cpio -o -H newc > initrd
gzip initrd
mkimage -A arm -O linux -T ramdisk -d initrd.gz uRamdisk
The command that can be used to run U-Boot inside QEMU, with loading Linux kernel image, device tree blob and uRamdisk
at the appropriate addresses is (do not forget to source the $PROJ_DIR/env.sh
file beforehand)
# Run U-Boot and Linux kernel in QEMU with uRamdisk
qemu-system-arm -M cubieboard -m 1G -kernel $UBOOT -nographic \
-device loader,file=$ZIMAGE,addr=0x42000000,force-raw=on \
-device loader,file=$DTB,addr=0x43000000,force-raw=on \
-device loader,file=uRamdisk,addr=0x43400000,force-raw=on
U-Boot 2024.01 (Mar 23 2024 - 20:40:51 +0100) Allwinner Technology
CPU: Allwinner A10 (SUN4I)
Model: Cubietech Cubieboard
DRAM: 1 GiB
Core: 74 devices, 24 uclasses, devicetree: embed
WDT: Not starting watchdog@1c20c90
MMC: mmc@1c0f000: 0
Loading Environment from FAT... Card did not respond to voltage select! : -110
** Bad device specification mmc 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 0.00
Bus usb@1c14400: USB OHCI 0.0
Bus usb@1c1c000: USB EHCI 0.00
Bus usb@1c1c400: USB OHCI 0.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... 1 USB Device(s) found
scanning bus usb@1c1c400 for devices... 1 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
U-Boot needs to be stopped and then following commands need to be entered so Linux kernel is started with the device tree and uRamdisk.
# Run Linux kernel from U-Boot with ramdisk
setenv bootargs "root=/dev/ram rdinit=/hello console=$console"
bootz 42000000 43400000 43000000
Kernel image @ 0x42000000 [ 0x000000 - 0xa7b200 ]
## Loading init Ramdisk from Legacy Image at 43400000 ...
Image Name:
Image Type: ARM Linux RAMDisk Image (gzip compressed)
Data Size: 1109384 Bytes = 1.1 MiB
Load Address: 00000000
Entry Point: 00000000
Verifying Checksum ... OK
## Flattened Device Tree blob at 43000000
Booting using the fdt blob at 0x43000000
Working FDT set to 43000000
Loading Ramdisk to 49ef1000, end 49fffd88 ... OK
Loading Device Tree to 49ee8000, end 49ef0b4d ... OK
Working FDT set to 49ee8000
DE is present but not probed
Starting kernel ...
[ 0.000000] Booting Linux on physical CPU 0x0
[ 0.000000] Linux version 6.8.1 (straxy@DESKTOP-CTRN0PE) (arm-none-linux-gnueabihf-gcc (Arm GNU Toolchain 13.2.rel1 (Build arm-13.7)) 13.2.1 20231009, GNU ld (Arm GNU Toolchain 13.2.rel1 (Build arm-13.7)) 2.41.0.20231009) #1 SMP Sat Mar 23 21:21:00 CET 2024
[snip]
[ 1.737288] Run /hello as init process
Hello, world!
In kernel arguments there are now two additional
root=/dev/ram
, indicating that root filesystem will be in RAM memory, where init ramdisk is extracted,rdinit=/hello
, overriding the defaultinit
program that is used from ramdisk so ourhello
is used.
Once kernel boots, it will run the hello
application and print "Hello, world!"
.
Summary
In this post the basic steps building U-Boot and Linux kernel were covered. This is still far from the actual use-case for an embedded Linux system, since root filesystem is missing.
The root filesystem will be covered in the next post, together with steps for running bootloader and Linux kernel from different mediums, which will be similar to the way and embedded Linux is used on real development boards.