Jan 25, 2022

Running Vexpress board under QEMU with Ubuntu root filesystem

This is part 2 of the QEMU Board Emulation post series.

In the previous post the basic steps for obtaining and compiling QEMU, U-Boot and Linux were presented. The only part that was missing for the complete system setup was the root filesystem. Also, all of the images were injected directly into emulated RAM memory using the QEMU's loader mechanism.

In this post I will cover the following things

Obtaining Ubuntu root filesystem

There are several ways to obtain root filesystem for an embedded system

In this post we will use the prebuilt Ubuntu root filesystem. In some of the future posts the Buildroot and Yocto approaches will be covered.

Download Ubuntu root filesystem

The archive with the Ubuntu minimal 20.04 root filesystem can be obtained using the following step

# Prepare Ubuntu
$ wget -c https://rcn-ee.net/rootfs/eewiki/minfs/ubuntu-20.04.3-minimal-armhf-2021-12-20.tar.xz

Now that we have the Ubuntu root filesystem, it needs to be supplied to the Linux running under QEMU. Two options are to create a SD card image or to access it over network, and both will be covered in the following sections.

Booting and running from SD card

QEMU supports emulation of the SD card interface. Depending on the board that is emulated, different SD card interfaces are available.

Preparing SD card image

QEMU provides tool for creating the emulated SD card, qemu-img. Before the tool can be used, the environment script created in the previous post needs to be sourced.

Creating an empty SD card image

The SD card image can be created using the following command:

# Create empty SD card
$ cd $PROJ_DIR
$ qemu-img create sd.img 4G
Formatting 'sd.img', fmt=raw size=4294967296

The last parameter that is passed is the size and for this work size of 4GB is selected. After executing the previous command, file sd.img will be created.

Before the SD card can be used to copy data, it has to be partitioned and formatted.

In order to simplify further work, a new line can be added to the $PROJ_DIR/env.sh with the path to the SD card

# env.sh update
export SD_IMG=$PROJ_DIR/sd.img

Partitioning SD card image

The SD card will be partitioned into two partitions.

The first one will be used for the kernel image and device tree files. The size will be 64MB and if will be later formatted as FAT32.

The second partition will take up the rest of the SD card and it will later be formatted as ext4.

We can use fdisk to check the status before and after partitioning:

$ fdisk -l ./sd.img
Disk ./sd.img: 4 GiB, 4294967296 bytes, 8388608 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

For formatting the SD card image we will use the sfdisk application.

Partitioning is done using the following command

# Partitioning the SD card
$ sfdisk ./sd.img << EOF
,64M,c,*
,,L,
EOF

Disk ./sd.img: 4 GiB, 4294967296 bytes, 8388608 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

>>> Created a new DOS disklabel with disk identifier 0x87facb0e.
./sd.img1: Created a new partition 1 of type 'W95 FAT32 (LBA)' and of size 64 MiB.
./sd.img2: Created a new partition 2 of type 'Linux' and of size 3,9 GiB.
./sd.img3: Done.

New situation:
Disklabel type: dos
Disk identifier: 0x87facb0e

Device     Boot  Start     End Sectors  Size Id Type
./sd.img1  *      2048  133119  131072   64M  c W95 FAT32 (LBA)
./sd.img2       133120 8388607 8255488  3,9G 83 Linux

The partition table has been altered.
Syncing disks.

The format of the sfdisk partitioning is start,size,type,bootable, meaning we are creating

  • first partition, from the beggining of the card, size 64MB, type c (FAT32) and bootable
  • second partition, that starts after the first partition, which will fill the available space, type L (ext4).

Formatting partitions

After the SD card has been partitioned, and before the partitions can be formated using the mkfs application, the SD card must be "plugged in", i.e. the partitions must be recognized by the host operating system. This is done using the kpartx tool

# 'Inserting' the SD card
$ sudo kpartx -av ./sd.img
add map loop12p1 (253:0): 0 131072 linear 7:12 2048
add map loop12p2 (253:1): 0 8255488 linear 7:12 133120

The value 12 in the output loop12p1 can differ from system to system and that is why we are using the -v switch, so the value is printed. After this command, the partitions are visible in the system under /dev/mapper/loop12p1 and /dev/mapper/loop12p2.

In order to format partitions, following commands will be used

# Formatting SD card partitions
$ sudo mkfs.vfat -F 32 -n "boot" /dev/mapper/loop12p1
$ sudo mkfs.ext4 -L rootfs /dev/mapper/loop12p2

Copying data to SD card image

After the partitions have been formatted, the data can be copied. In order to copy data, the partitions need to be mounted.

The /run/mount/ will be used as base for the mount points, where boot and rootfs directories will be created.

Boot partition

The boot partition is mounted in the following way

# Mounting boot partition
$ sudo mkdir -p /run/mount/boot
$ sudo mount /dev/mapper/loop12p1 /run/mount/boot

Linux kernel zImage file and Device tree file need to be copied to the boot partition

# Copying files to boot partition
$ sudo cp $ZIMAGE /run/mount/boot
$ sudo cp $DTB /run/mount/boot

After data has been copied, the boot partition can be unmounted using

# Umount
$ sudo umount /run/mount/boot

Root filesystem partition

The rootfs partition is mounted in the following way

# Mounting rootfs partition
$ sudo mkdir -p /run/mount/rootfs
$ sudo mount /dev/mapper/loop12p2 /run/mount/rootfs

Ubuntu rootfs needs to be unpacked and copied to the rootfs partition

# Copying Ubuntu files to boot partition
$ tar xf ubuntu-20.04.3-minimal-armhf-2021-12-20.tar.xz
$ sudo tar xfvp ./ubuntu-20.04.3-minimal-armhf-2021-12-20/armhf-rootfs-ubuntu-focal.tar -C /run/mount/rootfs/

Also, kernel modules must be installed

# Copying kernel modules and setting permissions
$ cd $PROJ_DIR/linux/build_vexpress
$ sudo make ARCH=arm INSTALL_MOD_PATH=/run/mount/rootfs modules_install
$ sync

After data has been copied, the rootfs partition can be unmounted using

# Umount
$ sudo umount /run/mount/rootfs

Now the SD card can be "unplugged" from the system using

# Unplug SD card
$ sudo kpartx -d $PROJ_DIR/sd.img

Running QEMU with SD card image

After the SD card is ready, the QEMU can be started using the following command

# Run QEMU with SD card
$ cd $PROJ_DIR
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel $UBOOT -nographic \
                  -drive file=sd.img,format=raw,if=sd

Once U-Boot starts, it can be used to copy kernel image and device tree file into RAM memory, as well as to set up the linux kernel command line

# Load items into memory and start kernel
u-boot> fatload mmc 0:1 0x62000000 zImage
u-boot> fatload mmc 0:1 0x68000000 vexpress-v2p-ca9.dtb
u-boot> setenv bootargs "console=ttyAMA0 root=/dev/mmcblk0p2 rw"
u-boot> bootz 0x62000000 - 0x68000000

After kernel boots the login prompt appears. User name is ubuntu, password temppwd

# Logged in Ubuntu
...
Ubuntu 20.04 LTS arm ttyAMA0

default username:password is [ubuntu:temppwd]

arm login:

Using TFTP to boot and running from NFS root filesystem

If the board has a network connection, then kernel files can be loaded from a remote location. Also, root filesystem can be accessed from a remote location.

QEMU emulates ethernet access, so it can be used for emulating the network boot.

TFTP server

TFTP (Trivial File Transfer Protocol) is a protocol which allows files to be obtained from a remote server. In this case, we will use it to obtain the compressed kernel image and device tree blob. U-Boot has an integrated TFTP client which will be used to load those files into RAM memory.

Set up TFTP server

In order to set up the TFTP server, it needs to be installed using the following command

# Install TFTP server
$ sudo apt install tftpd-hpa

Previous command will set up location /srv/tftp as the location from where the files can be downloaded remotely, so the zImage and vexpress-v2p-ca9.dtb files need to be copied into that directory.

# Copy files to the TFTP server.
$ sudo cp $ZIMAGE $DTB /srv/tftp/

NFS root filesystem

NFS (Network File System) is a file system that can be accessed over network as if it were physically present. This can be very useful during application development, since application binary files can be copied directly to a directory on host system, and they will be available in the target system.

Set up NFS server

In order to set up the NFS server, following needs to be installed

# Install NFS server
$ sudo apt install nfs-kernel-server

The directory in the host system that will be available to the target system needs to be configured by adding a line in the /etc/exports file (if the file does not exist, it should be created).

# /etc/exports addition
/home/user/rootfs *(rw,sync,no_subtree_check,no_root_squash)

In this example, the directory where NFS rootfs is located in the host system is /home/user/rootfs. After the line has been added, the exports information should be updated using

# Reload exportfs information
$ sudo exportfs -rav

The root filesystem contents should be copied into the exported directory. We will be using the same Ubuntu root filesystem, so the archive can be extracted directly into the directory. Also, kernel modules must be installed and correct permissions must be set on the filesystem.

# Extract root filesystem and set permissions
$ sudo tar xfvp ./ubuntu-20.04.3-minimal-armhf-2021-12-20/armhf-rootfs-ubuntu-focal.tar -C /home/user/rootfs/
$ cd $PROJ_DIR/linux/build_vexpress
$ sudo make ARCH=arm INSTALL_MOD_PATH=/home/user/rootfs modules_install
$ sync

Running QEMU with TFTP and NFS server

Before QEMU can emulate the TFTP booting and NFS root filesystem, network must be configured. QEMU, by default, creates a network connection to host machine. However, that network has limitations where emulated system can access outside network, but it is not accessible from the host system.

Besides the default network connection, QEMU supports different methods for enabling network access where emulated system is accessible from host system:

  • using tap interface,
  • using Bridged adapter network.

Both methods require manual setup before QEMU is started. The tap interface method is simpler, but the Bridged adapter network can make the emulated system accessible from rest of the network also, not only the host system.

In this example, we will use the tap interface to enable network connection.

More details about QEMU networking support can be found here.

Enable network in QEMU

The tap interface can be configured in the following way

# Create tap interface
$ sudo tunctl -u $(whoami) -t qemu-tap0
$ sudo ifconfig qemu-tap0 192.168.123.1
$ sudo route add -net 192.168.123.0 netmask 255.255.255.0 dev qemu-tap0
$ sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"

With commands above the QEMU instance will be able to ping and access host computer, and other way around, but it will not be able to access internet. In order to enable internet access, following commands are required on the host (set <interface> to network interface that is used on host machine for accessing internet)

# Enable guest internet access
$ iptables -t nat -A POSTROUTING -o <interface> -j MASQUERADE
$ iptables -I FORWARD 1 -i qemu-tap0 -j ACCEPT
$ iptables -I FORWARD 1 -o qemu-tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT

NOTE: These commands need to be executed every time the host system is rebooted.

The commands for NAT networking were adapted from here. The approach with the bridged networking is a bit more complex and can be found here.

After the tap interface has been created the QEMU can be started.

Run QEMU with network

The QEMU with networking can be started in the following way:

# Start QEMU with networking
$ qemu-system-arm -M vexpress-a9 -m 1G \
                  -kernel $UBOOT -nographic \
                  -net nic -net tap,ifname=qemu-tap0,script=no

After the U-Boot is started, the TFTP protocol can be used to copy Linux kernel and Device tree files into RAM memory.

# Load Linux kernel image and Device Tree file to RAM
u-boot> setenv serverip 192.168.123.1
u-boot> setenv ipaddr 192.168.123.101
u-boot> tftp 62000000 zImage                                                                               
smc911x: MAC 52:54:00:12:34:56
smc911x: detected LAN9118 controller
smc911x: phy initialized
smc911x: MAC 52:54:00:12:34:56
Using smc911x-0 device
TFTP from server 192.168.123.1; our IP address is 192.168.123.101
Filename 'zImage'.
Load address: 0x62000000
Loading: #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         ###########
         9.4 MiB/s
done
Bytes transferred = 9695744 (93f200 hex)
smc911x: MAC 52:54:00:12:34:56
u-boot&gt; tftp 68000000 vexpress-v2p-ca9.dtb
smc911x: MAC 52:54:00:12:34:56
smc911x: detected LAN9118 controller
smc911x: phy initialized
smc911x: MAC 52:54:00:12:34:56
Using smc911x-0 device
TFTP from server 192.168.123.1; our IP address is 192.168.123.101
Filename 'vexpress-v2p-ca9.dtb'.
Load address: 0x68000000
Loading: #
         1.4 MiB/s
done
Bytes transferred = 14173 (375d hex)
smc911x: MAC 52:54:00:12:34:56

Before the system can be started, the rootfs parameters must be configured so NFS is used:

# Use NFS as rootfs
u-boot> setenv npath /home/user/rootfs
u-boot> setenv bootargs "console=ttyAMA0 root=/dev/nfs rw nfsroot=${serverip}:${npath},tcp,v3 ip=${ipaddr}"
u-boot> bootz 62000000 - 68000000

Once the system is started, it will use NFS as rootfs, which can be checked

# Check NFS rootfs
...
[    3.351463] VFS: Mounted root (nfs filesystem) on device 0:15.
...
$ df -h
Filesystem                          Size  Used Avail Use% Mounted on
192.168.123.1:/home/user/rootfs     234G   83G  139G  38% /
devtmpfs                            464M     0  464M   0% /dev
tmpfs                               497M     0  497M   0% /dev/shm
tmpfs                               100M  2.8M   97M   3% /run
tmpfs                               5.0M     0  5.0M   0% /run/lock
tmpfs                               497M     0  497M   0% /sys/fs/cgroup
tmpfs                               100M     0  100M   0% /run/user/1000

The system can also be ping'ed from host

# Ping from host
host$ ping 192.168.123.101 -c 4
PING 192.168.123.101 (192.168.123.101) 56(84) bytes of data.
64 bytes from 192.168.123.101: icmp_seq=1 ttl=64 time=0.887 ms
64 bytes from 192.168.123.101: icmp_seq=2 ttl=64 time=0.726 ms
64 bytes from 192.168.123.101: icmp_seq=3 ttl=64 time=0.735 ms
64 bytes from 192.168.123.101: icmp_seq=4 ttl=64 time=0.743 ms

--- 192.168.123.101 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3074ms
rtt min/avg/max/mdev = 0.726/0.772/0.887/0.066 ms

Enable internet access in QEMU with NAT and tap

In order to enable internet access, two things need to be done in QEMU guest: set default gateway and update DNS server.

Setting default gateway can be done in the following way

# Set default gateway
$ sudo route add default gw 192.168.123.1

In order to set the DNS server, the /etc/resolv.conf file needs to be modified by adding the following line (use google server)

# Set DNS server
nameserver 8.8.8.8

After these changes are made, QEMU guest can access external network which can be simply verified

# Ping www.google.com
$ ping www.google.com -c 4
PING www.google.com (142.250.74.36) 56(84) bytes of data.
64 bytes from arn09s22-in-f4.1e100.net (142.250.74.36): icmp_seq=1 ttl=53 time=9.57 ms
64 bytes from arn09s22-in-f4.1e100.net (142.250.74.36): icmp_seq=2 ttl=53 time=6.89 ms
64 bytes from arn09s22-in-f4.1e100.net (142.250.74.36): icmp_seq=3 ttl=53 time=6.95 ms
64 bytes from arn09s22-in-f4.1e100.net (142.250.74.36): icmp_seq=4 ttl=53 time=8.15 ms

--- www.google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3007ms
rtt min/avg/max/mdev = 6.888/7.889/9.569/1.092 ms

Github helper scripts

In this repository I have added several scripts which cover most of the things presented in Parts 1 and 2.

Following scripts are present:

  • install-qemu.bash - downloads toolchain and Ubuntu root filesystem, and compiles QEMU, U-Boot and Linux
  • prepare-qemu.bash - creates an SD card image based on compiled files and Ubuntu root filesystem
  • enable-networking.bash - initializes a tap network interface so QEMU instance can have networking
  • mount-sd-card.bash - mounts rootfs partition of the SD card
  • umount-sd-card.bash - umounts rootfs partition of the SD card
  • run-qemu.bash - runs QEMU instance

Summary

In this post we have covered different methods for running the complete Linux system with bootloader in QEMU. For now, we have only used Versatile Express V2P-CA9 board.

In the following posts we will explore different functionalities that are emulated by QEMU for the Versatile express board, and also look at NXP iMX6 SabreLite and OrangePi PC boards.

Oct 9, 2021

Preparing U-Boot and Linux kernel for QEMU Board emulation

This is part 1 of the QEMU Board Emulation post series.

In the previous post I presented some of the goals for doing this work with QEMU.

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 Versatile Express V2P-CA9 will be used as the target board.

Before we start with the development, prerequisites need to be installed. I am using Ubuntu 20.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 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

The development will be done in the /home/user/qemu_devel directory, and that directory will be refered to as $PROJ_DIR.


Building QEMU

The latest stable version at the time of writing is 6.1.0.

Downloading 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-6.1.0.tar.xz
$ tar xf qemu-6.1.0.tar.xz && mv qemu-6.1.0 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 v6.1.0 -b devel
$ git submodule init
$ git submodule update --recursive

Configuring and building

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 bin/arm && cd bin/arm
$ ../../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 -j4

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/user/qemu_devel

export PATH=$PROJ_DIR/qemu/bin/arm/arm-softmmu:$PROJ_DIR/qemu/bin/arm:$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 vexpress-a9, 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-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf.tar.xz
$ tar xf gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf.tar.xz

Update the $PROJ_DIR/env.sh file so it looks like

# Environment file
PROJ_DIR=/home/user/qemu_devel

export PATH=$PROJ_DIR/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf/bin:$PROJ_DIR/qemu/bin/arm/arm-softmmu:$PROJ_DIR/qemu/bin/arm:$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 source code

As with QEMU, the source code can be obtained in the form of an archive or from a github repository.

NOTE: Version 2021.04 is the last one that supports this Vexpress board, so it is the version that is used.

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-2021.04.tar.bz2
$ tar xf u-boot-2021.04.tar.bz2 && mv u-boot-2021.04 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 v2021.04 -b devel

Configuring and building

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 Versatile Express V2P-CA9 board is done using the following command

# Configure U-Boot
$ make CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_vexpress vexpress_ca9x4_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_vexpress menuconfig

Once configuration is done, the build is started using

# Build U-Boot
$ make CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_vexpress -j4

After the build is completed, in the $PROJ_DIR/u-boot/build_vexpress 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. So after exporting this path the $PROJ_DIR/env.sh should look like

# Environment file
PROJ_DIR=/home/user/qemu_devel

export PATH=$PROJ_DIR/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf/bin:$PROJ_DIR/qemu/bin/arm/arm-softmmu:$PROJ_DIR/qemu/bin/arm:$PATH

export UBOOT=$PROJ_DIR/u-boot/build_vexpress/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 vexpress-a9 -m 1G -kernel $UBOOT -nographic
U-Boot 2021.04 (Aug 30 2021 - 01:45:32 +0200)

DRAM:  1 GiB
WARNING: Caches not enabled
Flash: 128 MiB
MMC:   MMC: 0
*** Warning - bad CRC, using default environment

In:    serial
Out:   serial
Err:   serial
Net:   smc911x-0
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 5.14.3

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/v5.x/linux-5.14.3.tar.xz
$ tar xf linux-5.14.3.tar.xz && mv linux-5.14.3 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 v5.14.3 -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 Versatile Express V2P-CA9 board is done using the following command

# Configure Linux kernel - multi_v7
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_vexpress 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_vexpress menuconfig

Once configuration is done, the build is started using

# Build Linux kernel
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_vexpress -j4

After the build is completed, two files will be needed for running in the QEMU:

  • file zImage in the $PROJ_DIR/linux/build_vexpress/arch/arm/boot directory - compressed Linux kernel image file
  • file vexpress-v2p-ca9.dtb in the $PROJ_DIR/linux/build_vexpress/arch/arm/boot/dts 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/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf/bin:$PROJ_DIR/qemu/bin/arm/arm-softmmu:$PROJ_DIR/qemu/bin/arm:$PATH

export UBOOT=$PROJ_DIR/u-boot/build_vexpress/u-boot

export ZIMAGE=$PROJ_DIR/linux/build_vexpress/arch/arm/boot/zImage

export DTB=$PROJ_DIR/linux/build_vexpress/arch/arm/boot/dts/vexpress-v2p-ca9.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 vexpress-a9 -m 1G -kernel $UBOOT -nographic \
                  -device loader,file=$ZIMAGE,addr=0x62000000,force-raw=on \
                  -device loader,file=$DTB,addr=0x68000000,force-raw=on
U-Boot 2021.04 (Aug 30 2021 - 01:45:32 +0200)

DRAM:  1 GiB
WARNING: Caches not enabled
Flash: 128 MiB
MMC:   MMC: 0
*** Warning - bad CRC, using default environment

In:    serial
Out:   serial
Err:   serial
Net:   smc911x-0
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
U-Boot&gt; setenv bootargs "console=ttyAMA0"
U-Boot&gt; bootz 62000000 - 68000000
Kernel image @ 0x62000000 [ 0x000000 - 0x93f200 ]
## Flattened Device Tree blob at 68000000
   Booting using the fdt blob at 0x68000000
   Loading Device Tree to 7fe6d000, end 7fe7375c ... OK

Starting kernel ...

[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 5.14.3 ...
...
[    3.208263] ---[ end 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 vexpress-a9 -m 1G -kernel $UBOOT -nographic \
                  -device loader,file=$ZIMAGE,addr=0x62000000,force-raw=on \
                  -device loader,file=$DTB,addr=0x68000000,force-raw=on \
                  -device loader,file=uRamdisk,addr=0x68080000,force-raw=on
U-Boot 2021.04 (Aug 30 2021 - 01:45:32 +0200)

DRAM:  1 GiB
WARNING: Caches not enabled
Flash: 128 MiB
MMC:   MMC: 0
*** Warning - bad CRC, using default environment

In:    serial
Out:   serial
Err:   serial
Net:   smc911x-0
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
U-Boot> setenv bootargs "root=/dev/ram rdinit=/hello console=ttyAMA0"
U-Boot> bootz 62000000 68080000 68000000
Kernel image @ 0x62000000 [ 0x000000 - 0x93f200 ]
## Loading init Ramdisk from Legacy Image at 68080000 ...
   Image Name:   
   Image Type:   ARM Linux RAMDisk Image (gzip compressed)
   Data Size:    1183353 Bytes = 1.1 MiB
   Load Address: 00000000
   Entry Point:  00000000
   Verifying Checksum ... OK
## Flattened Device Tree blob at 68000000
   Booting using the fdt blob at 0x68000000
   Loading Ramdisk to 7fd52000, end 7fe72e79 ... OK
   Loading Device Tree to 7fd4b000, end 7fd5175c ... OK

Starting kernel ...

[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 5.14.3 ...
...
[    2.840502] 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 default init program that is used from ramdisk so our hello 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.

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.

Dec 29, 2020

QEMU Board emulation - Introduction

There are many cheap COTS (Commercial Off-The-Shelf) development boards available for playing with and learning Embedded linux: Raspberry Pi, BeagleBone, OrangePi ...

The benefit of using COTS platform is that there are A LOT of available resources and examples, so one can create (mostly) functional prototype in a very short time. And this is good for someone who needs a quick solution for some problem without the need to go go into all the details.

However, for someone who wishes to go deeper into Embedded linux, boot process, optimization, driver development, etc, the COTS platform can provide only limited experience. Yes, one can develop drivers for some devices connected over external serial busses (even though someone has probably already developed a driver for that device), but that is about everything that can be done. There is no playing with memory maps, system-level device tree, bootloader adjustment, i.e. all of the things that are done when a custom board needs to be brought up.

One solution is to use development board based on SoC which combines Hard CPU core and FPGA parts, like Xilinx Zynq or Altera Cyclone. This SoC allows custom memory mapped hardware to be developed, which would require the developer to create a novel driver and learn a lot in the process. However, the 'creative' process is limited only to certain devices, not the system as a whole.


This is where QEMU (Quick EMUlator) can be very useful. QEMU has been used in industry during the design stage for new SoCs, since software development can be done before the hardware is not production ready (ZynqMP, RiscV).

QEMU has support for various development boards and devices, but new devices and development boards can be created with ease. For instance, one can create a new platform or I2C device, new system on chip with completely new memory map, or even new architecture (ok, highly unlikely that someone will go this far for educational purposes, but it is possible).


These series of posts will introduce QEMU board emulation for ARM architecture. The idea is to go from bootloader (U-Boot and Barebox), over kernel and root file-system (rootfs) creation. The first step will be to go over the whole procedure using several available boards:

  • ARM Versatile Express A9
  • OrangePI PC - covered in detail in QEMU docs
  • IMX6 SabreLite - covered in detail in QEMU docs

After this, I will go through creating new QEMU devices and developing drivers and userspace applications for them, in the following post series.


Quick links to other posts in the series:

  • Part 1 - Basics : covers basic steps for preparing the development environment, downloading and building U-Boot bootloader and Linux kernel
  • Part 2 - Running the system : covers root filesystem handling and booting from SD card or over network (TFTP+NFS)
  • Part 3 - Vexpress GUI : covers Linux kernel configuration in an attempt to display graphics
  • Part 4 - Vexpress Yocto : covers Yocto configuration for parts 2 and 3 of the blog series, so distribution and image can be built
  • Part 5 - Qt6 Yocto : covers Qt6 application development and Yocto integration