The interrupt you cannot move
Every guide says the same thing: park the PPS interrupt on its own isolated CPU, give it realtime priority, and keep the noisy world away from it.
On a Raspberry Pi 4, you cannot.
$ echo 3 > /proc/irq/41/smp_affinity_listtee: /proc/irq/41/smp_affinity_list: Operation not permittedYour PPS arrives on a GPIO pin, and GPIO interrupts on the BCM2711 are not first-class interrupts. They are demultiplexed through the GPIO controller:
$ grep -E 'pps|uart' /proc/interrupts 40: 3532866 0 0 0 GICv2 153 Level uart-pl011 41: 104835 0 0 0 pinctrl-bcm2835 18 Edge pps@12.-1Look at the difference. The UART is a GICv2 interrupt — a real line into the
interrupt controller, and it takes an affinity happily. The PPS is a
pinctrl-bcm2835 interrupt — one of dozens of GPIO lines multiplexed behind
a single parent IRQ. There is no per-line steering to give. Every GPIO interrupt
lands wherever the GPIO controller’s parent lands, together.
So the PPS interrupt goes where it goes, and no amount of configuration moves it.
The cruel bit
Section titled “The cruel bit”There is one way to gain control of it: PREEMPT_RT force-threads interrupt
handlers, and a thread can be taskset anywhere. Boot a realtime kernel and the
thing you couldn’t pin becomes pinnable:
$ ps -eo pid,class,rtprio,psr,comm | grep irq/41 239 RR 50 3 irq/41-pps@12.-1 ← RT priority, isolated CPU 3. It worked!The guides are vindicated. Except it’s a trap, because
threading the handler is what destroys the
timestamp — pps-gpio takes its
measurement inside that handler, so putting it behind the scheduler costs more
than the isolation ever gives back.
What to do instead
Section titled “What to do instead”Accept that the PPS interrupt lives on CPU 0, and then treat CPU 0 as sacred. You can’t move the interrupt, but you can move everything else:
# every other service gets an affinity that isn't 0[Service]CPUAffinity=1That’s the whole strategy. It’s not the one in the guides, but it’s the one the hardware permits. → cpu0 is sacred