Posts in "uconsole"

Booting uConsole CM4 from encrypted NVMe

I have a uConsole computer and a new NVMe adapter for it. I’m not going to walk around with a terabyte of unencrypted data. That wasn’t an option. I found several good HOWTOs for booting a uConsole off an encrypted NVMe drive, yet none of them worked well on their own. Each left out one essential detail or another.

This is my mashup of those instructions. For testing, I wiped my NVMe drive and started over, following these steps exactly, and at the end booted into a working system.

I claim zero credit for any of these. Much of this is copied-and-pasted from the resources I list at the bottom. Those are the people who did the hard work of figuring this out. My goal was to put combine the working bits of each resource into a single document that includes the best of each.

The gist of it

We’re starting with a uConsole booted from its SD card. From inside that running system, we’ll partition the NVMe drive, encrypt most of it, copy the working system onto the new drive, and make the tweaks that the new setup requires to boot successfully.

Install the dependencies, part 1

We’ll use cryptsetup in the “parent” SD card-based OS.

$ sudo apt install cryptsetup

Partition the drive

This is easiest with the desktop’s GParted command. If you’re using the terminal anyway, you can do this from the CLI.

$ sudo parted /dev/nvme0n1
mklabel gpt
yes
mkpart primary fat32 1M 513M
mkpart primary ext4 513M 100%
quit

Encrypt the root volume and make the filesystems

Now we prepare the drive for holding data. We format partition 1 with the FAT32 filesystem that the bootloader wants. Then we use cryptsetup encrypt partition 2.

$ sudo cryptsetup luksFormat -c xchacha20,aes-adiantum-plain64 /dev/nvme0n1p2
$ sudo cryptsetup open /dev/nvme0n1p2 ssd # Creates /dev/mapper/ssd

$ sudo mkfs.vfat /dev/nvme0n1p1
$ sudo mkfs.ext4 /dev/mapper/ssd
$ sudo lsblk -o NAME,UUID,PARTUUID

In the lsblk command’s output, note:

  • PARTUUID for the NVMe /boot/firmware filesystem
  • UUID for the new / filesystem

Mount and populate the new volumes under /mnt

When these steps are done, we’ll have a clone of our parent system in /mnt. See the -x flag to rsync? That says “stay in this one filesystem and don’t recurse into others”, so that we’re not cloning /proc and /sys, and so on.

$ sudo mount /dev/mapper/ssd /mnt
$ sudo rsync -aAXHvx / /mnt/
$ sudo mount /dev/nvme0n1p1 /mnt/boot/firmware
$ sudo rsync -aAXHvx /boot/firmware/ /mnt/boot/firmware/

Chroot into /mnt

From now on, the rest of these commands will run on the new, encrypted root volume.

$ for dir in sys dev proc ; do sudo mount --rbind /$dir /mnt/$dir && sudo mount --make-rslave /mnt/$dir ; done
$ sudo chroot /mnt

Quick note about editors

Also, I like using vi for quick-and-dirty system file edits. If that’s outside your comfort zone, replace vi with nano from now on. But really, if you’re going to be using Linux much, at least get the vi basics down. You’ll thank me later.

Of course, vi’s main purpose is to bootstrap Emacs, and then you can uninstall vi.

Configure fstab & crypttab

The parent OS has it’s idea of where / and /boot/firmware live. Now we configure the child OS with its new locations.

> vi /etc/fstab

============
# Find the line that mounts /boot/firmware and replace it with:
PARTUUID={{PARTUUID of the new /boot/firmware}} /boot/firmware vfat defaults 0 2
# Find the line that mounts / and replace it with:
/dev/mapper/ssd / ext4 defaults,noatime 0 1
============

> vi /etc/crypttab

============
# Append this line:
ssd UUID={{UUID of the new "raw" /}} none luks,discard
============

Install the dependencies, part 2

We’ll use these tools to make a new initramfs inside the child OS.

> apt install busybox cryptsetup cryptsetup-initramfs initramfs-tools

Edit initramfs.conf and change modules=dep to modules=most

This may not be needed. This was the default on the OS image I used.

> vi /etc/initramfs-tools/initramfs.conf

Add Kernel modules to initramfs

Add these modules to the initramfs so it display the boot-time encrypted volume password prompt, and can unlock the encrypted root volume.

> vi /etc/initramfs-tools/modules

============
# --- Crypto modules (AES-XTS + SHA256, ARM hardware-accelerated) ---
aes_arm64
aes_ce_blk
aes_ce_cipher
sha256_arm64
sha256_ce
dm-crypt
xts

# --- Display modules (needed so the password prompt shows on the uConsole screen) ---
drm
drm_kms_helper
drm_shmem_helper
drm_dma_helper
drm_ttm_helper
drm_display_helper
drm_panel_orientation_quirks
ttm
cec
backlight
ocp8178_bl
panel-cwu50
vc4
v3d
drm-rp1-dsi
============

Create Display Init Script

This is the bit that turns on the screen in text mode so we know when to enter the drive encryption password.

> vi /etc/initramfs-tools/scripts/init-top/display

============
#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; }
case $1 in prereqs) prereqs; exit 0;; esac

# Load display stack. Order matters.
modprobe drm
modprobe drm_kms_helper
modprobe backlight
modprobe ocp8178_bl         # uConsole backlight controller
modprobe panel-cwu50        # uConsole LCD panel driver
modprobe vc4                # Broadcom VideoCore GPU
modprobe v3d                # Broadcom V3D 3D engine

sleep 1                     # Give the panel time to initialize

modprobe drm-rp1-dsi        # RP1 DSI bridge (connects SoC to panel)
modprobe fbcon              # Framebuffer console (renders text on screen)
============

> chmod a+x /etc/initramfs-tools/scripts/init-top/display

Create Kernel Postinst Hook

This is suppose to rebuild the initramfs when we upgrade to a new kernel. I haven’t tested it yet.

> vi /etc/kernel/postinst.d/initramfs-rebuild

============
#!/bin/sh -e

version="$1"

# Only rebuild for the currently running kernel
[ "$version" = "$(uname -r)" ] || exit 0

# Back up existing initramfs before overwriting
[ -e /boot/firmware/initramfs_2712 ] && \
    cp /boot/firmware/initramfs_luks_nvme.img /boot/firmware/initramfs_luks_nvme.img.bak

update-initramfs -c -k "$version"
mkinitramfs -o /boot/firmware/initramfs_luks_nvme.img "$version"
============

> chmod a+x /etc/kernel/postinst.d/initramfs-rebuild

Build Initramfs

Now we manually do a stripped-down version of the above to create our first initramfs.

> update-initramfs -c -k "$(uname -r)"
> mkinitramfs -o /boot/firmware/initramfs_luks_nvme.img "$(uname -r)"

Configure Boot

This tells the system how to enable the NVMe drive, and how to boot into it.

> vi /boot/firmware/config.txt

============
[pi4]
...
enable_uart=1
dtparam=pciex1=on

[all]
...
auto_initramfs=1
initramfs initramfs_luks_nvme.img followkernel
============

> vi /boot/firmware/cmdline.txt

============
console=serial0,115200 console=tty1 root=/dev/mapper/ssd cryptdevice=UUID={{Same UUID of / as in /etc/fstab}}:ssd rootfstype=ext4 fsck.repair=yes rootwait fbcon=rotate:1 psi=1 quiet loglevel=3 cfg80211.ieee80211_regdom=US
============

In confess, I didn’t do exactly this. I kept some of the original cmdline.txt. Instead of rootwait fbcon=rotate:, I have rootwait splash plymouth.ignore-serial-consoles fbcon=rotate:1. Now I have a splash screen but don’t see the prompt to enter the drive encryption password. I just have to know when to enter it, and then the little computer does the rest. Yes, that’s a little risky: what if something breaks and I can’t see the error message? On the other hand, good luck to a casual thief even knowing where to begin with the thing.

Exit and power off

Leave the chroot and shut down the uConsole.

> exit
$ sudo shutdown -h now

Cross your fingers

Now eject the SD card from uConsole and turn it back on. With any luck, it’ll prompt you for the passphrase to unlock your encrypted NVMe root partition and then boot into a working system!

Now what?

Bump up the swap

I configured my uConsole to have 4x as much swap as RAM. That works out to 32GB, or about 3% of my NVMe. In exchange, I’m unlikely to ever run out of memory again. NVMe swap is decently quick, unlike sticking it on a spinning rust hard drive.

$ cd /etc/rpi
$ sudo mkdir swap.conf.d
$ sudo -e swap.conf.d/

============
[Main]
Mechanism=swapfile

[File]
RamMultiplier=4
MaxSizeMiB=65536
============

Resources

uConsole built-in SD card vs HackerGadgets NVMe speed

I have a ClockworkPi uConsole and although I’m having great fun with it, it comes with just 32GB of SD card storage. Even if it were bigger, SD cards are dog slow compared to almost anything else. I bought a HackerGadgets NVMe adapter and Crucial P310 1TB NVMe SSD to speed things up.

Sequential operations

In these tests, /dev/mmcblk0 is the SD card, and /dev/nvme0n1 is the NVMe adapter.

Reads

root@uconsole /h/me# time dd if=/dev/mmcblk0 of=/dev/null bs=1M count=8192
8192+0 records in
8192+0 records out
8589934592 bytes (8.6 GB, 8.0 GiB) copied, 198.193 s, 43.3 MB/s

________________________________________________________
Executed in  198.20 secs    fish           external
   usr time    0.06 secs    0.03 millis    0.06 secs
   sys time   23.06 secs    2.00 millis   23.06 secs

root@uconsole /h/me# time dd if=/dev/nvme0n1 of=/dev/null bs=1M count=8192
8192+0 records in
8192+0 records out
8589934592 bytes (8.6 GB, 8.0 GiB) copied, 21.366 s, 402 MB/s

________________________________________________________
Executed in   21.37 secs    fish           external
   usr time    0.03 secs    1.59 millis    0.03 secs
   sys time   10.38 secs    0.86 millis   10.38 secs

Summary: The NVMe drive was 9.3x faster than the SD card and took 55% less CPU.

Writes

I couldn’t write to the raw SD card because all of it is allocated in the partition map and it’s what I boot off of as of this writing. Instead, I formatted the NVMe drive with the same options as the root filesystem and mounted it at /tmp/nvme so that at least it’s an apples-to-apples comparison

root@uconsole /h/me# time dd if=/dev/zero of=/sdfile bs=1M count=8192
8192+0 records in
8192+0 records out
8589934592 bytes (8.6 GB, 8.0 GiB) copied, 331.464 s, 25.9 MB/s

________________________________________________________
Executed in  331.47 secs    fish           external
   usr time    0.05 secs    0.25 millis    0.05 secs
   sys time   25.98 secs    2.16 millis   25.98 secs

root@uconsole /h/me# time dd if=/dev/zero of=/tmp/nvme/sdfile bs=1M count=8192
8192+0 records in
8192+0 records out
8589934592 bytes (8.6 GB, 8.0 GiB) copied, 21.0683 s, 408 MB/s

________________________________________________________
Executed in   21.10 secs    fish           external
   usr time    0.03 secs    2.16 millis    0.03 secs
   sys time   18.92 secs    1.13 millis   18.92 secs

Summary: The NVMe drive was 15.7x faster than the SD card, and took 27% less CPU.

Random operations

These use the fio benchmark. First, the SD card:

root@uconsole /v/tmp# fio --profile=tiobench
seqwrite: (groupid=0, jobs=1): err= 0: pid=3128: Mon Jul  6 19:40:57 2026
  write: IOPS=1045, BW=4181KiB/s (4281kB/s)(30.5MiB/7482msec); 0 zone resets
randwrite: (groupid=1, jobs=1): err= 0: pid=3161: Mon Jul  6 19:40:57 2026
  write: IOPS=692, BW=2771KiB/s (2838kB/s)(30.5MiB/11287msec); 0 zone resets
seqread: (groupid=2, jobs=1): err= 0: pid=3206: Mon Jul  6 19:40:57 2026
  read: IOPS=2917, BW=11.4MiB/s (12.0MB/s)(30.5MiB/2680msec)
randread: (groupid=3, jobs=1): err= 0: pid=3255: Mon Jul  6 19:40:57 2026
  read: IOPS=3075, BW=12.0MiB/s (12.6MB/s)(30.5MiB/2543msec)

Run status group 0 (all jobs):
  WRITE: bw=4181KiB/s (4281kB/s), 4181KiB/s-4181KiB/s (4281kB/s-4281kB/s), io=30.5MiB (32.0MB), run=7482-7482msec

Run status group 1 (all jobs):
  WRITE: bw=2771KiB/s (2838kB/s), 2771KiB/s-2771KiB/s (2838kB/s-2838kB/s), io=30.5MiB (32.0MB), run=11287-11287msec

Run status group 2 (all jobs):
   READ: bw=11.4MiB/s (12.0MB/s), 11.4MiB/s-11.4MiB/s (12.0MB/s-12.0MB/s), io=30.5MiB (32.0MB), run=2680-2680msec

Run status group 3 (all jobs):
   READ: bw=12.0MiB/s (12.6MB/s), 12.0MiB/s-12.0MiB/s (12.6MB/s-12.6MB/s), io=30.5MiB (32.0MB), run=2543-2543msec

Disk stats (read/write):
  mmcblk0: ios=15548/15656, sectors=124384/125480, merge=0/27, ticks=4870/18365, in_queue=23235, util=93.05%

And then the NVMe drive:

root@wizzle /t/nvme# fio --profile=tiobench
seqwrite: (groupid=0, jobs=1): err= 0: pid=4865: Mon Jul  6 19:47:10 2026
  write: IOPS=21.6k, BW=84.4MiB/s (88.5MB/s)(30.5MiB/362msec); 0 zone resets
randwrite: (groupid=1, jobs=1): err= 0: pid=4870: Mon Jul  6 19:47:10 2026
  write: IOPS=21.2k, BW=82.8MiB/s (86.8MB/s)(30.5MiB/369msec); 0 zone resets
seqread: (groupid=2, jobs=1): err= 0: pid=4871: Mon Jul  6 19:47:10 2026
  read: IOPS=13.3k, BW=51.9MiB/s (54.4MB/s)(30.5MiB/589msec)
randread: (groupid=3, jobs=1): err= 0: pid=4876: Mon Jul  6 19:47:10 2026
  read: IOPS=13.1k, BW=51.3MiB/s (53.7MB/s)(30.5MiB/596msec)

Run status group 0 (all jobs):
  WRITE: bw=84.4MiB/s (88.5MB/s), 84.4MiB/s-84.4MiB/s (88.5MB/s-88.5MB/s), io=30.5MiB (32.0MB), run=362-362msec

Run status group 1 (all jobs):
  WRITE: bw=82.8MiB/s (86.8MB/s), 82.8MiB/s-82.8MiB/s (86.8MB/s-86.8MB/s), io=30.5MiB (32.0MB), run=369-369msec

Run status group 2 (all jobs):
   READ: bw=51.9MiB/s (54.4MB/s), 51.9MiB/s-51.9MiB/s (54.4MB/s-54.4MB/s), io=30.5MiB (32.0MB), run=589-589msec

Run status group 3 (all jobs):
   READ: bw=51.3MiB/s (53.7MB/s), 51.3MiB/s-51.3MiB/s (53.7MB/s-53.7MB/s), io=30.5MiB (32.0MB), run=596-596msec

Disk stats (read/write):
  nvme0n1: ios=13873/15650, sectors=110984/125184, merge=0/4, ticks=866/486, in_queue=1354, util=49.13%

Summary

BW measurements are in MB/s.

Operation SD NVMe Ratio
seqwrite IOPS 1045 21600 20.7x
seqwrite BW 4.2 84.4 20.1x
randwrite IOPS 692 21200 30.6x
randwrite BW 2.8 82.8 29.6x
seqread IOPS 2917 13300 4.6x
seqread BW 12.0 51.9 4.3x
randread IOPS 3075 13100 4.3x
randread BW 12.6 51.3 4.1x

Conclusion

In the words of the great Ferris Bueller, “It is so choice. If you have the means, I highly recommend picking one up.”

Replacing the login and lock screens on a Raspberry Pi

My uConsole computer finally arrived after a 10-month delay. I started kicking the tires by installing fun software on it, and quickly realized it’d run better if it looked cool. Here’s how I did it.

Change the boot image

Raspberry Pi OS uses Plymouth to make show a boot splashscreen. By default, it displays the image file at /usr/share/plymouth/themes/pix/splash.png. I’m sure there’s a “better” way to do this, but I simply replaced that file with my own 1280x720 image (to match the screen’s native resolution):

$ cd /usr/share/plymouth/themes/pix
$ sudo cp splash.png splash.png-dist  # Keep a backup
$ sudo cp myimage.png splash.png
$ sudo plymouth-set-default-theme --rebuild-initrd pix

That last line rebuilds the initrd image so that the kernel will use the new image.

Photo of a uConsole with a Hackers movie-style boot screen with blue plasma flames and a yellow flame logo inside a red trapezoid.

Change the lock image

I use Wayland instead of X11, and that setup uses pi-greeter to show a lock screen. That requires editing /etc/lightdm/pi-greeter.conf. I copied my new user image to /usr/share/plymouth/themes/pix/smiley.png, which isn’t the right place to put it exactly, but has it living next to the splash.png I installed in the previous step. Then I backed up pi-greeter.conf and edited its default-user-image and wallpaper values like so:

--- pi-greeter.conf-dist        2026-06-22 18:52:53.702242786 -0700
+++ pi-greeter.conf     2026-06-22 18:55:06.519726407 -0700
[@@](https://micro.blog/@) -1,7 +1,7 [@@](https://micro.blog/@)
 [greeter]
-default-user-image=/usr/share/raspberrypi-artwork/clockworkpi.png
+default-user-image=/usr/share/plymouth/themes/pix/smiley.png
 desktop_bg=#000000
-wallpaper=/usr/share/rpd-wallpaper/RPiSystem_dark.png
+wallpaper=/usr/share/plymouth/themes/pix/splash.png
 wallpaper_mode=center
 gtk-icon-theme-name=PiXflat
 gtk-font-name=Nunito Sans 12

Note that usr/share/raspberrypi-artwork/clockworkpi.png doesn’t even exist by default, so the lock screen falls back to a boring silhouette of a person.

The same image, but showing a username/password prompt. The icon is a yellow smiley with a black patch over one eye.

Make the screen automatically lock

I’m teaching my coworkers not to trust leaving their laptops unlocked, and I have to practice what I preach. I want my screen to automatically lock if I ever forget to manually do it. That’s easy! Edit the ~/.config/labwc/autostart file like this:

--- autostart-dist      2026-06-22 19:12:18.204495749 -0700
+++ autostart   2026-06-22 19:12:12.708859097 -0700
[@@](https://micro.blog/@) -1 +1 [@@](https://micro.blog/@)
-swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &
+swayidle -w timeout 300 'swaylock -f -p' timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &

The extra timeout 300 'swaylock -f -p' locks the screen after a 5 minute idle timeout.

The same screen, but with a black password box in the middle of the display.

Ta-da!

And that’s it! Reboot and enjoy your cool graphics and slightly more secure setup.

Why yes, you can run Factory’s Droid on a Raspberry Pi. Not officially, sure. This isn’t part of our CI/CD pipeline and we’re not publishing packages for it the way we do for our supported platforms. Still, it works!

Photo of a uConsole handheld computer displaying the Droid text logo in a terminal window.