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
git clone https://github.com/crosstool-ng/crosstool-ng
Compiling
./bootstrap # only if crosstool-NG is cloned from github
./configure --enable-local
make
Configuring a cross-compiling toolchain
The command
./ct-ng list-samples
will give a list of possible targets to compile for. The entry aarch64-rpi4-linux-gnu
is used by executing the following command:
./ct-ng aarch64-rpi4-linux-gnu
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:
./ct-ng menuconfig
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:
./ct-ng menuconfig
- 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:
./ct-ng build -j`nproc`
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
:
toolchain-gcc -v
# or, if the "Tuple's alias" option was not set when configuring the toolchain:
aarch64-rpi4-linux-uclibc-gcc -v
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:
tar -xvf buildroot-<VERSION_GOES_HERE>.tar.gz
Initial setup
To view the target devices Buildroot can build systems for, the following command can be executed:
make list-defconfigs
To configure Buildroot to build for the Raspberry Pi 4 (64 bit), the following command can be issued from the Buildroot directory:
make raspberrypi4_64_defconfig
To then configure the system, run the following command:
Configuring the target system
make menuconfig
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:
cp package/busybox/S10mdev ${TARGET_DIR}/etc/init.d/S10mdev
chmod 755 ${TARGET_DIR}/etc/init.d/S10mdev
cp package/busybox/mdev.conf ${TARGET_DIR}/etc/mdev.conf
Building
To build the embedded OS, the following command can be executed:
make clean && make
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:
mkimage -n 'Linux kernel' -A arm64 -O linux -C none -T kernel -a 0x00080000 -e 0x00080000 -d arch/arm64/boot/Image uImage
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.
git clone https://github.com/u-boot/u-boot
cd u-boot/
Configuring the U-boot source
To see which default configurations U-boot has, execute the following command:
ls configs
# or, to only see raspberry pi entries
ls configs | grep rpi
To create a basic configuration for the Raspberry Pi 4, the following command can be executed:
ARCH=arm64 CROSS_COMPILE=~/x-tools/aarch64-rpi4-linux-uclibc/bin/aarch64-rpi4-linux-uclibc- make rpi_4_defconfig
To then further configure the source, the make menuconfig
command can be given:
ARCH=arm64 CROSS_COMPILE=~/x-tools/aarch64-rpi4-linux-uclibc/bin/aarch64-rpi4-linux-uclibc- make menuconfig
In this post, the autoboot timer is changed from 2
to 0
for production-builds. This option can be changed in the following region:
Boot options ->
Autoboot options ->
delay in seconds before automatically booting
For test builds, it is recommended to leave this value as is.
Compiling U-boot
ARCH=arm64 CROSS_COMPILE=~/x-tools/aarch64-rpi4-linux-uclibc/bin/aarch64-rpi4-linux-uclibc- make -j`nproc`
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:
# Enable UART communication
enable_uart=1
# Enable 64 bit mode
arm_64bit=1
# Make the raspberry Pi 4 start U-boot (u-boot.bin) instead of the default kernel8.img
kernel=u-boot.bin
# If the bluetooth device interferes with any uart/serial messages, uncomment this
#dtoverlay=miniuart-bt
# Uncomment to go fast
#arm_boost=1
# Uncomment to enable DRM VC4 V3D driver
#dtoverlay=vc4-kms-v3d
#max_framebuffers=2
# Uncomment to disable overscan
#disable_overscan=1
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:
sleep 1 # some SD cards are slow and will fail to list files in time
echo +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
echo +-----------------------------------------------------------+
echo +----------------Embedded OS is starting!-------------------+
echo +-----------------------------------------------------------+
echo +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
echo ++++ Setting kernel parameters ++++++++++++++++++++++++++++++
setenv bootargs 'dwc_otg.lpm_enable=0 console=ttyAMA0,115200 console=tty1 kgdboc=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait'
echo ++++ Loading kernel into memory +++++++++++++++++++++++++++++
load mmc 0 $kernel_addr_r uImage
echo ++++ Loading device tree into memory ++++++++++++++++++++++++
load mmc 0 $fdt_addr_r bcm2711-rpi-4-b.dtb
echo ++++ Starting system ++++++++++++++++++++++++++++++++++++++++
bootm $kernel_addr_r - $fdt_addr_r
The boot.txt
file is then converted to boot.scr
using the following command:
mkimage -A arm64 -O linux -T script -C none -a 0 -e 0 -n "bootscript" -d boot.txt boot.scr
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:
# create a file called `system.img` with a size of 600MiB
dd if=/dev/zero of=system.img bs=1MiB count=600 status=progress
Then, this virtual disk image will be partitioned using parted
:
parted system.img
Within parted
, run the mklabel
command and set the partition table type to msdos
:
(parted) mklabel msdos
Then create the boot and root partitions:
(parted) mkpart primary fat16 2048s 30MiB
(parted) mkpart primary ext4 30MiB 100%
(parted) set 1 boot on
(parted) print
When done, exit parted by entering quit
:
(parted) quit
Partitioning the system image
First, mount the virtual disk image as follows:
# Create a variable for the mount drive path
SYSTEM_IMAGE=`sudo losetup -Pf system.img --show`
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:
echo $SYSTEM_IMAGE
Then, partition it using the following command:
# Create actual partitions for boot and rootfs now
sudo mkfs.vfat -n BOOT `echo "${SYSTEM_IMAGE}p1"` # Create a fat16 partition for boot files
sudo mkfs.ext4 -L ROOTFS `echo "${SYSTEM_IMAGE}p2"` # Create an ext4 partition for rootfs files
Mounting the partitions
To mount the system image, use the following commands to mount the virtual disk image to a folder called target_mnt
:
mkdir ./target_mnt
sudo mount `echo "${SYSTEM_IMAGE}p2"` ./target_mnt
sudo mkdir ./target_mnt/boot
sudo mount `echo "${SYSTEM_IMAGE}p1"` ./target_mnt/boot
Copying boot files
First, all required firmware files will be copied from the buildroot output folder to the boot folder:
sudo cp ./buildroot-<VERSION_GOES_HERE>/output/images/bcm2711-rpi-4-b.dtb ./target_mnt/boot/
sudo cp ./buildroot-<VERSION_GOES_HERE>/output/images/rpi-firmware/start4.elf ./target_mnt/boot/
sudo cp ./buildroot-<VERSION_GOES_HERE>/output/images/rpi-firmware/fixup4.dat ./target_mnt/boot/
Then, the kernel is copied to the boot folder:
sudo cp ./buildroot-<VERSION_GOES_HERE>/output/images/uImage ./target_mnt/boot/
After that, the bootloader and related configurations are copied to the boot folder:
# Copy the bootloader
sudo cp ./u-boot/u-boot.bin ./target_mnt/boot/
# Copy config.txt, to the Raspberry Pi boots U-boot
sudo cp ./u-boot/config.txt ./target_mnt/boot/
# Copy boot.scr, this bootscript starts the embedded OS
sudo cp ./u-boot/boot.scr ./target_mnt/boot/
Extracting the root filesystem to the ROOTFS
partition
cd ./target_mnt
sudo tar -xvf ../buildroot-<VERSION_GOES_HERE>/output/images/rootfs.tar.gz .
cd ../
Unmounting the image
sudo sync
sudo umount ./target_mnt/boot -l
sudo umount ./target_mnt -l
sudo losetup -D
sudo rm -rf ./target_mnt
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:
sudo dd if=system.img of=/dev/YOUR_DEVICE_GOES_HERE bs=1MiB status=progress
sudo sync
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