At this stage, you should have a partitioned and formatted micro-SD card with u-boot loaded on the first partition. Your project directory should look something like this:
opi-image/
| - - linux/
| - - u-boot/
| - - arm-trusted-firmware/
| - - linux-firmware/
Please note that these instructions assume you are running on arm64 architecture.
Configure the Kernel
Navigate into linux/
and run the following command:
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make defconfig
If all you want is the barebones linux kernel, then skip this next part. At the time of writing, the Orange Pi Zero3’s wifi chip (either the UWE5622 or 20U5622) are not supported in the mainline kernel. The linux-sunxi community does provide a link to the firmware blobs, but I have no idea what is in them (see security concerns in Part 1) so I opted for a safer option.
I have a TP-Link TL-WN725N USB Dongle on-hand, and I found a very helpful resource called Linux Wireless that provides information on chipsets with mainline kernel support. Weirdly, I couldn’t find which chipset the TL-WN725N used in any official documentation, but after some Google sleuthing I was able to determine that my model (probably) used a Realtek chipset named RTL8188EU. Using Linux Wireless, I was able to confirm that this chipset has mainline support using the rtl8xxxu driver.
Armed with this information, we can go about manually configuring our kernel to have the necessary driver support. Run the following command:
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make menuconfig
Navigate to the following menu:
Device drivers -> Network device support -> Wireless Lan
At this menu, scroll down to “Realtek 802.11n USB wireless chips support”. Include it as a module (press M while the option is highlighted). A new option for including untested 8xxx USB devices should appear. Include this option too. It should look something like this:
Now, exit out of the menu and, when prompted, select ‘yes’ to save your configuration options.
Creating our OS
Now that the kernel is configured, we can begin the compilation process. This stage has four parts:
- Compiling the kernel image.
- Compiling the device tree binary (.dtb) file.
- Compiling the kernel modules.
- Creating our Root filesystem.
- Installing header files.
While our image, device tree binary, and modules are created with make
commands using the Linux repo’s makefile, our Root filesystem (AKA Rootfs), requires a different approach.
Note that my VM has 4 cores, so the following commands run using all 4 (-j4
). You can change the -j{value}
accordingly based on your system.
Kernel Image
To compile the Kernel Image run this command:
sudo ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make Image -j4
Device Tree
To create the device tree binary file run this command:
sudo ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make dtbs -j4
Modules
To create the modules for the kernel run this command:
sudo ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make modules -j4
Rootfs
Now we need to create rootfs which, among other things, will actually contain the modules and header files our OS will need.
To do this, we’ll use a tool called deboostrap that will allow us to ‘bootstrap’ a root filesystem onto our target partition.
As an aside - the term ‘bootstrapping’ in embedded linux/linux generally was new to me. My current understanding is that it refers to the process of setting up a minimal base system for your image to use (directories like /usr, /proc, /lib, and so on). I tried to find a more in-depth definition but the term seems a bit ubiquitous - for example, I found a posts that used bootloader and bootstrap interchangeably (which seemed wrong but I’m no expert).
Note that these instructions assume that your workspace has the same architecture as your target and you are running a Debian-based system.
First, install debootstrap:
sudo apt-get -y install debootstrap
We have to mount the second partition of our Micro-SD card to /mnt. Run the following command, replacing ‘sdX2’ with the applicable block device representing your Micro-SD card:
sudo mount /dev/sdX2 /mnt
Stage 1 of the bootstrapping process requires that we give debootstrap a destination (/mnt
), a debian release (I chose bookworm
), and our architecture (arm64
). Run this command:
sudo debootstrap --arch=arm64 --foreign bookworm /mnt/
The foreign flag tells debootstrap to download and unpack the base system packages but not to fully configure them.
To complete stage 2, first run this command:
sudo chroot /mnt /bin/sh -i
Chroot
(short for ‘Change Root’) allows us to change the root directory for the current process and its children. Importantly, it also prevents the process from accessing any directories outside of the newly designated root directory.
So, this command is telling our shell to change root to /mnt
, and open an instance of the bourne shell (/bin/sh
) in interactive mode (-i
).
The two main reasons for doing this, from what I have been able to decipher, are:
- protecting our host system from debootstrap, and
- allowing us to use host system resources (e.g. network access) to configure our new filesystem.
Ok, without further ado, lets run the second stage in our newly chrooted shell:
/debootstrap/debootstrap --second-stage
Once complete, if you run the ls
command (still in your chrooted shell) you should see a normal root directory. There are a few housekeeping steps we should take care of before we exit out:
- set a root password by running the
passwd
command. This will be how you login as root to your new linux OS. - changing the new rootfs hostname by running
nano /etc/hostname
and changing it to be different from your host machine’s hostname. - enable the serial console by running
systemctl enable [email protected]
to ensure we have serial port access on ttyS0 on boot-up. - update your file system table (fstab) to reflect the partitions we set up in part 1. Run
nano /etc/fstab
and add the following:
none /tmp tmpfs defaults,noatime,mode=1777 0 0
/dev/mmcblk0p2 / ext4 defaults 0 1
/dev/mmcblk0p1 /boot vfat defaults 0 2
- We also need to update our sources.list so that we can install the appropriate firmware for our realtek usb wifi adapter. Run
nano /etc/apt/sources.list
and add the following sources:
deb http://deb.debian.org/debian bookworm main non-free-firmware
deb-src http://deb.debian.org/debian bookworm main non-free-firmware
deb http://deb.debian.org/debian-security/ bookworm-security main non-free-firmware
deb-src http://deb.debian.org/debian-security/ bookworm-security main non-free-firmware
deb http://deb.debian.org/debian bookworm-updates main non-free-firmware
deb-src http://deb.debian.org/debian bookworm-updates main non-free-firmware
Finally, it’d be good to have a few network tools installed, so while we have our host’s internet connection you can run the following command:
apt-get install -y network-manager wpasupplicant iw usbutils
After, run the clean command to remove cached packages:
apt-get clean
Also, remove the resolv.conf file inherited from the host machine:
rm /etc/resolv.conf
Once this is complete, you can exit the chroot environment by typing exit
. All done!
Install Modules
Earlier, we compiled our OS’s modules but didn’t actually install them anywhere. With our rootfs now configured (and still mounted at /mnt
), lets install them so that your kernel image will be able to make use of them. Ensuring you are back in your /linux
directory, run the following command:
sudo ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- INSTALL_MOD_PATH=/mnt make modules modules_install
Install Header Files
Linux header files act as your system’s API for user-space programs to access certain kernel level resources. Now that we have our rootfs set up, we can install these as well. Run the following command:
sudo ARCH=arm64 INSTALL_HDR_PATH=/mnt/usr/ make headers_install
In my case, even setting INSTALL_HDR_PATH didn’t guarantee that the headers would be installed properly. I copied the local header files that are generated by the above command into /mnt/usr/ manually as follows:
sudo cp -r usr/include/ /mnt/usr/
Linux Firmware
In my case, the USB Wifi dongle requires firmware in addition to the modules we just installed. Navigate to the opi-image/
directory. We need to copy the rtlwifi firmware into our rootfs so that our system will have access to it. Run the following command:
sudo cp -r linux-firmware/rtlwifi/ /mnt/lib/firmware/
Later, when we run the kernel module for our USB wifi dongle, our system will check /lib/firmware/
for the binaries that it needs in order to run properly.
Next Steps
Now that the rootfs is configured, modules have been installed, and the header files are installed, it’s time to set up the boot partition.
Copy Image and Device Tree
First things first, unmount /dev/sdX2
from /mnt
:
sudo umount /mnt
And mount /dev/sdX1
:
sudo mount /dev/sdX1 /mnt
We now need to copy the kernel and device tree binary into the boot partition. Ensure that you are still in your linux/
directory. Run the following command to copy the kernel image file to the boot partition:
sudo cp arch/arm64/boot/Image /mnt
Run the following command to copy the device tree binary to the boot partition:
sudo cp arch/arm64/boot/dts/allwinner/sun50i-h618-orangepi-zero3.dtb /mnt/device_tree.dtb
Note that this command renames the .dtb file. I did this to save some typing later on, but it is not mandatory.
Create Boot Script
If you were to try booting up with your Micro-SD card as-is, you’d see u-boot begin it’s set-up and then…nothing (or, more likely, some sort of error or warning). This is because we haven’t told u-boot which files to load or where anything is. Now, if you want to interrupt the auto-boot process and manually enter the necessary commands in each time you boot, you can. To make life easier though, let’s write a boot script.
First, navigate to opi-image/
and open a .txt file. I called mine boot.txt
:
vim boot.txt
In your .txt file, enter the following:
setenv bootargs root=/dev/mmcblk0p2
load mmc 0:1 $kernel_addr_r Image
load mmc 0:1 $fdt_addr_r device_tree.dtb
booti $kernel_addr_r - $fdt_addr_r
Save and exit your .txt file. Before we convert this to a boot script, let’s first break down what’s happening:
- We tell u-boot which partition our rootfs is located on.
- We load our kernel Image file from the multimedia card’s first partition into memory at the memory address stored in $kernel_addr_r
- We load our device tree binary from the mmc’s first partition into memory at the address stored in $fdt_addr_r
- We run the booti command (for booting Image files), passing in the kernel address first and then the device tree address.
A few important points:
- $kernel_addr_r and $fdt_addr_r were predefined variables in my u-boot version. If you’re unsure if your system has them, you can run the
printenv
command while in the u-boot environment and check - I was curious about the syntax for the
booti
command. I first though it was saying “boot the files located between X and Y” because of the dash. However, it looks like that’s actually just used as a way to demarcate separate arguments. We’re actually saying “Boot the image located at X, where the device tree is located at Y”.
Now let’s convert the .txt file into a script file. We can use the u-boot tool mkimage
to do this by running the following command:
u-boot/tools/mkimage -C none -A arm -T script -d boot.txt boot.scr
Now copy the newly-made boot.scr file to the boot partition:
sudo cp boot.scr /mnt
That’s it! You can now unmount the boot partition and eject your Micro-SD card:
sudo umount /mnt
sudo eject /dev/sdX
Insert the Micro-SD Card into the Zero3, power it on, and watch the custom linux OS boot. Once booted, you should be able to login as root
using the password set during the debootstrap process.
Hopefully you found this to be a helpful guide. Thanks for reading!
ETA (Mar 26 2024)
I realized after publishing that I hadn’t gone through how to enable the module support necessary to use the USB Wifi Dongle. These instructions assume you are logged in as root on your Zero3.
You can view your running modules by running:
lsmod
Check which chipsets you have module support for:
ls /lib/modules/$(uname -r)/kernel/drivers/net/wireless/
When I run this command, I see the following output:
user@orange:~$ ls /lib/modules/$(uname -r)/kernel/drivers/net/wireless/
ath broadcom marvell mediatek realtek rsi ti
I’m looking for the rtl8xxxu driver, which supports certain realtek chipsets, so when I run ls
in the realtek subdirectory, I see the following:
rtl818x rtl8xxxu rtlwifi
Now that I have confirmed that I have the required module, I can load it by running the following command:
modprobe rtl8xxxu
That should be it! You can confirm that it loaded properly buy running the lsmod
command again, but now your USB WiFi dongle should be automatically recognized by your OS when plugged in. You can check that it is working properly by running ip a
, the output of which should include a new wlan device.