/*
   _===_ _           _                  ______ _     _             _ _ _
  |  ___| |         | |                 | ___ (_)   | |           (_) | |
  | |__ | | ___  ___| |_ _ __ ___ ______| |_/ /_  __| | ___  _   _ _| | | ___ _   _ _ __
  |  __|| |/ _ \/ __| __| '__/ _ \______| ___ \ |/ _` |/ _ \| | | | | | |/ _ \ | | | '__|
  | |___| |  __/ (__| |_| | | (_) |     | |_/ / | (_| | (_) | |_| | | | |  __/ |_| | |
  \____/|_|\___|\___|\__|_|  \___/      \____/|_|\__,_|\___/ \__,_|_|_|_|\___|\__,_|_|

  Contrôle de référence 10 MHz par GPS: Projet avec contrôle DAC 12/14/16 bits T.I. DACx0501
  Électro-Bidouilleur, V2, Mai 2025. youtube.com/Électro-Bidouilleur
  Licence de droits: Attribution 4.0 International (CC BY 4.0)  Lisez creativecommons.org/licenses/by/4.0/deed.fr  

  Historique des Versions
  =======================
  V2 (2025-05): Changement du mode de transmission no.2 du SPI vers le DAC80501. Le mode no.0 précédemment utilisé ne corrspond pas au mode de fonconnement du DAC80501 (donnée saisie sur front descendant de l'horloge).
  V1 (2022-03): Version initiale.

*/

// DIRECTIVES AU COMPILATEUR
#include <SPI.h> 
#include <Wire.h>
#include <FlashStorage_STM32.h>               // Nécessite l'installation de FlashStorage_STM32 dans le gestionnaire de librairies de Arduino IDE.
#include <LiquidCrystal_PCF8574.h>            // Nécessite l'installation de LiquidCrystal_PCF8574 dans le gestionnaire de librairies de Arduino IDE.

// ********** Pour le STM32F103T8C6 (Blue Pill), il faut eliminer ou mettre en commentaire la ligne suivante
#define BLACK_PILL_USED


#define addr_LCD                0x27                                    // L'adresse I2C du LCD
LiquidCrystal_PCF8574 lcd(addr_LCD);                                    // Configure l'adresse I2C du LCD à 0x27

#define BROCHE_PPS              PA1                                     // Timer2, entrée de capture (Signal PPS)
#define BROCHE_REF_IN           PA0                                     // Timer2, entrée d'incrémentation du compteur (signal 10MHz).
#define BROCHE_SPI_SS           PB12                                    // La broche de sortie vers le Slave Select du DAC
#define BROCHE_SPI_MOSI         PB15                                    // La broche de données SPI vers le DAC
#define BROCHE_SPI_MISO         PB14                                    // La broche de données SPI venant du DAC (pas utilisée)
#define BROCHE_SPI_SCLK         PB13                                    // La broche de sortie SPI vers l'horloge du DAC
#define BROCHE_UART_RX          PA3                                     // La broche de données venant du GPS
#define BROCHE_UART_TX          PA2                                     // La broche de données allant au GPS (non utilisée)
#define BOUTON_EFFAC_ALM        PB9                                     // La broche du bouton-poussoir pour effacer les alarmes antérieures
#define FREQ_OCXO               10000000.0000                           // Fréquence de l'OCXO. Exprimer avec 4 décimales pour forcer la précision des calculs
#define OUI                     true
#define NON                     false
#define SEPAR                   "|"
#define VT_CLS                  "\x1B[2J\x1B[H"                         // Code VT-100 "clearscreen + cursorhome"
#define VT_DEFAULT_COLOR        "\x1B[49m"                              // Code VT-100 de la couleur d'arrière-plan par défaut
#define VT_REVERSE              "\x1B[7m"                               // Code VT-100 de la couleur d'arrière-plan inversée
#define VT_NORMAL               "\x1B[m"                                // Code VT-100 de la couleur d'arrière-plan normale
#define LIGNE_SEPAR             "================================================================================"
#define GPRMC_PREAMBLE          "$GNRMC"            
#define MESSAGE_ACCUEUIL        "\nContrôle de référence 10 MHz par GPS sur STM32, Version 2, Mai 2025.\r\nÉlectro-Bidouilleur: https://bidouilleur.ca\r\n"    // Message d'accueuil

HardwareSerial Serial2(BROCHE_UART_RX, BROCHE_UART_TX);                                       // Instanciation du UART logiciel pour recevoir les messages du GPS
HardwareTimer RefTimer(TIM2);                                           // Instanciation de la minuterie TIM2

// VARIABLES GLOBALES
unsigned short int            compte_nominal;                           // Le résultat de l'échantillon de TIM2 si la fréquence est d'exactement 10 MHz
volatile unsigned short int   nouv_val_capture;                         // La nouvelle valeur de l'échantillon de TIM2
volatile unsigned short int   preced_val_Capture;                       // L'ancienne valeur de l'échantillon de TIM2
volatile unsigned short       compteur_captures = 0;                    // Le compteur du nombre de PPS durant l'échantillonnage
volatile unsigned int         compte_osc;                               // L'échantillon de TIM2 à analyser
short int                     delta_compte_osc;                         // La différence entre l'échantillon et le compte nominal
short int                     somme_delta_compte_osc = 0;               // La somme des delta_compte_osc pour fins de calcul
unsigned short int            compteur_echantillons = 0;                // Le compteur du nombre d'échantillons 
double                        moyenne_delta_compte_osc;                 // La moyenne accumulée des différences échantillon-nominal
double                        moyenne_decalage_hz = 0;                  // La moyenne du décalage des échantillons, exprimé en Hz 
double                        frequence_mesuree_hz;                     // La fréquence 10 MHz mesurée
static double                 reponse_lsb_par_hz;                       // La variation du DAC correspondant à un changement de 1 Hz de l'oscillateur. Valeur calculée. Doit être "Static" sinon la valeur est perdue en entrant dans la fonction Loop! Comportement inattendu!
volatile bool                 echantillon_pret = NON;                   // Le signal annonçant qu'un échantillon est prêt
volatile bool                 detection_pps = NON;                      // Le signal indiquant qu'une pulsation PPS a été détectée
bool                          fin_de_cycle = NON;                       // Le signal annonçant la fin du cycle de mesure  
int                           delta_dac;                                // La correction à appliquer à la valeur du DAC 
unsigned short int            dac_val;                                  // La valeur courante de DAC (16 bits).
unsigned short int            nouv_dac_val;                             // La nouvelle valeur de DAC (16 bits) calculée.
char                          signe_delta_dac;                          // Le caractère (+ ou -) à afficher sur le résultat delta_dac
unsigned long int             debut_led_millis = 0;                     // La valeur conservée de millis pour fins d'extinction de la LED
unsigned long int             debut_stabilisation_millis = 0;           // La valeur conservée de millis pour fins de stabilisation du FLL
unsigned long int             debut_rx_commande_millis;                 // La valeur conservée de millis pour fins de délai de réception d'une commande
unsigned long int             debut_detection_pps_millis;               // La valeur conservée de millis pour fins de détection de l'absence de signal PPS
bool                          reinitialiser_compteur = NON;             // Le signal pour aviser de ré-initialiser le compteur TIM2
bool                          mode_rx_commande_uart;                    // Le signal indiquant qu'une commande reçue est en voie d'être complétée
char                          caract_recu;                              // Le caractère reçu du UART de l'ordi.
String                        chaine_caracts_recues = "";               // La chaîne de caractères reçue du UART de l'ordi.
String                        commande;                                 // Une commande reçue de l'ordi
unsigned short int            index1;                                   // La position du premier paramètre de la commande reçue
unsigned short int            index2;                                   // La position du deuxième paramètre de la commande reçue
unsigned short int            index3;                                   // La position du troisième paramètre de la commande reçue
unsigned short int            index4;                                   // La position du quatrième paramètre de la commande reçue
String                        param1;                                   // Le premier paramètre de la commande reçue
String                        param2;                                   // Le deuxième paramètre de la commande reçue
String                        param3;                                   // Le troisième paramètre de la commande reçue
bool                          fll_actif;                                // Signale l'état actif ou inactif du FLL
unsigned char                 hrs;                                      // Valeur numérique de l'heure
unsigned char                 mins;                                     // Valeur numérique de la minute
unsigned char                 secs;                                     // Valeur numérique de la seconde
unsigned char                 day_val;                                  // Valeur numérique du jour
unsigned char                 month_val;                                // Valeur numérique du mois
unsigned char                 year_val;                                 // Valeur numérique de l'année
String                        chaine_caracts_gps_recues;                // La chaîne de caractères reçue du UART du GPS.
String                        chaine_heure;                             // La chaine de caractères de l'heure reçue du GPS
String                        chaine_date;                              // La chaine de caractères de la date reçue du GPS
String                        hrs_string;                               // Valeur texte à afficher de l'heure
String                        mins_string;                              // Valeur texte à afficher de la minute
String                        secs_string;                              // Valeur texte à afficher de la seconde
String                        day_string;                               // Valeur texte à afficher du jour           
String                        month_string;                             // Valeur texte à afficher du mois
String                        year_string;                              // Valeur texte à afficher de l'année
bool                          leap_year;                                // L'état actif ou inactif de l'année bissextile
bool                          cycle_long = NON;                         // Indique si le cycle courant est un cycle long
bool                          cycle_moyen = NON;                        // Indique si le cycle courant est un cycle moyen
bool                          cycle_court = OUI;                        // Indique si le cycle courant est un cycle court
bool                          donnees_gps_valides = NON;                // Indique si le GPS envlie des données et un signal PPS valide
double                        boucle_PI_decalage_hz;                    // Le décalage (en Hz) calculé par la boucle P-I et servant à ajuster le DAC 
double                        moyenne_decalage_hz_array[10];            // Attention: un double prend 8 bytes! Tableau d'accumulation de la moyenne des décalages des cycles
int                           index_moyenne_decalage_hz_array;          // Index de position du tableau d'accumulation de la moyenne des décalages
int                           max_index_moyenne_decalage_hz_array;      // Maximum de l'index de position du tableau d'accumulation de la moyenne des décalages
double                        moyenne_moyenne_decalage_hz_array;        // La moyenne des moyenne des décalages des cycles tirées du tableau
byte                          delta[8] = {                              // La définition du caractère grec delta majuscule utilisé sur le LCD
                                           B00100,
                                           B00100,
                                           B01010,
                                           B01010,
                                           B10001,
                                           B10001,
                                           B11111
                                         }; 
// Alarmes du Système
unsigned char           alarmes_courantes =                 B00000000;  // Le registre des alarmes sauvegardant la présence d'alarmes courantes
unsigned char           alarmes_passees   =                 B00000000;  // Le registre des alarmes antérieures
// Les positions des bits d'alarme dans les registres d'alarmes et d'alarmes antérieures sont les suivantes
#define                 ALARME_ACQUISITION_INIT             0           // En acquisition initiale après démarrage
#define                 ALARME_DAC_LIMITE                   1           // Le DAC a atteint sa valeur minimale ou maximale
#define                 ALARME_FLL_ETEINT                   2           // Le FLL est eteint
#define                 ALARME_PPS_MANQUANT                 3           // Le signal PPS du GPS est manquant
#define                 ALARME_REJET_ECHANT                 4           // En cycle long, l'échantillon est rejeté car trop éloigné
#define                 ALARME_FLL_DEVERROUILLE             5           // Le FLL est déverrouillé car pas en cycle long
#define                 ALARME_10M_ABSENT                   6           // Le signal de référence 10 MHz est absent       
#define                 ALARME_GPS_NON_VALIDE               7           // Le GPS fournit des données non valides       

// Variables du système par défaut
unsigned short int      nomb_pps_par_echant =               10;         // Le nombre de pulsations PPS par échantillon de fréquence
unsigned short int      nomb_echantillons_par_cycle =       1;          // Le nombre d'échantillons dans la période d'analyse     
unsigned char           nomb_bits_dac =                     16;         // Le nombre de bits du DAC
unsigned short int      duree_cycle_court =                 1;          // Le nombre d'échantillons d'un cycle court
unsigned short int      duree_cycle_moyen =                 10;         // Le nombre d'échantillons d'un cycle moyen
unsigned short int      duree_cycle_long =                  100;        // Le nombre d'échantillons d'un cycle long
float                   seuil_cycle_moyen =                 0.101;      // Le seuil (en Hz) de changement de cycle moyen -> court
float                   seuil_cycle_long =                  0.0101;     // Le seuil (en Hz) de changement de cycle long -> moyen
float                   p_index =                           1;        // La composante Kp de la boucle P-I
float                   i_index =                           0;        // La composante Ki de la boucle P-I
unsigned char           seuil_rejet_delta_compte =          2;          // En cycle long, le seul forçant le rejet de l'échantillon
bool                    mode_verbose =                      OUI;        // Le signal indiquant que les messages envoyés au UART sont en mode détaillé
bool                    mode_verbose_vt100 =                OUI;        // L'opton indiquant si les caractères de contrôle VT-100 doiven être envoyés

// Valeurs fixes du système par défaut. Doivent être mesurées et saisies ici:
#define                 REPONSE_OCXO_HZ_PAR_V               1.489       // Morion MV89A: 1.489  Isotemp OCXO131-100: 2.6385  Réponse mesurée de l'OCXO utilisé. Doit etre un nombre avec decimale(s). Sera négatif si l'OCXO a une réponse inversée -Hz/V.
#define                 DAC_TENSION_MIN                     0.0123      // Tension minimale du DAC, sans tenir compte de l'étage de gain suivant. Doit etre un nombre avec decimale(s)
#define                 DAC_TENSION_MAX                     4.995       // Tension maximale du DAC, sans tenir compte de l'étage de gain suivant. Doit etre un nombre avec decimale(s)
#define                 DAC_POST_GAIN                       1.0         // Gain de l'ampli op en sortie du DAC. Doit etre un nombre avec decimale(s)


// Fonction ajoutant des zéros à l'avant d'une valeur "integer" pour garantir une largeur d'affichage constante
String int_bourre_zeros_str(int a)
{
  if (a >= 1000 && a < 10000) return "0" + String(a);
  else if (a >= 100 && a < 1000) return "00" + String(a);
  else if (a >= 10 && a < 100) return "000" + String(a);
  else if (a < 10) return "0000" + String(a);
  else return String(a);
}
// Fonction ajoutant des zéros à l'avant d'une valeur "double" pour garantir une largeur d'affichage constante
String double_bourre_zeros_str(double a)
{
  if (a >= 100 && a < 1000) return String(a,3);
  else if (a >= 10 && a < 100) return String(a,4);
  else if (a < 10) return String(a,5);
}

// Fonction gérant l'interruption de la minuterie T2 générée par le signal PPS
void interrupt_TIM2_PPS()
{
  detection_pps = OUI;                                    // Aviser de la détection d'une impulsion PPS
  if (reinitialiser_compteur)                             // Demande de réinitialiser le compteur?
  {  // Oui
    reinitialiser_compteur = NON;                         // Effacer la demande
    nouv_val_capture = TIM2->CCR2;                        // Prendre un nouvelle valeur du compteur
    compteur_captures = 1;                                // Ré-initialiser le compteur des captures
  }
  else if (compteur_captures++ >= nomb_pps_par_echant)    // Pas réinitialisé. Attendu la durée spécifiée en secondes?
    { // Oui
      if (fabs(TIM2->CCR2 - preced_val_Capture) < 1000) bitSet(alarmes_courantes, ALARME_10M_ABSENT);   // Détection de référence 10 MHz manquante
      else bitClear(alarmes_courantes, ALARME_10M_ABSENT);// Référence 10 MHz non manquante
      preced_val_Capture = nouv_val_capture;              // Conserver la précédente valeur capturée de TIM2 
      nouv_val_capture = TIM2->CCR2;                      // Lire la nouvelle valeur capturée de TIM2
      if (nouv_val_capture > preced_val_Capture) compte_osc = nouv_val_capture - preced_val_Capture;    // Gérer le débordement des valeurs nouvelle et précédente
      else compte_osc = 65536 + nouv_val_capture - preced_val_Capture;                                  //     "
      compteur_captures = 1;                              // Ré-initialiser le compteur des captures
      echantillon_pret = OUI;                             // Aviser que l'échantillon est prêt.
    }
}

// Cette fonction force le redémarrage d'un cycle d'acquisition
void redemarrer_acquisition()
{
  reinitialiser_compteur = OUI;                           // Ré-initialiser le compteur TIM2
  compteur_echantillons = 0;                              // Ré-initialiser les différentes valeurs pour démarrer un neauveau cycle d'échantillonnage.
  moyenne_delta_compte_osc = 0;                           //            "
  somme_delta_compte_osc = 0; 
  nomb_echantillons_par_cycle = duree_cycle_court;        // Débuter avec un cycle court.
  cycle_court = OUI;                                      //            " 
  cycle_moyen = NON;
  cycle_long = NON;            
}

// La fonction envoyant une nouvelle valeur au DACx0501
void Envoi_Val_Dac(unsigned short int valeur)
{
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE2)); // Début d'une écriture d'un des DACs via le port SPI (débit, ordre des bits, mode du bus SPI)
  digitalWrite(BROCHE_SPI_SS, LOW);                               // Abaissement du signal Slave Select du DAC  
  SPI.transfer(8);                                                // Transfert de l'adressse du registre de la valeur du DAC
  SPI.transfer16(valeur<<(16-nomb_bits_dac));                     // Transfert des 16 bits 
  digitalWrite(BROCHE_SPI_SS, HIGH);                              // Relèvement du signal Slave Select du DAC  
  SPI.endTransaction();                                           // Fin de l'écriture du DAC, libération du bus.
}

// Fonction qui récupère les différents paramètres du système de la mémoire flash
void lire_params_eeprom()
{
  unsigned long int signature;                                    // Contient la signature à lire
  EEPROM.get(0,signature);                                        // Lire la signature de l'adresse 0
  if (signature == 0xCAFEFADE)                                    // Concordance de signature avec "CAFEFADE"?
  {                                                               // Oui, récupérer les différents paramètres de la mémoire Flash
    EEPROM.get(4, nomb_pps_par_echant);                           //           "
    EEPROM.get(6, duree_cycle_court );
    EEPROM.get(8, duree_cycle_moyen);
    EEPROM.get(10, duree_cycle_long);
    EEPROM.get(12, seuil_cycle_moyen);
    EEPROM.get(16, seuil_cycle_long);
    EEPROM.get(20, mode_verbose);
    EEPROM.get(21, mode_verbose_vt100);
    EEPROM.get(22, seuil_rejet_delta_compte);    
    EEPROM.get(23, p_index);                      
    EEPROM.get(27, i_index);                     
    EEPROM.get(31, nomb_bits_dac);                     
    Serial.println("Parametres recuperes de la memoire Flash");
  } 
  // Non, pas de concordance de signature de détectée
  else Serial.println("Paramètres par défaut utilisés");          // Les paramètres par défaut seront utilisés
}   

// Fonction qui sauvegarde les différents paramètres du système en mémoire flash
// Attention! La mémoire Flash a une durée de vie garantie de 10 000 écritures.
void sauv_params_eeprom()
{ 
  EEPROM.setCommitASAP(NON);                                      // Forcer l'écriture au Flash seulement avec la commande commit()
  EEPROM.put(0, 0xCAFEFADE);                                      // 4 octets. Sauvegarde de la signature pour vérifier que la mémoire flash contien des paramètres.
  EEPROM.put(4, nomb_pps_par_echant);                             // 2 octets. Sauvegarde des autres paramètres.
  EEPROM.put(6, duree_cycle_court);                               // 2 octets
  EEPROM.put(8, duree_cycle_moyen);                               // 2 octets
  EEPROM.put(10, duree_cycle_long);                               // 2 octets
  EEPROM.put(12, seuil_cycle_moyen);                              // 4 octets
  EEPROM.put(16, seuil_cycle_long);                               // 4 octets
  EEPROM.put(20, mode_verbose);                                   // 1 octet
  EEPROM.put(21, mode_verbose_vt100);                             // 1 octet
  EEPROM.put(22, seuil_rejet_delta_compte);                       // 1 octet
  EEPROM.put(23, p_index);                                        // 4 octets
  EEPROM.put(27, i_index);                                        // 4 octets
  EEPROM.put(31, nomb_bits_dac);                                  // 1 octet
  EEPROM.commit();                                                // Compléter l'écriture de la mémoire
}

// Fonction d'affichage des résultats appelée quand il y un PPS de reçu. 
void afficher_resultats_console()
{
  if (!mode_rx_commande_uart)                                                     // Commande en train d'être reçue sur le UART?
  {                                                                               // Non, autorisé d'afficher les résultats
    if (mode_verbose)                                                             // Mode Verbose (détaillé) actif?
    { // Oui
      if (mode_verbose_vt100) Serial.print(VT_CLS);                               // Si mode d'affichage VT-100 actif, envoyer code d'effacement de l'écran
      Serial.println();                                                           // Imprimer tous les champs vers le port série
      Serial.println(LIGNE_SEPAR);                                                //           "
      Serial.println(chaine_date + "-" + chaine_heure + "-UTC");
      Serial.print("Alarmes:                                          ");
      if ((mode_verbose_vt100) && (alarmes_courantes)) Serial.print(VT_REVERSE);  // Si mode d'affichage VT-100 actif, afficher les alarmes en vidéo inversée
      Serial.print(bitRead(alarmes_courantes, ALARME_ACQUISITION_INIT) ? "A" : (bitRead(alarmes_passees, ALARME_ACQUISITION_INIT) ? "a" : "-"));
      Serial.print(bitRead(alarmes_courantes, ALARME_FLL_DEVERROUILLE) ? "V" : (bitRead(alarmes_passees, ALARME_FLL_DEVERROUILLE) ? "v" : "-"));
      Serial.print(bitRead(alarmes_courantes, ALARME_DAC_LIMITE) ? "D" : (bitRead(alarmes_passees, ALARME_DAC_LIMITE) ? "d" : "-"));
      Serial.print(bitRead(alarmes_courantes, ALARME_FLL_ETEINT) ? "F" : (bitRead(alarmes_passees, ALARME_FLL_ETEINT) ? "f" : "-"));
      Serial.print(bitRead(alarmes_courantes, ALARME_PPS_MANQUANT) ? "P" : (bitRead(alarmes_passees, ALARME_PPS_MANQUANT) ? "p" : "-"));
      Serial.print(bitRead(alarmes_courantes, ALARME_REJET_ECHANT) ? "R" : (bitRead(alarmes_passees, ALARME_REJET_ECHANT) ? "r" : "-"));      
      Serial.print(bitRead(alarmes_courantes, ALARME_10M_ABSENT) ? "O" : (bitRead(alarmes_passees, ALARME_10M_ABSENT) ? "o" : "-"));      
      Serial.println(bitRead(alarmes_courantes, ALARME_GPS_NON_VALIDE) ? "G" : (bitRead(alarmes_passees, ALARME_GPS_NON_VALIDE) ? "g" : "-"));      
      if (mode_verbose_vt100) Serial.print(VT_NORMAL);                            // Si mode d'affichage VT-100 actif, afficher les alarmes en vidéo normal
      if (!bitRead(alarmes_courantes, ALARME_PPS_MANQUANT))                       // PPS manquant?
      {                                                                           // Non, afficher le reste de l'info
        Serial.print("Cycle:                                            ");
        Serial.print(cycle_court ? "Court" : "");
        Serial.print(cycle_moyen ? "Moyen" : "");
        Serial.println(cycle_long ? "Long" : "");
        Serial.print("Échantillon:                                      ");
        Serial.print(compteur_echantillons);
        Serial.print(" / ");
        Serial.println(nomb_echantillons_par_cycle);     
        Serial.print("Valeur du DAC courante:                           ");
        Serial.println(dac_val);
        Serial.print("Compteur de l'oscillateur | Compte Nominal:       ");
        Serial.print(compte_osc);
        Serial.print(" | ");
        Serial.println(compte_nominal);      
        Serial.print("Écart vs. compte nominal:                         ");
        Serial.println(delta_compte_osc);
        Serial.print("Moyenne des écarts du compte:                     ");
        Serial.println(moyenne_delta_compte_osc,6);
        Serial.print("Moyenne des écarts (ppm)                          ");
        Serial.println(moyenne_decalage_hz / FREQ_OCXO * 1e6, 4);
        Serial.print("Moyenne des écarts (Hz):                          ");
        Serial.println(moyenne_decalage_hz,6);
        Serial.print("Fréquence moyenne calculée de la référence (Hz):  ");
        Serial.println(frequence_mesuree_hz,6);
        if (fin_de_cycle)                                                           // Fin de cycle d'échantillonnage?
        {                                                                           // Oui, Afficher les données de fin de cycle.
          if (mode_verbose)                                                         // Mode Verbose (détaillé) actif?               
          {                                                                         // Oui, donner plus de détails
            if (mode_verbose_vt100) Serial.print(VT_REVERSE);                       // Si mode d'affichage VT-100 actif, afficher en vidéo inversée
            Serial.println("CYCLE DE MOYENNE COMPLÉTÉ");                            // Impression des résultats de fin de cycle
            Serial.print("Ajustement fait au DAC:                           ");     //        "
            Serial.println(-delta_dac);     
            Serial.print("Nouvelle valeur du DAC:                           ");
            Serial.println(nouv_dac_val);     
            Serial.print("Calcul de l'écart en Hz (Boucle PI):              ");
            Serial.println(boucle_PI_decalage_hz,6);
            
            Serial.println("Pause de stabilisation...");   
            if (mode_verbose_vt100) Serial.print(VT_NORMAL);                        // Si mode d'affichage VT-100 actif, retourner mode vidéo normal
          }            
        }
      }
      Serial.println(LIGNE_SEPAR);
    }
    else                                                                            // Pas en mode verbose, afficher en condensé
    { 
      String tempstr =  String('S') + SEPAR +                                       // Construire la chaîne à afficher. Bogue du compilateur. Il faut faire String('S').
                        chaine_date + "_" + chaine_heure + SEPAR +
                        (bitRead(alarmes_courantes, ALARME_ACQUISITION_INIT) ? "A" : (bitRead(alarmes_passees, ALARME_ACQUISITION_INIT) ? "a" : "_")) +
                        (bitRead(alarmes_courantes, ALARME_FLL_DEVERROUILLE) ? "V" : (bitRead(alarmes_passees, ALARME_FLL_DEVERROUILLE) ? "v" : "_")) +
                        (bitRead(alarmes_courantes, ALARME_DAC_LIMITE) ? "D" : (bitRead(alarmes_passees, ALARME_DAC_LIMITE) ? "d" : "_")) +
                        (bitRead(alarmes_courantes, ALARME_FLL_ETEINT) ? "F" : (bitRead(alarmes_passees, ALARME_FLL_ETEINT) ? "f" : "_")) +
                        (bitRead(alarmes_courantes, ALARME_PPS_MANQUANT) ? "P" : (bitRead(alarmes_passees, ALARME_PPS_MANQUANT) ? "p" : "_")) +
                        (bitRead(alarmes_courantes, ALARME_REJET_ECHANT) ? "R" : (bitRead(alarmes_passees, ALARME_REJET_ECHANT) ? "r" : "_")) + 
                        (bitRead(alarmes_courantes, ALARME_10M_ABSENT) ? "O" : (bitRead(alarmes_passees, ALARME_10M_ABSENT) ? "o" : "_")) + 
                        (bitRead(alarmes_courantes, ALARME_GPS_NON_VALIDE) ? "G" : (bitRead(alarmes_passees, ALARME_GPS_NON_VALIDE) ? "g" : "_")) + SEPAR +
                        int_bourre_zeros_str(dac_val) + SEPAR; 
      if (!bitRead(alarmes_courantes, ALARME_PPS_MANQUANT))
      {
        tempstr = tempstr + (cycle_court ? "C" : "") +
                            (cycle_moyen ? "M" : "") +
                            (cycle_long ? "L" : "") + SEPAR +
                            int_bourre_zeros_str(compteur_echantillons) + SEPAR + 
                            int_bourre_zeros_str(nomb_echantillons_par_cycle) + SEPAR + 
                            (delta_compte_osc < 0 ? "-" : " ") + int_bourre_zeros_str(abs(delta_compte_osc)) + SEPAR +
                            (moyenne_decalage_hz < 0 ? "-" : " ") + double_bourre_zeros_str(abs(moyenne_decalage_hz)) + SEPAR + 
                            ((fin_de_cycle && cycle_long) ? ((boucle_PI_decalage_hz < 0 ? "-" : " ") + double_bourre_zeros_str(abs(boucle_PI_decalage_hz))) : "________") + SEPAR + 
                            (fin_de_cycle ? ((delta_dac > 0 ? "-" : " ") + int_bourre_zeros_str(abs(delta_dac))) : "______");                       
      }
      Serial.println(tempstr);                                                      // Afficher la chaîne
    }
  }
}

// Fonction d'affichage LCD des résultats du FLL. Appelée quand il y un PPS de reçu. 
void afficher_resultats_lcd()
{
  lcd.clear();                                            // Effacer l'écran
  lcd.setCursor(0, 0);                                    // positionner le curseur
  char buffer1[5];                                        // Créer des mémoires de caractères temporaires
  char buffer2[5];                                        //         "
  sprintf(buffer1, "%05d", dac_val);                      // Formatter la valeur de DAC sur 5 chiffres
  sprintf(buffer2, "%06d", -delta_dac);                   // Formatter l'écard de DAC sur 6 chiffres
  lcd.print("D:" + String(buffer1) + "  ");               // Afficher le DAC
  lcd.write(byte(0));                                     // afficher le caractère "delta" placé en mémoire du LCD dans la fonction setup
  lcd.print("D:" + String(buffer2));                      // Afficher le DAC
  lcd.setCursor(0, 1);                                    // positionner le curseur
  lcd.print("C:" + String(cycle_court ? "C" : "") + String(cycle_moyen ? "M" : "") + String(cycle_long ? "L" : ""));    // Afficher le cycle courant
  lcd.print("#" + String(compteur_echantillons) + "  ");  // Afficher le compteur de séchantillons
  lcd.write(byte(0));                                     // afficher le caractère "delta" placé en mémoire du LCD dans la fonction setup
  lcd.print("Hz:" + String(moyenne_decalage_hz,4));       // Afficher l'écart en Hz
  lcd.setCursor(0, 2);                                    // positionner le curseur
  lcd.print("AL:");                                       // Afficher les alarmes
  lcd.print(bitRead(alarmes_courantes, ALARME_ACQUISITION_INIT) ? "A" : (bitRead(alarmes_passees, ALARME_ACQUISITION_INIT) ? "a" : "-"));
  lcd.print(bitRead(alarmes_courantes, ALARME_FLL_DEVERROUILLE) ? "V" : (bitRead(alarmes_passees, ALARME_FLL_DEVERROUILLE) ? "v" : "-"));
  lcd.print(bitRead(alarmes_courantes, ALARME_DAC_LIMITE) ? "D" : (bitRead(alarmes_passees, ALARME_DAC_LIMITE) ? "d" : "-"));
  lcd.print(bitRead(alarmes_courantes, ALARME_FLL_ETEINT) ? "F" : (bitRead(alarmes_passees, ALARME_FLL_ETEINT) ? "f" : "-"));
  lcd.print(bitRead(alarmes_courantes, ALARME_PPS_MANQUANT) ? "P" : (bitRead(alarmes_passees, ALARME_PPS_MANQUANT) ? "p" : "-"));
  lcd.print(bitRead(alarmes_courantes, ALARME_REJET_ECHANT) ? "R" : (bitRead(alarmes_passees, ALARME_REJET_ECHANT) ? "r" : "-")); 
  lcd.print(bitRead(alarmes_courantes, ALARME_10M_ABSENT) ? "O" : (bitRead(alarmes_passees, ALARME_10M_ABSENT) ? "o" : "-")); 
  lcd.print(bitRead(alarmes_courantes, ALARME_GPS_NON_VALIDE) ? "G" : (bitRead(alarmes_passees, ALARME_GPS_NON_VALIDE) ? "g" : "-")); 
}

// Fonction permettant de faire un "RESET" de l'exécution du programme sur demande
void(* resetFunc) (void) = 0; 

// Fonction d'initialisation, style Arduino, exécutée une seule fois en lancement du programme
void setup()
{     
  // Réservation se l'espace suffisant pour manipulation des "Strings'  
  hrs_string.reserve(5);       
  mins_string.reserve(5);        
  secs_string.reserve(5);         
  day_string.reserve(5);            
  month_string.reserve(5);      
  year_string.reserve(5);   
  commande.reserve(10);
  param1.reserve(10);
  param2.reserve(10);
  param3.reserve(10);
  chaine_heure.reserve(12);
  chaine_date.reserve(12);
  chaine_caracts_recues.reserve(30);
  chaine_caracts_gps_recues.reserve(100);

  Serial.begin(115200);                               // Le port série pour lire les rapports et résultats
  while (Serial.available() > 0) Serial.read();       // Vider le tampon du port série matériel
  Serial2.begin(9600);                                // Le port série "soft" pour recevoir les données du GPS

  pinMode(BROCHE_PPS, INPUT_PULLUP);                  // Broche PPS en entrée avec pullup
  pinMode(BROCHE_REF_IN, INPUT_PULLUP);               // Broche de la référence 10 MHz en entrée avec pullup
  pinMode(LED_BUILTIN, OUTPUT);                       // Broche de la LED intégrée en sortie
  pinMode(BOUTON_EFFAC_ALM, INPUT_PULLUP);            // Broche du bouton d'effacement des alarmes antérieures

  // Assignation du port SPI #2
  pinMode(BROCHE_SPI_SS, OUTPUT);                     // Broche Slave Select vers le DAC
  digitalWrite(BROCHE_SPI_SS, HIGH);                  // Désactiver le SS du DAC
  SPI.setMOSI(BROCHE_SPI_MOSI);
  SPI.setMISO(BROCHE_SPI_MISO);
  SPI.setSCLK(BROCHE_SPI_SCLK);

  // Récupérer les paramètres du système de la mémoire EEPROM ici.
//  sauv_params_eeprom();
  lire_params_eeprom();                                // Charger les paramètres à partir de la mémoire flash s'ils existent.

  // Calcul du compte nominal d'un échantillon pour une référence d'exactement 10 MHz
  compte_nominal = round((nomb_pps_par_echant * FREQ_OCXO / 65536 - trunc(nomb_pps_par_echant * FREQ_OCXO / 65536)) * 65536); 

  // Calcul des paramètres du système en fonction des composants et de la topologie
  reponse_lsb_par_hz = 1 / double((DAC_TENSION_MAX - DAC_TENSION_MIN) * DAC_POST_GAIN / (1<<nomb_bits_dac) * REPONSE_OCXO_HZ_PAR_V);
  debut_stabilisation_millis = millis();

  // Initialiser le FLL, le DAC et le processus d'acquisition
  fll_actif = OUI;
  redemarrer_acquisition();
  fin_de_cycle  = OUI;
  bitSet(alarmes_courantes,ALARME_ACQUISITION_INIT);  // Activer l'alarme d'acquisition initiale

  // Configuration du DAC
  SPI.begin();                                                    // Préparer le bus SPI.
  delay(100);                                                     // Faire une pause pour permettre au port série de s'ouvrir sur l'ordi

  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE2));// Début d'une écriture du DAC via le port SPI (débit, ordre des bits, mode du bus SPI)
  digitalWrite(BROCHE_SPI_SS, LOW);                               // Abaissement du signal Slave Select du DAC  
  SPI.transfer(4);                                                // Transfert de l'adressse du registre de gain du DAC
  SPI.transfer16(1);                                              // Transfert des 16 bits, gain de 2 sur la référence interne
  digitalWrite(BROCHE_SPI_SS, HIGH);                              // Relèvement du signal Slave Select du DAC  
  SPI.endTransaction();                                           // Fin de l'écriture du DAC, libération du bus.
  dac_val = (1<<(nomb_bits_dac-1));                               // DAC placé à mi-course. 
  Envoi_Val_Dac(dac_val);                                         // Positionner le DAC-A 

  // Redirection des broches PA0 et PA1 vers le compteur TIM2. Uniquement requis avec le STM32F4x1 Black Pill
  #if defined(BLACK_PILL_USED)
    GPIOA->MODER |= 0b00001010;
    GPIOA->AFR[0] |= 0b00010001;
  #endif 
  
  // Configuration du compteur TIM2 du STM32 pour faire la "capture" de la référence 10 MHz
  TIM2->CR1 =   0b0000000000000000;                // TIM2 désactivé au départ
  TIM2->CR2 =   0b0000000000000000;                // TIM2 Canal 1 est l'entrée 
  TIM2->SMCR =  0b0100000001110111;                // ETR non-inversé, External Mode 2 sactivé, Prescaler éteint, pas de filtre. master-slave synchro,  entrée ETR va incrémenter TIM2
  TIM2->DIER =  0b0000000000000100;                // Capture/Compare 2 interrupt activé, 
  TIM2->EGR =   0b0000000000000100;                // Capture/compare 2 generation activé
  TIM2->CCMR1 = 0b0000000100000000;                // Canal 2 en entrée pour le déclenchement PPS, pas de filtre, pas de préselecteur
  TIM2->CCMR2 = 0b0000000000000000;                // Canaux pas utilisés
  TIM2->CCER =  0b0000000000010000;                // Front montant de CC2P, Capture activée
  TIM2->PSC =   0b0000000000000000;                // Prescaler à 1
  TIM2->ARR =   0b1111111111111111;                // Valeur Auto-Reload au maximum.
  TIM2->CR1 =   0b0000000000000001;                // TIM2 activé et incrémente

  RefTimer.attachInterrupt(2,interrupt_TIM2_PPS);     // Associer la fonction d'interruption au compteur TIM2, canal 2

  // Afficher le message de départ au port série
  delay(2000);                                        // Faire une pause pour permettre au port série de s'ouvrir sur l'ordi
  Serial.println(MESSAGE_ACCUEUIL);                   // Message d'accueuil
  Serial.println("Pause de stabilisation...");   

  // Configurer le LCD et afficher le message de départ
  lcd.begin(20, 4);                                   // Définir et initialiser le LCD comme ayant 16 colonnes et 2 lignes
  lcd.createChar(0, delta);
  lcd.setBacklight(255);                              // Allumer le rétro-éclairage
  lcd.home();                                         // positionner le curseur à la première case
  lcd.clear();                                        // Effacer l'écran
  // Affichage du logo d'allumage
  lcd.setCursor(0, 0);                                // positionner le curseur
  lcd.print("Électro-Bidouilleur");                   // Afficher le texte entre apostrophes
  lcd.setCursor(0, 1);                                // positionner le curseur
  lcd.print("Un instant s.v.p.");                     // Afficher le texte entre apostrophes
}

// Boucle principale, style Arduino
void loop()
{
  if (echantillon_pret)                                                                       // Est-ce qu'un échantilon est prêt?                                           
  {                                                                                           // Oui
    echantillon_pret = NON;                                                                   // Ré-initialiser le marqueur de nouvel échantillon prêt 

    // Calcul cumulatif des résultats de l'échantillonnage
    delta_compte_osc = compte_osc - compte_nominal;                                           // Calculer la différence entre le compte de l'échantillon et le compte nominal
    if (donnees_gps_valides && (!cycle_long || (cycle_long && (abs(delta_compte_osc) < seuil_rejet_delta_compte))))  // Rejeter les échantillons non valides ou trop mauvais (déviation subite de GPS) en cycle long.
    {
      ++compteur_echantillons;                                                                // Incrémenter le compteur d'échantillons

      // Calcul de la composante "Proportionnelle" accumulée de la boucle P-I dans tous les types de cycles
      somme_delta_compte_osc += delta_compte_osc;                                             // Accumuler ces différences pour fins de calcul
      moyenne_delta_compte_osc = double(somme_delta_compte_osc) / compteur_echantillons;      // Faire la moyenne de ces différences
      moyenne_decalage_hz = double(moyenne_delta_compte_osc) / nomb_pps_par_echant;           // Exprimer la moyenne des différences en Hz. Est la compasante P de la boucle P-I
      frequence_mesuree_hz = FREQ_OCXO + moyenne_decalage_hz;                                 // Calculée la fréquence mesurée de l'OCXO
 
      // Ici le traitement de la correction de l'OCXO est fait si le nombre d'échantillons dans le cycle est rencontré.
      if (compteur_echantillons >= nomb_echantillons_par_cycle)                               // Est-ce le temps de calculer la correction d'OCXO?
      {                                                                                       // Oui
        fin_de_cycle = OUI;                                                                   // Indiquer la fin du cycle d'échantillonnage
        if (cycle_long)                                                                       // Cycle long? 
        {                                                                                     // Oui
          // Calcul de la composante "Intégrale" de la boucle P-I à la fin du cycle
          // Les moyennes de décalage sont conservées dans un tableau "array". Une moyenne des moyennes est faite en fin de cycle
          moyenne_moyenne_decalage_hz_array = 0;                                              // Initialiser la moyenne des moyennes à zéro.
          // Déplacer d'une position les moyennes existantes dans le tableau des moyennes pour faire de la place pour la nouvelle moyenne
          for (index_moyenne_decalage_hz_array = (sizeof(moyenne_decalage_hz_array)/8 - 2); index_moyenne_decalage_hz_array >= 0; index_moyenne_decalage_hz_array--)
          {
            moyenne_decalage_hz_array[index_moyenne_decalage_hz_array + 1] = moyenne_decalage_hz_array[index_moyenne_decalage_hz_array];
          }
          moyenne_decalage_hz_array[0] = moyenne_decalage_hz;                                 // Sauvegarder la nouvelle moyenne dans le tableau
          // Incrémenter le compteur du nombre de moyennes existantes dans le tableau. Limiter la valeur à la grosseur du tableau.
          if (max_index_moyenne_decalage_hz_array < sizeof(moyenne_decalage_hz_array)/8) max_index_moyenne_decalage_hz_array++;          // 10 items dans le "array" de moyennes
          // Calculer la moyenne des moyennes d'écart
          for (index_moyenne_decalage_hz_array = 0; index_moyenne_decalage_hz_array <= (max_index_moyenne_decalage_hz_array-1); index_moyenne_decalage_hz_array++)
          {
            moyenne_moyenne_decalage_hz_array += moyenne_decalage_hz_array[index_moyenne_decalage_hz_array];    // Faire la somme des moyennes
          }
          // Diviser le résultat par le nombre de moyennes. Résultat: La moyenne des moyennes
          moyenne_moyenne_decalage_hz_array = double(moyenne_moyenne_decalage_hz_array) / max_index_moyenne_decalage_hz_array;  

          // Calcul de la compensaton P-I en Hz. Si pas en cycle long, Proportionnel uniquement.
          boucle_PI_decalage_hz = p_index * moyenne_decalage_hz + (cycle_long * i_index * moyenne_moyenne_decalage_hz_array);  // La somme des influences pondérées P et I
        }
        // Compensaton Proportionnelle uniquement si en cycle autre que long. Composante Intégrale non considérée
        else boucle_PI_decalage_hz = moyenne_decalage_hz;

        // Calcul du changement de valeur du DAC en fonction du décalage calculé
        delta_dac = int(lround(double(boucle_PI_decalage_hz) * reponse_lsb_par_hz));            // La correction sera l'opposé de la valeur du décalage mesurée. 
        if (delta_dac > ((1<<nomb_bits_dac)-1)) delta_dac = ((1<<nomb_bits_dac)-1);           // S'assurer que la correction loge dans 16 bits signés                    
        if (delta_dac < -((1<<nomb_bits_dac)-1)) delta_dac = -((1<<nomb_bits_dac)-1);         //            "

        // Dans le cas où le FLL est actif, calculer une nouvelle valeur du DAC
        if (fll_actif)                                      // FLL actif?
        {                                                   // Oui
          // Calcul de la valeur du DAC
           if (delta_dac > 0)   // moyenne de décalage positive?
          { // Oui, faire une correction négative
            if (delta_dac > dac_val)
            {
              dac_val = 0;
              bitSet(alarmes_courantes, ALARME_DAC_LIMITE);
            }
            else 
            {
              dac_val -= delta_dac;             // Ajouter l'effet du delta_dac à la valeur de dac_val.
              bitClear(alarmes_courantes, ALARME_DAC_LIMITE);
            }
          }
          else if (delta_dac < 0)                           // moyenne de décalage négative
          { // Faire une correction positive
            if (-delta_dac > (((1<<nomb_bits_dac)-1) - dac_val))
            {
              dac_val = ((1<<nomb_bits_dac)-1);                               
              bitSet(alarmes_courantes, ALARME_DAC_LIMITE);
            }
            else 
            {
              dac_val -= delta_dac;             // Ajouter l'effet du delta_dac à la valeur de dac_val. 
              bitClear(alarmes_courantes, ALARME_DAC_LIMITE);
            }
          }

          // Envoi de la valeur aux DAC
          if (delta_dac != 0)
          {
            Envoi_Val_Dac(dac_val);                       // Envoi de la valeur du DAC   
          }
          nouv_dac_val = dac_val;     // Valeur du DAC sur 16 bits calculee pour affichage 
        }
        else  // FLL inactif, la valeur du DAC ne change pas.
        {
          nouv_dac_val = dac_val;
          delta_dac = 0;
        }
      }
    }
    else  // Un échantillon a été rejeté car trop écarté de la tendance stable. Certainement une déviation soudaine du GPS.
    {
      bitSet(alarmes_courantes,ALARME_REJET_ECHANT);                            // Aviser d'un rejet d,échantillon
    }
    // Imprimer les différents résultats de calculs au port série
    afficher_resultats_console();

    // Compléter la préparation pour le prochain cycle d'échantillonnage
    if (fin_de_cycle)
    {
      // Sélection de la longueur du prochain cycle d'échantillonnage en fonction des résultats du cycle complété.
      if (cycle_long && (abs(moyenne_decalage_hz) > seuil_cycle_long))            // Passage du cycle long au cycle moyen?
      {
        nomb_echantillons_par_cycle = duree_cycle_moyen;
        cycle_long = NON;
        cycle_moyen = OUI;
      }
      else if (cycle_moyen && (abs(moyenne_decalage_hz) > seuil_cycle_moyen))     // Passage du cycle moyen au cycle court?
      {
        nomb_echantillons_par_cycle = duree_cycle_court;
        cycle_moyen = NON;
        cycle_court = OUI;
       }
      else if (cycle_moyen && (abs(moyenne_decalage_hz) <= seuil_cycle_long))     // Passage du cycle moyen au cycle long?
      {
        nomb_echantillons_par_cycle = duree_cycle_long;
        cycle_moyen = NON;
        cycle_long = OUI;
        bitClear(alarmes_courantes,ALARME_ACQUISITION_INIT);                      // Effacer l'alarme d'acquisiton initiale
        // Effacer le tableau des moyennes d'écarts
        for (index_moyenne_decalage_hz_array = 0; index_moyenne_decalage_hz_array < sizeof(moyenne_decalage_hz_array)/8; index_moyenne_decalage_hz_array++)
            moyenne_decalage_hz_array[index_moyenne_decalage_hz_array] = 0;
        
        max_index_moyenne_decalage_hz_array = 0;                                  // Ré-initialiser le compte du nombre de moyennes dans le tableau
      }
      else if (cycle_court && (abs(moyenne_decalage_hz) <= seuil_cycle_moyen))    // Passage du cycle court au cycle moyen?
      {
        nomb_echantillons_par_cycle = duree_cycle_moyen;
        cycle_court = NON;
        cycle_moyen = OUI;
      }       

      afficher_resultats_lcd();                           // Afficher les résultats au LCD
      
      // Ré-initialiser les différentes valeurs pour démarrer un neauveau cycle d'échantillonnage.
      compteur_echantillons = 0;                        
      moyenne_delta_compte_osc = 0;                     
      somme_delta_compte_osc = 0;
      debut_stabilisation_millis = millis();
    }
    else afficher_resultats_lcd();                        // Pas en fin de cycle: Afficher les résultats au LCD
  }

  // Traitement des pulsations PPS (partie non-critique)
  if (detection_pps)                                      // PPS détecté par la fonction d'interruption?
  {   // Oui
      detection_pps = NON;                                // Ré-initialiser le marqueur de PPS reçu

      // Récupération après une absence de PPS
      if (bitRead(alarmes_courantes,ALARME_PPS_MANQUANT)) // Absence de PPS?
      {  // Oui
        bitClear(alarmes_courantes, ALARME_PPS_MANQUANT); // Effacer l'alarme de PPS manquant
        redemarrer_acquisition();                         // Redémarrer un nouveau cylce d'acquision
      }
      digitalWrite(LED_BUILTIN, LOW);                     // Allumer la LED pour indiquer la réception d'un PPS
      debut_led_millis = millis();                        // Début du cycle d'extinction de la LED

      // Afficher le caractère clignotant indiquant la réception d'un PPS, ainsi que l'heure du PPS
      lcd.setCursor(19, 3);                               // positionner le curseur
      lcd.print('P');                                     // Afficher le caractère de PPS
      lcd.setCursor(0, 3);                                // positionner le curseur
      lcd.print(chaine_date + "  " + chaine_heure);       // Afficher l'heure et la date

      // Gestion de l'affichage du compteur des secondes durant l'échantillonnage en mode verbose
      if ((!fin_de_cycle) && mode_verbose && (!mode_rx_commande_uart))
      {
        Serial.print("_");                                // Afficher le numéro de l'impulsion PPS durant l'échantillonnage
        Serial.print(compteur_captures);                  //            "       
      }

      debut_detection_pps_millis = millis();              // Ré-initialiser le compte du délai de détection de PPS
  }

  // Pas de pulsations PPS de recue; on complète les travaux non critiques.
  else  
  {  
    // Gestion des alarmes
    cycle_long ? bitClear(alarmes_courantes,ALARME_FLL_DEVERROUILLE) : bitSet(alarmes_courantes,ALARME_FLL_DEVERROUILLE);   // 
    if (!digitalRead(BOUTON_EFFAC_ALM)) alarmes_passees = 0;
    alarmes_passees = alarmes_passees | alarmes_courantes;
    bitClear(alarmes_courantes, ALARME_REJET_ECHANT);     // Effacer l'alarme de rejet d'échantillon pour être prêt pour l'échantillon suivant

  
    // Détection d'absence de PPS
    if (millis() - debut_detection_pps_millis > 1010)               // Délai de PPS dépasse une seconde?
    {                                                               // Oui
      debut_detection_pps_millis = millis();                        // Ré-initialiser le compte du délai de PPS
      bitSet(alarmes_courantes, ALARME_PPS_MANQUANT);               // Activer l'alarme de PPS manquant
      afficher_resultats_console();                                 // Afficherles résultats sur le port série
    }
    
    // Extinction de la LED après un court délai
    if (millis() - debut_led_millis >= 50)                          // Le temps d'éteindre la LED du STM32 Blue Pill?
    {
      digitalWrite(LED_BUILTIN,HIGH);                               // Oui, éteindre la LED
    }

    // Extinction du marqueur clignotant sur l'affichage LCD        
    if (millis() - debut_led_millis >= 400)                         // Le temps d'effacer l'indicateur de PPS sur l'affichage LCD?
    {
      lcd.setCursor(19, 3);                                         // Oui, positionner le curseur
      lcd.print(" ");                                               // Afficher un espace vide par dessus l'indicateur
    }

    // Fin d'une période de stabilisation  de 5 secondes en fin de cycle d'échantillonnage
    if (fin_de_cycle && (millis() - debut_stabilisation_millis >= 5000))  // // Le temps de terminer la période de stabilisation de l'OCXO
    {
      fin_de_cycle = NON;                                           // Effacer le marqueur de fin de cycle
      reinitialiser_compteur = OUI;                                 // Démarrer un nouveau cycle 
    }

    // Gestion de la chaîne de caractères recue du GPS pour extraire la date et l'heure UTC
    while (Serial2.available() > 0)                                         // Boucler tant qu'il y a des caractères de déjà reçus du GPS
    {
      caract_recu = Serial2.read();                                         // Lire un caractère
      chaine_caracts_gps_recues = chaine_caracts_gps_recues + caract_recu;  // L'ajouter à la chaîne de caractères déjà reçues    
      if (caract_recu == '\n')                                              // Si le caractère est un saut de ligne, traiter la chaîne
      {   // Oui                                                       
        // Le format attendu de la chaîne est:  $GPRMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,ddmmyy,x.x,a*hh
        if (chaine_caracts_gps_recues.substring(0, 6) == GPRMC_PREAMBLE)    // Comparer les 6 premiers caractères de la chaîne avec le preambule GPS désiré...
        {
          if ((chaine_caracts_gps_recues.substring(7,8) == ",") || (chaine_caracts_gps_recues.substring(17,18) != "A"))     // ...et vérifier que la chaîne n'est pas vide ou invalide        
          {   // Données invalides
            bitSet(alarmes_courantes, ALARME_GPS_NON_VALIDE);               // Activer l'alarme de données GPS invalides
            donnees_gps_valides = NON;                                      // Abaisser le marqueur de données valides
            chaine_date = "--/--/--";                                       // Ne pas afficher la date et l'heure
            chaine_heure = "--:--:--";                                      //          "
          }
          else  // Sinon, les données GPS sont valides. Traitrer la chaîne de caractères
          {                                                                 
            donnees_gps_valides = OUI;                                      // Aviser que les données GPS sont valides
            bitClear(alarmes_courantes, ALARME_GPS_NON_VALIDE);             // Effacer l'alarme de données GPS non valides  
            // Extraction de l'heure
            hrs = chaine_caracts_gps_recues.substring(7,9).toInt();         // Extraire la valeur numérique de l'heure
            mins = chaine_caracts_gps_recues.substring(9,11).toInt();       // Extraire la valeur numérique de la minute
            secs = chaine_caracts_gps_recues.substring(11,13).toInt();      // Extraire la valeur numérique de la seconde
            if (++secs == 60)                                               // AJOUTER UNE SECONDE puisque la phrase GPS arrive après l'impulsion PPS. Ensuite traiter le dépassement des secondes?
            {                                                               // Oui
              secs = 0;                                                     // Ré-initialiser les secondes
              if (++mins == 60)                                             // Ensuite traiter le dépassement des minutes?
              {                                                             // Oui
                mins = 0;                                                   // Ré-initialiser les minutes
                if (++hrs == 24)                                            // Ensuite traiter le dépassement des heures?
                {                                                           // Oui
                  hrs = 0;                                                  // Ré-initialiser les heures
                }
              }
            }
            hrs_string = String(hrs);                                       // Convertir la valeur numérique des heures en chaîne de caractères
            if (hrs < 10) hrs_string = "0" + hrs_string;                    // Ajouter un zéro en avant si nécessaire
            mins_string = String(mins);                                     // Convertir la valeur numérique des minutes en chaîne de caractères
            if (mins < 10) mins_string = "0" + mins_string;                 // Ajouter un zéro en avant si nécessaire
            secs_string = String(secs);                                     // Convertir la valeur numérique des secondes en chaîne de caractères
            if (secs < 10) secs_string = "0" + secs_string;                 // Ajouter un zéro en avant si nécessaire
            chaine_heure = hrs_string + ":" + mins_string + ":" + secs_string;  // Créer la chaîne de caractères de l'heure à afficher
  
            //  Extraction de la date
            unsigned short int b = 0;                                       // utilisé comme compteur de virgules
            unsigned short int c = 0;                                       // utilisé comme compteur de caractères
            leap_year = false;                                              // Initialiser le marqueur d'année bissextile comme faux.
            do                                                              // Repérer le champ de la date dans la chaîne GPS reçue.
            { 
              if (chaine_caracts_gps_recues[c++] == ',') b++;               // Incrémenter le compteur de virgule si une d'entre elle est détectée
            } 
            while (b < 9);                                                      // Boucler si nécessaire car la date est localisée après la 9ème virgule
            day_val = chaine_caracts_gps_recues.substring(c,c+2).toInt();       // Extraire la chaîne de caractères représentant le jour
            month_val = chaine_caracts_gps_recues.substring(c+2,c+4).toInt();   // Extraire la chaîne de caractères représentant le mois
            year_val = chaine_caracts_gps_recues.substring(c+4,c+6).toInt();    // Extraire la chaîne de caractères représentant l'année
            if (((year_val%4 == 0) && (year_val%100 != 0)) || (year_val%400 == 0)) leap_year = true;  // Vérifier si l'année courante est bissextile?
            else leap_year = false;
            if ((secs == 0) && (mins == 0) && (hrs == 0))                       // Traiter le changement de jour (date)?
            {                                                                   // Oui
              day_val++;                                                        // Incrémenter la valeur du jour
              if ((day_val > (28)) && !((leap_year) && (month_val == 2) && (day_val == 29))) // Traiter le changement de mois? (prenant en compte les années bissextiles)
              {                                                                 // Oui
                month_val++;                                                    // Incrémenter la valeur du mois
                day_val = 1;                                                    // Réinitialiser le jour à 1
              }
              if (month_val > 12)                                               // Traiter le changement d'année?
              {                                                                 // Oui
                year_val++;                                                     // Incrémenter la valeur de l'année
                month_val = 1;                                                  // Réinitialiser le mois à 1
                day_val = 1;                                                    // Réinitialiser le jour à 1
              }
            }
            day_string = String(day_val);                                       // Convertir la valeur numérique du jour en chaîne de caractères
            if (day_val < 10) day_string = "0" + day_string;                    // Ajouter un zéro en avant si nécessaire
            month_string = String(month_val);                                   // Convertir la valeur numérique du mois en chaîne de caractères
            if (month_val < 10) month_string = "0" + month_string;              // Ajouter un zéro en avant si nécessaire
            year_string = String(year_val);                                     // Convertir la valeur numérique de l'année en chaîne de caractères        
            chaine_date = day_string + "/" + month_string + "/" + year_string;  // Créer la chaîne de caractères de la date à afficher
          }
        }
        else 
        {
          bitSet(alarmes_courantes, ALARME_GPS_NON_VALIDE);               // Activer l'alarme de données GPS invalides
          donnees_gps_valides = NON;                                      // Abaisser le marqueur de données valides
          chaine_date = "--/--/--";                                       // Ne pas afficher la date et l'heure
          chaine_heure = "--:--:--";                                      //          "
        }
        chaine_caracts_gps_recues = "";                                         // Après réception du caractère de fin de phrase et traitement de la chaîne, effacer la chaîne de caractères reçus
      }
    } 

    // Gestion d'une commande reçue sur le port Série
    if (Serial.available() > 0)                                                 // Caractère(s) reçu(s) sur le port série?
    {
      mode_rx_commande_uart = OUI;                                              // Enclencher le mode de réception UART d'une commande
      debut_rx_commande_millis = millis();                                      // Marquer le début du mode re réception de commande
      caract_recu = Serial.read();                                              // Lire un caractère du UART
      if (chaine_caracts_recues == "") Serial.println();                        // Au début d'uen réception de commande? Si oui, insérer une ligne vide
      Serial.print(caract_recu);                                                // Imprimer le caractère reçu
      if ((caract_recu == 8) || (caract_recu == 127))  chaine_caracts_recues.remove(chaine_caracts_recues.length()-1);          // Traitement de Backspace ou Delete: effacement du caractère
      else if ((caract_recu != '\n') && (caract_recu != '\r')) chaine_caracts_recues += caract_recu;  // Pas la fin d'une commande, ajouter le caractère à la chaîne.
      else                                                                                            // réception d'une commande compète détectée
      {
        // Séparation de la commande et des paramètres reçus
        index1 = chaine_caracts_recues.indexOf(' ');  // finds location of first ,
        commande = chaine_caracts_recues.substring(0, index1);   // captures first data String
        commande.toUpperCase();
        index2 = chaine_caracts_recues.indexOf(' ', index1+1 );   // finds location of second ,
        param1 = chaine_caracts_recues.substring(index1+1, index2);   // captures second data String
        param1.toUpperCase();
        index3 = chaine_caracts_recues.indexOf(' ', index2+1 );
        param2 = chaine_caracts_recues.substring(index2+1, index3);
        param2.toUpperCase();
        index4 = chaine_caracts_recues.indexOf(' ', index3+1 );
        param3 = chaine_caracts_recues.substring(index3+1, index4);
        param3.toUpperCase();

        // Traitement de la commande reçue
        if (commande == "REDEM")                              // Commande pour redémarrer le programme du microcontrôleur
        { 
          Serial.println("\nOK");
          NVIC_SystemReset();
        }
        else if (commande == "REACQ")                         // Commande pour redémarrer l'acquisition du FLL
        { 
          Serial.println("\nOK");
          redemarrer_acquisition();
        } 
        else if (commande == "EFFALM")                        // Commande pour effacer le registre des alarmes antérieures
        { 
          Serial.println("\nOK");
          alarmes_passees = 0;
        }
        else if (commande == "FLL")                           // Commande pour activer ou désactiver le FLL
        {
          if (param1 == "OUI") 
          {
            fll_actif = OUI;
            bitClear(alarmes_courantes, ALARME_FLL_ETEINT);
            redemarrer_acquisition();
            Serial.println("\nOK");
          }
          else if (param1 == "NON")
          {
            fll_actif = NON;     
            bitSet(alarmes_courantes, ALARME_FLL_ETEINT);
            Serial.println("\nOK"); 
          }
          else Serial.println("\nERREUR: paramètre non valide");               // Sinon, paramètre non reconnu.
        }
        else if (commande == "DAC")                                             // Commande pour forcer une valeur du DAC du FLL
        {
          if ((param1.toInt() >= 0) && (param1.toInt() < (1<<nomb_bits_dac)))  // Valider la valeur entre 0 et 65535
          {
            dac_val = param1.toInt();
            Envoi_Val_Dac(dac_val);                                         // Envoi de la valeur du DAC
            redemarrer_acquisition();
            Serial.println("\nOK");
          }
          else Serial.println("\nERREUR: paramètre non valide");
        }
        else if (commande == "DACBIT")                                             // Commande pour forcer une valeur du DAC du FLL
        {
          if ((param1.toInt() == 16) || (param1.toInt() == 14) || (param1.toInt() == 12))  // Valider la valeur entre 0 et 65535
          {
            nomb_bits_dac = param1.toInt();
            sauv_params_eeprom();                 
            redemarrer_acquisition();
            Serial.println("\nOK");
          }
          else Serial.println("\nERREUR: paramètre non valide");
        }
        else if (commande == "DURCYC")                                          // Commande pour changer la durée des cycles court, moyen et long (dans l'ordre)
        {
          if ((param1.toInt() >= 1) && (param1.toInt() < 65536))                // Validation de la valeur
          { 
            if ((param2.toInt() >= 1) && (param2.toInt() < 65536)) 
            {
              if ((param3.toInt() >= 1) && (param3.toInt() < 65536)) 
              {   // Acceptation des paramètres seulement si les trois valeurs sont validées
                duree_cycle_court = param1.toInt();                             // Le nombre d'échantillons d'un cycle court
                duree_cycle_moyen = param2.toInt();                             // Le nombre d'échantillons d'un cycle moyen
                duree_cycle_long = param3.toInt();                              // Le nombre d'échantillons d'un cycle long
                redemarrer_acquisition();
                Serial.println("\nOK");
                sauv_params_eeprom();                 
              }
              else Serial.println("\nERREUR: paramètres non valides");
            }
            else Serial.println("\nERREUR: paramètres non valides");
          }  
          else Serial.println("\nERREUR: paramètres non valides");
        }
        else if (commande == "NPPS")                                            // Commande pour changer le nombre d'impulsions PPS dans un échantillon
        {
          if ((param1.toInt() >= 1) && (param1.toInt() < 65536)) 
          {
            nomb_pps_par_echant = param1.toInt();
                                           
            // Calcul du compte nominal d'un échantillon pour une référence d'exactement 10 MHz, puisque ce paramèetre change la valeur résiduelle dans le compteur
            compte_nominal = round((nomb_pps_par_echant * FREQ_OCXO / 65536 - trunc(nomb_pps_par_echant * FREQ_OCXO / 65536)) * 65536); 
            sauv_params_eeprom();
            Serial.println("\nOK");
          }
          else Serial.println("\nERREUR: paramètre non valides");
        }
        else if (commande == "SEUIL")                                           // Commande pour changer les seuils d'écarts forçant le changement dy type de cycle
        {
          if ((param1.toFloat() >= 0) && (param1.toFloat() < 100))              // Seuil moyen-court 
          {
            if ((param2.toFloat() >= 0) && (param2.toFloat() < 100))            // Seuil long-moyen
            {
              seuil_cycle_moyen = param1.toFloat();   // 
              seuil_cycle_long = param2.toFloat();   // 
              redemarrer_acquisition();
              Serial.println("\nOK");
              sauv_params_eeprom();
            }
            else Serial.println("\nERREUR: paramètres non valides");
          }         
          else Serial.println("\nERREUR: paramètres non valides");
        }
        else if (commande == "VERBOS")                                          // Commande activant le mode verbosité sur le port série UART
        {
          if (param1 == "OUI") 
          {
            mode_verbose = OUI;
            Serial.println("\nOK");
            sauv_params_eeprom();
          }
          else if (param1 == "NON") 
          {
            mode_verbose = NON;      
            Serial.println("\nOK");
            sauv_params_eeprom();    
          }
          else Serial.println("\nERREUR: paramètre non valide");
        }
        else if (commande == "VT100")                                           // Commande activant l'affichage de type VT-100 en mode verbosité sur le port série UART
        {
          if (param1 == "OUI") 
          {
            mode_verbose_vt100 = OUI;
            Serial.println("\nOK");
            sauv_params_eeprom();
          }
          else if (param1 == "NON") 
          {
            mode_verbose_vt100 = NON;      
            Serial.println("\nOK");
            sauv_params_eeprom();
          }
          else Serial.println("\nERREUR: paramètre non valides");
        }        
        else if (commande == "PARAM")                                           // Commande affichant les valeurs des paramètres ajustables du système
        {
          Serial.println(LIGNE_SEPAR);          
          Serial.println("Valeur des Paramètres");
          Serial.println("---------------------");
          Serial.println("Nombre de bits de résolution du DAC:  " + String(nomb_bits_dac));                     
          Serial.println("Valeur du DAC:                        " + String(dac_val));                     
          Serial.println("Tension Vtune à l'OCXO (calcul):      " + String((float(dac_val)/(1<<nomb_bits_dac)) * (DAC_TENSION_MAX-DAC_TENSION_MIN) * DAC_POST_GAIN,5));                           
          Serial.println("FLL en fonction:                      " + String(fll_actif ? "OUI" : "NON")); 
          Serial.println("Nombre de PPS par échantillon:        " + String(nomb_pps_par_echant));                   
          Serial.println("Durée du cycle court:                 " + String(duree_cycle_court));
          Serial.println("Durée du cycle moyen:                 " + String(duree_cycle_moyen));
          Serial.println("Durée du cycle long:                  " + String(duree_cycle_long));
          Serial.println("Seuil de passage au cycle moyen (Hz): " + String(seuil_cycle_moyen,6));
          Serial.println("Seuil de passage au cycle long (Hz):  " + String(seuil_cycle_long,6));
          Serial.println("Mode d'affichage détaillé:            " + String(mode_verbose ? "OUI" : "NON"));
          Serial.println("Mode d'affichage détaillé VT100:      " + String(mode_verbose_vt100 ? "OUI" : "NON"));                
          Serial.println("Boucle PI  Index Kp:                  " + String(p_index,2));                
          Serial.println("Boucle PI  Index Ki:                  " + String(i_index,2));                
          Serial.println(LIGNE_SEPAR);          
        }
        else if (commande == "PI")                                             // Commande permettant de changer les constantes Kp et Ki de la bouble PI
        {
          if ((param1.toFloat() >= 0) && (param1.toFloat() <= 1)) 
          {
            if ((param2.toFloat() >= 0) && (param2.toFloat() <= 1) && (param2.toFloat() + param2.toFloat() <= 1)) 
            {
              p_index = param1.toFloat();   // 
              i_index = param2.toFloat();   // 
              redemarrer_acquisition();
              sauv_params_eeprom();
              Serial.println("\nOK");
            }
            else Serial.println("\nERREUR: paramètres non valides. Limites: Kp, Ki entre 0 et 1   Kp + Ki <= 1");
          }         
          else Serial.println("\nERREUR: parametrès non valides");
        }
        else if ((commande == "HELP") || (commande == "AIDE") || (commande == "?"))   // Commande affichant une aide aux commandes et aux alarmes
        {
          Serial.println(LIGNE_SEPAR);          
          Serial.println("Commandes:");
          Serial.println("----------");
          Serial.println("AIDE ou HELP ou ?                    : Cette aide."); 
          Serial.println("DEFIN                                : Aide pour les champs du mode d'affichage condensé.");
          Serial.println("DAC <0-65535>                        : Nouvelle valeur forcée du DAC.");
          Serial.println("DACBIT <16/14/12>                    : Le nombre de bits du DACx0501 utilisé.");
          Serial.println("DURCYC <1-65535> <1-65535> <1-65535> : Nombre d'échantillons par cycles court/moyen/long.");
          Serial.println("EFFALM                               : Efface les alarmes antérieures montrées en minuscules.");
          Serial.println("FLL <OUI/NON>                        : Mise en fonction du FLL.");
          Serial.println("NPPS <1-10000>                       : Nombre de PPS par échantillon.");                   
          Serial.println("PARAM                                : Liste des paramètres ajustables et leur valeur courante.");
          Serial.println("PI <0-1.0000> <0-1.0000>             : Constantes Kp et Ki de la boucle P-I du FLL.");
          Serial.println("REDEM                                : Redémarre le microcontroleur.");
          Serial.println("REACQ                                : Redémarre le processus d'acquisition.");
          Serial.println("SEUIL <0-100.0000> <0-100.0000>      : Seuils (en Hz) pour le passage aux cycles moyen et long.");
          Serial.println("VERBOS <OUI/NON>                     : Mode d'affichage détaillé.");
          Serial.println("VT100 <OUI/NON>                      : Option VT100 du mode d'affichage détaillé.");
          Serial.println("\nAlarmes:");
          Serial.println("--------");
          Serial.println("Les majuscules indiquent des alarmes présentement actives.");
          Serial.println("Les minuscules indiquent des alarmes antérieures. Effaçables avec la commande EFFALM.");
          Serial.println("A / a                                : En acquisition initiale après démarrage.");
          Serial.println("D / d                                : Le DAC a atteint sa valeur minimale ou maximale.");
          Serial.println("F / f                                : Le FLL est éteint.");
          Serial.println("P / p                                : Le signal PPS du GPS est manquant.");
          Serial.println("R / r                                : En cycle long, l'échantillon est rejeté car trop éloigné.");
          Serial.println("V / v                                : Le FLL est déverrouillé car pas en cycle long.");
          Serial.println("O / o                                : L'OCXO ne fournit pas de référence 10 MHz");          
          Serial.println("G / g                                : Le GPS fournit des données non valides.");
          Serial.println(MESSAGE_ACCUEUIL);          
          Serial.println(LIGNE_SEPAR);          
        }
        else if (commande == "DEFIN")                                                 // Commande affichant une aide aux champs du mode condensé (non verbose)
        {
          Serial.println("S|       a         |   b    |  c  |d| e   |  f  |  g   |    h   |    i   |  j   |\n");
          Serial.println("a: Date et heure UTC tirée du GPS.");
          Serial.println("b: Champ des alarmes.");
          Serial.println("c: Valeur courante du DAC (de 0 a 65535) contrôlant la fréquence de l'OCXO.");
          Serial.println("d: Type de cycle courant du FFL (C=Court, M=Moyen, L=Long).");
          Serial.println("e: Numéro de l'échantillon dans le cycle d'échantillonnage courant.");
          Serial.println("f: Nombre total d'échantillons à collecter dans le cycle courant.");
          Serial.println("g: Écart moyen en pulsations du compteur par rapport à la valeur nominale.");
          Serial.println("h: Écart moyen en pulsations du compteur (g) exprimé en Hertz.");
          Serial.println("i: Écart moyen en fréquence de la boucle P-I dans un cycle long.");
          Serial.println("j: Changement de DAC appliqué à la fin du cycle courant.");
          Serial.println(LIGNE_SEPAR);          
        }       
        else Serial.println("\nERREUR: commande non reconnue");                       // Sinon, aucune commande reconnue
        chaine_caracts_recues = "";
        mode_rx_commande_uart = NON;
        while (Serial.available()) Serial.read();
      }
    }

    // Fin d'une période de réception de commande sur le port UART
    if (mode_rx_commande_uart && ((millis() - debut_rx_commande_millis) >= 10000))    // Temps du mode commande du UART expiré?
    {
      Serial.println("\r\nCommande Annulée");                                         // Oui, aviser sur la console
      mode_rx_commande_uart = NON;                                                    // Désactiver le mode commande
    }
  }
}
// Fin du code source
