Create a Linux system using Buildroot

1st Jun 2019 • Marco Cetica

final_result.webp

What is Buildroot?

Buildroot is a set of tools to simplify the process of bootstrapping a Linux distribution for embedded devices. The project is fully open source and is driven by its community which keeps it updated and functional.

Features of Buildroot

Buildroot is an excellent choice for most embedded applications. It significantly simplifies the creation of a complete embedded system by providing an easy-to-use configuration system that includes a kernel-style menuconfig, an interactive download mechanism and a flexible overlay system.

Designed to manage the entire build process autonomously, Buildroot requires only that you choose the components you wish to include. From there, it will download, configure, compile and install everything without the need for additional manual setup. In about 30 minutes of build time, it will generates a fully functional image that you can flash on the ROM of your embedded device.

Prerequisites

In order to follow this guide, you will need:

For a full list of updated requirements, you can visit this page. You should also have some knowledge on how to use a Linux system and how to fix normal issues by yourself.

Downloading Buildroot

You can obtain Buildroot through three different methods:

At the time of writing this article, I'm using the stable release; however, any other version will be just fine. Buildroot can be downloaded from this link.

Set up your target architecture

Once you've downloaded one of the available releases, extract the archive file and then search your target architecture. By default, Buildroot stores supported architectures under the configs/ directory. For instance, if we want to target the Raspberry Pi Zero, we should use one of the following configuration files:

$ ls -lh configs | grep raspberry .rw-r--r-- marco marco 1.1 KB Mon Jun 16 23:13:14 2019 raspberrypi0_defconfig .rw-r--r-- marco marco 1 KB Mon Jun 16 23:13:14 2019 raspberrypi0w_defconfig .rw-r--r-- marco marco 1.1 KB Mon Jun 16 23:13:14 2019 raspberrypi2_defconfig .rw-r--r-- marco marco 1.2 KB Mon Jun 16 23:13:14 2019 raspberrypi3_64_defconfig .rw-r--r-- marco marco 1.1 KB Mon Jun 16 23:13:14 2019 raspberrypi3_defconfig .rw-r--r-- marco marco 1.5 KB Mon Jun 16 23:13:14 2019 raspberrypi3_qt5we_defconfig .rw-r--r-- marco marco 1.1 KB Mon Jun 16 23:13:14 2019 raspberrypi4_64_defconfig .rw-r--r-- marco marco 1.1 KB Mon Jun 16 23:13:14 2019 raspberrypi4_defconfig .rw-r--r-- marco marco 1.1 KB Mon Jun 16 23:13:14 2019 raspberrypi_defconfig

To select raspberry0w_defconfig, type make raspberrypi0w_defconfig.

The other relevant directories are:

System Setup

Now that we have selected the target platform, it's time to configure our system. Before proceeding, however, we must set up a cross compiler. This is needed due to the fact that the RPI uses an ARM CPU whereas our development environment is likely an x86 system; therefore, we also need to set up a compatible version of binutils. Since this is a time consuming and error-prone task to do manually, we can delegate it to Buildroot by simply running make all.

Once this initial step is finished, we can run make menuconfig to open Buildroot interactive configuration menu. You should be welcomed by a window similar to the following one:

menu_config_home.webp

This is the usual configuration menu used by Linux developers to configure the kernel. If you have already seen such menu before, you probably know how useful and well organized it is compared to manual file editing. By moving the cursor with the keyboard arrows, you can enter the various sub-menus and select what you want to include. Below, I've made a list of the bare minimum modules needed by the RPI Zero platform. I've included support for Wi-Fi, OpenSSH and core utilities.

Let's start by enabling the compiler cache by going to build and then toggling the following value:

Build options -> Enable compiler cache (BR2_CCACHE)

Under toolchain enable WCHAR support, this is need by some packages such as bash or vim:

Toolchain -> Enable WCHAR support (BR2_TOOLCHAIN_BUILDROOT_WCHAR)

Under the system configuration sub menu, you can customize the system by changing the default hostname, the system banner, the default root password and the default shell (I suggest you to replace the raw busybox default shell into something more polished such as Bash):

System configuration -> System hostname (BR2_TARGET_GENERIC_HOSTNAME) System configuration -> System banner (BR2_TARGET_GENERIC_ISSUE) System configuration -> Root password (BR2_TARGET_GENERIC_ROOT_PASSWD) System configuration -> /bin/sh (busybox' default shell) ---> (x) Bash

Note that, in order to change the default shell, you may need to enable the BR2_PACKAGE_BUSYBOX_SHOW_OTHERS flag under Target packages sub menu.

Let's now move to the target packages sub menu. Here, you can select which packages supported by Buildroot to install. Below, there's a comprehensive list of the most common packages that you may want to include in your target system. You can exclude anything that seems irrelevant for your use case.

Target packages -> Shell and utilities -> bash completion (BR2_PACKAGE_BASH_COMPLETION) Target packages -> Shell and utilities -> sudo (BR2_PACKAGE_SUDO) Target packages -> Shell and utilities -> time (BR2_PACKAGE_TIME) Target packages -> Hardware handling -> Firmware -> rpi-wifi-firmware (BR2_PACKAGE_RPI_WIFI_FIRMWARE) Target packages -> Interpreter languages and scripting -> Python3 (BR2_PACKAGE_PYTHON3) Target packages -> Networking applications -> wget (BR2_PACKAGE_WGET) Target packages -> Networking applications -> wpa_supplicant (BR2_PACKAGE_WPA_SUPPLICANT) Target packages -> Networking applications -> wpa_supplicant -> Enable nl80211 support (BR2_PACKAGE_WPA_SUPPLICANT_NL80211) Target packages -> Networking applications -> wpa_supplicant -> Enable AP mode (BR2_PACKAGE_WPA_SUPPLICANT_AP_SUPPORT) Target packages -> Networking applications -> wpa_supplicant -> Enable autoscan (BR2_PACKAGE_WPA_SUPPLICANT_AUTOSCAN) Target packages -> Networking applications -> wpa_supplicant -> Enable WPA3 support (BR2_PACKAGE_WPA_SUPPLICANT_WPA3) Target packages -> Networking applications -> wpa_supplicant -> Install wpa_cli binary (BR2_PACKAGE_WPA_SUPPLICANT_CLI) Target packages -> Networking applications -> wpa_supplicant -> Install wpa_client shared library (BR2_PACKAGE_WPA_SUPPLICANT_WPA_CLIENT_SO) Target packages -> Networking applications -> wpa_supplicant -> Install wpa_passphrase binary (BR2_PACKAGE_WPA_SUPPLICANT_PASSPHRASE) Target packages -> Networking applications -> iptables (BR2_PACKAGE_IPTABLES) Target packages -> Networking applications -> ntp (BR2_PACKAGE_NTP) Target packages -> Networking applications -> ntp -> sntp (BR2_PACKAGE_NTP_SNTP) Target packages -> Libraries -> Networking -> libcurl -> curl binary (BR2_PACKAGE_LIBCURL_CURL) Target packages -> System Tools -> htop (BR2_PACKAGE_HTOP) Target packages -> Text editors and viewers -> vim (BR2_PACKAGE_VIM) Target packages -> Hardware handling -> rng-tools (BR2_PACKAGE_RNG_TOOLS) Target packages -> Hardware handling -> NIST Entropy Beacon support (BR2_PACKAGE_RNG_TOOLS_NISTBEACON) Target packages -> Libraries -> Crypto -> gnutls (BR2_PACKAGE_GNUTLS) Target packages -> Libraries -> Crypto -> gnutls -> OpenSSL compatibility library (BR2_PACKAGE_GNUTLS_OPENSSL) Target packages -> Libraries -> Crypto -> gnutls -> install tools (BR2_PACKAGE_GNUTLS_TOOLS) Target packages -> Libraries -> Crypto -> CA Certificates (BR2_PACKAGE_CA_CERTIFICATES)

Have you ever noticed the small logo displayed at the top of the TTY, representing the number of CPU cores? That is:

vclagokjunz51.webp

This is known as the framebuffer logo and although you can simply leave it unchanged, it is nice to have the option to change it. In some cases, you may even be required to replace the default Raspbian logo with the branding provided by your company. Traditionally, updating the framebuffer logo was a tedious and error-prone process but with Buildroot, this procedure becomes quick and straightforward (that means that you don't have to deal with format conversions anymore). All you need is a 64x64 PNG or JPG image with a black background (for the latter format) placed in the root of your Buildroot directory. Then, in menuconfig, set the value of BR2_LINUX_KERNEL_CUSTOM_LOGO_PATH to the absolute path of your custom image.

Once you are satisfied with your configuration, save the file and then exit from menuconfig.

Kernel configuration

Another advantage of Buildroot is the ability to manually configure the kernel (Linux); this can be useful if you intend to enable or disable features beyond the default settings. To access this configuration interface, simply run make linux-menuconfig, which will open another configuration menu.

While for many projects adjusting the kernel is unnecessary (the default configuration is typically sufficient and will work out of the box), for the sake of this tutorial, we will look at how to remove a component. For instance, since I do not intend to use audio on my RPI Zero, I can safely disable ALSA support (CONFIG_SND):

alsa_linux.webp

Once done that, save the configuration file and then run again make all. Buildroot will download, compile and install everything we configured so far.

Init system

As you may have noticed, throughout the system configuration process we have not yet discussed one of the most critical components of any operating system: its init system. By default, Buildroot ships with the default Busybox init system which is a solid choice for an embedded device since it is small, fast and has no external dependencies. However, if you would like to use another init system (such as systemd), you can do so from System configuration -> Init system. Keep in mind that systemd requires a different file system skeleton and some larger dependencies such as dbus and udev. Therefore this configuration is not supported by this guide.

Overlay filesystem

Even after selecting all the packages required by your embedded project, none of them are actually configured yet. You could also configure everything manually after installation, but what if you are expected to ship the system with a predefined SSH configuration or a specific netfiler ruleset?

To address this kind of problem, Buildroot provides an elegant mechanism for managing configuration files and startup scripts: overlays. An overlay is simply a directory that mirrors the structure of the target root filesystem. Any files placed there will be copied into the final image during the build process.

For example, if you want to place a file inside the /etc/ssh/ directory, you only need to create the following structure:

overlayfs └── etc └── ssh └── sshd_config 2 directories, 1 file

Buildroot will copy this file into the image according to the absolute path of the overlay you have specified on the BR2_ROOTFS_OVERLAY value inside the Buildroot menu. Once it has been registered, we can start tweaking the default settings of the system. We will start from OpenSSH:

$ mkdir -p $OVERLAY_FS/etc/ssh $ cp -R output/target/etc/ssh/sshd_config $OVERLAY_FS/etc/ssh $ vim $OVERLAY_FS/etc/ssh/sshd_config # Edit ssh server

By default, OpenSSH does not permit remote root login. If, however, you require this feature for the first boot, you can enable it by adding PermitRootLogin yes to the sshd_config file.

Another thing that we need to take care is the fact that the default SSH init script kills active connection during restart actions (that is, when you invoke /etc/init.d/S50sshd restart). To avoid this, we can create the following directories inside the overlay:

$ mkdir -p $OVERLAY_FS/etc/init.d $ cp -R output/target/etc/init.d/S50sshd $OVERLAY_FS/etc/init.d

And then edit the SSH configuration file (vim $OVERLAY_FS/etc/init.d/S50sshd) like so:

... 20 stop() { 21 printf "Stopping sshd: " 22 killall sshd 23 rm -f /var/lock/sshd 24 echo "OK" 25 } 26 restart() { 27 kill -HUP $(cat /var/run/sshd.pid) 28 echo "OK" 29 }

By doing this, the SSH daemon will be restarted without killing out the active sessions.

Let's now move to configure Wi-Fi connection. By default, the kernel does not load the module for the RPI Zero network card. Thus, we need to do so manually from the inittab file:

$ cp -R output/target/etc/inittab $OVERLAY_FS/etc $ vim $OVERLAY_FS/etc/inittab # Edit inittab file

By adding the following configuration:

16 # Startup the system 17 ::sysinit:/bin/mount -t proc proc /proc 18 ::sysinit:/bin/mount -o remount,rw / 19 ::sysinit:/bin/mkdir -p /dev/pts /dev/shm 20 ::sysinit:/bin/mount -a 21 ::sysinit:/sbin/swapon -a 22 null::sysinit:/bin/ln -sf /proc/self/fd /dev/fd 23 null::sysinit:/bin/ln -sf /proc/self/fd/0 /dev/stdin 24 null::sysinit:/bin/ln -sf /proc/self/fd/1 /dev/stdout 25 null::sysinit:/bin/ln -sf /proc/self/fd/2 /dev/stderr 26 ::sysinit:/bin/hostname -F /etc/hostname 27 ::sysinit:/sbin/modprobe brcmfmac 28 # now run any rc scripts 29 ::sysinit:/etc/init.d/rcS

Then, we can configure the network interface:

mkdir -p $OVERLAY_FS/etc/network cp -R output/target/etc/network/interfaces $OVERLAY_FS/etc/network vim $OVERLAY_FS/etc/network/interfaces # Edit the interfaces

by adding this:

# interface file auto-generated by buildroot auto lo iface lo inet loopback auto wlan0 iface wlan0 inet dhcp pre-up wpa_supplicant -B -Dnl80211 -iwlan0 -c/etc/wpa_supplicant.conf post-down killall -q wpa_supplicant wait-delay 15 udhcpc_opts -t 10 iface default inet dhcp

On the next boot, the system will load the Wi-Fi module, configure the interface, execute the wpa_supplicant command using /etc/wpa_supplicant.conf and, finally, negotiate an IP address using DHCP.

Serial communication

Debugging an embedded system can be very tricky, especially when you have to figure out which parts of your system do not work and you do not have any output interface beside a serial port. In such cases, an UART (Universal Asynchronous Receiver-Transmitter) device is an essential tool to exchange data with your embedded device. In this guide, I will be using the Adafruit FT232H which also supports a wide range of serial protocols such as SPI, I2C, JTAG and many more. Whatever you choose to use, you will need to connect the RX and the DX pins and the usual VCC and GND to power up the Raspberry Pi Zero. In other words, you should replicate the following wiring:

pcb_wiring.webp

Once the circuit is configured, connect the UART device to your computer using the USB interface, open up a terminal and run the following command:

$ screen /dev/ttyUSBX 115200 # for GNU/Linux $ screen /dev/tty.usbserial-XXXXX 115200 # for macOS

Be sure to replace the X with the correct device. If you are on Windows, you can use GNU screen through PuTTY or by using WSL. The second parameter of the command is the baud rate, that is the number of symbols per seconds. If you didn't change it from the Buildroot menu, it should be set at 115200 by default.

Initial configuration

Once serial communication is ready and the device is powered on, you should start to receive the system logs on your terminal:

Peek-2020-12-01-00-38.gif

Once the system has loaded, you should be able to log in using the root account with the password you have set on the Buildroot configuration. Note that, on the first boot, the Wi-Fi connection will fail since no network has been configured yet. This will hang the system for about 10 seconds (after that, udhcpc will desist from trying to negotiate an IP address).

Let's configure a wireless connection:

root@rpi:~ wpa_passphrase "SSID_NAME" "NETWORK_PW" >> /etc/wpa_supplicant.conf

Be sure to delete any other occurrence of the network entry inside the wpa_supplicant.conf file. You should have something like this:

ctrl_interface=/var/run/wpa_supplicant ap_scan=1 network={ ssid="Router1" #psk="a_bad_pw" psk=XXX }

After a reboot, the Raspberry should be able to connect to the network by itself, let's try to retrieve its local IP address by using this command:

root@rpi:~ ip a | grep wlan0 | grep inet | awk '{print $2}' | sed '$s/...$//'

At this point, you should be able to connect to your device using SSH.

The next step is to configure a new user. To do so, issue the following commands:

root@rpi:~ mkdir -p /home root@rpi:~ adduser -G users -s /bin/bash marco Changing password for marco New password: Retype password: passwd: password for marco changed by root

Let's then add this user to the sudoers file by running the visudo command:

## ## Runas alias specification ## ## ## User privilege specification ## root ALL=(ALL) ALL marco ALL=(ALL) ALL ## Uncomment to allow members of group wheel to execute any command # %wheel ALL=(ALL) ALL ## Same thing without a password # %wheel ALL=(ALL) NOPASSWD: ALL ## Uncomment to allow members of group sudo to execute any command %sudo ALL=(ALL) ALL ## Uncomment to allow any user to run sudo if they know the password ## of the user they are running the command as (root by default). -- INSERT -- 80,20 91%

Last but not least, we need to disable remote root login since we don't need it anymore. In order to do so, open the /etc/ssh/sshd_config file and set the PermiRootLogin option back to no.

After that, restart the SSH daemon by running /etc/init.d/S50sshd restart.