Horloge matérielle (PHC) et phc2sys#

Pourquoi#

Même avec un noyau temps réel bien configuré, un timestamp logiciel (pris au moment où le kernel traite le paquet) accumule 10 à 100 µs de latence non-déterministe entre l’arrivée physique du signal sur le câble réseau et le moment où le timestamp est réellement enregistré : réception PHY, DMA vers la RAM, interruption, traversée de la pile réseau. Pour servir des clients avec une précision meilleure que la dizaine de microsecondes, il faut sortir cette latence de l’équation, en demandant à la carte réseau elle-même d’horodater le paquet, avant tout traitement logiciel.

Comment#

Le PHC (PTP Hardware Clock)#

Le contrôleur réseau du Raspberry Pi 5 expose une horloge matérielle dédiée accessible via /dev/ptp0, le PHC, standardisé par IEEE 1588. Chrony peut l’utiliser directement pour horodater les paquets NTP entrants et sortants au niveau du silicium, indépendamment de la latence du noyau.

# Dans chrony.conf
hwtimestamp end0 rxfilter all txcomp 0 rxcomp 0 minpoll 0

hwtimestamp end0 seul est incomplet : sans ces options explicites, chrony doit deviner les meilleurs réglages, ce qui ne fonctionne pas toujours de façon fiable. rxfilter all demande d’horodater tous les paquets reçus au niveau matériel, pas seulement ceux reconnus comme PTP par le filtre du contrôleur (sans quoi le trafic NTP ordinaire ne bénéficierait pas du timestamp matériel). txcomp 0 active l’horodatage matériel à la complétion de l’émission, sans compensation logicielle additionnelle (0). rxcomp 0 fait de même en réception : aucune correction n’est appliquée après coup, le timestamp matériel brut est pris tel quel. minpoll 0 n’impose aucun plancher artificiel à la fréquence de mesure au-delà du grain standard de 2⁰ = 1 seconde.

phc2sys : maintenir le PHC aligné#

Le PHC a besoin d’être maintenu synchronisé sur l’horloge système (qui, elle, est disciplinée par chrony depuis le PPS). C’est le rôle de phc2sys, du paquet linuxptp :

cat > /etc/systemd/system/phc2sys-chrony.service <<'EOF'
[Unit]
Description=Synchronize PHC to system clock
After=chrony.service
Wants=chrony.service

[Service]
Type=simple
ExecStart=/usr/sbin/phc2sys -s CLOCK_REALTIME -c /dev/ptp0 -O 0 -R 1 -q
Restart=always
RestartSec=5

CPUSchedulingPolicy=fifo
CPUSchedulingPriority=90
CPUAffinity=1

[Install]
WantedBy=multi-user.target
EOF

systemctl enable --now phc2sys-chrony.service

-s CLOCK_REALTIME désigne l’horloge système (disciplinée par chrony depuis le PPS) comme source de référence pour le PHC, -c /dev/ptp0 la cible à aligner. -O 0 fixe l’offset TAI-UTC à zéro : ce n’est pas un oubli des secondes intercalaires, c’est chrony (via leap-seconds.list, chapitre dédié) qui gère déjà cet aspect, phc2sys n’a pas à le refaire en double. -q (quiet) supprime la journalisation par défaut, sans quoi le tmpfs /var/log (chapitre Système de fichiers) se remplirait rapidement d’une ligne par mesure.

Erreurs à éviter#

Requires=chrony.service casse silencieusement phc2sys en production. Une dépendance systemd Requires= arrête automatiquement le service dépendant à chaque transition d’état de sa dépendance, y compris un simple redémarrage ou un échec temporaire de chrony. Un incident réel sur ce projet : phc2sys s’arrêtait silencieusement à chaque cycle de chrony, laissant le PHC dériver de 94 µs par seconde sans aucune alerte visible. La solution : Wants=chrony.service (une dépendance souple, qui n’entraîne pas d’arrêt en cascade), combiné à Restart=always pour la résilience en cas de crash réel.

La fréquence de mesure -R de phc2sys doit être réduite, pas maximisée. Le PHC est un compteur matériel unique, lu et écrit simultanément par deux processus concurrents : phc2sys qui l’écrit pour le maintenir aligné, et chrony qui le lit pour horodater les paquets entrants. Le pilote réseau ne sérialise pas explicitement ces accès. Mesures en conditions réelles : à -R 16 (valeur par défaut de linuxptp), seulement 63% des paquets reçoivent un timestamp matériel (le reste bascule silencieusement sur un timestamp logiciel, moins précis) ; à -R 1, ce taux monte à 86%. Réduire la fréquence d’écriture réduit la contention, au prix d’un offset PHC marginalement plus élevé, mais qui reste largement sous la microseconde.

  • S’attendre à un horodatage matériel TX proche de 100%. Sur Pi 5, le pilote réseau ne pose le flag matériel nécessaire à l’horodatage TX que pour les paquets reconnus comme PTP par le filtre matériel, les paquets NTP “normaux” n’en bénéficient pas. C’est un bug documenté du pilote (raspberrypi/linux, issue #5904), pas une erreur de configuration : le taux d’horodatage matériel TX reste proche de 0% quelle que soit la configuration. L’impact réel côté client est modeste (quelques dizaines de microsecondes au pire, largement filtré par la PLL du client).

Vérifier#

# Le PHC doit être détecté sur l'interface
ethtool -T end0
# Doit afficher "PTP Hardware Clock: 0" (ou un autre numéro, pas "none")

# Offset instantané entre le PHC et l'horloge système
phc_ctl /dev/ptp0 cmp
# ex : "offset from CLOCK_REALTIME is 14ns"

Une lecture unique de phc_ctl cmp est un instantané non filtré, donc parfois sujet à un pic ponctuel d’ordonnancement (quelques centaines de ns à quelques µs). Pour un diagnostic fiable, mieux vaut répéter la lecture plusieurs fois avec une priorité temps réel (chrt -f 99) et prendre la médiane, exactement la même logique de filtrage statistique qu’utilise chrony pour le PPS (chapitre Chrony).

# Taux d'horodatage matériel effectif, côté chrony
chronyc -h /run/chrony/chronyd.sock serverstats | grep -i hardware

Limite actuelle : le PPS n’est pas horodaté en matériel#

Le PHC de ce Pi 5 horodate les paquets réseau, mais il ne peut pas horodater le PPS : le contrôleur réseau intégré (Cadence GEM via le southbridge RP1) n’expose aucune broche de timing, et le Pi 5 ne présente aucune entrée “PPS IN” routée vers la carte réseau. Il n’existe donc aucun point matériel où injecter le PPS du GNSS dans le PHC. Vérification directe :

cat /sys/class/ptp/ptp0/n_external_timestamps   # 0 : aucune entrée EXTTS
cat /sys/class/ptp/ptp0/n_periodic_outputs      # 0 : aucune sortie PEROUT
cat /sys/class/ptp/ptp0/n_programmable_pins     # 0 : aucune broche SDP

Le pps_available = 1 du même répertoire ne désigne qu’un PPS interne émis par le PHC vers l’API noyau, pas une entrée pour un signal externe. Conséquence : le PPS du GNSS ne peut être capté que par le GPIO, via pps-gpio et une interruption noyau, dont la latence (~5 à 30 µs en PREEMPT_RT, chapitre Noyau temps réel) constitue le plancher de bruit du système. C’est la raison de fond pour laquelle la performance se situe “autour de la microseconde” et non au niveau nanoseconde.

[ALTERNATIVE] Horodater le PPS en matériel avec une NIC à broches de timing. Une carte réseau exposant ses broches SDP lèverait cette limite. Le HAT PCIe TimeHATv6 (NIC Intel i226) les expose : une entrée SMA (SDP1) horodate le front PPS dans le silicium (EXTTS), une sortie SMA (SDP0) génère un PPS (PEROUT), et un emplacement M.2 accueille un module GNSS dont le 1PPS est routé directement sur la NIC ; un TCXO intégré assure le holdover pendant une panne réseau. Le PHC deviendrait une vraie référence de discipline, potentiellement au niveau de la dizaine de nanoseconde, et non plus un simple horodateur de paquets. Une bonne piste d’évolution.

L’erreur en dents de scie (sawtooth) du PPS#

Même en horodatant le PPS en matériel, il resterait une source d’erreur plus fine, propre au récepteur : l’erreur en dents de scie (sawtooth).

Le récepteur ne peut poser le front du PPS que sur un tic de son horloge interne, une grille quantifiée. La seconde UTC exacte tombe presque toujours entre deux tics ; le récepteur retient le plus proche, et un petit décalage subsiste à chaque impulsion. Comme la fréquence interne n’est pas un multiple entier exact du hertz, ce décalage dérive lentement d’une seconde à l’autre, atteint une demi-période d’horloge, puis bascule de l’autre côté et recommence, d’où le motif en dents de scie :

 err
+T/2 ┤   /|   /|   /|
   0 ┤  / |  / |  / |
     │ /  | /  | /  |
-T/2 ┤/   |/   |/   |
     └────┴────┴────┴──►  t

Son amplitude est bornée à la moitié de la période de l’horloge de référence du timepulse : de quelques nanosecondes à quelques dizaines de nanosecondes sur ce récepteur (la spécification “timepulse ~30 ns RMS” du M9N l’inclut déjà).

Ce sawtooth est propre à la génération du PPS, pas à sa capture : l’horodatage matériel (le HAT ci-dessus) supprime le jitter d’interruption, mais pas le sawtooth, déjà inscrit dans le front. Les deux corrections sont donc complémentaires.

Le compenser. Le récepteur mesure de combien il a raté chaque seconde et publie cette valeur, la quantization error. Chez u-blox, elle est portée par le message binaire UBX-TIM-TP (champ qErr, en picosecondes, signé, une valeur par impulsion) : il suffit de lire qErr, de l’associer à la bonne impulsion, et de le retrancher de l’horodatage du front pour ramener le sawtooth au niveau sub-nanoseconde. ntpsec intègre cette correction pour u-blox ; chrony ne le fait pas nativement, il faudrait un intermédiaire lisant UBX-TIM-TP.

Sur ce serveur, la chaîne n’exploite que le NMEA (GxRMC/GxGGA, chapitre Trames NMEA), qui ne transporte pas qErr : compenser le sawtooth imposerait d’activer le protocole binaire UBX, donc de sortir du choix “NMEA seul, reproductible partout” du projet. Le gain (~ns) resterait de toute façon masqué aujourd’hui par le plancher de bruit de l’ordre de la microseconde de la chaîne PPS/GPIO. L’ordre logique vers le nanoseconde est donc : d’abord l’horodatage matériel, ensuite seulement la correction sawtooth.


📚 Pour aller plus loin

Le timestamping matériel est standardisé par IEEE 1588 (Precision Time Protocol, dernière révision 2019). Linux l’expose via l’option de socket SO_TIMESTAMPING (depuis le noyau 2.6.30) : quand chrony active hwtimestamp end0, il demande au noyau, via setsockopt(), que les timestamps de réception et d’émission soient pris directement dans le silicium de la carte réseau plutôt que dans la pile logicielle, une précision typique inférieure à 100 ns une fois la contention PHC/chrony maîtrisée (mesures publiées par M. Lichvar, mainteneur de chrony, Linux.conf.au 2020).