1,8" TFT Display und SD-Kartenleser gleichzeitig am ESP32 betreiben - AZ-Delivery

Dieser Beitrag wurde uns von dem Gastautoren Rudolf Reiber zugesand:

Vorbemerkung

Vor gut vier Jahren baute ich einen GPS-Tracker, um im Auto die gefahrenen Strecken aufzuzeichnen. Dazu verwende ich ein ESP32 DEV Modul, ein 20x4 LCD Display, ein GPS Modul sowie den separaten Kartenleser.

Vor einigen Wochen kam ich auf die Idee, das LCD Display gegen das TFT Display auszutauschen und dabei auch gleich den hierauf mit vorhandenen Kartenleser zu verwenden.

Hierbei stellte sich das Problem, dass ich zwar für den Anschluss des Displays Anleitungen fand, aber nirgendwo war zu finden, wie das Display und der Kartenleser gleichzeitig zu betreiben sein. Beide Teile werden mit dem SPI Protokoll angesteuert.

Hardware

Bei dem Projekt eingesetzte Hardware:

  1. TFT-Display mit Kartenleser: 1,8 Zoll SPI TFT Display 128 x 160 Pixeln kompatibel mit Arduino und Raspberry Pi
  1. ESP32 DEV Kit: ESP32 NodeMCU Module WLAN WiFi Development Board | Dev Kit C V2 mit CP2102 kompatibel mit Arduino
  1. Kabel
  2. Breadboard
  3. Anstatt des im TFT-Display verbauten Kartenlesers kann man auch diesen Leser verwenden. Zusätzlich ist dann noch Vcc (5,0V) und GND (0V) zu verdrahten: SPI Reader Micro Speicher SD TF Karte Memory Card Shield Modul

Seltsames

Zur Programmierung der ESP32-Module verwende ich gerne das Programm “Visual Studio Code” weil mir der dortige Editor viel besser gefällt. Doch dieses Mal wurde mir das zum Verhängnis. Obwohl der Sketch fehlerfrei übersetzt wurde, kam meistens die Meldung, dass keine Karte gefunden wurde. Ausprobiert mit 5 verschiedenen SD-Karten. Alle waren korrekt mit FAT32 initialisiert. Ich verbrachte Stunden damit dieses Problem zu lösen, aber alles hatte keinen Erfolg.

Bis ich, warum auch immer, auf die Idee kam, das Ganze doch mal mit der ARDUINO-IDE zu versuchen. Und siehe da, die Karten wurden problemlos eingebunden, alle 5! Es ist also offensichtlich so, dass IDE und VSC bei gleichem Quellcode verschiedene Maschinencodes liefern können. Das ist nun wirklich sehr ärgerlich! Wie soll man darauf kommen?

Weitere Überlegungen

Ich verwende dieses Display:

Man kann zwar mit einem SPI-Bus mehrere Clients (Slaves) ansteuern, muss dann aber jeweils alle CS-Leitungen korrekt bedienen, damit kein Durcheinander entsteht.

Da sollte doch ein ESP32 mit seinen 4 SPI-Bussen sehr gut geeignet sein, doch wie schließe ich das Board korrekt an? Trotz langem Suchens fand ich zunächst nirgendwo einen Hinweis im Netz. Bis ich dank einer Suchanfrage bei phind doch einen Treffer landen konnte: die Website von Randomnerds mit Hinweisen zum ESP32 und seinen SPIs.

Grundlegendes

Der ESP32 verfügt über 4 SPI Busse, von denen zwei intern belegt sind, die beiden anderen werden mit VSPI und HSPI bezeichnet.

Neben der Spannungsversorgung verwendet ein SPI-Bus diese Leitungen:

  • MISO: Master In Slave Out
  • MOSI: Master Out Slave In
  • SCLK: Serial Clock
  • CS: Chip Select - Leitung um den Baustein anzusprechen

 

Die voreingestellte Belegung der Pins beim ESP32 ist:

SPI

MOSI

MISO

SCLK

CS

VSPI

GPIO 23

GPIO19

GPIO 18

GPIO 5

HSPI

GPIO 13

GPIO 12

GPIO 14

GPIO 15


Wie also das TFT-Board ansprechen?

Libraries

Das Weitere wurde von den verwendeten Libraries bestimmt.

Für das TFT-Display verwende ich die Adafruit_ST7735 Library (zu Laden als “Adafruit ST7753 and ST7789 Library”), die Adafruit_GFX Library (die GFX wird im Paket mit der ST… geladen.) und für den Kartenleser die SD Library.

Der Kartenleser muss an den “default” SPI, also den VSPI-Port. Denn bei der Initialisierung der SD-Library kann nur der CS-Pin mitgegeben werden. Damit das ordnungsgemäß funktioniert ist folgendes nötig:

  1. Ein Objekt für die SPI-Klasse muss erzeugt werden:

            SPIClass sdSPI = SPIClass(VSPI);

  1. Im Setup:

            sdSPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS);

  1. Und zum Schluss die Initialisierung von SD:

              if (!SD.begin(SD_CS, sdSPI)){ … }

Bei der Adafruit_ST7735 sieht es anders aus: hier kann man alle Pins bei der Initialisierung festlegen und damit wird der HSPI-Port verwendet:

Adafruit_ST7735 tft = Adafruit_ST7735 (TFT_CS, TFT_MISO, TFT_MOSI, TFT_SCLK, TFT_RST)

Bei dieses Library hatte ich das Problem, dass zunächst Überetzungsfehler auftraten, die jedoch verschwanden, nachdem ich die Library gelöscht und wieder neu geladen hatte.

Die Dokumentation aller Adafruit_ST7735 Library-Befehle gibt es hier.

Damit war klar: der Kartenleser kommt an VSPI mit den voreingestellten Pins, das Display wird mit den Pins des HSPI versorgt, der Pin “RESET” kann beliebig vergeben werden.

Die Verdrahtung

Die Beschriftung des TFT-Boards ist etwas verwirrend, da sie sich nicht an die SPI-Standards hält: SCLK heißt SCK, MOSI heißt SDA und MISO heißt A0.

Mit diesem Wissen geht es an die Verkabelung (die Boards sind von oben gesehen, Lötstifte unten):

Download Schaltplan

Das Testprogramm

Um nun alles zu Testen habe ich erstens das Beispielprogramm SD —> SD_Test verwendet und es um eine kurze Routine für das Display ergänzt. Programmiert mit “Arduino IDE 2.3.0”. Das Programm (Download):

/*  Betrieb des 1,8" TFT Farb Displays von AZ-Delivery
    an ESP 32 unter gleichzeitiger Benutzung des Kartenlesers
    Zum Test des Kartenlesers wird das Beispiel von Arduino
    angepasst und verwendet: Datei --> Beispiele --> SD --> SD_Test
    4. März 2024
    Rudolf Reiber
*/

/*
  Den Kartenlesers wie folgt anschließen:
  SD_CLK    18
  SD_MISO   19
  SD_MOSI   23
  SD_SC      5
  Er benutzt den VSPI Kanal des ESP32
  Es funktioniert genauso mit dem Kartenleser von AZ-Delivery:
  SPI Reader Micro Speicher SD TF Karte Memory Card Shield Modul
*/

#include <FS.h>
#include <SD.h>
#include <SPI.h>
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library for ST7735

// Das sind die Pins für den HSPI Kanal. Der TFT_RST kann beliebig gewählt werden.

#define TFT_CS         15
#define TFT_MOSI       13 //Der Pin ist auf dem Board mit SDA beschriftet
#define TFT_MISO       12 //Der Pin ist auf dem Board mit A0 beschriftet
#define TFT_RST        27 //Der Pin ist auf dem Board mit RESET beschriftet
#define TFT_CLK        14 //Der Pin ist auf dem Board mit SCK beschriftet

#define SD_CLK         18
#define SD_MISO        19
#define SD_MOSI        23
#define SD_CS           5

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_MISO, TFT_MOSI, TFT_CLK, TFT_RST);
SPIClass sdSPI = SPIClass(VSPI);



void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\n", dirname);

    File root = fs.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(fs, file.path(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("  SIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}

void createDir(fs::FS &fs, const char * path){
    Serial.printf("Creating Dir: %s\n", path);
    if(fs.mkdir(path)){
        Serial.println("Dir created");
    } else {
        Serial.println("mkdir failed");
    }
}

void removeDir(fs::FS &fs, const char * path){
    Serial.printf("Removing Dir: %s\n", path);
    if(fs.rmdir(path)){
        Serial.println("Dir removed");
    } else {
        Serial.println("rmdir failed");
    }
}

void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\n", path);

    File file = fs.open(path);
    if(!file){
        Serial.println("Failed to open file for reading");
        return;
    }

    Serial.print("Read from file: ");
    while(file.available()){
        Serial.write(file.read());
    }
    file.close();
}

void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\n", path);

    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("File written");
    } else {
        Serial.println("Write failed");
    }
    file.close();
}

void appendFile(fs::FS &fs, const char * path, const char * message){
    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");
        return;
    }
    if(file.print(message)){
        Serial.println("Message appended");
    } else {
        Serial.println("Append failed");
    }
    file.close();
}

void renameFile(fs::FS &fs, const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\n", path1, path2);
    if (fs.rename(path1, path2)) {
        Serial.println("File renamed");
    } else {
        Serial.println("Rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    Serial.printf("Deleting file: %s\n", path);
    if(fs.remove(path)){
        Serial.println("File deleted");
    } else {
        Serial.println("Delete failed");
    }
}

void testFileIO(fs::FS &fs, const char * path){
    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);
        file.close();
    } else {
        Serial.println("Failed to open file for reading");
    }


    file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }

    size_t i;
    start = millis();
    for(i=0; i<2048; i++){
        file.write(buf, 512);
    }
    end = millis() - start;
    Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
    file.close();
}

void setup(){
    Serial.begin(115200);
    sdSPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS); // den SPI-Port des Lesers initialisieren
    tft.initR(INITR_BLACKTAB);                    // den ST7735S Chip initialisieen, schwarz
    tft.fillScreen(ST77XX_BLACK);                 // und den Schirm mit Schwarz füllen
    tft.setTextWrap(false);                       // automatischen Zeilenumbruch ausschalten
    tft.setTextSize(2);                           // Zeichengröße auf 2                    
    tft.setRotation(1);
    tft.setTextColor(ST77XX_GREEN, ST77XX_BLACK); // Grüne schrift auf schwarzem Hintergrund

    if(!SD.begin(SD_CS, sdSPI)){
        Serial.println("Keine Karte!");
        tft.fillScreen(ST77XX_BLACK); 
        tft.setCursor(0,0);
        tft.print("keine Karte!");
        delay(3000);
    }
    else {

      uint8_t cardType = SD.cardType();

       if(cardType == CARD_NONE){
        Serial.println("Keine Karte!");
        tft.fillScreen(ST77XX_BLACK); 
        tft.setCursor(0,0);
        tft.print("keine Karte!");
        delay(3000);

      }
      else{
        tft.fillScreen(ST77XX_BLACK); 
        tft.setCursor(0,0);
        tft.print("Karte da!");
        Serial.println("Karte da!");
        Serial.println("");
        Serial.print("SD Card Type: ");
        tft.setCursor(0,20);
        tft.print("Typ: ");
        if(cardType == CARD_MMC){
        Serial.println("MMC");
        tft.print("MMC");
        } else if(cardType == CARD_SD){
          Serial.println("SDSC");
          tft.print("SDSC");
        } else if(cardType == CARD_SDHC){
          Serial.println("SDHC");
          tft.print("SDHC");
        } else {
          Serial.println("UNBEKANNT");
          tft.print("UNBEKANNT");
        }
        tft.setCursor(0,40);
        tft.print("Weiteres im ");
        tft.setCursor(0,60);
        tft.print("Ser. Monitor");
        delay (3000);

       uint64_t cardSize = SD.cardSize() / (1024 * 1024);
       Serial.printf("SD Card Size: %lluMB\n", cardSize);

        listDir(SD, "/", 0);
        createDir(SD, "/mydir");
        listDir(SD, "/", 0);
        removeDir(SD, "/mydir");
        listDir(SD, "/", 2);
        writeFile(SD, "/hello.txt", "Hello ");
        appendFile(SD, "/hello.txt", "World!\n");
        readFile(SD, "/hello.txt");
        deleteFile(SD, "/foo.txt");
        renameFile(SD, "/hello.txt", "/foo.txt");
        readFile(SD, "/foo.txt");
        testFileIO(SD, "/test.txt");
        Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
        Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
      }
    }
    tft.fillScreen(ST77XX_BLACK); 
    tft.setTextSize(8);
}

void loop(){
  for (int i = 0; i < 255; i++) tftPrint(i);
  delay(2000);
}

void tftPrint(int zahl)
{
  if (zahl <= 30 ) tft.setTextColor(ST77XX_GREEN, ST77XX_BLACK);
  else
    if(zahl <= 50)  tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK);
    else
    if (zahl <= 80) tft.setTextColor(ST77XX_MAGENTA, ST77XX_BLACK);
      else
      if (zahl <= 100) tft.setTextColor(ST77XX_CYAN, ST77XX_BLACK);
      else tft.setTextColor(ST77XX_RED, ST77XX_BLACK);
  tft.setCursor(12,35);
  if (zahl < 10)  tft.print(" ");
  if (zahl < 100) tft.print(" ");
  tft.print(zahl);
}

Überprüfung

Auf dem Display werden die Ziffern von 0 bis 254 angezeigt, ob der Kartenleser funktioniert, lässt sich im Seriellen Monitor der Arduino IDE überprüfen:


SD Card Type: SDHC
SD Card Size: 7624MB
Listing directory: /
  FILE: foo.txt  SIZE: 13
  FILE: test.txt  SIZE: 1048576
  FILE: 22-05-31.gpx  SIZE: 49917
  FILE: 22-06-01.gpx  SIZE: 50029
Creating Dir: /mydir
Dir created
Listing directory: /
  DIR : mydir
  FILE: foo.txt  SIZE: 13
  FILE: test.txt  SIZE: 1048576
  FILE: 22-05-31.gpx  SIZE: 49917
  FILE: 22-06-01.gpx  SIZE: 50029
Removing Dir: /mydir
Dir removed
Listing directory: /
  FILE: foo.txt  SIZE: 13
  FILE: test.txt  SIZE: 1048576
  FILE: 22-05-31.gpx  SIZE: 49917
  FILE: 22-06-01.gpx  SIZE: 50029
Writing file: /hello.txt
File written
Appending to file: /hello.txt
Message appended
Reading file: /hello.txt
Read from file: Hello World!
Deleting file: /foo.txt
File deleted
Renaming file /hello.txt to /foo.txt
File renamed
Reading file: /foo.txt
Read from file: Hello World!
1048576 bytes read for 2595 ms
1048576 bytes written for 3312 ms
Total space: 7608MB
Used space: 3MB

Es tut!

Den Beitrag als PDF herunterladen

Vielen Dank an Rudolf Reiber für diesen Beitrag.

DisplaysEsp-32Projekte für anfänger

5 Kommentare

Dieter Behm

Dieter Behm

Moin,
das mit dem Display funktioniert bei mir super, leider schreibt “er” keine Karte.
Irgendein Tip oder kann man die noch anders formatieren.

Gruß
Dieter

Rudolf Reiber

Rudolf Reiber

@ Tobias:
Es war sowohl in der Arduino-IDE als auch in CODE der identische Quellcode. Was den Unterschied im Compilat ausmachte, entzieht sich meiner Kenntnis. Ich weiß nur, dass es mit der ARDUINO-IDE tat, mit VS-CODE nicht.

Jes Guinea

Jes Guinea

Grazie Rudolf Reiber

Karl Stanger

Karl Stanger

Zur Programmierung einer SD-Karte zusammen mit einem TFT-Display gibt es auch noch eine anderen Lösung, die mit einer einzigen SPI-Schnittstelle funktioniert: Software SPI. Hier ist die Initialisierung mit einem Arduino und einem 480×320 TFT-Display, welches den ILI9486 Treiber benutzt, beschrieben:
//———————————————— SD-Card Initialisierung -——————————————
// Die SD-Card, die im TFT LCD Shield (480×320, ILI9486) eingebaut ist, kann per SPI nicht angesteuert
// werden, da der Bildschirm bereits SPI nutzt. Die Lösung ist das sog. Software SPI. Dieses muss aber
// in der SdFatConfig.h eingerichtet werden. Dort muss die Zeile
// #define SPI_DRIVER_SELECT 0
// durch die Zeile
// #define SPI_DRIVER_SELECT 2
// ersetzt werden.
//
#include // Version 2.2.0
#define SD_FAT_TYPE 0 // Nimmt das, was in SdFatConfig.h steht
const uint8_t SD_CS_PIN = 10; // Chip select may be constant or RAM variable
const uint8_t SOFT_MISO_PIN = 12; // Pin Nummern im Template müssen als Konstanten definiert sein
const uint8_t SOFT_MOSI_PIN = 11;
const uint8_t SOFT_SCK_PIN = 13;
SoftSpiDriver<SOFT_MISO_PIN, SOFT_MOSI_PIN, SOFT_SCK_PIN> softSpi; // SdFat Software SPI Template
#define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SD_SCK_MHZ(0), &softSpi)
// Speed Argument wird für Software SPI ignoriert
Da der Soft-SPI in meiner Anwendung für Arduino programmiert ist, muss geprüft werden, ob das für einen ESP32 genau so funktioniert (ich gehe davon aus).

Tobias

Tobias

Vielen Dank. Ich bin jetzt aber verwirrt: was genau war denn das Problem ArduinoIDE vs platformio? Hatte der veröffentlichte Sketch das Problem bei platformio gelöst? Falls ja, welcher Code hatte denn ursprünglich das Problem bei platformio ausgelöst? Ich denke, die Ansteuerung beider Devices ist sehr interessant, aber es wäre nützlich zu wissen, wo genau das Problem im Zusammenhang mit den verwendeten IDEs liegt. Kann man das vlt nachtragen? Vielen Dank!

Kommentar hinterlassen

Alle Kommentare werden von einem Moderator vor der Veröffentlichung überprüft

Empfohlene Blogbeiträge

  1. ESP32 jetzt über den Boardverwalter installieren - AZ-Delivery
  2. Internet-Radio mit dem ESP32 - UPDATE - AZ-Delivery
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1 - AZ-Delivery
  4. ESP32 - das Multitalent - AZ-Delivery