Projet Réveil-Matin (partie X) - Amélioration de la classe ClockTime

Dans l'article consacré à la classe ClockTime, il avait été mis en évidence la limite de fonctionnement de la fonction millis(), utilisée pour mesurer le temps écoulé, du fait que la valeur exprimée en millisecondes de celle-ci est codée sur un entier long non signé (sur 32 bits). De ce fait elle ne fonctionne que 49 j 17 h 8mn et 47,295 s (temps correspondant à la valeur 0xFFFFFFFF millisecondes). Si on veut mesurer le temps écoulé sur un intervalle de temps plus important, il faut procéder par une astuce consistant à programmer une fonction renvoyant une valeur entière non signée sur un nombre de bits lus important.
Encore faudrait-il disposer d'un type de données permettant de manipuler des entiers sur 64 bits. Car, d'un façon inexpliquée, le type uint64_t ou long long ou double long ne fonctionne pas sur toutes les versions d'Arduino. Sans doute est-ce dû à la limite des registres du processeur utilisé.
Dans les applications scientifiques, en astronomie ou en physique des particules, il arrive souvent qu'il soit nécessaire d'effectuer des opérations sur des grands nombres dont la définition est très importante. C'est dans ce but  que la classe DoubleLong, objet d'un précédent article, a été développée.
Le problème est de détecter à quel moment la fonction millis() va basculer à 0. Heureusement, il est possible de résoudre ce problème grâce à une astuce de programmation bien connue par les développeurs d'applications pour l'astronomie, où la gestion des très grands nombres et du temps sont courantes. Cela consiste à détecter la bascule de dépassement de la valeur de millis() pour incrémenter une retenue dans une variable initialisée à zéro au démarrage du système. Ce qui peut être fait facilement, puisque la fonction millis() est invoquée plusieurs fois par seconde dans la boucle loop(), en comparant deux lectures consécutives de la fonction millis(). En fonctionnement normal, la deuxième valeur lue est toujours plus grande que la précédente. Mais lorsque la bascule a lieu, l'horloge interne repassant par zéro, la deuxième valeur est nécessairement inférieure à la précédente.

Cet article présent une nouvelle version de la classe ClockTime, qui implémente une fonction __longMillis() qui renvoie un résultat sur 64 bits en utilisant la classe DoubleLong

Correctif à effectuer dans le fichier ClockTime.h

#ifndef ClockTime_h
#define ClockTime_h

/* 1 */
#include "DoubleLong.h"
#include "Component.h"
#include "Event.h"
#include "Time.h"

class ClockTime;

class ClockTimeEvent {
  ClockTime* __sender;
  public:
  ClockTimeEvent(ClockTime* sender){
    this->__sender = sender;
  }
  ClockTime* getSender() { return this->__sender; }
};


class ClockTimeListener : public EventListener<ClockTimeEvent>
{
  protected:
  virtual void receive(ClockTimeEvent* event) {
    onTimeChanged(event->getSender());
  }
  virtual void onTimeChanged(ClockTime*) = 0;
};


class ClockTime : public Component,  public Time, public EventListenable<ClockTimeEvent>
{
/* 2 */
  DoubleLong __millis0;
/* 3 */
  uint32_t __lastMillis; 
/* 4 */
  uint8_t __carry;       

/* 5 */
  DoubleLong __longMillis();

  public:
  ClockTime();

  virtual void begin();
  virtual void loop();
  void reset(Time&);
};

#endif // ClockTime_h
  1. La classe ClockTime utilisant la classe DoubleLong, il faut inclure sa définition.
  2. L'attribut __millis0, relatif à l'origine des temps utilisée pour le réglage de l'horloge, est maintenant déclaré DoubleLong au lieu de uint32_t pour pouvoir être implémenté sur 64 bits. 
  3. La comparaison entre deux lectures successives de l'horloge interne par la fonction millis(), pour détecter la bascule à 0, nécessite un attribut privé __lastMillis pour mémoriser la dernière lecture.
  4. L'attribut __carry, initialisé à zéro au démarrage du système, est incrémenté à chaque overflow de l'horloge interne. Il est codé comme un entier non signé sur un octet (type uint8_t). Sa valeur maximum est 255. Ce qui permet à l'horloge d'afficher l'heure pendant 35 ans environs. A remarquer que, si l'attribut était déclaré de type uint16_t, sa valeur maximum serait  de 65535. Ce qui pousserait le fonctionnement de l'horloge à 8925 ans. A remarquer que la classe DoubleLong autoriserait le type uint32_t ce qui assurerait le fonctionnement de l'horloge pendant plusieurs millénaires.
  5. La fonction __longMilis() a la même fonction que la fonction millis() à la différence que le temps écoulé depuis le démarrage du système est exprimé sur 64 bits. Il s'agit d'une nouvelle méthode privée de la classe ClockTime.

Correctif à effectuer dans le fichier ClockTime.cpp

Dans le code ci-dessous, afin de bien mettre en évidence les modifications apportées à la classe ClockTime, les instructions modifiées sont mise en commentaires et surlignées en rouge. Les nouvelles instructions base sur la classe DoubleLong sont surlignées en vert.
#include "ClockTime.h"


ClockTime::ClockTime() : Time()
{
/* 1 */
  this->__lastMillis = 0;
  this->__carry = 0;
}

void ClockTime::begin() {
/* 2 */
  //this->__millis0 = millis();  
  this->__millis0 = this->__longMillis();
}

void ClockTime::loop() {
/* 3 */
  //Time newTime(millis() - this->__millis0);
  Time newTime(uint32_t((this->__longMillis() - this->__millis0) % DoubleLong(K_DAY)));
  if (this->_SS != newTime.SS()) {
    this->_HH = newTime.HH();
    this->_MN = newTime.MN();
    this->_SS = newTime.SS();
    this->_dispatch(new ClockTimeEvent(this));
  }
}
/* 4 */
DoubleLong ClockTime::__longMillis() {
  uint32_t now = millis();
  if (now < this->__lastMillis) this->__carry++;
  this->__lastMillis = now;
  return DoubleLong(this->__carry, now);
}

void ClockTime::reset(Time& newTime) {
/* 5 */
  //this->__millis0 = millis() - (newTime.HH() * K_HOUR + newTime.MN() * K_MINUTE);  
  this->__millis0 = this->__longMillis() - DoubleLong(newTime.HH() * K_HOUR + newTime.MN() * K_MINUTE);
}
  1. La classe ClockTime possède maintenant deux attributs privés supplémentaires. Il sont initialisés dans le constructeur de la classe :
    • __lastMillis, de type uint32_t, contient la dernière lecture effectuée par la fonction millis() dans la boucle loop(). Cela permet de détecter la bascule lorsque le compteur repasse par 0.
    • __carry, de type uint8_t, est la retenue. Elle est incrémentée à chaque bascule.
  2. L'attribut privé __millis0, contenant l'origine des temps et utilisé pour la mise à l'heure de l'horloge, est maintenant de classe DoubleLong sur 64 bits. Pour être initialisé il doit invoquer la nouvelle méthode __longMillis().
  3. Dans la boucle loop(), le calcul de la nouvelle valeur du temps a été adaptée (attention à la priorité des opérateurs utilisés et à la position des parenthèses) :
    • La différence __longMillis() - __millis0 est de classe DoubleLong sur 64 bits. Celle-ci correspond au temps écoulé depuis l'origine des temps en millisecondes sur 64 bits.
    • Comme le constructeur de la classe Time ne fonctionne qu'avec un nombre de 32 bits uint32_t. Ce temps écoulé doit subir quelques opérations pour pourvoir être passé au constructeur de la classe Time sans avoir à modifier celle-ci. L'expression est complexe du fait des règles de priorité du compilateur C++ pour les opérateurs. La position des parenthèse est importante pour obtenir le résultat attendu.
    • L'opérateur % de la classe DoubleLong est utilisé pour calculer le reste de la division sur 64 bits de ce temps écoulé par K_DAY correspondant au nombre de millisecondes d'une journée complète de 24 heures. 
    • K_DAY est lui-même converti sur 64 bits en utilisant le constructeur de conversion de la classe DoubleLong pour l'opération % fonctionne sur 64 bits.
    • Le résultat de l'opération est le nombre de millisecondes depuis minuit (00:00:00) du jour courant sur 64 bits. Ce résultat est tronqué sur 32 bits en utilisant l'opérateur de conversion uint32_t() de la classe DoubleLong.
    • Cette valeur de 32 bits est passée en paramètre du constructeur de la classe Time pour obtenir l'heure du jour HH:MN:SS.
  4. Une nouvelle méthode privée __longMillis() est programmée pour fournir la durée de fonctionnement du système depuis son démarrage sur 64 bits.
    • Une lecture sur 32 bits par la fonction standard millis() est affectée à une variable locale now codée sur 32 bits.
    • Celle-ci est comparée à la lecture millis() précédente sauvegardée dans l'attribut __lastMillis à l'itération précédente. Si la nouvelle durée est inférieure à la précédente, c'est que la bascule de dépassement a eu lieu. Dans ce cas la retenue __carry est incrémentée.
    • La nouvelle valeur now lue par millis() est sauvegardée dans l'attribut __lastMillis pour pouvoir être exploité à l'itération suivante.
    • Le retour de la fonction est construit sur 64 bits en prenant l'attribut __carry sur les32 bits de poids fort et now les 32 bits de poids faibles.
  5. La méthode reset() permet de remettre l'horloge à l'heure en modifiant l'origine des temps __millis0. Cet attribut étant sur 64 bits, l'opération est maintenant effectuée sur 64 bits en invoquant les opérateurs de la classe DoubleLong.

Conclusion

Voilà maintenant une classe ClockTime permettant de dépasser le seuil de 50 jours limitant le fonctionnement de l'horloge. Dans la nouvelle version présentée dans cet article, l'attribut __carry est de type uint8_t sur 8 bits. Ce qui permet de faire fonctionner l'horloge pendant 35 ans environ. Mais cette durée, déjà raisonnable, peut être dépassée. En déclarant l'attribut __carry de type int16_t sur 16 bits, la durée est prolongée à 8925 ans. A remarquer que l'utilisation de la classe DoubleLong dans ClockTime permet d'envisager le codage de __carry sur 32 bits. Ce qui permet le fonctionnement de l'horloge pendant 584 942 millénaires, autrement dit une éternité.
Par ailleurs, l'implémentation de la classe DoubleLong utilisée pour coder le temps sur 64 bits respecte la règle d'économie de mips consommés par la boucle loop(). En effet, avec l'implémentation présentée, celle-ci ne dépasse pas 3,2 millisecondes d'exécution. Ce qui est largement suffisant pour une application comme le projet Horloge.

Commentaires

Posts les plus consultés de ce blog

Afficheur à LED 7 segments (classe SegmentLedDisplay)

Piloter un clavier matriciel sur le bus I2C avec un PCF8574

Utiliser Visual Studio Code pour développer sur Arduino