Ryan Malloy 3b505644b2 Kernel release 6.18.38-rt-cuckoo+: reproducible build, guarded installer
Rebuilt with KBUILD_BUILD_USER/HOST/TIMESTAMP pinned. Without them the kernel
bakes the builder's user@hostname and wall-clock build time into /proc/version,
which publishes operator identity in a public artifact and makes the build
unreproducible. Pinned, anyone can rebuild and diff the bytes — the only trust
story available for an unsigned community kernel.

The installer refuses to run on anything but a Pi 4, refuses a mismatched
image/modules pair (vermagic failure means no /dev/pps0 on a kernel that
otherwise boots fine — silent and miserable to debug), verifies SHA256SUMS,
never touches kernel8.img, and leaves rollback as deleting one config.txt line.

Boot-tested on the Pi: Stratum 1, 273 ns offset, PPS handler confirmed NOT
threaded. Binaries ship as release assets, not in git.
2026-07-14 09:52:17 -06:00
..

PREEMPT_RT kernel for gps-ntp (Raspberry Pi 4)

A cross-compiled RPi-native realtime kernel, plus a one-line pps-gpio fix that turned out to be the whole point.

Why

Chasing PPS jitter. The theory (from pixie) is that PREEMPT_RT plus CPU isolation lets you park the PPS interrupt on a dedicated core at realtime priority.

On a Pi 4 that doesn't work out of the box, for two reasons we found the hard way:

  1. The PPS interrupt can't be moved on a stock kernel. It's a GPIO interrupt demuxed through pinctrl-bcm2835, so writing smp_affinity returns Operation not permitted. Individual GPIO lines follow the GPIO controller's parent IRQ. Pixie's approach assumes a Pi 5.

  2. PREEMPT_RT alone made jitter 3× worse. RT force-threads interrupt handlers — but pps-gpio timestamps the pulse inside its handler (pps_get_ts()pps_event()). Threaded, that timestamp is taken after thread-wakeup latency instead of at the electrical edge. We measured raw PPS jitter go from 2134 ns → 6947 ns.

The fix

0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch adds IRQF_NO_THREAD to the pps-gpio IRQ request, so the timestamp stays in hard-irq context while the rest of the system keeps RT's preemptibility.

Ironically, RT also solved problem (1): a force-threaded IRQ is a schedulable thread, and a thread can be taskset to an isolated core — which the stock kernel refused. So RT unlocked the pinning the hardware denied us, and the patch undoes the damage RT did on the way.

Results (measured, chrony sourcestats / raw ppstest)

Config RMS offset Raw PPS jitter
Baseline (stock kernel, stock chrony) 823 ns
+ chrony median-filter/prefer 440 ns 2134 ns
+ PREEMPT_RT (threaded PPS) 2468 ns 6947 ns
+ IRQF_NO_THREAD patch 199 ns 2568 ns

Build (cross-compile from x86)

sudo pacman -S --needed aarch64-linux-gnu-gcc      # Arch/EndeavourOS
git clone --depth=1 --branch rpi-6.18.y https://github.com/raspberrypi/linux
cd linux
export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
make bcm2711_defconfig
./scripts/config --enable EXPERT --enable PREEMPT_RT \
                 --set-str LOCALVERSION "-rt-timepi" --disable LOCALVERSION_AUTO
make olddefconfig
patch -p1 < ../0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch
make -j$(nproc) Image modules dtbs

Deploy (safe, revertible, headless-friendly)

The Pi 4's SD and ext4 drivers are built-in (CONFIG_MMC_BCM2835=y, CONFIG_EXT4_FS=y), so no initramfs is needed. Install the kernel under a new name and leave kernel8.img alone — the whole change becomes one revertible line.

gzip -9 -c arch/arm64/boot/Image > Image.gz        # match Pi OS's gzip format
scp Image.gz pi:/tmp/ && scp rt-modules.tar.gz pi:/tmp/
# on the Pi:
sudo cp /tmp/Image.gz /boot/firmware/kernel-rt.img   # NEW name; kernel8.img untouched
sudo tar xzf /tmp/rt-modules.tar.gz -C /             # new /lib/modules/<rel>/
sudo depmod 6.18.38-rt-timepi+
echo 'kernel=kernel-rt.img' | sudo tee -a /boot/firmware/config.txt

Recovery: if it doesn't boot, pull the card and delete the kernel=kernel-rt.img line. The stock kernel returns.