80 lines
3.2 KiB
Markdown
80 lines
3.2 KiB
Markdown
|
|
# 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.
|