/* Programme pc_rm1 : régleur de montre - chronocomparateur Auteur : P. Chour - 2018-12-31 - V1.3 Matériel : carte Arduino pro micro, afficheur I2C 2 lignes 16 caractères, 2 LEDS, une entrée "signal", un contacteur rotatif avec poussoir Différences entre V1.0 et V1.1 - amélioration de la gestion du bouton poussoir - correction bug sur signe avance/retard par heure Différences entre V1.1 et V1.2 - passage en 64 bits - test durée d'affichage (53ms pour deux lignes). Alternance de l'affichage pour diminuer cette durée. - amélioration du comptage de la durée à long terme - Affichage avance-retard sur 24 heures Différences entre V1.1 et V1.2 - Ajout battement 19800, 25200 */ #include <limits.h> #include <PinChangeInterrupt.h> #include <PinChangeInterruptBoards.h> #include <PinChangeInterruptPins.h> #include <PinChangeInterruptSettings.h> #include <EEPROM.h> #include <Wire.h> #include <LiquidCrystal_I2C.h> #include <TimerThree.h> typedef struct TCompteur TCompteur; // // Constantes, define et variables pour la mesure. // #define MaxMesures 20 // Taille des buffer circulaires pour le calcul des durées struct TCompteur { int IndexMesure = 0; // Index du buffer circulaire long NbCumuls = 0; // nb de Cumuls pour le buffer courant (IndexMesure) uint64_t Mesure[MaxMesures]; // Buffer circulaire pour conserver les durées des Tic et des Tac long MesureLongTerme; // Pour les mesures à long terme des écarts des durées de Tic et de Tac, valeur signée }; TCompteur CompteurTic; TCompteur CompteurTac; struct TCompteur* PCompteur; uint64_t DureeLongTerme; // Pour mesure précise des périodes. Limité à 1 heure. En µS. uint64_t NbDureeLongTerme; // Nombre de mesures DureeLongTerme #define MaxBattements 7 // Nombre de valeurs dans Battements[] (et idem pour Periode, demi periode et Aberrant) // Les constantes qui suivent dépendent de Battements. Elles sont précalculées pour éviter de perdre du temps lors des traitements. // Si on souhaite ajouter un nouveau battement, on l'ajoute dans le tableau Battement à la position i // On calcule la période correspondante que l'on ajoute dans le tableau Periode à la position i // On calcule la demi période que l'on ajoute dans le tableau DemiPeriode à la position i // On calcule une valeur de durée de demi période que l'on considère comme aberrante. Pour ma part, c'est en général 1/4 de période. On l'ajoute dans // le tableau Aberrant à la position i // On met à jour la valeur MaxBattements qui correspond aux nombres de battements que l'on a défini. const uint64_t Battements[MaxBattements] = {18000, 19800, 21600, 25200, 28800, 36000, 3600}; // en battements par heures const uint64_t Periode[MaxBattements] = {400000, 363636, 333333, 285714, 250000, 200000, 1000000}; // periode en µS const uint64_t DemiPeriode[MaxBattements] = {200000, 181818, 166666, 142857, 125000, 100000, 500000}; // demi période en µS const uint64_t Aberrant[MaxBattements] = {100000, 90909, 83333, 71428, 62500, 50000, 500000}; // valeur de durée aberrantes en dessous de laquelle on ne prend pas en compte la mesure (1/4 de période en général) en µS int IndexBattements = 0; // Valeur du battement courant dans le tableau Battements (et Periode et Aberrant). uint64_t TempsTicTac; // Durée d'un Tic ou d'un Tac boolean ITTic = false; // Vrai si un Tic ou un Tac a été détecté (génération interruption). Doit être remis à faux par l'utilisateur // Pin du processeur et usage // #define SignalEntree 9 // PIN Signal d'entrée de la mesure #define LED_tic 4 // PIN LED rouge (Tic) #define LED_tac 5 // PIN LED verte (Tac) #define poussoir 8 // PIN bouton poussoir #define droite 7 // PIN contact "droite" du contacteur #define gauche 6 // PIN contact "gauche du contacteur // Etats de l'automate // #define E_selection 0 #define E_mesure 1 #define E_selection_trans 2 #define E_mesure_trans 3 #define E_pause 4 #define E_pause_in_trans 5 #define E_pause_out_trans 6 #define E_mesure_redemarre_trans 7 #define E_selection_change_trans 8 #define E_calcule 9 byte EtatAutomate = E_selection_trans; // Etat courant de l'automate // gestion du contacteur rotatif // #define AntiRebondPoussoir 50 // durée en millisecondes de l'anti rebond unsigned long TimerPoussoir = 0; // Timer pour antirebond #define DureeDouble 700 // Durée max d'un double appui en ms unsigned long TimerDouble = 0; // Timer pour double appui boolean Gauche = false; // Contacteur va à gauche boolean Droite = false; // contacteur va à droite byte GaucheSeq = 0; // séquence courante de lecture du contacteur rotatif byte DroiteSeq = 0; boolean GauchePrec = false; // Ancienne valeur du contacteur rotatif boolean DroitePrec = false; #define AntiRebondRotation 5 // durée en millisecondes de l'anti rebond unsigned long TimerRotation = 0; // timer pour anti rebond de la rotation LiquidCrystal_I2C lcd(0x3f, 16, 2); // Initialisation de l'afficheur : adresse 0x27, 16 caractères, 2 lignes /* Initialisation du programme */ void setup() { int i; lcd.init(); // Initialisation de l'afficheur lcd.clear(); lcd.print("Initialisation"); // Un petit texte pour faire patienter lcd.setCursor(0, 1); lcd.print("P. Chour V1.3"); // C'est moi, c'est moi, c'est moi ! pinMode(poussoir, INPUT); // La pin où est connecté le bouton poussoir en entrée. pinMode(droite, INPUT); // La pin où est connecté le contact "droite" pinMode(gauche, INPUT); // La pin où est connecté le contact "gauche" digitalWrite(poussoir, HIGH); // active résistance pull up digitalWrite(droite, HIGH); // active résistance pull up digitalWrite(gauche, HIGH); // active résistance pull down pinMode(SignalEntree, INPUT); // La pin où est connecté le signal à mesurer digitalWrite(SignalEntree, LOW); // active résistance pull up pinMode(LED_tic, OUTPUT); // La pin où est connecté la LED rouge pinMode(LED_tac, OUTPUT); // La pin où est connecté la LED verte for (i = 0; i < 5; i++) { lcd.noBacklight(); // Allumage rétro éclairage afficheur digitalWrite(LED_tic, HIGH); digitalWrite(LED_tac, LOW); delay(200); lcd.backlight(); // Allumage rétro éclairage afficheur digitalWrite(LED_tic, LOW); digitalWrite(LED_tac, HIGH); delay(200); } digitalWrite(LED_tac, LOW); attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(SignalEntree), TicTacIT, RISING); // Signal d'entrée sous interruption. enablePinChangeInterrupt(digitalPinToPinChangeInterrupt(SignalEntree)); // On active l'interruption du signal d'entrée attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(poussoir), PoussoirIT, FALLING); // Signal poussoir sous interruption. enablePinChangeInterrupt(digitalPinToPinChangeInterrupt(poussoir)); // On active l'interruption du poussoir sei(); } /* Procédure sous interruption Déclenchée si signal en entrée (tic-tac) */ void TicTacIT() { TempsTicTac = micros(); // Sauve la valeur du compteur courant ITTic = true; // Indique qu'un Tic ou un Tac a été détecté } /* Procédure sous interruption Déclenchée lorsque l'on appuie sur le poussoir */ void PoussoirIT() { unsigned long courante; courante = millis(); // valeur courante de l'horloge systeme if (DureeVrai(TimerPoussoir, courante) >= AntiRebondPoussoir) { // Prend-on en compte l'impulsion (antireboond) ? TimerPoussoir = courante; switch (EtatAutomate) { case E_mesure: // On est dans l'état E_mesure. On passe en pause EtatAutomate = E_pause_in_trans; TimerDouble = courante; break; case E_pause: EtatAutomate = E_pause_out_trans; TimerDouble = courante; break; case E_pause_in_trans: // On est en transition vers la pause : si deux appui rapide, on passe en sélection case E_pause_out_trans: if (DureeVrai(TimerDouble, courante) <= DureeDouble) { EtatAutomate = E_selection_trans; } break; case E_selection: // On est dans l'état E_selection. On passe dans l'état E_mesure EtatAutomate = E_mesure_trans; break; default: // On peut être dans l'exécution d'une transition. Dans ce cas, on ne fait rien break; } } } // // La fonction ci-dessous mesure le temps en prenant en compte un éventuel bouclage de la fonction millis ou micro // On lui donne la valeur de départ de la valeur à mesurer et la valeur courante de millis ou micro. Elle rend la durée écoulée en millisecondes ou microseconde. // DureeVrai = courante-dernière // unsigned long DureeVrai(unsigned long derniere, unsigned long courante) { if (courante < derniere) { // on est passé par zéro return (0xffffffffu - derniere) + courante; } else { return courante - derniere; } } // La fonction ci-dessous convertit un nombre de 64 bits en string // En entrée : N = nombre de 64 bits // En sortie : string contenant le nombre String Uint64ToString(uint64_t N) { String S; if (N <= ULONG_MAX) { return String((unsigned long)N); } else { S = ""; while (N > ULONG_MAX) { S = String((unsigned long)(N%10))+S; N = N / 10; } if (N > 0) { S = String(((unsigned long)N)) + S; } if (S == "") {S = "0";} return S; } } // // La fonction qui suit teste le contacteur rotatif et détermine s'il y a eu // rotation ou pas. Elle fonctionne par test d'état (donc pas très performante). // Positionne les variables d'état Gauche, Droite // La fonction rend vrai si il y a eu rotation et que le sens a pu être déterminé // void TestRotation() { unsigned long courante; courante = millis(); // valeur courante de l'horloge systeme // Lit les signaux présents sur les pins droite et gauche du contacteur boolean GaucheVal = digitalRead(gauche); boolean DroiteVal = digitalRead(droite); if ((GauchePrec != GaucheVal) || (DroitePrec != DroiteVal)) { // il y a eu changement d'état if (DureeVrai(TimerRotation, courante) >= AntiRebondRotation) { // Prend-on en compte l'impulsion (antirebond) ? TimerRotation = courante; GaucheSeq <<= 1; GaucheSeq |= GaucheVal; GaucheSeq &= 0b00001111; GauchePrec = GaucheVal; DroiteSeq <<= 1; DroiteSeq |= DroiteVal; DroiteSeq &= 0b00001111; DroitePrec = DroiteVal; // Mask the MSB four bits // Compare the recorded sequence with the expected sequence if ((GaucheSeq == 0b00001001) && (DroiteSeq == 0b00000011)) { Gauche = true; } if ((GaucheSeq == 0b00000011) && (DroiteSeq == 0b00001001)) { Droite = true; } if (Droite || Gauche) {EtatAutomate = E_selection_change_trans;} } } } // // Effectue une différence entre deux valeurs (V1 et V2) non signées et formatte le résultat pour affichage // V1 = valeur en µS // V2 = valeur en µS // L = longueur // Signe = boolean si un signe doit être mis. Si V1>V2, signe +. Sinon - // String FormatEcart(uint64_t V1, uint64_t V2, int L, boolean Signe) { String S; S = ""; if (V1 > V2) { V1 = V1-V2; if (Signe) {S = "+";} } else { V1 = V2-V1; if (Signe) {S = "-";} } return FormatS(V1,L,S,4); } // // Formatte une valeur pour affichage // V1 = valeur // L = longueur // Signe = Signe à afficher // Unite de la valeur= 4=µS, 3=ms, 2=s, 1=mn, 0=h (unité dans laquelle est le nombre) // Rend "***" si dépassement capacité ou affichage impossible // Affichage => unité sur deux caractères + signe sur 1 (+ ou -) ou 0 (pas de signe) caractère + 1 chiffre => L >= 3 si pas de signe et L >= 4 si signe // String FormatS(uint64_t V1, int L, String Signe, byte Unite) { const String Depassement = "***"; String S; uint64_t VDec; uint64_t VTemp; int LPartieEntiere; unsigned int Diviseur; int i; if ((L < 3) || ((L == 4) && (Signe.length() == 1)) || (Signe.length() > 1)) {return Depassement;} // dépassement ou erreurs de certains paramètres S = ""; VDec = 0; LPartieEntiere = 0; while (S == "") { switch (Unite) { case 4: case 3: Diviseur = 1000; break; case 2: case 1: Diviseur = 60; break; //default: S = "> " + Signe + "1h"; break; } if ((V1 < Diviseur) && (Unite > 0)) { S = Signe+Uint64ToString(V1); LPartieEntiere = S.length(); if (VDec != 0) { S = S+"."+Uint64ToString(VDec); for (i=S.length(); i < L-2; i++) {S = S+"0";} } } else { VTemp = V1; V1 = V1 / Diviseur; VDec = VTemp-V1*Diviseur; Unite = Unite-1; } } if (S.length() > L-2) { S.remove(L-2,S.length()-L-2); // On tronque le résultat pour une longueur L (+ longueur des unités) } if (S.length() < LPartieEntiere) {return Depassement;} // Si la longueur du résultat est inférieure à la partie entière du nombre, alors, dépassement, on sort switch (Unite) { // On accole les unités case 4: S = S+"us";break; case 3: S = S+"ms";break; case 2: S = S+" s";break; case 1: S = S+"mn";break; case 0: S = S+" h";break; default: S = "***";break; } for (i=S.length(); i < L; i++) { // On complète le résultat avec des espaces en tête si besoin S = " "+S; } return S; } // // Affichage des mesures // AffFlip : affichage 1ère ligne (true) ou deuxième ligne (false) // void AffMesure(boolean AffFlip) { int i; String S; String Signe; long MoyenneTic; long MoyenneTac; uint64_t VTempU; /* Test durée exécution affichage unsigned long ENTREE; unsigned long SORTIE; ENTREE = millis(); Fin test durée exécution affichage */ // Affichage période. On n'affiche que si l'on a au moins MaxMesures mesures if ((CompteurTic.NbCumuls >= MaxMesures) && (CompteurTac.NbCumuls >= MaxMesures)) { if (AffFlip) { // Affichage 1ère ligne MoyenneTic = 0; MoyenneTac = 0; for (i=0;i < MaxMesures; i++) { MoyenneTic = MoyenneTic+CompteurTic.Mesure[i]; MoyenneTac = MoyenneTac+CompteurTac.Mesure[i]; } MoyenneTic = MoyenneTic / MaxMesures; MoyenneTac = MoyenneTac / MaxMesures; lcd.setCursor(0,0); S = FormatEcart(MoyenneTic+MoyenneTac, Periode[IndexBattements], 8, true); S = S+" "+FormatEcart(MoyenneTic, MoyenneTac, 7, false); lcd.print(S); } else { // Affichage 2ème ligne. MoyenneTic sert de variable temporaire lcd.setCursor(0,1); MoyenneTic = (CompteurTic.MesureLongTerme/CompteurTic.NbCumuls)+(CompteurTac.MesureLongTerme/CompteurTac.NbCumuls); // Moyenne des différence entre la durée d'un TicTac par rapport à la durée théorique attendue if (MoyenneTic < 0) { // Si la durée d'un Tic Tac est négative, c'est qu'elle est plus courte que la durée théorique attendue MoyenneTic = 0-MoyenneTic; Signe = "-"; // On affichera -X ms (ou µs ou s...) => La montre avance } else { Signe = "+"; // La durée d'un TicTac est plus grande que la durée théorique attendue, on affichera + X ms (ou µs ou s...) => La montre retarde } S = FormatS((uint64_t)MoyenneTic,8,Signe,4); VTempU = DemiPeriode[IndexBattements]*NbDureeLongTerme; // Durée attendue if (VTempU < DureeLongTerme) { // Si durée attendue est inférieure à durée mesurée VTempU = (DureeLongTerme-VTempU)/NbDureeLongTerme; // Alors, la montre retarde Signe = "-"; } else { VTempU = (VTempU-DureeLongTerme)/NbDureeLongTerme; // Sinon, la montre avance Signe = "+"; } S = S+" "+FormatS(VTempU*Battements[IndexBattements]*(uint64_t)24,7,Signe,4); lcd.print(S); } /* Test durée exécution affichage SORTIE = millis(); lcd.setCursor(0,0); lcd.print(String(DureeVrai(ENTREE,SORTIE))+" "); Fin test durée exécution affichage */ } } /********************************** APPLICATION **********************************/ void loop() { const String NonInitialise = "------ ------"; int i; boolean LEDFlip = true; // Pour alterner l'allumage des LED boolean AffFlip = true; // pour affichage alterné des indications int Passe; // Pour ne pas prendre en compte les deux premières ITTic unsigned long AffTimer; uint64_t TempsTic; uint64_t TempsTac; uint64_t DiffTicTac; AffTimer = millis(); while (1) { if (ITTic) { // Action faite quelque soit l'état de l'atomate s'il y a eu un Tic LEDFlip = !LEDFlip; // Pour affichage alterné des LED if (LEDFlip) { TempsTic = TempsTicTac; digitalWrite(LED_tic, HIGH); digitalWrite(LED_tac, LOW); } else { TempsTac = TempsTicTac; digitalWrite(LED_tic, LOW); digitalWrite(LED_tac, HIGH); } ITTic = false; // Prise en compte du Tic if (EtatAutomate == E_mesure) {EtatAutomate = E_calcule;} // Traitement du Tic si on est dans l'état de mesure } switch (EtatAutomate) { // // Passage en mode sélection, on efface l'écran et on passe en changement de battement // case E_selection_trans: lcd.clear(); lcd.print("Selection"); for (i=0; i < MaxMesures;i++) { CompteurTic.Mesure[i] = 0; CompteurTac.Mesure[i] = 0; } CompteurTic.IndexMesure = 0; CompteurTac.IndexMesure = 0; CompteurTic.MesureLongTerme = 0; CompteurTac.MesureLongTerme = 0; CompteurTic.NbCumuls = 0; CompteurTac.NbCumuls = 0; DureeLongTerme = 0; NbDureeLongTerme = 0; Passe = 2; ITTic = false; // // Transition de changement de battement // case E_selection_change_trans: if (Gauche) { Gauche=false; IndexBattements--; if (IndexBattements < 0) {IndexBattements = MaxBattements-1;} } if (Droite) { Droite=false; IndexBattements++; if (IndexBattements >= MaxBattements) {IndexBattements = 0;} } lcd.setCursor(0, 1); lcd.print(Uint64ToString(Battements[IndexBattements]) + " Bat/h "); EtatAutomate = E_selection; break; // // Passage en mode mesure. On affiche les valeurs courantes de la mesure // case E_mesure_trans: EtatAutomate = E_mesure; lcd.clear(); lcd.print(NonInitialise); lcd.setCursor(0, 1); lcd.print(NonInitialise); break; // // passage en pause // case E_pause_in_trans: // on n'était pas dans l'état E_pause if (DureeVrai(TimerDouble, millis()) > DureeDouble) { EtatAutomate = E_pause; lcd.setCursor(9, 1); lcd.print(" PAUSE "); } break; case E_pause_out_trans: // on était dans l'état E_pause if (DureeVrai(TimerDouble, millis()) > DureeDouble) { EtatAutomate = E_mesure_redemarre_trans; lcd.setCursor(9, 1); lcd.print(" "); } break; // // En pause, on attend un événement (bouton poussoir) // case E_pause: delay(1); break; // // Si on n'a pas eu de double appui, alors, on repars en mesure // case E_mesure_redemarre_trans: lcd.setCursor(10, 1); lcd.print(" "); Passe = 2; EtatAutomate = E_mesure; break; case E_calcule: if (Passe > 0) { Passe = Passe-1; } else { if (LEDFlip) { PCompteur = &CompteurTic; } else { PCompteur = &CompteurTac; } // Calcule de la dérive à court terme (MaxMesures) et à long terme if (TempsTic > TempsTac) {DiffTicTac = TempsTic-TempsTac;} else {DiffTicTac = TempsTac-TempsTic;} /* Test Durée mesurée brute lcd.setCursor(0,0); lcd.print(String(DiffTicTac)); */ if ((DiffTicTac > Periode[IndexBattements]) || (DiffTicTac < Aberrant[IndexBattements])) { Passe = 2; // si valeur aberrante, on ne prend pas en compte la mesure et on laisse passer 2 battements } else { PCompteur->Mesure[PCompteur->IndexMesure] = DiffTicTac; // On conserve les durées des Tic ou Tac PCompteur->MesureLongTerme = PCompteur->MesureLongTerme+(PCompteur->Mesure[PCompteur->IndexMesure]-DemiPeriode[IndexBattements]); // On ne conserve que les écarts par rapport à la valeur attendue (valeru signée) PCompteur->NbCumuls = PCompteur->NbCumuls+1; if (DureeLongTerme < 0x0FFFFFFFFFFFFFFF) { DureeLongTerme = DureeLongTerme + DiffTicTac; NbDureeLongTerme = NbDureeLongTerme+1; } PCompteur->IndexMesure = PCompteur->IndexMesure+1; if (PCompteur->IndexMesure >= MaxMesures) {PCompteur->IndexMesure = 0;} } } EtatAutomate = E_mesure; break; case E_mesure: if (DureeVrai(AffTimer, millis()) > 1000) { AffTimer = millis(); AffMesure(AffFlip); AffFlip = !AffFlip; } break; // // Dans l'état E_selection, on ne fait que choisir le battement de la montre. // La gestion du contacteur rotatif est fait par test d'état car on n'a que ça à faire. // On sort de l'état E_selection par appui sur le bouton poussoir du contacteur. Cet appui est // détecté par interruption // case E_selection: TestRotation(); break; // // L'état default correspond à une erreur d'automate (pas d'état connu). // En cas d'erreur, on retourne dans l'état E_Selection // default: EtatAutomate = E_selection_trans; lcd.clear(); lcd.print("Erreur"); delay(1000); break; } } }