Linux Kernel Development on Raspberry Pi 5

Introduction

Linux kernel development is often seen as complex and inaccessible—especially for beginners. The sheer volume of code can become overwhelming quickly, especially if you’re unfamiliar with legacy codebases. Additionally, the documentation is often vague and lacks basic examples. Kernel code is tightly coupled with hardware, requiring a solid understanding of computer architecture and electronics.

Fortunately, all we need to begin is a small entry point. This guide provides one.

In this article, I’ll show you how to set up a Linux kernel development environment using a Raspberry Pi 5. We’ll go through the following stages:

The development flow goes sth like this:

  1. Raspberry boots up.
  2. Raspberry receives DHCP offer with Network boot info.
  3. Raspberry recognizes TFTP boot options in DHCP request.
  4. Raspberry downloads linux kernel image over TFTP.
  5. Linux kernel recognizes NFS options in cmdline.txt file.
  6. Linux kernel mounts NFS share as root file system.
  7. Linux kernel starts init program.

I believe this development flow is significantly more efficient than relying on SD cards.

Compile the Linux Kernel

Obtaining the Linux source code is not as straightforward as fetching your favorite library. Typically, there's a mainline version of your library that everyone contributes to. The same principle applies to the Linux kernel. We have the mainline kernel maintained by Linus Torvalds, but the Raspberry Pi organization also maintains its own fork.

So why do we need both?

Great question. The mainline kernel is maintained by the core Linux team, but because the kernel is tightly tied to hardware, vendors often maintain their own forks to support their devices immediately—without waiting for official upstream integration. Over time, these vendor changes are merged into the mainline. This is exactly the case with the Raspberry Pi 5. If you compare the device tree file for the Pi 5 in the mainline and the vendor fork, you’ll see how it evolves over time.

For this guide, we’ll use the vendor-maintained kernel:

git clone --branch rpi-6.6.y --depth=1 --single-branch https://github.com/raspberrypi/linux.git rpi_linux
cd rpi_linux

Before compiling, install the necessary dependencies:

sudo apt install build-essential libncurses5-dev bison flex libssl-dev libelf-dev bc xz-utils tar

Kernel Configuration

The kernel uses the kbuild system to configure compilation. You generally don’t want to compile everything—only what’s relevant to your hardware. For instance, since the Raspberry Pi 5 uses the ARM64 architecture, you won’t need drivers for unrelated architecture like x86.

Customizing your build helps you:

  • Embed only the needed features into the kernel image
  • Choose which features to compile as modules
  • Disable features you don’t need

First, install the cross-compiler:

sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu

Next, configure the environment:

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
export KERNEL=kernel2712

Generate the default Raspberry Pi 5 config:

make bcm2712_defconfig

Customize your kernel using the graphical menu:

make menuconfig

In the General Setup section, set the Local Version to -SuperKernel.

Now compile the kernel:

make -j $(( $(nproc) * 2 )) all

Generate compile_commands.json

Modern IDEs rely on compile_commands.json for features like code completion. To generate this file, we’ll use the bear tool:

sudo apt install bear

Clean up any previous build artifacts:

make clean

Rebuild with bear:

bear -- make -j1 bcm2712_defconfig

Reapply your local version string:

bear --append -- make -j1 menuconfig

Now compile the kernel again with bear:

bear --append -- make -j1 all

This will take some time, but the good news is that you won’t need to regenerate compile_commands.json frequently.

Prepare Raspberry for network boot

By default raspberry bootloader allows booting only from sdcard. To change bootloader settings you need to install rpi-imager:

sudo apt install rpi-imager

Once rpi-imager is installed, open it and select:

  • Raspberry Pi Device: Raspberry Pi 5
  • Operating System > Misc Utility Images > Bootloader pi 5 family: Network boot
  • Choose Storage: Here pick your sdcard.
  • Click Write and wait for the flashing process to complete.

Update the Bootloader

Insert the SD card into your Raspberry Pi 5 and power it on. The green LED will blink rapidly, and if you have a display connected, you may see a bootloader update message.

After a few seconds, the update will finish automatically. Once complete, power off the device and remove the SD card—it’s no longer needed for booting.

Your Raspberry Pi is now configured to support network booting.

Generate Boot Filesystem (bootfs)

The goal of this step is to collect all the files required to boot the Raspberry Pi:

  • Linux kernel image – loaded into memory and executed on boot by the bootloader.
  • Device tree files – used by the kernel to understand the hardware layout.
  • Overlays directory – optional tweaks that modify the base device tree files.
  • config.txt – Raspberry Pi-specific configuration file that applies overlays and boot settings.
  • cmdline.txt – contains kernel command-line arguments (like flags to a program).

Copy the Kernel Files

mkdir -p bootfs
cp arch/arm64/boot/Image bootfs/
cp arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dtb bootfs/
cp arch/arm64/boot/dts/broadcom/bcm2712d0-rpi-5-b.dtb bootfs/
mkdir -p bootfs/overlays
cp arch/arm64/boot/dts/overlays/overlay_map.dtb bootfs/overlays/
cp arch/arm64/boot/dts/overlays/bcm2712d0.dtbo bootfs/overlays/

Create Raspberry Pi Boot Configuration

cmdline.txt – basic kernel arguments:

echo "console=serial0,115200 root=/dev/nfs nfsroot=192.168.2.155:/opt/nfs/<nothing here yet>/,vers=3,tcp rw ip=dhcp rootwait ignore_loglevel debug" > bootfs/cmdline.txt

Ensure that ip in nfsroot argument is matching your nfs server.

config.txt – Pi-specific configuration and overlay:

cat <<EOF > bootfs/config.txt
# Run in 64-bit mode
arm_64bit=1

# Disable display overscan compensation
disable_overscan=1

# Max performance (within safe firmware limits)
arm_boost=1

# Kernel image name
kernel=Image

# Workaround for UART pin mismatch between bcm2712c1 and bcm2712d0
dtoverlay=bcm2712d0
EOF

Package bootfs

Finally, package the boot filesystem into a tar archive:

tar -cf bootfs.tar -C bootfs .

This bootfs.tar archive contains everything required for the Pi 5 to boot.

Generate Root Filesystem (rootfs)

We’ll use Buildroot to generate a minimal root filesystem.

Install Required Tools

sudo apt install zip unzip tar rsync

Download and Configure Buildroot

git clone https://github.com/buildroot/buildroot --depth=1
cd buildroot
make raspberrypi5_defconfig

Adjust Buildroot Configuration

Launch the configuration interface:

make menuconfig

Make the following changes:

  • Target Options → MMU Page Size: 16KB
  • Toolchain → Kernel Headers: Linux 6.6.x kernel headers
  • System configuration → Custom scripts to run after creating filesystem images: (leave empty)
  • Kernel → Linux Kernel: Disable
  • Filesystem images → ext2/3/4 root filesystem: Disable
  • Filesystem images → tar root filesystem: Enable

Build the Root Filesystem

make

After completion, the tarball can be found at:

output/images/rootfs.tar

Serve Linux via TFTP

The Raspberry Pi 5 bootloader supports network booting via TFTP. Beside TFTP server we need DHCP server to tell raspberry to use TFTP.

Install and Configure Kea DHCP Server

sudo apt install kea-dhcp4-server

Create /etc/kea/kea-dhcp4.conf with the following contents:

{
  "Dhcp4": {
    // Configure which network interface Kea should listen on
    "interfaces-config": {
      "interfaces": [ "eth1" ]
    },

    // Subnet and IP allocation pool
    "subnet4": [
      {
        "subnet": "192.168.2.0/24",
        "pools": [
          { "pool": "192.168.2.2 - 192.168.2.200" }
        ],

        "option-data": [
          { "name": "routers", "data": "192.168.2.1" },
          { "name": "domain-name-servers", "data": "1.1.1.1, 9.9.9.9" }
        ],

        // IP of the TFTP server (your host machine)
        "next-server": "192.168.2.155",
      }
    ],

    // Define custom PXE-related DHCP options
    "option-def": [
      {
        // Control PXE discovery behavior
        "name": "PXEDiscoveryControl",
        "code": 6,
        "space": "vendor-encapsulated-options-space",
        "type": "uint8"
      },
      {
        // Menu prompt shown by PXE client
        "name": "PXEMenuPrompt",
        "code": 10,
        "space": "vendor-encapsulated-options-space",
        "type": "record",
        "record-types": "uint8,string"
      },
      {
        // Boot menu entry
        "name": "PXEBootMenu",
        "code": 9,
        "space": "vendor-encapsulated-options-space",
        "type": "record",
        "record-types": "uint16,uint8,string"
      }
    ],

    // Match clients that identify as PXE bootloaders
    "client-classes": [
      {
        "name": "rpi-pxe",

        // Match Raspberry Pi 5 firmware PXE signature
        "test": "option[vendor-class-identifier].text == 'PXEClient:Arch:00000:UNDI:002001'",

        "option-data": [
          // First file to load over TFTP — starts boot process on Pi 5
          { "name": "boot-file-name", "data": "config.txt" },

          // Identify client type
          { "name": "vendor-class-identifier", "data": "PXEClient" },

          // Required for custom PXE options
          { "name": "vendor-encapsulated-options" },

          // Define a boot menu entry shown by PXE clients
          {
            "name": "PXEBootMenu",
            "csv-format": true,
            "data": "0,17,Raspberry Pi Boot",
            "space": "vendor-encapsulated-options-space"
          },

          // Control boot behavior (3 = display menu, send menu prompt)
          {
            "name": "PXEDiscoveryControl",
            "data": "3",
            "space": "vendor-encapsulated-options-space"
          }
        ]
      }
    ],

    // Store DHCP lease database in memory (and persist it to disk)
    "lease-database": {
      "type": "memfile",
      "persist": true,
      "name": "/var/lib/kea/kea-leases4.csv"
    },

    // Enable detailed logging to systemd journal
    "loggers": [
      {
        "name": "kea-dhcp4",
        "output_options": [
          {
            "output": "stdout" // Systemd catches stdout and routes to journalctl
          }
        ],
        "severity": "DEBUG",   // Most verbose log level
        "debuglevel": 10       // 99 is Maximum verbosity
      }
    ]
  }
}

Enable and start the service:

sudo systemctl enable kea-dhcp4-server
sudo systemctl restart kea-dhcp4-server

Install and Configure TFTP Server

sudo apt install tftpd-hpa

Create directories and config:

sudo mkdir -p /opt/tftp
sudo tee /etc/default/tftpd-hpa > /dev/null <<EOF
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/opt/tftp"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure -vvvv"
EOF

Enable and start the TFTP service:

sudo systemctl enable tftpd-hpa
sudo systemctl restart tftpd-hpa

Upload bootfs for Your Board

To find your board's serial number, tail the TFTP logs:

sudo journalctl -f -u tftpd-hpa.service

Look for a line like:

RRQ from 192.168.2.110 filename b7afe72d/config.txt

Here, b7afe72d is the board’s serial number.

Before we upload bootfs we need to modify cmdline.txt to substitue <nothing here yet> to b7afe72d, once it is done, recreate tar package.

Now extract bootfs.tar to the correct location:

sudo mkdir -p /opt/tftp/b7afe72d
sudo tar -xf bootfs.tar -C /opt/tftp/b7afe72d

Serve rootfs via NFS

Once kernel is running it will look for rootfs, which we need to server over nfs.

Install and configure NFS server

sudo apt install nfs-kernel-server

Create directory and config:

sudo mkdir -p /opt/nfs
sudo sed -i '/^RPCMOUNTDOPTS=/d' /etc/default/nfs-kernel-server
echo 'RPCMOUNTDOPTS="--port 20048 --bind 0.0.0.0"' | sudo tee -a /etc/default/nfs-kernel-server > /dev/null
echo "/opt/nfs/b7afe72d 192.168.2.0/24(rw,sync,no_root_squash,no_subtree_check)" | sudo tee -a /etc/exports

Enable and start the NFS service:

sudo systemctl enable nfs-kernel-server
sudo systemctl restart nfs-kernel-server

Now extract rootfs ot the correct location:

sudo mkdir -p /opt/nfs/b7afe72d
sudo tar -xf rootfs.tar -C /opt/nfs/b7afe72d

Now restart raspberry, once linux boots up you can login using root username and empty password. To confirm that our custom kernel is really in place check custom kernel version:

uname -r

which should output sth like:

6.6.78-SuperKernel+

Conclusions

Whew—this guide covers everything you need to get started with Linux kernel development on the Raspberry Pi 5, from compiling the kernel to setting up network booting with TFTP and NFS. I hope it helps make the whole process feel a little less intimidating and a lot more efficient. No more swapping SD cards—just fast, repeatable development. Enjoy hacking on the kernel!

Published:

Last updated:

Category: Linux

Comments

Leave a comment