Vorstellung des M5Stack Atomic TF-Card Moduls: SD-Kartenleser inkl. Atom Lite

In einen vorhergehenden Artikel hatte ich ja den M5Stack Atom Lite selbst vorgestellt. Dort habe ich erklärt, dass er auf dem ESP32-PICO-D4 basiert und das Pinout beschrieben. Außerdem bin ich auf den Knopf, den I2C-Port, den SPI-Port, und die interne RGB-LED eingegangen.

Ich hatte erklärt, wie man den Atom Lite einrichtet und in der Arduino IDE programmiert und habe ein kleines Beispielprogramm zum austesten geschrieben.



Den Atom Lite gibt es aber nicht nur allein als 24x24x9.5 mm-Modul zu kaufen (ca. 7.50€ Preisstand Feb. 2022), sondern es gibt ihn auch in Kombination mit weiteren Modulen, in der er dann eingesteckt wird und sich damit in ein größeres Gehäuse einschmiegt und den 9-poligen Anschluss auf der Unterseite des Atom Lite mit dem größeren Gehäuse teilt.

Das ist eine von zwei Möglichkeiten, den M5Stack Atom Lite mit Peripherie zu erweitern; die andere ist über den I2C-Bus, für den ich im nächsten Artikel ein Beispiel liefern werde.

Es gibt zahlreiche Module, in der der Atom Lite eingesteckt werden kann, z. B. das DIY Proto Kit mit einer Mini-Platine zum selbst löten, einen Lautsprecher, ein PoE Kit, ein LCD-Treiber Kit, ein Kit für den CAN-Bus, oder ein GPS-Empfänger-Kit (mit über 30 Euro leider etwas teuer finde ich).


Oder eben das M5Stack Atomic TF-Card Reader Dev Kit, das ich heute vorstellen möchte. Der Preis ist mit 11.50 € (Preisstand Feb. 2022) nicht wesentlich höher als der nackte Atom Lite. Man zahlt also 4 Euro Aufpreis für den TF-Card-Reader und das Umgehäuse. Das ist ein fairer Preis finde ich.

Im Kit sind außerdem ein sehr kurz geratenes USB-C-Kabel und ein Tütchen mit Inbus-Schlüssel und einer kleinen Schraube. Das ganze kommt in einer hübschen kleinen Plastikbox, so dass man für einen Transport alles beisamen hat und es geschützt ist.


Allerdings ist auch nicht allzuviel drin in dem Zusatz-Gehäuse. Aber hey, wer weiß, wieviel Arbeit es macht, ein genau passendes Gehäuse mit dem 3D-Drucker zu drucken und natürlich vorher zu entwerfen, der weiß das zu schätzen.

Es ist eigentlich nur eine kleine Platine, die unten im Gehäuse eingeschraubt ist. An dessen Vorderseite sind die 9 Pins für den Header für SPI, I2C und ein bisschen mehr herausgeführt, auf den der Atom Lite gesteckt wird. Und im hinteren Teil ist dann der SD-Karten-Controller und der Einschub für die Micro SD-Karte, oder wie man international sagt "TF-Card".


Der 9-polige Bus wird allerdings nirgends durchgeschleift und erneut herausgeführt. Auf der Unterseite gibt es nur Verschraubungsmöglichkeiten und Gummi-Füßchen, ja, und ein Loch, um das Teil an einer Schraube an die Wand zu hängen ;)

Es gibt aber eine Möglichkeit für Bastler. Neben den Header-Pins gibt es Lötösen auf der Platine, die sogar vorbildlich beschriftet sind. Hier könnte man ein Kabel nach hinten heraus führen. Vielleicht ist das Loch auf der Rückseite ja dafür gedacht?

Aber eigentlich sollte das M5Stack-System doch für Nicht-Löter gemacht sein? Aber dieses Manko, dass man die Elemente nicht mehrmals aneinander reihen kann, habe ich ja schon im Vorstellungsartikel erwähnt gehabt.

Das Kit-Gehäuse samt Atom kommt auf die Ausmaße 24 x 48 x 19 mm, ist also doppelt so lang und doppelt so hoch wie der Atom allein, oder anders ausgedrückt: nimmt den Platz von 4 Atom Lites ein.

Das SD-Kartenleser-Kit Atomic TF-Card

SD-Kartenleser gibt es ja schon für kleines Geld für Arduino und Co. zu kaufen. Das Thema SD-Kartenleser habe ich schon vor längeren Zeit behandelt und den Artikel RC522-RFID und SD-Kartenleser mit OLED-Display (128x64 px, 0.96") an STM32 betreiben dazu geschrieben.

Und man kann sie eigentlich immer gebrauchen. Zum Beispiel, wenn man etwas protokollieren will, wie eine Temperatur über den Tag verteilt ist. Dann kann man nach einiger Zeit die SD-Karte entnehmen und am PC auswerten, sich Kurven zeichnen lassen etc. pp.

Was bekommen wir hier für einen SD-Kartenleser geliefert? Die Specs lesen sich so: Oho! Nur bis zu 16 GB große Micro-SD-Karten sollen unterstützt werden? Das wäre übel. So etwas habe ich eigentlich gar nicht mehr. Das Preis/Leistungsverhältnis unter 32 GB ist nicht gut und wenn, dann kaufe ich heutzutage minimum 32 GB, besser 128 GB.

Da ich nur eine mindestens 32 GB große SD-Karte da habe, versuche ich es einfach mal damit. Es ist eine mit FAT32 unter Windows formatierte MicroSD-Karte, eine 32 GB SanDisk Ultra A1 HC 1.

Den Kartenleser testen

Das von M5Stack mitgelieferte Beispielprogramm wandle ich ein wenig ab und lasse die interne LED in blau leuchten, wenn auf die SD-Karte zugegriffen wird. Außerdem kapsle ich die SD-Kartenfunktionen in einer Datei namens "cardreader.h" und erweitere die Funktionen mit Returnwerten und füge weitere, eigenen Funktionen dazu.

cardreader.h (klicken, um diesen Abschnitt aufzuklappen)
//////////////////////////////////////////////////////// // cardreader.h für M5Stack Atomic TF-Card // // V 1.00, 2022-02-24 // // (C) 2022 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: https://cool-web.de/esp8266-esp32/ // //////////////////////////////////////////////////////// int initCardReader() { M5.dis.drawpix(0, 0x0000FF); SPI.begin(23,33,19,-1); // CLK, MISO, MOSI, SS? if(!SD.begin(-1, SPI, 10000000)){ Serial.println("Fehler: kein SD-Kartenleser oder keine Karte eingelegt."); M5.dis.drawpix(0, 0x000000); return -10; } uint8_t cardType = SD.cardType(); if(cardType == CARD_NONE){ Serial.println("Fehler: keine SD-Karte erkannt."); M5.dis.drawpix(0, 0x000000); return -20; } Serial.print("SD Kartentyp: "); 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("(unbekannt)"); } uint64_t cardSize = SD.cardSize() / (1024 * 1024); Serial.printf("SD Kartengroesse: %6llu MB\n", cardSize); uint64_t cardUsed = SD.usedBytes() / (1024 * 1024); Serial.printf("SD bereits belegt: %6llu MB\n", cardUsed); uint64_t cardFree = cardSize - cardUsed; Serial.printf("SD noch frei: %6llu MB\n", cardFree); if (cardSize <= 0) { M5.dis.drawpix(0, 0x000000); return -30; } M5.dis.drawpix(0, 0x000000); return 0; } //Listing directory. int listDir(fs::FS &fs, const char * dirname, uint8_t levels){ M5.dis.drawpix(0, 0x0000FF); Serial.printf("Listing directory: %s\n", dirname); File root = fs.open(dirname); if(!root){ Serial.println("Failed to open directory"); M5.dis.drawpix(0, 0x000000); return -10; } if(!root.isDirectory()){ Serial.println("Not a directory"); M5.dis.drawpix(0, 0x000000); return -20; } File file = root.openNextFile(); while(file){ if(file.isDirectory()){ Serial.print(" DIR : "); Serial.println(file.name()); if(levels){ listDir(fs, file.name(), levels -1); } } else { Serial.print(" FILE: "); Serial.print(file.name()); Serial.print(" SIZE: "); Serial.println(file.size()); } file = root.openNextFile(); } M5.dis.drawpix(0, 0x000000); return 0; } //Creating Dir. int createDir(fs::FS &fs, const char * path){ M5.dis.drawpix(0, 0x0000ff); Serial.printf("Creating Dir: %s\n", path); if(fs.mkdir(path)){ Serial.println("Dir created"); M5.dis.drawpix(0, 0x000000); return 0; } else { Serial.println("mkdir failed"); M5.dis.drawpix(0, 0x000000); return -10; } } //Removing Dir. int removeDir(fs::FS &fs, const char * path){ M5.dis.drawpix(0, 0x0000ff); Serial.printf("Removing Dir: %s\n", path); if(fs.rmdir(path)){ Serial.println("Dir removed"); M5.dis.drawpix(0, 0x000000); return 0; } else { Serial.println("rmdir failed"); M5.dis.drawpix(0, 0x000000); return -10; } } //Test if File exist. boolean existFile (fs::FS &fs, const char * path){ M5.dis.drawpix(0, 0x0000ff); Serial.printf("Testing on existence of file: %s\n", path); File file = fs.open(path); M5.dis.drawpix(0, 0x000000); if(!file){ return false; } else { return true; } } //Reading file. int readFile(fs::FS &fs, const char * path){ M5.dis.drawpix(0, 0x0000ff); Serial.printf("Reading file: %s\n", path); File file = fs.open(path); if(!file){ Serial.println("Failed to open file for reading"); M5.dis.drawpix(0, 0x000000); return -10; } Serial.print("Read from file: "); while(file.available()){ Serial.write(file.read()); } file.close(); M5.dis.drawpix(0, 0x000000); return 0; } //Writing file int writeFile(fs::FS &fs, const char * path, const char * message){ M5.dis.drawpix(0, 0x0000ff); Serial.printf("Writing file: %s\n", path); File file = fs.open(path, FILE_WRITE); if(!file){ Serial.println("Failed to open file for writing"); M5.dis.drawpix(0, 0x000000); return -10; } if(file.print(message)){ Serial.println("File written"); } else { Serial.println("Write failed"); M5.dis.drawpix(0, 0x000000); return -20; } file.close(); M5.dis.drawpix(0, 0x000000); return 0; } //Appending to file int appendFile(fs::FS &fs, const char * path, const char * message){ M5.dis.drawpix(0, 0x0000ff); Serial.printf("Appending to file: %s\n", path); File file = fs.open(path, FILE_APPEND); if(!file){ Serial.println("Failed to open file for appending"); M5.dis.drawpix(0, 0x000000); return -10; } if(file.print(message)){ Serial.println("Message appended"); } else { Serial.println("Append failed"); M5.dis.drawpix(0, 0x000000); return -20; } file.close(); M5.dis.drawpix(0, 0x000000); return 0; } //Renaming file int renameFile(fs::FS &fs, const char * path1, const char * path2){ M5.dis.drawpix(0, 0x0000ff); Serial.printf("Renaming file %s to %s\n", path1, path2); if (fs.rename(path1, path2)) { Serial.println("File renamed"); M5.dis.drawpix(0, 0x000000); return 0; } else { Serial.println("Rename failed"); M5.dis.drawpix(0, 0x000000); return -10; } } //Deleting file int deleteFile(fs::FS &fs, const char * path){ M5.dis.drawpix(0, 0x0000ff); Serial.printf("Deleting file: %s\n", path); if(fs.remove(path)){ Serial.println("File deleted"); M5.dis.drawpix(0, 0x000000); return 0; } else { Serial.println("Delete failed"); M5.dis.drawpix(0, 0x000000); return -10; } } int testFileIO(fs::FS &fs, const char * path){ M5.dis.drawpix(0, 0x0000ff); int ret = 0; File file = fs.open(path); static uint8_t buf[512]; size_t len = 0; uint32_t start = millis(); uint32_t end = start; if(file){ len = file.size(); size_t flen = len; start = millis(); while(len){ size_t toRead = len; if(toRead > 512){ toRead = 512; } file.read(buf, toRead); len -= toRead; } end = millis() - start; Serial.printf("%u bytes read for %u ms\n", flen, end); Serial.printf("read rate: %u kB/s\n", 1048576 / end ); file.close(); } else { Serial.println("Failed to open file for reading"); ret -= 10; } file = fs.open(path, FILE_WRITE); if(!file){ Serial.println("Failed to open file for writing"); ret -= 20; M5.dis.drawpix(0, 0x000000); return ret; } size_t i; start = millis(); for(i=0; i<2048; i++){ file.write(buf, 512); } end = millis() - start; Serial.printf("%u bytes written in %u ms\n", 2048 * 512, end); Serial.printf("write rate: %u kB/s\n", 1048576 / end ); file.close(); M5.dis.drawpix(0, 0x000000); return ret; } int testCardReader() { int ret=0; ret -= listDir(SD, "/", 0); ret -= createDir(SD, "/mydir"); ret -= listDir(SD, "/", 0); ret -= removeDir(SD, "/mydir"); ret -= listDir(SD, "/", 2); ret -= writeFile(SD, "/hello.txt", "Hello "); ret -= appendFile(SD, "/hello.txt", "World!\n"); ret -= readFile(SD, "/hello.txt"); if (existFile (SD, "/foo.txt")) { ret -= deleteFile(SD, "/foo.txt"); } ret -= renameFile(SD, "/hello.txt", "/foo.txt"); delay(100); ret -= readFile(SD, "/foo.txt"); ret -= deleteFile(SD, "/foo.txt"); ret -= testFileIO(SD, "/test.txt"); if (ret == 0) { Serial.println ("Alle Tests bestanden."); M5.dis.drawpix(0, 0x00ff00); } else { Serial.println ("Es traten Fehler beim Test auf."); M5.dis.drawpix(0, 0xff0000); } return ret; }

In meinem Hauptprogramm, das ich zum Testen der Funktionen des Atom Lite benutze, befindet sich noch der folgende Code. Die Zeilen, die die IR-LED testen, die nicht so richtig funktionieren will, habe ich einmal rausgeschmissen. Darum geht es hier ja nicht.
//////////////////////////////////////////////////////// // m5stack-atom-lite-test.ino // // V1.2, 2022-02-24 // // (C) 2022 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: https://cool-web.de/esp8266-esp32/ // //////////////////////////////////////////////////////// // API-Dokumentationen: // https://docs.m5stack.com/en/api/atom/system // https://docs.m5stack.com/en/api/atom/led_display #include "M5Atom.h" // Für TF-Card-Reader #include <SPI.h> #include "FS.h" #include "SD.h" #include "cardreader.h" void setup() { // Serial.begin(115200); // erledigt M5.begin M5.begin(true, false, true); // void begin(bool LCDEnable=true, bool SDEnable=true, bool SerialEnable=true,bool I2CEnable=false); delay(100); M5.dis.drawpix(0, 0x000000); // Der Atom Lite hat nur eine LED (Nr. 0); es folgen die Farbwerte in Hex für RGB (rot, grün, blau) Serial.println("Teste TF-Kartenleser..."); int ret = initCardReader(); if (ret != 0) { // Fehler aufgetreten, SD-Kartenleser nicht benutzbar Serial.println("SD-Kartenleser nicht benutzbar."); M5.dis.drawpix(0, 0xFF0000); } else { M5.dis.drawpix(0, 0x00FF00); delay (100); ret = testCardReader(); } } uint32_t color; void loop() { M5.update(); // Auslesen des Knopf-Status if (M5.Btn.wasPressed()){ // wenn Knopf gedrückt wurde color=random(pow(2,24)); // Zufallsfarbwert holen M5.dis.drawpix(0, color); // und die LED in dieser Farbe leuchten lassen while (M5.Btn.wasPressed()){ // warten, bis Knopf wieder losgelassen M5.update(); delay(10); } } delay(10);
} Was macht das Programm? Nun, es testet, ob es auf den Kartenleser zugreifen kann. Dann, ob eine gültige Karte eingelegt ist. Dann liest es die Verzeichnisse auf der Karte, legt eines an, legt ein File an, schreibt es, liest es, benennt es um und löscht es. Zum Schluss testet es noch den Datendurchsatz. Die Zugriffe auf die SD-Karte zeigt es mit einer blauen LED an.

Liefen alle Tests ohne Fehler durch, ist die LED danach grün; bei Fehlern rot. Dann sollte man das Debug-Protokoll auf der seriellen Schnittstelle lesen, das anzeigt, was passiert und was daneben gegangen ist.

Schauen wir uns das Protokoll für die 32 GB SanDisk Ultra A1 HC 1 einmal an:
Teste TF-Kartenleser... SD Kartentyp: SDHC SD Kartengroesse: 30436 MB SD bereits belegt: 1 MB SD noch frei: 30435 MB Listing directory: / FILE: /test.txt SIZE: 1048576 DIR : /Dir1 DIR : /Dir2 DIR : /Dir3 FILE: /file1.txt SIZE: 7 FILE: /file2.txt SIZE: 7 FILE: /file3.txt SIZE: 7 Creating Dir: /mydir Dir created Listing directory: / FILE: /test.txt SIZE: 1048576 DIR : /mydir DIR : /Dir1 DIR : /Dir2 DIR : /Dir3 FILE: /file1.txt SIZE: 7 FILE: /file2.txt SIZE: 7 FILE: /file3.txt SIZE: 7 Removing Dir: /mydir Dir removed Listing directory: / FILE: /test.txt SIZE: 1048576 DIR : /Dir1 Listing directory: /Dir1 DIR : /Dir2 Listing directory: /Dir2 DIR : /Dir3 Listing directory: /Dir3 FILE: /Dir3/file4.txt SIZE: 7 FILE: /file1.txt SIZE: 7 FILE: /file2.txt SIZE: 7 FILE: /file3.txt SIZE: 7 Writing file: /hello.txt File written Appending to file: /hello.txt Message appended Reading file: /hello.txt Read from file: Hello World! Testing on existence of file: /foo.txt Renaming file /hello.txt to /foo.txt File renamed Reading file: /foo.txt Read from file: Hello World! Deleting file: /foo.txt File deleted 1048576 bytes read for 1590 ms read rate: 659 kB/s 1048576 bytes written in 3078 ms write rate: 340 kB/s Alle Tests bestanden.
Na bravo. Es gehen also doch 32 GB. Zumindest mit der SanDisk. Wer weiß, wie das bei anderen Herstellern ist.

Als nächstes teste ich eine 64 GB Transcend Premium 400x XC1, unter Windows 7 mit exFAT formatiert.
Teste TF-Kartenleser... Fehler: kein SD-Kartenleser oder keine Karte eingelegt. SD-Kartenleser nicht benutzbar.
Leider kein Glück. Das hat nicht geklappt. Ich schätze mal, der SD-Kartenleser stört sich an exFAT, weil er das nicht kann.

Also versuche ich mal das Tool fat32format.exe, das auch größere Partitionen mit FAT32 formatieren kann:
D:\tools\usb-formatter>fat32format.exe s: Warning ALL data on drive 's' will be lost irretrievably, are you sure (y/n) :y Size : 63GB 124745728 sectors 512 Bytes Per Sector, Cluster size 32768 bytes Volume ID is 13f3:f52 32 Reserved Sectors, 15225 Sectors per FAT, 2 fats 1948675 Total clusters 1948674 Free Clusters Formatting drive s:... Clearing out 30546 sectors for Reserved sectors, fats and root cluster... Wrote 15639552 bytes in 1.17189 seconds, 13345618.962206 bytes/sec Initialising reserved sectors and FATs... Done
Damit funktioniert die 64 GB Karte dann doch noch:
Teste TF-Kartenleser... SD Kartentyp: SDHC SD Kartengroesse: 60927 MB SD bereits belegt: 1 MB SD noch frei: 60926 MB Listing directory: / FILE: /test.txt SIZE: 1048576 DIR : /Dir1 FILE: /file1.txt SIZE: 0 Creating Dir: /mydir Dir created Listing directory: / FILE: /test.txt SIZE: 1048576 DIR : /mydir DIR : /Dir1 FILE: /file1.txt SIZE: 0 Removing Dir: /mydir Dir removed Listing directory: / FILE: /test.txt SIZE: 1048576 DIR : /Dir1 Listing directory: /Dir1 FILE: /file1.txt SIZE: 0 Writing file: /hello.txt File written Appending to file: /hello.txt Message appended Reading file: /hello.txt Read from file: Hello World! Testing on existence of file: /foo.txt Renaming file /hello.txt to /foo.txt File renamed Reading file: /foo.txt Read from file: Hello World! Deleting file: /foo.txt File deleted 1048576 bytes read for 1900 ms read rate: 551 kB/s 1048576 bytes written in 28676 ms write rate: 36 kB/s Alle Tests bestanden.
Allerdings ist die Schreibrate unterirdisch. Vielleicht liegt das an der komischen von Formatierungsprogramm erzwungenen Sektorgröße von 512 Bytes? Das HP USB Disk Storage Format Tool, das ich noch als alternativen Formater hätte, lehnt das Formatieren auf FAT32 mit der Begründung ab, dass die Karte zu groß sei.

Fazit

Speicherkarten bis 32 GB scheinen zu gehen. Darüber wird es kompliziert und langsam und man sollte es lieber lassen. Wer sicher gehen will, nimmt nur 8 GB oder 16 GB Micro-SD-Karten, wobei wohl auch schon 1 GB für die allermeisten Anwendungen ausreichend ist. Man protokolliert ja meist nur in Textdateien.

Die Durchsatzraten sind auf dem Atom Lite mit dem TF-Card-Kit nicht toll, aber für einen Mikrocontroller über SPI-Interface nicht schlecht und eigentlich ausreichend.

Falls man hin und wieder etwas protokollieren will, kann ich die 4 Euro Mehrausgabe ruhigen Gewissens empfehlen. Sie sind es sicher wert.