DS3231 RTC Modul

DS3231 Echtzeituhr-Modul

Auf Basis des Chips DS3231 werden günstige und sehr genaue RTC-Module (real time clock) zum Einsatz mit Microcontrollern angeboten. Sie versorgen z.B. Arduinos, die keinen WLAN-Chip und somit keinen Zugang zu einem Internet-Zeitserver haben, mit der aktuellen Zeit. Die Uhr läuft sehr präzise, weil die Schwingungen des Quarzes im DS3231 an die Umgebungstemperatur angepasst werden. Der dazu verwendete Sensor kann auch abgefragt werden, hat aber nur eine Genauigkeit von ±3 Grad – deutlich ungenauer als die speziellen Temperatursensoren DS18B20 oder DHT22.

Lt. Datenblatt beträgt die Abweichung bei „normaler” Umgebungstemperatur von 0-40 °C maximal ±1 Minute pro Jahr bzw. rund eine Sekunde alle sechs Tage – zum Datenloggen o.ä. sicher genau genug. Die Kommunikation erfolgt über das I2C-Protokoll, nutzt am ESP32 also die beiden GPIO-Pins 21 (SDA – Daten) und 22 (SCL – Takt; alternativ GPIO 17 für SDA und 16 für SCL am zweiten I2C-Bus). Die Uhr wird über eine Knopfzelle versorgt, die je nach Modell mehrere Jahre halten soll.

Verschiedene DS3231 Modultypen

Anschlüsse des DS3231-Moduls
Anschlüsse des DS3231-Moduls

Es gibt mehrere Modultypen: das hier gezeigte ist sehr kompakt mit Abmessungen von ca. 1,3 cm * 1,3 cm * 1,3 cm (die Höhe wird maßgeblich von den ca. 8 mm hohen Anschlussbuchsen bestimmt). Bei Nutzung am Raspberry Pi kann es direkt auf dessen GPIO-Leiste gesteckt werden, andernfalls benötigt man vier Kabel (für VCC, GND und die beiden erwähnten I2C-Anschlüsse). Das Modul hat die I2C-Adresse 0x68 und wird von einer angelöteten 3 Volt-Knopfzelle versorgt – Typ CR927. [1]Laut Kundenantwort bei Amazon. Ich habe noch nicht nachgeguckt und werde das erst tun, wenn die Batterie den Geist aufgibt – was hoffentlich erst in ein bis zwei Jahren der Fall ist.

Ein anderes häufig angebotenes Modul besteht aus einer etwas größeren Platine (ca. 4 cm * 2 cm) mit einem Batteriefach für Standard-Knopfzellen vom Typ CR2032 [2]Bei einer Versorgungsspannung von 5 Volt (wie am Arduino) versucht das Modul die Batterie zu laden (siehe z.B. diese Diskussion im Arduino-Forum). Standardknopfzellen vertragen das nicht (Zerstörung … Continue reading und Anschlusspins statt Buchsen (Bild bei AZ-Delivery ). Die I2C-Adresse ist 0x57 und kann durch ein Adressfeld mit drei Kontakten angepasst werden. Außerdem ist ein EEPROM AT24C32 zum Speichern kleiner Datenmengen vorhanden (32 Kbit bzw. 4 KByte Speicher), um das Modul z.B. als Datenlogger einzusetzen. Eine ausführliche Beschreibung des Adressfelds und des AT24C32-Speichers gibt es auf lastminuteengineers.com. Neben diesen beiden sehr preiswerten RTC-Modulen (ca. 1 bis 3 €) gibt es weitere, die z.B. an bestimmte Microcontroller-Boards angepasst sind.

Wegen des größeren Funktionsumfangs und der leicht austauschbaren Batterie würde ich bei einem Neukauf das zweite Modul wählen, es sei denn minimaler Platzbedarf ist von Bedeutung. Ich habe nun aber das kleine Modul und mache, was damit geht, nämlich die Zeit zu stellen und sie auszulesen, um die Systemzeit im ESP32 zu setzen.

Bibliothek RTClib

Als Bibliothek nutze ich die RTClib von Adafruit, die man über die Bibliotheksverwaltung der Arduino-IDE (Suche nach „rtclib”) oder nach dem Download von der GitHub-Seite installieren kann. Es gibt aber viele Alternativen, wenn man in der Bibliotheksverwaltung nach „rtc” oder „ds3231” sucht, z.B. die Bibliothek Rtc by Makuna von Michael Miller (Github). Sie unterstützt neben dem DS3231 auch das oben erwähnte AT24C32-EEPROM und andere RTC-Chips und hat eine ordentlich aussehende Dokumentation und Beispielprogramme.

In einem mit der Bibliothek installierten Beispielprogramm (Menü Datei → Beispiele → RTClib → ds3231) wird gezeigt, wie man die Zeit einstellen kann, wenn man (z.B. am Arduino) keinen Zugriff auf einen NTP-Server hat – man nutzt den Zeitstempel im Programm, der den Zeitpunkt des Kompilierens in der Arduino-IDE angibt. Lädt man das Programm sofort hoch, hat man nur eine Differenz von wenigen Sekunden zur tatsächlichen Zeit auf dem Rechner, an den der Microcontroller angeschlossen ist.

    // following line sets the RTC to the date & time this sketch was compiled
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

Genauer ist es, die Zeit von einem NTP-Server zu holen – Details dazu sind im Beitrag zum TM1637-Modul beschrieben. Eine weitere Möglichkeit zeigt das zweite Programm – das manuelle Setzen der Zeit per Eingabe im seriellen Monitor der Arduino-IDE.

Einstellen der Zeit per NTP-Server

Im Programm müssen SSID (Name) und Passswort für das WLAN gesetzt werden. Im Setup wird geprüft, ob die RTC bereits gestellt ist. Falls nicht, z.B. bei der ersten Inbetriebnahme oder nach einem Batteriewechsel, werden die Systemzeit des ESP32 und die RTC per NTP-Server-Abfrage gestellt. Andernfalls wird nur die Systemzeit nach der RTC gesetzt. Will man die Echtzeituhr auf jeden Fall stellen, lässt man einfach die entsprechende If-Abfrage if (rtc.lostPower()) im Programm weg bzw. ersetzt sie durch if (true).

Die Programmschleife zeigt alle 10 Sekunden die Zeit der Echtzeituhr (UTC) und die lokale Uhrzeit (Systemzeit anhand der eingestellten Zeitzone) sowie die vom internen Sensor gemessene Temperatur an.

/* DS3231 RTC - Zeit per NTP setzen und aus der RTC auslesen
 *
 * 2019-07-21 Heiko (unsinnsbasis.de)
 */
 
#include <Wire.h>
#include "RTClib.h"
#include <WiFi.h>
#include "time.h"
#include <sys/time.h>
 
RTC_DS3231 rtc;
struct tm tdata;  // lokale Zeit
 
const char* ssid = "Deine_WLAN_SSID";
const char* password = "Dein_WLAN_Passwort";
const char* ntp_server = "fritz.box"; // oder (de/at/ch).pool.ntp.org
 
// lokale Zeitzone definieren
// https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
#define TIMEZONE "CET-1CEST,M3.5.0/02,M10.5.0/03"
 
char days_of_week[7][11] = {"Sonntag", "Montag", "Dienstag", "Mittwoch",
        "Donnerstag", "Freitag", "Samstag"};
 
void setup () {
  DateTime now;
  timeval tv;
  int rc;
 
  Serial.begin(115200);
 
  if (! rtc.begin()) {
    Serial.println("Keine Echtzeituhr gefunden!");
    while (1);
  }
 
  if (rtc.lostPower()) {
    Serial.println("RTC wird neu gestellt");
 
    // mit dem WLAN verbinden; nach Ermitteln der Zeit kann die
    // Verbindung wieder getrennt werden
    Serial.printf("Verbindung herstellen mit %s ", ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
    }
    Serial.println(" Verbunden!");
 
    // Zeit vom NTP-Server holen
    configTime(0, 0, ntp_server);
    // vor Trennung des WLANs einmal getLocalTime() aufrufen
    getLocalTime(&tdata);
    setenv("TZ", TIMEZONE, 1);  // Zeitzone einstellen
 
    // WLAN trennen
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
 
    // RTC anhand der Systemzeit stellen
    rtc.adjust(time(NULL));
  } else {
    // (System-)Zeit aus der RTC übernehmen
    now = rtc.now();
    tv.tv_sec = now.unixtime();
    tv.tv_usec = 0;  // keine Mikrosekunden setzen
    rc = settimeofday(&tv, NULL);  // NULL = keine Zeitzoneninfo an dieser Stelle ...
    setenv("TZ", TIMEZONE, 1);  // ... sondern hier: Zeitzone einstellen
    Serial.printf("Returncode beim Setzen der Systemzeit nach der RTC: %d\n\n", rc);
  }
}
 
void loop () {
    DateTime now = rtc.now();
 
    Serial.printf("RTC Unix-Zeit - Datum TT.MM.JJJJ und Zeit: %s %02d.%02d.%d %02d:%02d:%02d\n",
        days_of_week[now.dayOfTheWeek()],
        now.day(), now.month(), now.year(), 
        now.hour(), now.minute(), now.second());
    Serial.printf("RTC Unix-Zeit seit 1.1.1970 - %d Sekunden bzw. %d Tage\n", 
        now.unixtime(), now.unixtime() / 86400L);
    getLocalTime(&tdata);
    Serial.printf("Systemzeit: time() = %d\n", time(NULL));
    Serial.printf("Lokale Uhrzeit (aus Systemzeit): %02d:%02d:%02d\n", 
        tdata.tm_hour, tdata.tm_min, tdata.tm_sec);
    Serial.printf("RTC-Temperatur: %2.1f °C\n\n",
        rtc.getTemperature());
    delay(10000);
}

Wurde die Uhr vorher nicht noch nicht gestellt, verbindet sich der ESP32 mit dem WLAN . Das Programm macht folgende Ausgabe (Screenshot):

RTC wird neu gestellt
Verbindung herstellen mit HS Gastzugang ..... Verbunden!
RTC Unix-Zeit - Datum TT.MM.JJJJ und Zeit: Samstag 03.08.2019 16:24:29
RTC Unix Zeit seit 1.1.1970 - 1564849469 Sekunden bzw. 18111 Tage
Systemzeit: time() = 1564849469
Lokale Uhrzeit (aus Systemzeit): 18:24:29
RTC-Temperatur: 26.8 °C

RTC Unix-Zeit - Datum TT.MM.JJJJ und Zeit: Samstag 03.08.2019 16:24:39
[...]

Bei einem späteren Durchlauf mit bereits gestellter Uhr sieht die Ausgabe so aus:

Returncode beim Setzen der Systemzeit nach der RTC: 0

RTC Unix-Zeit - Datum TT.MM.JJJJ und Zeit: Samstag 03.08.2019 16:43:47
RTC Unix Zeit seit 1.1.1970 - 1564850627 Sekunden bzw. 18111 Tage
Systemzeit: time() = 1564850627
Lokale Uhrzeit (aus Systemzeit): 18:43:47
RTC-Temperatur: 26.2 °C

Manuelle Eingabe der Zeit im seriellen Monitor

DS3231 am ESP32
DS3231 am ESP32

Außerdem kann man auch Daten über die serielle Schnittstelle (den COM-Port, der auf dem PC dem USB-Anschluss zugewiesen wird) zum ESP32 übertragen – in der Arduino-IDE nutzt man für Tastatureingaben den seriellen Monitor.

Das folgende Programm zeigt die in der RTC gespeicherte Zeit an und wartet auf eine Eingabe. Zwei Eingabeformate sind möglich: JJJJ-MM-TT HH:MM:SS, z.B. „2019-07-26 18:00:00” für den 26. Juli 2019 18 Uhr, und HH:MM:SS. Im zweiten Fall wird nur die Uhrzeit neu gestellt, das Datum bleibt unverändert. Die Sekunden können jeweils weggelassen werden (dann werden sie auf Null gesetzt), und die Trennzeichen (Minuszeichen, Doppelpunkte, Leerzeichen …) sind frei wählbar. Wichtig ist nur, dass das Jahr vierstellig und alle anderen Zahlen zweistellig eingegeben werden mit jeweils genau einem Zeichen dazwischen. Im Programm findet keine Prüfung der Eingabe statt – wer Buchstaben, ungültige Datumswerte oder zu viele Trennzeichen eingibt, hat selber Schuld. 😉 Die Eingabe kann man so oft wiederholen, bis die gewünschte Zeit eingestellt ist. Die Programmschleife zählt die Sekunden hoch und zeigt alle 10 Sekunden die Zeit als UTC und Ortszeit an.

Die Eingabezeile befindet sich am oberen Rand des seriellen Monitors. Damit die Enter-Taste (Zeilenvorschub) als Ende der Eingabe erkannt wird, muss unten in der Auswahlbox „Neue Zeile” eingestellt sein. Außerdem muss – wie immer bei Nutzung des seriellen Monitors – die dort eingestellte Übertragungsrate mit dem im Programm per Serial.begin() eingestellten Wert übereinstimmen; in diesem Fall 115200.

DS3231: manuelle Eingabe der Zeit im seriellen Monitor
DS3231: manuelle Eingabe der Zeit im seriellen Monitor
/* DS3231 RTC - Eingabe der Zeit im seriellen Monitor
 *
 * 2019-07-26 Heiko (unsinnsbasis.de)
 */
 
#include <sys/time.h>
#include "RTClib.h"
 
// lokale Zeitzone definieren
// https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
#define TIMEZONE "CET-1CEST,M3.5.0/02,M10.5.0/03"
 
RTC_DS3231 rtc;
time_t tm_before = 0;
const char * init_msg = "Datum/Zeit im Format YYYY-MM-DD HH:MM:SS eingeben\n"
          "oder Zeit HH:MM:SS (lässt das Datum unverändert).\n"
          "(Trennzeichen beliebig; Sekunden optional)";
 
void setup() {
  DateTime now;
  timeval tv;
  int rc;
 
  Serial.begin(115200);
 
  if (! rtc.begin()) {
    Serial.println("Keine Echtzeituhr gefunden!");
    while (1);
  }
 
  if (rtc.lostPower()) {
    Serial.println("RTC muss gestellt werden.");
  } else {
    // Systemzeit aus RTC übernehmen
    now = rtc.now();
    tv.tv_sec = now.unixtime();
    Serial.printf("Unix-Zeit aus RTC: %d\n", tv.tv_sec);
    tv.tv_usec = 0;  // keine Mikrosekunden setzen
    rc = settimeofday(&tv, NULL);  // NULL = keine Zeitzoneninfo an dieser Stelle ...
    Serial.printf("Zeit wurde aus RTC übernommen; Returncode: %d\n\n", rc);
  }
  setenv("TZ", TIMEZONE, 1);  // Zeitzone einstellen
  Serial.println(init_msg);
}
 
void loop() {
  String str;
  int year, mon, day, hour, min, sec, rc;
  time_t tm_now;
  struct tm *tms;
  timeval tv;
  DateTime now;
 
  delay(200);  // 5 mal pro Sekunde auf Eingabe warten reicht
  tm_now = time(NULL);
  if (tm_now > tm_before) {
    // ist eine neue Sekunde angebrochen? -> Zeit anzeigen
    tm_before = tm_now;
    if (tm_now % 10 == 0) {
      // alle 10 s die aktuelle Zeit ausgeben
      Serial.print("\nGMT/UTC:  ");
      Serial.print(asctime(gmtime(&tm_now)));
      Serial.print("Ortszeit: ");
      Serial.print(asctime(localtime(&tm_now)));
    } else {
      // ansonsten nur die Sekunden ausgeben
      Serial.printf("...%1d ", tm_now % 10);
    }
  }
  if (Serial.available() > 0) {
    str = Serial.readStringUntil('\n');
    if (str.length() < 5) {
      Serial.printf("\nFalsche Eingabe.\n");
    } else {
      if (str.length() < 9) {  // zwischen 5 und 8 Zeichen, also kein Datum, nur Zeit
        hour = str.substring(0,2).toInt();
        min = str.substring(3,5).toInt();
        sec = 0;
        if (str.length() > 6) {
          sec = str.substring(6,8).toInt();
        }
        // Datum aus Systemzeit übernehmen
        tms = gmtime(&tm_now);
        year = tms->tm_year+1900;
        mon  = tms->tm_mon+1;
        day  = tms->tm_mday;
      } else {
        // komplettes Datum mit Zeit eingegeben
        year = str.substring(0,4).toInt();
        mon  = str.substring(5,7).toInt();
        day  = str.substring(8,10).toInt();
        hour = str.substring(11,13).toInt();
        min  = str.substring(14,16).toInt();
        sec = 0;
        if (str.length() >= 19) {
          sec = str.substring(17,19).toInt();
        }
      }
      Serial.printf("\nJahr = %04d, Monat = %02d, Tag = %02d\n",
          year, mon, day);
      Serial.printf("Stunde = %02d, Minute = %02d, Sekunde = %02d\n",
          hour, min, sec);
      // RTC und Systemzeit setzen
      rtc.adjust(DateTime(year, mon,day, hour, min, sec));
      now = rtc.now();
      Serial.printf("RTC Unix-Zeit: %d\n", now.unixtime());
      tv.tv_sec =  now.unixtime();
      tv.tv_usec = 0;  // keine Mikrosekunden setzen
      rc = settimeofday(&tv, NULL);  // NULL = keine Zeitzoneninfo an dieser Stelle
      Serial.printf("Returncode beim Setzen der Systemzeit: %d\n", rc);
      tm_before =  now.unixtime();
      Serial.println("Für eine erneute Korrektur der Zeit:");
      Serial.println(init_msg);
    }
  }
}

Datenblätter

  • DS3231: Produktseite des Herstellers Maxim Integrated mit Link zum Datenblatt (PDF)
  • AT24C32: PDF zum in einigen Modulen verbauten EEPROM

Fußnoten

Fußnoten
1 Laut Kundenantwort bei Amazon. Ich habe noch nicht nachgeguckt und werde das erst tun, wenn die Batterie den Geist aufgibt – was hoffentlich erst in ein bis zwei Jahren der Fall ist.
2 Bei einer Versorgungsspannung von 5 Volt (wie am Arduino) versucht das Modul die Batterie zu laden (siehe z.B. diese Diskussion im Arduino-Forum). Standardknopfzellen vertragen das nicht (Zerstörung droht)! In der Diskussion ist ein Bild verlinkt, auf dem man erkennen kann, welche Leitung auf der Platine durchtrennt werden muss, damit die Batterie nicht geladen wird. Alternativ kann man einen wiederaufladbaren LIR2032-Akku verwenden oder das Modul mit 3,3 Volt betreiben – am ESP32 sollten also keine Probleme auftreten.

4 Kommentare

  1. Hallo
    erstmal vielen Dank!!!
    deine 2 sketch funktionieren wunderbar am ESP32

    – jedoch nur mit den standart GPIO-Pins 21 (SDA – Daten) und 22 (SCL – Takt)
    – alternativ GPIO 17 für SDA und 16 für SCL am zweiten I2C-Bus – funktioniert hingegen leider nicht.

    muss ich irgendwo was ändern um den 2ten I2C Bus SDA=17 und SCL=16 vom ESP32 nutzen zu können.

    mfg Rico

  2. Hi
    hat sich erledigt.
    Habe es hinbekommen den alternativen i2c Bus zum laufen zu bekommen.

    Bitte oben einfügen

    #define SDA 17
    #define SCL 16

    und

    Serial.begin(115200);

    if (! rtc.begin()) {

    in

    Serial.begin(115200);
    Wire.begin(SDA, SCL);
    if (! rtc.begin()) {

    ändern

  3. Hallo

    habe es hinbekommen läuft jetzt auch mit alternativeb I2C Bus.

    #define SDA 17
    #define SCL 16

    am anfang einfügen (lief auch mit z.b. GPIO 32 und 33)

    und am anfang vom void setup bereich noch Wire.begin(SDA, SCL); einfügen.

    also etwa so

    void setup () {
    Wire.begin(SDA, SCL);
    DateTime now;

    usw…………

    das war es auch schon

    mfg Rico

    1. Moin Rico,

      danke für die Frage und vor allem, dass du dann auch die erfolgreiche Lösung nachgereicht hast. Der Beitrag ist schon etwas älter, deswegen fehlt der Hinweis auf den Text zum I2C-Interface (https://unsinnsbasis.de/i2c-schnittstelle-esp32-arduino/). Ich nehme das zum Anlass, das demnächst zu ergänzen (und noch ein paar andere Dinge zu ändern).
      Das betrifft vermutlich auch noch andere Beiträge, die untereinander besser verlinkt sein könnten und um die mich dann in der nächsten Zeit auch kümmern werde.

      Viele Grüße,
      Heiko

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht.