Interfacer notre système avec l’horloge RTC

Posted on dim. 10 septembre 2023 in Datalogging, Arduino

Petit changement matériel

Pour des raisons pratiques, je passe sur un (clône d’)Arduino Nano. Ça ne change rien au code ou aux broches utilisées, mais 1) ça rend mon projet un peu plus compact; et 2) pour l’installation finale, je compte utiliser un Nano (moins de place utilisée), donc autant prendre de l’avance.

Une fois les headers soudés au Nano, je n’ai plus qu’à l’enficher dans ma breadboard.

Connexion de l’horloge RTC

La première étape est de relier le module RTC à l’Arduino. Sur les 6 pins exposés par le module, nous n’en utiliserons que 5 : VCC, GND, SDA, SCL et SQW. VCC et GND correspondent à l’alimentation électrique du module, SDA et SCL aux lignes du protocole I2C, et SQW (également connu comme INT') est le pin sur lequel sont reportées les alarmes. C’est lui qui va passer à l’état Low lorsque la condition d’alarme sera remplie.

Adaptons notre code

Commençons par le commencement

En premier lieu, il faut installer la bibliothèque RTClib d’Adafruit depuis le gestionnaire de bibliothèques, puis l’importer dans notre code. À partir de là, on peut initialiser notre RTC.

Le code présenté ci-après est inspiré de l’exemple DS3231_alarm fourni par Adafruit.

Initialisation de la RTC

Dans un premier temps, au sein de la fonction setup classique de l’Arduino, on initialise la connexion avec la RTC et, point important, on vérifie si elle a subi une perte totale d’alimentation (coupure de l’alimentation principale et de la pile de secours). En effet, si la RTC perd toute alimentation électrique, elle arrête de fonctionner (logique…) et n’offrira donc plus de suivi du temps. Dans ce cas, il sera nécessaire 1) de comprendre pourquoi il y a eu coupure totale, afin notamment de changer la pile de secours si nécessaire ; et 2) de lui redonner la bonne date et heure.

Ici, on réinitialise la date et heure à partir de la date et heure de compilation du programme (les deux variables __DATE__ et __TIME__, converties en objet DateTime), mais on pourrait imaginer créer une fonction qui prend en paramètre un timestamp numérique ou une date/heure au format ISO 8601, et qui met à jour la RTC à n’importe quel moment de l’exécution du programme. Ce genre de fonctions permettrait de mettre à jour l’heure ultérieurement via la liaison série, par exemple.

Ensuite, on acquitte les éventuelles alarmes encore en place, on désactive la fonction Square Wave (étant donné que le pin est commun avec l’alerte, c’est l’un ou l’autre). On désactive également l’alarme 2 puisqu’on ne va travailler qu’avec l’alarme 1, ça évitera de fausses alertes.

Pour définir le timing de l’alarme 1, on utilise… setAlarm1. Elle prend deux paramètres : la date & heure à laquelle doit se déclencher l’alarme, et le « masque » sur cette même date & heure. En gros, le masque va définir comment va être interprété le timestamp. Ici, en mettant DS2321_A1_Second, on demande à ce que l'alarme se déclenche à chaque fois que les secondes matchent (donc ici, à chaque fois qu'on va être à HH:MM:00).

#include <RTClib.h>

#define INT_PIN 2

RTC_DS3231 rtc; // On crée un objet RTC_DS3231

void setup() {
  Serial.begin(9600);
  pinMode(INT_PIN, INPUT_PULLUP);

  if(!rtc.begin()) {
    Serial.println("RTC non initialisée");
    while(1) delay(10);
  }

  if (rtc.lostPower()) {
    Serial.println("RTC lost power, let's set the time!");
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  rtc.disable32K();
  rtc.clearAlarm(1);
  rtc.clearAlarm(2);
  rtc.writeSqwPinMode(DS3231_OFF);
  rtc.disableAlarm(2);

  if(!rtc.setAlarm1(
    DateTime(2023, 9, 9, 10, 0, 0), DS3231_A1_Second // Bien noter A1_Second et pas A1_Minute, sinon ça matche à chaque XX/XX/XXXX XX:00:00, donc toutes les heures…
    // Voir https://garrysblog.com/2020/07/05/using-the-ds3231-real-time-clock-alarm-with-the-adafruit-rtclib-library/
  )) {
    Serial.println("Alarm couldn’t be set");
  } else {
    Serial.println("Wakeup at next minute");
  }
}

Préparation de quelques fonctions utilitaires

Une fois la RTC initialisée, on peut définir un callback (fonction qui sera appelée au déclenchement de l'alarme). On en profite pour créer une fonction d'affichage de la date.

void wakeup() {
  Serial.println("Wakeup");
}

void print_dt(DateTime now) {
  char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
  Serial.print(now.year(), DEC);
    Serial.print('/');
    Serial.print(now.month(), DEC);
    Serial.print('/');
    Serial.print(now.day(), DEC);
    Serial.print(" (");
    Serial.print(daysOfTheWeek[now.dayOfTheWeek()]);
    Serial.print(") ");
    Serial.print(now.hour(), DEC);
    Serial.print(':');
    Serial.print(now.minute(), DEC);
    Serial.print(':');
    Serial.print(now.second(), DEC);
    Serial.println();
}

On inclut la RTC dans la boucle principale

La majeure partie du code est assez explicite. La gestion de la RTC commence à partir du if(rtc.alarmFired(1)). En gros, on regarde si le pin SQW du module RTC est à l'état bas (low), ce qui signalerait qu'une alarme s'est déclenchée (les alarmes ne se réinitialisent pas automatiquement). Si tel est le cas, on annule l'alarme, puis on prépare l'Arduino à une interruption externe avec attachInterrupt.

Avec attachInterrupt(digitalPinToInterrupt(INT_PIN), wakeup, FALLING);, on demande à l'Arduino d'écouter une interruption entrante sur le pin INT_PIN. Cette interruption est de type FALLING, ou descendante, ce qui signifie que la broche INT_PIN normalement à l'état haut va passer à l'état bas en cas d'alarme. Ce passage à l'état bas est notre interruption. Enfin, le second paramètre dit à l'Arduino quoi faire lors d'une interruption, c'est notre fonction wakeup de callback.

Comme évoqué dans un précédent post, avant de mettre l'Arduino en sommeil, il est nécessaire de vider le tampon Serial si l'on veut pouvoir avoir un affichage correct des caractères transmis au réveil.

Enfin, on passe l'Arduino en sommeil avec LowPower.powerDown. SLEEP_FOREVER signifie que la mise en veille durera tant qu'il n'y aura pas eu d'interruption. À partir de ce moment-là, l'Arduino est en sommeil. Au réveil, suite à l'interruption, il reprendra le cours de la fonction loop() : il annulera l'interruption (dernière ligne de la fonction), puis repartira au début : lecture de l'heure, de la mesure analogique, mise en sommeil… Et tout ça à l'infini (enfin, tant que l'Arduino et la RTC sont alimentés).

void loop() {
  // Ces deux lignes permettent d’afficher la date et l’heure
  DateTime now = rtc.now();
  print_dt(now);

  // Prendre une mesure analogique, afficher le résultat sur l'échelle 0-1023
  // puis convertir en micromètres et l'afficher
  analog_reading = analogRead(A0);
  Serial.print(analog_reading);
  Serial.print("bin = ");
  value_micros = map(analog_reading, 0, 1023, 0, 50000);
  Serial.print(value_micros);
  Serial.println("μm");

  if(rtc.alarmFired(1)) {
    rtc.clearAlarm(1);
    Serial.println("Alarm Cleared");
  }

  attachInterrupt(digitalPinToInterrupt(INT_PIN), wakeup, FALLING);
  Serial.println("Gonna sleep");
  Serial.flush();
  LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);
  detachInterrupt(digitalPinToInterrupt(INT_PIN));
}

Le code complet

#include "LowPower.h"
#include <RTClib.h>

#define INT_PIN 2

RTC_DS3231 rtc; // On crée un objet RTC_DS3231

void setup() {
  Serial.begin(9600);
  pinMode(INT_PIN, INPUT_PULLUP);

  if(!rtc.begin()) {
    Serial.println("RTC non initialisée");
    while(1) delay(10);
  }

  if (rtc.lostPower()) {
    Serial.println("RTC lost power, let's set the time!");
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  rtc.disable32K();
  rtc.clearAlarm(1);
  rtc.clearAlarm(2);
  rtc.writeSqwPinMode(DS3231_OFF);
  rtc.disableAlarm(2);

  if(!rtc.setAlarm1(
    DateTime(2023, 9, 9, 10, 0, 0), DS3231_A1_Second // Bien noter A1_Second et pas A1_Minute, sinon ça matche à chaque XX/XX/XXXX XX:MIN 00, donc toutes les heures…
    // Voir https://garrysblog.com/2020/07/05/using-the-ds3231-real-time-clock-alarm-with-the-adafruit-rtclib-library/
  )) {
    Serial.println("Alarm couldn’t be set");
  } else {
    Serial.println("Wakeup at next minute");
  }
}

void loop() {
  // Ces deux lignes permettent d’afficher la date et l’heure
  DateTime now = rtc.now();
  print_dt(now);

  analog_reading = analogRead(A0);
  Serial.print(analog_reading);
  Serial.print("bin = ");
  value_micros = map(analog_reading, 0, 1023, 0, 50000);
  Serial.print(value_micros);
  Serial.println("μm");

  if(rtc.alarmFired(1)) {
    rtc.clearAlarm(1);
    Serial.println("Alarm Cleared");
  }

  attachInterrupt(digitalPinToInterrupt(INT_PIN), wakeup, FALLING);
  Serial.println("Gonna sleep");
  Serial.flush();
  LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);
  detachInterrupt(digitalPinToInterrupt(INT_PIN));
}

void wakeup() {
  Serial.println("Wakeup");
}

void print_dt(DateTime now) {
  char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
  Serial.print(now.year(), DEC);
    Serial.print('/');
    Serial.print(now.month(), DEC);
    Serial.print('/');
    Serial.print(now.day(), DEC);
    Serial.print(" (");
    Serial.print(daysOfTheWeek[now.dayOfTheWeek()]);
    Serial.print(") ");
    Serial.print(now.hour(), DEC);
    Serial.print(':');
    Serial.print(now.minute(), DEC);
    Serial.print(':');
    Serial.print(now.second(), DEC);
    Serial.println();
}

Cette méthode fonctionne, et l’Arduino se réveille toutes les minutes pour effectuer une mesure.