May 19, 2022

Learning Linux device driver development using QEMU - Introduction

In the previous blog series QEMU board emulation I introduced simple methods to prepare and run a QEMU image for board emulation. Target board was Versatile express and mainline U-Boot and Linux kernel were used for testing. The goal of that blog series is to show how to use QEMU for board emulation.


In this blog post series I will go deeper into the topics of device driver and userspace application development for embedded Linux systems.

We will continue using QEMU for testing. There are multiple benefits from using QEMU over some standard COTS board:

  • it is cheaper, there is no need to buy additional hardware, also making it easier to try and follow
  • 'new' hardware can be designed in QEMU, compared to COTS board where hardware cannot change and drivers are already available
    • even if we consider connecting peripherals over external parallel or serial busses as 'changing the COTS board', drivers already exist even for those peripherals
  • it is easier to debug during development in QEMU

There is also a possibility to use FPGA-SoC boards, where custom peripherals can be created in the FPGA part, but it is much more complex and also requires the board to be available.


The goals of this post series would be to show development of

  • character device driver for a custom memory-mapped hardware emulated in QEMU
  • userspace application that interacts with character device driver for memory-mapped peripheral
  • userspace I2C application/driver used to interact with custom I2C peripheral

In both cases I will also cover details on how to write a simple memory-mapped and I2C peripherals in QEMU.


Quick links to other posts in the series:

Apr 23, 2022

Yocto for Vexpress A9 in QEMU

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

In parts 2 and 3 of this post series the complete boot procedure from SD card has been presented, as well as how to configure kernel support required to enable graphical display for Vexpress-A9 board.

In both posts Ubuntu was used as root filesystem. Using Ubuntu as root filesystem is simple and fast to use, but it also has a lot of packages which are not necessary.

In this post I will cover the Yocto setup for Vexpress-A9 board. Using Yocto we will be able to build a custom distribution which will allow us to run Linux with or without GUI on QEMU Vexpress-A9.

Items that will be covered are

Sources for the Yocto build environment for Vexpress-A9 can be found in following repositories:

Yocto introduction

Yocto is tool used to build Board Support Package (BSP) and Linux distributions, especially for embedded targets. It is very configurable and provides fine grained control of output images.

The project is organized into layers and applications that can be built are described in recipes. Configuration for a build depends on selected image, machine and distribution, as well as local configuration parameters.

For more details about Yocto Bootling Yocto training slides can be used.

I tried to use the Freescale/NXP Yocto organization as a reference when creating these layers, so method of use is very similar to the method when working with iMX SoCs.

Base Yocto setup

As stated previously, the base repository holds the manifest file and base configuration.

The manifest file describes all the layers that are used in the project. In this case only 'dunfell' branch is selected, but selection can be made on a specific commit.

<?xml version="1.0" encoding="UTF-8" ?>
  <manifest>
  <default revision="dunfell" sync-j="4"/>

  <remote fetch="git://git.yoctoproject.org" name="yocto"/>
  <remote fetch="https://github.com/straxy" name="vexpress"/>
  <remote fetch="https://github.com/openembedded" name="oe"/>

  <project name="poky" path="sources/poky" remote="yocto" revision="dunfell"/>
  <project name="meta-openembedded" path="sources/meta-openembedded" remote="oe" revision="dunfell"/>
  <project name="qemu-vexpress-yocto" path="sources/base" remote="vexpress" revision="main">
    <copyfile dest="setup-environment" src="scripts/setup-environment"/>
  </project>
  <project name="meta-vexpress" path="sources/meta-vexpress" remote="vexpress" revision="main"/>

  </manifest>

The poky and meta-oe layers provide base applications and images, which can be extended by the higher-level layers, like meta-vexpress.

The setup-environment script is used to initialize a build environment. Part of it initializes the bblayers.conf and local.conf based on the input files in the templates directory.

Distribution configuration

Distribution configuration is in the meta-vexpress layer, in the conf/distro directory.

Two distributions are configured:

  • The framebuffer distribution which disables all graphical backends, like X11, Wayland, Vulkan. This way the output image size is reduced.
  • The X11 distribution which uses the X11 server.

Selection of distribution is made at compile time by passing either mistra-framebuffer or mistra-x11 to the DISTRO variable.

Machine configuration

Machine configuration is in the meta-vexpress layer, in the conf/machine directory.

Machine has definitions output images that should be built, as well as parameters for U-Boot and Linux kernel. This way, differences between machines can be kept in separate files and same distribution can be used for different machines.

U-boot recipe

The goal with this exercise is to use the same software versions as in part 2 of the blog post series.

The u-boot related recipes are in recipes-bsp directory.

The u-boot directory holds the main u-boot recipe, which targets a specific git commit in order to use the same version as in part 2 of the blog post series.

In the u-boot-scr directory is recipe used to build a u-boot script. The u-boot script is a special script run by U-Boot at boot. This way, all of the commands that were entered manually in part 2 of the blog post series will be automatically executed.

The commands are in boot.cmd file, and instructions on how this script can be manually built are in the do_compile step of the u-boot-scr.bb recipe.

Linux kernel recipe

Linux recipe is stored in the recipes-kernel. It selects the appropriate git commit in order to have the same version as in part 2 of the blog post series.

Image recipe

Image recipe is stored in the recipes-extended/images directory.

This is a copy of the core-image-minimal image supplied from Yocto, but can be extended if needed. This is an image that does not use graphics.

Another image that will be used is core-image-sato, which uses graphical environment.

Building and running images in QEMU

Prerequisites

Before Yocto image can be built, several packages must be installed:

$ sudo apt-get install gawk wget git-core diffstat unzip texinfo \
     gcc-multilib build-essential chrpath socat cpio python3 \
     python3-pip python3-pexpect xz-utils debianutils iputils-ping \
     python3-git python3-jinja2 libegl1-mesa libsdl1.2-dev pylint3 \
     xterm

NOTE: For details look at Yocto reference manual

Preparing environment

In order to prepare for a build, the manifest repository must be used with repo tool.

$ repo init -u https://github.com/straxy/qemu-vexpress-yocto -m default.xml
$ repo sync

After the repo is initialized and synced, all recipe sources will be in the sources directory. Two additional directories will be created, downloads, where archives and git repositores with sources for packages are downloaded and kept, and sstate-cache intermediate build products for reuse will be stored during the build process. The sstate-cache directory can speed up rebuilds, or different flavor builds, since packages that are not changed will be reused.

The next step is to initialize the build environment

$ source setup-environment <build_dir>

where <build_dir> is custom directory where build will be performed and output files stored. After this command is executed current directory is automatically changed to build_dir.

Building and running images

The build is started using the following command

# build command
$ DISTRO=<selected_distribution> MACHINE=<selected_machine> bitbake <selected_image>

Once build is completed, output image will be placed in <build_dir>/tmp/deploy/images/<selected_machine>/<selected_image>-<selected_machine>.wic. There will be also other build products, like u-boot binary (u-boot.elf will be used for running QEMU), linux kernel binary, etc.

Image is in wic format and can be copied to the SD card image using following commands

# create SD card image
$ qemu-img create sd.img 4G
# 'insert' SD card
$ sudo kpartx -av ./sd.img
# note the loopXX that is used and use dd to copy wic image
# if there is no output, look at $ losetup -a to find the loop device
$ sudo dd if=<selected_image>-<selected_machine>.wic of=/dev/loopXX bs=1M iflag=fullblock oflag=direct conv=fsync
# after copying is done 'remove' the SD card
$ sudo kpartx -d ./sd.img

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

# Run QEMU with SD card and networking
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel u-boot.elf \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio

Framebuffer image

Distro that is used is mistra-framebuffer and image is core-image-test, so complete build command is

# framebuffer image build command
$ DISTRO=mistra-framebuffer MACHINE=vexpress-qemu bitbake core-image-test

After image is built, copied to the SD card and QEMU is run, the following output will be visible

# QEMU output
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel u-boot.elf \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio
[ ... ]
Mistra FrameBuffer 3.1 vexpress-qemu /dev/ttyAMA0
vexpress-qemu login:

X11 image and Sato

Distro that is used is mistra-x11 and image is core-image-sato, so complete build command is

# framebuffer image build command
$ DISTRO=mistra-x11 MACHINE=vexpress-qemu bitbake core-image-sato

After image is built, copied to the SD card and QEMU is run, the following output will be visible

# QEMU output
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel u-boot.elf \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio
[ ... ]
Mistra X11 3.1 vexpress-qemu /dev/ttyAMA0
vexpress-qemu login:

During the boot process following image is visible

After the boot process is complete the environment is ready

Summary

In this blog post the procedure for using Yocto to build the distribution is shown. It can be further extended with other layers (like meta-qt5).

Feb 6, 2022

Emulating Ubuntu GUI on Vexpress in QEMU

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

In the previous post the complete boot procedure from SD card or network boot has been presented.

In this post I will cover the following things

The goal is to use the emulated Versatile express board to display some simple graphics. This is also a very good feature of QEMU, where graphical applications can be run for some of the emulated boards.

Modifying Linux kernel configuration

Versatile Express V2P-CA9 has the PL111 LCD display controller which is emulated in QEMU. The LCD controller is connected to a SiI9022 display bridge controlled over an I2C bus, and a Versatile display panel. However, none of it is not enabled by default in the Linux configuration.

In order to enable it, several configuration options in the linux kernel should be enabled

  • CONFIG_DRM_PL111 - enable LCD display controller
  • CONFIG_I2C_VERSATILE - enable I2C bus support; the display bridge is on the I2C bus
  • CONFIG_DRM_SII902X - enable display bridge
  • CONFIG_DRM_PANEL_ARM_VERSATILE - enable ARM versatile display panel

Besides the hardware dependencies, in order to enable framebuffer and display image on the display, several additional options must be enabled

  • CONFIG_FB - enable framebuffer support
  • CONFIG_FB_ARMCLCD - enable ARM LCD framebuffer support
  • CONFIG_FRAMEBUFFER_CONSOLE - enable console to be shown on the framebuffer
  • CONFIG_LOGO - show tux logo when booting up

Enabling these configuration options is done using menuconfig. First the environment script env.sh should be sourced and then the menuconfig interface can be entered using

# Enter menuconfig
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_vexpress menuconfig

This will show an interface like

The menuconfig can be traversed using arrow keys. Options are selected using the space key.

There are two types of options that can be selected

  • boolean
    • [ ] - option is not enabled
    • [*] - option is enabled
  • tristate
    • < > - option is not enabled
    • <M> - functionality will be compiled as a kernel module
    • <*> - functionality will be compiled in the linux kernel image

There are other filed types that are available, but they are not important for our analysis.

If we want to search for a keyword in menuconfig, we can press / and then type the keyword. This will open a window where selection can be made using a number on the keyboard.


Since we know which configuration options we want, we can search for them. Otherwise, they can be found in the following paths

CONFIG_DRM_PL111
-> Device Drivers
  -> Graphics support
    <*> DRM Support for PL111 CLCD Controller
CONFIG_I2C_VERSATILE
-> Device Drivers
  -> I2C support
    -> I2C Hardware Bus support
      <*> ARM Versatile/Realview I2C bus support
CONFIG_DRM_SII902X
-> Device Drivers
  -> Graphics support
    -> Display Interface Bridges
      <*> Silicon Image sii902x RGB/HDMI bridge
CONFIG_DRM_PANEL_ARM_VERSATILE
-> Device Drivers
  -> Graphics support
    -> Display Panels
      <*> ARM Versatile panel driver
CONFIG_FB
-> Device Drivers
  -> Graphics support
    -> Frame buffer Devices
      <*> Support for frame buffer devices  --->
CONFIG_FB_ARMCLCD
-> Device Drivers
  -> Graphics support
    -> Frame buffer Devices
      -> Support for frame buffer devices
        <*> ARM PrimeCell PL110 support
CONFIG_FRAMEBUFFER_CONSOLE
-> Device Drivers
  -> Graphics support
    -> Console display driver support
      [*] Framebuffer Console support
CONFIG_LOGO
-> Device Drivers
  -> Graphics support
    [*] Bootup logo

After the kernel configuration is modified, kernel has to be rebuilt again and all related files copied to the SD card (kernel image and kernel modules). For detailed instructions consult part 1 and part 2 of this series. If scripts from github are used, then prepare-qemu.bash script has to be executed after the Linux kernel is rebuilt in order to pull new image and kernel modules to the SD card.

Updating kernel config manually

If scritps from github are used for testing, the supplied defconfig file can be used to update the kernel configuration to support display.

The defconfig needs to be copied into the $PROJ_DIR/linux/build_vexpress/ directory and renamed to .config. After that the olddefconfig command has to be executed.

# Using defconfig
$ cp defconfig $PROJ_DIR/linux/build_vexpress/.config
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_vexpress olddefconfig

After this the build can be started and prepare-qemu.bash script can be used to prepare the new SD card.

Enabling graphics in Ubuntu minimal

In order to enable graphics in the Ubuntu minimal image, we need to boot the image and install apropriate packages.

QEMU can be started in the following manner in order to use image from SD card and allow network connection and use graphics for display

# Run QEMU with SD card and networking
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel $UBOOT \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio

NOTE: It is assumed that network is enabled in the host according to previous instructions.

Please note that the command for starting QEMU has a significant difference compared to instructions from part 1 and part 2 of blog series: instead of passing parameter -nographics the parameter -serial mon:stdio is used.

This will enable graphical window to appear and serial output for U-Boot will still go to terminal window from where QEMU is started. Serial output direction for Linux kernel can be controlled by specifying console parameter in the bootargs U-Boot variable:

  • if console=ttyAMA0 linux console output will go to the same terminal as U-boot output,
  • if console=tty0 linux console output will go to the graphical window.

First check if everything is working ok can be done by looking at the display while the system is booting, it should show a tux logo

An addition check if framebuffer support is working can be done by writing random data to framebuffer, which should show a 'noisy' image.

# Write random data to framebuffer
$ sudo cat /dev/urandom > /dev/fb0

Following image is the result


After network is enabled in the emulated board then apt-get can be used to update and download necessary applications (this post from askubuntu.com has been used as a reference for installation of packages).

# Install xserver apps
$ sudo apt-get update
$ sudo apt-get install xserver-xorg-core --no-install-recommends --no-install-suggests
$ sudo apt-get install openbox --no-install-recommends --no-install-suggests
$ sudo apt-get install xinit
$ sudo apt-get install slim

In order to display graphics on the graphics window following command can be used

# Start graphics
$ sudo service slim start

This will show following login screen and, after logging in, an application running (xterm). Please note that this is bare minimum, so not much can be done without installing additional applications.

Summary

In this post a way to configure Linux kernel to enable graphics support for Versatile Express board is shown. Enabling on the root filesystem side can be optimized by using a root filesystem image built by Yocto or Buildroot.

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