Tricks, die ich über die Verwendung einer DS3231-Echtzeituhr zum Unterbrechen eines Arduino gelernt habe
Sie möchten genaue Zeiten bestimmen, zu denen der Arduino spezielle Codesegmente in einem Sketch ausführt, ohne die Timer und Zähler in der Arduino-Hardware zu verwenden. Sie verfügen über einige Fähigkeiten und Erfahrungen beim Schreiben von Code mit der Arduino IDE und möchten die Verwendung von Codeanweisungen wie delay()
vermeiden.
Stattdessen möchten Sie ein sehr präzises DS3231-Echtzeituhrmodul als Quelle für externe Interrupts anschließen. Es kann für Sie wichtig sein, dass der DS3231 eine Batterie verwenden kann, um die genaue Zeit aufrechtzuerhalten, selbst wenn der Arduino vorübergehend die Stromversorgung verliert.
Schließlich möchten Sie lernen, wie Sie die DS3231.h-Bibliothek von Andrew Wickert verwenden, auf die in der Arduino-Online-Referenz verwiesen wird: https://www.arduino.cc/reference/en/libraries/ds3231/. Es enthält einige Tricks, deren Beherrschung sich durchaus lohnen kann. Sie können diese Bibliothek mit dem Bibliotheksmanager (Extras > Bibliotheken verwalten...) in Ihre Arduino-IDE importieren.
Gehen Sie Schritt für Schritt vor. Dieses Tutorial demonstriert die folgenden Schritte:
Wenn Sie möchten, dass der spezielle Code mehr als einmal in von Ihnen festgelegten Intervallen ausgeführt wird, kann Ihr Code Schritt 3 erneut ausführen und einen neuen Alarm festlegen. Die diesem Tutorial beiliegende Beispielskizze unterbricht den Arduino wiederholt in 10-Sekunden-Intervallen.
Dieses Tutorial basiert auf „offiziellen“ Referenzen, darunter:
attachInterrupt()
: https://www.arduino.cc/reference/en/sprache/funktionen/external-interrupts/attachinterrupt/. Der spezielle Code in diesem Beispiel ist trivial: Er gibt nur die aktuelle Uhrzeit vom DS3231 aus. Ein Beispiel aus dem wirklichen Leben könnte etwas Nützliches tun, wie zum Beispiel eine Pflanze gießen oder eine Temperaturmessung protokollieren. Was auch immer die Aufgabe ist, der Code sollte in einen eigenen, speziellen Block verschoben werden, der nur dann ausgeführt wird, wenn der DS3231-Alarm den Arduino unterbricht.
Ich halte es für eine gute Praxis, Code in kurze Funktionen zu unterteilen, wobei jede Funktion nur eine Aufgabe oder einen Satz verwandter Aufgaben erledigt. Der Name einer Funktion kann beliebig sein; Warum lässt man es nicht beschreiben, was die Funktion tut? Hier ist ein Teil meiner Funktion, die den speziellen Code in diesem Beispiel ausführt.
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
}
Sie werden Drähte zwischen fünf Stiftpaaren verlegen. Jedes Paar erfüllt einen elektrischen Zweck und verbindet einen Pin am Arduino mit einem entsprechenden Pin am DS3231. Gehen Sie es langsam an, schließen Sie jedes Paar an und überprüfen Sie dann beide Enden, um sicherzustellen, dass jeder Draht dort verläuft, wo er hingehört. Die Tabelle listet die Paare in der Reihenfolge auf, in der sie von links nach rechts von einem Arduino Uno an den DS3231 angeschlossen werden.
Zweck | DS3231-Pin | Arduino-Pin |
---|---|---|
Alarm | SQW | 3* |
SCL | SCL | SCL** |
SDA | SDA | SDA** |
5 Volt Strom | VCC | 5V |
Boden | GND | GND |
Wie gesagt, nehmen Sie sich Zeit, diese Verbindungen herzustellen. Langsam und sicher ist oft der schnellste Weg, etwas richtig zu erledigen.
Wir werden mit dem DS3231-Modul über ein DS3231-„Objekt“ kommunizieren, eine Art Software-Toolbox mit einem Namen darauf. Die DS3231-Bibliothek definiert viele Funktionen – betrachten Sie sie als Werkzeuge – in der Box. Wenn wir eine Funktion verwenden möchten, schreiben wir den Namen der Toolbox, gefolgt von einem Punkt und dann dem Namen des Tools. Der Beispiel-Sketch erstellt zu diesem Zweck eine Variable „clock“. Anschließend kann die Skizze auf die Werkzeuge im Feld „Uhr“ zugreifen. Alle Tools sind in der oben erwähnten Datei DS3231.h deklariert.
#include <DS3231.h>
DS3231 clock;
Serial.println(clock.getMinute()); // the current minute, 0..59
Wir werden Werkzeuge in unserem „Uhr“-Objekt verwenden, um den Alarm einzustellen. Aber zuerst müssen wir die Alarmzeit berechnen.
Bei diesem Schritt wird davon ausgegangen, dass Sie zuvor die tatsächliche Uhrzeit auf dem DS3231 eingestellt haben. Die Beispielskizze enthält Code, mit dem Sie bei Bedarf die Uhrzeit einstellen können. Entfernen Sie einfach die Kommentartrennzeichen /* und */, die den Kommentar umgeben.
Die Beispielskizze in diesem Tutorial berechnet eine Alarmzeit in der Zukunft, indem zur aktuellen Zeit ein Intervall in Sekunden addiert wird. Das Beispiel fügt zehn Sekunden hinzu. Eine Minute würde 60 Sekunden hinzufügen. Eine Stunde würde 3.600 Sekunden hinzufügen. Ein Tag, 86.400 Sekunden. Und so weiter.
Die DS3231-Bibliothek verfügt über einen versteckten „Trick“, der es einfach macht, die Zeit als Anzahl von Sekunden hinzuzufügen. Dieser Trick ist nicht unter den verfügbaren Funktionen auf der README-Seite der DS3231-Bibliothek aufgeführt. Es ist auch nicht ganz offensichtlich, wenn man sich die Datei DS3231.h ansieht. Einige Details warten darauf, in der Codedatei DS3231.cpp gefunden zu werden. Hier sind die Schritte zur Durchführung der Berechnung.
now()
, auf die auf besondere Weise zugegriffen werden muss.Die Schritte 4 und 5 können kombiniert werden.
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
Obwohl das alarmTime-Objekt auf der Grundlage einer Anzahl von Sekunden erstellt wird, bietet es in seiner Toolbox Tools zum Ausdrücken von Jahr, Monat, Tag, Stunde, Minute und Sekunde. Wir stellen die Alarmzeit auf dem DS3231 ein, wie im Folgenden beschrieben, und verwenden dabei das alarmTime-Objekt als Quelle der benötigten Werte.
Nehmen wir zum Beispiel an, dass die vom DS3231-Modul gemeldete aktuelle Zeit am Mittwoch, dem 27. Oktober 2021, 7 Sekunden nach 10:42 Uhr morgens lag. Die oben berechnete alarmTime wäre am selben Tag zehn Sekunden später 10:42:17 Uhr.
Ein DS3231 stellt zwei verschiedene Alarme zur Verfügung: Alarm Nr. 1 (A1) und Alarm Nr. 2 (A2). Beide Alarme können auf einen Tag und eine Uhrzeit genau bis auf eine Minute genau festgelegt werden. Der Unterschied besteht darin, dass A1 bis auf eine Sekunde weiter spezifiziert werden kann. Jeder Alarm verfügt über ein eigenes Funktionspaar in der DS3231-Bibliothek zum Einstellen der Alarmzeit und zum Lesen dieser Zeit. Der Zugriff auf die Funktionen erfolgt alle über ein DS3231-Objekt, zum Beispiel das Objekt mit dem Namen „clock“:
Clock.setA1Time(), Clock.getA1Time(), Clock.setA2Time() und Clock.getA2Time()
Die Funktion setA1Time() benötigt acht Parameter, wie in diesem Zitat aus der Datei DS3231.h aufgeführt:
void setA1Time(byte A1Day, byte A1Hour, byte A1Minute, byte A1Second, byte AlarmBits, bool A1Dy, bool A1h12, bool A1PM);
Eine genaue Lektüre der Header-Datei DS3231.h kann die Parameter erklären. Was hier folgt, sind meine Versuche, sie mir selbst mit meinen eigenen Worten noch einmal zu erklären. Wenn der Leser eine Diskrepanz zwischen meiner Version und der Header-Datei feststellt, gehen Sie davon aus, dass der Header korrekt ist.
Die ersten fünf Parameter sind vom Typ „Byte“. Die cppreference-Website definiert den Bytetyp folgendermaßen: https://en.cppreference.com/w/cpp/types/byte:
std::byte ist ein eindeutiger Typ, der das Byte-Konzept implementiert, wie in der C++-Sprachdefinition angegeben.
Wie char und unsigned char kann es verwendet werden, um auf Rohspeicher zuzugreifen, der von anderen Objekten belegt ist (Objektdarstellung), aber im Gegensatz zu diesen Typen ist es kein Zeichentyp und kein arithmetischer Typ. Ein Byte ist nur eine Ansammlung von Bits, und die einzigen dafür definierten Operatoren sind bitweise.
In dieser besonderen Situation können wir uns erlauben, uns die Byte-Variablen für Tag und Uhrzeit so vorzustellen, als wären sie vorzeichenlose Ganzzahlen. Sie können einen ganzzahligen Wert zwischen 0 und 255 enthalten . ACHTUNG: Der Codeschreiber muss unsinnige Werte vermeiden. Beispielsweise macht ein Wert von 102 für keinen dieser Parameter Sinn. Es ist Ihre Aufgabe als Codeschreiber, sinnvolle Werte bereitzustellen.
Fahren wir mit der im vorherigen Schritt erstellten alarmTime fort: am 27. Tag des Monats, 17 Sekunden nach 10:42 Uhr morgens. Die folgende Auflistung zeigt, wie Sie diese Werte in die Funktion eingeben können. Ich liste jeden Parameter in einer eigenen Zeile auf, um sie für Menschen besser lesbar zu machen und Platz für Kommentare zu schaffen. Das Beispiel hier ist unvollständig; Es zeigt nur die Byte-Werte für Datum und Uhrzeit. Die Funktion erfordert weitere Parameter, wie unten beschrieben, und wird nicht in der hier gezeigten Form ausgeführt.
Beachten Sie übrigens, dass die Variablen „clock“ und „alarmTime“ Objekte sind, also Software-Toolboxen. Wie Sie sehen, verwenden wir Tools aus den jeweiligen Toolboxen, um auf die in den Objekten enthaltenen Informationen zuzugreifen.
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
);
Der nächste Byte-Parameter namens AlarmBits ist eigentlich nur eine Ansammlung von Bits. Die Namen der Bits sind im DS3231-Datenblatt (auf Seite 11) definiert.
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
Zusammen bilden die Bits eine „Maske“ oder ein Muster, das dem DS3231 mitteilt, wann und wie oft ein Alarm signalisiert werden soll. Eine Tabelle auf Seite 12 des Datenblatts gibt die Bedeutung für verschiedene Sammlungen der Bits an. Basierend auf dieser Tabelle verwendet die Beispielskizze in diesem Tutorial die folgende Sammlung von Bits:
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
Diese Anordnung der Bits kann explizit im Code ausgedrückt werden: 0x00001110
. Es weist den DS3231 an, den Alarm immer dann zu signalisieren, wenn „die Sekunden übereinstimmen“, d. h. wenn der „Sekunden“-Wert der Alarmeinstellung mit dem „Sekunden“-Wert der aktuellen Zeit übereinstimmt.
Die letzten drei Parameter der Funktion setA1Time()
sind boolesche oder wahr/falsch-Werte. Sie geben dem DS3231 weitere Informationen zur Auswertung der Alarmeinstellung. Das folgende Codesegment zeigt den abgeschlossenen Aufruf von setA1Time() und setzt damit das oben begonnene Beispiel fort:
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.
);
In der Beispielskizze, in der wir den Alarm so eingestellt haben, dass er alle 10 Sekunden unterbrochen wird, sind nur die Parameter A1Second und AlarmBits von Bedeutung. Wir müssen sie jedoch alle bereitstellen, wenn wir die Funktion setA1Time()
aufrufen. Korrekte Werte sind nicht schwieriger anzugeben als Junk-Werte; wir könnten genauso gut vorsichtig mit ihnen umgehen.
Die Funktion setA2Time()
funktioniert ähnlich, jedoch ohne Parameter für Sekunden. Nehmen Sie sich etwas Zeit, um die Zeilen 119 bis 145 der Datei DS3231.h in der Bibliothek und die Seiten 11–12 im Datenblatt durchzugehen. Bleiben Sie geduldig bei diesen Referenzen, bis Sie darin die Informationen gefunden haben, die Sie zum Einstellen einer Weckzeit benötigen.
Nach dem Einstellen der Zeit muss der Sketch zusätzliche Maßnahmen ergreifen, um den Alarm im DS3231 zu aktivieren. Ich ermutige den Leser, konsequent einer dreistufigen Abfolge zu folgen, auch wenn ein Schritt aus irgendeinem Grund zu einem bestimmten Zeitpunkt weniger notwendig erscheint. Wenn Ihr Code fehlerhaft sein muss, lassen Sie ihn auf der sicheren Seite.
Für den Alarm A1 wären die Anweisungen in der DS3231-Bibliothek:
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
Für Alarm A2 ändern Sie einfach den Parameter auf 2. Beispiel: checkIfAlarm(2); // clear A2 flag bit in register 0Fh
.
Ein in diesem Repo von @flowmeter beschriebenes Problem betont, dass beide Alarmflags gelöscht werden müssen, bevor der DS3231 einen Alarm signalisieren kann. Um sicherzugehen, sollten Sie checkIfAlarm() zweimal aufrufen, einmal für jeden Alarm, auch wenn Sie nur einen der Alarme verwenden :
checkIfAlarm(1);
checkIfAlarm(2);
Warum sollten Code-Autoren einen Alarm „überprüfen“, von dem sie glauben, dass er derzeit kein Signal sendet? Der Grund dafür ist, dass die Funktion checkIfAlarm()
einen nicht offensichtlichen Nebeneffekt hat. Es löscht das Alarm-Flag-Bit. Wir verwenden die Funktion checkIfAlarm()
, da sie die einzige in der DS3231-Bibliothek ist, die den erforderlichen Vorgang ausführt.
Denken Sie darüber nach. Aus Gründen, die im Folgenden erläutert werden, benötigt die Arduino-Interrupt-Sensing-Hardware, dass die Spannung am SQW-Pin des DS3231 HIGH ist, bevor der Alarm auftritt. Das Alarmereignis ändert zwei Dinge im DS3231:
Der SQW-Pin bleibt LOW, solange eines dieser Alarm-Flag-Bits gesetzt bleibt. Solange ein Alarm-Flag-Bit im DS3231 den SQW-Pin auf LOW hält, kann der Arduino keine Alarme mehr erkennen. Die Alarmflag-Bits müssen beide gelöscht werden, damit der DS3231 eine HIGH-Spannung an seinem SQW-Alarmpin wiederherstellen kann. Weitere Informationen finden Sie in der Erläuterung der Bits 1 und 0 im „Statusregister (0Fh)“ auf Seite 14 des DS3231-Datenblatts.
Jeder Alarm verfügt über ein eigenes Alarm-Flag-Bit im DS3231. Jedes der Alarmflag-Bits kann den SQW-Pin auf LOW halten. Der DS3231 löscht ein Alarmflagbit nicht aus eigener Initiative. Es ist die Aufgabe des Codeschreibers, die Alarmflagbits zu löschen, nachdem der Alarm aufgetreten ist .
Ihre Hauptschleife muss die Zeit nicht messen. Es muss lediglich ein Flag überprüft werden, um festzustellen, ob ein Alarm aufgetreten ist. In der Beispielskizze ist dieses Flag eine boolesche Variable mit dem Namen „alarmEventFlag“:
if (alarmEventFlag == true) {
// run the special code
}
Meistens ist das Flag false und die Schleife überspringt den speziellen Code. Wie stellt die Skizze die Flagge auf? Drei Schritte:
bool alarmEventFlag = false;
void rtcISR() {alarmEventFlag = true;}
attachInterrupt()
alles zusammen. Das folgende Beispiel weist die Arduino-Hardware an, die Funktion rtcISR()
sofort auszuführen, wenn sie ein „FALLING“-Signal an einem bestimmten digitalen Pin erkennt.attachInterrupt(digitalPinToInterrupt(dataPin), rtcISR, FALLING);
Verwenden Sie aus tiefgründigen und verborgenen Gründen immer die Sonderfunktion digitalPinToInterrupt()
, wenn Sie die Pin-Nummer für einen Interrupt angeben. Ich überlasse es dem Leser als Übung, herauszufinden, warum wir diese Funktion benötigen.
Was ist ein FALLING-Signal? Dies bedeutet eine Änderung der Spannung von HIGH auf LOW, wie vom digitalen Pin des Arduino erkannt. Woher kommt die Spannungsänderung? Es entsteht am Alarm-Pin des DS3231-Moduls. Dieser Pin trägt die Bezeichnung SQW und gibt die meiste Zeit eine HOCHspannung ab, die in der Nähe des VCC-Versorgungspegels (dh 5 Volt am Uno) liegt. Ein Alarm veranlasst den DS3231, die Spannung am SQW-Pin auf LOW zu ändern. Der Arduino erkennt die vom SQW-Pin kommende Spannung und bemerkt die Änderung. Wir weisen den Arduino an, FALLING zu bemerken, da dieses Ereignis nur einmal pro Alarm auftritt, wohingegen der LOW-Pegel bestehen bleiben und den Arduino dazu bringen kann, viele Interrupts auszulösen.
Welcher Pin kann eine FALLENDE Spannungsänderung erkennen? Für Unos können Sie einen der Pins 2 oder 3 wählen. Für Leonardo kann es einer der Pins 0, 1 oder 7 sein. (Ja, ich weiß, Leonardo erkennt Interrupts auch an den Pins 2 und 3. Das sind jedoch so Leonardos I2C-Pins, was bedeutet, dass das DS3231-Modul sie verwenden würde. Ich beginne mit Pin 7 für Interrupts auf einem Leonardo.) Die Beispielskizze definiert eine dataPin-Variable und initialisiert ihren Wert auf 3 für die Ausführung auf einem Uno so:
int dataPin = 3;
Der spezielle Code kann auch eine neue Alarmzeit einstellen, wenn Sie den Zyklus wiederholen möchten. Beginnen Sie mit der Berechnung der neuen Weckzeit, wie in Schritt 3 beschrieben, und befolgen Sie die Schritte von dort aus.
Ich würde erwarten, dass die Beispielskizze eine Ausgabe erzeugt, die der Abbildung unten ähnelt, wenn sie auf einem Arduino Uno ausgeführt wird, der korrekt mit einem DS3231-Modul verbunden ist, so wie ich es in diesem Tutorial beschreibe. Seien Sie nicht überrascht, wenn die angezeigten Zeiten unterschiedlich sind. Sie sollten auf jeden Fall mit Ihren eigenen Zeiten arbeiten.