Porting Alpine Linux to a RISC-V Microcontroller


A few months back I had gotten my hands on this1 RISC-V microcontroller which advertised itself to be capable of running Linux. The prospect of programming a microcontroller the same way I would any computer sounded totally awesome, but could it really be that simple?

Turns out, the answer is no. Instead of mainline kernel support, the board came with a custom buildroot instead, no package manager among other annoyances. Naturally, I decided to remedy this by replacing the rootfs with something more sensible for an embedded SBC, like Alpine Linux.

In this blog post, I will explain how I accomplished this.

Creating an Alpine chroot

First, let’s create an alpine root at /alpine. We can grab a copy of the latest Alpine rootfs here.

mkdir /alpine && cd /alpine
wget -O- https://dl-cdn.alpinelinux.org/latest-stable/releases/riscv64/alpine-minirootfs-{LATEST_VER}-riscv64.tar.gz | tar xz

We’ll need to access the real system devices inside the chroot:

for dir in /proc /sys /dev /dev/pts; do
    mount --bind "$dir" "/alpine/$dir"
done

We also need to move some configs into the new root as well:

cp /etc/resolv.conf /etc/fstab /alpine/etc
cp /etc/network/interfaces /alpine/etc/network

Let’s also make sure we can still access the old root from inside the chroot:

mkdir /alpine/old_root
mount --bind / /alpine/old_root

Now, we can enter the chroot and move the old root out of the way:

chroot /alpine /bin/ash
mkdir /old_root/old
mv old_root/* old_root/old

Then we can move the Alpine root into position:

apk add rsync
cd /
rsync -ax bin etc home lib media mnt opt root sbin srv tmp usr var

Now we can exit the chroot and delete /alpine.

Configuring Alpine

At this point, we have successfully replaced our root with Alpine. However, as the minirootfs is designed for docker containers, it doesn’t have things like an init system or an SSH server among other things. We will need them to boot and access our new Alpine installation.

apk add openrc dropbear vim chrony
rc-update add chrony default
rc-update add dropbear default

Since the MilkV Duo S doesn’t have normal /dev/ttyX devices, let’s comment out everything in /etc/inittab. Optionally, uncomment the ttyS0 line. This will allow us to use connect to the board via UART console if anything ever goes wrong.

SSH need a network connection to work, so add the following to /etc/network/interfaces

auto eth0
iface eth0 inet dhcp
    pre-up ip link set dev eth0 address <YOUR_MAC_ADDR>

Since the MilkV Duo S doesn’t have a fixed MAC address, we’ll need to fix one manually. Replace <YOUR_MAC_ADDR> with any MAC address unique within the network. I suggest using ip a and taking the preexisting one.

At this point, we should be ready to reboot. However, there are a few more steps left for a fully functional system.

Copying Board-Specific Drivers

If we want things like the TPU and Wi-Fi to work, we need to copy some drivers from the old root. Luckily for us, more are located in one place under /mnt. While this is very bad practice, I found it much more convenient to leave it be.

cp -ar /old/mnt/* /mnt/
cp -a /old/etc/uhubon.sh /old/etc/run_usb.sh /etc/
cp -a /old/usr/bin/duo-pinmux /old/usr/bin/cvi-pinmux /usr/bin/
cp -a /old/usr/sbin/wiringx-* /usr/sbin/
cp -a /old/usr/lib/libwiringx.so /usr/lib/

The old /etc/profile contains some exports we will need. Since it’s better than the one that Alpine ships with, let’s replace Alpine’s /etc/profile with it.

cp -a /old/etc/profile /etc/profile

Loading Firmware on Startup

To do this, create /etc/init.d/firmware with the following content:

#!/sbin/openrc-run

name="Firmware loader"
description="Load kernel modules and start user apps"

# Dependencies: make sure root fs and basic system services are ready
depend() {
    need localmount
    after sysinit
}

start() {
    ebegin "Initializing MPP system"

    export USERDATAPATH=/mnt/data/
    export SYSTEMPATH=/mnt/system/

    # Load kernel modules
    if [ -d "$SYSTEMPATH/ko" ]; then
        sh "$SYSTEMPATH/ko/loadsystemko.sh"
    fi

    # Start system apps
    for f in duo-init.sh blink.sh usb.sh; do
        if [ -f "$SYSTEMPATH/$f" ]; then
            "$SYSTEMPATH/$f" &
        fi
    done

    # Start user auto scripts
    for f in auto.sh; do
        if [ -f "$USERDATAPATH/$f" ]; then
            usleep 30000
            "$USERDATAPATH/$f" &
        elif [ -f "$SYSTEMPATH/$f" ]; then
            usleep 30000
            "$SYSTEMPATH/$f" &
        fi
    done

    eend 0
}

stop() {
    # Optionally implement stopping/killing background scripts
    ebegin "Stopping firmware scripts"
    # pkill -f duo-init.sh (or other scripts) if needed
    eend 0
}

And make sure that it runs on startup:

rc-update add firmware default

Reboot the board to see the changes take effect.

Conclusion

At this point, you should now have Alpine Linux working on the MilkV Duo S, without sacrificing any of the board’s peripheral functionalities. I hope you found this interesting or helpful!

Notes

Footnotes

  1. MilkV Duo S