Spuren hinterlassen – Datenlogging mit Arduino

Nicht nur beim Benutzen von Sensoren wollen manche Arduino-Projekte Daten dauerhaft protokollieren. Zeiterfassung könnte mit batteriegepufferten Echtzeituhren erfolgen. Zum persistenten Speichern eignen sich (Micro-)SD-Karten.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen
Lesezeit: 14 Min.
Von
  • Dr. Michael Stal
Inhaltsverzeichnis

Nicht nur beim Benutzen von Sensoren wollen manche Arduino-Projekte Daten dauerhaft protokollieren. Neben Messwerten und anderen Daten sollen die Logs Datums- und Zeitstempel enthalten. Zeiterfassung könnte mit batteriegepufferten Echtzeituhren erfolgen. Zum persistenten Speichern eignen sich größenbedingt (Micro-)SD-Karten. Der vorliegende Beitrag illustriert, wie sich den eigenen Projekten entsprechende Funktionalität hinzufügen lässt.

Es gibt separate SD-Karten- und RTC/Echtzeituhren-Breakout-Boards für den Arduino. SD-Karten-Leser wie den nachfolgend abgebildeten Reader erhalten Maker für wenige Euro im Online-Handel. Der Reader auf dem Foto kostet beispielsweise rund 3,68 Euro. Typischerweise bieten die Boards SPI zur Kommunikation mit dem Arduino und unterstützen Micro-SD-Karten mit maximal 2 GB Kapazität.

Da SD-Card-Reader 3.3V-Logik benötigen, befinden sich die entsprechenden 5V/3.2V-Level-Shifter bereits auf den Boards.

Die SD-Karten müssen dabei im Format FAT/FAT16 oder FAT32 vorliegen. Des Weiteren müssen sich darauf befindliche Dateien und Ordner an das 8.3-Format halten (etwa: LOGFILE1.TXT). Zu beachten: Bei Dateinamen kann das Dateisystem nicht zwischen Groß- und Kleinschreibung unterscheiden. Die Formatierung einer SD-Karte kann der Maker über ein entsprechendes Formatierungs-Programm auf dem Entwicklungsrechner durchführen. Die SD Association bietet hier ein solches Programm für verschiedene Betriebssysteme zum Herunterladen an.

Ein SD-Card-Reader für Micro-SDs mit 3.3V-Logik

(Bild: eckstein-shop.de)

Um Zeitstempel zur Verfügung zu haben, brauchen Maker eine Echtzeituhr (RTC = Real-Time Clock). Die sind über eine Knopfzelle vom Arduino unabhängig batteriegepuffert, um auch bei Stromausfall des Arduino-Boards noch Jahre lang zu funktionieren. Die Abbildung zeigt ein Breakout-Board mit I2C-Unterstützung und dem verbreiteten Baustein DS3231 bzw. DS1307.

Board mit Echtzeituhr

(Bild: smart-prototyping.com)

Ein solches Board ist inkl. Knopfzelle ab 6-7 Euro zu haben. Daneben bieten Händler auch Boards mit anderen Bausteinen an wie z.B. den PCF8523.

Da das gleichzeitige Verwenden von SD-Cards und einer Echtzeituhr zum Datenlogging sehr viel Sinn macht, existieren mittlerweile auch kombinierte Datalogger-Shields inklusive SD-Card-Reader und RTC. Für das Beispielsprojekt in dieser Folge kommt zum Beispiel das Datalogger-Shield von Adafruit zum Einsatz (siehe Abbildung).

Das Board ist zwar klobiger als separate RTC/SD-Card-Boards, lässt sich dafür aber huckepack als Shield auf einen Uno, Mega, Leonardo oder Due aufsetzen. Zudem unterstützt es auch die größeren SD-Karten. Micro-SDs sind natürlich ebenfalls über Adapter nutzbar. Dem Autor gefällt der Prototyping-Bereich auf dem Shield, der bereits den Aufbau kleinerer Schaltungen ermöglicht.

Im Onlineshop von Adafruit ist das Shield für 13,95 US-Dollar zu erwerben. Baugleiche Shields kosten auf eBay um die 8 Euro. Wer die Bestellung in China wagt, kann bereits für unter 3 Euro fündig werden.

Neben einer von Adafruit bereitgestellten Anleitung im PDF-Format gibt es auch ein Online-Tutorial.

Datalogger-Shield mit Echtzeituhr und SD-Karten-Leser

(Bild: adafruit.com)


In einem Anwendungsbeispiel erfolgt der Anschluss eines Temperatursensors vom Typ TMP36 an den Arduino-Uno beziehungsweise an das Datalogger-Shield. In dem Beispiel soll periodisch eine Temperaturmessung erfolgen. Die Zeit zwischen Messungen ist im Code definierbar.

Das Fritzing-Diagram zeigt den Sensor mit der flachen Seite zum Betrachter ausgerichtet. Der Sensor besitzt einen Eingang für 5 V Versorgungsspannung (links) und einen für Erde (rechts). Zudem in der Mitte einen Datenausgang, den der Anwender im Beispiel mit dem Analogeingang A0 des Uno verbindet.

Einfache Schaltung mit Adafruit Data Logger Shield und TMP36-Temperatursensor

Programmtechnisch wäre der direkte Zugriff auf SD-Cards und Echtzeituhren zwar machbar, aber dafür ziemlich fehlerträchtig und aufwendig. Stattdessen bieten sich diverse Bibliotheken als Alternative an. Die Menge der verfügbaren Bibliotheken ist durchaus ansehnlich.

Es gibt eine exzellente Bibliothek für den Zugriff auf Echtzeituhren namens RTClib. Diese liegt bereits installationsfähig in der Arduino-IDE vor. Um sie tatsächlich zu installieren, suchen Entwickler in der Arduino-IDE Bibliotheksverwaltung nach RTClib:

  • Sketch | Include Library | Manage Libraries...

und installieren sie dann.

In eigenen Programmen verwenden wir die Bibliothek über folgende Deklarationen.

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

RTC_DS1307 rtc; // Zugriff auf RTC

Ist keine Uhr angeschlossen, muss der Sketch das Problem behandeln.

   if (! rtc.begin()) { // Keine Uhr entdeckt
Serial.println("Echtzeituhr fehlt");
// Fehlerbehandlung
}

Ist die Uhr vorhanden, aber noch nicht mit Datum und Uhrzeit initialisiert, gibt das Programm entsprechend die Information vor. Im Beispielfragment erfolgt das Setzen von Zeit und Datum mit Hilfe der übergebenen Übersetzungszeit des Sketches. Natürlich ließe sich das Ganze auch implizit oder über eine Anfrage an einen NTP-Server bewerkstelligen.

  if (! rtc.isrunning()) { // Uhr schon gesetzt?
Serial.println("RTC bisher noch nicht gesetzt!");
// => Initialisiere Zeit und Datum auf Datum/Zeit des Host-PCs
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}

Nun ist die Echtzeituhr jederzeit abfragbar:

    DateTime jetzt = rtc.now();
    Serial.print(jetzt.year(), DEC);
Serial.print('/');
Serial.print(jetzt.month(), DEC);
Serial.print('/');
Serial.print(jetzt.day(), DEC);
Serial.print(" ");
Serial.print(jetzt.hour(), DEC);
Serial.print(':');
Serial.print(jetzt.minute(), DEC);
Serial.print(':');
Serial.print(jetzt.second(), DEC);
Serial.println();

Es gibt bereits eine Integrierte Bibliothek für SD-Karten in der Arduino-IDE (Name SD). Der Autor hat aber manche Probleme mit der Bibliothek erlebt, und benutzt alternativ die Bibliothek SdFat.

Diese Bibliothek laden Entwickler über eine GitHub-Webseite als .ZIP-Datei herunter. Hier deren URL.

Anschließend geben wir sie noch der IDE bekannt:

  • Sketch | Include Library | Add .ZIP Library

Sobald die IDE die Bibliothek erfolgreich installiert hat, können wir sie für unsere Zwecke nutzen.

Um die Bibliothek einsetzen zu können, sind zunächst diverse Vereinbarungen notwendig:

#include <SPI.h>
#include "SdFat.h"

// Chip Selector auf Arduino Board
const uint8_t chipSelect = SS;

// Separatorzeichen für Dateiausgabe

const String SEP = ";";

// Zugriff auf Dateisystem der SD Card
SdFat sd;

// Log-Datei
SdFile datei;

// Fehlermeldungen im Flash ablegen.
#define error(msg) sd.errorHalt(F(msg))

Im nachfolgenden Code initialisieren wir ein Objekt zum Zugriff auf das Dateisystem der Karte. Ich arbeite mit halber Geschwindigkeit (SPI_HALF_SPEED), was sich aber auch in SPI_FULL_SPEED ändern lässt, sofern keine Probleme zur Laufzeit auftreten.

  if (!sd.begin(chipSelect, SPI_HALF_SPEED)) { // Zugriff auf SD
sd.initErrorHalt();
}

Eine neue oder existierende Datei lässt sich zum Beispiel wie folgt kreieren und öffnen:

  if (datei.open(dateiName, O_CREAT | O_WRITE | O_EXCL)) {
// alles ok => Daten schreiben
}

Nun kann das Programm einen Datensatz in die geöffnete Datei schreiben:

  datei.print("Die Antwort lautet ");
datei.print(42);
datei.println("!!!");

Um Datenverluste zu vermeiden, bietet sich am Ende eines Schreibvorgangs folgender Code an, speziell wenn Schreibvorgänge schnell aufeinander folgen:

  if (!datei.sync() || datei.getWriteError()) {
error("Schreibfehler!");
}

Dateischließen funktioniert anhand folgenden Codes:

  datei.close();

Das nachfolgende Programm für das Beispielsprojekt enthält drei Funktionalitäten:

  • Messung der Temperatur über TMP36-Sensor
  • Echtzeituhr
  • SD-Card-Reader

Während des Setups initialisiert der Sketch die angeschlossene Echtzeituhr (initRTC()) , ebenso wie den SD-Card-Leser initSDCardReader(). Beim Initialisieren des Kartenlesers sucht der Sketch auch einen noch nicht vergebenen Dateinamen wie TMP3600.CSV. Die Zählung des Dateinamen-Suffixes beginnt bei 00 und geht bis 09. Sollten alle Namen vergeben sein, beginnt der Sketch erneut bei 00. Zuerst schreibt der Sketch einen Header in die neue Messwertdatei: schreibeHeader().

In jeder Iteration erfolgt zunächst das Auslesen der Temperatur (temperaturInCMessen()), das Erfassen von Zeit und Datum (DateTime jetzt = rtc.now()) und das Schreiben eines Datensatzes auf die Logdatei: schreibeMessung(temperaturInC, "C", jetzt)

Schreiben von Daten geschieht im CSV-Format (Comma Separated Values), weil sich diese Dateien hinterher gut mit Tabellenkalkulationen einlesen und auswerten lassen.

Das Messen und Protokollieren passiert so lange bis eine Zufallszahl mit 1 übereinstimmt. In der Praxis würde natürlich eine andere Endbedingung zum Tragen kommen, etwa das Auslösen eines externen Interrupts an Pin 2 oder 3.

Sobald die Datenerfassung abgeschlossen ist, kann der Anwender die SD-Karte entnehmen und in einem Windows/macOS/Linux-Rechner auswerten. Ein Beispiel ist der folgenden Abbildung zu entnehmen.

Die vom Sketch erzeugte CSV-Datei lässt sich gut in Tabellenkalkulations-Software auswerten

Das abgedruckte Programm-Beispiel können Sie auch gerne als Basis für Ihre eigenen Projekte benutzen.

/////////////////////////////////////////////////////////////////
//
// Datalogger Demo (c) Michael Stal, 2016
//
// Messung der Temperatur an einem TMP36
// Zeitverarbeitung mit RTClib (Chip DS1307, über IIC)
// SD Card Zugriff mit SdFat (über SPI MISO/MOSI)
// RTC und SD Card entweder separat angeschlossen
// oder über Adafruit Datalogger Shield
//
/////////////////////////////////////////////////////////////////



// ******************** DEKLARATIONEN ***************************

/////////////////////////////////////////////////////////////////
// DEKLARATIONEN RTC (Echtzeituhr)
/////////////////////////////////////////////////////////////////

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

RTC_DS1307 rtc; // Zugriff auf RTC

char WochenTage[7][12] = {"Sonntag",
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag"};


/////////////////////////////////////////////////////////////////
// DEKLARATIONEN SD Card
/////////////////////////////////////////////////////////////////

#define LOGDATEI "TMP36"



#include <SPI.h>
#include "SdFat.h"

// Chip Selector auf Arduino Board
const uint8_t chipSelect = SS;

// Separatorzeichen für Dateiausgabe

const String SEP = ";";

// Zugriff auf Dateisystem der SD Card
SdFat sd;

// Log-Datei
SdFile datei;

// Fehlermeldungen im Flash ablegen.
#define error(msg) sd.errorHalt(F(msg))


/////////////////////////////////////////////////////////////////
// DEKLARATIONEN TemperaturSensor
/////////////////////////////////////////////////////////////////


const int TMP36Pin = A0;
const float VersorgungsSpannung = 5.0; // ändern für 3.3V
const int ZeitZwischenMessungen = 5; // in Sekunden


// ********************** METHODEN ******************************

/////////////////////////////////////////////////////////////////
//
// initRTC()
// Echtzeituhr initialisieren
//
/////////////////////////////////////////////////////////////////

void initRTC()
{
if (! rtc.begin()) { // ist eine Uhr angeschlossen?
Serial.println("Echtzeituhr fehlt");
while(1); // Fehlerschleife
}
if (! rtc.isrunning()) { // Uhr schon gesetzt?
Serial.println("RTC bisher noch nicht gesetzt!");
// => Initialisiere Zeit und Datum auf Datum/Zeit des Host-PCs
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
}


/////////////////////////////////////////////////////////////////
//
// initSDCardReader()
// Kartenleser initialisieren
//
/////////////////////////////////////////////////////////////////

void initSDCardReader()
{
const uint8_t NAMENSLAENGE= sizeof(LOGDATEI) - 1;
char dateiName[13] = LOGDATEI "00.csv"; // Z.B. TMP3604.csv

delay(1000);
// SD Card mit SPI_HALF_SPEED initialisieren, um Fehler
// bei Breadboardnutzung zu vermeiden. Sonst => SPI_FULL_SPEED

if (!sd.begin(chipSelect, SPI_HALF_SPEED)) { // Zugriff auf SD?
sd.initErrorHalt();
}

// Dateiformat 8.3
if (NAMENSLAENGE > 6) {
error("Dateipräfix zu lang");
}

// Standarddateiname LOGDATEI + laufende Nummer, z.B.
// TMP3603.csv
// Sobald alle Suffixe 00..09 verbraucht sind,
// geht es von vorne los: round robin


while (sd.exists(dateiName)) {
if (dateiName[NAMENSLAENGE + 1] != '9') {
dateiName[NAMENSLAENGE + 1]++;
}
else if (dateiName[NAMENSLAENGE] != '9') {
dateiName[NAMENSLAENGE + 1] = '0';
dateiName[NAMENSLAENGE]++;
}
else {
error("Kann Datei nicht erzeugen");
}
}
// Jetzt öffnen:
if (!datei.open(dateiName, O_CREAT | O_WRITE | O_EXCL)) {
error("Datei öffnen misslungen!");
}

Serial.print(F("Logging auf: "));
Serial.println(dateiName);

// Header schreiben
schreibeHeader();
}


/////////////////////////////////////////////////////////////////
//
// setup()
// Echtzeituhr und SD Card Leser initialisieren
//
/////////////////////////////////////////////////////////////////

void setup()
{
Serial.begin(9600); // Serielle Kommunikation an
initRTC(); // Echtzeituhr initialisieren
initSDCardReader(); // SD Card initialisieren
}


/////////////////////////////////////////////////////////////////
//
// temperaturInCMessen()
// Temperatur vom TMP36 über Analogeingang lesen
//
/////////////////////////////////////////////////////////////////

float temperaturInCMessen()
{
int digitalWertTmp36 = analogRead(TMP36Pin); // A0 einlesen
float messwertSpannung = digitalWertTmp36 * VersorgungsSpannung / 1024.0;

Serial.print("Spannung gemessen an Analogeingang ");
Serial.print(TMP36Pin);
Serial.print(" => ");
Serial.println(messwertSpannung);

// Wir berücksichtigen ausschließlich Celsius
float temperaturInC = (messwertSpannung - 0.5) * 100;

Serial.print("Temperatur in C = ");
Serial.println(temperaturInC);
return temperaturInC;
}


/////////////////////////////////////////////////////////////////
//
// ausgebenZeit()
// Zeitausgabe zur Diagnose
//
/////////////////////////////////////////////////////////////////

void ausgebenZeit(DateTime jetzt) // easy Code
{
Serial.print(jetzt.year(), DEC);
Serial.print('/');
Serial.print(jetzt.month(), DEC);
Serial.print('/');
Serial.print(jetzt.day(), DEC);
Serial.print(" (");
Serial.print(WochenTage[jetzt.dayOfTheWeek()]);
Serial.print(") ");
Serial.print(jetzt.hour(), DEC);
Serial.print(':');
Serial.print(jetzt.minute(), DEC);
Serial.print(':');
Serial.print(jetzt.second(), DEC);
Serial.println();
}


/////////////////////////////////////////////////////////////////
//
// loop()
// In jeder Iteration
// Temperatur messen
// Zeit erfassen
// Daten auf SC Card schreiben
// Terminierungsbedingung prüfen und ggf. stoppen
// inkl. Dateischließen!
//
/////////////////////////////////////////////////////////////////

void loop()
{

// Messwert am TMP36 auslesen
float temperaturInC = temperaturInCMessen();

// Vorgegebene Zeit warten
delay(ZeitZwischenMessungen * 1000);

// Datum & Zeit holen:
DateTime jetzt = rtc.now();
ausgebenZeit(jetzt); // und schreiben am seriellen Monitor

// Jetzt Daten schreiben:
schreibeMessung(temperaturInC, "C", jetzt);

// Zufallsmechanismus für Stopp der Messungen
if (random(7) == 1) {
dateiSchliessen();
}
}


/////////////////////////////////////////////////////////////////
// Statt in einer .csv-Datei könnte man z.B. auch im JSON Format
// speichern !!!
/////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////
//
// schreibeHeader
// Schreiben von Header Information in Datei
//
/////////////////////////////////////////////////////////////////

void schreibeHeader() {
datei.println(F("Temperaturmessungen mit TMP36 mit Nutzung eines Dataloggers"));
datei.print(F("Datum"));
datei.print(SEP);
datei.print(F("Zeit"));
datei.print(SEP);
datei.print(F("Temperatur (Einheit Celsius)"));
datei.println();
}


/////////////////////////////////////////////////////////////////
//
// schreibeMessung()
// messwert nehmen und in gewählter Einheit ausgeben
// und zusammen mit Datum und Uhrzeit in Datei schreiben
//
/////////////////////////////////////////////////////////////////

void schreibeMessung(float messwert,
String einheit,
DateTime jetzt)
{
datei.print(jetzt.day());
datei.print(".");
datei.print(jetzt.month());
datei.print(".");
datei.print(jetzt.year());
datei.print(SEP);
datei.print(jetzt.hour());
datei.print(":");
datei.print(jetzt.minute());
datei.print(":");
datei.print(jetzt.second());
datei.print(SEP);
datei.print(messwert);
datei.print(einheit);
datei.println();
// Dateisync, um Datenverlust zu vermeiden:
if (!datei.sync() || datei.getWriteError()) {
error("Schreibfehler!");
}
}


/////////////////////////////////////////////////////////////////
//
// dateiSchliessen()
// Datei wieder schließen
//
/////////////////////////////////////////////////////////////////

void dateiSchliessen()
{
datei.close();
Serial.println(F("Datei steht bereit!"));
SysCall::halt();
}

// ********************** DATEIENDE *****************************

Irgendeine Form der Zeiterfassung oder der persistenten Speicherung beziehungsweise Protokollierung benötigt jedes nicht triviale Projekt. Dafür sind ein (Micro-)SD-Card-Reader-Board und ein Echtzeit-Uhren-Board notwendig. Oder alternativ ein Shield, das beides in sich vereinigt. Diese Hardware lässt sich gegen geringen Obolus erwerben. Durch den Einsatz entsprechender Bibliotheken hält sich auch der Programmieraufwand in engen Grenzen. Die häufige Begrenzung auf 2-GByte-SD-Cards ist in der Praxis leicht zu verkraften. Der Erweiterung eigener Projekte steht also nichts mehr im Wege.

()