Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

linux-rt — PREEMPT_RT kernels for Raspberry Pi 4 / 5

CI-built PREEMPT_RT kernel Debian packages for distributions that do not ship one, based on the Raspberry Pi kernel tree.

  • Source: https://github.com/raspberrypi/linux, branch rpi-6.18.y
  • Kernel version selected by tag: v6.18.40 → builds 6.18.40 (default v6.18.40)
  • Targets: pi4 (BCM2711, 4K pages) and pi5 (BCM2712, 16K pages)
  • Output: linux-image / linux-headers / linux-libc-dev .debs from make bindeb-pkg LOCALVERSION=-rt, plus rpi-boot-install.sh

No RT patch is applied or needed: PREEMPT_RT has been in mainline since 6.12 and arm64 selects ARCH_SUPPORTS_RT. Raspberry Pi even ships an official arch/arm64/configs/bcm2711_rt_defconfig, so RT on the Pi 4 is a vendor-supported configuration. There is no bcm2712_rt_defconfig, so the Pi 5 config is derived here from bcm2712_defconfig.

Building

Actions → Build RT kernelsRun workflow:

Input Meaning
kernel_ref Kernel version (v6.18.40, v6.18.40-2), branch, tag or SHA
targets both, pi4 or pi5
release_tag Publish the packages as a GitHub release under this tag

Releasing a kernel version

git tag v6.18.40 && git push origin v6.18.40

That builds kernel 6.18.40 for both boards and publishes the packages as release v6.18.40 — the tag is the version selector. Tags must be v<major>.<minor>.<sublevel>[-<revision>]; anything else is rejected with a clear error instead of failing later in the build.

To rebuild the same kernel after changing the config or packaging, add a revision:

git tag v6.18.40-2 && git push origin v6.18.40-2

Same kernel commit, release v6.18.40-2, and the revision becomes the Debian package revision — linux-image-6.18.40-v8-rt_6.18.40-v8-rt-2_arm64.deb — so apt/dpkg treat it as an upgrade over the previous build.

The Raspberry Pi tree has no per-version tags (only stable_YYYYMMDD); it merges the upstream stable releases into a rolling rpi-<maj>.<min>.y branch. scripts/resolve-kernel-ref.sh therefore turns v6.18.40 into the newest commit on rpi-6.18.y whose Makefile still reports 6.18.40 — i.e. the stable release plus any Pi fixes on top of it, and never the first commit of 6.18.41.

If that version is still at the branch tip the tip is used directly. Otherwise the search runs over the branch's first-parent history, which is the part that is easy to get wrong: the branch merges upstream stable, so a plain history walk also sees Linux 6.18.41 and its ancestors, which report the right version while containing none of the Pi patches — no bcm2711_defconfig, no Pi drivers. Every first-parent ancestor is a Pi-branch commit by construction, and the result is checked for arch/arm64/configs/bcm2711_defconfig regardless. The history is fetched commits-only (--filter=tree:0), so it costs a few hundred MB once rather than a full clone.

The workflow resolves once in the prepare job so both boards build the identical commit, and build.sh re-asserts the version of the checked-out tree before configuring, so a tag can never quietly build something else.

Two consequences worth knowing:

  • While a version is still at the branch tip, v6.18.40 follows that tip, so re-running the same tag can pick up Pi fixes landed since. Once 6.18.41 appears the resolution is frozen for good.
  • The commit actually built is recorded in build-info-<target>.txt and in the release notes; pass that SHA as kernel_ref to reproduce a build exactly.

Locally the same build runs with:

./scripts/build.sh pi4          # packages land in ./dist

Local requirements (Debian/Ubuntu):

sudo apt install gcc-aarch64-linux-gnu libc6-dev-arm64-cross bc bison flex \
                 libssl-dev libelf-dev dwarves debhelper dpkg-dev kmod cpio \
                 rsync zstd ccache

libc6-dev-arm64-cross is easy to miss: kernel code is freestanding and never includes libc headers, but the linux-headers package rebuilds fixdep and modpost for the target so out-of-tree modules can be built on the Pi, and that compiles userspace code. build.sh probes for it up front.

A CONFIG_ONLY=1 run needs only the first six plus dwarves; the packaging tools are not checked for. Either way build.sh verifies what it needs in its first second and names anything missing.

Budget ~30 GB of free disk for a full local build — CONFIG_DEBUG_INFO (needed for BTF) makes the object tree large. A config-only check is far cheaper (~2 GB, a couple of minutes, no compile) and catches Kconfig regressions on its own:

CONFIG_ONLY=1 ./scripts/build.sh pi4

Both targets pass that check against 6.18.40; the resulting kernel releases are 6.18.40-v8-rt and 6.18.40-v8-16k-rt.

CI checks

ci.yml runs on every push and pull request (tag pushes go to build.yml instead) and takes a few minutes:

  • lintshellcheck over scripts/ and packaging/, and a parse of every workflow file.
  • configCONFIG_ONLY=1 ./scripts/build.sh for both boards, so a Kconfig option that silently stops applying is caught without compiling anything.
  • packaging./scripts/build.sh smoke: a tinyconfig kernel taken through the complete bindeb-pkg path in a few minutes, so packaging breakage no longer waits an hour to appear.

smoke is not an RT build and proves nothing about rt.fragment — it exists to exercise the packaging work: modules get installed, stripped, compressed and depmod-ed, the -dbg package is generated, and linux-headers cross-builds fixdep/modpost. It then checks the results: the image package carries vmlinuz and at least one .ko, and the headers package's fixdep is an AArch64 binary — if it were not, out-of-tree module builds on the Pi would fail, and nothing else in the pipeline would notice.

./scripts/build.sh smoke     # ~7 min cold, ~2 min incremental

Installing on the Pi

sudo dpkg -i linux-image-6.18.40-v8-rt_*.deb   # Pi 5: linux-image-6.18.40-v8-16k-rt_*.deb
sudo ./rpi-boot-install.sh
sudo reboot
uname -a                                        # expect "PREEMPT_RT"

Add the matching linux-headers-*.deb if you build out-of-tree modules against this kernel — see Native XDP on the Pi NICs.

The .deb puts the kernel in /boot/vmlinuz-<release>, its device trees in /usr/lib/linux-image-<release>/ and its modules in /lib/modules/<release> — none of which is where the Raspberry Pi firmware looks. rpi-boot-install.sh bridges that gap: it detects the board, decompresses vmlinuz into /boot/firmware/kernel8-rt.img (Pi 4) or kernel_2712-rt.img (Pi 5), copies the device trees and overlays, and appends a marked, board-conditional block to config.txt:

# BEGIN linux-rt (6.18.40-v8-rt)
[pi4]
kernel=kernel8-rt.img
[all]
# END linux-rt (6.18.40-v8-rt)

The distro kernel image is never overwritten, so a bad boot is fixed by deleting that block from any other machine; sudo ./rpi-boot-install.sh --uninstall does the same in place. Replaced device trees are backed up to /boot/firmware/linux-rt-backup/. On Debian systems with the raspi-firmware package installed, its kernel hook already performs this step and the script is unnecessary.

Release naming keeps the Pi convention, so both boards can be served from one release and module directories never collide with the distro's: CONFIG_LOCALVERSION from the defconfig (-v8 / -v8-16k) plus LOCALVERSION=-rt on the make command line → 6.18.40-v8-rt and 6.18.40-v8-16k-rt.

Configuration

All deviations from the Pi defconfig live in config/rt.fragment and are re-checked after make olddefconfig by scripts/verify-config.sh — Kconfig drops unsatisfiable symbols silently, and two of the requested options are affected:

  • CONFIG_DEBUG_INFO_BTF needs DWARF debug info, and the "Debug information" choice is hidden behind CONFIG_DEBUG_KERNEL, which the Pi defconfigs do not set. Enabling BTF alone is a no-op; the fragment therefore also sets DEBUG_KERNEL=y and DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y. pahole ≥ 1.21 must be installed on the builder (the workflow installs dwarves).
  • CONFIG_NO_HZ_FULL is part of a Kconfig choice; the defconfig sits on NO_HZ_IDLE, which must be unset explicitly or the choice can resolve back. The same applies to the preemption choice. NO_HZ_FULL already selects RCU_NOCB_CPU, CPU_ISOLATION and VIRT_CPU_ACCOUNTING_GEN.

The fragment adds one option beyond the requested set: CONFIG_TIMERLAT_TRACER, which bcm2711_rt_defconfig also enables and which rtla timerlat needs.

Things worth knowing

  • CONFIG_PREEMPT=y and CONFIG_PREEMPT_RT=y coexist — that is correct. PREEMPT_RT sits outside the "Preemption Model" choice, and since PREEMPT_NONE/PREEMPT_VOLUNTARY depend on !PREEMPT_RT, the choice resolves to its first remaining member, PREEMPT. The expanded bcm2711_rt_defconfig contains exactly the same set, so do not try to unset it. CONFIG_PREEMPTION is the symbol that says the kernel is preemptible.
  • What RT costs in driver terms: compared with the plain Pi defconfig, the only features dropped are the ones that declare depends on !PREEMPT_RTLEDS_TRIGGER_CPU and IR_GPIO_TX — plus internal changes RT implies (QUEUED_RWLOCKS, SOFTIRQ_ON_OWN_STACK, tick-based CPU accounting).
  • NO_HZ_FULL does nothing without a command line. Add isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 irqaffinity=0,1 to /boot/firmware/cmdline.txt to actually get tickless isolated CPUs; without it you only pay the context-tracking overhead. On a 4-core Pi, dedicating two cores is the usual split.
  • Modular BCMGENET/MACB means the Pi must not need the network to reach its root filesystem. SD/USB root is fine (storage drivers stay built in); NFS root needs rpi-boot-install.sh --initramfs or built-in drivers. Keeping them modular is also what lets the in-tree drivers be swapped for the native XDP ones below without rebuilding the kernel.
  • A -dbg package is discarded by default: CONFIG_DEBUG_INFO makes mkdebian emit linux-image-<release>-dbg at roughly a gigabyte. Set KEEP_DBG=1 to keep it.
  • RT and the vendor tree. bcm2711_rt_defconfig exists precisely because a few drivers depend on !PREEMPT_RT (e.g. LEDS_TRIGGER_CPU); olddefconfig drops those automatically. Downstream Pi drivers (VC4/DRM, camera) see much less RT testing than the mainline core — validate with cyclictest and rtla timerlat before trusting latency numbers.
  • Version pinning is described under Releasing a kernel version.

Native XDP on the Pi NICs

The in-tree bcmgenet (Pi 4) and macb (Pi 5) drivers have no native XDP support, so CONFIG_XDP_SOCKETS alone gets you AF_XDP in copy/SKB mode only — the packet still traverses sk_buff allocation before any BPF program sees it.

Out-of-tree drivers that retrofit native XDP onto both NICs are available at https://github.com/stratuMAK/xdp-backports. They are derived from the same rpi-6.18.y tree this kernel is built from, target PREEMPT_RT, and add page_pool RX plus ndo_bpf/ndo_xdp_xmit — which is what the userspace EtherCAT master at stratuMAK/ethercat, a userspace port of the IgH EtherCAT master, uses for its xdp-native transport.

They install as genet_xdp.ko / macb_xdp.ko replacing the in-tree modules — hence CONFIG_MACB=m and CONFIG_BCMGENET=m here. Their DKMS packages rebuild against the running kernel's headers, so install the matching headers package from this repo alongside the image:

sudo dpkg -i linux-image-6.18.40-v8-rt_*.deb linux-headers-6.18.40-v8-rt_*.deb

Build cost

A full defconfig build on a 4-vCPU GitHub-hosted runner takes roughly 45–90 minutes per board (BTF generation adds a few minutes); ccache brings repeat builds down substantially. Both boards build in parallel. The workflow cross-compiles from ubuntu-24.04 with gcc-aarch64-linux-gnu, which works on private repositories too; scripts/build.sh also runs unmodified on an ubuntu-24.04-arm runner (free for public repos), where it skips the cross prefix automatically.

scripts/build.sh compiles with make -j$(nproc) before calling bindeb-pkg, because dpkg-buildpackage invokes debian/rules with -j1 and only inherits parallelism through make's jobserver; doing the heavy lifting up front means the packaging step has nothing left to compile either way.

Two consequences of cross-building the packages, both handled in build.sh:

  • bindeb-pkg runs dpkg-buildpackage -a arm64, so dpkg-checkbuilddeps demands arm64 builds of libssl-dev/libdw-dev, which cannot be installed on an amd64 runner. Everything is already compiled with our own cross toolchain by then, so the check is skipped via DPKG_FLAGS=-d rather than satisfied with a foreign-architecture apt setup.
  • ccache is wired in through PATH, never through CC=. debian/rules runs make without our overrides, so an overridden CC would make every recorded .cmd line differ from the pre-build and kbuild would recompile the whole tree inside the packaging step.

A preflight check at the start of build.sh verifies the toolchain, the packaging tools and the pahole version in about a second, so a missing debhelper fails immediately instead of after an hour of compiling.

About

RT enabled linux kernel packages (RPi4/RPi5)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages