Einbinden eines OLED-Displays auf Basis des SSD1306

https://bildung.social/deck/@oerinformatik/

https://oer-informatik.de/esp_ssd1306

tl/dr; (ca. 3 min Lesezeit): Kleine OLED-Displays eignen sich hervorragend, um bei MCU-Projekten Messwerte und den Systemstatus auszugeben. Einige beliebte Display bauen auf dem Chip SSD1306 auf. In diesem Artikel finden sich Codeausschnitte und Verdrahtungspläne, um ein einfaches “Hello World” auf dem Display auszugeben. (Zuletzt geändert am 06.04.2025)

Ein günstiges und häufig genutztes Display für Microcontrollerprojekte sind Breakout-Boards auf Basis von SSD1306-Displays. Diese können häufig per I2C und (seltener) per SPI angesprochen werden.

Generelle Überlegungen zu Installation und Inbetriebnahme von Devices am Microcontrollerboard

Das Vorgehen ist eigentlich bei allen Geräten im Arduino-Kosmos identisch, unabhängig davon, ob es ein Sensor, ein Aktor, ein Display oder etwas völlig anderes ist:

  1. Typenbezeichnung suchen: Zuerst muss die Art und Typ des neuen Geräts herausgefunden werden. Manchmal steht dies auf der Platine, manchmal muss man im Netz nach Fotos/Pinouts suchen (siehe unten).

  2. Bibliothek wählen und installieren: Wird mit der Arduino-IDE, so muss im Menü Werkzeuge / Bibliotheken verwalten eine passende Bibliothek für das Gerät gesucht werden. Häufig sind die Bibliotheken von Adafruit relativ gut.

  3. Pins des Geräts recherchieren und Logiklevel prüfen: Zum Anschließen des Geräts benötigen wir die Pinbezeichnungen der jeweils genutzten Platine. Nicht immer ist der Aufdruck auf den Platinen komplett, häufig wirkt ein zusätzliches Pinout (Bildersuche nach “Pinout Gerätebezeichnung”) Wunder. Dabei sollte im Datenblatt auch geprüft werden, ob das Logiklevel der Platine mit dem des Microcontrollers kompatibel ist (Arduino Uno: 5V, ESPs, Arduino Mega: 3,3V).

  4. Freie Pins des Microcontrollers wählen: Bevor wir verdrahten können, sollten wir uns noch entscheiden, welche Schnittstelle wir verwenden wollen (wenn das Gerät mehrere anbietet - z.B. SPI oder I2C). Dazu benötigen wir eine Liste der freien Pins unseres Microcontrollers, der für diese Schnittstelle geeignet ist (für einige ESPs beispielsweise in diesem Artikel). Ggf. hilft auch ein Blick in die Beispielquelltexte mit Hinweisen (s.u.).

  5. Verdrahten und mit Beispielcode erkunden: Dann kann das Gerät verdrahtet werden und über Beispiel-Quelltexte (Datei/ Beispiele häufig ganz unten: “Beispiele angepasster Bibliotheken”) erkundet werden.

Zu (1) Typenbezeichnung suchen: SSD1306 - siehe oben

Zu (2) Bibliothek wählen und installieren: Werkzeuge / Bibliotheken verwalten

Die Suche nach “SSD1306” liefert u.a. die Bibliothek “Adafruit SSD1306”, die ich in dem Beispiel nutze - andere Bibliotheken können aber ebenso eingesetzt werden.

Installieren der Bibliothek “Adafruit SSD1306”
Installieren der Bibliothek “Adafruit SSD1306”

Die Bibliothek benötigt eine Reihe von weiteren Abhängigkeiten, die direkt mit installiert werden können:

Adafruit SSD1306 ist abhängig von BusIO und GFX Library
Adafruit SSD1306 ist abhängig von BusIO und GFX Library

Aufbau und Beispielcode mit der I2C-Schnittstelle

Zu (3) Pins des Geräts recherchieren

Ich schließe das Display per I2C-Schnittstelle an - hierfür sind neben der Spannungsversorgung (3V3, GND) noch die Signale Clock (SCL oder SCK) und Data (SDA) nötig. Falls weitere Pins existieren (z.B. CSB, SDO) sind diese wahrscheinlich für die Ansteuerung per SPI-Schnittstelle gedacht.

Zu (4) Freie Pins des Microcontrollers wählen

Da wir das Display per I2C-Schnittstelle anschließen wollen, nutzen wir einfach die vorgesehenen Pins:

  • bei ESP32 ist dies: GPIO21 für SDA und GPIO22 für SCL.

  • bei ESP8266 ist dies: D2 für SDA und D1 für SCL.

  • bei Arduino Uno ist dies: A4 für SDA und A5 für SCL.

Wenn bereits andere I2C Geräte an diesen Leitungen angeschlossen sind, dann kann das Display zusätzlich parallel angeschlossen werden - schließlich handelt es sich hierbei um ein Bus-System, dass genau für den Anschluss mehrere Geräte gedacht ist.

Es wären auch andere Pins möglich (siehe unten), aber so sparen wir uns etwas Konfigurationsarbeit.

Zu (5) Verdrahten und mit Beispielcode erkunden:

Der Aufbau sieht bei mir mit einem ESP32 Dev V1 so aus (vorsicht: ggf. ist die Pinleiste andersherum aufgelötet und alle Pins gespiegelt):

Beispiel der Verdrahtung des SSD1306 per I2C an einem ESP32 Dev V1 (erstellt mit Fritzing, weitere Quellen hier)
Beispiel der Verdrahtung des SSD1306 per I2C an einem ESP32 Dev V1 (erstellt mit Fritzing, weitere Quellen hier)

Natürlich müssen bei einem anderen Microcontroller auch andere Pins verwendet werden, da die Bibliothek die I2C-Standardpins der jeweiligen Boards verwendet. (Wie man von den Standardports abweichen kann erkläre ich weiter unten).

Die I2C-Schnittstelle identifiziert Geräte am Bus über eine eindeutige Adresse. Jedes Gerät erkennt anhand dieser Adresse, wer Empfänger der Nachrichten am Bus sein soll. Die Adresse umfasst sieben Bit, es gibt also 2^7 = 128 verschiedene Adressen ( also 0 und 127 dezimal, 0x00-0x7F hexadezimal, 0b00000000 - 0b01111111 binär). Damit mehrere Geräte des gleichen Typs angeschlossen werden können, wird häufig nur ein Teil dieser Adresse durch den Hersteller fest vorgegeben, einige Bit können manuell konfiguriert werden.

Das SSD1306-DIsplay wird das letzte Bit angepasst - es können die I2C-Adressen 0x3C (binär: 0b00111100) oder 0x3D (binär: 0b00111101) konfiguriert werden.

Ein minimales Programm, das das Display über die Adafruit-Bibliothek ansteuert, könnte also so aussehen:

Der seriellen Monitor (Strg+ Shift+M) sollte bei 115200 Baud eingestellt jetzt folgendes ausgeben (beim BME zusätzlich noch die Spalte “Humidity”):

#include <Adafruit_SSD1306.h>

#define SCREEN_ADDRESS 0x3C                // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(128, 64);

unsigned int seitenNr = 0;
unsigned long milliSekundenZeit = 0;

void setup(){
  Serial.begin(115200);
  if(!display.begin(-1, SCREEN_ADDRESS)) {
      Serial.println(F("SSD1306 allocation failed"));
    }
  milliSekundenZeit = millis();
}

void loop(){
  display.clearDisplay();                  // Lösche alles 
  display.setCursor(0,0);                  // Den Cursor auf Anfang (die obere linke Ecke) setzen
  display.setTextSize(2);                  // Etwas größere, leichter lesbare Schrift (4 Zeilen, 10 Zeichen)
  display.setTextColor(SSD1306_WHITE);     // Helle Schrift (auf dunklem Hintergrund)

  display.setTextSize(2);                  // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);     // Draw white text
  display.setCursor(0, 0);                 // Start at top-left corner
  display.cp437(true);                     //https://de.wikipedia.org/wiki/Codepage_437

  display.setTextSize(2);                  // Textsize 2 => ca. 10 Buchstaben pro Zeile, 4 Zeilen möglich
  display.println("ABCDEFGHIJ");       
  display.setTextSize(1);                  // Textsize 1 => ca. 21 Buchstaben pro Zeile, 8 Zeilen möglich   
  display.println("ABCDEFGHIJKLMNOPQRSTU");  
  display.display();                       // Anzeigen des zusammengestellten Bilds
  delay(1000);

  char textBuffer[21];                     // Formatierte Zahlen lassen sich z.B. über Char-Buffer erzeugen
  sprintf(textBuffer, "Seite    :    %3i ", seitenNr);
  display.println(textBuffer);

  sprintf(textBuffer, "Zeit [s] : %6.1f", (millis()-milliSekundenZeit)/1000.0);
  Serial.println(textBuffer);

  display.println(textBuffer);
  seitenNr++;
  display.display();
  delay(1000);

  display.setTextSize(1);     
  display.println("Zu langen       bricht mitten im Wort um."); // Aufgepasst: zu viel Text fällt hinten rüber...
  int16_t xpos = display.getCursorX();     // Text kann exakt positioniert werden, hier wird aktuelle
  int16_t ypos = display.getCursorY();     // position ausgelesen.
  display.display();
  delay(1000);  

  display.setCursor(xpos+60,ypos-16); //y: Zeile(Size=1)=8, Zeile(Size=2) = 16 / x: Buchstabe(Size=1)=6, Buchstabe(Size=2)=12  
  display.println("Text");
  display.setCursor(xpos,ypos); // Setze Cursor an oben ausgelesen Position am Ende des Texts.
  display.display();
  delay(1000);
  
  display.print("Umlaute: ae=");   // Test wird regulär mit US-Zeichencodierung ausgegeben. Umlaute liegen dort an anderen
  display.print(char(0x84));       // Positionen. Siehe https://de.wikipedia.org/wiki/Codepage_437
  display.print(", ue=");          // Hexadezimalwert entspricht Zeile und Spalte der oben verlinkten Tabelle
  display.print(char(0x81));       // Das ü steht in Zeilenindex 8 / Spaltenindex 1 (Zählung startet bei 0)
  display.display();
  delay(5000);
}

Wenn dieser Programmcode oder die vom Treiber mitgelieferten Beispielsketche (Datei/ Beispiele / ganz unten: ) nicht gehen, dann hat das häufig die folgenden Ursachen:

  • Could not find a valid BME280 sensor, check wiring, i2c address: Die I2C-Adresse ist falsch konfiguriert oder wird nicht erkannt. Welche Adresse hat der Sensor überhaupt und ist alles richtig verdrahtet? Hier hilft ein Scan des I2C-Buses über das Beispielprogramm: Datei/ Beispiele / Wire/ WireScan. Im Normalfall reicht es, sowohl die 0x76 als auch die 0x77 als Adresse zu versuchen.

  • Ausgabe der Werte ist unrealistisch, z.B. 0.0 °C | 0.0 hPa | 44330.0 m |: Es handelt sich um den falschen Treiber für den Sensor - bitte nochmals prüfen, ob es ein BME280 oder BMP280 ist (siehe oben).

  • Die Pins wurden vertauscht - das passiert insbesondere, wenn die Beschriftung auf der Unterseite ist und man sich alles “auf dem Kopf” vorstellen muss.

Individuelle Konfiguration per I2C-Schnittstelle

Sofern nicht die Standardports der I2C-Schnittstelle verwendet werden sollen (weil diese z.B. zwingend anderweitig vergeben werden müssen), können wir die Pins auch individuell konfigurieren.

Intern nutzt der Adafruit_BMx280-Treiber eine Arduino-Bibliothek namens Wire, um die I2C-Kommunikation abzuwickeln. Mit Hilfe dieser Bibliothek können wir die I2C-Verbindung auch auf andere Pins legen, indem wir zu Beginn unseres Programms die Pin-Bezeichnungen über den Aufruf Wire.begin(MCU_SDA, MCU_SCL); übergeben. Natürlich müssen die beiden Variablen, die ich hier mal MCU_SDA und MCU_SCL genannt habe, vorher deklariert und initialisiert werden. Alternativ können auch direkt die Pinbezeichnungen übergeben werden (Wire.begin(18, 19);), war aber den Quelltext schlecht wartbar machen würde.

Quellen und weitere Informationen


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: Einbinden eines OLED-Displays auf Basis des SSD1306” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/esp_ssd1306 veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/mcu/arduino-esp. Stand: 06.04.2025.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: