Arch Linux ARM on ClockworkPi uConsole w/ RPi CM5 and SwayWM
April 13, 2025•2,440 words
Okay, I've known about the ClockworkPi uConsole for a few years. In fact, I pre-ordered the first batch when it became available, but then regretted and cancelled the order before it finally shipped. Actually, a mere months before it finally shipped.
One of the reasons for cancellation was that I didn't feel a strong use case for that. True, it is an interesting gadget, but with the strongest "core" being a RPi CM4 at the time, it would have been barely useful for anything. The BCM2711 SoC is not slow, but use cases like HD/FHD video playback was still somewhat of a tall order.
Until I saw the performance numbers of RPi CM5. Admittedly, it uses up WAY more peak power, but the performance scales beyond the increased power consumption. It suddenly looked like if RPi CM5 can be used, the uConsole could be useful as a "real" mobile computing device.
What finally made me pull the trigger are two things. First, I travelled to Japan (first time travelling since COVID!) earlier this year, and that made me realize that, while on the go, I really wanted something that's a bit more capable than a phone -- like, you know, having a keyboard and such -- but also a bit smaller than a laptop, even though my own laptop is also one of those tiny 10-inch models. Second, I was watching uConsole's user forum for some time, and it started to look like that CM5 support was falling into shape just a few weeks ago.
So, I finally ordered a uConsole of my own. Admittedly, I bought it from one of the scalpers because the official 90-day shipping timeline is plain unacceptable. I felt that given the performance of the CM5, this is going to be something I'd actually use, and the scalpers' pricing seemed acceptable. Here I have it, a few weeks later, and, of course, the first thing to do is to get Arch Linux ARM running, instead of a Debian-based OS.
(Note: the rest of the article is a brief write-up, not an installation guide. Here's a short installation guide.)
Kernel
The first order of business here is to get a working kernel package installable for Arch Linux ARM. Unfortunately, mainline support for RPi (and uConsole) hardware is still a bit spotty, and it seemed that the best Debian image available for it is a community one by a developer named Rex, who also maintains a downstream kernel tree which includes most of the required patches.
Since I just received the hardware and was just trying to get an OS image up and running, I did not want to start another downstream kernel tree and/or patchset just for Arch Linux ARM. Instead, I decided to just package Rex's kernel tree for Arch Linux ARM as a linux-clockworkpi-git
package.
This was based on Arch Linux ARM's linux-aarch64
PKGBUILD with some minor changes, mostly to add a pkgver
function for -git
and to move dtbs to a path where RPi's bootloader expects, in order for auto-detection to work similar to the Debian image. Of course, I also stole a defconfig
from Rex's Debian package.
With the kernel binary package in place, I was able to start from an Arch Linux ARM rootfs tarball, remove the default linux-aarch64
kernel, install my version, and then add a /boot/config.txt
and /boot/cmdline.txt
for it to boot normally.
Boot Config
In the kernel package, I intentionally did not package /boot/config.txt
, because I felt that this should not be managed by the package manager. It is a configuration file, like what you'd have for systemd-boot
or grub
on a x86 PC, and not something to be installed by a package.
Again, it was easy to basically steal it from the Debian image builder:
[pi5]
dtoverlay=clockworkpi-uconsole-cm5
dtoverlay=vc4-kms-v3d-pi5,cma-384
dtparam=pciex1=off
enable_uart=0
arm_freq_min=400
[all]
ignore_lcd=1
max_framebuffers=2
disable_overscan=1
dtparam=audio=on
dtoverlay=audremap,pins_12_13
dtoverlay=dwc2,dr_mode=host
dtparam=ant2
kernel=vmlinuz-linux-clockworkpi-git
initramfs initramfs-linux-clockworkpi-git.img followkernel
Only change that needed to be made is the path to our kernel image. The kernel package installs the kernel image as /boot/vmlinuz-linux-clockworkpi-git
, not kernel8.img
like what's expected by default on RPi OS. Same applies to the initrd image -- technically, if you use a rootfs filesystem that is built in to the kernel, you won't need it, but having it doesn't really hurt, and will allow you to configure, for example, full disk encryption via LUKS, if so desired.
Disk Encryption
You can for sure set up LUKS full-disk encryption using Arch Linux's guide. However, because this is kind of my development "toy" device, I could not be bothered with having to run a bunch of cryptsetup
commands every time I want to mount the sdcard on my desktop machine.
Instead, for this device I elected to use systemd-homed's integration with fscrypt. This is indeed very easy to set up -- just had to format the root partition as f2fs
with the appropriate flag enabled, and then homectl create --storage=fscrypt
gives me a home directory that's encrypted by fscrypt
. The home directory unlocks fine even with SSH connections when the device itself cannot be accessed directly.
WiFi
RaspberryPi's WLAN adapter interface does show up without an issue by default. However, if you attempt to connect to a WPA(/2/3)-protected AP using NetworkManager, the handshake will fail without any obviously helpful error message.
Turns out, this seems to be a known issue with upstream wpa_supplicant
since around the version 2.11. Instead, I attempted to use iwd
, which did seem to work for my guest AP at home, which supports associating with WPA (and not requiring WPA2 or 3). However, my main AP does require at least WPA2, and even iwd
was not able to connect to that.
Since I know WPA2 works on Raspbian, I decided to revert to wpa_supplicant
+ NetworkManager
and figure out what happened between wpa_supplicant
2.10 and 2.11. Turns out, I am far from the first person to run into this on bleeding-edge distributions: folks at Asahi Linux ran into the same bug last year, which in fact broke WPA2 association on almost all Broadcom WLAN chips. Digging up the mentioned fix in Fedora, it seems like this is just caused by one single upstream commit, and reverting it fixed everything downstreams have been seeing. The funniest thing is that this commit seems like a bugfix from Broadcom itself, which ended up breaking things for almost all Broadcom chips currently out there.
In any case, with the fix at hand, I packaged a wpa_supplicant-raspberrypi-git package based on wpa_supplicant-git
, with that one single revert commit. This seems to make WPA2 association function just as well on Arch Linux ARM as it is on Raspbian.
Desktop & Input
I am a huge fan of tiling window managers. Since a few years ago, I have been running SwayWM exclusively on my work and personal machines. Although I did switch to Niri a while back because I really liked its scrolling setup, it (1) had issues with devices that have separate DRM master and render nodes, RaspberryPi being one of them; and (2) I didn't think the scrolling layout is very suited to a device of this size. So, I ended up going with Sway.
My configuration is pretty much based on my old Sway setup for desktop machines, with a few tweaks that I'll get into a bit below. In general, there is ~nothing I had to do in order to "make it work" -- only thing that's really mandatory is switching the default Mod
key to Alt
, because the uConsole does not come with a Win
(or Super) key.
I also found the DPI a bit too small by default at a 100% scale, so output "DSI-2" scale 1.2
applies a 120% scale (for Wayland programs), which I found is the sweet spot. Some default key assignments might be a bit awkward on the uConsole too, especially those 3-key combinations -- I don't think I ended up with the best config, but by shuffling them around a bit I was able to make most things I use regularly 2-key combos.
There's one more issue with the uConsole, and that is scrolling. By default there's simply nothing like a scrolling wheel on this device, which is a bit of a pain for even simple tasks like web browsing. Fortunately, I have used devices like this before -- I used to have a trackball whose scrolling wheel was absolutely horrible, next to nonexistent -- and from that I knew that libinput
comes with scrolling emulation, where you can configure mouse movement itself to act as scrolling when a given button is pressed. This is also exposed by Sway:
input type:pointer {
scroll_button button3
scroll_method on_button_down
}
Idling & Locking
The uConsole is very much a mobile device to me, somewhat like a phone. And one of the things I'd like these to do is to be able to lock and turn the screen off after a certain idle timeout.
Usually, it's pretty trivial to set this up with swayidle
, Sway's preferred way to perform actions on idle. You'd just invoke swaylock
after swayidle
's inactivity timeout and call it a day. Something like
exec_always swayidle -w timeout 120 'swaylock -f -c 000000' timeout 300 'swaymsg "output * dpms off"' resume 'swaymsg "output * dpms on"'
...would lock the session after 120 seconds of idling, then turn off the display after 300 seconds.
This doesn't work well on the uConsole, though, because unlike your standard laptop, the uConsole has a completely exposed keyboard and trackball that are just waiting to be pressed. swayidle
considers any input event to be a wakeup signal, and would call the resume
action immediately. This means if you carry your uConsole around like this, it'll probably never be able to turn off its screen for long.
At first, I thought this is a bit of a sticky situation, because I do need some way to unlock the device, but I can't just "tell" swayidle
to ignore some input events from the keyboard or mouse but not others. Then I realized we have the power button to our disposal -- but, unfortunately, it seems to trigger the shutdown action by default without sending any input event that can be consumed by the desktop.
Although, this action is actually handled by systemd-logind
, which can be configured to ignore the power key:
[Login]
HandlePowerKey=ignore
This did disable the shutdown action triggered by the power button. Then, by binding something to the XF86PowerOff
button in Sway's config, it looked to me like the event was being correctly passed through to Sway. Best of all, it seems that this button is located on its own input device named pwr_button
(defined in dtbs as a GPIO button, I think), completely separate from other input devices.
So, we could simply turn the regular keyboard and mouse off when swayidle
turns the screen off, and then the power button will be the only key that is able to trigger the resume
action (recall that swayidle
considers any input event to be a "wake up" event). The mouse and keyboard are USB devices named 7855:36:ClockworkPI_uConsole_Keyboard
and 7855:36:ClockworkPI_uConsole_Mouse
. My swayidle
config then becomes:
exec_always swayidle -w timeout 120 'swaylock -f -c 000000' timeout 300 'swaymsg "output * dpms off" && swaymsg input "7855:36:ClockworkPI_uConsole_Keyboard" events disabled && swaymsg input "7855:36:ClockworkPI_uConsole_Mouse" events disabled' resume 'swaymsg "output * dpms on" && swaymsg "input * events enabled"'
(of course, tune this as you like, such as a shorter timeout and a different swaylock
background)
To match the power button on devices like phones, I also decided to make it trigger lock + screen off in one step when the device is not locked:
bindsym XF86PowerOff exec swaylock -f -c 000000 && sleep 1 && pkill -SIGUSR1 swayidle
Here, sending SIGUSR1
to swayidle
triggers the screen off action.
Note that we'll also need to override this action in locked mode to make it do nothing instead of launching swaylock
again:
bindsym --locked XF86PowerOff exec yes
I also ended up invoking another script to set the CPU frequency to 400MHz when the screen is off. This is just a simple wrapper over cpupower
and allowlisted in sudoers
.
Backlight Adjustment
For my desktops, I had a simple setup for adjusting backlight logarithmically using the light package:
bindsym --locked XF86MonBrightnessUp exec light -S "$(light -G | awk '{ print int(($1 + .72) * 1.4) }')"
bindsym --locked XF86MonBrightnessDown exec light -S "$(light -G | awk '{ print int($1 / 1.4) }')"
This does not seem to work well on the uConsole. Some investigation showed that the backlight driver seems to round the backlight value down to certain set points in the 0 - 100 scale, instead of supporting every single value. So, any simple arithmetic based on the current value might end up causing problems -- if the calculated new value ends up getting rounded to the same value, that creates a loop and you'll be stuck.
I had to instead do the adjustment in steps of 20, while ensuring that it never actually goes below 20:
bindsym --locked XF86MonBrightnessUp exec light -S "$(light -G | awk '{ print (int($1 / 10) + 2) * 10 }')"
bindsym --locked XF86MonBrightnessDown exec light -S "$(light -G | awk '{ if (int($1 - 10) > 20) { print (int($1 / 10) - 1) * 10 } else { print 20 } }')"
Speaker / Headphone Jack
The Debian image comes with a Python script that detects the headphone jack being plugged in in userspace. Instead of relying on Python, I thought this could just be a simple Bash script:
#!/usr/bin/env bash
set -e
if [ $UID -ne 0 ]; then
echo "Please run as root!"
exit 1
fi
if ! which pinctrl; then
echo "Please install pinctrl (raspberrypi-utils)!"
exit 1
fi
cleanup() {
echo "Disabling speaker before exitting"
# Disable speaker when this script exits
pinctrl set 11 dl
}
trap cleanup EXIT
# Initialize GPIO pin 10 (3.5mm sense)
pinctrl set 10 ip pn
# Pin 11 (speaker)
pinctrl set 11 op
while true; do
if pinctrl 10 | grep -q "lo"; then
echo "Enabling speaker"
pinctrl set 11 op dh
else
echo "Disabling speaker"
pinctrl set 11 op dl
fi
sleep 1
done
This does rely on the pinctrl
command, and I have packaged this as a standalone package in my Arch Linux ARM repo -- you can simply install this and enable clockworkpi-speaker-auto-switch.service
.
Also, a mandatory picture: