Piero V.

Alpine Linux on the Orange Pi Zero

Motivation

A few years ago, I bought an Orange Pi Zero to run a Telegram bot I use to open and close my garage door.

It worked very well for several years. Then, when Debian bookworm became stable, I decided to reinstall the system, and the trouble began.

After more than one month of frequent crashes, I first tried to upgrade the kernel. However, apt was unusable, and the system reinstallation was my only choice. So, I decided to also check other distributions out.

First, I tried with OpenWrt. It was a good setup, except I could not use GPIO through /dev/mem. This was a big problem because memory-GPIO is more versatile and performant.

So, I decided to also try Alpine Linux. Before this adventure, I had used this distribution only for containers, but I felt this could also be a case in which it can shine. But the effort it took me to get it working as I wanted reminded me of the old days 😅️. It also made me appreciate how easy setting up Debian is these days.

Notice: so far, I have always managed to install the other systems on a USB thumb drive. I tried also with Alpine, but I had some problems with its initial ramdisk. After several hours of trial and error, I gave up and switched to a microSD.

The bootstrap image

The first challenge was finding the correct architecture to use: armv7. Alpine has also armhf, but it is only for old Raspberry Pi 1 and 2 that have an armv6 processor. It is the opposite of the Debian naming scheme 😂️.

Then, I decided to start with the mini root filesystem aimed at containers. They also offer generic ARM images, which include U-Boot but run from RAM, from what I understood. I did not need it because I have U-Boot on the small soldered flash and preferred a persistent system.

Therefore, I downloaded the tarball with the minimal filesystem and chrooted into it. You can also do it from a machine of another architecture, including x86_64, provided you have the QEMU binaries for user-mode emulation.

If you do that from the Orange Pi itself, be sure its clock is more or less correct since you will need to download stuff over HTTPS. You can check it with the date command and set it with date -s YYYYMMDDHHmm (it does not need to be very precise, just enough to make the TLS validation succeed).

Kernel and init system

Container images include only the packages to get a basic system to run a specific service. Therefore, they contain neither a kernel nor an init system. But we need them because we are running on bare metal.

So, we can install them with this command:

apk add linux-lts linux-firmware-none openrc

The first two packages are the kernel. linux-firmware-none avoids us installing hundreds of MB, close to a GB, of actual firmware.

Alpine uses OpenRC as its init system. Creating services with OpenRC is very easy, probably easier than systemd. For example, here is my Telegram bot’s service file:

#!/sbin/openrc-run
command="/usr/bin/node"
command_args="bot.mjs"
command_background=yes # My node script does not fork by itself
directory="..." # The working directory for the service
pidfile="/run/${RC_SVCNAME}.pid" # Required, from what I understood
command_user="nobody:nogroup" # Drop privileges for the JS stuff
name="my-bot"
description="My Telegram bot"

The OpenRC package depends also on ifupdown-ng, a daemon to configure the network with the good old /etc/network/interfaces. However, it is not enabled by default, so we need to run this command:

rc-update add networking boot

Also, its default configuration is empty, so we need to populate it. My interfaces file looks like this:

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
	address your-ip-address/netmask
	gateway your-gateway-address
	dns-nameservers your-dns-address

Then, we need a replacement of udev to keep /dev updated, mount devpts, create /dev/shm, and other tasks. Alpine’s busybox includes mdev, but we need to install its integration with OpenRC to start it automatically:

apk add mdevd-openrc
rc-update add mdevd
rc-update add mdevd-init

Finally, we should now add a simple /etc/fstab. This is mine:

LABEL=orangepi / ext4 noatime 0 1

SSH and user setup

So far, we have installed everything we needed to get a working system. We could even install some services, and they would work as expected.

However, we do not have a way to interact with the system.

First, we only have the root user, but it does not have a password. We should add one with passwd root. You can create additional unprivileged users with the adduser command.

Second, we should install an SSH server, or the serial console will be the only way to log in to the system 😄️.

This is an embedded system, so we could install Dropbear, like OpenWrt, but this board should be powerful enough, therefore I installed OpenSSH:

apk add openssh-server
rc-update add sshd

If you want to allow root to log in over SSH, you should edit /etc/ssh/sshd_config and set PermitRootLogin to prohibit-password or to yes, depending on whether you want to login with your SSH key, or the password.

Also, if you come from a previous system, you might want to copy your old keys to avoid security errors/warnings when you connect the next time. They are /etc/ssh/ssh_host_*. Their owner should be root and their permissions of the private keys should be 600 (i.e., only the owner can read them):

chown root:root /etc/ssh/ssh_host_*
chmod 600 /etc/ssh/ssh_host_*
chmod 644 /etc/ssh/ssh_host_*.pub

If you see this error when you log in over SSH to your machine, please make sure you setup the mdev services in the previous step:

PTY allocation request failed on channel 0

NTP daemon

Like many single-board computers, the Orange Pi Zero does not have an RTC (real-time clock). Thus, its clock will be reset at each reboot.

To avoid this, we can install an NTP daemon to keep the time updated and avoid any errors with HTTPS requests:

apk add chrony
rc-update add chronyd

Make the system boot

Now it is time for the most “fun” part: finding a way to boot it. It took me many hours, and I also needed to connect over the serial interface, or I would have never found all the problems I had.

Like all the other distributions, Alpine uses U-Boot, and we need to configure a boot script for it. Here is mine:

load ${devtype} ${devnum}:${distro_bootpart} ${kernel_addr_r} /boot/vmlinuz-lts
load ${devtype} ${devnum}:${distro_bootpart} ${ramdisk_addr_r} /boot/initramfs-lts.uimg
load ${devtype} ${devnum}:${distro_bootpart} ${fdt_addr_r} /boot/dtbs-lts/${fdtfile}
setenv bootargs root=LABEL=orangepi rw rootfstype=ext4 rootwait iomem=relaxed
bootz ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr_r}

As you can see on the last line, my U-Boot binary (the one I installed with Debian) supports the bootz command, so I could use the kernel binary from Alpine, without converting it to the U-Boot format.

However, the initial ramdisk must be in that format, and it must be converted manually. But first, you might want to customize it to be sure it will manage to load all the hardware you need. I created a /etc/mkinitfs/features.d/sun8i.modules file with a list of modules that I believed were helpful or needed for my hardware:

kernel/drivers/gpu/drm/sun4i
kernel/drivers/net/ethernet/stmicro/stmmac
kernel/sound/soc/sunxi
kernel/drivers/nvmem/nvmem_sunxi_sid.ko*
kernel/drivers/rtc/rtc-sunxi.ko*
kernel/drivers/usb/musb/sunxi.ko*
kernel/drivers/mmc/host/sunxi-mmc.ko*
kernel/drivers/media/rc/sunxi-cir.ko*
kernel/drivers/watchdog/sunxi_wdt.ko*
kernel/drivers/net/ethernet/stmicro/stmmac/dwmac-sunxi.ko*
kernel/drivers/ata/ahci_sunxi.ko*

I enabled it by appending sun8i to features in /etc/mkinitfs/mkinitfs.conf.

The next step was updating the image and converting it to the U-Boot format with these commands:

mkinitfs
mkimage -A arm -T ramdisk -O linux -C none -d /boot/initramfs-lts /boot/initramfs-lts.uimg

While mkinitfs should be automatically called when installing a new kernel, Alpine does not provide hooks to convert the new image to the format we need automatically. We will have to pay attention to each update.

I am not sure the initrd customization is needed, after all.

My goal was to enable USB support and to boot from there. From what I understood, Orange Pi’s main USB port is actually an On-The-Go one and needs a custom module (the musb one). However, including it in the image was not enough to load it automatically or to load the USB storage module. If I remember correctly, I managed to make the kernel recognize the device, but it did not add the corresponding /dev/sdX file. I forgot to try adding the relevant lines to /etc/modules, but I have already wasted too much time on it to make another attempt. If you do and it works, please let me know. Notice that you might need to add /etc/modules to the list of files to copy to the image.

In any case, after switching to a microSD to work around this issue, my system kept not booting. The card was detected, but the kernel refused to mount the filesystem because it did not load the ext4 module automatically. I solved this by adding the rootfstype parameter to the kernel arguments.

Finally, I passed iomem=relaxed to make sure /dev/mem worked as I needed, but I do not know whether this is really needed because I have not tried without.

The system has run for a couple of months, and it has crashed only once. I am very relieved it is stable again. I wonder if the newer kernel would have solved the problem, but, at the moment, I do not have a reason to change again. But if I have to, at least I will have more knowledge on how to deal with the various boot shenanigans.