Bending bits...

Bytes and Words...

Linux drivers and cross compilation

Some years ago, probably too many, I liked to implement drivers for my Beaglebone Black board. Nothing too fancy, just some char drivers that measured a pwm signal using Input-Capture, or toggled some GPIOS. All those drivers were compiled and tested directly on the target. I know is not the best practice, but I didn’t have the time to build the kernel, launch an emulation in QEMU and so on.

A few days ago I found a Raspberry Pi kit, with fan, grills, and an official box. Pretty nice, but I didn’t liked the fan, because there was no way to control it and it was really loud (continuosly ran at full rpm). The first idea was to implement a user space application in C or probably Python to control somehow the fan. So, took a NPN transistor and a 1k resistor, connected everything together and it works. Then I thought, what if I make it a kernel driver. There is no single benefit to make it a driver, probably is even better if I keep it as a userspace application, but I wanted to play with the kernel APIs. So, start the Raspberry, ssh on it and write the driver? No, definitely no!

My plan:

  1. cross compile the kernel
  2. do some voodoo magic and launch Qemu
  3. develop and test the driver.

Cross compiling the kernel

First try was a disaster, but I found 2 nice guides which I will link at the end of this post.

Preparing the environment

  1. Install the cross compiler for arm64 sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
  2. Install Qemu sudo apt install qemu-system-arm
  3. Download the kernel (when I wrote this article, the longterm version was 6.12.53 ) wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.53.tar.xz and untar it.
  4. Download BusyBox wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2 and untar it.
  5. Download a Raspberry Pi OS from Raspi OS.

Building the kernel

  1. Change directory into the kernel source code cd linux-6.12.53
  2. Create a config file ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make defconfig
  3. Create a kvm_guest config ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make kvm_guest.config
  4. Build the kernel ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make -j8

Preparing the OS

  1. Calculate the offset of the first (512 bytes * 16384) device by inspecting the partitions:

    fdisk -l 2025-10-01-raspios-trixie-arm64-lite.img
    Disk 2025-10-01-raspios-trixie-arm64-lite.img: 2.73 GiB, 2927624192 bytes, 5718016 sectors
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disklabel type: dos
    Disk identifier: 0x7351b90c
    
    Device                                    Boot   Start     End Sectors  Size Id Type
    2025-10-01-raspios-trixie-arm64-lite.img1        16384 1064959 1048576  512M  c W95 FAT32 (LBA)
    2025-10-01-raspios-trixie-arm64-lite.img2      1064960 5718015 4653056  2.2G 83 Linux
  2. Mount the image:

    sudo mkdir /mnt/rpi
    sudo mount -o loop,offset=8388608 2025-10-01-raspios-trixie-arm64-lite.img /mnt/rpi
  3. Create a file named ssh: cd /mnt/rpi && sudo touch ssh
  4. Create a file userconf: sudo touch userconf.txt
  5. Generate a password and save it to userconf:

    openssl passwd -6                                     # Generate the <hashed-password>
    echo 'pi:<hashed-password>' | sudo tee userconf.txt   # Put them inside `userconf.txt`
  6. Unmount: sudo umount /mnt/rpi

Build a minimalistic Linux system

  1. Defconfig the busybox: ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make defconfig
  2. Enable static link and dissable the use of special instructions for SHA. (The options are in Busybox Settings): ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make menuconfig
  3. Build busybox: ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make -j8
  4. And install it: ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make install

Now, we must create a rootfs directory and populate it.

  1. Create rootfs: mkdir rootfs
  2. Copy the content from busybox/install into rootfs: cp -av busybox-1.24.2/_install/* rootfs/
  3. Create a symlink to busybox ln -s bin/busybox init.
  4. Create the required directories: mkdir -pv rootfs/{bin,sbin,etc,proc,sys,mnt,var/{log},usr/{bin,sbin}}
  5. Create the required /dev with mem and ttys:

    mkdir dev && cd dev
    sudo mknod -m 660 mem c 1 1
    sudo mknod -m 660 tty2 c 4 2
    sudo mknod -m 660 tty3 c 4 3
    sudo mknod -m 660 tty4 c 4 4
  6. Use the content of this directory as initram disk, thus, from rootfs directory we run: find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz

Emulate the minimalistic Linux system

  1. Launch Qemu: qemu-system-aarch64 -machine virt -cpu cortex-a72 -smp 6 -m 4G -kernel Image -initrd rootfs.cpio.gz -serial stdio -append "root=/dev/mem serial=ttyAMA0" -virtfs local,id=hostshare,path=/home/share,mount_tag=hostshare,security_model=none
  2. Inside Qemu shell create the /mnt/hostshare directory mkdir /mnt/hostshare and mount it mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt/hostshare.

Emulate Raspberry Pi in Qemu

To emulate the Raspberry Pi in Qemu, just run the following command:

qemu-system-aarch64 -machine virt -cpu cortex-a72 -smp 6 -m 4G \
    -kernel Image -append "root=/dev/vda2 rootfstype=ext4 rw panic=0 console=ttyAMA0" \
    -drive format=raw,file=2023-05-03-raspios-bullseye-arm64.img,if=none,id=hd0,cache=writeback \
    -device virtio-blk,drive=hd0,bootindex=0 \
    -netdev user,id=mynet,hostfwd=tcp::2222-:22 \
    -device virtio-net-pci,netdev=mynet \
    -monitor telnet:127.0.0.1:5555,server,nowait

and connect with the ssh as ssh -l pi localhost -p 2222.

Creating the driver

Preparing the Makefile

Now, the problem is that we want to cross-compile the driver using the Linux kernel we built. Thus, we have to define a simple Makefile that use the kernel sources, checks them, and use the cross-compiler:

ifneq ($(KERNELRELEASE),)
obj-m += rpifan.o
else
KDIR := ../cross_compile/linux-6.12.53
PWD := $(shell pwd)
CROSS=/bin/aarch64-linux-gnu-

default:
   @echo '   Building RPI fan driver for kernel 6.12.'
   $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=$(CROSS) modules

install:
   sudo insmod ./rpifan.ko

clean:
   make -C $(KDIR) M=$(PWD) clean

uninstall:
   sudo rmmod rpifan
endif

Building the Driver

We will try to build and insert the following char driver:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>



#define DEVICE_NAME "rpifan"
#define CLASS_NAME "rpi"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sebastian Ardelean");
MODULE_DESCRIPTION("A Linux Driver for fan controlling on RPi.");
MODULE_VERSION("0.1");

static char *name = "world";

module_param(name, charp, S_IRUGO);
MODULE_PARM_DESC(name, "The name to display");


static int __init rpifan_init(void) {

  printk(KERN_INFO "RpiFan: Initializing\n");
  return 0;
}

static void __exit rpifan_exit(void) {
  printk(KERN_INFO "RpiFan: Goodbye! :(\n");
}


module_init(rpifan_init);
module_exit(rpifan_exit);

There are probably more include statements that needed but, I extracted this skeleton from a working driver, and adjusted it.

And Happy development!

Thank you to the authors of the following resources:

  1. Emulating Raspberry Pi 4 with QEMU
  2. Minimalistic Linux system on qemu ARM