Pourquoi ? Quelques cas d'utilisation
Dans un système embarqué, on a souvent besoin d'une base de temps (au minimum pour mettre à l'heure le système). On peut se baser sur une Real Time Clock (RTC) qui ne s'arrête jamais grâce à une pile, mais la dérive de temps n'est pas négligeable, et la mise à l'heure ne peut être que manuelle. On a donc très vite besoin d'aller chercher une référence de temps ailleurs, et si le réseau vient immédiatement à l'esprit, cela n'est pas possible si justement c'est le système en question qui doit fournir le réseau à d'autres appareils !
Voici deux cas assez différents mais qui aboutissent au même besoin :
- une "gateway" embarquée dans des véhicules de transport en commun, dont le but est de fournir un réseau à d'autres appareils (billetique, affichage voyageurs, pupitre du conducteur, etc.)
- un réseau de radio numérique dont tous les relais doivent être parfaitement synchronisés pour permettre à un poste itinérant de basculer d'un relais à un autre sans coupure
Dans ces deux cas, on trouve un GPS au niveau matériel : en partie pour fournir une position géographique, mais aussi la date et l'heure. En effet, les mesures de temps sont la base même du système GPS, qui est donc parfait pour obtenir cette information.
Une petite note à propos de systemd-timesync
Cet article explique le fonctionnement de ntpd qui est un client/serveur ntp complet, mais qui n'est plus installé par défaut sur la plupart des distributions Linux. Les distributions Linux modernes utilisent systemd-timesync qui est beaucoup plus limité
- systemd-timesync utilise le protocole SNTP, une version simplifiée de NTP mais suffisante pour la plupart des utilisations normales
- systemd-timesync est uniquement un client. Il ne peut pas faire serveur NTP.
Si vous avez des besoins simples (simplement être à l'heure) systemd-timesync devrait grandement vous suffire. Cet article est surtout intéressant si ce n'est pas le cas.
Mettre en place le démon GPS
Pour commencer, on a besoin d'exploiter les trames envoyées par le GPS. Évidemment, chaque GPS a ses spécificités, mais tous savent générer des trames standardisées (typiquement au format NMEA, que ce soit sous forme ASCII ou binaire). Pour les analyser, on peut utiliser le démon gpsd, qui va mettre à disposition de "clients" toutes les informations reçues du GPS.
La configuration de gpsd est assez simple, il "suffit" d'indiquer sur quel périphérique trouver le GPS (il s'agit toujours d'un périphérique de type tty). Par exemple, si on utilise SystemD pour gérer le service, on doit créer le fichier
/etc/default/gpsd.machine :
START_DAEMON="true" GPSD_OPTIONS="-b -n" DEVICES="/dev/ttyGPS" USBAUTO="false" GPSD_SOCKET="/var/run/gpsd.sock"
Note : ici on a un GPS connecté sur une liaison série, si à la place on a une connexion USB (qui peut être branchée et débranchée à volonté), on pourra l'indiquer à gpsd via la variable USBAUTO.
Vérifier le bon fonctionnement du GPS
Comment savoir si gpsd fonctionne bien ? Pour commencer on peut vérifier qu'on reçoit des trames NMEA, directement sur la liaison série :
# microcom /dev/ttyGPS $GPGGA,,,,,,0,,,,,,,,*66 $GPRMC,,V,,,,,,,,,,N*53 $GPGGA,,,,,,0,,,,,,,,*66 $GPRMC,,V,,,,,,,,,,N*53 ...
Ici on lit des trames $GPGGA (Global Positioning System Fix Data) et $GPRMC (Recommended minimum specific GPS/Transit data). On aimerait y voir des informations de temps ou de position, mais il faut laisser un peu de temps au GPS avant de produire ces données.
On peut aussi utiliser différents utilitaires, par exemple cgps qui affiche l'ensemble des données reçues sous forme plus ou moins graphique. Cet outil fait partie du paquetage gps-utils.
# cgps ┌───────────────────────────────────────────┐┌──────────────────Seen 0/Used 0┐ │ Time: 2020-10-20T12:58:20.000Z ││ PRN Elev Azim SNR Use │ │ Latitude: 45.18741022 N ││ | │ Longitude: 5.72200755 E ││ │ │ Altitude: 821.522 ft ││ │ │ Speed: 0.00 mph ││ │ │ Heading: 45.9 deg (true) ││ │ │ Climb: 19.69 ft/min ││ │ │ Status: 3D FIX (1 secs) ││ │ │ Long Err (XDOP, EPX): n/a , n/a ││ │ │ Lat Err (YDOP, EPY): n/a , n/a ││ │ │ Alt Err (VDOP, EPV): n/a , n/a ││ │ │ 2D Err (HDOP, CEP): n/a , +/- 105 ft ││ │ │ 3D Err (PDOP, SEP): n/a , n/a ││ │ │ Time Err (TDOP): n/a ││ │ │ Geo Err (GDOP): n/a ││ │ │ ECEF X, VX: n/a n/a ││ │ │ ECEF Y, VY: n/a n/a ││ │ │ ECEF Z, VZ: n/a n/a ││ │ │ Speed Err (EPS): n/a ││ │ │ Head Err (EPD): n/a ││ │ │ Time offset: 0.011 sec ││ │ │ Grid Square: JN25ue ││ │ └───────────────────────────────────────────┘└─────────────────────────────────┘
Le cas particulier des chips combinés Modem + GPS
Parfois on utilise une puce GPS intégrée à un autre périphérique, souvent un modem GSM. Cela ne change pas grand'chose, à part le fait qu'il faut peut-être activer le GPS. Cela implique de communiquer d'abord avec le modem, avec des commandes "AT". On peut faire cela de différentes manières, par exemple en utilisant le démon "pppd" relatif au modem, avec un chatscript dédié.
Il est nécessaire de noter que la plupart des modems combinant GSM+GPS ne le permettent pas en simultané mais de manière séquentielle.
Cela sort du cadre de cet article, mais voici les informations les plus importantes.
/etc/ppp/peers/gps :
/dev/ttyGPS-CTRL 115200 connect 'chat -s -v -f /etc/chatscripts/gps-enable'
/etc/chatscripts/gps-enable :
ABORT "BUSY" ABORT "NO CARRIER" ABORT "NO DIALTONE" ABORT "ERROR" ABORT "NO ANSWER" TIMEOUT 20 "" AT OK AT+qgpscfg="gpsnmeatype",3 OK AT+qgps=1 OK \c
et on peut alors activer le GPS avec la commande :
# pppd call gps
Le protocole NTP
Nous avons à présent un GPS opérationnel, il est donc temps de nous intéresser à NTP. NTP signifie Network Time Protocol, ce qui est prometteur pour notre besoin !
NTP est né dans les années 80, et s'est appelé Internet Clock Service pendant un temps. Son but est de synchroniser l'horloge d'un système avec un serveur considéré comme la référence, à travers un réseau. NTP utilise UDP avec le port 123. Un client NTP peut à son tour diffuser des informations de temps, en tant que serveur, ce qui forme un arbre. On appelle alors stratum la couche à laquelle un serveur appartient. Plus le stratum est faible, plus on est proche des serveurs racine (qui appartiennent au stratum 1). Il faut noter que les sources de temps telles que les horloges atomiques (ou les GPS !) qui fournissent le temps sur un lien dédié (liaison série) en dehors du réseau appartiennent au stratum 0.
Par rapport à nos cas d'utilisations, on va donc créer un serveur NTP de stratum 1 qui est donc "de première main" pour ses clients.
NTP en tant que client
Pour s'essayer à l'utilisation de NTP, le plus simple est encore de configurer un client. Pour cela le fichier de configuration de base peut être le suivant :
/etc/ntp.conf :
server 0.fr.pool.ntp.org server 1.fr.pool.ntp.org server 2.fr.pool.ntp.org server 3.fr.pool.ntp.org server 127.127.1.0 fudge 127.127.1.0 stratum 10 driftfile /etc/ntp/drift
Quelques détails sur cette configuration :
- on peut et on doit définir plusieurs serveurs de référence, avec une idée de redondance pour le cas où l'un des serveurs ne répondrait pas.
- aussi, on se référence soi-même (127.127.1.0 qui représente l'horloge locale pour NTP par pure convention, à ne pas confondre avec 127.0.01 !)... Cela peut sembler étrange de se déclarer soi-même en tant que serveur. C'est utile en cas de perte complète du réseau : ntp continuera de se synchroniser avec lui-même, en attendant des jours meilleurs ! La ligne fudge sert à indiquer que nous sommes nous-mêmes un serveur de stratum 10 (donc le moins fiable, à considérer en dernier recours).
- le fichier de dérive est configuré avec la ligne driftfile. Ce fichier sert à accumuler des données sur la dérive de l'horloge locale par rapport au réseau.
Les commandes utiles
On peut afficher le statut de NTP avec la commande ntpq. Sur un PC de bureau, on obtiendra par exemple :
remote refid st t when poll reach delay offset jitter ============================================================================== 2.fedora.pool.n .POOL. 16 p - 64 0 0.000 +0.000 0.000 *2001:41d0:2:8a6 27.165.110.64 3 u 1 64 1 23.964 +3.428 0.495 2001:41d0:2:37c 131.188.3.222 2 u 2 64 1 22.659 +2.676 2.141 x.ns.gin.ntt.ne 249.224.99.213 2 u 2 64 1 20.018 +1.846 2.244 y.ns.gin.ntt.ne 249.224.99.213 2 u 1 64 1 20.049 +3.028 0.694 163.172.150.183 10.194.3.3 5 u 1 64 1 19.275 +1.699 0.524 ns1.univ-montp3 145.238.203.14 2 u 2 64 1 31.570 +1.942 0.137 213.251.53.11 195.66.241.10 2 u 2 64 1 18.790 -0.255 2.482 51.15.175.180 ( 134.64.19.180 2 u 1 64 1 19.915 +1.512 0.505 83.68.137.76 131.130.251.107 2 u 1 64 1 39.320 +1.687 0.704
La commande ntpstat affiche un statut plus global :
# ntpstat synchronised to NTP server (2001:41d0:701:1100::1ecc) at stratum 3 time correct to within 280 ms polling server every 64 s
Ensuite, NTP synchronise l'horloge système par petits incréments mais on peut forcer une mise à l'heure brutale, en spécifiant le serveur à utiliser. Il faut cependant arrêter le démon ntpd avant de lancer la commande :
# ntpdate 2.fedora.pool.ntp.org 26 Nov 22:33:59 ntpdate[3768746]: adjust time server 2001:418:3ff::53 offset +0.010493 sec
Détecter une mise à l'heure brutale
Parfois, on n'a pas d'horloge matérielle à disposition (un exemple au hasard : sur une Raspberry Pi !). En conséquence, au démarrage, la date est fixée au 1/1/1970. Quelques secondes plus tard, dès que NTP parvient à se connecter à un serveur de temps, on saute à la date réelle.
Il peut être utile de détecter ce saut au niveau applicatif, et on peut utiliser l'astuce suivante qui consiste en l'utilisation d'un timer avec une période arbitraire, et en considérant le message d'erreur qui indiquera le saut :
#include <sys/timerfd.h>
int rtcsync_timer_fd;
uint64_t value;
int ret;
/* Il faut creer le timer en mode non bloquant */
rtcsync_timer_fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK);
struct itimerspec timer ={
.it_interval = {
.tv_sec = 10,
.tv_nsec = 0
},
.it_value = {
.tv_sec = 10,
.tv_nsec = 0
}
};
timerfd_settime(rtcsync_timer_fd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &timer, NULL);
/* Puis lorsqu'on veut détecter le saut de temps */
ret = read(rtcsync_timer_fd, &value, sizeof(value));
/* errno = ECANCELED implique un changement d'heure (NTP) */
if ((ret == -1) && (errno == ECANCELED)) {
/* Ici on peut lire l'horloge qui est devenue fiable */
}
NTP en tant que serveur, et le lien avec le GPS
En fait, la configuration précédente n'interdit pas d'interroger notre système pour obtenir une information de temps. Un client est donc aussi un serveur !
Pour faire les choses proprement, on va restreindre les opérations autorisées sur notre serveur, en ajoutant les lignes :
restrict default kod nomodify notrap
kod signifie Kiss Of Death et permet au démon NTP de limiter les requêtes des clients qui se comporteraient mal
nomodify interdit aux clients de modifier l'horloge du système
notrap ignore les messages de contrôle
Le protocole NTP est né avec le réseau, vit avec le réseau, pimente le réseau ! Cela se voit jusque dans sa configuration, qui devient un peu étrange quand il s'agit de parler d'autre chose que du réseau. Pour connecter NTP à des drivers du système, on utilise les adresses fictives 127.127.X.Y. Par exemple 127.127.40.0 référence l'horloge matérielle (RTC), et ce qui nous intéresse : 127.127.46.0 pour exploiter le GPS via sa ligne série, ou 127.127.28.0 pour accéder au GPS via une zone de mémoire partagée.
Pourquoi la mémoire partagée ?
On pourrait donner le contrôle du périphérique tty du GPS au démon NTP. L'inconvénient (majeur !) est que personne d'autre ne pourra lire les données GPS, ce qui est très probablement un problème. On va donc choisir d'utiliser une zone de mémoire partagée, mise à disposition par gpsd.
Pour commencer, on peut vérifier que la mémoire partagée existe, et qu'elle est initialisée, avec l'utilitaire ntpshmmon (qui fait partie du paquetage gps-utils) :
# ntpshmmon ntpshmmon: version 3.19 # Name Seen@ Clock Real L Prc sample NTP0 1606472641.016489384 1606472641.015587375 1606472641.000000000 0 -20 sample NTP0 1606472642.016918756 1606472642.015889412 1606472642.000000000 0 -20 sample NTP0 1606472643.016393526 1606472643.015563851 1606472643.000000000 0 -20
Ensuite, comme décrit plus haut, on indique à NTP qu'il doit utiliser cette mémoire partagée comme source de temps. On ajoute les lignes :
server 127.127.28.0 minpoll 4 maxpoll 4 prefer fudge 127.127.28.0 flag1 1 stratum 1 time1 0.113 refid GPS
Cette configuration précise beaucoup de détails. Les mots clés intéressants sont :
- minpoll et maxpoll (identiques) fixent la période d'interrogation du driver, en puissance de 2 (ici on a donc une période de 16s)
- prefer fait de ce driver la source privilégiée, à utiliser en priorité
- flag1 est spécifique à l'utilisation de la mémoire partagée, et ici il sert à autoriser les changements d'heure abrupts depuis cette source
- refid permet de nommer ce driver
Si tout fonctionne comme prévu, la commande ntpq doit afficher un statut du type :
# ntpq -p remote refid st t when poll reach delay offset jitter ============================================================================== *SHM(0) .GPS. 1 l 1 16 3 0.000 +54.630 31.344
Depuis l'extérieur, il suffit d'utiliser la commande ntpdate en indiquant l'adresse IP de notre nouveau serveur de temps pour vérifier qu'il fournit l'heure comme on l'espère.
Améliorer la précision avec le signal PPS
Avec les manipulations précédentes, on peut espérer une précision de l'ordre de la seconde. C'est assez décevant, en fait, et cette performance est liée au fait qu'il est difficile d'horodater les messages reçus du GPS sur la ligne série. Le périphérique tty n'a pas de contraintes temps réel, et le noyau peut avoir bien d'autres choses à faire avant de transmettre les caractères de la liaison série, qui pour encore dégrader la performance, utilise peut-être un débit faible à notre échelle (9600 bits par seconde par exemple).
Certains GPS fournissent un signal (sur une broche dédiée de la puce) appelé 1PPS ou PPS pour One Pulse Per Second, qui envoie tout simplement une impulsion toutes les secondes. La largeur de l'impulsion importe peu. Elle est envoyée pour marquer le début des messages de date et heure :
Ce signal en lui-même a une précision qui avoisine la nanoseconde, et dans NTP on peut espérer une précision de quelques micro-secondes.
Au niveau électronique on doit relier cette sortie particulière du GPS à n'importe quelle entrée GPIO du CPU. On doit activer le driver adapté dans le noyau Linux :
Aussi, on indique le GPIO choisi au driver via le device tree, avec un noeud du type :
pps { compatible = "pps-gpio"; gpios = <&gpio1 19 0>; assert-rising-edge; };
Du fait que le signal PPS est géré par le noyau, il profite directement à l'horloge système, en plus d'améliorer la précision du GPS.
Le signal PPS peut être observé avec les commandes fournies dans le package pps-tools, en particulier ppswatch :
# ppswatch -a /dev/pps0 trying PPS source "/dev/pps0" found PPS source "/dev/pps0" timestamp: 1606465254, sequence: 61, offset: -241483148 timestamp: 1606465256, sequence: 62, offset: -241531613 timestamp: 1606465258, sequence: 63, offset: -241460621 timestamp: 1606465260, sequence: 64, offset: -241477419 timestamp: 1606465262, sequence: 65, offset: -241511468 timestamp: 1606465264, sequence: 66, offset: -241477309 timestamp: 1606465266, sequence: 67, offset: -241474733 timestamp: 1606465268, sequence: 68, offset: -241471532 timestamp: 1606465270, sequence: 69, offset: -241519164 timestamp: 1606465272, sequence: 70, offset: -241500838
On doit voir apparaître une nouvelle ligne chaque seconde.
Au niveau de NTP, on doit déclarer un driver supplémentaire :
server 127.127.22.0 minpoll 3 maxpoll 3 fudge 127.127.22.0 refid PPS0
On doit voir apparaître une nouvelle zone de mémoire partagée avec la commande ntpshmmon.
Le statut de NTP doit montrer la prise en compte du signal PPS :
# ntpq -p remote refid st t when poll reach delay offset jitter ============================================================================== PPS(0) .PPS0. 0 l - 8 0 0.000 0.000 0.000 SHM(0) .GPS0. 1 l - 32 0 0.000 0.000 0.000
Conclusion
Avec un peu de configuration, on peut mettre en place un serveur de temps de grande qualité. Le GPS est un source fiable pour cet usage et on peut alors offrir des possibilités de synchronisation sans connexion vers un réseau public.
Pour aller plus loin, on peut s'intéresser au protocole PTP (Precision Time Protocol) dont le but est d'atteindre une précision bien meilleure que NTP (il vise plus petit que la micro-seconde, mais nécessite d'être supporté au niveau matériel) pour des systèmes qui n'ont pas accès au GPS.