Ryan Malloy 6881489bf6 The Cuckoo Escapement: field report, kernel patch, dashboard
What the Raspberry Pi time-server guides get wrong on a Pi 4, with the
measurements. The headline artifact is a four-line pps-gpio patch: PREEMPT_RT
force-threads IRQ handlers, and pps-gpio takes its timestamp inside its handler,
so the realtime kernel puts a scheduler between the electrical edge and the
clock. IRQF_NO_THREAD takes RMS offset from 2468 ns to 199 ns.

- kernel/     the patch
- dashboard/  live status page (position hidden by default)
- docs-site/  the write-up (Astro/Starlight, brass, no tutorial section)
2026-07-14 09:21:25 -06:00

80 lines
3.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](https://github.com/josh-blake/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)
```bash
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.
```bash
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.