Astuces que j'ai apprises sur l'utilisation d'une horloge en temps réel DS3231 pour interrompre un Arduino
Vous souhaitez déterminer les moments précis auxquels l'Arduino exécutera des segments de code spéciaux dans une esquisse, sans utiliser les minuteries et les compteurs du matériel Arduino. Vous avez des compétences et de l'expérience en écriture de code avec l'IDE Arduino et vous souhaitez éviter d'utiliser des instructions de code telles que delay()
.
Au lieu de cela, vous souhaiteriez connecter un module d'horloge en temps réel DS3231 très précis comme source d'interruptions externes. Il peut être important pour vous que le DS3231 puisse utiliser une batterie pour maintenir une heure précise même si l'Arduino perd temporairement de l'alimentation.
Enfin, vous souhaitez apprendre à utiliser la bibliothèque DS3231.h d'Andrew Wickert mentionnée dans la référence en ligne Arduino : https://www.arduino.cc/reference/en/libraries/ds3231/. Il contient quelques astuces qui valent la peine d’être maîtrisées. Vous pouvez importer cette bibliothèque dans votre IDE Arduino en utilisant le gestionnaire de bibliothèques (Outils > Gérer les bibliothèques...).
Allez-y étape par étape. Ce didacticiel illustre les étapes suivantes :
Si vous souhaitez que le code spécial s'exécute plus d'une fois, à des intervalles que vous spécifiez, votre code peut refaire l'étape 3 et définir une nouvelle alarme. L'exemple de croquis qui accompagne ce didacticiel interrompt l'Arduino à plusieurs reprises, à intervalles de 10 secondes.
Ce tutoriel s'appuie sur des références « officielles », notamment :
attachInterrupt()
: https://www.arduino.cc/reference/en/lingual/functions/external-interrupts/attachinterrupt/. Le code spécial dans cet exemple est trivial : il imprime uniquement l'heure actuelle du DS3231. Un exemple concret pourrait faire quelque chose d’utile comme arroser une plante ou enregistrer une mesure de température. Quelle que soit la tâche, le code doit être placé dans son propre bloc spécial pour être exécuté uniquement lorsque l'alarme DS3231 interrompt l'Arduino.
Je pense que c'est une bonne pratique de diviser le code en fonctions courtes, où chaque fonction ne gère qu'une seule tâche ou un ensemble de tâches connexes. Le nom d'une fonction peut être n'importe quoi ; pourquoi ne pas lui faire décrire ce que fait la fonction ? Voici une partie de ma fonction qui exécute le code spécial dans cet exemple.
void runTheSpecialCode() {
// get the current time
// using the DateTime and RTClib classes
// defined in DS3231.h
DateTime dt = RTClib::now();
// print the current time
Serial.print(dt.hour()); Serial.print(":");
if (dt.minute() < 10) Serial.print("0");
Serial.print(dt.minute()); Serial.print(":");
if (dt.second() < 10) Serial.print("0");
Serial.println(dt.second());
// There will be more to do here, as you will see.
// This is enough, for now, to illustrate the idea:
// put special code in its own, special function
}
Vous ferez passer des fils entre cinq paires de broches. Chaque paire remplit une fonction électrique et fait correspondre une broche de l'Arduino avec une broche correspondante sur le DS3231. Allez-y lentement, connectez chaque paire, puis vérifiez les deux extrémités pour vous assurer que chaque fil va là où il doit. Le tableau répertorie les paires dans l'ordre où elles se fixent, de gauche à droite, sur le DS3231 à partir d'un Arduino Uno.
But | Broche DS3231 | Broche Arduino |
---|---|---|
Alarme | SQW | 3* |
SCL | SCL | CL** |
SDA | SDA | SDA** |
Alimentation 5 volts | VCC | 5V |
Sol | GND | GND |
Comme je l'ai dit, prenez votre temps pour établir ces liens. Lentement et sûrement, c’est souvent le moyen le plus rapide de terminer quelque chose correctement.
Nous parlerons au module DS3231 au moyen d'un "objet" DS3231, une sorte de boîte à outils logicielle portant un nom. La bibliothèque DS3231 définit de nombreuses fonctions (considérez-les comme des outils) à l'intérieur de la boîte. Lorsqu'on veut utiliser une fonction, on écrit le nom de la boîte à outils, suivi d'un point, puis du nom de l'outil. L'exemple d'esquisse crée une variable "horloge" à cet effet. Ensuite, l'esquisse peut accéder aux outils dans la case "horloge". Tous les outils sont déclarés dans le fichier DS3231.h mentionné ci-dessus.
#include <DS3231.h>
DS3231 clock;
Serial.println(clock.getMinute()); // the current minute, 0..59
Nous utiliserons les outils de notre objet "horloge" pour régler l'alarme. Mais nous devons d’abord calculer l’heure de l’alarme.
Cette étape suppose que vous avez préalablement réglé l'heure réelle sur le DS3231. L'exemple de croquis contient du code que vous pouvez utiliser pour régler l'heure sur l'horloge, au cas où vous en auriez besoin. Supprimez simplement les délimiteurs de commentaires, /* et */, qui l'entourent.
L'exemple de croquis de ce didacticiel calcule une heure d'alarme dans le futur en ajoutant un intervalle, en nombre de secondes, à l'heure actuelle. L'exemple ajoute dix secondes. Une minute ajouterait 60 secondes. Une heure ajouterait 3 600 secondes. Un jour, 86 400 secondes. Et ainsi de suite.
La bibliothèque DS3231 a une « astuce » cachée qui permet d'ajouter facilement du temps en nombre de secondes. Vous ne trouverez pas cette astuce répertoriée parmi les fonctions disponibles sur la page README de la bibliothèque DS3231. Ce n’est pas non plus tout à fait évident en examinant le fichier DS3231.h. Certains détails attendent d'être trouvés dans le fichier de code DS3231.cpp. Voici les étapes pour effectuer le calcul.
now()
, à laquelle il faut accéder d'une manière spéciale.Les étapes 4 et 5 peuvent être combinées.
const Uint32_t interval = 10; // number of seconds to add
DateTime currentTime; // default declaration
currentTime = RTClib::now(); // RTClib is defined in DS3231.h
uint32_t currentSeconds = currentTime.unixtime(); // express the date in seconds
DateTime alarmTime(currentSeconds + interval); // add 10 seconds and create a new date
Même si l'objet alarmTime est créé sur la base d'un nombre de secondes, il fournit des outils dans sa boîte à outils pour exprimer son année, mois, jour, heure, minute et seconde. Nous définissons l'heure de l'alarme sur le DS3231, comme décrit ci-dessous, en utilisant l'objet alarmTime comme source des valeurs dont nous aurons besoin.
Supposons, par exemple, que l'heure actuelle signalée par le module DS3231 était 7 secondes après 10 h 42 du matin le mercredi 27 octobre 2021. L'heure d'alarme calculée ci-dessus serait 10 h 42 min 17 s ce même jour, dix secondes plus tard.
Un DS3231 propose deux alarmes différentes : l'alarme n°1 (A1) et l'alarme n°2 (A2). Les deux alarmes peuvent être spécifiées avec un jour et une heure, jusqu'à une minute. La différence est que A1 peut être spécifié davantage à la seconde près. Chaque alarme possède sa propre paire de fonctions dans la bibliothèque DS3231 pour régler l'heure de l'alarme et lire cette heure. Les fonctions sont toutes accessibles via un objet DS3231, par exemple celui que nous avons nommé « horloge » :
clock.setA1Time(), clock.getA1Time(), clock.setA2Time() et clock.getA2Time()
La fonction setA1Time() prend huit paramètres, comme indiqué dans cette citation du fichier DS3231.h :
void setA1Time(byte A1Day, byte A1Hour, byte A1Minute, byte A1Second, byte AlarmBits, bool A1Dy, bool A1h12, bool A1PM);
Une lecture attentive du fichier d'en-tête DS3231.h peut expliquer les paramètres. Ce qui suit ici sont mes tentatives pour me les réexpliquer dans mes propres mots. Si le lecteur constate une divergence entre ma version et le fichier d'en-tête, supposez que l'en-tête est correct.
Les cinq premiers paramètres sont de type « octet ». Le site Web cppreference définit le type d'octet de cette façon https://en.cppreference.com/w/cpp/types/byte :
std::byte est un type distinct qui implémente le concept d'octet tel que spécifié dans la définition du langage C++.
Comme char et unsigned char, il peut être utilisé pour accéder à la mémoire brute occupée par d'autres objets (représentation d'objet), mais contrairement à ces types, ce n'est pas un type caractère ni un type arithmétique. Un octet n'est qu'une collection de bits, et les seuls opérateurs définis pour celui-ci sont ceux au niveau du bit.
Nous pouvons nous permettre de considérer les variables de type octet pour le jour et l'heure comme s'il s'agissait d'entiers non signés, dans cette situation particulière. Ils peuvent contenir une valeur entière comprise entre 0 et 255. ATTENTION : le rédacteur du code doit éviter les valeurs absurdes. Par exemple, une valeur de 102 n’a de sens pour aucun de ces paramètres. C'est votre travail en tant que rédacteur de code de fournir des valeurs raisonnables.
Continuons avec l'alarmTime créé à l'étape précédente : le 27ème jour du mois, à 10h42 17 secondes. La liste ci-dessous montre comment vous pouvez fournir ces valeurs dans la fonction. Je liste chaque paramètre sur sa propre ligne, pour les rendre plus lisibles par les humains et laisser de la place aux commentaires. L'exemple ici est incomplet ; il montre uniquement les valeurs de type octet pour la date et l'heure. La fonction nécessite plus de paramètres, comme décrit ci-dessous, et ne s'exécutera pas sous la forme présentée ici.
A propos, notez que les variables "clock" et "alarmTime" sont des objets, c'est-à-dire des boîtes à outils logicielles. Comme vous le voyez, nous utilisons des outils provenant des boîtes à outils respectives pour accéder aux informations contenues dans les objets.
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
// ... the remaining parameters are explained below
);
Le paramètre de type octet suivant, nommé AlarmBits, n'est en réalité qu'une collection de bits. Les bits portent des noms tels que définis dans la fiche technique DS3231 (à la page 11).
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
Ensemble, les bits forment un « masque », ou motif, qui indique au DS3231 quand et à quelle fréquence signaler une alarme. Un tableau à la page 12 de la fiche technique donne la signification des différentes collections de bits. Sur la base de ce tableau, l'exemple d'esquisse de ce didacticiel utilise la collection de bits suivante :
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
Cette disposition des bits peut être exprimée explicitement dans le code : 0x00001110
. Il indique au DS3231 de signaler l'alarme chaque fois que « les secondes correspondent », c'est-à-dire lorsque la valeur « secondes » du réglage de l'alarme correspond à la valeur « secondes » de l'heure actuelle.
Les trois derniers paramètres de la fonction setA1Time()
sont des valeurs booléennes ou vraies/fausses. Ils donnent au DS3231 plus d'informations sur la façon d'évaluer le réglage de l'alarme. Le segment de code suivant montre l'appel terminé à setA1Time(), poursuivant l'exemple commencé ci-dessus :
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
0x00001110, // AlarmBits = signal when the seconds match
false, // A1Dy false = A1Day means the date in the month;
// true = A1Day means the day of the week
false, // A1h12 false = A1Hour in range 0..23;
// true = A1Hour in range 1..12 AM or PM
false // A1PM false = A1Hour is a.m.;
// true = A1Hour is p.m.
);
Dans l'exemple de croquis, où nous définissons l'alarme pour qu'elle s'interrompe toutes les 10 secondes, seuls les paramètres A1Second et AlarmBits comptent. Cependant, nous devons tous les fournir lorsque nous appelons la fonction setA1Time()
. Les valeurs correctes ne sont pas plus difficiles à fournir que les valeurs indésirables ; autant faire preuve de prudence avec eux.
La fonction setA2Time()
fonctionne de la même manière, mais sans paramètre pendant les secondes. Prenez le temps de revoir les lignes 119 à 145 du fichier DS3231.h dans la bibliothèque et les pages 11 et 12 de la fiche technique. Asseyez-vous patiemment avec ces références jusqu'à ce que vous y ayez trouvé les informations dont vous avez besoin pour régler une heure d'alarme.
Après avoir réglé l'heure, le croquis doit effectuer une action supplémentaire pour activer l'alarme dans le DS3231. J'encourage le lecteur à suivre systématiquement une séquence en trois étapes, même si certaines étapes peuvent sembler moins nécessaires à un moment donné pour une raison quelconque. Si votre code doit se tromper, laissez-le pécher par excès de certitude.
Pour l'alarme A1, les instructions dans la bibliothèque DS3231 seraient :
turnOffAlarm(1); // clear the A1 enable bit in register 0Eh
checkIfAlarm(1); // clear the A1 alarm flag bit in register 0Fh
turnOnAlarm(1); // set the A1 enable bit in register 0Eh
Pour l'alarme A2, changez simplement le paramètre à 2. Par exemple : checkIfAlarm(2); // clear A2 flag bit in register 0Fh
.
Un problème décrit dans ce dépôt par @flowmeter souligne que les deux indicateurs d'alarme doivent être effacés avant que le DS3231 puisse signaler une alarme. Pour en être sûr, pensez à appeler checkIfAlarm() deux fois, une fois pour chaque alarme, même si vous n'utilisez qu'une seule des alarmes :
checkIfAlarm(1);
checkIfAlarm(2);
Pourquoi les rédacteurs de codes choisiraient-ils de « vérifier » une alarme qui, selon eux, n'envoie pas actuellement de signal ? La raison est que la fonction checkIfAlarm()
a un effet secondaire non évident. Il efface le bit du drapeau d'alarme. Nous utilisons la fonction checkIfAlarm()
, car c'est la seule de la bibliothèque DS3231 qui effectue l'opération nécessaire.
Pensez-y. Pour les raisons expliquées ci-dessous, le matériel de détection d'interruption Arduino a besoin que la tension sur la broche SQW du DS3231 soit ÉLEVÉE avant le moment où l'alarme se produit. L'événement d'alarme modifie deux choses à l'intérieur du DS3231 :
La broche SQW restera FAIBLE tant que l'un ou l'autre de ces bits d'indicateur d'alarme restera activé. Tant qu'un bit d'indicateur d'alarme à l'intérieur du DS3231 maintient la broche SQW FAIBLE, l'Arduino ne peut plus détecter d'alarmes. Les bits de l'indicateur d'alarme doivent tous deux être effacés pour que le DS3231 rétablisse une tension HAUTE sur sa broche d'alarme SQW. Reportez-vous à la discussion sur les bits 1 et 0 dans le « Registre d'état (0Fh) », à la page 14 de la fiche technique DS3231.
Chaque alarme possède son propre bit d'indicateur d'alarme à l'intérieur du DS3231. L’un ou l’autre des bits du drapeau d’alarme peut maintenir la broche SQW FAIBLE. Le DS3231 n’effacera pas un bit d’indicateur d’alarme de sa propre initiative. Il incombe au rédacteur du code d'effacer les bits de l'indicateur d'alarme une fois l'alarme déclenchée .
Votre boucle principale n'a pas besoin de mesurer le temps. Il lui suffit de vérifier un indicateur pour voir si une alarme s'est produite. Dans l'exemple de croquis, cet indicateur est une variable booléenne nommée "alarmEventFlag" :
if (alarmEventFlag == true) {
// run the special code
}
La plupart du temps, l'indicateur sera false et la boucle ignorera le code spécial. Comment le croquis met-il en place le drapeau ? Trois étapes :
bool alarmEventFlag = false;
void rtcISR() {alarmEventFlag = true;}
attachInterrupt()
fournie par l'IDE Arduino rassemble le tout. L'exemple suivant indique au matériel Arduino d'exécuter la fonction rtcISR()
immédiatement chaque fois qu'il détecte un signal « FALLING » sur une broche numérique désignée.attachInterrupt(digitalPinToInterrupt(dataPin), rtcISR, FALLING);
Pour des raisons profondes et occultes, utilisez toujours la fonction spéciale, digitalPinToInterrupt()
, lorsque vous spécifiez le numéro de broche pour une interruption. Je laisse cela comme un exercice pour que le lecteur découvre pourquoi nous avons besoin de cette fonction.
Qu'est-ce qu'un signal CHUTE ? Cela signifie un changement de tension, de LOW à HIGH, tel que détecté par la broche numérique de l'Arduino. D'où vient le changement de tension ? Il provient de la broche d'alarme du module DS3231. Cette broche est étiquetée SQW et émet une tension ÉLEVÉE, proche du niveau d'alimentation VCC (c'est-à-dire 5 volts sur l'Uno) la plupart du temps. Une alarme amène le DS3231 à modifier la tension sur la broche SQW sur FAIBLE. L'Arduino détecte la tension provenant de la broche SQW et remarque le changement. Nous disons à l'Arduino de remarquer FALLING car cet événement ne se produit qu'une fois par alarme, alors que le niveau FAIBLE peut persister et confondre l'Arduino en déclenchant de nombreuses interruptions.
Quelle broche peut détecter un changement de tension en baisse ? Pour Unos, vous pouvez choisir l'une des broches 2 ou 3. Pour Leonardo, il peut s'agir de l'une des broches 0, 1 ou 7. (Oui, je sais, Leonardo détecte également les interruptions sur les broches 2 et 3. Cependant, celles-ci sont Les broches I2C de Leonardo, ce qui signifie que le module DS3231 les utiliserait. Je commence par la broche 7 pour les interruptions sur un Leonardo.) L'exemple d'esquisse définit une variable dataPin et initialise sa valeur à 3 pour l'exécution. un Uno de cette façon :
int dataPin = 3;
Le code spécial peut également définir une nouvelle heure d'alarme, si vous souhaitez répéter le cycle. Commencez par calculer la nouvelle heure de l'alarme, comme décrit à l'étape 3, et suivez la séquence d'étapes à partir de là.
Je m'attendrais à ce que l'exemple de croquis produise une sortie similaire à l'illustration ci-dessous, lorsqu'il est exécuté sur un Arduino Uno correctement connecté à un module DS3231, comme je le décris dans ce didacticiel. Ne soyez pas surpris si les horaires affichés sont différents. De toute façon, vous devriez travailler selon votre propre horaire.