Skip to content

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.

Terminal window
$ echo 3 > /proc/irq/41/smp_affinity_list
tee: /proc/irq/41/smp_affinity_list: Operation not permitted

Your PPS arrives on a GPIO pin, and GPIO interrupts on the BCM2711 are not first-class interrupts. They are demultiplexed through the GPIO controller:

Terminal window
$ 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.-1

Look 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.

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:

Terminal window
$ 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 timestamppps-gpio takes its measurement inside that handler, so putting it behind the scheduler costs more than the isolation ever gives back.

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=1

That’s the whole strategy. It’s not the one in the guides, but it’s the one the hardware permits. → cpu0 is sacred