How to Build a Linux Distribution for the BeagleBone Black

by Javier Vega | December 10, 2019


Introduction

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 from scratch, 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 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, however, I will show you how to build a simple one using a tool called BusyBox. Once you have all the components, you will 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.

BeagleBone Black Serial Cable

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

Toolchain

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 up to date and that it 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"

BeagleBone Black Compiler

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.

Gconfig Dependencies

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

Partition Manager

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 steps is to construct the BootLoader.

BootLoader

The BootLoader is a fundamental component in any embedded system. It plays an important role, 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.

U-Boot

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.

Configure U-Boot

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, if 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

Compile U-Boot

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 run into 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.

Linux Kernel

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

Adding a New Device Driver

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.

Kernel Configuration for BeagleBone Black

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

You should see the device driver under Device Drivers > Character devices. To select the driver to be part of the kernel set it to Y, be loaded at later time set it to M, or if you do not want to included it use N. Gconfig

Build the Kernel

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 latter in this section: Install Kernel Modules

Preparing the MicroSD Card

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 ran into 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, otherwise, you could partition your hard drive.

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. Gparted

Root File System

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 follow:

~/EmbeddedWorkspace$ ls
arm-toolchain  busybox-1_30_0  elinux  u-boot-2018.11

Configure BusyBox

Once you download and extract BusyBox, you then 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

Busy Box

Build BusyBox

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 will need to create some important directories and files required by the Linux Kernel to boot the system.

dev/ Directory

mkdir dev
sudo mknod dev/console c 5 1
sudo mknod dev/null c 1 3
sudo mknod dev/zero c 1 5

lib/ and usr/lib/ Directories

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

etc/ Directory

Create the etc/ and etc/init.d directories and add some important files.

sudo mkdir etc etc/init.d

etc/inittab

After the kernel boots, it spawns the first user process, called the init. init is a background process that runs unitil the system is shut down. This process requires a configuration file called etc/inittab, which contains actions the system needs to perform at a given runlevel.

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

etc/fstab

sudo nano etc/fstab

Copy the following into etc/fstab:

proc    /proc    proc   defaults  0 0
sysfs   /sys     sysfs  defaults  0 0

etc/hostname and etc/passwd

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.

init.d/rcS Directory

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

Install Kernel Modules

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.

Boot Partition

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/

uEnv.txt

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.

Final Result

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. Booting The System

References

Dr. Alexander Perez-Pons

Embedded Linux Primer

Building Embedded Linux from Scratch

Boot Beaglebone Black in 60 Minutes

Booting Linux Problems

Root File System for OMAP35x

Misc