D1 Mini Datenlogger – Teil 2: SD Karte und Realtime Clock

Features: RTC, WLAN, SD-Karte, Temperatur-, Luftdruck- und Luftfeuchtigkeitssensor und OLED-Display, Solarzelle

In diesem Beitrag wollen wir das bereits erstellte Datenlogger Modul um die Echtzeituhr (Real-Time-Clock) erweitern. Zusätzlich sollen die Messwerte auf einer Mikro-SD-Karte gespeichert werden.

In dieser Beitragsserie wollen wir einen Datenlogger bauen der Temperatur, Luftfeuchtigkeit und Luftdruck in eine .csv-Datei auf eine SD-Karte schreibt und nebenbei auch noch mobil von einer Solarzelle versorgt wird. In Teil 1 wird der Sensor und das LCD Display eingebunden. Anschließend folgt in Teil 2 die Erweiterung um eine Real-Time-Clock für präzise Zeitstempel und die SD-Karte für langfristiges Speichern der Messwerte. In Teil 3 machen wir die Anwendung mittels Solarzelle mobil und optimieren den Stromverbrauch. Dann folgt in Teil 4 noch das zusätzliche Senden der Daten an einen Datenserver und abschließend stellen wir diese Daten in Teil 5 noch grafisch dar.

Bauteile für SD Karte und Realtime Clock

Folgende Bauteile wurden für diesen Beitrag verwendet / genauer betrachtet:

Data logger shield mit Real-Time-Clock DS1307gibt’s bei ebay*
Übersicht verwendete Bauteile

Data logger shield

realtime clock
SD/RTC-Shield

Das verwendete Shield enthält sowohl die Real-Time-Clock (RTC) als auch die Mikro-SD-Karte.

Die RTC basiert auf dem DS1307 Chip und wird über I2C angesprochen. Zwar handelt es sich bei diesem Chip leider um einen nicht sehr präzisen bzw. langzeitstabilen. Er benötigt nämlich einen externen Oszillator und bei diesen ist die Frequenz in der Regel temperaturabhängig (s. Hinweis im Datenblatt auf Seite 7). Die Uhrzeit driftet also im Laufe der Zeit etwas. Wir verwenden dieses Modul aufgrund des schönen Huckepack-Formfaktors trotzdem :). Wer es genauer braucht kann sich z.B. eine RTC auf Basis des DS3231 anschauen. Dieser Chip verwendet einen temperaturkompensierten Oszillator und gibt die Frequenzstabilität mit +/- 2 ppm im Bereich von 0 bis 40°C an:

Frequenzstabilität beim DS3231 im Vergleich zu unkompensierten Oszillatoren. Quelle: Datenblatt des DS3231

Die SD-Karte wird über SPI angesprochen.

ACHTUNG: Ich hatte bei SD-Karten > 16 GB das Phänomen, dass sich der D1 mini „komisch“ verhalten hat. Betätigen des Reset Tasters bieb beispielsweise ohne Effekt, so dass z.B. das Display bzw. generell die setup()/Initialisierungsroutine ordnungsgemäß durchgelaufen ist. Ich konnte die Ursache nicht eindeutig ermitteln, aber der Zusammenhang war eindeutig auf die SD-Karte zurückzuführen. Sobald ich z.B. die SD-Karte einfach nur rausgezogen habe war das Verhalten wieder „normal“.

Tipp: Am besten mehrere verschiedene SD-Karten bereit halten. Falls etwas nicht klappt kann es nämlich auch einfach an der Karte liegen. So lief es bei mir mit der 2GB Karte nämlich auch => Schrott 🙂 Am Ende habe ich eine neue 16 GB eingelegt, da die zwischenzeitliche 64 GB Karte auch nicht beschrieben werden konnte (aber zumindest nicht schrott war).

Schaltplan

Fritzing-Schaltplan vom Testaufbau. Im Vergleich zu Teil 1 ist nur das Shield dazugekommen. Die Abbildung vom Shield ist leicht anders als das tatsächlich verwendete Shield.
Modul PinVerbunden mit
VCC3,3V
GNDGND
CLKD5
MISOD6
MOSID7
SCKD8
Pinbelegung SD-Karte (SPI)
Modul PinVerbunden mit
VCC3,3V
GNDGND
SCLD1
SDAD2
Pinbelegung RTC (I2C)

Software

Nun bauen wir das Programm schrittweise auf. In diesem Teil des Blogs kommt zunächst die Auswertung des Sensors und das Anzeigen der Werte auf dem Display. Für die ungeduldigen hier der vollständige Code des gesamten Projekts auf github. Dies beinhaltet, aber bereits den letzten Stand des gesamten Projekts und ist deshalb abweichend und umfangreicher als das u.g. Aber natürlich kann man sich dort auch das rauspicken, was man gerade benötigt.

Benutzte Bibliotheken

  • SD.h (System-Lib ESP)
  • RtcDS1307 (V2.3.7 verwendet) github

Code

Für diesen Teil wurde aus Übersichtlichkeitsgründen im setup() die separaten Module bzw. Funktionen in einzelne „Unter“-setupXXX()-Funktionen ausgegliedert. Also z.B. setupDisplay() für den setup-Teil des Displays.

Auf der SD-Karte wird im Messinterval jeweils eine Zeile mit Messwerten in der Log-Datei im .csv Format abgelegt. Damit kann man später z.B. in Excel sehr leicht weitere Auswertungen machen. Die Datei wird bei jedem Schreibvorgang im FILE_WRITE Mode geöffnet. Damit werden neue Daten angehängt ohne den vorherigen Dateiinhalt zu überschreiben. Dann wird eine Zeile mit Messdaten kommasepariert geschrieben und direkt nach dem Schreibvorgang wird die Datei wieder geschlossen. Man könnte die Datei prinzipiell auch geöffnet lassen, aber um das Risiko von Datenverlust zu minimieren soll die Zeitspanne möglichst kurz gehalten werden. Damit man später noch weiß welche Spalte welche Bedeutung hat wird im setupSD() eine Textdatei (loggerDaten_beschreibung.txt) angelegt in der die Spaltenüberschriften niedergeschrieben werden.

#include <Wire.h> // fuer I2C
#include <ESP8266WiFi.h> // fuer WIFI aus- / einschalten
#include <Adafruit_BME280.h>

// lcd display include
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>

#include <SD.h> // fuer SD karte
#include <RtcDS1307.h> // rtc include https://github.com/Makuna/Rtc/blob/master/examples/DS1307_Simple/DS1307_Simple.ino

// fuer sd
#define CS D8

RtcDS1307<TwoWire> Rtc(Wire);
RtcDateTime now;

Adafruit_BME280 bme1;
float temp1(NAN), hum1(NAN), press1(NAN);

bool SDKarteError = false;    // sd karte nicht vorhanden oder nicht ok?
bool RTCError = false;        // rtc nicht vorhanden oder nicht ok? 
bool bme1SensorError = false; // bme sensor nicht vorhanden oder nicht ok?
bool displayError = false; // display initialisierung fehlgeschlagen?

// [ms] wie lange bleibt die anzeige an nach aktivierung ?
const unsigned long MS_ANZEIGEDAUER = 5000000; 

// [us] wie lange darf ich schlafen bevor ich wieder messe ?
const uint64_t US_SCHLAFINTERVAL = 10e6;

const String NAME_LOGDATEI = "loggerDaten";
const String NAME_LOGDATEI_CSV = NAME_LOGDATEI + String(".csv");

// lcd defines
#define OLED_RESET -1
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

//Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_SSD1306* displayPtr = NULL;
/* ACHTUNG: der display konstruktor allokiert dynamischen speicher,
 * da das display aber zur laufzeit an und ausgeschaltet werden soll
 * um strom zu sparen muss der dynamische speicher wieder freigegeben
 * werden. da die implementierung in der bibliothek fix ist muessen 
 * wir es bei uns loesen und da sind die new / delete operatoren
 * die einzige moeglichkeit die ich aktuell sehe. */

// Achtung: D4 ist auch der Pin der User LED
#define PIN_SCREEN_POWER D4

#define countof(a) (sizeof(a) / sizeof(a[0]))

void setupDisplay(bool periphBegin = false) {
    digitalWrite(PIN_SCREEN_POWER, HIGH); // display einschalten
    delay(100); // etwas zeit geben zum spannung stabilisieren (WICHTIG)
                // etwas grosszuegig, aber wir habens ja nicht eilig :)
    if (displayPtr) {
        delete displayPtr;
        displayPtr = NULL;
    }
    displayPtr = new Adafruit_SSD1306(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

    if (!displayPtr->begin(SSD1306_SWITCHCAPVCC, 0x3C, /*reset*/ true, /*periphBegin*/ periphBegin)){
        Serial.println("display init error!");
        displayError = true;
        return;
    }
    displayPtr->setTextSize(2);
    displayPtr->setTextColor(WHITE); // ohne Effekt -> das eingesetzte Display kann nur blau
    displayPtr->clearDisplay();
    displayPtr->setCursor(5, 0);
    displayPtr->setTextSize(2);
    displayPtr->println("Init");
    displayPtr->display(); // Text zeigen
}

void setupSd() 
{
    Serial.print("Initialisiere SD-Karte...");
    if (!SD.begin(CS)) {
        Serial.println(" fehlgeschlagen!");
        SDKarteError = true;
    }
    Serial.println(" fertig.");
    // Datei mit Schreibzugriff oeffnen bzw erzeugen wenn nicht vorhanden.
    File zielDatei = SD.open(NAME_LOGDATEI + String("_beschreibung.txt"), FILE_WRITE);
    
    if (zielDatei) { // oeffnen erfolgreich?
        Serial.print("Datei gefunden.");
        // Spaltenueberschrift eintragen als beschreibung fuer csv datei
        zielDatei.println("Datum;Uhrzeit;Temperatur;Feuchte;Druck;");
        zielDatei.close();
    } else { // fehler beim Zugriff die Datei...
        Serial.println("Fehler beim Oeffnen der Datei auf SD Karte :(");
        SDKarteError = true;
    }
}

void printDateTime(const RtcDateTime& dt)
{
    char datestring[20];
    snprintf_P(datestring, 
            countof(datestring),
            PSTR("%02u.%02u.%04u %02u:%02u:%02u"),
            dt.Day(),
            dt.Month(),
            dt.Year(),
            dt.Hour(),
            dt.Minute(),
            dt.Second() );
    Serial.print(datestring);
}

void setupRTC() 
{
    RtcDateTime compiled = RtcDateTime(__DATE__, __TIME__);
    Serial.print("Kompilierzeitstempel: ");
    printDateTime(compiled);
    Serial.println();

    if (!Rtc.IsDateTimeValid()) {
        if (Rtc.LastError() != 0) {
            // we have a communications error
            // see https://www.arduino.cc/en/Reference/WireEndTransmission for
            // what the number means
            Serial.print("RTC communications error = ");
            Serial.println(Rtc.LastError());
            RTCError = true;
        } else  {
            // Common Causes:
            //    1) first time you ran and the device wasn't running yet
            //    2) the battery on the device is low or even missing

            Serial.println("RTC lost confidence in the DateTime!");
            // following line sets the RTC to the date & time this sketch was compiled
            // it will also reset the valid flag internally unless the Rtc device is
            // having an issue

            Rtc.SetDateTime(compiled);
        }
    }
    if (!Rtc.GetIsRunning()) {
        Serial.println("RTC was not actively running, starting now");
        Rtc.SetIsRunning(true);
    }
    now = Rtc.GetDateTime();
    if (now < compiled) {
        Serial.println("RTC is older than compile time!  (Updating DateTime)");
        Rtc.SetDateTime(compiled);
    }
    else if (now > compiled) {
        Serial.println("RTC is newer than compile time. (this is expected)");
    }
    else if (now == compiled) {
        Serial.println("RTC is the same as compile time! (not expected but all is fine)");
    }
    // never assume the Rtc was last configured by you, so
    // just clear them to your needed state
    Rtc.SetSquareWavePin(DS1307SquareWaveOut_Low);
}

void setupBME280() 
{
    int i;
    for (i = 0; i < 5; i++) { // max 5x versuchen mit bme zu kommunizieren / initialisieren
        if (!bme1.begin()) { // KEIN erfolg?
            Serial.println("BME280 sensor nicht gefunden. Versuche es in 1s noch einmal.");
            delay(1000); // dann 1s warten
        } else {
            break; // nicht weiter probieren
        }
    }
    if (i >= 5) { // 5x ohne erfolg versucht zu kommunizieren?
        bme1SensorError = true;
        Serial.println("Konnte BME280 Sensor nicht finden!");
    } else {
        Serial.println("BME280 Sensor gefunden! ERFOLG!!");
    }
    if (!displayPtr) {
        Serial.println("init display");
        displayPtr = new Adafruit_SSD1306(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
    } else {
        Serial.println("error display ptr not NULL");
    }
}

void setup()
{
    Serial.begin(9600); // optional: aber praktisch fuer debug ausgaben ;)
    while (!Serial) ; 
    delay(200);
    Serial.println(">>>>>>>>>>>>>>>>>>>> Starte Datenlogger...");

    pinMode(PIN_SCREEN_POWER, OUTPUT); // spannungsversorgung vom display
    digitalWrite(PIN_SCREEN_POWER, HIGH); // display einschalten

    setupBME280();
    setupSd();
    setupRTC();
    setupDisplay(true);
    delay(200);
    displayPtr->clearDisplay();
    if (bme1SensorError) {
        displayPtr->setCursor(5, 0);
        displayPtr->println("Sensor-");
        displayPtr->setCursor(5, 30);
        displayPtr->print("fehler");
    } else {
        displayPtr->setCursor(5, 0);
        displayPtr->println("Hallo");
        displayPtr->setTextSize(2);
        displayPtr->setCursor(5, 30);
        displayPtr->print("Datenlogger");
    }
    displayPtr->display(); // Text zeigen
    WiFi.mode( WIFI_OFF );

    //digitalWrite(PIN_SCREEN_POWER, LOW); // display ausschalten -> strom sparen
    WiFi.forceSleepBegin(); 

    //ESP.deepSleep(10e6); // [us]
}

void readRTC() {
    if (!Rtc.IsDateTimeValid()) {
        if (Rtc.LastError() != 0) {
            // we have a communications error
            // see https://www.arduino.cc/en/Reference/WireEndTransmission for 
            // what the number means
            Serial.print("RTC communications error = ");
            Serial.println(Rtc.LastError());
            RTCError = true;
        }
        else {
            // Common Causes:
            //    1) the battery on the device is low or even missing and the power line was disconnected
            Serial.println("RTC lost confidence in the DateTime!");
        }
    }
    now = Rtc.GetDateTime();
}

void writeDataToSD() {
// Zeilenaufbau CSV Datei (comma-sperated-value) => excel format
// Datum;Uhrzeit;Temperatur;Feuchte;Druck;
// 01.09.22;18:10:23;23.2;41.23;1023.22;
  char buffer[80];

  sprintf(buffer, "%02d.%02d.%d;%d:%d:%d;%4.1f;%.0f;%.0f;", 
      now.Day(), now.Month(), now.Year(),
      now.Hour(), now.Minute(), now.Second(),
      temp1, hum1, press1);
    Serial.println(buffer);

    File zielDatei = SD.open(NAME_LOGDATEI_CSV, FILE_WRITE);
    
    if (zielDatei) { //existiert die Datei ?
        zielDatei.println(buffer);
        zielDatei.close();
    } else { // Dateifehler
        Serial.print("Fehler beim Schreiben auf SD-Karte");  
    }
}

void loop()
{
    static unsigned long millisLastRead = 0; // letztes sensor auslesen
    unsigned long curMillis = millis();

    // anzeigezeit abgelaufen ?
    if (curMillis > MS_ANZEIGEDAUER) {
        // dann display ausschalten
        //digitalWrite(PIN_SCREEN_POWER, LOW);
        Serial.println("stoppe anzeige. lege mich schlafen...");
        ESP.deepSleep(US_SCHLAFINTERVAL); // [us]
    } else { // anzeigezeit laeuft ? 
        // dann alle 2s werte anzeigen/schreiben
        if (curMillis - millisLastRead > 2000) {
            readRTC();
            if (!RTCError) {
                printDateTime(now); // RTC Zeit
                Serial.print(" | ");
            }
            if (!bme1SensorError) {
                readAndPrintBME280Data();
            }
            if (!RTCError && !bme1SensorError && !SDKarteError) {
                writeDataToSD();
            }
            millisLastRead = curMillis;
        }
    }
}

void readAndPrintBME280Data()
{
    temp1 = bme1.readTemperature();
    hum1 = bme1.readHumidity();
    press1 = bme1.readPressure() / 100;

    String Temperatur = String(temp1);
    Serial.print("Temp = " + Temperatur + " C | ");

    String feuchte = String(hum1);
    Serial.print("Feuchte = " + feuchte + " %RF | ");

    // ohne nachkommastellen darstellen und dabei richtig runden
    String druck = String(int(press1 + 0.5f));
    Serial.println("Druck = " + druck + " hPa");

    // lcd
    displayPtr->clearDisplay();
    displayPtr->setTextSize(2);
    displayPtr->setTextColor(WHITE);
    displayPtr->setCursor(1, 1);
    displayPtr->println(Temperatur + " C");

    displayPtr->setCursor(1, 25);
    displayPtr->println(feuchte + " % RF");

    displayPtr->setCursor(1, 50);
    displayPtr->println(druck + "  hPa");
    displayPtr->display(); // anzeige auftrag ausfuehren
}
Vollständig anzeigen

Ergebnis / Auswertung

Nach dem Aufspielen der Software wurde der Logger für einige Sekunden gestartet und der Inhalt der SD-Karte dann auf dem Rechner begutachtet.

Angelegte Dateien auf der SD-Karte

Und nun noch die Beispielauswertung mit Excel. Die Kurve zeigt den Verlauf nach dem der Sensor mal kurz angehaucht wurde 🙂

Excel Auswertung der .csv-Datei

Damit wären wir für diesen Teil fertig.


Beitrag veröffentlicht

in

von

Schlagwörter:

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert