13 minutes
Creating a minimal OS using buildroot (Raspberry Pi)
What is an embedded OS?
An Embedded Operating System is an Operating System designed to run on specific hardware. For example, many smart TV’s these days are running an embedded OS. Because the OS is made for the specific hardware, it can be configured to start up in the blink of an eye.
What is Buildroot?
Buildroot, just like the yocto project, is a set of tools to help automate the build process of an embedded Linux based OS. Buildroot is configured through a set of configuration files, after which it builds an entire Linux based OS.
What are the benefits of creating a custom embedded OS?
There are a few great benefits to creating a custom embedded OS (and not using a prebuilt image file):
- The kernel and root filesystem can be seperated from the bootloader and stored on a remote server, to make the local system image really small.
- If Buildroot is configured to only create a root filesystem, the resulting OS can run from a bootloader and/or kernel that is not built by Buildroot.
- The U-boot bootloader is very configurable and can be configured to start the right kernel for the right Raspberry Pi board if needed. This includes downloading the right kernel (and device tree) from a remote server and then booting it.
- The embedded OS built by buildroot supports many init systems (busybox, openrc, systemd, etc.).
- The embedded OS is very minimal. This saves space and teaches how a Linux based OS works.
- Almost any program that would normally run on Raspberry Pi OS (formerly called Raspbian) can easily be cross-compiled during the build process. This includes using CMake.
- There are many debugging tools that can be built into the embedded OS to debug a certain application.
- The cross-compiling toolchain can be fully configured within Buildroot. An external cross-compiling toolchain can also be used if desired (crosstool-NG for example).
The goal
In this post, a basic Buildroot OS will be created. This will be a minimal build; it boots but is utterly useless. At the end of this post, the minimal Buildroot OS will be used for two other projects:
- Creating an OS that serves as an access point.
- Cross-compiling Allegro5 and Dune Dynasty and adding it to the minimal OS.
Setting up a cross-compiling toolchain
If the kernel is compiled using arm-none-eabi-gcc
and the rest of the system is compiled using aarch64-linux-gnu-gcc
, the system will not run. To ensure that the system runs (well) on the target board, a cross-compiling toolchain will be set up. This toolchain contains a cross-compiling gcc
, ld
and other tools that would normally be used to (cross-)compile a program or OS. The tools in the toolchain will be used to compile the whole system.
In this post, crosstool-NG is used.
Downloading
Compiling
Configuring a cross-compiling toolchain
The command
will give a list of possible targets to compile for. The entry aarch64-rpi4-linux-gnu
is used by executing the following command:
Because the GNU C library is quite big compared to something like uClibc, the .config
file will be edited to use uClibc instead of the GNU C library. The goal is to make the target system a little smaller in disk size. The .config
file is edited using the following command:
The following changes were made inside the menuconfig menu:
- Under
C library
, the optionC library
is changed touClibc
Then the menuconfig is exited and restarted to reload all options:
- Under
C library
, the optionAdd support for locales
is enabled - Under
C library
, the optionAdd support for IPv6
is enabled - Under
C library
, the optionenable iconv
is enabled - Under
C library
, the optionAdd support for fenv.h
is enabled - Under
C compiler
, the optionVersion of gcc
is set to the second to latest version (at time of writing this was set to version10.3.0
) - Under
C compiler
, the optionC++
is enabled - Under
Operating system
, the optionVersion of linux
is set to the latest version (at time of writing this was version5.15.2
) - Under
Debug facilities
, the optiongdb
is disabled (optional) - Under
Toolchain options
, the optionTuple's alias
is set totoolchain
(optional)
Building the toolchain
After the toolchain is configured, it can be built using the following command:
This build process will take a long time. Therefore, it should not be done in a (misconfigured) VM, but on a pc with lots of cores and/or threads. This can reduce the build time from a day to a few hours (maybe even less).
Checking if the toolchain works
After crosstool-NG is done creating a toolchain, a folder called x-tools
can be found in the home-directory of the current user (/home/$USER/x-tools
). Inside the x-tools
directory, all cross-compiling toolchains can be found. To test if the new toolchain is the right version, the following command is executed inside /home/$USER/x-tools/aarch64-rpi4-linux-uclibc/bin
:
The executed binary states that it is version 10.3.0
, just like the version that was specified in the menuconfig of crosstool-NG. For now, it is assumed that the binary will cross-compile without errors.
Creating a basic Buildroot OS
Downloading Buildroot
To get started, Buildroot can be downloaded from the Buildroot website. There are two download options to choose from; “LTS” or “stable”. For newer devices, the stable option is a good option. For this blog, the stable .tar.gz archive is used.
Extracting
To extract the .tar.gz file, the following command can be executed:
Initial setup
To view the target devices Buildroot can build systems for, the following command can be executed:
To configure Buildroot to build for the Raspberry Pi 4 (64 bit), the following command can be issued from the Buildroot directory:
To then configure the system, run the following command:
Configuring the target system
For this post, the following changed were made to the configuration:
- Under
Build options
, the optionEnable compiler cache
is enabled - Under
Bootloaders
, all the options are disabled (the bootloader will be compiled manually) - Under
Filesystem images
, the optionext2/3/4 root filesystem
is disabled - Under
Filesystem images
, the optiontar the root filesystem
is enabled, along with theCompression method
set togzip
- Under
Toolchain
, the optionToolchain type
is set toExternal toolchain
(this post uses a toolchain built by crosstool-NG) - Under
Toolchain
, the optionToolchain
is set toCustom toolchain
- Under
Toolchain
, the optionToolchain origin
is set toPre-installed toolchain
- Under
Toolchain
, the optionToolchain path
is set to/home/<YOUR_USERNAME_GOES_HERE>/x-tools/aarch64-rpi4-linux-uclibc
(the path has to be absolute and may not contain~/
or/home/$USER/
!). - Under
Toolchain
, the optionToolchain prefix
is set toaarch64-rpi4-linux-uclibc
- Under
Toolchain
, the optionExternal toolchain gcc version
is set to10.x
(this version has to match the version of the cross-compiling toolchain) - Under
Toolchain
, the optionExternal toolchain kernel headers series
is set to5.15.x or later
(this version has to match the version of the cross-compiling toolchain) - Under
Toolchain
, the optionToolchain has locale support?
is enabled - Under
Toolchain
, the optionToolchain has C++ support?
is enabled - Under
Toolchain
, the optionToolchain has SSP support?
is enabled - Under
System configuration
, the optionSystem hostname
is set toembedded
- Under
System configuration
, the optionSystem banner
is set toWelcome to embedded OS!
- Under
System configuration
, the optionRoot password
is set toroot
(for testing test builds) - Under
System configuration
, the option/dev management
is set toDynamic using devtmpfs + mdev
(to load drivers automatically when the target device boots)
Most linux desktops use udev
to manage device drivers, but because this system uses BusyBox
as init system, so mdev
is used. To enable mdev
at boot, the file buildroot-<VERSION_GOES_HERE>/board/raspberrypi/post-build.sh
needs the following extra lines at the bottom, leaving the rest of the file as is:
Building
To build the embedded OS, the following command can be executed:
The reason for the make clean
part is that Buildroot remembers a lot, and will sometimes refuse to build (correctly) because of a cached configuration.
After compiling
When buildroot is done compiling, a file called rootfs.tar.gz
can be found in the output/images
folder. This is the entire embedded OS, minus the bootloader. The bootloader will be compiled manually.
The same output/images
folder also contains a linux image called Image
. This image needs to be converted into a U-boot image (uImage
)
In addition to the rootfs.tar.gz
file being present, the folder output/images
will also contain a folder called rpi-firmware
.
The files, start4.elf
and fixup4.dat
are needed later. These firmware files are files needed to boot the Raspberry Pi 4.
Converting the linux Image into a uImage
Buildroot created a file called Image
. However, the U-boot bootloader (which wil be built in the next chapter) cannot boot this image file as it is. In order for U-boot to boot the kernel, the image file has to be converted to a U-boot image (uImage
).
To convert the linux image into a uImage, the package u-boot-tools
(or uboot-tools
) is required. Then the Image
file can be converted using the following command:
A file called uImage
will now be created. This file contains the entire linux kernel binary, including some extra header stuff that u-boot-tools
added. This file is the kernel which runs the system.
Adding a bootloader (U-boot)
Although Buildroot can include the U-boot bootloader (among others) by enabling some settings, it might be nice to go a little more in depth to learn how U-boot actually works.
Downloading U-boot
The U-boot bootloader can be downloaded from github.
Configuring the U-boot source
To see which default configurations U-boot has, execute the following command:
To create a basic configuration for the Raspberry Pi 4, the following command can be executed:
To then further configure the source, the make menuconfig
command can be given:
In this post, the autoboot timer is changed from 2
to 0
for production-builds. This option can be changed in the following region:
For test builds, it is recommended to leave this value as is.
Compiling U-boot
The compile process will not take long. After the compilation is complete, a file called u-boot.bin
(the U-boot binary) will exist.
Creating Raspberry Pi boot configuration
Normally, the Raspberry Pi 4 boots a file called kernel.img
(sometimes kernel<version number here>.img
). In this post, the Raspberry Pi 4 should start the U-boot bootloader instead of the kernel. This can be done by creating a file called config.txt
with the following content:
Creating a U-boot boot script
By default, when the U-boot bootloader starts up, it will look for a file called boot.scr
. The boot.scr
file is a boot script. This script will be used to load the kernel (uImage
) and device tree (bcm2711-rpi-4-b.dtb
) into memory and boot the system.
To create a bootscript, a file called boot.txt
is created. This file will be converted to boot.scr
and contains the following:
The boot.txt
file is then converted to boot.scr
using the following command:
Creating a system image
Creating an image file
To create partitions, the program parted
will be used. There will be two partitions: a boot partition called BOOT
and a root partition called ROOTFS
. The boot partition will have a fat16 filesystem and the root filesystem will use the ext4 filesystem.
To prevent disks from being destroyed, a virtual disk image will be created using dd:
Then, this virtual disk image will be partitioned using parted
:
Within parted
, run the mklabel
command and set the partition table type to msdos
:
Then create the boot and root partitions:
When done, exit parted by entering quit
:
Partitioning the system image
First, mount the virtual disk image as follows:
Normally, disk devices in linux have paths like /dev/sda
, /dev/sdb
, /dev/mmcblk0
, etc. This virtual device will have the path /dev/loop0
. Multiple virtual devices can be mounted. The next mounted virtual disks will have the path /dev/loop1
, /dev/loop2
, /dev/loop3
, etc.
In this post, the path to the virtual disk is stored in the SYSTEM_IMAGE
variable. To view the path to the virtual disk device, run the following command:
Then, partition it using the following command:
Mounting the partitions
To mount the system image, use the following commands to mount the virtual disk image to a folder called target_mnt
:
Copying boot files
First, all required firmware files will be copied from the buildroot output folder to the boot folder:
Then, the kernel is copied to the boot folder:
After that, the bootloader and related configurations are copied to the boot folder:
Extracting the root filesystem to the ROOTFS
partition
Unmounting the image
Flashing the virtual disk image to an SD
The system.img
file (which now contains the entire embedded OS) can be flashed to an SD card using dd
as follows:
Since system.img
is an image file the flashing process is exactly the same as all other “Raspberry Pi Linux distro’s”.
Therefore, if dd
seems too scary, the following tools can be used to flash the system.img
file:
Raspberry Pi Imager,
Etcher,
GNOME Disks or
Win32 Disk Imager.
Sources
- https://bootlin.com/doc/training/buildroot/buildroot-slides.pdf
- https://blog.crysys.hu/2018/06/enabling-wifi-and-converting-the-raspberry-pi-into-a-wifi-ap/
- https://unix.stackexchange.com/questions/439559/udhcpc-no-lease-failing-when-booting-on-embedded-linux-created-by-buildroot
- https://raspberrypi.stackexchange.com/questions/107858/raspberry-pi-4-b-5ghz-wifi-access-point-problem
- http://lists.busybox.net/pipermail/buildroot/2019-June/252256.html