by Javier Vega
In this tutorial, I will explain how to build your own Embedded Linux Distribution for the BeagleBone Black. During my research, I have found different blogs, tutorials, and other resources about the topic, but most of them are incomplete or outdated. It was challenging for me when I started, so I hope this tutorial gives you a wider understanding of embedded linux and how to build your own.
There are many components involved to build an Embedded Linux Distribution for the BeagleBone Black or any other processor. The first component you need, is a Toolchain to compile and package applications for the BeagleBone Black. Once you have all the tools, you need a BootLoader to provide early initialization so that other programs can run. Next, you need the Linux Kernel, which is the main component of your system. Lastly, you need a Root File System that will contain programs and utilities to boot the system. It is complicated to build a root file system, so I will show you how to build one using a tool called BusyBox. Once you have all the components, you need a micro SD card to boot your binary image. The micro SD card could be of any size, but I will be using a 32GB one.
You will also need a Serial cable for the BeagleBone Black. It is very important that you have one, otherwise, you will not be able to check if your system is working. You can find more about it here: BeagleBone Black Serial
The Toolchain
is one of the most crucial parts of embedded development.
If you do not have the right tools installed on your host development machine, it can impact the success of the project.
For this reason, you need to setup a development environment with the essential tools to successfully build your Linux Distribution.
The first step is to make sure that your host machine is updated and contains the essential utilities.
Before proceeding, create a working area to place the tools and components of the system.
cd ~
mkdir EmbeddedWorkspace
cd EmbeddedWorkspace
By running the following commands, you can update your machine, install the essential tools, and other tools required for cross compilation.
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install build-essential
sudo apt-get install flex bison
sudo apt-get install lzop
sudo apt-get install u-boot-tools
Additionally, you need to install and configure git before building the linux kernel.
sudo apt-get install git
git config --global user.email "your_email"
Once the host machine is updated, the next step is to download the compiler for the BeagleBone, unzip it, and export it.
mkdir arm-toolchain
cd arm-toolchain
wget -c https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
tar xf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
export ARM_CC=$(pwd)/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
Important: If you are running multiple shells, you need to export the compiler for each of the shell.
The next thing you need, is a configuration subsystem that will allow you to easily configure the Linux Kernel and BusyBox using a GUI.
There are different configuration subsystems available, but Gconfig
is one of the simplest to use.
Run the following commands to install GTK (Graphical Toolkit) dependencies:
sudo apt-get install libgtk2.0-dev
sudo apt-get install libglib2.0-dev
sudo apt-get install libglade2-dev
You also need a partition manager called Gparted
. This will allow you to partition the SD card into different sections.
Install it using the following command:
sudo apt-get install gparted
Once you have a solid working environment, the next step is to construct the BootLoader.
The BootLoader is a fundamental component in any embedded system. It plays an important role by initializing critical parts of the hardware that are necessary to get the system to a running state. There are different bootloaders available, however, I recommend using U-Boot because it is well documented and there is a vast amount of resources available. You can find the source code for U-Boot in: GitHub - u-boot/u-boot: "Das U-Boot" Source Tree.
Run the following commands to download and extract U-Boot 2018.11 into ~/EmbeddedWorkspace/
directory:
cd ~/EmbeddedWorkspace/
wget -c https://github.com/u-boot/u-boot/archive/v2018.11.tar.gz
tar -xvf v2018.11.tar.gz
rm v2018.11.tar.gz
NOTE: I am using u-boot-2018.11
because I experienced some issues with newer versions.
Next, go into u-boot directory and generate the default configuration for the BeagleBone Black using the defined configuration am335x_boneblack_defconfig
cd u-boot-2018.11
make ARCH=arm CROSS_COMPILE=${ARM_CC} am335x_boneblack_defconfig
At this stage, you should have the .config
file as shown below:
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
YACC scripts/kconfig/zconf.tab.c
LEX scripts/kconfig/zconf.lex.c
HOSTCC scripts/kconfig/zconf.tab.o
HOSTLD scripts/kconfig/conf
#
# configuration written to .config
#
NOTE: You can also modify the default configuration if necessary using gconfig:
make ARCH=arm CROSS_COMPILE=${ARM_CC} gconfig
Once the .config
file has been generated, you can then start to compile u-boot using the following command:
make ARCH=arm CROSS_COMPILE=${ARM_CC} -j4
Note: If you experience an issue with evp.h include: include/image.h:1101:12: fatal error: openssl/evp.h: No such file or directory
, just run apt-get install libssl-dev
.
After the BootLoader
has been compiled, run the ls
command to make sure that you have the executables shown below.
The ones you care about, are MLO
and u-boot.img
.
The MLO
file is the first stage BootLoader, and u-boot.img
is the second stage BootLoader also known as the Bootstrap Loader.
~/EmbeddedWorkspace/u-boot-2018.11$ ls
api Documentation lib scripts u-boot.img
arch drivers Licenses spl u-boot.lds
board dts MAINTAINERS System.map u-boot.map
cmd env Makefile test u-boot-nodtb.bin
common examples MLO tools u-boot.srec
config.mk fs MLO.byteswap u-boot u-boot.sym
configs include net u-boot.bin
disk Kbuild post u-boot.cfg
doc Kconfig README u-boot.cfg.configs
Later, you will need to copy MLO
and u-boot.img
to the boot partition of the SD card. Go to Boot Partition section for more information.
However, we can leave the BootLoader
for now and move into building the Linux Kernel
.
At this stage, you should have a workspace with two folders: the arm-toolchain
and the u-boot-2018.11
.
In this section, I will show you how to configure the Linux Kernel
for the BeagleBone Black
, add a custom device driver, and compile the driver to be part of your kernel. The github source code can be found here: BeagleBone Linux Kernel
In the EmbeddedWorkspace
directory, clone the BeagleBone Black Linux Kernel source code and place it in the directory elinux
.
Note: This will take some time since the linux kernel is huge.
cd ~/EmbeddedWorkspace/
git clone https://github.com/beagleboard/linux.git elinux
Once you have the source code, you can customize the Linux Kernel based on what you are building.
I have created a character device driver that will display a welcome message in Morse Code on LED3
.
Create a directory to place the driver using this command: mkdir ~/EmbeddedWorkspace/elinux/drivers/char/morse_code
Next, copy the source code into the new directory, so that the driver becomes part of the kernel when compiled.
~/EmbeddedWorkspace/elinux/drivers/char$ ls
agp generic_nvram.c misc.c powernv-op-panel.c tb0219.c
apm-emulation.c hangcheck-timer.c morse_code ppdev.c tile-srom.c
applicom.c hpet.c mspec.c ps3flash.c tlclk.c
applicom.h hw_random mwave random.c toshiba.c
bfin-otp.c ipmi nsc_gpio.c raw.c tpm
bsr.c Kconfig nvram.c rtc.c ttyprintk.c
ds1302.c lp.c nwbutton.c scx200_gpio.c uv_mmtimer.c
ds1620.c Makefile nwbutton.h snsc.c virtio_console.c
dsp56k.c mbcs.c nwflash.c snsc_event.c xilinx_hwicap
dtlk.c mbcs.h pc8736x_gpio.c snsc.h xillybus
efirtc.c mem.c pcmcia sonypi.c
Add to the Kconfig file in ~/EmbeddedWorkspace/elinux/drivers/char/
the following:
config MORSE_CODE
tristate "MORSE_CODE"
default y
help
Select this option to include the morse code driver.
Once the Kconfig file has been modified, add the driver to the Makefile to build it into the kernel.
Append obj-$(CONFIG_MORSE_CODE) += morse_code/
at the end of the Makefile in ~/EmbeddedWorkspace/elinux/drivers/char/
directory.
Finally, create a new Makefile in ~/EmbeddedWorkspace/elinux/drivers/char/morse_code
directory and append the following: obj-$(CONFIG_MORSE_CODE) += MorseCode.o
.
Navigate to the root directory of the linux source code ~/EmbeddedWorkspace/elinux/
and run the following command to create the default configuration for the BeagleBone Black
:
sudo make ARCH=arm CROSS_COMPILE=${ARM_CC} bb.org_defconfig
Now that you have a default configuration, run gconfig to add or remove any feature from the kernel.
sudo make ARCH=arm CROSS_COMPILE=${ARM_CC} gconfig
Once you run the previous command, you should see the device driver under Device Drivers
> Character devices
.
Use Y
to make the driver part of your kernel, M
to include your driver to be loaded at later time, or N
if you do not want to included it.
Build the kernel and the device binary trees by running the following command:
sudo make ARCH=arm CROSS_COMPILE=${ARM_CC} uImage dtbs LOADADDR=0x80008000 -j4
You should see this output once the uImage
is ready.
AS arch/arm/boot/compressed/piggy.o
LD arch/arm/boot/compressed/vmlinux
OBJCOPY arch/arm/boot/zImage
Kernel: arch/arm/boot/zImage is ready
UIMAGE arch/arm/boot/uImage
Image Name: Linux-4.14.108+
Created: Thu Dec 5 09:41:41 2019
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 10035712 Bytes = 9800.50 KiB = 9.57 MiB
Load Address: 80008000
Entry Point: 80008000
Kernel: arch/arm/boot/uImage is ready
After compiling the kernel, build all the modules as follow:
sudo make ARCH=arm CROSS_COMPILE=${ARM_CC} -j4 modules
Note: We will need to install the modules in the root file system later in this section: Install Kernel Modules
Before building the Root File System
, you need to prepare the micro SD card.
Building the Root File System
directly into the SD card worked 100% of the time for me.
I had issues when building it in the workspace folder and copying it to the SD card.
You need to create two partitions in the micro SD card, one partition for booting the kernel, and another one for the Root File System
.
Open gparted
and select the SD card.
⚠️ Warning: Make sure that you have the SD card selected because by default your hard driver could be selected. Make sure you have selected the SD card, otherwise you could partition your hard drive and damage it.
If the SD card has any partition, delete it. Next, create a new partition with 50 MiB
for the size. Change the file system type to fat32
and label it boot
.
Add a second partition with 1000 MiB
for the size, label it rootfs
, and make sure that the file type is ext4
.
After these changes, flag the first partition as boot.
Important: If you do not add a boot flag to the first partition, the system might not boot from your SD card.
At this moment, you should have the BootLoader
, the Linux Kernel
, and the SD card ready.
Now, you can start to build the Root File System
using BusyBox.
BusyBox is a tool that provides minimalist replacements for most of the UNIX utilities.
You need to configure, and build BusyBox before installing it on the rootfs
partition of your SD card.
Navigate to your workspace directory ~/EmbeddedWorkspace/
, download, and extract BusyBox
.
cd ~/EmbeddedWorkspace/
wget -c https://github.com/mirror/busybox/archive/1_30_0.tar.gz
tar -xvf 1_30_0.tar.gz
rm 1_30_0.tar.gz
At this point, the workspace should look as follows:
~/EmbeddedWorkspace$ ls
arm-toolchain busybox-1_30_0 elinux u-boot-2018.11
Once you download and extract BusyBox
, you need to configure it and add the tools that you want in your system.
cd busybox-1_30_0/
make ARCH=arm CROSS_COMPILE=${ARM_CC} defconfig
Then, run gconfig and select Build static binary (no shared libs)
under Build Options
.
make ARCH=arm CROSS_COMPILE=${ARM_CC} gconfig
Build BusyBox and install it into the rootfs partition of your SD card.
sudo make ARCH=arm CROSS_COMPILE=${ARM_CC} CONFIG_PREFIX=/media/<USER>/rootfs/ install
Important: USER is the name of you computer.
Run uname -n
to find yours.
Once it is finished, you should see some directories in rootfs
.
cd /media/<USER>/rootfs
/media/<USER>/rootfs$ ls
bin linuxrc sbin usr
Note: Inside the bin/
directory you should an executable called busybox
and many utilities linked to busybox.
Next, you need to create some important directories and files required by the Linux Kernel
to boot the system.
mkdir dev
sudo mknod dev/console c 5 1
sudo mknod dev/null c 1 3
sudo mknod dev/zero c 1 5
Copy the libraries from the arm toolchain into the root file system lib.
mkdir lib usr/lib
sudo cp -r ~/EmbeddedWorkspace/arm-toolchain/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib/* ./lib/
sudo cp -r ~/EmbeddedWorkspace/arm-toolchain/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib/* ./usr/lib/
sync
Create additional directories for mounting virtual file systems.
mkdir proc sys root
Create the etc/
and etc/init.d
directories and add some important files.
sudo mkdir etc etc/init.d
After the kernel boots, it spawns the first user process called the init
.
This process requires a configuration file called etc/inittab
, which contains actions the system needs to perform at a given runlevel.
Good to Know: init
is the first user space process to run and it is the mother of all the processes in Linux.
It runs as a background process unitil the system shuts down.
Create a new file called etc/inittab
:
sudo nano etc/inittab
Copy the following into the etc/inittab
file:
::sysinit:/etc/init.d/rcS
# /bin/ash
#
# Start an "askfirst" shell on the serial port
console::askfirst:-/bin/ash
# Stuff to do when restarting the init process
::restart:/sbin/init
null::sysinit:/bin/mount -a
null::sysinit:/bin/hostname -F /etc/hostname
null::respawn:/bin/cttyhack /bin/login root
null::restart:/sbin/reboot
sudo nano etc/fstab
Copy the following into etc/fstab
:
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
Create the hostname sudo nano etc/hostname
and write your hostname.
Then, create passwd sudo nano etc/passwd
and copy root::0:0:root:/root:/bin/sh
into it.
Create the file init.d/rcS
to setup the system.
Run sudo nano etc/init.d/rcS
to create the file and append the following:
#!/bin/sh
# ---------------------------------------------
# Common settings
# ---------------------------------------------
HOSTNAME=<YOUR_HOSTNAME>
VERSION=1.0.0
hostname $HOSTNAME
# ---------------------------------------------
# Prints execution status.
#
# arg1 : Execution status
# arg2 : Continue (0) or Abort (1) on error
# ---------------------------------------------
status ()
{
if [ $1 -eq 0 ] ; then
echo "[SUCCESS]"
else
echo "[FAILED]"
if [ $2 -eq 1 ] ; then
echo "... System init aborted."
exit 1
fi
fi
}
# ---------------------------------------------
# Get verbose
# ---------------------------------------------
echo ""
echo " System initialization..."
echo ""
echo " Hostname : $HOSTNAME"
echo " Filesystem : v$VERSION"
echo ""
echo ""
echo " Kernel release : `uname -s` `uname -r`"
echo " Kernel version : `uname -v`"
echo ""
# ---------------------------------------------
# MDEV Support
# (Requires sysfs support in the kernel)
# ---------------------------------------------
echo -n " Mounting /proc : "
mount -n -t proc /proc /proc
status $? 1
echo -n " Mounting /sys : "
mount -n -t sysfs sysfs /sys
status $? 1
echo -n " Mounting /dev : "
mount -n -t tmpfs mdev /dev
status $? 1
echo -n " Mounting /dev/pts : "
mkdir /dev/pts
mount -t devpts devpts /dev/pts
status $? 1
echo -n " Enabling hot-plug : "
echo "/sbin/mdev" > /proc/sys/kernel/hotplug
status $? 0
echo -n " Populating /dev : "
mkdir /dev/input
mkdir /dev/snd
mdev -s
status $? 0
# ---------------------------------------------
# Mount the default file systems
# ---------------------------------------------
echo -n " Mounting other filesystems : "
mount -a
status $? 0
# ---------------------------------------------
# Set PATH
# ---------------------------------------------
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin
# ---------------------------------------------
# Start other daemons
# ---------------------------------------------
echo -n " Starting syslogd : "
/sbin/syslogd
status $? 0
echo -n " Starting telnetd : "
/usr/sbin/telnetd
status $? 0
# ---------------------------------------------
# Starts Sending Morse Code in User LED3
# MorseCode Driver runs as a background process
# ---------------------------------------------
echo "Starting Morse Code in USR3 LED\n"
echo none > /sys/class/leds/beaglebone\:green\:usr3/trigger
echo "Welcome to Embedded Linux" > /dev/MorseCode
# ---------------------------------------------
# Done!
# ---------------------------------------------
echo ""
echo "System initialization complete."
Once you have created this file, you need to assign execution privileges.
Run chmod +x etc/init.d/rcS
At the end your file system should look as follow:
/media/<USER>/rootfs$ ls
bin etc linuxrc proc sbin usr
dev lib lost+found root sys
After creating the above files and directories, navigate to ~/EmbeddedWorkspace/elinux
and install the kernel modules into the Root File System
.
cd ~/EmbeddedWorkspace/elinux/
make ARCH=arm CROSS_COMPILE=${ARM_CC} INSTALL_MOD_PATH=/home/<USER>/rootfs/ modules_install
At this stage, the rootfs
partition of the SD card should be ready with your new Root File System
.
The next step is to prepare the BOOT
partition for booting the Linux Kernel
.
Create a boot folder in your workspace directory and copy MLO
, u-boot.img
, uImage
, and am335x-boneblack.dbt
.
cd ~/EmbeddedWorkspace/
mkdir boot
cp u-boot-2018.11/MLO boot/
cp u-boot-2018.11/u-boot.img boot/
cp elinux/arch/arm/boot/uImage boot/
cp elinux/arch/arm/boot/dts/am335x-boneblack.dtb boot/
Go into the boot
folder and create a file called uEnv.txt
.
This file will tell the BootLoader
where to load the kernel image and the device binary tree.
In addition, it should have the arguments that will be passed to the Linux Kernel
.
cd boot/
gedit uEnv.txt
Copy the following into uEnv.txt
uenv_addr=0x81000000
load_addr=0x82000000
dtb_addr=0x88000000
mmc_args=setenv bootargs console=ttyO0,115200n8 noinitrd root=/dev/mmcblk0p2 rw rootfstype=ext4 rootwait
sdboot=echo Booting from SD Card ...;mmc rescan;fatload mmc 0:1 ${uenv_addr} uEnv.txt;env import -t ${uenv_addr} $filesize;fatload mmc 0:1 ${load_addr} uImage;fatload mmc 0:1 ${dtb_addr} am335x-boneblack.dtb;run mmc_args;bootm ${load_addr} - ${dtb_addr}
uenvcmd=run sdboot
Important: Leave a newline at the end of the uEnv.txt
file.
Finally, copy everything from the ~/EmbeddedWorkspace/boot/
directory to the BOOT
partition of your SD card.
cd ~/EmbeddedWorkspace/boot/
sudo cp * /media/<username>/BOOT/
sync
Once your SD card is ready, put it into the BeagleBone Black
and boot from it.
You need to use a BeagleBone Black Serial Cable to see the results.
After you boot up the board, the system should mount the root file system successfully and you should automatically be logged in as root #
.
Besides the Serial output, you should see LED3
on the BeagleBone Black
blink at different rates.
Building Embedded Linux from Scratch