SD-Karten und SD-Kartenmodule

Über den Beitrag

In diesem Beitrag beschäftige ich mich (und euch 🙂) mit der Ansteuerung von SD-Karten, beziehungsweise SD-Kartenmodulen. Dabei gehe ich auf die Unterschiede bei der Nutzung von Arduino-, ESP8266- und ESP32-Boards ein.

Das erwartet euch: 

Wozu braucht man SD-Karten?

Es gibt viele Möglichkeiten, Daten mithilfe von Mikrocontrollern zu speichern:

  • Kleinere Datenmengen von einigen hundert Byte bis wenigen Kilobyte lassen sich auf dem internen EEPROM speichern. Das habe ich hier beschrieben.
  • Für etwas größere Datenmengen bis ~1 Megabyte könnt ihr externe EEPROMs verwenden. Diese zeichnen sich durch geringen Platz- und Strombedarf aus. Externe, I2C-gesteuerte EEPROMs habe ich hier beschrieben, einen Beitrag über die schnelleren, SPI-basierten Vertreter findet ihr hier.
  • Wenn ihr ein ESP32- oder ESP8266-basiertes Board verwendet, könnt ihr mehrere Megabyte Daten mit dem LittleFS (früher SPIFFS = SPI Flash File Storage) speichern.
  • Schließlich gibt es noch diverse Möglichkeiten, Daten in der Cloud zu speichern oder über ein Netzwerk beispielsweise an einen PC zu senden.

Wozu braucht man dann noch SD-Karten? Da gibt es mehrere Gründe:

  1. Die SD-Karten zeichnen sich durch hohe Speicherkapazitäten im Gigabyte Bereich aus.
  2. Die Daten sind bequem transportabel. So könnt ihr die gespeicherten Daten mit einem Card-Reader am PC lesen und auswerten, während euer Aufbau zur Datenerfassung am Ort des Geschehens verbleibt.
  3. Im Gegensatz zu Online-Speichermethoden seid ihr nicht auf Netzverfügbarkeit angewiesen.

SD-Kartenformate, -klassen und -module

SD-Karten („Secure Digital Memory Cards“) unterscheiden sich unter anderem in:

  • ihren Abmessungen,
  • ihrer maximalen Speicherkapazität und
  • ihrer minimalen Schreibgeschwindigkeit.

Die Bezeichnung „SD-Karte“ meint strenggenommen die großen Modelle mit den Maßen 24 mm x 32 mm x 2.1 mm und einer maximalen Speicherkapazität von 2 Gigabyte. Die etwas klobige SD-Karte wurde in den meisten Anwendungen durch die kleine microSD-Karte im Format 11 mm × 15 mm × 1.0 mm abgelöst. Dazwischen gibt es noch die eher selten eingesetzte miniSD-Karte.

Bezüglich der maximalen Speicherkapazitäten werden SD-, SDHC-, SDXC- und SDUC-Karten unterschieden. Günstige Module für Arduino & Co gibt es vor allem für die SD- und die microSD-/microSDHC-Karten, beispielsweise diese hier: 

 

Li: microSD-/microSDHC Modul bis max. 32 GB, Re: SD-Kartenmodul bis 2 GB.
Li: microSD-/microSDHC Modul bis max. 32 GB, Re: SD-Kartenmodul bis 2 GB.

Mit maximalen 32 Gigabyte haben SDHC-Karten eine wesentlich höhere Speicherkapazität als die SD-Karten.

Die Mindestschreibgeschwindigkeit lässt sich aus der Klasse der SD-Karte ablesen. Das ist die kleine Zahl (2 bis 10) in dem nicht geschlossen Kreis. Die Angabe ist in Megabyte pro Sekunde.

Stromverbrauch

Für das microSDHC-Kartenmodul habe ich einen Stromverbrauch von 1.8 Milliampere im Ruhezustand ermittelt. Für batteriebetriebene Projekte ist das recht hoch. Ihr könntet euch überlegen, das Modul über einen Transistor oder besser über einen MOSFET bei Bedarf an- und auszuschalten. Während des Schreibens liegt der Bedarf bei knapp 40 Milliampere.

microSD-Shield für den Wemos D1 mini

Eine besonders praktische Lösung gibt es für das Wemos D1 Mini Board, nämlich ein microSD-Kartenshield:

Wemos D1 Mini microSD Shield
Wemos D1 Mini microSD Shield

Vorbereitungen

SD-Karten formatieren

Wenn ihr euch eine SD-Karte kauft, dann ist diese vorformatiert. Das Format kann mit den Speicherkartenmodulen harmonieren, muss es aber nicht. Aber auch wenn ihr eine alte SD-Karte, beispielsweise aus einer Kamera, wiederverwerten wollt, ist das eventuell nicht ohne Weiteres möglich, da dort zum Teil herstellerspezifische Formate zum Einsatz kommen.

Für die Verwendung in den SD-Kartenmodulen benötigt ihr das FAT16- oder FAT32-Format. Die Formatierung könnt ihr am PC oder Laptop vornehmen. Wenn ihr keinen SD-Kartenslot habt, dann solltet ihr euch einen USB SD-Kartenleser für ca. 10 Euro zulegen. 

Als Software empfehle ich das kostenlose Programm SD Card Formatter, das ihr hier bekommt. Die Installationsdatei müsst ihr „entzippen“ und könnt dann das Programm installieren.

SD Card Formatter ist mit seinen wenigen Einstellungen so ziemlich das einfachste Programm, das ich je verwendet habe. Die SD-Karte wird vom Programm automatisch erkannt. Wählt die Option „Overwrite format“ und klickt auf „Format“. Der Vorgang dauert einige Minuten. Das war’s.

Formatierung von SD-Karten - SD Card Formatter Programmoberfläche
SD Card Formatter Programmoberfläche

Die SD-Bibliothek

Die SD-Bibliothek gehört zur Grundausstattung der Arduino IDE und muss nicht extra installiert werden. Genau genommen gibt es nicht „die eine“ SD-Bibliothek, sondern verschiedene Versionen für verschiedene Boards. Insbesondere die Version für den ESP32 hat ein paar Eigenheiten, auf die ich im weiteren Verlauf noch eingehe.

Je nach eingestelltem Board werden euch unterschiedliche Beispielsketche anzeigt. Leider werden die „Any Board“-Beispiele („jedes Board“) bei Verwendung von ESP32 basierten Boards nicht ausgeblendet, obwohl sie auf diesen nicht funktionieren. Geht stattdessen zu den ESP32 Beispielen → SD(esp32). 

Auswahl an Beispielsketchen für verschiedene Boards
Auswahl an Beispielsketchen für verschiedene Boards

Anschluss an den Mikrocontroller

Die zulässige Versorgungsspannung für SD-Karten beträgt in der Regel 2.7 – 3.6 Volt. Einige SD-Karten vertragen auch 1.8 Volt. Die meisten SD-Kartenmodule haben Spannungsregler und können deshalb mit 5 Volt betrieben werden. Das von mir verwendete microSD-Kartenmodul hat (absurderweise) keinen 3 Volt Pin und muss deshalb mit 5 Volt betrieben werden.

Die Kommunikation mit dem Mikrocontroller erfolgt über SPI. Also kommt MISO an MISO, MOSI an MOSI, SCK an SCK und CS an CS. Den CS-Pin könnt ihr bei den meisten Boards selbst festlegen.

Für den Arduino Nano sieht die Verkabelung folgendermaßen aus:

Ein microSD-Kartenmodul am Arduino Nano
Ein microSD-Kartenmodul am Arduino Nano

Prüfen

Wenn ihr ein Arduino Board verwendet, dann könnt ihr die Schaltung und die SD_Karte mit dem CardInfo Sketch aus den Beispielen überprüfen. Vergesst dabei nicht, ggf. den CS Pin anzupassen.

Der Sketch verrät euch den SD-Kartentyp, das Format, die Speicherkapazität und die vorhandenen Verzeichnisse und Dateien:

Ausgabe von CardInfo.ino aus den Beispielen
Ausgabe von CardInfo.ino aus den Beispielen

CardInfo funktioniert nicht auf ESP32- oder ESP8266Boards. Als ersten Test für ein ESP32-basiertes Board empfehle ich stattdessen den Beispielsketch SD_Test.ino und für ein ESP8266-Board den Sketch listfiles.ino.

Schreiben und Lesen

Nun schauen wir uns ein paar Funktionen der SD-Bibliothek an und beginnen mit einfachen Schreib- und Leseoperationen.

Schreiben

Der folgende Beispielsketch schreibt ein Integer-Array auf die SD-Karte. Wieder, aber zum letzten Mal der Hinweis: Eventuell müsst ihr den CS-Pin anpassen.

#include <SD.h>
const int chipSelect = 10; // choose an appropriate pin for your board

void setup(){
  String myFile = "integers.txt"; // for ESP32: "/integers.txt";
  int intArray[10] = {42, 424, 4242, 17, 1234, 56, 1967, 299, 3333, 5678};
  Serial.begin(9600);
//  while(!Serial){} // needed for some boards
//  delay(1000); // helps in case of incomplete output on serial monitor
  
  if(!SD.begin(chipSelect)){
    Serial.println("SD-Card not connected!");
    while(1);
  }
  else{
    Serial.println("SD-Card initialized");
  }
  
  SD.remove(myFile);
  File dataFile = SD.open(myFile, FILE_WRITE);

  if(dataFile){
    for(int i = 0; i < 10; i++){
      dataFile.println(intArray[i]);
    }
    dataFile.close();
    Serial.println("Data written");
  }
  else{
    Serial.println("Could not open file");
  }
}

void loop(){}

Erklärungen zu sd_card_write.ino

Die SD-Bibliothek wird mit #include <SD.h> eingebunden. Die Initialisierung nehmt ihr mit SD.begin(chipSelectPin) vor. Wenn dieser Prozess funktioniert, dann liefert SD.begin() den Wert true, sonst false. Ein SD-Objekt müsst ihr nicht erzeugen, es sei denn, ihr wollt zwei oder mehr SD-Karten verwenden (siehe letztes Kapitel).

Die Datei, die wir kreieren und füllen, heißt „integers.txt“. Den Namen speichern wir in der String-Variable „myFile“. Es sind nur 8 Zeichen für den Dateinamen plus 3 Zeichen für die Dateiendung erlaubt – ein Rücksturz ins letzte Jahrtausend.

Die Anweisung SD.remove(myFile); löscht die Datei myFile (bzw. „integers.txt“), falls vorhanden. Ohne diese Maßnahme würden die zu speichernden Daten bei jedem Programmdurchlauf erneut an das Ende der Datei gehängt werden (außer beim ESP32). 

Die Zeile 20 kommt unscheinbar daher, hat es aber in sich:

File dataFile = SD.open(myFile, FILE_WRITE);

Zum Verständnis (oder Verwirrung?):

  • SD ist ein Objekt der Klasse SDClass. Das ist nicht offensichtlich, da ihr SD nicht selbst erzeugen musstet.
  • File ist eine Eigenschaft von SDClass. 
  • SD.open(myFile) öffnet den Pfad zu der Datei „myFile“. Die Funktion gibt ein Objekt der Klasse File, also ein Dateiobjekt zurück, welches die Bezeichnung dataFile erhält. Der Parameter FILE_WRITE legt fest, dass die Datei zum Schreiben geöffnet wird.
  • „myFile“ enthält also nur den Dateinamen bzw. den Pfad zu der Datei. Hingegen ist „dataFile“ das eigentliche Dateiobjekt.
    • Folgerichtig liefert dataFile.name() den Inhalt der Variablen „myFile“ zurück, hier also „integers.txt“. Das könnt ihr ja mal ausprobieren.

Die Anweisungen zum Schreiben lauten print(), println() oder alternativ auch write(). Die Ähnlichkeiten zu den Serial Funktionen sind nicht zufällig, denn File und Serial sind Verwandte, nämlich „Kinder“ der Klasse Stream.

Am Ende schließt ihr den Schreibvorgang mit close() ab.

Eigenheiten des ESP32

Für den ESP32 gibt es zu beachten:

  • Den Namen von Dateien und Verzeichnissen ist ein Slash („/“) voranzustellen.
  • Wird „FILE_WRITE“ auf eine vorhandene Datei angewendet, dann wird ihr Inhalt überschrieben. Mithilfe des Parameters „FILE_APPEND“ könnt ihr Inhalt anhängen. Die SD-Bibliothek für Arduino Boards kennt den Parameter „FILE_APPEND“ nicht und hängt den Inhalt automatisch an.
  • SD.begin() kann ohne CS Pin aufgerufen werden. In dem Fall greift die Voreinstellung GPIO5

Lesen

Der folgende Sketch liest die eben gespeicherten Daten wieder aus:

#include <SD.h>
const int chipSelect = 10; // choose an appropriate pin for your board

void setup(){
  char myFile[] = "integers.txt"; // for ESP32: "/integers.txt";
  Serial.begin(9600);
//  while(!Serial){} // needed for some Boards
//  delay(1000); // helps in case of incomplete output on serial monitor
  
  if(!SD.begin(chipSelect)){
    Serial.println("SD-Card not connected!");
    while(1);
  }
  else{
    Serial.println("SD-Card initialized");
  }

  File dataFile = SD.open(myFile);

  if(dataFile){
    while(dataFile.available()){
      String line = dataFile.readStringUntil('\n');
      Serial.println(line);
    }
//    Alternative: read as Integer
//    while(dataFile.available()){
//      int readInt = dataFile.parseInt();
//      Serial.println(readInt);
//    }
    dataFile.close();
  }
  else{
    Serial.println("Could not open file");
  }
}

void loop(){}

Erklärungen zu sd_card_read.ino

Vieles ähnelt dem zuvor besprochenen Schreibsketch. Ich gehe nur auf die wesentlichen Unterschiede ein.

  • „myFile“ habe ich dieses Mal als Character Array definiert. Der Grund ist lediglich zu zeigen, dass das geht.
  • Indem SD.open(myFile) ohne „FILE_WRITE“ aufgerufen wird, kann die Datei nur gelesen werden.
  • Für das Lesen der Daten gibt es zwei Optionen:
    • readStringUntil('\n') liest alle Zeichen bis zum nächsten Zeilenumbruch als String ein.
    • Um die Daten als Integer auszulesen, bietet sich die praktische Funktion parseInt() an. Für Fließkommazahlen stünde parseFloat() zur Verfügung.

Am Ende wird die Datei wieder mit close() geschlossen.

Die Ausgabe ist wenig überraschend:

Ausgabe von sd_card_read.ino
Ausgabe von sd_card_read.ino

Dateien und Verzeichnisse anlegen / löschen / anzeigen

Ihr habt gerade gesehen, wie ihr Dateien erzeugt und löscht. Nun schauen wir uns an, wie Ihr dasselbe mit Verzeichnissen macht und wie ihr ganze Verzeichnisstrukturen, einschließlich der darin erhaltenen Dateien, anzeigt. Hier zunächst der Sketch, der auch auf einem ESP8266, nicht aber auf einem ESP32 funktioniert:

/* Based on:
  listfiles.ino, created Nov 2010 by David A. Mellis;
  modified 9 Apr 2012 by Tom Igoe
  modified 2 Feb 2014 by Scott Fitzgerald
*/
#include <SD.h>
const int chipSelectPin = 10;

void setup() {
  Serial.begin(9600);
  while (!Serial) {}
  
  Serial.print("Initializing SD card...");

  if (!SD.begin(chipSelectPin)) {
    Serial.println("initialization failed!");
    while (1);
  }
  Serial.println("initialization done.");

  Serial.println("Adding files:");
  SD.mkdir("New_Fold"); // make new folder
  File dataFile = SD.open("file_1.txt", FILE_WRITE);
  dataFile.println("Some Content");
  dataFile.close();

  dataFile = SD.open("new_fold/file_2.txt", FILE_WRITE);
  dataFile.println("Some other Content");
  dataFile.close();

  File root = SD.open("/");
  printDirectory(root, 0);

  Serial.println();
  Serial.println("After removing files:");
  SD.remove("file_1.txt");
  SD.remove("new_fold/file_2.txt");
  SD.rmdir("new_fold");

  root = SD.open("/");
  printDirectory(root, 0);

  Serial.println("done!");
}

void loop() {}

void printDirectory(File &dir, int numTabs) {
  while (true) {

    File entry =  dir.openNextFile();
    if (! entry) {
      // no more files
      break;
    }
    for (uint8_t i = 0; i < numTabs; i++) {
      Serial.print('\t');
    }
    Serial.print(entry.name());
    if (entry.isDirectory()) {
      Serial.println("/");
      printDirectory(entry, numTabs + 1);
    } else {
      // files have sizes, directories do not
      Serial.print("\t\t");
      Serial.println(entry.size(), DEC);
    }
    entry.close();
  }
}

 

Erklärungen zu add_remove_list_files_and_folders.ino

Das setup()

  • SD.mkdir("New_Fold") erzeugt ein Verzeichnis mit dem Namen „New_Fold“.
  • Die Datei „file_1.txt“ erzeugen wir lediglich, um etwas mehr zum Anzeigen zu haben.
  • SD.open("New_Fold/file_2.txt", FILE_WRITE); erzeugt die Datei „file_2.txt“ im Ordner „New_Fold“.
  • Die Zuweisung File root = SD.open("/"); erstaunt vielleicht ein wenig, da ein Dateiobjekt namens „root“ (= Wurzel, Ursprung) erzeugt wird, das das Hauptverzeichnis repräsentiert. Nutzer von Linux oder Unix dürften weniger überrascht sein, denn für diese Systeme ist ein Verzeichnis lediglich eine spezielle Datei. 
  • Die Dateien und Verzeichnisse werden mit der Funktion printDirectory() angezeigt. Dazu kommen wir gleich.
  • Wir entfernen die hinzugefügten Dateien und Verzeichnisse mit SD.remove() beziehungsweise SD.rmdir().
  • Danach wird der Inhalt der SD-Karte noch einmal ausgegeben.

Die Funktion printDirectory()

Der Funktion printDirectory() werden ein Dateiobjekt (beim ersten Aufruf ist das root) und die Anzahl Tabs übergeben. Die Anzahl der Tabs dient der Formatierung und repräsentiert die aktuelle Verzeichnistiefe.

File entry = dir.openNextFile(); öffnet die nächste Datei, die auf der SD-Karte gefunden wird und erzeugt daraus das Dateiobjekt „entry“. Drei Szenarien sind möglich:

  • Es gibt keine weitere Datei oder Verzeichnis (!entry). Die Schleife wird abgebrochen.
  • Ein weiterer Eintrag ist vorhanden und es handelt sich um ein Verzeichnis (entry.isDirectory()). In dem Fall wird printDirectory() erneut ausgeführt und die aktuelle Verzeichnistiefe um 1 erhöht. Das ist ein schönes Beispiel für einen rekursiven Aufruf. 
  • Ein weiterer Eintrag ist vorhanden und es handelt sich dabei um eine Datei. In dem Fall wird die Dateigröße mit entry.size() abgefragt und ausgegeben. Danach wird die while-Schleife weiter durchlaufen. 

Hier die Ausgabe:

Ausgabe von add_remove_list_files_and_folders.ino
Ausgabe von add_remove_list_files_and_folders.ino

Ein paar Dinge fallen auf:

  • SYSTEM~1 ist das Verzeichnis SystemVolumeInformation. Es wird von Windows automatisch erstellt. Da sein Name mehr als acht Buchstaben hat, wird es abgekürzt.
    • Ihr könnt das Verzeichnis bedenkenlos löschen, da es für FAT16/FAT32 nicht benötigt wird. Allerdings müsst ihr zunächst die beiden Dateien im Verzeichnis löschen. SD.rmdir() funktioniert nur mit leeren Verzeichnissen.
    • Wenn ihr die SD-Karte danach wieder mit einem Lesegerät am PC auslest, wird das Verzeichnis samt Inhalt erneut erstellt.
  • Die SD-Bibliothek wandelt alle Datei- und Verzeichnisnamen in Großbuchstaben um.

add_remove_list_files_and_folders für den ESP32

Der entsprechende Sketch für den ESP32 macht im Prinzip dasselbe. Hier muss allerdings die Verzeichnistiefe („noOfFolderLevels“) vorgegeben werden. Ein Wert, der größer ist als die tatsächliche Verzeichnistiefe (die ihr ja vielleicht nicht kennt), schadet nicht.

Der Sketch basiert auf Teilen des Beispielsketches SD_Test.ino. Der Originalsketch ist ein wenig komplexer als er sein müsste, da dort auch das SD-Objekt an die Funktionen übergeben wird. Das ist aber nicht notwendig. 

Zusätzlich liefert der Sketch allgemeine Informationen über die SD-Karte, nämlich Typ und Größe.

#include "SD.h"

void setup(){
  int noOfFolderLevels = 3;
  Serial.begin(115200);
  delay(1000);
  if(!SD.begin()){
    Serial.println("Card Mount Failed");
    return;
  }
  uint8_t cardType = SD.cardType();

  if(cardType == CARD_NONE){
    Serial.println("No SD card attached");
    return;
  }

  Serial.print("SD Card Type: ");
  if(cardType == CARD_MMC){
    Serial.println("MMC");
  } else if(cardType == CARD_SD){
      Serial.println("SDSC");
  } else if(cardType == CARD_SDHC){
      Serial.println("SDHC");
  } else {
      Serial.println("UNKNOWN");
  }
  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);

  Serial.println("Adding files:");
  SD.mkdir("/new_fold"); // make new folder
  File dataFile = SD.open("/file_1.txt", FILE_WRITE);
  dataFile.println("Some Content");
  dataFile.close();

  dataFile = SD.open("/new_fold/file_2.txt", FILE_WRITE);
  dataFile.println("Some other Content");
  dataFile.close();
  
  listDir("/", noOfFolderLevels);

  Serial.println();
  Serial.println("After removing files:");
  SD.remove("/file_1.txt");
  SD.remove("/new_fold/file_2.txt");
  SD.rmdir("/new_fold");

  listDir("/", noOfFolderLevels);
}

void loop(){}

void listDir(const char * dirname, uint8_t levels){
  Serial.printf("Listing directory: ");
  Serial.println(dirname);
   
  File root = SD.open(dirname);
  if(!root){
    Serial.println("Failed to open directory");
    return;
  }
  if(!root.isDirectory()){
    Serial.println("Not a directory");
    return;
  }

  File file = root.openNextFile();
  while(file){
    if(file.isDirectory()){
      Serial.print("  DIR : ");
      Serial.println(file.name());
      if(levels){
        listDir(file.name(), levels -1);
      }
    } else {
        Serial.print("  FILE: ");
        Serial.print(file.name());
        Serial.print("  SIZE: ");
        Serial.println(file.size());
    }
    file = root.openNextFile();
  }
}

 

Und so sieht die Ausgabe aus:

Ausgabe von add_remove_list_files_and_folders_esp32.ino

Weitere Funktionen der Klassen SD und File

Viele Funktionen der Klassen SD und File haben wir schon besprochen. Eine vollständige Auflistung findet ihr hier auf den Arduino Seiten.

Die Funktionen der Klassen SD und File
Die Funktionen der Klassen SD und File

Testen der Schreibgeschwindigkeit

Um zu prüfen, wie schnell ein Schreibvorgang ist, habe ich den folgenden Sketch verfasst. Er schreibt 1000 Integerwerte in eine Datei. Dabei werden zwei Varianten getestet. Einmal wird die Datei geöffnet und die Werte „in einem Rutsch“ geschrieben (numbersa.txt). In einem zweiten Durchgang wird die Datei für jeden Schreibvorgang erneut geöffnet (numbersb.txt).

#include <SD.h>
const int chipSelect = 10;

void setup(){
  String myFile = "numbersa.txt";
  Serial.begin(9600);
  SD.begin(chipSelect);
  SD.remove(myFile);
  unsigned long startTime = millis();

  // Continuous writing:
  File dataFile = SD.open(myFile, FILE_WRITE);
  for (int i=0; i<1000; i++){
    dataFile.println(i);
  }
  dataFile.close();
  
  unsigned long writingTime = millis() - startTime;
  Serial.println("Writing 1000 integers took: ");
  Serial.print(writingTime);
  Serial.println(" [ms],");
  Serial.println("when keeping file open");
  Serial.println();

  // Discontinuous writing: n x (open, write, close)
  myFile = "numbersb.txt";
  SD.remove(myFile);
  startTime = millis();
  for (int i=0; i<1000; i++){
    dataFile = SD.open(myFile, FILE_WRITE); // FILE_APPEND for ESP32
    dataFile.println(i);
    dataFile.close();
  }
  
  writingTime = millis() - startTime;
  Serial.println("Writing 1000 integers took: ");
  Serial.print(writingTime);
  Serial.println(" [ms],");
  Serial.println("when reopening the file for every entry");
  Serial.println();
  delay(1000);
}

void loop(){}

 

Hier das Ergebnis bei Verwendung eines Arduino  Nano:

Ausgabe von sd_card_speed_test.ino
Ausgabe von sd_card_speed_test.ino

Das Ergebnis spricht für sich: Das Öffnen und Schließen der Datei nimmt jeweils ein paar Millisekunden in Anspruch, während sich das eigentliche Schreiben eines einzelnen Integerwertes im Mikrosekundenbereich abspielt. Wenn es auf Geschwindigkeit ankommt, dann solltet ihr also die Datei offen lassen.

Die Datei numbersa.txt ist 4.77 Kilobyte groß. Dafür haben wir 0.266 Sekunden benötigt, was eine Schreibgeschwindigkeit von 17.9 KB/s ergibt. Aber lag die Mindestschreibgeschwindigkeit von SD-Karten nicht im Bereich von einigen MB/s? Das stimmt zwar, aber der Flaschenhals ist hier die Datenverarbeitung und -übertragung.

Wesentlich schneller war der Schreibvorgang auf einem Wemos D1 Mini Board, er betrug nämlich 57 (!) Millisekunden. Für ein ESP32-Entwicklungsboard habe ich 75 Millisekunden ermittelt.

Überdies hängt die Geschwindigkeit von der SD-Karte ab. Die obigen Werte habe ich mit einer 16 GB microSDHC-Karte ermittelt. Die benötigte Zeit zum Schreiben der 1000 Integerwerte lag bei Verwendung einer 2 GB microSD-Karte (ohne „HC“) hingegen um 60 bis 100 Millisekunden höher.

Beschleunigung für Arduino AVR Boards

Wenn ihr größere Datenmengen schreiben wollt, dann ist es effektiver, diese in einem Puffer zwischenzuspeichern und dann den Puffer in einem Rutsch zu übertragen. Zumindest gilt das bei Verwendung eines AVR-basierten Arduino Boards. Mithilfe dieses Sketches (basierend auf dem nonBlockingWrite Beispielsketch), konnte ich die Schreibzeit für 1000 Integer von 266 Millisekunden auf 141 Millisekunden reduzieren.

#include <SD.h>

const char filename[] = "demo.txt";
File txtFile;
unsigned long lastMillis = 0;

void setup() {
    Serial.begin(115200);
    while (!Serial);

    // reserve memory for a String used as a buffer
    String buffer;
    buffer.reserve(288);  // reserve memory for the buffer (slightly bigger than the chunk size)

    if (!SD.begin(10)) {
        Serial.println("Card failed, or not present");
        // don't do anything more:
    while (1);
    }
    // If you want to start from an empty file,
    // uncomment the next line:
    SD.remove(filename);
    
    txtFile = SD.open(filename, FILE_WRITE);
    if (!txtFile) {
        Serial.print("error opening ");
        Serial.println(filename);
        while (1);
    }
    
    int counter = 0;
    unsigned long startingTime = millis();
    
    while(counter < 1000){
        fastWrite(buffer, counter);
        counter++;
    }
    txtFile.write(buffer.c_str()); // write what is left in the buffer
    unsigned long writingTime = millis() - startingTime;
    txtFile.close(); 
    Serial.print("Writing Time [ms]: ");
    Serial.println(writingTime);
}

void loop() {}

void fastWrite(String &buf, int counter){
    const unsigned int chunkSize = 256;
    char charBuf[10]; // buffer for t
    dtostrf(counter, 1, 0, charBuf); // convert counter char array
    buf += charBuf;
    buf += "\r\n";  // add carriage return and line feed
    if ((buf.length() >= chunkSize)) {
        txtFile.write(buf.c_str(), chunkSize); // write chunk
        buf.remove(0, chunkSize); // remove from buffer what's already written
    }
}

 

Der Sketch ist recht gehaltvoll. Aber um den Artikel nicht zu lang werden lassen, erkläre ich ihn nur in groben Zügen:

  • buffer ist der Pufferspeicher für die zu schreibenden Zahlen. Seine Kapazität muss etwas höher sein als die Größe der zu schreibenden Teilstücke (chunks).
  • dtostrf() konvertiert die zu schreibende Zahl in ein Character Array, welches an das Ende des Pufferspeichers geschrieben wird.
  • Wenn der Puffer größer als die Teilstückgröße ist, wird das Teilstück auf die SD-Karte geschrieben und der Puffer entsprechend reduziert. Danach wird der Puffer wieder gefüllt.
  • Am Ende wird der Rest des Puffers auf die SD-Karte geschrieben.

Für das Wemos D1 Mini Board konnte ich damit keine Steigerung der Schreibgeschwindigkeit erzielen. Auf dem ESP32 funktioniert er nicht, aber weitere Erklärungen dazu würden zu weit führen.

SD-Karten als Data Logger

Eine der Hauptanwendungen für SD-Karten im Arduinobereich ist der Einsatz als Datenlogger. In den Beispielsketchen findet ihr dazu einen einfachen Sketch. Ich bin einen Schritt weitergegangen und steuere die Aufnahme der Werte zeitlich über einen Alarm eines DS3231 RTC Moduls. Das hat den Vorteil, dass ihr Datum und Uhrzeit eures Messwertes festhalten könnt. Überdies könnt ihr euren Mikrocontroller zwischenzeitlich in den Schlaf schicken und durch den DS3231 aufwecken. Hier die Basisschaltung:

Datalogger mit dem DS3231
Datalogger mit dem DS3231

Um einfach irgendetwas zu messen, habe ich ein Poti an den analogen Eingang A1 gehängt. Ihr könnt das ja durch etwas Spannenderes ersetzen. Hier der Sketch:

#include <RTClib.h>
#include <Wire.h>
#include <SD.h>
#include <avr/sleep.h> // comment if you don't use an AVR based Board
const int chipSelectPin = 10;
const int clockInterruptPin = 2;
const int analogPin = A1;
String myFile = "data_log.csv";

RTC_DS3231 rtc;

void setup() {
  Serial.begin(9600);
  pinMode(analogPin, INPUT);
  
  SD.begin(chipSelectPin); 
  SD.remove(myFile); 
  File dataFile = SD.open(myFile, FILE_WRITE);
  dataFile.println("Time;analogRead"); // you my need a "," instead of ";"
  dataFile.close();
  
  rtc.begin();
  rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // set time to system at when compiling    
  rtc.disable32K(); //disable 32K pin
  rtc.clearAlarm(1); // in case there's still an alarm 
  rtc.clearAlarm(2);
  rtc.writeSqwPinMode(DS3231_OFF); // we use the SQW pin for interrupts
  rtc.disableAlarm(2); // we don't need alarm 2
  rtc.setAlarm1(rtc.now() + TimeSpan(10), DS3231_A1_Second);  // alarm in 10 seconds

  pinMode(clockInterruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(clockInterruptPin), onAlarm, FALLING);
}

void loop() {
  if(rtc.alarmFired(1)) {
    unsigned int measuredValue = analogRead(analogPin);
    char date[18] = "DD.MM.YY hh:mm:ss";
    rtc.now().toString(date);
    rtc.clearAlarm(1);
    
    Serial.println(date);
    Serial.print("Analog Read: "); 
    Serial.println(measuredValue);
    File dataFile = SD.open(myFile, FILE_WRITE); // FILE_APPEND for ESP32
    dataFile.print(date);
    dataFile.print(";");
    dataFile.println(measuredValue);
    dataFile.close();
    rtc.setAlarm1(rtc.now() + TimeSpan(10),DS3231_A1_Second);
    delay(100); // give some time for Serial.print() / Data.print() to complete
      
    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // choose power down mode, comment if you don't use an AVR based Board
    sleep_mode(); // sleep now! // comment if you don't use an AVR based Board
  }  
}

void onAlarm() {
    Serial.print("Alarm at: ");
}

 

Erklärungen zu data_logger.ino

Um den Beitrag nicht zu lang werden zu lassen, gehe ich nicht auf die Details der DS3231 Ansteuerung ein. Schaut dazu bitte in diesen Beitrag. In groben Zügen macht der Sketch Folgendes:

  • Alle 10 Sekunden löst der DS3231 einen Alarm aus.
  • Der Alarm weckt den Arduino, der dann einen Messwert aufnimmt.
  • Der Messwert wird auf die SD-Karte in die Datei data_log.csv geschrieben, zusammen mit Datum und Uhrzeit. Das Format ist: „DD.MM.YY hh:mm:ss;Messwert“.
  • Die erste Zeile in data_log.csv ist „Time;analogRead“ und dient als Überschrift.
  • Messwert und Uhrzeit werden auch auf dem seriellen Monitor ausgegeben, was nur der Kontrolle dient.
  • Der Arduino wird in den Schlaf geschickt und alles beginnt wieder von vorn.

So sah die Ausgabe auf dem seriellen Monitor aus:

Ausgabe von data_logger.ino
Ausgabe von data_logger.ino

Anzeigen / Auswerten der Daten in Excel

Das Schöne an „.csv“-Dateien ist, dass ihr sie in Excel auswerten könnt, sofern sie richtig formatiert sind. Richtig formatiert heißt:

  • Ein Wertesatz pro Zeile.
  • Die Werte werden durch ein Trennzeichen, in Deutschland ein Semikolon, getrennt.
  • Excel kann „.csv“-Dateien einlesen, wenn diese maximal 1.048.576 (=220) Zeilen enthalten.

Das Trennzeichen ist nicht in Excel, sondern im Betriebssystem festgelegt. In Windows 11 könnt ihr das ändern unter: Einstellungen → Zeit und Sprache → Sprache und Region → Administrative Spracheinstellungen → Tab: Formate → Weitere Einstellungen → Listentrennzeichen. Gut versteckt!

Ihr nehmt also eure „.csv“-Datei und importiert sie in Excel:

data_log.csv nach Import in Excel
data_log.csv nach Import in Excel

Keine Sorge, die Sekunden sind nicht weg. Wenn ihr in die einzelnen Zeilen klickt, dann werden sie angezeigt.

Und nun könnt ihr statistische Auswertungen vornehmen oder die vielen Optionen zur grafischen Darstellung nutzen, beispielsweise so:

Darstellung der Messdaten in Excel
Darstellung der Messdaten in Excel

Mehrere SD-Kartenmodule verwenden

Ihr wollt mehrere SD-Kartenmodule verwenden? Kein Problem. Ihr müsst lediglich für jede SD-Karte bzw. jedes SD-Kartenmodul ein eigenes SDClass Objekt erzeugen und einen CS-Pin zuweisen. Die MISO-, MOSI- und SCK-Leitungen teilen sich die SD-Karten. So würde das für zwei Module aussehen:

#include <SD.h>
const int csPin_1 = 9;
const int csPin_2 = 10;

SDClass mySD_1;
SDClass mySD_2;

void setup(){
  Serial.begin(9600);  
  if(!mySD_1.begin(csPin_1)){
    Serial.println("SD-Card 1 not connected!");
    while(1);
  }
  else{
    Serial.println("SD-Card 1 initialized");
  }

  if(!mySD_2.begin(csPin_2)){
    Serial.println("SD-Card 2 not connected!");
    while(1);
  }
  else{
    Serial.println("SD-Card 2 initialized");
  }
}

void loop(){}

Auf einem ESP32 könntet ihr alternativ zwei separate SPI-Schnittstellen nutzen.

15 thoughts on “SD-Karten und SD-Kartenmodule

  1. Hallo,
    danke für das Tutorial. Ich kämpfe gerade mit dem ESP32 und habedort die SD auf HSPI angeschlossen. Wollte eigentlich nur drei Dateien darauf speichern. Er schleift mir immer das FS.h mit. Das kommt von Espressif und eigentlich brauch ich es nicht und ehrlich, verstehs auch nicht, da ich keine Erklärung irgentwo gefunden habe. Hast du eine Idee wie man „normal“ SD auf HSPI zum laufen bringt. Mit dem FS funktionierts. Mit SD allein nicht.

    Danke
    Klaus

      1. Hallo Wolfgang,
        sorry konnte mich leider nicht eher melden.
        Ich habe einen ESP32, den ich mit der Arduino 2.2.1 IDE programmiere. Vorher habe ich die ATMEL 2560 benutzt. Beim ESP32 habe in HSPI die SD-Carte angeschlossen. Man kann ein Beispiel für die SD Karte herunterladen.. Es ist die Datei SD_Test. Wenn ich den HSPI erkläre läuft das Programm normal durch und ich kann auf die SD-Karte schreiben und lesen. Im Sketch wird jedoch noch das FS.h als include mit aufgerufen. Da ich kein filesystem haben will, wollte ich es so betreiben, wie ich es bei den 2560 gemacht habe. Ganz normal über die SD.h.
        Gehe ich jetzt her und nutze nur SD.h und verwende die Befehle wie unter dem ATMEGA, so läuft es nicht. Ich habe jetzt in der .h und .ccp Datei etwas gesucht und gefunden, dass dort das FS.h wieder aufgerufen wird. Das meinte ich mit mitschleifen.
        Was ich suche, ist eine SD Library, die mich die normalen Befehle der Arduino Umgebung für SD Karten verwenden lässt und auf dem ESP32 läuft.
        Das Ansprechen der HSPI Schnittstelle, das funktioniert. Das ist genauso erklärt, wie du es beschrieben hast.
        Ich habe dann bei Espressif nachgeschaut, und nach deren Matrix muss ich wohl immer das Filessystem mit dabei haben.
        Hast du da schon Erfahrung gesammelt?

        Gruß
        klaus

        1. Hallo Klaus,
          leider habe ich bzgl. dieses Problems keine Erfahrung. Es reizt mich zwar schon wieder da tiefer einzusteigen und selbst herumzuprobieren und zu recherchieren, nur komme ich zeitlich an meine Grenzen. Vielleicht meldet sich ja ein anderer Leser dazu.
          VG, Wolfgang

  2. Hallo Wolle,

    ich habe deinen Datenlogger nachgebaut. Ich habe auch die LEDs auf dem RTC Modus und auf dem Pro Mini entfernt. Ich komme aber immer noch auf 8 mA Stromverbrauch, wenn er in den Schlafmodus geht.

    Ohne Peripherie braucht er nur 0,15mA.

    Hast du eine Idee, woher der hohe mA Verbrauch herkommt und wie ich diesen senken kann? Wünschenswert wäre, wenn ich auf 1-2mA Verbrauch kommen kann.

    1. Hi Sven,
      das von mir getestete SD-Kartenmodul hat 1.8 mA verbraucht. Das DS3231-Modul verbraucht nach meinen Messungen 3.6 mA bei Stromversorgung über VCC. Wenn du einen Akku im DS3231 verwendest und der gerade geladen wird, dann ist es noch mehr. Das Hauptaugenmerk würde ich also erst einmal auf den DS3231 legen, da gibt es mehrere Möglichkeiten:
      – Wenn du im VCC Betrieb bleiben willst, dann entferne die Betriebs-LED auf dem DS3231 Modul. Nach meinen Messungen ist der Verbrauch dann bei weniger als 1 mA. Außerdem betreibe das Modul mit 3.3 anstelle 5 V.
      – Oder: Lass den DS3231 im Batteriebetrieb laufen. Trenne also die Verbindung zu VCC. GND muss aber verbunden bleiben, sonst klappt die SPI Verbindung nicht.
      – Du könntest auch die Stromversorgung für die beiden Module nur bei Bedarf zuschalten. Das wäre die stromsparendste Lösung. Als „Schalter“ könntest du einen kleinen MOSFET verwenden.
      Viel Erfolg! VG, Wolfgang

  3. Hallo Wolfgang ! Ein Super Artikel und auch fein die Unterschiede zum ESP32 erklärt. TOP ! Den besten Artikel, den ich bisher gefunden habe.
    ABER (Du kannst nix dafür. Ich weiss), leider ist es so, das mit der Standard LIB „SD“ (ESP32) kein TimeStamp im Dateisystem für „erstellt“ und „geändert“ abgelegt wird. Danach habe ich eigentlich gesucht 🙂
    Das man in den Dateinamen das Datum einbauen kann, weiss ich. Aber ich möchte es auch mit den TimeStamps haben. Also muss ich weitersuchen.

    1. Frage doch einfach: Wolfgang, kennst du eine Möglichkeit, dass Timestamps im Dateisystem übernommen werden? Antwort, ja, kenne ich. Library SdFat, example „RtcTimestampTest.ino“:
      https://github.com/greiman/SdFat/blob/master/examples/RtcTimestampTest/RtcTimestampTest.ino
      Das funktioniert. Wenn man keine RTC angeschlossen hat, dann sollte man Zeile 12 auf #define RTC_TYPE 0 ändern.
      Ich habe die SdFat nicht in meinem Beitrag behandelt, da sie viel wahrscheinlich überfordert.

      1. Guten Morgen Wolfgang !
        Mir lag es am Herzen mich erst mal zu Bedanken! Das war mir wichtig.

        Deine Glaskugel funktioniert übrigens gut. Ich habe mich bis 1:00 Uhr heute morgen schon mit SdFat beschäftigt.
        Leider klappt da noch nix richtig. Die RTClib V 2.1.1 erzeugte Fehlermeldungen beim Compilieren und ich bin erst mal auf 2.0.0 zurückgegangen. RTC_TYPE 0 hatte ich auch gemacht, da die RTC Module noch unterwegs sind. SD Card Typ und Grösse werden erkannt. Weiter bin ich in der Nacht noch nicht gekommen.

  4. Hallo Wolfgang,

    ich habe Deinen Artikel mit Aufmerksamkeit gelesen und möchte dieses Modul an einen SAM-Prozessor von Microchip anschließen.
    Leider habe ich dazu kein Manuell beim Hersteller gefunden.
    Hast Du den kompletten Befehlssatz?

    Gruß und schöne Weihnachten
    Wolfgang

    1. Hallo Wolfgang,

      ich habe bisher noch keinen SAM Prozessor direkt programmiert, sondern nur Arduino Boards, die den SAMD21 Cortex®-M0+ 32bit low power ARM MCU Prozessor verwenden, wie den Arduino Nano 33 IoT. Wenn man mit diesen Boards arbeitet und die SD Bibliothek einbindet, dann ist es dieselbe, die auch für die AVR Arduino Boards verwendet wird, d. h. die Befehle für die Nutzung der SD Karten sind identisch. Oder habe ich die Frage falsch verstanden?

      Dir auch schöne Weihnachten!

      VG, Wolfgang

  5. Hallo,
    wieder einmal ein sehr guter Lehrgang.
    Ich hätte eine Anmerkung, das SPIFFS Filesystem ist für ESPs deprecated. LittleFS soll der Nachfolger werden.
    Andererseits frage ich mich, warum überhaupt extra Filesysteme, vor allem bei größeren Controllern, wie ESP8266 und ESP32. Dafür gibt es doch dte FFAT (FATFS) Lib für FAT/EXFAT.

    1. Hallo, stimmt, vielen Dank, da war ja was! Hab ich gleich abgeändert. Warum man keine bewährten Dateisysteme implementiert hat, das weiß ich schlicht nicht.

      VG, Wolfgang

      1. Soviel ich weiss existiert eine FATFS implementation mit Free Rtos in der ESP-IDF, ich habe damit allerdings noch nicht gearbeitet.
        Bisher habe ich FATFS bisher nur auf STM32F411 ausprobiert, ohne Arduino in der STM32CubeIde.

        Gruß Hubert

Schreibe einen Kommentar

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