SH1106 1,3 Zoll OLED mit Arduino & ESP32: Unterschied zum SSD1306, Pixel-Versatz beheben & großes Dashboard bauen

Du hast ein 1,3 Zoll OLED mit 128x64 Pixeln gekauft, den bewährten SSD1306-Sketch hochgeladen – und siehst entweder gar nichts oder ein Bild, das um zwei Pixel verschoben ist und rechts einen seltsamen Streifen hat. Das ist kein Defekt, sondern der Unterschied zwischen zwei Controllern, die fast gleich sind: Der SH1106 hat 132 Spalten Speicher, aber nur 128 sichtbare Pixel. In diesem Artikel erfährst du, wie du beide auseinanderhältst, welcher Konstruktor der richtige ist, wie der Versatz sauber verschwindet – und wie du die zusätzliche Fläche für ein wirklich großes, aus zwei Metern ablesbares Dashboard nutzt.

1,3 Zoll oder 0,96 Zoll – der Größenunterschied ist größer als er klingt

Beide Displays haben 128x64 Pixel. Beide sprechen I2C. Beide hängen an denselben zwei Kabeln. Der Unterschied steckt in der Fläche pro Pixel – und die entscheidet darüber, ob du deinen Sensorwert vom Schreibtisch aus ablesen kannst oder nur mit der Nase am Gehäuse.

Merkmal 0,96 Zoll (SSD1306) 1,3 Zoll (SH1106)
Auflösung 128 x 64 128 x 64
Aktive Fläche ca. 22 x 11 mm ca. 29 x 15 mm
Fläche relativ 100 % ca. 180 %
Speicher im Controller 128 Spalten 132 Spalten
Sichtbarer Bereich Spalte 0–127 Spalte 2–129
Horizontales Adressieren ja (Auto-Increment über Seiten) nein – nur seitenweise
Kontrast / Helligkeit sehr hoch etwas geringer, dafür gleichmäßiger
Typische Stromaufnahme ca. 12 mA ca. 18 mA
I2C-Adresse 0x3C (selten 0x3D) 0x3C (selten 0x3D)

💡 180 % Fläche bei gleicher Pixelzahl bedeutet: dieselbe Schriftart wird fast doppelt so groß. Genau das ist der Grund, warum sich das 1,3-Zoll-Display für Wetterstationen, Thermostate und alles lohnt, was man aus einiger Entfernung ablesen will. Auf dem 0,96er wirkt ein u8g2_font_logisoso32 gerade richtig groß, auf dem 1,3er wird daraus eine Anzeige, die man vom Sofa aus liest.

Warum der Versatz überhaupt entsteht

Der SH1106 ist ursprünglich für Panels mit 132 Spalten entwickelt worden. Sein Grafikspeicher ist entsprechend 132 Spalten breit. Auf den gängigen Modulen ist aber ein 128-Pixel-Panel angeschlossen – und zwar mittig, also an den Spalten 2 bis 129.

Was daraus folgt:

  • Schreibst du in Speicherspalte 0 und 1, landet das Ergebnis im unsichtbaren Bereich links.
  • Dein gesamtes Bild erscheint um zwei Pixel nach links versetzt.
  • Die letzten beiden Pixelspalten deines Bildes fallen rechts aus dem sichtbaren Bereich – oder erscheinen als Müllstreifen, wenn dort alte Daten stehen.

Dazu kommt ein zweiter, subtilerer Unterschied: Der SSD1306 kennt einen Horizontal Addressing Mode, bei dem der Spaltenzähler am Zeilenende automatisch in die nächste Seite springt. Der SH1106 kann das nicht – er braucht vor jeder der acht Seiten einen eigenen Adressbefehl. Deshalb reicht es nicht, im SSD1306-Treiber nur einen Offset zu addieren; die Übertragungsroutine selbst ist eine andere. Genau das ist der Grund, warum eine reine SSD1306-Bibliothek an diesem Display scheitert.

ℹ️ Wie erkenne ich, was ich vor mir habe? Von außen praktisch gar nicht – die Module sehen identisch aus und tragen selten eine Beschriftung. Zwei zuverlässige Anhaltspunkte: Größe (1,3 Zoll ist fast immer SH1106, 0,96 Zoll fast immer SSD1306) und der Praxistest aus Schritt 2 unten – einfach beide Konstruktoren nacheinander ausprobieren. Das dauert zwei Minuten und ist endgültig.

Das brauchst du

1,3 Zoll I2C OLED Display 128x64 (weiß) – das Display um das es hier geht
1,3 Zoll OLED mit EC11 Rotary Encoder – Display und Drehgeber als fertige Einheit
ESP32 DEV KIT WROOM32 – reichlich RAM für den Full-Buffer-Modus
Arduino Nano V3 – geht auch, aber nur im Page-Buffer-Modus komfortabel
BMP280 Sensor – Temperatur und Luftdruck fürs Dashboard
DHT22 / AM2302 – Luftfeuchte für die zweite Kachel
Rotary Encoder KY-040 – zum Blättern zwischen den Seiten
DS3231 Echtzeituhr – Uhrzeit ohne WLAN, teilt sich den I2C-Bus
Breadboard + Dupont-Kabel Set – vier Kabel genügen

1Anschließen – vier Pins, keine Überraschungen

OLED Arduino Nano / UNO ESP32 ESP8266 (D1 Mini)
VCC 5 V 3V3 3V3
GND GND GND GND
SDA A4 GPIO21 D2 (GPIO4)
SCL A5 GPIO22 D1 (GPIO5)

Der schnellste Weg, um festzustellen ob überhaupt etwas antwortet, ist ein I2C-Scan:

#include <Wire.h>

void setup() {
  Serial.begin(115200);
  Wire.begin();
  delay(200);

  Serial.println(F("I2C-Scan..."));
  uint8_t gefunden = 0;

  for (uint8_t addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) {
      Serial.print(F("Geraet bei 0x"));
      if (addr < 16) Serial.print('0');
      Serial.print(addr, HEX);

      if (addr == 0x3C || addr == 0x3D) Serial.print(F("  <- OLED (SH1106 oder SSD1306)"));
      if (addr == 0x76 || addr == 0x77) Serial.print(F("  <- BMP280/BME280"));
      if (addr == 0x68)                 Serial.print(F("  <- DS3231"));
      Serial.println();
      gefunden++;
    }
  }

  if (gefunden == 0)
    Serial.println(F("Nichts gefunden - SDA/SCL vertauscht oder VCC fehlt?"));
}

void loop() { }

ℹ️ Die Adresse verrät den Controller nicht. SH1106 und SSD1306 melden sich beide unter 0x3C. Ein erfolgreicher Scan sagt dir also nur, dass die Verkabelung stimmt – welcher Chip drinsteckt, klärt erst der Test im nächsten Schritt. Mehr zum Bus im Artikel I2C-Bus mit Arduino verstehen.

2Der Zwei-Minuten-Test: SH1106 oder SSD1306?

U8g2 bringt Treiber für beide Controller mit. Dieser Sketch zeichnet einen Rahmen exakt an den Bildschirmrand plus je eine Markierung in allen vier Ecken – damit siehst du sofort, ob etwas verrutscht ist:

#include <Wire.h>
#include <U8g2lib.h>

// --- Variante A: SH1106 (typisch fuer 1,3 Zoll) ---
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// --- Variante B: SSD1306 (typisch fuer 0,96 Zoll) ---
// U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

void setup() {
  Wire.begin();
  Wire.setClock(400000);
  u8g2.begin();
  u8g2.enableUTF8Print();
}

void loop() {
  u8g2.clearBuffer();

  // Rahmen exakt auf dem Rand - muss rundum sichtbar sein
  u8g2.drawFrame(0, 0, 128, 64);

  // Eckmarkierungen: 5x5 Pixel in jeder Ecke
  u8g2.drawBox(0, 0, 5, 5);
  u8g2.drawBox(123, 0, 5, 5);
  u8g2.drawBox(0, 59, 5, 5);
  u8g2.drawBox(123, 59, 5, 5);

  // Mittelkreuz
  u8g2.drawHLine(0, 32, 128);
  u8g2.drawVLine(64, 0, 64);

  u8g2.setFont(u8g2_font_6x10_te);
  u8g2.drawStr(10, 28, "SH1106 Test");
  u8g2.drawStr(10, 44, "Rahmen rundum?");

  u8g2.sendBuffer();
  delay(1000);
}
Was du siehst Bedeutung Was zu tun ist
Rahmen rundum sichtbar, vier Ecken vollständig Konstruktor stimmt nichts – fertig
Bild 2 Pixel nach links versetzt, rechts fehlt der Rahmen SH1106 mit SSD1306-Treiber auf U8G2_SH1106_... wechseln
Rechts ein schmaler Müll- oder Punktestreifen SH1106 mit SSD1306-Treiber auf U8G2_SH1106_... wechseln
Bild 2 Pixel nach rechts versetzt, links Rand abgeschnitten SSD1306 mit SH1106-Treiber auf U8G2_SSD1306_... wechseln
Alles schwarz Adresse, Verkabelung oder fehlendes sendBuffer() Schritt 1 wiederholen
Wirre Streifen über den ganzen Schirm falsche Auflösung im Konstruktor 128X64 statt 128X32 prüfen

💡 U8g2 erledigt den Offset für dich – das ist der ganze Trick. Der SH1106-Treiber schreibt intern in die Spalten 2 bis 129 und benutzt die seitenweise Übertragung. Du zeichnest weiterhin ganz normal in Koordinaten von 0 bis 127. Deshalb ist der Wechsel des Konstruktors auch die komplette Lösung – kein +2 in deinem Zeichencode, keine Sonderfälle.

⚠️ Wer bei Adafruit_SSD1306 bleiben will, hat ein Problem. Die Bibliothek kann den SH1106 nicht ansteuern, weil ihr das seitenweise Adressieren fehlt. Es gibt Forks („Adafruit_SH1106") die das nachrüsten, aber sie werden nicht gepflegt und kollidieren gerne mit dem Original. Nimm für SH1106-Displays U8g2 – es unterstützt beide Controller sauber und ist die deutlich bessere Wahl.

3Full Buffer oder Page Buffer? Der Konstruktor entscheidet

Der Buchstabe in der Mitte des U8g2-Konstruktors bestimmt, wie viel RAM das Display belegt – und auf einem Arduino Nano mit 2 kB ist das die wichtigste Entscheidung überhaupt:

Konstruktor RAM Zeichnen Empfohlen für
_F_ (Full) 1024 Byte clearBuffer() → zeichnen → sendBuffer() ESP32, ESP8266, RP2040
_1_ (Page, 1 Seite) 128 Byte firstPage() / nextPage()-Schleife Arduino Nano / UNO
_2_ (Page, 2 Seiten) 256 Byte wie _1_, halb so viele Durchläufe Nano mit etwas Luft

So sieht derselbe Bildschirm in beiden Varianten aus:

// ============ Full Buffer (ESP32) ============
#include <Wire.h>
#include <U8g2lib.h>

U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

float temperatur = 21.4;

void setup() {
  Wire.begin();
  Wire.setClock(400000);
  u8g2.begin();
  u8g2.enableUTF8Print();
}

void loop() {
  u8g2.clearBuffer();

  u8g2.setFont(u8g2_font_6x12_te);
  u8g2.drawUTF8(0, 10, "Wohnzimmer");

  u8g2.setFont(u8g2_font_logisoso32_tn);
  char t[10];
  snprintf(t, sizeof(t), "%.1f", temperatur);
  u8g2.drawStr(0, 50, t);

  u8g2.setFont(u8g2_font_6x12_te);
  u8g2.drawUTF8(96, 30, "°C");

  u8g2.sendBuffer();
  delay(500);
}
// ============ Page Buffer (Arduino Nano) ============
#include <Wire.h>
#include <U8g2lib.h>

U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

float temperatur = 21.4;

void setup() {
  Wire.begin();
  Wire.setClock(400000);
  u8g2.begin();
  u8g2.enableUTF8Print();
}

void loop() {
  char t[10];
  dtostrf(temperatur, 4, 1, t);      // auf AVR kein %f in snprintf

  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_6x12_te);
    u8g2.drawUTF8(0, 10, "Wohnzimmer");

    u8g2.setFont(u8g2_font_logisoso32_tn);
    u8g2.drawStr(0, 50, t);

    u8g2.setFont(u8g2_font_6x12_te);
    u8g2.drawUTF8(96, 30, "°C");
  } while (u8g2.nextPage());

  delay(500);
}

⚠️ Im Page-Modus wird der Zeichenblock achtmal durchlaufen. Alles zwischen firstPage() und nextPage() läuft acht Mal. Sensoren dort auszulesen ist ein klassischer Fehler – du liest achtmal, brauchst achtmal die Wartezeit, und wenn sich der Wert dabei ändert, zeigt das Display oben eine andere Zahl als unten. Messwerte immer vor der Schleife holen und in eine Variable legen.

⚠️ snprintf mit %f funktioniert auf AVR nicht. Auf dem Nano ist die Gleitkomma-Ausgabe standardmäßig aus dem printf-Code herauskompiliert – du bekommst nur ein ?. Nimm dtostrf(). Auf dem ESP32 gibt es dieses Problem nicht.

4Das große Dashboard – Kacheln statt Textzeilen

Jetzt zum eigentlichen Grund für das größere Display: eine Anzeige, die man aus zwei Metern lesen kann. Statt acht Textzeilen übereinander teilst du die Fläche in Kacheln auf – ein großer Hauptwert plus zwei kleinere Nebenwerte, dazu eine Statuszeile.

#include <Wire.h>
#include <U8g2lib.h>

U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// --- Messwerte (hier Platzhalter, real vom BMP280 / DHT22) ---
float temperatur = 21.4;
float feuchte    = 58.2;
float druck      = 1013.6;
bool  wlanOk     = true;
uint8_t akku     = 82;

// --- Verlauf der Temperatur fuer die Sparkline ---
#define VERLAUF_N 62
int8_t  verlauf[VERLAUF_N];
uint8_t verlaufIdx = 0;
bool    verlaufVoll = false;

void verlaufHinzu(float wert) {
  // In Zehntelgrad relativ zu 20 Grad speichern - passt in int8_t
  int16_t v = (int16_t)((wert - 20.0f) * 4.0f);
  if (v >  127) v =  127;
  if (v < -128) v = -128;

  verlauf[verlaufIdx] = (int8_t)v;
  verlaufIdx++;
  if (verlaufIdx >= VERLAUF_N) { verlaufIdx = 0; verlaufVoll = true; }
}

void zeichneSparkline(uint8_t x, uint8_t y, uint8_t breite, uint8_t hoehe) {
  uint8_t n = verlaufVoll ? VERLAUF_N : verlaufIdx;
  if (n < 2) return;

  int8_t mn = 127, mx = -128;
  for (uint8_t i = 0; i < n; i++) {
    if (verlauf[i] < mn) mn = verlauf[i];
    if (verlauf[i] > mx) mx = verlauf[i];
  }
  if (mx == mn) mx = mn + 1;

  uint8_t vorherY = 0;
  for (uint8_t i = 0; i < n && i < breite; i++) {
    uint8_t idx = verlaufVoll ? (verlaufIdx + i) % VERLAUF_N : i;
    int16_t w = verlauf[idx];

    uint8_t py = y + hoehe - 1
               - (uint16_t)(w - mn) * (hoehe - 1) / (mx - mn);

    if (i > 0) u8g2.drawLine(x + i - 1, vorherY, x + i, py);
    vorherY = py;
  }
}

void akkuIcon(uint8_t x, uint8_t y, uint8_t prozent) {
  u8g2.drawFrame(x, y, 13, 7);
  u8g2.drawBox(x + 13, y + 2, 2, 3);
  uint8_t f = (uint16_t) 11 * prozent / 100;
  if (f > 0) u8g2.drawBox(x + 1, y + 1, f, 5);
}

void wlanIcon(uint8_t x, uint8_t y, bool ok) {
  if (ok) {
    for (uint8_t i = 0; i < 3; i++)
      u8g2.drawBox(x + i * 3, y + 6 - i * 2, 2, 1 + i * 2);
  } else {
    u8g2.drawLine(x, y, x + 7, y + 7);
    u8g2.drawLine(x + 7, y, x, y + 7);
  }
}

void setup() {
  Wire.begin();
  Wire.setClock(400000);
  u8g2.begin();
  u8g2.enableUTF8Print();

  // Testdaten fuer den Verlauf
  for (uint8_t i = 0; i < VERLAUF_N; i++)
    verlaufHinzu(21.0f + sin(i * 0.15f) * 1.2f);
}

void loop() {
  u8g2.clearBuffer();

  // ---- Kopfzeile: Ort links, Icons rechts ----
  u8g2.setFont(u8g2_font_5x8_te);
  u8g2.drawUTF8(0, 7, "WOHNZIMMER");

  wlanIcon(96, 1, wlanOk);
  akkuIcon(110, 1, akku);
  u8g2.drawHLine(0, 10, 128);

  // ---- Hauptwert: gross und fett ----
  u8g2.setFont(u8g2_font_logisoso30_tn);
  char t[12];
  snprintf(t, sizeof(t), "%.1f", temperatur);
  u8g2.drawStr(0, 42, t);

  u8g2.setFont(u8g2_font_9x15_te);
  u8g2.drawUTF8(72, 26, "°C");

  // ---- Nebenwerte rechts, kleiner ----
  u8g2.setFont(u8g2_font_6x10_te);
  snprintf(t, sizeof(t), "%.0f%%", feuchte);
  u8g2.drawUTF8(90, 30, t);

  snprintf(t, sizeof(t), "%.0f", druck);
  u8g2.drawStr(84, 42, t);
  u8g2.setFont(u8g2_font_4x6_tf);
  u8g2.drawStr(112, 42, "hPa");

  // ---- Trennlinie und Verlauf unten ----
  u8g2.drawHLine(0, 46, 128);

  u8g2.setFont(u8g2_font_4x6_tf);
  u8g2.drawStr(0, 63, "6h");
  zeichneSparkline(14, 49, 100, 14);

  u8g2.sendBuffer();
  delay(500);
}

💡 Warum der Verlauf in int8_t statt float gespeichert wird: 62 float-Werte sind 248 Byte, 62 int8_t sind 62 Byte. Auf dem ESP32 egal, auf dem Nano der Unterschied zwischen „läuft" und „stürzt ab". Der Trick ist die Skalierung: Temperatur minus 20 Grad, mal 4 – damit deckst du −12 bis +51 Grad in 0,25-Grad-Schritten ab. Für eine Kurve auf 14 Pixeln Höhe ist das mehr als genug. Mehr solcher Kniffe im Artikel Arduino Speicher voll? RAM sparen.

5Echte Sensordaten und Seiten mit dem Encoder

Zum Abschluss die vollständige Version: BMP280 für Temperatur und Druck, KY-040 zum Blättern, alles ohne blockierendes delay():

#include <Wire.h>
#include <U8g2lib.h>
#include <Adafruit_BMP280.h>

U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
Adafruit_BMP280 bmp;

#define ENC_CLK  25
#define ENC_DT   26
#define ENC_SW   27

volatile int8_t  encDelta = 0;
volatile uint8_t letzterZustand = 0;

void IRAM_ATTR encoderIsr() {
  uint8_t a = digitalRead(ENC_CLK);
  uint8_t b = digitalRead(ENC_DT);
  uint8_t zustand = (a << 1) | b;
  if (zustand == letzterZustand) return;

  if ((letzterZustand == 0b00 && zustand == 0b01) ||
      (letzterZustand == 0b01 && zustand == 0b11) ||
      (letzterZustand == 0b11 && zustand == 0b10) ||
      (letzterZustand == 0b10 && zustand == 0b00)) encDelta++;
  else                                              encDelta--;

  letzterZustand = zustand;
}

const uint8_t SEITEN = 3;
uint8_t seite = 0;

float tempAktuell = 0, druckAktuell = 0;
float tempMin = 999, tempMax = -999;
uint32_t letzteMessung = 0;
const uint16_t MESS_INTERVALL = 2000;

void seiteHaupt() {
  u8g2.setFont(u8g2_font_5x8_te);
  u8g2.drawUTF8(0, 7, "TEMPERATUR");
  u8g2.drawHLine(0, 10, 128);

  u8g2.setFont(u8g2_font_logisoso30_tn);
  char t[12];
  snprintf(t, sizeof(t), "%.1f", tempAktuell);
  u8g2.drawStr(4, 48, t);

  u8g2.setFont(u8g2_font_9x15_te);
  u8g2.drawUTF8(84, 32, "°C");

  u8g2.setFont(u8g2_font_5x8_tf);
  snprintf(t, sizeof(t), "%.0f hPa", druckAktuell);
  u8g2.drawStr(84, 48, t);
}

void seiteMinMax() {
  u8g2.setFont(u8g2_font_5x8_te);
  u8g2.drawUTF8(0, 7, "MIN / MAX");
  u8g2.drawHLine(0, 10, 128);

  char t[16];
  u8g2.setFont(u8g2_font_9x15_te);

  snprintf(t, sizeof(t), "MIN %.1f", tempMin);
  u8g2.drawUTF8(0, 30, t);

  snprintf(t, sizeof(t), "MAX %.1f", tempMax);
  u8g2.drawUTF8(0, 50, t);

  u8g2.setFont(u8g2_font_5x8_tf);
  snprintf(t, sizeof(t), "Spanne %.1f K", tempMax - tempMin);
  u8g2.drawStr(0, 62, t);
}

void seiteInfo() {
  u8g2.setFont(u8g2_font_5x8_te);
  u8g2.drawUTF8(0, 7, "SYSTEM");
  u8g2.drawHLine(0, 10, 128);

  u8g2.setFont(u8g2_font_6x10_tf);
  char t[24];

  snprintf(t, sizeof(t), "Display SH1106");
  u8g2.drawStr(0, 24, t);

  snprintf(t, sizeof(t), "Laufzeit %lu min", millis() / 60000UL);
  u8g2.drawStr(0, 38, t);

  snprintf(t, sizeof(t), "Heap %u kB", (unsigned)(ESP.getFreeHeap() / 1024));
  u8g2.drawStr(0, 52, t);
}

void seitenPunkte() {
  for (uint8_t i = 0; i < SEITEN; i++) {
    uint8_t x = 56 + i * 6;
    if (i == seite) u8g2.drawBox(x, 61, 4, 3);
    else            u8g2.drawPixel(x + 1, 62);
  }
}

void setup() {
  Serial.begin(115200);
  Wire.begin();
  Wire.setClock(400000);

  u8g2.begin();
  u8g2.enableUTF8Print();

  if (!bmp.begin(0x76)) {
    if (!bmp.begin(0x77)) Serial.println(F("BMP280 nicht gefunden"));
  }
  bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
                  Adafruit_BMP280::SAMPLING_X2,
                  Adafruit_BMP280::SAMPLING_X16,
                  Adafruit_BMP280::FILTER_X16,
                  Adafruit_BMP280::STANDBY_MS_500);

  pinMode(ENC_CLK, INPUT_PULLUP);
  pinMode(ENC_DT,  INPUT_PULLUP);
  pinMode(ENC_SW,  INPUT_PULLUP);
  letzterZustand = (digitalRead(ENC_CLK) << 1) | digitalRead(ENC_DT);
  attachInterrupt(digitalPinToInterrupt(ENC_CLK), encoderIsr, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENC_DT),  encoderIsr, CHANGE);
}

void loop() {
  uint32_t jetzt = millis();

  // --- Messen, aber nicht bei jedem Durchlauf ---
  if (jetzt - letzteMessung >= MESS_INTERVALL) {
    letzteMessung = jetzt;
    tempAktuell  = bmp.readTemperature();
    druckAktuell = bmp.readPressure() / 100.0f;

    if (tempAktuell < tempMin) tempMin = tempAktuell;
    if (tempAktuell > tempMax) tempMax = tempAktuell;
  }

  // --- Encoder auswerten ---
  noInterrupts();
  int8_t d = encDelta;
  encDelta = 0;
  interrupts();

  if (d != 0) {
    int8_t schritte = d / 4;              // 4 Flanken pro Rastung
    if (schritte != 0) {
      int8_t neu = (int8_t)seite + schritte;
      while (neu < 0) neu += SEITEN;
      seite = neu % SEITEN;
    }
  }

  if (digitalRead(ENC_SW) == LOW) {
    tempMin = tempMax = tempAktuell;      // Tastendruck: Min/Max zuruecksetzen
    while (digitalRead(ENC_SW) == LOW) delay(10);
  }

  // --- Zeichnen ---
  u8g2.clearBuffer();
  switch (seite) {
    case 0: seiteHaupt();  break;
    case 1: seiteMinMax(); break;
    case 2: seiteInfo();   break;
  }
  seitenPunkte();
  u8g2.sendBuffer();

  delay(40);
}

ℹ️ Warum IRAM_ATTR vor der ISR steht: Auf dem ESP32 liegt Code standardmäßig im Flash. Während eines Flash-Zugriffs darf aber kein Interrupt aus dem Flash laufen – das führt zum Absturz. IRAM_ATTR legt die Funktion ins interne RAM. Auf dem Arduino Nano lässt du das Attribut weg. Alles zum Thema im Artikel Arduino Interrupts verstehen.

Troubleshooting

⚠️ Das Bild ist um zwei Pixel verschoben: Der Klassiker – SSD1306-Konstruktor an einem SH1106. Wechsle auf U8G2_SH1106_128X64_NONAME_.... Ein manueller Offset im Zeichencode ist keine Lösung, weil auch die Übertragungsroutine anders sein muss.

⚠️ Rechts steht ein schmaler Streifen mit zufälligen Punkten: Dasselbe Problem. Der SSD1306-Treiber beschreibt die Spalten 0–127, die beim SH1106 auf 128–129 unsichtbar enden, während rechts alte Speicherinhalte stehen bleiben.

⚠️ Der Bildschirm bleibt komplett schwarz: Meist fehlt u8g2.sendBuffer() – U8g2 zeichnet nur in den RAM-Puffer. Zweithäufigste Ursache: u8g2.begin() vergessen. Dritthäufigste: die I2C-Adresse ist 0x3D statt 0x3C, dann hilft u8g2.setI2CAddress(0x3D * 2) vor begin().

⚠️ Der Sketch kompiliert nicht mehr, seit U8g2 dazugekommen ist (Nano): Flash voll. U8g2 zieht die Fonts mit, die du verwendest – vier verschiedene Schriften sind schnell 8 kB. Reduziere auf zwei Fonts und nimm _tn-Varianten (nur Ziffern), wo du keine Buchstaben brauchst.

⚠️ Die Anzeige flackert bei jedem Update: Du zeichnest zu oft. 2 bis 5 Bilder pro Sekunde reichen für Messwerte völlig. Setz ein millis()-Intervall vor den Zeichenblock – siehe Arduino millis() statt delay().

⚠️ Umlaute und das Gradzeichen erscheinen als Kauderwelsch: Du brauchst einen Font mit der Endung _te oder _tf, dazu enableUTF8Print() und drawUTF8() statt drawStr(). Mit _tn-Fonts (nur Ziffern) geht das prinzipbedingt nicht.

⚠️ Nach einigen Monaten Dauerbetrieb bleibt ein Schatten stehen: Einbrennen. OLED-Pixel altern dort schneller, wo sie dauerhaft hell sind. Gegenmittel: u8g2.setPowerSave(1) bei Inaktivität, oder das Layout alle paar Minuten um ein bis zwei Pixel verschieben.

⚠️ Das Display wird bei längeren Kabeln unzuverlässig: I2C ist keine Fernverbindung. Ab etwa 50 cm brauchst du stärkere Pullups – 2,2 kΩ aus dem Widerstands-Sortiment parallel zu den 10 kΩ auf dem Modul – oder senke den Takt auf 100 kHz.

Praxistipps

  • Erst Font wählen, dann Layout planen. Auf dem 1,3-Zoll-Display wirkt u8g2_font_logisoso30_tn wie eine Digitaluhr – groß genug fürs andere Zimmerende. Darunter bleiben noch 14 Pixel für eine Sparkline oder zwei kleine Zeilen.
  • Ziffern-Fonts sparen viel Flash. Fonts mit _tn enthalten nur Zahlen und Satzzeichen und sind oft ein Zehntel so groß wie der volle Zeichensatz. Für den Hauptwert reicht das immer.
  • Wire.setClock(400000) nicht vergessen. Ein volles 128x64-Bild sind 1024 Byte. Bei 100 kHz dauert die Übertragung rund 10 ms, bei 400 kHz nur noch 2,5 ms – spürbar, sobald sich etwas bewegt.
  • Der SH1106 verträgt 3,3 und 5 V. Die Module haben Regler und Pegelanpassung an Bord. Am ESP32 nimm trotzdem 3,3 V, sonst ziehen die I2C-Pullups die Pins auf 5 V.
  • Displays mischen geht problemlos. Ein SH1106 auf 0x3C und ein 0,91 Zoll SSD1306 auf 0x3D am selben Bus – du legst einfach zwei U8g2-Objekte an, jedes mit seinem passenden Konstruktor.
  • Im Batteriebetrieb abschalten. u8g2.setPowerSave(1) senkt den Verbrauch auf unter 10 µA. Bei 18 mA im Betrieb ist das Display sonst der größte Verbraucher überhaupt – mehr als der schlafende ESP32.
  • Die Version mit Encoder spart Lötarbeit. Das 1,3 Zoll OLED mit EC11 Rotary Encoder hat beides auf einer Platine – für Menüsysteme ist das die deutlich saubere Lösung als zwei Module auf dem Breadboard. Wie ein scrollbares Menü aufgebaut wird, zeigt der Artikel OLED-Menüsystem mit Rotary Encoder bauen.

🚀 Bauteile für dein großes Display-Projekt

Fazit

Der berüchtigte Zwei-Pixel-Versatz ist kein Fehler und auch kein Qualitätsproblem – er ist die logische Folge davon, dass ein 128-Pixel-Panel mittig an einem 132-Spalten-Controller hängt. Die komplette Lösung ist ein anderer U8g2-Konstruktor, und danach zeichnest du wie gewohnt in Koordinaten von 0 bis 127.

Was bleibt, ist der eigentliche Grund für dieses Display: rund 80 % mehr Fläche bei identischer Pixelzahl. Dieselbe Schrift wird sichtbar größer, dieselbe Sparkline deutlicher, dasselbe Menü bedienbarer. Für alles, was nicht direkt vor der Nase steht – Wetterstation im Regal, Thermostat an der Wand, Statusanzeige im Serverschrank – ist das 1,3-Zoll-Modul die bessere Wahl. Zwei Minuten Testsketch, ein geänderter Konstruktor, und der Rest ist Layout-Arbeit.

Zurück zum Blog