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:
- federunterstützter Auswurf mit Klick-Mechanismus (Self elastic TF-card (microSD) slot)
- Unterstützte Karten: 16G FAT/FAT32 MicroSD
- Gewicht: 23g netto, 33g brutto
- Produktgröße: 24x48x18mm
- Packungsgröße: 54x54x20mm
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);
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.
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.
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
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.
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.