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:
- cross compile the kernel
- do some voodoo magic and launch Qemu
- 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
- Install the cross compiler for arm64
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu - Install Qemu
sudo apt install qemu-system-arm - 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.xzand untar it. - Download BusyBox
wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2and untar it. - Download a Raspberry Pi OS from Raspi OS.
¶Building the kernel
- Change directory into the kernel source code
cd linux-6.12.53 - Create a config file
ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make defconfig - Create a kvm_guest config
ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make kvm_guest.config - Build the kernel
ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make -j8
¶Preparing the OS
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
Mount the image:
sudo mkdir /mnt/rpi sudo mount -o loop,offset=8388608 2025-10-01-raspios-trixie-arm64-lite.img /mnt/rpi- Create a file named
ssh:cd /mnt/rpi && sudo touch ssh - Create a file
userconf:sudo touch userconf.txt 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`
- Unmount:
sudo umount /mnt/rpi
¶Build a minimalistic Linux system
- Defconfig the busybox:
ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make defconfig - 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 - Build busybox:
ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make -j8 - And install it:
ARCH=arm64 CROSS_COMPILE=/bin/aarch64-linux-gnu- make install
Now, we must create a rootfs directory and populate it.
- Create rootfs:
mkdir rootfs - Copy the content from busybox/install into rootfs:
cp -av busybox-1.24.2/_install/* rootfs/ - Create a symlink to busybox
ln -s bin/busybox init. - Create the required directories:
mkdir -pv rootfs/{bin,sbin,etc,proc,sys,mnt,var/{log},usr/{bin,sbin}} Create the required
/devwithmemandttys: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- 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
- 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 - Inside Qemu shell create the
/mnt/hostsharedirectorymkdir /mnt/hostshareand mount itmount -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: