Skip to content

Initial Setup

Introduction

In this lab, you will complete the following objectives:

  1. Install the software development environment for the target system on your host machine. The various software to install and configure are:
    • Git
    • Docker
    • Visual Studio Code (VSCode) along the Dev Container extension
    • balenaEtcher
    • Serial communication
  2. Prepare the containerized development environment for the target system
  3. Install and configure Buildroot for the NanoPI Neo Plus2
  4. Build a SD card image using Buildroot and flash it
  5. Connect the target system to the host
  6. Successfully boot Linux and obtain a shell

Git

You will use git to manage the source code of the various labs

Git configuration

  • Configure git with the following commands:

    git config --global user.name "Your name"
    git config --global user.email "Your school email address"
    

  • Example for Jane Doe:

    git config --global user.name "Jane Doe"
    git config --global user.email "jane.doe"@hes-so.ch"
    

  • On Windows, make sure to configure this as well (here is why):
    git config --global core.autocrlf false
    

Docker

We will use Docker and Compose to provide a Linux environment that’s independent from the host operating system

  • On Linux, install Docker and Compose using your distribution’s package manager, e.g. on Ubuntu/Debian:
    sudo apt-get install docker.io docker-compose-v2
    
  • On Windows and OSX, install Docker Desktop instead (which is based on a Linux Virtual Machine)

Visual Studio Code

Visual Studio Code (VSCode) is a cross-platform integrated development environment (IDE) that you’ll use for most of your development

  • It can be downloaded from https://code.visualstudio.com/

  • If you use GNU/Linux, it can be installed from your distribution’s package manager (or through snap with snap install code --classic)

Dev Containers for VSCode

This VSCode extension automatically handles Docker images and containers within VSCode, hiding most of the complexity of using Docker. This extension reads the configuration present in a .devcontainer which describes what image(s) to create and possibly what container(s) to run.

Here is how to install the extension:

  • Launch VSCode and go to the “Extensions” pane on the left
  • Search for “Dev Containers” from Microsoft and install it

After installation, you can simply use VSCode to open a directory and if the extension finds a “.devcontainer” directory, asks you to reopen it in a container (thus building the corresponding image(s) and running the container(s)).

Here is the documentation about the extension.

balenaEtcher

balenaEtcher is a user-friendlay tool to flash SD cards.

  • If you’re a Unix/Linux fan, be a warrior and use dd instead 🥷🏼

  • balenaEtcher can be installed from https://etcher.balena.io/

Serial communication

From your host computer, a serial interface program will be required to communicate with the target system.

For Linux and Mac users, a simple and popular software is picocom. To install it on Ubuntu/Debian:

sudo apt-get install picocom

For Windows, you can use either PuTTY or TeraTerm.

Prepare the development environment

The various resources needed for the labs, including the containerized development environment, are available in the following git repository: https://github.com/MA-SeS/resources

The development environment used for the course’s labs is containerized in order to be portable across various operating systems (GNU/Linux, Windows, Mac).

Clone the resources repository above and use VSCode to open the labs directory. VSCode’s Dev Containers’ extension will detect the .devcontainer directory and show a popup with “Reopen folder to develop in a container”. Click on reopen in order to launch a container in the development environment. Then, use VSCode to open a new terminal and you’ll notice that you are now in the /workspace directory in the development environment’s container. You can see that this /workspace directory is actually mapped to the content of your host’s machine’s labs directory.

This workspace directory will be the base directory for the course’s labs.

Install Buildroot

We will use Buildroot to generate the cross-compilation toolchain, the U-Boot bootloader, the Linux kernel, the root filesystem (rootfs) and the final disk image to flash to a SD card.

Download Buildroot’ source code from its git repository (cf. course’ slides). You can list the various tagged versions with git tag. Use Buildroot’s 2022.08.3 version by creating a new “ses” branch for it. This specific version is known to work well with our target platform, the NanoPi Neo Plus 2.

Configure Buildroot

To better support our target, a few extra config files must be copied to Buildroot’s file tree. Moreover, make sure to copy the dedicated “ses” Buildroot configuration present in the git. To do so, proceed by copying the files present in resources inside the buildroot’s directory as shown below:

  • Copy the file tree in resources/buildroot/board/ into buildroot/board/
  • Copy the config file in resources/buildroot/configs/ into buildroot/configs/

Then, load the new ses defconfig you just copied.

Run Buildroot’s menuconfig target and browse through the various menus to ensure that the configuration you loaded is indeed the one for the NanoPi Neo Plus 2.

You can also inspect the various menus and options to get an idea of the possibilities offered by Buildroot.

Compile Buildroot

Once convinced Buildroot’s configuration is correct, start compiling it for the target system. Beware this can take quite a while (between 30min to an hour!). Use the -j argument with make to spawn several compilation processes in order to take advantage of multi-core CPU architectures (your laptop is likely to feature multiple processors).

After the build process is completed, can you identify which directory contains:

  1. The downloaded packages (tarballs)?
  2. The tools for the host, in particular the cross-compilation toolchain (e.g. gcc for the target platform)?
  3. The packages’ source and compiled files?
  4. The generated binaries and images: Secondary Program Loader (SPL), U-Boot, DTB/FDT, Linux kernel, rootfs, and final SD card image

Among the generated files, the most interesting one is sdcard.img. It is a disk image meant to be flashed on a SD card. Locate it in the generated file hierarchy.

Rootfs image creation

When Buildroot creates the rootfs, it uses the skeleton file hierarchy specified in system/skeleton. This skeleton is copied to the pseudo rootfs directory output/target.

This skeleton can be customized using an overlay mechanism explained in slide “Root filesystem customization” in Buildroot lectures.

Buildroot then creates an empty ext4 filesystem image and populates it with the content of the output/target directory.

As an example, here is how to create an image of a filesystem and populate it:

# Create a 64KB image filled with zeroes
dd if=/dev/zero of=image.ext4 bs=1024 count=65536
# Create an ext4 filesystem in it
mkfs.ext4 -L myfs image.ext4
# Mount the filesystem image into /mnt
mount -o loop image.ext4 /mnt
# Copy files to the ext4 filesystem
cp -r files /mnt
# Umount the image
umount /mnt

Post-build and SD card creation

At the end of the build, but before creating filesystem images, Buildroot executes post-build.sh. This scripts uses mkimage to create boot.scr which is an image of the U-Boot script boot.cmd. Here is how it uses mkimage:

mkimage -C none -A arm64 -T script -d $BOARD_DIR/boot.cmd $BUILDROOT_DIR/output/images/boot.scr

After creating filesystem images, Buildroot executes genimage.sh to build the SD Card image file, sdcard.img. This script reads parameters from board/friendlyarm/nanopi-neo-plus2/genimages.cfg.

As a reminder, both scripts are specified in Buildroot’s system configuration as explained in slide “Post build script” in Buildroot lectures.

The config file genimage.cfg describes how to create the disk image, notably what partitions to create and what files to copy and at which offset in the image:

image boot.vfat {
        vfat {
                files = {
                        "Image",
                        "sun50i-h5-nanopi-neo-plus2.dtb",
                        "boot.scr"
                }
        }
        size = 64M
}

image sdcard.img {
        hdimage {
        }

        partition spl {
                in-partition-table = "no"
                image = "sunxi-spl.bin"
                offset = 8K
        }

        partition u-boot {
                in-partition-table = "no"
                image = "u-boot.itb"
                offset = 40K
                size = 1M # 1MB - 40KB
        }

        partition boot {
                partition-type = 0xC
                bootable = "true"
                image = "boot.vfat"
        }

        partition rootfs {
                partition-type = 0x83
                image = "rootfs.ext4"
        }
}

The resulting disk image looks like this:

Nano Pi NEO Plus2 typical boot sequence

The Nano Pi NEO Plus2 typically follows the boot sequence described below.

  1. BootROM loads + executes the Secondary Program Loader (SPL) sunxi-spl.bin
  2. sunxi-spl.bin:
    • Initializes DRAM
    • Loads u-boot.bin (BL33)
    • Loads + executes bl31.bin ARM Trusted Firmware (ATF) EL3 runtime firmware
  3. bl31.bin:
    • Sets up exception levels, PSCI, secure monitor services
    • Executes u-boot.bin
  4. u-boot.bin loads + executes Linux

In the files generated in output/images by Buildroot, use dumpimage -l to inspect the content of u-boot.itb.

What kind of file is u-boot.itb and what does it contain exactly?

Inspect SD card image and flash it

Use fdisk -l on sdcard.img to obtain the image’s sector size and its partitions, notably the offset at which each partition starts. Note that the values are in sectors, thus you need to convert them into bytes by using the identified sector size.

How many partitions do you see, what are their offsets and sizes, and of what are their types?

You can mount a filesystem inside an image’s partition by using mount (as root or with sudo) on the image file with a specific offset and size both specified in bytes. Here is the syntax where 1234 is the partition’s offset in bytes, 5678 the size of the partition in bytes, and dir the directory where to mount the partition (assuming the filesystem type inside the partition is supported by the kernel). It’s a good idea to mount it read-only with -r to avoid modifying the filesystem:

mount -r -o loop,offset=1234,sizelimit=5678 disk.img dir`

Mount the filesystems of each partition. What are the filesystem types?

What files are present in the first partition and what is their purpose?

What’s the contents of the second partition?

Once done, don’t forget to unmount both filesystems.

You can finally use dd or balenaEtcher to flash the SD card image into the SD card.

Connect the NanoPI to your host computer

In order to use the NanoPI, you must connect to it and have it boot the micro SD card you just prepared. Make sure you have 3 USB-A ports available otherwise use a USB hub. Then, proceed with the following steps as illustrated in the figure below:

  1. Connect the network cable to the USB network adapter which you must in turn connect to your computer
  2. Connect the NanoPi serial cable to your computer and make sure the switch is in the same position as indicated on the figure below with a red circle
  3. Insert the micro SD card into the NanoPi card reader
  4. Connect the microUSB cable to your computer, which should power-on the board and start the boot process

To read and write from/to the serial interface connected to the board, use the serial communication program you installed previously. The connection speed should be 115200 bauds and on Linux at least, the serial interface to connect to on the host is /dev/ttyUSB0. Here is an example using picocom:

picocom -b 115200 /dev/ttyUSB0
This is what you should see when booting the NanoPI:
U-Boot SPL 2020.10-rc5 (Aug 30 2025 - 18:46:55 +0200)
DRAM: 512 MiB
Trying to boot from MMC1
alloc space exhausted

U-Boot 2020.10-rc5 (Aug 30 2025 - 18:46:55 +0200) Allwinner Technology

CPU:   Allwinner H5 (SUN50I)
Model: FriendlyARM NanoPi NEO Plus2
DRAM:  512 MiB
MMC:   mmc@1c0f000: 0, mmc@1c10000: 2, mmc@1c11000: 1
In:    serial
Out:   serial
Err:   serial
Net:   phy interface7
eth0: ethernet@1c30000
Hit any key to stop autoboot:  0 
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found U-Boot script /boot.scr
279 bytes read in 1 ms (272.5 KiB/s)
## Executing script at 4fc00000
39475712 bytes read in 1886 ms (20 MiB/s)
22620 bytes read in 4 ms (5.4 MiB/s)
Moving Image from 0x40080000 to 0x40200000, end=42850000
## Flattened Device Tree blob at 4fa00000
   Booting using the fdt blob at 0x4fa00000
   Loading Device Tree to 0000000049ff7000, end 0000000049fff85b ... OK

Starting kernel ...

[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034]
[    0.000000] Linux version 6.3.6 (root@f0329bcb2419) (aarch64-buildroot-linux-uclibc-gcc.br_real (Buildroot 2022.08.3-dirty) 11.3.0, GNU ld (GNU Binutils) 2.37) #1 SMP PREEMPT Sat Aug 30 18:25:27 CEST 2025
[    0.000000] Machine model: FriendlyARM NanoPi NEO Plus2
[    0.000000] efi: UEFI not found.
[    0.000000] NUMA: No NUMA configuration found
...

Once the boot process is completed, you should see this login prompt:

Welcome to FriendlyARM Nanopi NEO Plus2
buildroot login: 

You should now be able to log on into a shell (Busybox’s ash) using the root user with no password.

Check that the system booted from the SD card and that the root filesystem is mounted from the second partition (mmcblk0p2) with:

# cat /proc/cmdline 
console=ttyS0,115200 earlyprintk root=/dev/mmcblk0p2 rootwait

Finally, check that the kernel version is 6.3.6 with:

# uname -a
Linux buildroot 6.3.6 #1 SMP PREEMPT Sat Aug 30 18:25:27 CEST 2025 aarch64 GNU/Linux