XIAO ESP32S3 – Kapitel 3: Erweiterungsboard und OLED-Display

In Kapitel 1 und 2 hast du LED und Taster selbst auf dem Breadboard verkabelt. Jetzt kommt das Seeed Studio Expansion Board für XIAO dazu. Darauf sind schon einige Bauteile fest eingebaut – ganz ohne Kabel. In diesem Kapitel nutzt du vor allem das OLED-Display: Es zeigt Texte, Zahlen und einfache Grafiken an. Damit kannst du endlich sehen, was in deinem Programm passiert: Schleifendurchläufe, Variablenwerte oder den Zustand des Tasters.

Ziele:

Wichtig:

  • Den XIAO nur ohne USB-Kabel in das Erweiterungsboard stecken oder herausnehmen.
  • Auf dem Erweiterungsboard hängt der Taster an D1 / GPIO2 – also an dem Pin, an dem in Kapitel 1 die LED war. Schalte Pin(2) hier niemals als Ausgang (Pin.OUT). Sonst entsteht beim Drücken des Tasters ein Kurzschluss.

1. Benötigte Materialien

Breadboard, LED und Widerstände brauchst du in diesem Kapitel nicht.


2. Das Erweiterungsboard kennenlernen

Das Erweiterungsboard mit eingesetztem XIAO und seinen wichtigsten Bauteilen
Abb. 1: Das Erweiterungsboard mit eingesetztem XIAO und seinen wichtigsten Bauteilen

Für dieses Kapitel sind diese Bauteile wichtig:

Bauteil angeschlossen an im Code Benutzen wir?
OLED-Display 0,96″, 128 × 64 Pixel I²C: D4 (SDA) und D5 (SCL) sda=Pin(5), scl=Pin(6) ja
User-Taster D1 Pin(2, Pin.IN, Pin.PULL_UP) ja
Echtzeituhr (RTC, Chip PCF8563) I²C (wie das Display) – nur im I²C-Scan
Summer (Buzzer) A3 / D3 Pin(4) nein (später)
RESET-Taster Reset des XIAO – wie RST auf dem XIAO
Ein/Aus-Schalter Stromversorgung des Boards – auf ON stellen
Grove-Buchsen I²C, UART, A0/D0 – nein (später)

Erinnerung aus Kapitel 1: Im Code zählt immer die GPIO-Nummer. D1 ist GPIO2, D4 ist GPIO5, D5 ist GPIO6.


3. Den XIAO einsetzen

  1. USB-Kabel vom XIAO abziehen.
  2. Den XIAO so über die beiden Buchsenleisten halten, dass seine USB-C-Buchse zum linken Rand des Erweiterungsboards zeigt – zur Seite mit der Beschriftung USB5V.
  3. Kontrolle: Die Beschriftung auf dem Erweiterungsboard muss passen. Die untere Leiste ist mit 0 1 2 3 4 5 6 beschriftet, die obere mit USB5V G 3V3 10 9 8 7.
  4. Den XIAO gerade und vorsichtig bis zum Anschlag einstecken. Keine Beinchen verbiegen!
  5. Erst jetzt das USB-Kabel am XIAO einstecken.

Tipp: Die Buchsenleisten neben dem XIAO führen dieselben Pins nach außen. Später kannst du dort mit Jumperkabeln weitere Bauteile anschließen.


4. Wie ist das Display angeschlossen? – Der I²C-Bus

Das Display braucht nur zwei Datenleitungen. Es benutzt den I²C-Bus (sprich: „I-Quadrat-C“). An dieselben zwei Leitungen können mehrere Geräte angeschlossen werden. Damit der XIAO weiß, mit wem er spricht, hat jedes Gerät eine eigene Adresse – so ähnlich wie eine Hausnummer.

SDA: Daten
D4 / GPIO5

SCL: Takt
D5 / GPIO6

XIAO ESP32S3
Chef am Bus

I²C-Bus

OLED-Display
Adresse 0x3C = 60

Echtzeituhr RTC
Adresse 0x51 = 81

Leitung Bedeutung XIAO-Pin im Code
SDA Serial Data – hier fließen die Daten D4 Pin(5)
SCL Serial Clock – der Takt, der den Rhythmus vorgibt D5 Pin(6)

5. Programm 1: Wer hängt am I²C-Bus?

Bevor wir das Display benutzen, fragen wir den Bus ab: Welche Geräte melden sich? Für diesen Test brauchst du noch keine Bibliothek.

pythonfrom machine import Pin, I2C

# I2C-Bus 0 mit SCL an D5 (GPIO6) und SDA an D4 (GPIO5)
i2c = I2C(0, scl=Pin(6), sda=Pin(5))

geraete = i2c.scan()
print("Gefundene Adressen:", geraete)

for adresse in geraete:
    print("Gerät gefunden bei", hex(adresse))

Erwartetes Ergebnis

textGefundene Adressen: [60, 81]
Gerät gefunden bei 0x3c
Gerät gefunden bei 0x51

Die Zahlen bedeuten jeweils dasselbe: 0x3c ist die Hexadezimalschreibweise von 60. Adressen werden in Datenblättern fast immer hexadezimal angegeben.

Leere Liste []? Dann sitzt der XIAO nicht richtig im Board oder die Pins im Code stimmen nicht. Kabel ab, XIAO kontrollieren, nochmal versuchen.


6. Die Bibliothek ssd1306 installieren

Im Display steckt ein Steuerchip namens SSD1306. Damit wir nicht jeden Pixel einzeln über I²C schicken müssen, benutzen wir eine fertige Bibliothek. Das ist eine Python-Datei mit vorbereiteten Funktionen. Sie heißt ssd1306.py und stammt aus der offiziellen Bibliothekssammlung micropython-lib.

Die Bibliothek ist nicht in MicroPython eingebaut. Du musst sie einmal auf den XIAO kopieren. Danach bleibt sie dort gespeichert – auch nach dem Ausschalten.

Weg A (empfohlen): Datei herunterladen und auf dem XIAO speichern

  1. Im Browser diese Adresse öffnen: https://raw.githubusercontent.com/micropython/micropython-lib/master/micropython/drivers/display/ssd1306/ssd1306.py Du siehst reinen Python-Code.
  2. Mit Strg + A alles markieren und mit Strg + C kopieren.
  3. In Thonny eine neue Datei anlegen und den Code mit Strg + V einfügen.
  4. Im Dateibrowser von Thonny im Bereich MicroPython device mit der rechten Maustaste auf eine freie Stelle klicken. Dort Neues Verzeichnis… wählen und es lib nennen. Falls lib schon existiert, diesen Schritt überspringen.
  5. Datei → Speichern unter… → MicroPython device → in den Ordner lib wechseln → Dateiname exakt ssd1306.py → Speichern.
  6. Die Datei ssd1306.py im Editor schließen. Du sollst sie nicht verändern.

Browser:
ssd1306.py öffnen

Strg+A, Strg+C

Thonny:
neue Datei, Strg+V

Speichern unter
MicroPython device

/lib/ssd1306.py

Warum der Ordner lib? MicroPython sucht Bibliotheken automatisch im Hauptverzeichnis und im Ordner /lib. In lib bleiben deine eigenen Programme und die Bibliotheken ordentlich getrennt. Notfalls funktioniert auch das Hauptverzeichnis.

Weg B: Thonny-Paketverwaltung

Thonny kann Pakete auch selbst installieren: Werkzeuge → Pakete verwalten… → nach ssd1306 suchen → Installieren. Je nach Thonny-Version und Paketquelle funktioniert das nicht immer zuverlässig. Wenn es klappt, landet die Bibliothek ebenfalls in /lib. Wenn nicht, nimm Weg A.

Kontrolle: Ist die Bibliothek da?

Tippe in die Thonny-Shell (unten, hinter >>>):

pythonimport ssd1306

Blick in die Bibliothek

Eine Bibliothek ist kein Zauberwerk, sondern ganz normaler Python-Code – geschrieben von anderen Menschen. Öffne ssd1306.py im Ordner lib auf dem XIAO nur zum Lesen (nichts ändern, nichts speichern!) und finde heraus:

  1. Wie heißt die Klasse, die wir mit ssd1306.SSD1306_I2C(...) benutzen? In welcher Zeile beginnt sie? Tipp: Suche nach class.
  2. Welche Adresse ist dort als Standard eingetragen (addr=...)? Vergleiche mit deinem I²C-Scan aus Programm 1. Passt das zusammen?
  3. Suche die Funktion show(). Was macht sie, grob gesagt? Tipp: Achte auf die letzte Zeile der Funktion.
  4. In der Datei selbst steht keine Lizenz. Öffne auf GitHub die Datei LICENSE der Sammlung micropython-lib: https://github.com/micropython/micropython-lib/blob/master/LICENSE. Unter welcher Lizenz stehen die Bibliotheken dort? Was bedeutet das für uns – dürfen wir sie kopieren und benutzen?

Merke: import ssd1306 funktioniert nur, weil die Datei ssd1306.py auf dem XIAO liegt. Python sucht beim import nach einer Datei mit genau diesem Namen – im Hauptverzeichnis und im Ordner lib.


7. So funktioniert das Display

Koordinatensystem des OLED-Displays mit 128 × 64 Pixeln und 8 Textzeilen
Abb. 2: Koordinatensystem des OLED-Displays mit 128 × 64 Pixeln und 8 Textzeilen

Die wichtigsten Regeln:

  1. Das Display hat 128 × 64 Pixel. Die Ecke oben links ist (0, 0). x wächst nach rechts (bis 127), y nach unten (bis 63).
  2. Jedes Zeichen ist 8 × 8 Pixel groß. Es passen also 16 Zeichen in eine Zeile und 8 Zeilen untereinander. Die Zeilen beginnen bei y = 0, 8, 16, 24, 32, 40, 48, 56.
  3. Erst zeichnen, dann anzeigen: Alle Befehle wie text() malen zuerst nur in einen Zwischenspeicher. Sichtbar wird alles erst mit oled.show().
  4. Mit oled.fill(0) wird der Zwischenspeicher gelöscht (alle Pixel aus). oled.fill(1) schaltet alle Pixel an.
  5. Keine Umlaute! Die eingebaute Schrift kennt nur die einfachen Zeichen von Tastatur (ASCII). Statt ä ö ü ß schreibst du ae oe ue ss. Statt ° schreibst du Grad oder C.
  6. text() kann nur Text (Strings) anzeigen. Zahlen musst du vorher mit str() umwandeln.

Die wichtigsten Befehle

Befehl Was passiert?
oled.fill(0) alles löschen (schwarz)
oled.text("Hallo", x, y) Text an Position (x, y) zeichnen
oled.show() Zwischenspeicher auf das Display schicken
oled.pixel(x, y, 1) einen einzelnen Pixel anschalten
oled.hline(x, y, breite, 1) waagerechte Linie
oled.rect(x, y, breite, hoehe, 1) Rechteck (nur Rand)
oled.rect(x, y, breite, hoehe, 1, True) gefülltes Rechteck
oled.invert(1) / oled.invert(0) Farben umkehren / normal

8. Programm 2: Hallo Welt auf dem Display

Jedes Display-Programm beginnt mit denselben vier Zeilen. Diesen Startblock wirst du ab jetzt immer wieder sehen:

pythonfrom machine import Pin, I2C
import ssd1306

i2c = I2C(0, scl=Pin(6), sda=Pin(5))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

oled.fill(0)                         # Zwischenspeicher löschen
oled.text("Hallo Welt!", 0, 0)       # Zeile 1 (y = 0)
oled.text("XIAO ESP32S3", 0, 16)     # Zeile 3 (y = 16)
oled.text("Klasse 10a", 0, 32)       # Zeile 5 (y = 32)
oled.show()                          # jetzt erst wird es sichtbar!

print("Text wurde angezeigt.")

Erwartetes Ergebnis

Auf dem Display stehen drei Zeilen Text. Zwischen den Zeilen ist je eine Zeile frei, weil wir y in Schritten von 16 erhöht haben.

Aufgaben – Stufe 1

  1. Zeige deinen Namen in der ersten Zeile und dein Lieblingsfach in der letzten Zeile an (y = 56).
  2. Schreibe einen Text, der genau in die Mitte des Displays passt. Tipp: Ein Wort mit 4 Buchstaben ist 32 Pixel breit. Wo muss x dann liegen?
  3. Was passiert, wenn du oled.show() weglässt? Was passiert bei einem Text mit mehr als 16 Zeichen?
  4. Zeichne mit oled.rect(0, 0, 128, 64, 1) einen Rahmen um das ganze Display. Damit der Text nicht auf dem Rahmen liegt, verschiebe ihn ein Stück nach innen.

9. Programm 3: Variablen anzeigen

Jetzt zeigen wir nicht nur festen Text an, sondern den Inhalt von Variablen.

pythonfrom machine import Pin, I2C
import ssd1306

i2c = I2C(0, scl=Pin(6), sda=Pin(5))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

a = 7
b = 6
ergebnis = a * b

oled.fill(0)
oled.text("a = " + str(a), 0, 0)
oled.text("b = " + str(b), 0, 16)
oled.text("a * b = " + str(ergebnis), 0, 32)
oled.show()

Mit + werden Texte aneinandergehängt. str(a) macht aus der Zahl 7 den Text "7".

Typischer Fehler: oled.text(ergebnis, 0, 32) ohne str() führt zu einer Fehlermeldung (TypeError). Das Display kann nur Text anzeigen.

Aufgaben – Stufe 1

  1. Ändere a und b und starte das Programm neu.
  2. Ergänze eine Zeile für a + b und eine für a - b.
  3. Berechne dein Alter in Tagen (ungefähr: Jahre × 365) und zeige es an.

10. Programm 4: Schleifendurchläufe zählen

In einer Schleife ändert sich die Variable bei jedem Durchlauf. Das Display zeigt dir das live an.

pythonfrom machine import Pin, I2C
import ssd1306
import time

i2c = I2C(0, scl=Pin(6), sda=Pin(5))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

zaehler = 0

while True:
    zaehler = zaehler + 1

    oled.fill(0)                          # alten Inhalt löschen
    oled.text("Durchlauf:", 0, 0)
    oled.text(str(zaehler), 0, 16)
    oled.show()

    time.sleep(1)                         # 1 Sekunde warten

Beenden kannst du das Programm mit dem roten Stop-Knopf in Thonny.

zaehler = 0

zaehler um 1 erhöhen

Display löschen:
fill(0)

Text zeichnen:
text(...)

Anzeigen:
show()

1 s warten

Experiment: Was macht fill(0)?

Setze vor die Zeile oled.fill(0) ein #, damit sie nicht mehr ausgeführt wird. Starte das Programm neu und beobachte die Zahlen ab 10. Was siehst du? Warum?

Erklärung: Ohne fill(0) werden die neuen Zeichen einfach über die alten gemalt. Pixel, die einmal an waren, bleiben an. So entsteht ein unleserlicher Zeichensalat.

Aufgaben – Stufe 2

  1. Schneller zählen: Ändere die Wartezeit auf 0,1 Sekunden (time.sleep(0.1)).
  2. Countdown: Zähle mit einer for-Schleife von 10 bis 0 rückwärts und zeige danach START! an. Tipp: for zahl in range(10, -1, -1):
  3. Einmaleins: Zeige die Siebenerreihe an: 1 x 7 = 7, 2 x 7 = 14 und so weiter, jede Rechnung 1 Sekunde lang.
  4. Zeit seit dem Start: Zeige zusätzlich an, wie viele Sekunden seit dem Programmstart vergangen sind. Tipp: time.ticks_ms() // 1000

11. Programm 5: Ein echter Messwert – die Chip-Temperatur

Der ESP32-S3 hat einen eingebauten Temperatursensor. Damit zeigen wir einen echten Messwert an, der sich von selbst ändert.

pythonfrom machine import Pin, I2C
import ssd1306
import esp32
import time

i2c = I2C(0, scl=Pin(6), sda=Pin(5))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

while True:
    temperatur = esp32.mcu_temperature()   # Temperatur des Chips in Grad Celsius

    oled.fill(0)
    oled.text("Chip-Temperatur", 0, 0)
    oled.text(str(temperatur) + " Grad C", 0, 24)
    oled.show()

    time.sleep(1)

Achtung: Gemessen wird die Temperatur im Chip, nicht die Raumtemperatur. Der Chip erwärmt sich beim Arbeiten selbst, deshalb ist der Wert höher als die Raumtemperatur.

Aufgaben – Stufe 2

  1. Lege einen Finger auf den XIAO. Ändert sich der Wert?
  2. Merke dir in einer Variablen hoechstwert die höchste gemessene Temperatur und zeige sie zusätzlich an. Tipp: if temperatur > hoechstwert:

12. Programm 6: Den Taster auf dem Board anzeigen

Der Taster auf dem Erweiterungsboard (Beschriftung Button(D1)) verbindet D1 beim Drücken mit GND. Wir schalten deshalb den internen Pull-up-Widerstand ein – genau wie in Kapitel 2, Abschnitt 9.

Tasterzustand taster.value()
losgelassen 1
gedrückt 0
pythonfrom machine import Pin, I2C
import ssd1306
import time

i2c = I2C(0, scl=Pin(6), sda=Pin(5))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

taster = Pin(2, Pin.IN, Pin.PULL_UP)     # Taster auf dem Board an D1 = GPIO2

while True:
    wert = taster.value()

    oled.fill(0)
    oled.text("Taster D1", 0, 0)
    oled.text("value() = " + str(wert), 0, 16)

    if wert == 0:
        oled.text("GEDRUECKT", 0, 40)
    else:
        oled.text("losgelassen", 0, 40)

    oled.show()
    time.sleep_ms(50)

Aufgaben – Stufe 2

  1. Lass das ganze Display invertieren, solange der Taster gedrückt ist. Tipp: oled.invert(1) und oled.invert(0)
  2. Zeichne ein Kästchen, das beim Drücken gefüllt und beim Loslassen leer ist. Tipp: oled.rect(56, 28, 16, 16, 1, True) zeichnet ein gefülltes Kästchen.

13. Programm 7: Tastendrücke zählen

Jetzt soll jeder Tastendruck genau einmal gezählt werden. Dafür brauchen wir das Entprellen aus Kapitel 2 (Version A).

pythonfrom machine import Pin, I2C
import ssd1306
import time

i2c = I2C(0, scl=Pin(6), sda=Pin(5))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

taster = Pin(2, Pin.IN, Pin.PULL_UP)

anzahl = 0

while True:
    if taster.value() == 0:              # Taster wurde gedrückt
        anzahl = anzahl + 1

        time.sleep_ms(50)                # Prellen abwarten
        while taster.value() == 0:       # warten, bis losgelassen
            time.sleep_ms(10)
        time.sleep_ms(50)

    oled.fill(0)
    oled.text("Tastendruecke:", 0, 0)
    oled.text(str(anzahl), 0, 24)
    oled.show()

    time.sleep_ms(10)

Aufgaben – Stufe 3

  1. Gerade oder ungerade? Zeige unter der Zahl zusätzlich gerade oder ungerade an. Tipp: if anzahl % 2 == 0:
  2. Zurücksetzen: Nach 10 Drücken springt der Zähler wieder auf 0.
  3. Würfel: Bei jedem Tastendruck wird eine Zufallszahl von 1 bis 6 angezeigt. Tipp: import random und random.randint(1, 6)

14. Programm 8: Werte als Balken anzeigen

Zahlen sind gut, aber eine Grafik erfasst man schneller. Dieses Programm zeigt einen Ladebalken, der von 0 bis 100 % wächst.

pythonfrom machine import Pin, I2C
import ssd1306
import time

i2c = I2C(0, scl=Pin(6), sda=Pin(5))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

for prozent in range(0, 101, 5):
    breite = prozent * 128 // 100        # 100 % = 128 Pixel

    oled.fill(0)
    oled.text("Laden: " + str(prozent) + " %", 0, 0)
    oled.rect(0, 24, 128, 16, 1)               # Rahmen
    oled.rect(0, 24, breite, 16, 1, True)      # gefüllter Balken
    oled.show()

    time.sleep_ms(200)

oled.text("Fertig!", 0, 52)
oled.show()

Rechentrick: // teilt und schneidet die Nachkommastellen ab. Pixel-Positionen müssen ganze Zahlen sein.

Aufgaben – Stufe 3

  1. Der Balken soll rückwärts von 100 % auf 0 % schrumpfen.
  2. Taster-Balken: Solange der Taster gedrückt ist, wächst der Balken. Beim Loslassen schrumpft er wieder.
  3. Temperatur-Balken: Zeige die Chip-Temperatur aus Programm 5 als Balken an: 20 Grad = leer, 60 Grad = voll.

15. Projekt: Stoppuhr

Zum Abschluss kombinierst du alles: Variablen, Schleife, Taster, Zeitmessung und Anzeige.

Tastendruck (Start bei 0)

Tastendruck (Zeit bleibt stehen)

Angehalten

Laeuft

pythonfrom machine import Pin, I2C
import ssd1306
import time

i2c = I2C(0, scl=Pin(6), sda=Pin(5))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

taster = Pin(2, Pin.IN, Pin.PULL_UP)

laeuft = False      # läuft die Stoppuhr gerade?
start = 0           # Zeitpunkt des Starts in ms
zeit_ms = 0         # gemessene Zeit in ms

while True:
    # --- Taster auswerten ---
    if taster.value() == 0:
        if laeuft:
            laeuft = False                  # anhalten
        else:
            laeuft = True                   # starten
            start = time.ticks_ms()

        time.sleep_ms(50)                   # entprellen
        while taster.value() == 0:
            time.sleep_ms(10)
        time.sleep_ms(50)

    # --- Zeit berechnen ---
    if laeuft:
        zeit_ms = time.ticks_diff(time.ticks_ms(), start)

    # --- Anzeigen ---
    oled.fill(0)
    oled.text("Stoppuhr", 0, 0)
    oled.text(str(zeit_ms / 1000) + " s", 0, 24)
    if laeuft:
        oled.text("laeuft ...", 0, 52)
    else:
        oled.text("Taste = Start", 0, 52)
    oled.show()

Erweiterungen – Stufe 3

  1. Reaktionstest: Nach einer zufälligen Wartezeit (2 bis 5 Sekunden) erscheint JETZT!. Gemessen wird die Zeit bis zum Tastendruck. Tipp: time.sleep_ms(random.randint(2000, 5000))
  2. Bestzeit: Speichere beim Reaktionstest die schnellste Zeit in einer Variablen und zeige sie in der letzten Zeile an.
  3. Als Gerät speichern: Speichere dein bestes Programm als main.py auf dem XIAO (siehe Kapitel 1). Dann läuft es nach dem Einstecken auch ohne PC, zum Beispiel an einer Powerbank.

16. Fehlersuche

Beobachtung Mögliche Ursache Abhilfe
ImportError: no module named 'ssd1306' Bibliothek nicht auf dem XIAO Abschnitt 6 wiederholen; Dateiname exakt ssd1306.py
i2c.scan() liefert [] XIAO sitzt nicht richtig, falsche Pins im Code XIAO stromlos neu einsetzen; scl=Pin(6), sda=Pin(5) prüfen
OSError beim Erstellen von oled Display antwortet nicht zuerst Programm 1 (I²C-Scan) ausführen; Adresse 60 muss erscheinen
i2c.scan() liefert [], obwohl der XIAO richtig sitzt Ein/Aus-Schalter des Boards steht auf OFF Schalter auf ON stellen
Display bleibt schwarz, keine Fehlermeldung oled.show() fehlt nach dem Zeichnen immer oled.show()
Zahlen werden zu Zeichensalat oled.fill(0) fehlt vor jedem neuen Bild oled.fill(0)
TypeError bei oled.text(...) Zahl statt Text übergeben Zahl mit str() umwandeln
Seltsame Zeichen statt ä, ö, ü Schrift kennt keine Umlaute ae, oe, ue schreiben
Text wird rechts abgeschnitten mehr als 16 Zeichen in einer Zeile Text kürzen oder auf zwei Zeilen verteilen
Taster zeigt immer 1 Pin.PULL_UP fehlt oder falscher Pin Pin(2, Pin.IN, Pin.PULL_UP)
Ein Druck zählt mehrfach Prellen nicht abgefangen Entprell-Block aus Programm 7 übernehmen