SPI-Bus mit Arduino & ESP32 verstehen: Mehrere Geräte an einem Bus betreiben
Ein Datenlogger, der Messwerte auf die SD-Karte schreibt, sie auf einem TFT anzeigt und den Zugang per RFID absichert – und das alles über vier gemeinsame Leitungen. Genau dafür wurde SPI gebaut. In diesem Tutorial verstehst du, was MOSI, MISO, SCK und CS wirklich tun, warum ein einziges Modul mit schlecht implementiertem Chip-Select den ganzen Bus blockiert, wie du Geräte mit unterschiedlichen Taktraten und SPI-Modi friedlich zusammenbringst und wie du auf dem ESP32 gleich zwei Busse gleichzeitig nutzt.
Warum es SPI überhaupt gibt
Stell dir vor, jedes Modul bräuchte seine eigene Datenleitung. Ein TFT-Display, eine SD-Karte und ein RFID-Leser – schon wären zwölf Pins weg, und der Arduino Nano hätte kaum noch etwas übrig. SPI löst das elegant: Drei Leitungen werden von allen Geräten geteilt, und nur eine einzige zusätzliche Leitung pro Gerät entscheidet, wer gerade angesprochen wird.
Die vier Signale heißen:
- SCK (Serial Clock) – der Takt. Der Mikrocontroller gibt ihn vor, jedes Bit wird an einer Taktflanke übernommen.
- MOSI (Master Out, Slave In) – Daten vom Mikrocontroller zum Modul.
- MISO (Master In, Slave Out) – Daten vom Modul zurück zum Mikrocontroller.
- CS bzw. SS (Chip Select / Slave Select) – die Auswahlleitung. LOW bedeutet aktiv. Jedes Gerät bekommt einen eigenen CS-Pin.
ℹ️ SPI ist vollduplex. Anders als beim I2C-Bus laufen Senden und Empfangen gleichzeitig: Mit jedem Takt schiebt der Master ein Bit über MOSI hinaus und liest zeitgleich ein Bit auf MISO ein. Deshalb gibt es in der Arduino-Bibliothek auch nur eine einzige Funktion für beides: SPI.transfer() sendet ein Byte und gibt das gleichzeitig empfangene Byte zurück.
SPI oder I2C – wann was?
Beide Busse teilen sich Leitungen zwischen mehreren Geräten, aber sie lösen unterschiedliche Probleme:
| Eigenschaft | SPI | I2C |
|---|---|---|
| Leitungen | 3 gemeinsame + 1 CS pro Gerät | 2 gemeinsame (SDA, SCL) |
| Typische Geschwindigkeit | 1–40 MHz | 100–400 kHz |
| Adressierung | über CS-Pin (Hardware) | über 7-Bit-Adresse (Software) |
| Adresskonflikte möglich | nein | ja, häufiges Problem |
| Pull-up-Widerstände nötig | nein | ja (meist auf dem Modul) |
| Gut für | Displays, SD-Karten, große Datenmengen | viele kleine Sensoren |
💡 Faustregel: Fließen viele Daten (Bilder, Dateien, schnelle Bildwiederholung), nimm SPI. Geht es um ein paar Messwerte pro Sekunde von vielen Sensoren, nimm I2C – dort sparst du Pins statt Zeit. Viele Module wie das SSD1306 OLED bieten sogar beides an.
Das brauchst du
1Die Verkabelung – gemeinsam und getrennt
Der Kern des Ganzen: SCK, MOSI und MISO gehen von allen Modulen an dieselben drei Pins des Mikrocontrollers. Nur CS bekommt jeweils einen eigenen freien Digitalpin.
| Signal | UNO R3 / Nano V3 | ESP32 (VSPI) | ESP8266 D1 Mini |
|---|---|---|---|
| SCK | D13 | GPIO18 | D5 (GPIO14) |
| MISO | D12 | GPIO19 | D6 (GPIO12) |
| MOSI | D11 | GPIO23 | D7 (GPIO13) |
| CS SD-Karte | D10 | GPIO5 | D8 (GPIO15) |
| CS TFT | D9 | GPIO15 | D3 (GPIO0) |
| CS RFID | D8 | GPIO21 | D4 (GPIO2) |
| TFT DC / RST | D7 / D6 | GPIO2 / GPIO4 | D1 / D2 |
| RFID RST | D5 | GPIO22 | D0 (GPIO16) |
⚠️ 3,3 V oder 5 V? SD-Kartenmodule, das ST7735 und der RC522 arbeiten intern mit 3,3 V. Die meisten Breakout-Boards haben zwar einen Spannungsregler für VCC, aber nicht unbedingt Pegelwandler an den Datenleitungen. Am 5-V-Arduino ist das oft grenzwertig. Sicher wird es mit einem Spannungsteiler aus 1 kΩ und 2 kΩ in den Leitungen SCK, MOSI und CS – MISO darf direkt bleiben, weil das Modul dort sendet. Am ESP32 stellt sich die Frage nicht: Der arbeitet ohnehin mit 3,3 V.
2SPI zu Fuß: SPI.transfer() verstehen
Bevor du fertige Bibliotheken benutzt, lohnt sich ein Blick auf die nackte Mechanik. So sieht eine SPI-Transaktion aus – und du siehst genau, wo CS auf LOW geht:
#include <SPI.h>
#define CS_GERAET 10
void setup() {
Serial.begin(115200);
pinMode(CS_GERAET, OUTPUT);
digitalWrite(CS_GERAET, HIGH); // WICHTIG: erst deaktivieren, dann SPI starten
SPI.begin();
}
// Ein Register lesen: Adresse senden, dann Dummy-Byte schieben
uint8_t leseRegister(uint8_t adresse) {
// 8 MHz, hoechstwertiges Bit zuerst, SPI-Modus 0
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_GERAET, LOW); // Geraet aktivieren
SPI.transfer(adresse | 0x80); // Bit 7 = Lesebefehl (geraeteabhaengig!)
uint8_t wert = SPI.transfer(0x00); // Dummy senden, Antwort einlesen
digitalWrite(CS_GERAET, HIGH); // Geraet freigeben
SPI.endTransaction();
return wert;
}
void schreibeRegister(uint8_t adresse, uint8_t wert) {
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_GERAET, LOW);
SPI.transfer(adresse & 0x7F); // Bit 7 = 0 -> schreiben
SPI.transfer(wert);
digitalWrite(CS_GERAET, HIGH);
SPI.endTransaction();
}
void loop() {
Serial.println(leseRegister(0x00), HEX);
delay(1000);
}
ℹ️ Warum beginTransaction()? Ohne diesen Aufruf gilt für alle Geräte dieselbe Taktrate und derselbe Modus. Sobald zwei Module unterschiedliche Anforderungen haben – und das ist der Normalfall – brauchst du die Transaktionen. Sie stellen vor jedem Zugriff Takt, Bitreihenfolge und Modus passend ein und schützen den Zugriff zusätzlich gegen Unterbrechungen durch Interrupts.
3Die vier SPI-Modi – und warum dein Modul schweigt
SPI kennt vier Betriebsarten, die sich aus zwei Bits ergeben: CPOL (Ruhepegel des Takts) und CPHA (auf welcher Flanke die Daten übernommen werden). Passt der Modus nicht, bekommst du keinen Fehler – du bekommst nur Müll oder lauter Nullen.
| Modus | CPOL | CPHA | Takt im Ruhezustand | Typische Geräte |
|---|---|---|---|---|
| SPI_MODE0 | 0 | 0 | LOW, Daten bei steigender Flanke | SD-Karte, RC522, ST7735, die große Mehrheit |
| SPI_MODE1 | 0 | 1 | LOW, Daten bei fallender Flanke | einige ADCs |
| SPI_MODE2 | 1 | 0 | HIGH, Daten bei fallender Flanke | selten |
| SPI_MODE3 | 1 | 1 | HIGH, Daten bei steigender Flanke | manche Displays und Flash-Chips |
Steht der Modus nicht im Datenblatt, hilft brutales Ausprobieren – vier Varianten sind schnell durch:
#include <SPI.h>
#define CS_PIN 10
uint8_t testeModus(uint8_t modus, uint8_t adresse) {
uint8_t spiModi[4] = { SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3 };
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, spiModi[modus]));
digitalWrite(CS_PIN, LOW);
SPI.transfer(adresse | 0x80);
uint8_t wert = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH);
SPI.endTransaction();
return wert;
}
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH);
SPI.begin();
delay(500);
// Register mit bekanntem Inhalt lesen, z.B. eine Chip-ID
for (uint8_t m = 0; m < 4; m++) {
Serial.print("SPI_MODE");
Serial.print(m);
Serial.print(" -> 0x");
Serial.println(testeModus(m, 0x37), HEX); // 0x37 = VersionReg beim RC522
delay(50);
}
}
void loop() {}
Beim RC522 liefert das Versionsregister 0x37 in der Regel 0x91 oder 0x92. Genau der Modus, bei dem ein plausibler Wert herauskommt, ist der richtige.
4Drei Geräte an einem Bus – das komplette Beispiel
Jetzt die Kür: SD-Karte, Display und RFID-Leser gleichzeitig. Der entscheidende Trick steckt in setup() – alle CS-Pins werden auf HIGH gesetzt, bevor irgendeine Bibliothek initialisiert wird:
#include <SPI.h>
#include <SD.h>
#include <MFRC522.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#define CS_SD 10
#define CS_TFT 9
#define CS_RFID 8
#define TFT_DC 7
#define TFT_RST 6
#define RFID_RST 5
Adafruit_ST7735 tft = Adafruit_ST7735(CS_TFT, TFT_DC, TFT_RST);
MFRC522 rfid(CS_RFID, RFID_RST);
bool sdBereit = false;
void setup() {
Serial.begin(115200);
// ---- ALLE Chip-Selects deaktivieren, bevor irgendetwas startet ----
pinMode(CS_SD, OUTPUT); digitalWrite(CS_SD, HIGH);
pinMode(CS_TFT, OUTPUT); digitalWrite(CS_TFT, HIGH);
pinMode(CS_RFID, OUTPUT); digitalWrite(CS_RFID, HIGH);
delay(10);
SPI.begin();
// ---- Display zuerst, damit du Statusmeldungen sehen kannst ----
tft.initR(INITR_BLACKTAB);
tft.setRotation(1);
tft.fillScreen(ST77XX_BLACK);
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
tft.setCursor(4, 4);
tft.println("SPI-Bus Demo");
// ---- SD-Karte ----
sdBereit = SD.begin(CS_SD);
tft.setCursor(4, 18);
tft.println(sdBereit ? "SD: OK" : "SD: FEHLER");
Serial.println(sdBereit ? F("SD bereit") : F("SD nicht gefunden"));
// ---- RFID ----
rfid.PCD_Init();
byte version = rfid.PCD_ReadRegister(MFRC522::VersionReg);
tft.setCursor(4, 32);
tft.print("RC522 v0x");
tft.println(version, HEX);
Serial.print(F("RC522 Version: 0x"));
Serial.println(version, HEX);
}
void loop() {
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) {
delay(50);
return;
}
// UID als Hex-String zusammenbauen
String uid = "";
for (byte i = 0; i < rfid.uid.size; i++) {
if (rfid.uid.uidByte[i] < 0x10) uid += "0";
uid += String(rfid.uid.uidByte[i], HEX);
}
uid.toUpperCase();
Serial.print(F("Karte: "));
Serial.println(uid);
tft.fillRect(0, 50, 160, 40, ST77XX_BLACK);
tft.setCursor(4, 52);
tft.setTextColor(ST77XX_GREEN);
tft.println("Karte erkannt:");
tft.setCursor(4, 66);
tft.println(uid);
tft.setTextColor(ST77XX_WHITE);
// Auf SD protokollieren - RFID vorher sauber beenden!
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
if (sdBereit) {
File log = SD.open("zugang.csv", FILE_WRITE);
if (log) {
log.print(millis());
log.print(";");
log.println(uid);
log.close();
Serial.println(F("auf SD gespeichert"));
} else {
Serial.println(F("Datei liess sich nicht oeffnen"));
}
}
delay(1200);
}
💡 Die Reihenfolge in setup() ist kein Zufall. Startest du SD.begin(), während der CS-Pin des RFID-Moduls noch schwebt, hört das RC522 mit und antwortet womöglich auf MISO – der SD-Init schlägt fehl, scheinbar grundlos. Alle CS-Pins zuerst auf HIGH zu ziehen kostet drei Zeilen und erspart dir Stunden Fehlersuche.
5ESP32: zwei SPI-Busse parallel nutzen
Der ESP32 bringt zwei frei nutzbare SPI-Controller mit: VSPI (SPI3) und HSPI (SPI2). Das ist Gold wert, wenn ein langsames Gerät ein schnelles ausbremsen würde – etwa eine SD-Karte, die mit 4 MHz läuft, neben einem Display, das 40 MHz verträgt:
#include <SPI.h>
#include <SD.h>
// VSPI - Standardbus, hier fuer das schnelle Display
#define VSPI_SCK 18
#define VSPI_MISO 19
#define VSPI_MOSI 23
#define CS_TFT 5
// HSPI - zweiter Bus, hier fuer die SD-Karte
#define HSPI_SCK 14
#define HSPI_MISO 12
#define HSPI_MOSI 13
#define CS_SD 15
SPIClass spiTFT(VSPI);
SPIClass spiSD(HSPI);
void setup() {
Serial.begin(115200);
delay(300);
pinMode(CS_TFT, OUTPUT); digitalWrite(CS_TFT, HIGH);
pinMode(CS_SD, OUTPUT); digitalWrite(CS_SD, HIGH);
spiTFT.begin(VSPI_SCK, VSPI_MISO, VSPI_MOSI, CS_TFT);
spiSD.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, CS_SD);
// SD an den zweiten Bus haengen, bewusst langsam mit 4 MHz
if (SD.begin(CS_SD, spiSD, 4000000)) {
Serial.println("SD auf HSPI bereit");
Serial.printf("Groesse: %llu MB\n", SD.cardSize() / (1024ULL * 1024ULL));
} else {
Serial.println("SD-Init fehlgeschlagen");
}
// Beispielhafte Rohtransaktion auf dem schnellen Bus
spiTFT.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_TFT, LOW);
spiTFT.transfer(0x00);
digitalWrite(CS_TFT, HIGH);
spiTFT.endTransaction();
}
void loop() {
File f = SD.open("/messwerte.csv", FILE_APPEND);
if (f) {
f.printf("%lu;%.2f\n", millis(), (float)random(0, 10000) / 100.0);
f.close();
}
delay(2000);
}
ℹ️ Belegte Pins beim ESP32. Die GPIOs 6 bis 11 hängen am internen Flash-Speicher und dürfen niemals belegt werden – das Board startet sonst gar nicht erst. GPIO34 bis GPIO39 sind reine Eingänge und taugen nicht als CS. Und GPIO12 zieht beim Booten den Flash-Spannungspegel: Liegt dort beim Start ein HIGH an, bootet der ESP32 unter Umständen nicht. Wenn du HSPI nutzt, teste den Kaltstart mit angeschlossener Peripherie.
Taktrate richtig wählen
Schneller ist nicht automatisch besser. Auf einem Breadboard mit 20 cm Jumperkabeln wird das Signal ab etwa 10 MHz merklich unsauber – Übersprechen und Reflexionen sorgen für sporadische Bitfehler, die sich als „funktioniert meistens“ äußern. Das ist die unangenehmste Fehlerklasse überhaupt.
| Gerät | Empfohlener Takt | Anmerkung |
|---|---|---|
| Micro-SD-Modul | 4–8 MHz | Initialisierung läuft immer mit 400 kHz |
| ST7735 TFT | bis 24 MHz (ESP32: 40 MHz) | am AVR ohnehin auf F_CPU/2 = 8 MHz begrenzt |
| GC9A01 rund | bis 40 MHz am ESP32 | hohe Rate zahlt sich bei Animationen aus |
| RC522 RFID | max. 10 MHz | 4 MHz sind völlig ausreichend |
| Breadboard-Aufbau allgemein | ≤ 8 MHz | bei Aussetzern zuerst den Takt halbieren |
Wenn dein Aufbau stabil läuft und du ihn dauerhaft haben willst, ist das ein guter Moment für den Umzug auf eine Lochrasterplatine. Kurze, gelötete Leitungen vertragen deutlich höhere Taktraten – wie das geht, zeigt der Artikel Vom Breadboard zur Lochrasterplatine.
Troubleshooting
⚠️ Alles funktioniert einzeln, aber zusammen nichts: Der Klassiker. Mindestens ein CS-Pin steht beim Start nicht auf HIGH. Setze alle CS-Pins per pinMode() und digitalWrite(pin, HIGH), bevor du SPI.begin() oder eine Bibliothek aufrufst.
⚠️ SD.begin() schlägt fehl, sobald das Display angeschlossen ist: Manche billigen Displaymodule geben MISO nicht frei, wenn CS auf HIGH liegt – sie sind schlicht nicht busfähig. Abhilfe: Display an einen zweiten Bus (ESP32) hängen oder MISO des Displays gar nicht anschließen, falls du nur schreibst.
⚠️ Beim RC522 kommt als Version 0x00 oder 0xFF zurück: 0x00 heißt meist fehlende Spannung oder MISO nicht verbunden, 0xFF meist fehlende MOSI-Verbindung oder falscher Pegel. Das RC522 will 3,3 V an VCC – 5 V zerstören es.
⚠️ Das Display bleibt weiß oder zeigt Rauschen: Fast immer der DC-Pin (Data/Command). Er ist kein SPI-Signal, sondern ein normaler Ausgang, der pro Byte umgeschaltet wird. Prüfe ihn zuerst, bevor du am Takt drehst.
⚠️ Sporadische Aussetzer nach Stunden Laufzeit: Taktrate zu hoch oder Masseverbindung zu dünn. Halbiere den Takt und lege eine zusätzliche, kurze GND-Leitung direkt vom Modul zum Board.
⚠️ ESP32 startet nicht mehr, sobald die SD-Karte steckt: Wahrscheinlich hängt CS oder MISO an GPIO12. Dieser Pin wird beim Boot ausgewertet. Weiche auf einen anderen GPIO aus.
🚀 Bauteile für deinen SPI-Aufbau
Alles aus diesem Tutorial auf einen Blick:
Fazit
SPI ist schnell, robust und angenehm frei von Adresskonflikten – man muss nur eine einzige Regel wirklich verinnerlicht haben: Genau ein CS-Pin darf LOW sein, alle anderen HIGH. Die meisten „SPI funktioniert nicht“-Probleme lösen sich auf, sobald alle Chip-Selects sauber initialisiert sind, bevor irgendeine Bibliothek startet.
Dazu kommen drei Stellschrauben, die du jetzt kennst: der passende SPI-Modus, eine realistische Taktrate für deinen Aufbau und – auf dem ESP32 – die Möglichkeit, langsame und schnelle Geräte auf zwei getrennte Busse zu verteilen. Damit steht deinem Datenlogger mit Display und Kartenleser nichts mehr im Weg.