BPI-R64 fully-tickless hardpps soak — 5 s GPS timepulse
Cortex-A53, 12.5 MHz counter, NO_HZ_IDLE, all periodic wakeup sources
quiesced (vmstat, runtime-PM autosuspend, fair dl_server, ethernet pollers) —
the only wakeup is the GPS pulse every 5 s. hardpps disciplines
CLOCK_REALTIME via STA_PPSTIME with phase delivery scaled to 1/5 rate
(matching the 5 s delivery window — the unscaled rate is unstable, gain
−5). Kernel carries the timekeeping series; snapshot ntp_error
captured in the PPS hardirq at every pulse.
6095 pulses over 8.5 h.
Steady state (|ntp_error| at pulse, after 60 min):
median 4 ns,
p95 759 ns,
p99 4281 ns,
max 18180 ns.
Corrected pulse phase: median 5.5 µs,
p95 33.1 µs (irq-latency floor).
Per-pulse snapshot ntp_error
Pulse arrival phase (corrected ts_real)
Slew term attribution
Discipline state
Tickless gap verification
Methodology
This run is the test asked for on the list: hardpps on a tickless kernel
driven by a real PPS source, with its real jitter and IRQ latency, and the
CPU genuinely idle between pulses.
- Hardware: Banana Pi R64 (MT7622, 2×Cortex-A53), 12.5 MHz
arch counter, u-blox GNSS HAT. Timepulse reconfigured to one pulse per
5 s (UBX-CFG-TP5, GNSS-locked, aligned to top of second) so idle gaps
are bounded by the pulse, not the pulse by the gaps. PPS on a GPIO via
pps-gpio; timestamp taken in the hardirq handler.
- Kernel: the timekeeping series on 7.3-rc1,
CONFIG_NO_HZ_IDLE.
For this run STA_PPSTIME phase delivery is scaled to 1/5 rate to match the
5 s delivery window (the stock undamped rate is computed for a 1 s
window; across a 5 s tickless gap it over-delivers 5× and the phase
loop diverges geometrically — found by this bench, fix to be part of the
series before NTP_PPS loses its !NO_HZ_COMMON dependency).
- Quiescence: no distro userspace — a single static PID 1
binary sleeps between once-a-minute dumps. All periodic wakeup sources found
by tracing were disabled: vmstat interval raised, runtime-PM autosuspend
clamped, per-CPU fair dl_server off, ethernet (mtk_eth_soc + mt7530 1 s
pollers) not probed. The per-minute log dump and its mmc/console I/O are
issued immediately after a pulse edge so they never perturb a
timestamp. The gap histogram (above) verifies the result: the timekeeper
routinely advances in single 5 s bursts (1250 ticks at HZ=250) —
the only wakeup between dumps is the pulse itself.
- Instrumentation: at each pulse the hardirq records
snapshot_ntp_error() — the divergence between the ideal
NTP-disciplined time and the sanitized clock_gettime() line that
the series corrects in pps_get_ts() — plus the corrected
ts_real phase against the second boundary. The kernel discipline
state (hardpps corrections, second_overflow decisions, every mult step) is
logged via printk to the kmsg ring and drained to disk per dump.
- Caveats: the ∼30 µs correction stripe in the slew plot is
bench self-noise (a recurring IRQ-latency event, one-sided: latency can only
make a pulse look late), and this kernel still carries the trace printks;
a quiet-kernel confirmation run and the tickful (
nohz=off) A/B
comparison on identical hardware are the follow-ups.
Data
- pulses.csv — per-pulse snapshot
ntp_error, tk ledger, cycle_delta, corrected ts_real phase
- hardpps_phase.csv — per-pulse
raw error, median-filtered correction, jitter, injected time_offset
- adjtimex.csv — PLL state, 1/min
- hist.csv — per-minute log2 histogram of
timekeeping-advance gap sizes + max
- ntp_so.csv,
tk_mult.csv,
tk_cancel.csv — kernel discipline
traces (second_overflow chunks, every mult step, mid-tick cancels)
- bench.log.gz — the raw on-board log
(dumps + kmsg), from which the CSVs are extracted