Mesure de la dérive d’un module RTC

DS1307 vs DS3231

Comme nous l’avons vu dans mon blog, un module RTC permet à un Arduino de connaître l’heure et la date courante. Il existe différents modules RTC, mais leur précision n’est pas identique et certains ont une dérive (avant ou retard de l’horloge) relativement élevée. Dans ce tuto je vais comparer les deux modules DS1307 et DS3231 aussi vous pourrez choisir votre module en fonction de votre besoin et de votre budget. Ce tutoriel utilise un NodeMCU et non un Arduino car j’ai besoin de récupérer l’heure courante sur un serveur NTP via une connexion WiFi.

La dérive annoncée du DS1307 est de 2 secondes par jour, soit environ 10 minutes par an. L’avantage de ce composant est son prix très intéressant. Si un retard ou une avance d’environ 10 minutes par an est acceptable pour votre réalisation c’est un excellent choix de module RTC.

Si vous avez besoin d’une horloge plus précise, le DS3231 est plus adapté car il inclus un oscillateur compensé en température (TCXO). En effet la fréquence des oscillateurs varient avec la température. Ce type de module mesure la température et modifie en conséquence la fréquence de l’oscillateur. Il en résulte des modules dont la dérive en fonction de la température est presque nulle. Ce module est annoncé avec une dérive de ±2 ppm, soit environ 1 minute par an maximum.

Nous allons tenter de vérifier ces chiffres, dans un but pédagogique comme toujours dans mes tutos.

Principe de la mesure

Le principe est simple, il se décompose en trois étapes :

  • Lancer le client NTP et récupérer l’heure courante sur un serveur NTP .
  • Démarrer le module RTC et y stocker l’heure courante au démarrage du programme.
  • Ensuite, il suffit de boucler sur la lecture de l’horloge du module RTC et de la comparer à l’heure courante qui est régulièrement mise à jour via le serveur NTP.

Récupérer l’heure courante sur un serveur NTP

L’heure courante est obtenue grâce à un serveur NTP (Network Time Protocol). Pour faire simple, NTP est un protocole qui va récupérer l’heure sur un serveur qui se trouve sur internet.Le client NTP qui tourne sur le NodeMCU va récupérer régulièrement (1 fois par heure) l’heure sur le serveur NTP et met à jour son horloge interne. L’heure courante sur le NodeMCU reste ainsi très précise dans le temps.

J’utilise la librairie ESPPerfectTime qui s’installe depuis le gestionnaire de bibliothèque de l’IDE Arduino. J’ai rencontré quelques problèmes de récupération de l’heure avec la librairie NTPClient donc je la déconseille. La librairie ESPPerfectTime utilise le protocole Simple Network Time Protocol (SNTP), qui est une version simplifiée du protocole NTP.

L’initialisation du client NTP se fait simplement en appelant la méthode configTzTime avec le fuseau horaire correspondant à votre localisation et le serveur NTP de votre choix en paramètre.

#include <TZ.h>
#include <ESPPerfectTime.h>

pftime::configTzTime(TZ_Europe_Paris, "fr.pool.ntp.org");

// Pour fuseau horaire France - ne gere pas le changement heure ete/hiver

Après avoir démarré le client NTP, il suffit d’appeler la méthode time pour récupérer l’heure sur le serveur NTP au format Unix, il s’agit du nombre de secondes depuis le 1er janvier 1970 00:00:00 UTC.

  heureDebutTestUnixTime = pftime::time(nullptr);

Pour ne pas surcharger inutilement les serveurs NTP, l’heure n’est pas récupérée sur le serveur NTP à chaque appel de la méthode time mais uniquement 1 fois par heure (configuration par défaut de la librairie).

Bien entendu, si votre micro-contrôleur possède un accès à internet, l’utilisation d’une module RTC est inutile car vous pouvez récupérer l’heure sur un serveur NTP. Dans ce tuto nous utilisons l’heure du serveur NTP comme horloge de référence pour tester nos modules RTC.

Accès à internet via le réseau WiFi

Pour récupérer l’heure sur un serveur NTP, il faut que notre Arduino soit connecté à internet. Il est possible d’ajouter un shield WiFi ou Ethernet sur un Arduino Uno par exemple.

Pour ce tuto j’ai décidé d’utiliser un NodeMCU ESP8266 qui est un équipement peu onéreux offrant une connexion WiFi et qui se programme facilement avec l’IDE Arduino.

NodeMCU ESP8266
NodeMCU ESP8266

Il suffit de déclarer notre équipement comme une station WiFi dans l’appel à WiFi.mode et de passer le nom du réseau (SSID) et son mot de passe lors de l’appel à WiFi.begin.

#include <ESP8266WiFi.h>

WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);

Afficheur OLED

Un petit afficheur OLED permet d’afficher la date et l’heure retournée par horloge du module RTC ainsi que sa dérive par rapport à l’heure courante retournée par le serveur NTP. J’affiche également d’autres paramètres comme le nom du module RTC utilisé ou encore la durée depuis le lancement du test. J’affiche également la température retournée par le module RTC lorsqu’elle est disponible (sur le DS3231 par exemple).

Affichage des résultats du test du module RTC DS3231 sur un écran OLED

Au début j’ai utilisé la bibliothèque ESP8266 and ESP32 OLED driver for SSD 1306 displays, qui semblait convenir à l’utilisation d’un module OLED 0.96″ sur un NodeMCU ESP8266. Mais j’ai rencontré des problèmes assez étranges que je n’explique pas car je n’arrivais plus à communiquer avec le module RTC d’origine chinoise Je n’ai pas pris le temps d’investiguer ce problème, et j’ai finalement choisi la bibliothèque Adafruit_SSD1306 qui fonctionne très bien.

Le schéma du montage

Le montage ci-dessous correspond au montage pour le module RTC DS1307. La subtilité pour ce module est qu’il est alimenté en 5 V. L’alimentation en 5 V peut se faire via la broche VU du NodeMCU ESP8266 V3. Dans ce montage j’alimente également l’écran OLED en 5 V.

Pour le montage du module RTC DS3231 c’est plus simple puisqu’il est alimenté en 3 V. Il est possible de récupérer l’alimentation 3V via différentes broches du NodeMCU. Dans ce montage j’alimente également l’écran OLED en 3 V.

NodeMCU ESP8266 module RTC3231 et OLED 0.96"
Câblage du DS1307 et de l’écran OLED sur bus I2C du NodeMCU ESP8266

Le montage

J’utilise deux NodeMCU afin de pouvoir comparer les deux modules RTC en même temps. Notez que je fais clignoter la LED toutes les minutes. La LED est allumée lorsque la valeur du champ seconde de l’heure retourné par le module RTC vaut « 00 ». Cela permet de vérifier visuellement le décalage de l’horloge d’un module RTC par rapport à l’autre.

Test comparatif des modules RTC DS1307 et DS3231

Comparatif en vidéo

Dans cette vidéo vous trouverez les résultats de plusieurs tests que j’ai réalisés à partir de ce montage :

  • Comparaison de la dérive des deux modules RTC Adafruit DS1307 et DS3231
  • Influence de la température sur la dérive du DS1307
  • Comparaison d’un module DS1307 Adafruit avec un équivalent chinois à bas coût
Comparaison de la dérive des modules DS1307 et DS3231

Le programme

// Utilisation d'un module RTC avec un Arduino Uno
// https://tutoduino.fr/
// Copyleft 2020

#include <TZ.h>
#include <ESP8266WiFi.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>  
#include <ESPPerfectTime.h>
#include "RTClib.h"
 
// Si le module RTC DS1307 est utilise ne pas commenter 
// la ligne suivante. Si vous utilisez le module DS3231
// il faut commenter la ligne suivante
//#define RTCDS1307
 
// Declaration du module RTC
#ifdef RTCDS1307
RTC_DS1307 rtc;
#else
RTC_DS3231 rtc;
#endif

#define SCREEN_WIDTH 128 
#define SCREEN_HEIGHT 64

// Declaration de l'afficheur OLED SSD1306
#define OLED_RESET        -1    // pas de reset
#define I2C_ADDRESS_OLED  0x3C  // adresse I2C ecran
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &amp;Wire, OLED_RESET);

// La LED interne du NodeMCU est D4
#define LED_INTERNE_NODEMCU D4
 

// Declaration des paramètres du WIFI
const char *ssid     = "xxxxxxx";
const char *password = "xxxxxxxxxxxxx";
 
// Afin de mesurer la derive en evitant les sauts d'horloge d'une seconde à l'autre, 
// on stocke les valeurs des NB_ELEM_LISTE_DERIVES valeurs des dernières derives dans
// le tableau listeDerives. Lors de l'affichage de la derive on fera la moyenne des 
// valeurs contenues dans ce tableau
#define NB_ELEM_LISTE_DERIVES 20
int8_t listeDerives[NB_ELEM_LISTE_DERIVES];
uint16_t indiceListeDerives;
 
// Date et heure du lancement du test sous format date Unix
time_t heureDebutTestUnixTime;
 
//
// Fonction d'initialisation du WIFI
//
void setup_wifi(){
  // Connexion au wifi en mode station
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }

  display.setCursor(0, 10);
  display.print(F("WiFi ok"));
  display.display();
  delay(1000);
  display.clearDisplay();  
}
 
//
// Ajoute la derive courante dans la table et calcule
// la moyenne de toutes les derives stockees dans la table
//
int8_t moyenneDerive(int8_t deriveCourante) {
 
  int8_t i;
  int16_t somme=0;
 
  listeDerives[indiceListeDerives] = deriveCourante;
  if (indiceListeDerives == NB_ELEM_LISTE_DERIVES -1)
    indiceListeDerives = 0;
  else
    indiceListeDerives++;
     
  for (i=0 ; i<NB_ELEM_LISTE_DERIVES ; i++) {
    somme += listeDerives[i];
    }
 
  return (somme / NB_ELEM_LISTE_DERIVES);
 
}

void setupOled() {

  display.begin(SSD1306_SWITCHCAPVCC, I2C_ADDRESS_OLED);
  display.display();
  delay(1000);
  display.clearDisplay();

  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.print(F("Tutoduino"));
  display.setTextSize(1);
  display.setCursor(0, 20);
  display.print(F("Apprendre"));
  display.setCursor(0, 30);
  display.print(F("l'electronique"));  
  display.setCursor(0, 40);
  display.print(F("avec un Arduino"));
  display.display();

  delay(1000);
  display.clearDisplay();

}
 
//
// Fonction d'initialisation appelle au demarrage de l'Arduino
//
void setup() {
  
  Serial.begin(9600);

  // Initialisation de l'ecran OLED
  setupOled(); 
   
  // Initialisation du Wifi
  setup_wifi();
   
  // Intialisation du client SNTP
  pftime::configTzTime(TZ_Europe_Paris, "fr.pool.ntp.org");
  delay(1000);

  // Enregistre l'heure du debut du test au format Unix
  heureDebutTestUnixTime = pftime::time(nullptr);
  
  // Demarre le module RTC et initialise son horloge
  // avec l'heure retournee par le serveur SNTP
  rtc.begin();
  rtc.adjust(DateTime(heureDebutTestUnixTime));
  
  // Declaration en sortie de la broche sur laquelle est reliee
  // la LED interne que nous allons faire clignoter
  pinMode(LED_INTERNE_NODEMCU, OUTPUT);
 
  // Met a zero toutes les valeurs de la table
  // des derives
  memset(listeDerives, 0, NB_ELEM_LISTE_DERIVES);
  indiceListeDerives = 0;
  
}
 
//
// Fonction appellee en boucle apres la fonction d'initialisation
//
void loop() {
  char heure[11];
  char date[6];
  int8_t derive;
 
  
  // Recupere la date et l'heure depuis le module RTC et la stocke dans 
  // des variables sous forme de tableaux de caracteres
  DateTime horlogeRtc = rtc.now();
 
  // Recupere l'heure courante (synchronisee sur le serveur NTP)
  time_t horlogeNtpUnixTime = pftime::time(nullptr);
   
  sprintf(heure, "%02d:%02d:%02d", horlogeRtc.hour(), horlogeRtc.minute(), horlogeRtc.second());
  sprintf(date, "%02d/%02d", horlogeRtc.day(), horlogeRtc.month());
  
  // Calcule la derive moyenne à partir de la table 
  // des derives (evite le bagotement de la valeur)
  derive = moyenneDerive(horlogeNtpUnixTime-horlogeRtc.unixtime());
 
  // Affiche les informations sur l'ecran OLED
   display.clearDisplay();

#ifdef RTCDS1307  
  display.setCursor(0,0);
  display.println("Test module DS1307");   
#else
  display.setCursor(0,0);
  display.println("Test module DS3231");   
  // Le module DS3231 permet de retourner la temperature, on l'affiche 
  display.setCursor(0,50);
  display.println("Temperature = "+String(rtc.getTemperature()));
#endif
  display.setCursor(0,10);
  display.println(date);   
  display.setCursor(0,20);
  display.println(heure);   
  display.setCursor(0,30);
  display.println("Duree  = "+String(horlogeNtpUnixTime-heureDebutTestUnixTime)+" s");  
  display.setCursor(0,40);
  display.println("Derive = "+String(derive)+" s");  
  display.display();
 
  // Allume la led interne du module NodeMCU toutes les minutes pile
  // Attention cette LED fonctionne en mode inversee (allumee si etat LOW)
  if (horlogeRtc.second() == 0) {
    digitalWrite(LED_INTERNE_NODEMCU,LOW);
  } else {
    digitalWrite(LED_INTERNE_NODEMCU,HIGH);
  } 
   
  delay(200);
}