Horloge TRÈS précise pilotée par GPS avec Arduino Nano

Bonjour !

Je dois avouer que j'aime bien tout ce qui tourne autour des horloges.
Mon premier bricolage en électronique (en 1976, ça date !) était d'ailleurs une horloge à afficheurs 7 segments à LEDs.
Du côté Arduino, après les horloges à base de DS3132 ou synchronisées par un serveur NTP (qui demandent la présence d'un réseau fournissant l'info) j'ai voulu m'attaquer à ce projet où c'est un module GPS qui donne l'heure.

Le cahier des charges est le suivant :
Par défaut (mode "auto") afficher l'heure française avec correction automatique pour l'heure d'été.
Pouvoir passer en mode "manuel" et sélectionner n'importe quel fuseau horaire de la planète entre -14 et + 12 heures par rapport à l'heure UTC, avec un pas minimal de 1/4 d'heure. (si jamais on décide d'emmener son horloge avec soi lors d'un voyage !)
Pas de correction automatique pour l'heure d'été en mode manuel.
Et tant qu'à faire avoir une possibilité d'afficher ses coordonnées géographiques et son altitude.
Le but principal de ce projet étant de "voir si ça marche" je me contenterai pour l'instant d'un antédiluvien affichage LCD à 4 lignes de 20 caractères, qui communiquera en I2C, pour "économiser" des fils.
Quitte à faire à l'avenir un truc plus léché !

Le montage est piloté par 3 boutons dont voilà le fonctionnement :

BOUTON "+" (BP1 sur le schéma) :
Appui long : passage en mode manuel (qui permet de choisir le fuseau horaire)
Appui court : si mode manuel : +1h, si mode auto pas d'effet
Double appui : si mode manuel : +1/4h, si mode auto pas d'effet

BOUTON "-" (BP2 sur le schéma) :
Appui long : passage en mode auto (mode par défaut, heure de l'Europe de l'ouest et heure d'été automatique)
Appui court : si mode manuel : -1h, si mode auto pas d'effet
Double appui : si mode manuel : -1/4h, si mode auto pas d'effet

BOUTON "Affich" (BP3 sur le schéma) :
Appui court : bascule entre affichage de heure-date et affichage de longitude-latitude-altitude
Appui long : bascule entre allumage et extinction du backlight de l'écran LCD

On trouve actuellement des petits modules GPS avec antenne intégrée ou extérieure à pas cher, j'ai pour ma part choisi un L86-M33 de chez Quectel (à antenne intégrée mais qui permet de brancher une antenne externe) pour sa petite taille et car j'ai entendu du bien sur ses qualités de réception des signaux.
C'est vraiment minuscule !

Je lui ai soudé une barrette de 6 contacts pour pouvoir le brancher soit sur une platine d'essai soit sur un câble.

Les avantages :

  • les satellites GPS tournent autour de la terre (heureusement sinon ils tomberaient) et où qu'on soit, on pourra en capter.
  • à priori les données d'heure et de dates sont très précises car les satellites embarquent des horloges atomiques.

L'inconvénient :

  • La qualité de réception des signaux dépend de l'endroit où on se trouve. Dans une maison avec par exemple du béton armé ou une toiture métallique il y a des chances que le GPS ne capte rien, à moins qu'il ait une antenne extérieure.
    Dans mon cas (maison individuelle à un étage avec planchers en bois) je capte l'heure après environ 30 secondes et elle devient vraiment précise (j'y reviendrai) dans la minute, même au rez de chaussée.

Ce projet m'a permis de creuser un peu le fonctionnement d'un GPS, et une vidéo de la sympathique chaîne Youtube de "Électro-Bidouilleur" m'a bien aidé pour éviter certains écueils :

Pour ceux que ça intéresse, comment cela marche-t-il ?

Les modules GPS utilisent majoritairement le protocole NMEA 0183 qui envoie toutes les secondes des trames formatées toujours de la même manière à travers la liaison série du GPS.
Ces trames commencent par un $ et contiennent des informations délimitées par des virgules qui seront les mêmes quelle que soit la marque du module GPS, facilitant ainsi leur exploitation.
Pour ne pas trop remplir cette page voici un document qui explique ce qui est renvoyé :
-Explication des trames GPS.zip (8,5 Ko)

Voici un exemple de ce que renvoie mon module quelques secondes après la mise sous tension :

Les 9 lignes entre les deux traits oranges sont renvoyées toutes les secondes.
On constate que même si à priori on ne capte pas encore de satellites (0 souligné en rouge) on a quand même déjà l'heure (soulignée en vert) et la date (soulignée en bleu)
J’appellerai ça plus tard le "mode dégradé", car la précision dans ce mode est d'environ 1 seconde.

Une fois qu'on capte des satellites, voici ce que renvoie le GPS :

On voit du premier coup d'oeil qu'il y a bien plus d'infos.
Se sont rajoutées la latitude et la longitude (soulignées de mauve) ainsi que l'altitude (soulignée de gris).
Le nombre de satellites captés est ici de 7. (souligné de rouge)
Il y a encore plein d'autres infos (vitesse, cap, détails sur les satellites) qui ne nous intéressent pas ici.
J’appellerai ça plus tard le "mode précis" car une fois qu'on capte des satellites l'horloge change de seconde pile-poil au bon moment.

Il faut maintenant extraire les données, soit "manuellement" par rapport au nom des trames qui nous intéressent ($GPRMC et $GPGGA) soit en étant fainéant comme moi et en utilisant une bibliothèque comme "TinyGPSPlus" disponible à GitHub - mikalhart/TinyGPSPlus: A new, customizable Arduino NMEA parsing library qui servira sur un plateau les données demandées.

Mais à ce niveau des questions se posent :
L'envoi des trames par le module GPS (par une liaison série pas très rapide à 9600 bauds par défaut) prend déjà un certain temps (dans le cas ci-dessus c'est déjà plus de 0,6 secondes), et quand je capterai encore plus de satellites (une ligne $GPGSV se rajoute tous les 4 satellites) je serai à plus de 0,8 secondes, sachant que la série de trames est envoyée toutes les secondes. Ce qui me laisse 0,2 seconde pour extraire les données et les afficher...
De quoi se créer des "bouchons" dans l'Arduino si ces 0,2 secondes ne suffisent plus pour décoder et à afficher les données !

Autre interrogation :
L'extraction des données commence une fois toutes les trames reçues, donc du temps est passé (temps variable selon la longueur des trames).
Et quand les données seront prêtes à être affichées on sera quelque part dans la seconde en cours ou même déjà à la prochaine seconde.
Ce qui n'est pas la mort, mais bon, si déjà on veut se synchroniser sur une horloge atomique, autant que ce soit précis !

C'est là qu'intervient le signal PPS (pulse per second) dont quasiment tous les modules GPS disposent, et qui est une impulsion TRÈS précise envoyée AU DÉBUT de chaque nouvelle seconde, une fois que le module GPS capte 4 satellites ou plus.

Voici un dessin (extrait de la vidéo d'Électro-Bidouilleur plus haut) qui montre ce qui se passe chaque seconde :

On a d'un côté le signal PPS en début de seconde, et en-dessous sont schématisées les trames qu'on reçoit (la zone "Acquisition").
Suit ensuite le traitement (extraction et mise en forme des données des trames reçues)
Pour être précis, on va donc pouvoir synchroniser l'affichage au front montant de la prochaine impulsion PPS.
D'où le mode "précis" de mon montage, contrairement au mode "dégradé" où l'affichage se fait immédiatement après le décodage des données sans synchronisation.

Mais attention !
Nous venons d'extraire les données de la seconde courante, et le prochain "Top affichage" sera le début de la seconde suivante, il va donc falloir incrémenter les données qu'on vient d'extraite de 1 seconde.
On peut donc ajouter 1 aux secondes mais en vérifiant qu'on est pas à la seconde 59, qui nous fera changer de minute, puis en vérifiant si on est pas à la minute 59 qui nous fera changer d'heure, etc... jusqu'à l'année !
J'ai préféré une autre approche :
Transformer les données (heures, minutes, secondes, jour, mois, années) de TinyGPSPlus en temps unix qui est un nombre sous la forme 1705843961, en fait c'est le nombre de secondes écoulées depuis le 1er janvier 1970.
Il suffit de rajouter 1 et de reconvertir ensuite le nouveau nombre obtenu en données de date et d'heure.
Là aussi la tâche va être facilitée par une bibliothèque pas toute jeune mais qui a toujours de l'intérêt :
"Time" disponible ici : GitHub - PaulStoffregen/Time: Time library for Arduino

Cette approche simplifie aussi la prise en compte de l'heure d'été ou d'hiver, il suffira d'ajouter 3600 secondes en été.
Pareil pour afficher l'heure d'autres fuseaux horaires, il suffira d'ajouter ou de soustraire autant de fois 3600 qu'il y a d'heures d'écart par rapport à l'heure UTC.

Pour accélérer l'acquisition des données, et ainsi donner du temps au Nano de faire son travail, la plupart des modules GPS peuvent être paramétrés pour n'envoyer que les trames dont on a besoin ($GPRMC et $GPGGA), en leur envoyant une commande sur leur liaison série.
Il y a aussi une autre commande pour éviter l'envoi de la trame $GPTXT, spécifique à mon module GPS.
En cherchant un peu j'ai trouvé la doc qui me donne les commandes à envoyer. Voici d'ailleurs un lien vers des documents concernant mon module Quectel L86-M33 qui m'ont aidé :

Si vous utilisez un autre module GPS, les commandes à lui envoyer pour le paramétrer seront bien sûr différentes, à ce niveau il n'y a pas de normalisation !

J'ai aussi accessoirement passé la vitesse de transmission de la liaison série du GPS de 9600 à 19200 bauds (bien que ce n'était franchement pas nécessaire après le "dégraissage" des trames), il y a aussi une commande pour ça. Je me retrouve donc avec ces infos venant du GPS tenant sur 2 lignes :

Je passe de 0,8 secondes (toutes les trames à 9600 bauds avec une quinzaine de satellites "en vue") à 75 millisecondes, ce qui laisse plus que largement le temps au Nano de travailler.

Infos en vrac :

Mon afficheur LCD associé à une liaison I2C n'étant pas très rapide, j'affiche en premier les secondes, puis les minutes et les heures, tout le reste après.
Et je ne constate aucun retard visuel par rapport à une LED que j'ai mis sur la sortie PPS du module GPS.

Le module GPS utilisé fonctionne en 3,3 V.
Cela ne pose pas de problème pour l'entrée Rx du Nano alimenté lui avec du 5 V, et qui en théorie voit les entrées à 1 dès que la tension dépasse 3V.
Par contre pour la transmission du Nano vers le GPS, j'ai fait un pont diviseur avec 2 résistances, pour transformer le 5 V en sortie du Tx du Nano en environ 3,3 V.

Dans le cas de mon module Quectel L86-M33 les broches V_BCKP et +VCC doivent obligatoirement être reliées ensemble.
A voir dans la documentation de votre module s'il est différent si c'est aussi nécessaire...

A noter aussi que le signal PPS du GPS (donné par le fabriquant pour une précision de moins de 15 nanosecondes !) pourra servir de base de temps à ceux qui ont besoin d'une grande précision, et même pas besoin de l'Arduino, puisque le module sort ce signal dès qu'il est alimenté et qu'il capte assez de satellites.

Quelques photos du montage en marche :
Au démarrage ( le "A" à droite indique qu'on est en mode "auto", donc heure française) :

Nous voici en mode "dégradé" (on a déjà l'heure mais pas encore de satellite)

Nous voilà en mode "précis" (un "P" s'affiche à droite) :

Et voilà le second écran qui donne la position :

Nous voilà en mode "manuel" sur le fuseau horaire de Taïwan :

Le montage et sa platine d'essai :

Et grande première pour moi (c'est ma première utilisation de Fritzing), l'implantation des composants et le schéma.

Et pour finir le code de ce projet :

/***************************************************************************************************
 *                                        Horloge GPS précise                                      *
 *                                       Roland MATHIS 01/2024                                     *
 ***************************************************************************************************

Fonctionnement :
En mode auto (par défaut) , affiche l'heure UTC+1 (Europe de l'ouest) et prend en compte l'heure d'été et d'hiver
En mode manuel, permet d'afficher l'heure de n'importe quel fuseau horaire de la planète (de +11 h à -11 h),
mais sans prise en compte de l'heure d'été.

 * UTILISATION DES 3 BOUTONS
 * 
 * BOUTON + (BP1 sur le schéma) :
 * Appui long :  passage en mode manuel
 * Appui court : si mode manuel  +1h, si mode auto pas d'effet
 * Double appui : si mode manuel +1/4h, si mode auto pas d'effet
 *         
 * BOUTON - (BP2 sur le schéma):
 * Appui long :  passage en mode auto
 * Appui court : si mode manuel  -1h, si mode auto pas d'effet
 * Double appui : si mode manuel -1/4h, si mode auto pas d'effet
 *
 * BOUTON Affich (BP3 sur le schéma): 
 * Appui court : bascule entre affichage de heure-date et l'affichage de longitude-latitude-altitude
 * Appui long : bascule entre allumage et extinction du backlight de l'écran LCD
 */
#include <SoftwareSerial.h>     // Pour gérer la liaison série avec le module GPS
#include <TinyGPSPlus.h>        // Pour extraire facilement les données des trames envoyées par le GPS
#include <OneButton.h>          // Pour la gestion des boutons-poussoirs
#include <LiquidCrystal_I2C.h>  // Pour gérer l'afficheur LCD et sa liaison I2C (appelle wire.h)
#include <TimeLib.h>            // Pour les conversions de temps au format unix et réciproquement

static const int RXPin = 2, TXPin = 3;
static const char PPS = 4; 
byte  nbsat;                    // Nombre de satellites captés
float latit, longit, altitud;   // Coordonnées géographiques
bool  PPS_low;                  // Mémorise l'état bas du signal PPS, permet de n'afficher qu'une fois au front montant de PPS
bool  precis;                   // Mis à true lorsqu'on capte au moins 4 satellites
bool  bcklght = true;           // Etat du backlight de l'écran LCD
unsigned long top_PPS;          // Compte les millis() à partir de chaque front montant du signal PPS
bool  autoDST = true;           // Quand l'horloge est en mode auto
long decal_UTC = 3600;            // Décalage par rapport à l'heure UTC, +1 par défaut. Peut être négatif donc du type int
int  decal_DST = 0;            // Décalage pour l'heure d'été (0 en hiver, 1 en été)
bool  ecran2 = false;           // Pour afficher un deuxième écran
bool  effac_mess = false;       // Pour effacer une seule fois un texte
String JourSem[7] = {"Dimanche"," Lundi  "," Mardi  ","Mercredi"," Jeudi  ","Vendredi"," Samedi "};
byte preci[] =                  // Création d'un caractère personnalisé (un "P" blanc sur fond noir)
{ B11111, B10001, B10101, B10001, B10111, B10111, B11111, B00000 };
byte mode_auto [] =             // Création d'un caractère personnalisé (un "A" blanc sur fond noir)
{ B11111, B10001, B10101, B10001, B10101, B10101, B11111, B00000 };
byte heure_ete [] =             // Création d'un caractère personnalisé (un "S" blanc sur fond noir)
{ B11111, B10001, B10111, B10001, B11101, B10001, B11111, B00000 };
byte e_aigu [] =                // Création d'un caractère personnalisé (un "é")
{ B00010, B00100, B01110, B10001, B11111, B10000, B01110, B00000 };

OneButton btn_plus(A0, true);   // Déclaration du bouton + sur l'entrée A0
OneButton btn_moins(A1, true);  // Déclaration du bouton - sur l'entrée A1
OneButton btn_affich(A2, true); // Déclaration du bouton de changement d'affichage sur l'entrée A2

// Déclaration de l'objet TinyGPSPlus
TinyGPSPlus gps;

// Déclaration de la connexion série au module GPS
SoftwareSerial ss(RXPin, TXPin);

// Définition l'adresse de l'écran LCD, de la longueur et du nombre des lignes
LiquidCrystal_I2C lcd(0x27, 20, 4);

// Déclaration de la structure contenant les éléments de TimeLib
tmElements_t UTC;

void setup()
{
// Pour le fonctionnement de la librairie OneButton  
  // fonctions appellées par le bouton +
  btn_plus.attachClick(clic_plus);
  btn_plus.attachDoubleClick(clic_plus_quart);
  btn_plus.attachLongPressStart(clic_long_plus);
  // fonctions appellées par le bouton -
  btn_moins.attachClick(clic_moins);
  btn_moins.attachDoubleClick(clic_moins_quart);
  btn_moins.attachLongPressStart(clic_long_moins);
  // fonctions appellées par le bouton affich
  btn_affich.attachClick(clic_affich);
  btn_affich.attachLongPressStart(clic_long_affich);

// ******************************** INITIALISATION DU MODULE GPS ***********************************
// ************ Ces commandes ne sont valables que pour des module GPS de marque Quectel ***********
// ATTENTION : sorti d'usine le port série du GPS est paramétré à 9600, pour le premier démarrage mettre "ss.begin(9600);"
  ss.begin(19200);                 // Initialisation de la connexion série avec module GPS à 19200 bps

// Activer cette ligne seulement au premier démarrage, pour éviter l'envoi de la trame propriétaire $GPTXT (mémorisé par le GPS)
// ss.println("$PQTXT,W,0,1*23")

// Activer cette ligne seulement au premier démarrage, passe le port série du GPS de 9600 à 19200 bauds (mémorisé par le GPS)
// ss.println("$PQBAUD,W,19200*7E");

// Envoi d'une trame au module GPS pour qu'il n'envoie que les trames $GPRMC et $GPGGA utilisées par TinyGPSPlus
// et ainsi accélérer l'acquisition (non mémorisé par le GPS)
  ss.println("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28");
  
  pinMode(PPS, INPUT_PULLUP);      // Configurer la broche PPS comme entrée

  lcd.init();                      // Initialisation de l'afficheur LCD
  lcd.createChar(0, preci);        // Assignation de preci au caractère personnalisé 0 dans la RAM de l'afficheur
  lcd.createChar(1, mode_auto);    // Assignation de mode_auto au caractère personnalisé 1 dans la RAM de l'afficheur
  lcd.createChar(2, heure_ete);    // Assignation de heure_ete au caractère personnalisé 2 dans la RAM de l'afficheur
  lcd.createChar(3, e_aigu);       // Assignation de degre au caractère personnalisé 3 dans la RAM de l'afficheur
  
// Affichage du logo d'allumage
  lcd.setCursor(7, 1); lcd.print("Horloge");
  lcd.setCursor(5, 2); lcd.print("GPS pr\003cise");   // "\003" écrit le caractère spécial 3 (le "é")
  lcd.backlight();                 // Allumer le rétroéclairage de l'écran
  delay(2000);                     // Durée d'affichage du texte

  // "Squelette" de l'affichage
  lcd.clear();
  lcd.setCursor(6, 0); lcd.print("--:--:--");
  lcd.setCursor(5, 1); lcd.print("--/--/----");
  lcd.setCursor(1, 2); lcd.print("Cherche satellites");
  lcd.setCursor(0, 3); lcd.print("UTC           Sat:");
}

void loop()
{
// Détection des appuis sur les boutons par OneButton
  btn_plus.tick();       // Surveillance du bouton +
  btn_moins.tick();      // Surveillance du bouton -
  btn_affich.tick();     // Surveillance du bouton affich

// Boucle de lecture des données du GPS suivie de leur mise en forme (infos reçue 1 fois par seconde)
// et de leur affichage en mode dégradé (quand pas assez de satellites)
  while (ss.available() > 0)
  {
    { gps.encode(ss.read());
      if (gps.date.isUpdated())
        {
          // Passage des variables gps.xxx renvoyées par TinyGPSPlus dans TimeLib
          UTC.Day =    (gps.date.day());         // Les variables sous la forme UTC.xxx sont utilisées au format
          UTC.Month =  (gps.date.month());       // int dans une structure définie avant le setup par TimeLib
          UTC.Year =   (gps.date.year())-1970;   // UTC.Year est une variable qui doit commencer en 1970
          UTC.Hour =   (gps.time.hour());
          UTC.Minute = (gps.time.minute());
          UTC.Second = (gps.time.second());
          latit =      (gps.location.lat());
          longit =     (gps.location.lng());
          altitud =    (gps.altitude.meters());
          nbsat =      (gps.satellites.value());

          if (autoDST)    // Si on est en mode auto : Temps français (UTC+1) et vérif si heure d'été ou d'hiver)
            {
              if (Heure_ete(UTC.Year-30, UTC.Month, UTC.Day, UTC.Hour)) // appel à la fonction "bool Heure_ete..." qui retourne true ou false
                   decal_DST = 3600;
              else decal_DST = 0;
            }
         // Adaptation de l'heure au fuseau horaire et à l'heure d'été.
         // Le L derrière 3600 force un calcul au format long car (3600L*(decal_UTC+decal_DST)) peut dépasser + ou - 32767
         unsigned long temps_unix_local =  makeTime(UTC) + 1 + decal_UTC + decal_DST;
         setTime(temps_unix_local);         // Transformation du temps_unix_local en ses composantes avec TimeLib
                                            // second(), minute(), hour(), etc...
         if (millis() - top_PPS > 500)      // Si pas d'impulsion PPS pendant plus d'un cycle, on passe en mode d'affichage dégradé
                                            // valeur déterminée expérimentalement
           {
             precis = false;
             ecrit_lcd(); ecrit_lcd_UTC();  // Affichage des infos à l'écran en mode dégradé
           } 
       }
    }
  }

  if (!(digitalRead(PPS))) PPS_low = true;  // Met PPS_low à true dès le front descendant du signal PPS
                                            // Pour ne permettre qu'une fois l'affichage
                                           
// Affichage des infos en mode précis, synchronisé sur le front montant du signal PPS (une fois par seconde)  
  if (digitalRead(PPS) && (PPS_low))        // Moment du front montant de PPS
  {
    top_PPS = millis();                     // Début de comptage des millis à partir du front montant de PPS
    precis = true; PPS_low = false;    
    ecrit_lcd(); ecrit_lcd_UTC();           // Affichage des données à l'écran
  }
}

void ecrit_lcd()                            // Affichage heure, date et jour
{
  if (year() != 2080 && year() != 1970)     // 2080 est la date de défaut quand le GPS n'a pas de fix.
  {                                         // 1970 est l'année par défaut dans TimeLib avant qu'il n'ait fait des calculs
    if (!(ecran2))                          // Affichage de l'écran de base (heures et date)
      {
        lcd.setCursor(12, 0);
        if (second() < 10) lcd.print("0"); lcd.print(second());
        lcd.setCursor(9, 0);
        if (minute() < 10) lcd.print("0"); lcd.print(minute());
        lcd.setCursor(6, 0);
        if (hour() < 10) lcd.print("0"); lcd.print(hour());
        lcd.setCursor(5, 1);
        if (day() < 10) lcd.print("0"); lcd.print(day());
        lcd.setCursor(8, 1);
        if (month() < 10) lcd.print("0"); lcd.print(month());
        lcd.setCursor(11, 1); lcd.print(year());
        if (effac_mess == false)          // Effacement une seule fois du message "Recherche satelittes"
          {
            effac_mess = true;
            lcd.setCursor(1, 2); lcd.print("                  ");
          }
        lcd.setCursor(6, 2); lcd.print(JourSem[weekday()-1]);
      }
    else                                  // Affichage de l'écran avec les données de position
      { ecrit_lcd_geo(); }
  }
  lcd.setCursor(18, 3); lcd.print(nbsat); 
  if (nbsat < 10) lcd.print(" ");         // Cas où nbsat passe sous 10, pour effacer les unités
  // Affichage de différentes indications (mode précis, mode auto, heure d'été)
  lcd.setCursor(19, 0); if (precis) lcd.write(byte(0)); else lcd.print(" ");
  lcd.setCursor(19, 1); if (autoDST) lcd.write(byte(1)); else lcd.print(" ");
  lcd.setCursor(19, 2); if (decal_DST == 3600) lcd.write(byte(2)); else lcd.print(" ");
}

void ecrit_lcd_UTC()                        // Affichage du décalage par rapport à l'heure UTC en heures et minutes
{
  int h = decal_UTC / 3600;                 // extraction des heures du décalage (nombre entier positif ou négatif)
  byte mi = abs(((decal_UTC / 3600.0) - h)*60); // extraction des minutes du décalage (nombre entier positif)
  lcd.setCursor(3,3);
  if (decal_UTC > 0) lcd.print("+");
  if (decal_UTC < 0) lcd.print("-");        // Pour avoir un "-" aussi devant le 0 si on est entre 0 et -1 heure
  if (decal_UTC != 0)
    {lcd.print (abs(h));
      if (mi > 0) { lcd.print(":"); lcd.print(mi); } else { lcd.print("   "); }
    }
  if (decal_UTC == 0) lcd.print("      ");  // ne rien afficher
  if (abs(decal_UTC) < 36000) { lcd.setCursor(8,3); lcd.print(" "); } // ôte le chiffre à droite quand on passe sous 10
}
void ecrit_lcd_geo()                        // Affichage des coordonnées géographiques
{
  lcd.setCursor(10, 0); lcd.print(latit,4); lcd.print("\337");      // \337 = symbole "°" dans le charset de l'écran LCD
  lcd.setCursor(10, 1); lcd.print(longit,4); lcd.print("\337"); 
  lcd.setCursor(10, 2); lcd.print(altitud,1); lcd.print("m"); 
}

bool Heure_ete(byte anUTC, byte moisUTC, byte jourUTC, byte heureUTC)
//*********************************************************************************************************
// Fonction extraite du fichier "simpleRTC.ccp" d'une librairie de "Bricoleau" sur le forum Arduino ici : *
// https://forum.arduino.cc/t/partage-librairie-simplertc-ds1307-ds3231-avec-heures-ete-hiver/376814      *
//         Détermine si on est en heure d'été ou en heure d'hiver en retournant true ou false             *
//*********************************************************************************************************
{
  //En France métropolitaine :
  //Passage de l'heure d'hiver à l'heure d'été le dernier dimanche de mars à 1h00 UTC (à 2h00 locales il est 3h00)
  //Passage de l'heure d'été à l'heure d'hiver le dernier dimanche d'octobre à 1h00 UTC (à 3h00 locales il est 2h00)
  if (moisUTC == 3)
    {
     byte derDimancheMars = 31 - ((5 + anUTC + (anUTC >> 2)) % 7);
     return jourUTC > derDimancheMars || (jourUTC == derDimancheMars && heureUTC != 0);
    }
  if (moisUTC == 10)
    {
     byte derDimancheOct = 31 - ((2 + anUTC + (anUTC >> 2)) % 7);
     return jourUTC < derDimancheOct || (jourUTC == derDimancheOct && heureUTC == 0);
    }
  return 3 < moisUTC && moisUTC < 10;  // true d'avril à septembre inclus
}

// *********************** Gestion des appuis sur les boutons ***********************
void clic_plus()         // Fonction appelée par un appui court sur le bouton +
{ if (!autoDST && decal_UTC <= 39600) { decal_UTC += 3600; ecrit_lcd(); ecrit_lcd_UTC(); } }

void clic_plus_quart()
{ if (!autoDST && decal_UTC <= 42300) { decal_UTC += 900; ecrit_lcd(); ecrit_lcd_UTC(); } }

void clic_long_plus()    // Fonction appelée par un appui long sur le bouton +
{ autoDST = false; decal_DST = 0; }

void clic_moins()        // Fonction appelée par un appui court sur le bouton -
{ if (!autoDST && decal_UTC >= -46800) { decal_UTC -= 3600; ecrit_lcd(); ecrit_lcd_UTC(); } }

void clic_moins_quart()
{ if (!autoDST && decal_UTC >= -49500) { decal_UTC -= 900; ecrit_lcd(); ecrit_lcd_UTC(); } }

void clic_long_moins()   // Fonction appelée par un appui long sur le bouton -
{ autoDST = true; decal_UTC = 3600; }

void clic_affich()       // Fonction de bascule entre affichages appelée par un appui long sur le bouton affich
{
  if (precis)
    { ecran2 = !ecran2;  // n'affiche les coordonnées géographique qu'en mode précis
      if (ecran2)
        {
          lcd.setCursor(0, 0); lcd.print("Longitude: "); 
          lcd.setCursor(1, 1); lcd.print("Latitude: "); 
          lcd.setCursor(1, 2); lcd.print("Altitude: ");
          ecrit_lcd_geo(); 
        }
      else
        {
          lcd.setCursor(0, 0); lcd.print("        :  :       "); 
          lcd.setCursor(1, 1); lcd.print("      /  /        ");
          lcd.setCursor(1, 2); lcd.print("                  ");
          ecrit_lcd();
        }
    }
}

void clic_long_affich()  // fonction de bascule pour allumer-éteindre le backlight de l'écran
{
  bcklght = !bcklght;
  if (bcklght)  lcd.backlight();
  if (!bcklght) lcd.noBacklight();
}

Et bravo à ceux qui ont réussi à tout lire ! :upside_down_face:

Roland

4 Likes

Super, beau projet.
C'est clair, bien expliqué et le code commenté.
Merci

J'adore !! :star_struck: :smiling_face_with_three_hearts:

Et, si je peux me permettre, quelques remarque pour ajouter encore à la perfection et au fun de la réalisation :

  • les fuseaux horaires s'étendent de UTC-12 à UTC+14 (si si) avec des bizarreries comme UTC +5:45 (pour le Népal) ou UTC -3:30 pour Terre-Neuve ou Labrador
  • Si déjà tu récupères la position, tu peux essayer de prédire le fuseau horaire (les données cartographiques devraient être trouvables, mais honnêtement je ne sais pas où)

Mais surtout, le temps GPS est décorrélé de l'UTC qui a comporté jusqu'à maintenant des « secondes intercalaires » pour rectifier les variations du mouvement de la Terre. Actuellement le temps GPS est en avance de 18 s. Ce décalage est transmis dans les données GPS mais je n'ai pas regardé tout ton code et je n'ai pas vu si tu en tenais compte (ou la lib. TinyGPSPlus.h) ou pas : mais 18 s c'est énorme, en comparant avec une base de temps NTP ou de téléphone, ça devrait sauter aux yeux...

En tout cas, un grand bravo !

Merci pour ton retour @ProfesseurMephisto !
Je ne savais pas que ça allait jusqu'à -12 et + 14, je vais modifier le code en conséquence..
Pour les 1/4 ou 1/2 h je vais passer mes sauts à 900 secondes au lieu de 3600, et modifier l'affichage en conséquence.
Ah, si les fuseaux horaires étaient droits, bien sûr que je calculerai le décalage à partir de la longitude.
Mais comme les fuseaux font des zig et des zag pour s'adapter au conditions géographico-politiques, c'est moins évident.
(Anecdote à ce sujet : au nord de la Norvège il y a un point où les frontières de la Norvège, de la Finlande et de l'URSS je rejoignent, chacun des 3 pays ayant une heure différente ! :robot: )
De plus, dans un même fuseau horaire il peut y avoir des règles d'heure d'été différentes, alors pour l'instant je vais laisser tomber ça !
Les "leap seconds" ajoutées de temps à autre sont bien prises en compte par le module GPS, qui affiche l'heure correcte sans qu'il faille que je fasse une correction.
Les satellites GPS sont effectivement réglés à une "heure GPS", mais les infos qu'ils envoient au modules GPS contiennent l'écart actuel, et ainsi les GPS renvoient l'heure UTC exacte.
Voir par exemple là :

Roland

Tiens, je n'avais pas tiqué sur ton pseudo, mais on est voisins et amateur de la même gastronomie...

1 Like

Et hop, suite aux infos de @ProfesseurMephisto j'ai retravaillé mon code pour que je puisse aller de UTC-12 à UTC+14, avec des "crans" d'un 1/4 d'heure.
J'ai mis mon post d'origine à jour...

En bonus l'écran avec l'heure au Népal !

Roland

Bonjour !
Petite variante de mon projet en remplaçant l'Arduino Nano par un XIAIO Seeduino, une petit carte de développement de la taille du pouce, avec 11 entrées-sorties. Vu l'encombrement ça permettra de plus facilement caser le projet dans un boîtier.
Le cerveau est un microcontrôleur ARM® Cortex®-M0+ 32bit à 48MHz,(SAMD21G18) avec 256KB de mémoire flash,et 32KB de SRAM.
Les détails sur cette carte ici :

C'est aussi là qu'est expliqué comment intégrer cette carte dans l'IDE d'Arduino.

Voici son brochage :

Les modification principales du code concernent le nom des entrées-sorties.
Etant donné que cette carte fonctionne avec 3,3 V comme le module GPS, plus besoin de pont de résistance sur le Rx du module.
Le module d'affichage LCD, qui tourne à 5 V n'a aucun problème avec les 3,3 V qui sont envoyés vers SDA et SCL.

Voici le comment c'est câblé :

  • Bouton BP1 >> D0 (commun des 3 boutons sur GND)
  • Bouton BP2 >> D1
  • Bouton BP3 >> D2
  • Sortie PPS du GPS >> D3 (plus si on veut une LED avec une résistance de 470 ohms en série vers le GND)
  • SDA de l'afficheur >> D4
  • SCL de l'afficheur >> D5
  • Rx du GPS >> D6
  • Tx du GPS >> D7
  • 3,3 V du GPS >> 3V3
  • 5 V de l'afficheur >> 5V

Une petite image du montage :

Et pour finir le code :

/***************************************************************************************************
 *                                        Horloge GPS précise                                      *
 *                                       Roland MATHIS 02/2024                                     *
 ***************************************************************************************************

Fonctionnement :
En mode auto (par défaut) , affiche l'heure UTC+1 (Europe de l'ouest) et prend en compte l'heure d'été et d'hiver
En mode manuel, permet d'afficher l'heure de n'importe quel fuseau horaire de la planète (de +11 h à -11 h),
mais sans prise en compte de l'heure d'été.

 * UTILISATION DES 3 BOUTONS
 * 
 * BOUTON + (BP1 sur le schéma) :
 * Appui long :  passage en mode manuel
 * Appui court : si mode manuel  +1h, si mode auto pas d'effet
 * Double appui : si mode manuel +1/4h, si mode auto pas d'effet
 *         
 * BOUTON - (BP2 sur le schéma):
 * Appui long :  passage en mode auto
 * Appui court : si mode manuel  -1h, si mode auto pas d'effet
 * Double appui : si mode manuel -1/4h, si mode auto pas d'effet
 *
 * BOUTON Affich (BP3 sur le schéma): 
 * Appui court : bascule entre affichage de heure-date et l'affichage de longitude-latitude-altitude
 * Appui long : bascule entre allumage et extinction du backlight de l'écran LCD
 * 
 */
 
#include <TinyGPSPlus.h>        // Pour extraire facilement les données des trames envoyées par le GPS
#include <OneButton.h>          // Pour la gestion des boutons-poussoirs
#include <LiquidCrystal_I2C.h>  // Pour gérer l'afficheur LCD et sa liaison I2C (appelle wire.h)
#include <TimeLib.h>            // Pour les conversions de temps au format unix et réciproquement

static const char PPS = D3; 
byte  nbsat;                    // Nombre de satellites captés
float latit, longit, altitud;   // Coordonnées géographiques
bool  PPS_low;                  // Mémorise l'état bas du signal PPS, permet de n'afficher qu'une fois au front montant de PPS
bool  precis;                   // Mis à true lorsqu'on capte au moins 4 satellites
bool  bcklght = true;           // Pour le pilotage du backlight de l'écran LCD
unsigned long top_PPS;          // Compte les millis() à partir de chaque front montant du signal PPS
bool  autoDST = true;           // Quand l'horloge est en mode auto
long  decal_UTC = 3600;         // Décalage par rapport à l'heure UTC, +3600 par défaut. Peut être négatif
int   decal_DST = 0;            // Décalage pour l'heure d'été (0 en hiver, 1 en été)
bool  ecran2 = false;           // Pour afficher un deuxième écran
bool  effac_mess = false;       // Pour effacer une seule fois un texte
// String JourSem[7] = {"Dimanche"," Lundi  "," Mardi  ","Mercredi"," Jeudi  ","Vendredi"," Samedi "};
// L'expression ci-dessous prends moins de place en mémoire que "String"
const char* JourSem[7] = {"Dimanche"," Lundi  "," Mardi  ","Mercredi"," Jeudi  ","Vendredi"," Samedi "};

byte preci[] =                  // Création d'un caractère personnalisé (un "P" blanc sur fond noir)
{ B11111, B10001, B10101, B10001, B10111, B10111, B11111, B00000 };
byte mode_auto [] =             // Création d'un caractère personnalisé (un "A" blanc sur fond noir)
{ B11111, B10001, B10101, B10001, B10101, B10101, B11111, B00000 };
byte heure_ete [] =             // Création d'un caractère personnalisé (un "S" blanc sur fond noir)
{ B11111, B10001, B10111, B10001, B11101, B10001, B11111, B00000 };
byte e_aigu [] =                // Création d'un caractère personnalisé (un "é")
{ B00010, B00100, B01110, B10001, B11111, B10000, B01110, B00000 };

OneButton btn_plus(D0, true);   // Déclaration du bouton + sur l'entrée A0
OneButton btn_moins(D1, true);  // Déclaration du bouton - sur l'entrée A1
OneButton btn_affich(D2, true); // Déclaration du bouton de changement d'affichage sur l'entrée A2

// Déclaration de l'objet TinyGPSPlus
TinyGPSPlus gps;

// Définition l'adresse de l'écran LCD, de la longueur et du nombre des lignes
LiquidCrystal_I2C lcd(0x27, 20, 4);

// Déclaration de la structure contenant les éléments de TimeLib
tmElements_t UTC;

void setup()
{
// Pour le fonctionnement de la librairie OneButton  
  // fonctions appellées par le bouton +
  btn_plus.attachClick(clic_plus);
  btn_plus.attachDoubleClick(clic_plus_quart);
  btn_plus.attachLongPressStart(clic_long_plus);
  // fonctions appellées par le bouton -
  btn_moins.attachClick(clic_moins);
  btn_moins.attachDoubleClick(clic_moins_quart);
  btn_moins.attachLongPressStart(clic_long_moins);
  // fonctions appellées par le bouton affich
  btn_affich.attachClick(clic_affich);
  btn_affich.attachLongPressStart(clic_long_affich);

// ******************************** INITIALISATION DU MODULE GPS ***********************************
// ************ Ces commandes ne sont valables que pour des module GPS de marque Quectel ***********
// ATTENTION : sorti d'usine le port série du GPS est paramétré à 9600, pour le premier démarrage mettre "Serial1.begin(9600);"
  Serial1.begin(19200);                 // Initialisation de la connexion série avec module GPS à 19200 bps

// Activer cette ligne seulement au 1er démarrage, pour éviter de recevoir la trame $GPTXT (mémorisé par le GPS)
// Serial1.println("$PQTXT,W,0,1*23")

// Activer cette ligne seulement au 1er démarrage, passe le port série du GPS de 9600 à 19200 bauds (mémorisé par le GPS)
// Serial1.println("$PQBAUD,W,19200*7E");
// Envoi d'une trame au module GPS pour qu'il n'envoie que les trames $GPRMC et $GPGGA utilisées par TinyGPSPlus
// et ainsi accélérer l'acquisition (non mémorisé par le GPS)
  Serial1.println("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28");
  
  pinMode(PPS, INPUT_PULLUP);      // Configurer la broche PPS comme entrée

  lcd.init();                      // Initialisation de l'afficheur LCD
  lcd.createChar(0, preci);        // Assignation de preci au caractère personnalisé 0 dans la RAM de l'afficheur
  lcd.createChar(1, mode_auto);    // Assignation de mode_auto au caractère personnalisé 1 dans la RAM de l'afficheur
  lcd.createChar(2, heure_ete);    // Assignation de heure_ete au caractère personnalisé 2 dans la RAM de l'afficheur
  lcd.createChar(3, e_aigu);       // Assignation de e_aigu au caractère personnalisé 3 dans la RAM de l'afficheur
  
// Affichage du logo d'allumage
  lcd.setCursor(7, 1); lcd.print("Horloge");
  lcd.setCursor(5, 2); lcd.print("GPS pr\003cise");   // "\003" écrit le caractère spécial 3 (le "é")
  lcd.backlight();                 // Allumer le rétroéclairage de l'écran
  delay(2000);                     // Durée d'affichage du texte

  // Affichage du "squelette" de l'affichage
  lcd.clear();
  lcd.setCursor(6, 0); lcd.print("--:--:--");
  lcd.setCursor(5, 1); lcd.print("--/--/----");
  lcd.setCursor(1, 2); lcd.print("Cherche satellites");
  lcd.setCursor(0, 3); lcd.print("UTC           Sat:");
}

void loop()
{
// Détection des appuis sur les boutons par OneButton
  btn_plus.tick();       // Surveillance du bouton +
  btn_moins.tick();      // Surveillance du bouton -
  btn_affich.tick();     // Surveillance du bouton affich

// ******** Boucle de lecture des données du GPS et de leur mise en forme (info reçue 1 x par seconde) ********
// et de leur affichage en mode dégradé (quand pas assez de satellites)
  while (Serial1.available() > 0)
  {
    { gps.encode(Serial1.read());
      if (gps.date.isUpdated())
        {
          // Passage des variables gps.xxx renvoyées par TinyGPSPlus dans TimeLib
          UTC.Day =    (gps.date.day());         // Les variables sous la forme UTC.xxx sont utilisées au format
          UTC.Month =  (gps.date.month());       // int dans une structure définie avant le setup par TimeLib
          UTC.Year =   (gps.date.year())-1970;   // UTC.Year est une variable qui doit commencer en 1970
          UTC.Hour =   (gps.time.hour());
          UTC.Minute = (gps.time.minute());
          UTC.Second = (gps.time.second());
          latit =      (gps.location.lat());
          longit =     (gps.location.lng());
          altitud =    (gps.altitude.meters());
          nbsat =      (gps.satellites.value());

          if (autoDST)    // Si on est en mode auto : Temps français (UTC+1) et vérif si heure d'été ou d'hiver)
            {
              if (Heure_ete(UTC.Year-30, UTC.Month, UTC.Day, UTC.Hour)) // appel à la fonction "bool Heure_ete..." qui retourne true ou false
                   decal_DST = 3600;
              else decal_DST = 0;
            }
         // Adaptation de l'heure au fuseau horaire et à l'heure d'été.
         // Le L derrière 3600 force un calcul au format long car (3600L*(decal_UTC+decal_DST)) peut dépasser + ou - 32767
         unsigned long temps_unix_local =  makeTime(UTC) + 1 + decal_UTC + decal_DST;
         setTime(temps_unix_local);         // Transformation du temps_unix_local en ses composantes avec TimeLib
                                            // second(), minute(), hour(), etc...
         if (millis() - top_PPS > 500)      // Si pas d'impulsion PPS pendant plus d'un cycle, on passe en mode d'affichage dégradé
                                            // valeur déterminée expérimentalement
           {
             precis = false;
             ecrit_lcd();  // Affichage des infos à l'écran en mode dégradé
           } 
       }
    }
  }

  if (!(digitalRead(PPS))) PPS_low = true;  // Met PPS_low à true dès le front descendant du signal PPS
                                            // Pour ne permettre qu'une fois l'affichage
                                           
// ****** Affichage des infos en mode précis, synchronisé sur le front montant du signal PPS (une fois par seconde) ******
  if (digitalRead(PPS) && (PPS_low))        // Moment du front montant de PPS
  {
    top_PPS = millis();                     // Début de comptage des millis à partir du front montant de PPS
    precis = true; PPS_low = false;    
    ecrit_lcd();                            // Affichage des données à l'écran
  }
}

void ecrit_lcd()                            // Affichage heure, date et jour
{
  if (year() != 2080 && year() != 1970)     // 2080 est la date de défaut quand le GPS n'a pas de fix.
  {                                         // 1970 est l'année par défaut dans TimeLib avant qu'il n'ait fait des calculs
    if (!(ecran2))                          // Affichage de l'écran de base (heures et date)
      {
        lcd.setCursor(12, 0);
        if (second() < 10) lcd.print("0"); lcd.print(second());
        lcd.setCursor(9, 0);
        if (minute() < 10) lcd.print("0"); lcd.print(minute());
        lcd.setCursor(6, 0);
        if (hour() < 10) lcd.print("0"); lcd.print(hour());
        lcd.setCursor(5, 1);
        if (day() < 10) lcd.print("0"); lcd.print(day());
        lcd.setCursor(8, 1);
        if (month() < 10) lcd.print("0"); lcd.print(month());
        lcd.setCursor(11, 1); lcd.print(year());
        if (effac_mess == false)            // Effacement une seule fois du message "Recherche satelittes"
          {                                 // Et affichage une seule fois du décalage UTC
            effac_mess = true;
            lcd.setCursor(1, 2); lcd.print("                  ");
            ecrit_lcd_UTC();                // Affichage du décalage par rapport à l'heure UTC
          }
        lcd.setCursor(6, 2); lcd.print(JourSem[weekday()-1]);
      }
    else
      { ecrit_lcd_geo(); }                  // Affichage de l'écran avec les données de position
  }
  lcd.setCursor(18, 3); lcd.print(nbsat); 
  if (nbsat < 10) lcd.print(" ");           // Cas où nbsat passe sous 10, pour effacer les unités
  // Affichage de différentes indications (mode précis, mode auto, heure d'été)
  lcd.setCursor(19, 0); if (precis) lcd.write(byte(0)); else lcd.print(" ");
  lcd.setCursor(19, 1); if (autoDST) lcd.write(byte(1)); else lcd.print(" ");
  lcd.setCursor(19, 2); if (decal_DST == 1) lcd.write(byte(2)); else lcd.print(" ");
}

void ecrit_lcd_UTC()                        // Affichage du décalage par rapport à l'heure UTC en heures et minutes
{
  int h = decal_UTC / 3600;                 // extraction des heures du décalage (nombre entier positif ou négatif)
  byte m = abs(((decal_UTC / 3600.0) - h)*60); // extraction des minutes du décalage (nombre entier positif)
  lcd.setCursor(3,3);
  if (decal_UTC > 0) lcd.print("+");
  if (decal_UTC < 0) lcd.print("-");        // Pour avoir un "-" aussi devant le 0 si on est entre 0 et -1 heure
    if (decal_UTC == 0) lcd.print("      ");  // ne rien afficher
    else
      {
        lcd.print (abs(h));
        if (m > 0)  { lcd.print(":"); lcd.print(m); } else  lcd.print("   ");
      }
  if (abs(decal_UTC) < 36000) { lcd.setCursor(8,3); lcd.print(" "); } // ôte le chiffre à droite quand on passe sous 10
}

void ecrit_lcd_geo()                        // Affichage des coordonnées géographiques
{
  lcd.setCursor(10, 0); lcd.print(latit,4); lcd.print("\337");      // \337 = symbole "°" dans le charset de l'écran LCD
  lcd.setCursor(10, 1); lcd.print(longit,4); lcd.print("\337"); 
  lcd.setCursor(10, 2); lcd.print(altitud,1); lcd.print("m"); 
}

bool Heure_ete(byte anUTC, byte moisUTC, byte jourUTC, byte heureUTC)
//*********************************************************************************************************
// Fonction extraite du fichier "simpleRTC.ccp" d'une librairie de "Bricoleau" sur le forum Arduino ici : *
// https://forum.arduino.cc/t/partage-librairie-simplertc-ds1307-ds3231-avec-heures-ete-hiver/376814      *
//         Détermine si on est en heure d'été ou en heure d'hiver en retournant true ou false             *
//*********************************************************************************************************
{
  //En France métropolitaine :
  //Passage de l'heure d'hiver à l'heure d'été le dernier dimanche de mars à 1h00 UTC (à 2h00 locales il est 3h00)
  //Passage de l'heure d'été à l'heure d'hiver le dernier dimanche d'octobre à 1h00 UTC (à 3h00 locales il est 2h00)
  if (moisUTC == 3)
    {
     byte derDimancheMars = 31 - ((5 + anUTC + (anUTC >> 2)) % 7);
     return jourUTC > derDimancheMars || (jourUTC == derDimancheMars && heureUTC != 0);
    }
  if (moisUTC == 10)
    {
     byte derDimancheOct = 31 - ((2 + anUTC + (anUTC >> 2)) % 7);
     return jourUTC < derDimancheOct || (jourUTC == derDimancheOct && heureUTC == 0);
    }
  return 3 < moisUTC && moisUTC < 10;  // true d'avril à septembre inclus
}

// *********************** Gestion des appuis sur les boutons ***********************
void clic_plus()         // Fonction appelée par un appui court sur le bouton +
{ if (!autoDST && decal_UTC <= 39600) { decal_UTC += 3600; ecrit_lcd(); ecrit_lcd_UTC(); } }

void clic_plus_quart()
{ if (!autoDST && decal_UTC <= 42300) { decal_UTC += 900; ecrit_lcd(); ecrit_lcd_UTC(); } }

void clic_long_plus()    // Fonction appelée par un appui long sur le bouton +
{ autoDST = false; decal_DST = 0; }

void clic_moins()        // Fonction appelée par un appui court sur le bouton -
{ if (!autoDST && decal_UTC >= -46800) { decal_UTC -= 3600; ecrit_lcd(); ecrit_lcd_UTC(); } }

void clic_moins_quart()
{ if (!autoDST && decal_UTC >= -49500) { decal_UTC -= 900; ecrit_lcd(); ecrit_lcd_UTC(); } }

void clic_long_moins()   // Fonction appelée par un appui long sur le bouton -
{ autoDST = true; decal_UTC = 3600; ecrit_lcd_UTC();}

void clic_affich()       // Fonction de bascule entre affichages appelée par un appui long sur le bouton affich
{
  if (precis)
    { ecran2 = !ecran2;  // n'affiche les coordonnées géographique qu'en mode précis
      if (ecran2)
        {
          lcd.setCursor(0, 0); lcd.print("Longitude: "); 
          lcd.setCursor(1, 1); lcd.print("Latitude: "); 
          lcd.setCursor(1, 2); lcd.print("Altitude: ");
          ecrit_lcd_geo(); 
        }
      else
        {
          lcd.setCursor(0, 0); lcd.print("        :  :       "); 
          lcd.setCursor(1, 1); lcd.print("      /  /        ");
          lcd.setCursor(1, 2); lcd.print("                  ");
          ecrit_lcd();
        }
    }
}

void clic_long_affich()  // fonction de bascule pour allumer-éteindre le backlight de l'écran
{
  bcklght = !bcklght;
  if (bcklght)  lcd.backlight();
  if (!bcklght) lcd.noBacklight();
}

Bon bidouillages à toutes z'et tous !

Roland

Beau projet.
Pourquoi ne pas mettre un écran OLED, plus compact et plus joli que le LCD ?

En fait j'ai prévu un projet final qui sera basé sur un un écran TFT IPS de 800 x 480 pixels, avec un affichage bien plus "léché" et d'autres infos comme les éphémérides.
Bon là ce sera avec un ESP32-S3 parce que ça demande pas mal de mémoire pour l'écran.
Du neuf normalement d'ici quelques semaines !

Roland

Bonjour @rollmops67,
Tout d'abord bravo pour la réalisation de ce projet. Pour ne pas perdre la main au niveau programmation j'ai lu ton code en diagonale et une chose m'a de suite sautée aux yeux. Tu utilises la classe String pour déclarer un tableau :

String JourSem[7] = {"Dimanche"," Lundi  "," Mardi  ","Mercredi"," Jeudi  ","Vendredi"," Samedi "};

Lorsque j'ai débuté en programmation et en particulier sur des µC de type nano comme c'est le cas dans la première partie de ton projet, on m'a appris à ne pas utiliser la classe String mais plutôt les c-string (chaînes de caractères, tableaux comportant plusieurs données de type char, dont le dernier élément est un caractère nul '\0' non visible). Pourquoi ? à cause du peu de mémoire dont tu disposes notamment dans la première partie de ton projet avec l'utilisation d'une nano. Du coup je me permets juste cette petite remarque mise en évidence par ce bout de code :

constexpr char *jourSem[] = {"Dimanche", " Lundi  ", " Mardi  ", "Mercredi", " Jeudi  ", "Vendredi", " Samedi "}; //Tableau de pointeurs
constexpr int nbElements = (sizeof jourSem / sizeof(*jourSem));
//constexpr int nbElements = (sizeof JourSem / sizeof(JourSem[0])); 

void setup() {
  Serial.begin(115200);
//  for (auto &t : jourSem) Serial.println(t); //C++
//  Serial.print('\n'); Serial.println("deuxième boucle"); Serial.print('\n');
  for (uint8_t x = 0; x < nbElements ; x++) Serial.println(jourSem[x]); 
}

void loop() { 

}

En ce qui concerne l'affichage des jours de la semaines dans ton code, j'aurai plutôt fait comme ça :

au lieu de :

String JourSem[7] = {"Dimanche"," Lundi  "," Mardi  ","Mercredi"," Jeudi  ","Vendredi"," Samedi "};

lire :

const char* JourSem[7] = {"Dimanche"," Lundi  "," Mardi  ","Mercredi"," Jeudi  ","Vendredi"," Samedi "};

ou :

const char* JourSem[] = {"Dimanche"," Lundi  "," Mardi  ","Mercredi"," Jeudi  ","Vendredi"," Samedi "};

Voilà c'est juste une remarque comme ça :wink: et surtout bravo pour ton travail.

Bonne soirée.
Philippe.

Merci @philippe86220 pour ton commentaire instructif !

Je sais que la classe String "mange" pas mal de mémoire mais vu mes connaissances (j'ai appris 100% de ce que je sais "sur le tas" et sans bouquin, j'ai donc encore bien des lacunes, même pour des choses basiques) j'ai encore un peu de peine avec tout ce qui est "char", et si en plus il y a le "*" (pointeur ?) je plane encore plus.
Pareil pour ton exemple où tu utilises "constexpr", c'est la première fois de ma vie que je vois cette expression...
C'est quoi la différence entre "constexpr int nbElements" et "static const int nbElements" par exemple ?

Roland

Je ne comprends pas pourquoi tu utilises SoftwareSerial sur D6 et D7 qui sont les entrées/sorties de l'UART matériel. L'utilisation de l'UART matérielle aurait très certainement de meilleures performances.

Le SAMD21 a quand même plus de RAM que les AVR 'classiques' l'utilisation des String devrait poser moins de problèmes,

Pour constexpr, voir ici :

Mais c'est surtout du C++, tu peux oublier pour l'instant et utiliser const.
Quant à static :

En tant que variables globales c'est inutile car dans ce cas static sert à limiter la portée des variables au seul fichier de sa déclaration or ce n’est pas justifié ici :wink:

Dans une fonction, un bloc, si une variable est déclarée static elle va conserver sa valeur malgré la sortie de la procédure, fonction. Ce n'est pas le cas si elle n'est pas déclarée static, elle disparait en fin d'exécution de fonction …

Exemple dans une procédure :

void procedureSans () {
  uint8_t a = 1 ;
  a++;
  Serial.print("valeur de a sans static : "); Serial.println(a);
  
}
void procedureAvec () {
  static uint8_t a = 1 ;
  a++;
  Serial.print("valeur de a avec static : "); Serial.println(a);
  
}
void setup() {
  Serial.begin(115200);
  procedureSans();
  procedureSans();
  procedureAvec ();
  procedureAvec ();
}

void loop() {
  

}

Bonne soirée.

1 Like

C'est bien pour cette raison que :

Bonsoir !
Simplement parce que je ne sais pas faire autrement qu'avec SoftwareSerial...
(ou HardwareSerial avec un ESP32)
Mais je suis prêt à apprendre !

Roland

Tu as tout en main pourtant.
Le lien que tu as donné au #7 sur le Wiki, il y a un exemple pour utiliser l'UART. Regarde le paragraphe Serial et tu verras que pour utiliser l'UART matériel il faut utiliser Serial1

Pfff, mais que je suis c.n...
Modif faite (et post #7 mis à jour), ça ronronne maintenant à 19200 bps sans la moindre perte...

Un grand MERCI, encore un jour où je vais me coucher moins bête ! :upside_down_face:

Roland

Bonjour,
si vous êtes intéressé, j'ai toutes les routines éphémérides, avec les saisons, équinoxe et solstices.
J'ai aussi 2 questions:
1 pourquoi ne pas utiliser une interrupt service routine déclenchée par 1PPS ?
2 J'ai un programme similaire du genre Bruce E. Hall mais qui distingue acqisition de donnée GPS et affichage déclenché par ISR sur 1PPS. Cela permet d'évaluer le temps d'attente pour la synchro et donc la reserve de puissance CPU.
A votre disposition.

Bonjour JLBCS !

Pour l'instant pour les éphémérides j'utilise cette bibliothèque :

qui me donne ce dont j'ai besoin (heures de lever, de coucher, de transit, azimut, élévation), mais vos routines peuvent bien sûr être intéressantes !

Pour le 1) : oui j'aurai effectivement pu utiliser une interruption, mais dans les fait le programme s'en passe très bien, et je ne note pas de lag VISIBLE entre la LED 1PPS (directement branchées sur la puce GPS) et l'afficheur LCD.
Pour le 2) : Quand vous parlez de projet similaire du genre Brice E. Hall, je suppose que vous parlez de ça ?

Je suis en train d'en finaliser un similaire (qui se base sur le projet de cette discussion), il ne me reste plus que la mise en boîtier à faire.

La grosse différence est qu'il utilise un ESP32 S3 avec 8 Mo de PSRAM et 16 Mo de mémoire flash, car l'afficheur de 800 x 480 est assez gourmand en mémoires.
Voici déjà un aperçu de ce que cela donne :

Les éphémérides affichées sont (dans le sens de la lecture) :
Azimut - heure du transit - Elévation (avec une flèche qui pointe vers le haut où le bas selon que l'élévation augmente ou diminue)
Durée du jour
Heure de lever - Différence de durée du jour entre hier et aujourd'hui (la flèche pointe vers le haut où le bas selon que la durée des jours augmente ou baisse) - Heure de coucher.

Là aussi même sans interruption on ne constate pas de lag entre l'affichage des secondes et la LED 1PPS.

Quant à "évaluer le temps d'attente pour la synchro et donc la réserve de puissance CPU" je n'en suis pas encore là avec mes petites connaissances !!
Je me débrouille simplement pour que le module GPS m'envoie juste ce qu'il faut comme données, ce qui prends environ 75 ms (GPRMC et GPGGA à 19200 bps), et laisse le temps qu'il faut pour les calculs avant de passer à l'affichage.

Cordialement, Roland

Bonjour,
j'ignorais tout sur les BBL time et solar. J'ai donc écrit mes 2 routines pour l'EOT et la déclinaison du soleil à midi ainsi que les saisons, les equinoxes et les solstice. Un développement assez rudimentaire.
Venant des grands systèmes IBM j'ai mis du temps à comprendre la lecture et l'organisation du programme. Maintenant c'est bon je maîtrise.
J'ai 2 pages sur ce sujet en PDF. Mail adresse ?
Cordialement, Jacques