Zum Inhalt

I2C-Bus Engineering, Protokoll & Hardware-Diagnose

Der Inter-Integrated Circuit (I2C)-Bus – ursprünglich von Philips Semiconductor (heute NXP) entwickelt – ist der weltweite Industriestandard für die Platinen-Kommunikation über kurze Distanzen zwischen Mikrocontrollern, Einplatinencomputern und Sensoren. Mit lediglich zwei bidirektionalen Signalleitungen (SDA für Daten und SCL für den Takt) erlaubt I2C einem Raspberry Pi als Bus-Master die Adressierung von bis zu 127 Slaves über einen gemeinsamen Multidrop-Bus.

Während das Anschließen eines einzelnen Sensor-Breakout-Boards am Schreibtisch simpel ist, erfordert der Bau robuster industrieller Sensornetzwerke über längere Kabelstrecken ein solides Verständnis von Buskapazität, Open-Drain-Physik, Pull-Up-Dimensionierung und Kernel-Wiederherstellungsmechanismen.


1. Elektrische Architektur: Open-Drain & Pull-Up-Physik

Im Gegensatz zu Push-Pull-Schnittstellen (wie SPI oder Standard-UART), bei denen Ausgangstreiber aktiv zwischen High (3,3 V) und Low (0 V) umschalten, arbeitet I2C ausnahmslos als Open-Drain (Open-Collector)-Bus.

%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    subgraph PowerRail["3,3 V VDD Spannungsversorgung"]
        RP1["Rp (SDA Pull-Up: 1,8 kΩ)"]
        RP2["Rp (SCL Pull-Up: 1,8 kΩ)"]
    end

    subgraph BusLines["Gemeinsamer 2-Draht Open-Drain-Bus"]
        direction TB
        SDA["SDA-Leitung (Serielle Daten)"]
        SCL["SCL-Leitung (Serieller Takt)"]
        SDA --- SCL
    end

    subgraph Nodes["Bus-Teilnehmer (Wired-AND)"]
        direction TB
        RPI["Raspberry Pi Host-Controller"]
        SEN1["BME280 Sensor (Adresse: 0x76)"]
        SEN2["OLED-Display (Adresse: 0x3C)"]
        RPI --> SEN1 --> SEN2
    end

    PowerRail ==> BusLines
    BusLines ==> Nodes

Das Wired-AND-Prinzip

Jedes an den Bus angeschlossene Bauteil kann die Leitungen nur auf zwei Arten beeinflussen: 1. Leitung auf LOW ziehen: Ein interner N-Kanal-MOSFET schaltet durch und zieht den Pin auf Masse (0 V). 2. Leitung loslassen: Der MOSFET sperrt (hochohmiger Zustand). Ein externer Pull-Up-Widerstand (Rp) zieht die Spannung auf 3,3 V.

Da kein Baustein den Bus jemals aktiv auf HIGH treibt, können gleichzeitige Sendeversuche niemals zu Kurzschlüssen führen: Senden zwei Geräte gleichzeitig, bleibt die Leitung einfach auf Masse. Dies ermöglicht Bus-Arbitrierung und Clock-Stretching.

Dimensionierung der Pull-Up-Widerstände (RC-Glied & Kapazität)

Der Raspberry Pi besitzt auf Board-Ebene fest verlötete 1,8-kΩ-Pull-Up-Widerstände an Pin 3 (GPIO 2 / SDA) und Pin 5 (GPIO 3 / SCL) gegen 3,3 V.

Werden mehrere Sensoren über längere Leitungen angeschlossen, steigt die parasitäre Buskapazität (Cb). Die I2C-Spezifikation erlaubt eine maximale Buskapazität von 400 pF (Fast-Mode). Ist der Gesamtwiderstand zu hoch oder die Kapazität zu groß, steigt das Signal zu langsam an, was zu Bit- und Übertragungsfehlern führt.

Minimaler Pull-Up-Widerstand (begrenzt durch max. Stromaufnahmefähigkeit):
Rp(min) = (VDD - VOL) ÷ IOL
Für 3,3 V Logik mit VOL = 0,4 V und max. Senkenstrom IOL = 3 mA:
Rp(min) = (3,3 V - 0,4 V) ÷ 0,003 A = 966 Ω

Maximaler Pull-Up-Widerstand (begrenzt durch zulässige Anstiegszeit):
Rp(max) = tr ÷ (0,8473 · Cb)
Für Fast-Mode (400 kHz) mit maximal erlaubter Flankenanstiegszeit tr = 300 ns und 200 pF Buskapazität:
Rp(max) = 300 × 10-9 s ÷ (0,8473 · 200 × 10-12 F) ≈ 1,77 kΩ

Praxis-Tipp für Breakout-Boards

Nahezu jedes Sensor-Breakout-Board (Adafruit, SparkFun etc.) besitzt eigene integrierte 10-kΩ- oder 4,7-kΩ-Pull-Up-Widerstände. Werden 5 solche Module parallel geschaltet, sinkt der resultierende Gesamtwiderstand:

1 / Rges = 1/RPi + 1/R1 + 1/R2 + ...

Fällt der Gesamtwiderstand unter 800 Ω, überschreitet der Strom die 3-mA-Grenze und die Sensoren können den Bus nicht mehr sauber auf Low ziehen. Bei Kommunikationsproblemen empfiehlt es sich, die SMD-Widerstände auf überzähligen Sensorplatinen abzulöten.


2. Kernel-Aktivierung & Taktfrequenz-Konfiguration

Unter Raspberry Pi OS Bookworm wird der I2C-Hardware-Controller über /boot/firmware/config.txt aktiviert und konfiguriert.

Konfigurationsdatei öffnen:

sudo nano /boot/firmware/config.txt

Folgende Parameter prüfen oder eintragen:

# I2C-1 Hardware-Controller aktivieren
dtparam=i2c_arm=on

# Bustakt konfigurieren:
# 100000 = Standard-Mode (100 kHz, Default)
# 400000 = Fast-Mode (400 kHz, empfohlen für moderne Sensoren)
# 1000000 = Fast-Mode Plus (1 MHz, nur für sehr kurze Leiterbahnen < 10 cm)
dtparam=i2c_arm_baudrate=400000

Neu starten, um die Änderungen zu laden:

sudo reboot

Prüfen, ob die Gerätedatei /dev/i2c-1 vorhanden ist:

ls -l /dev/i2c*
# Ausgabe: crw-rw---- 1 root i2c 89, 1 /dev/i2c-1

3. Hardware-Diagnose mit i2c-tools

Das Werkzeugpaket i2c-tools ist das wichtigste Hilfsmittel zur Fehlersuche auf Linux-Systemen.

Installation:

sudo apt update
sudo apt install -y i2c-tools

Bus-Scan mit i2cdetect

Mit i2cdetect werden alle 7-Bit-Adressen (von 0x03 bis 0x77) auf Bus 1 gescannt:

i2cdetect -y 1

Beispielhafte Ausgabe:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- UU 77                         

Auswertung der Adressmatrix

  • --: Keine Rückmeldung an dieser Adresse (NACK erhalten).
  • 3c: Ein Sensor/Display hat geantwortet (z. B. ein SSD1306 OLED-Display).
  • UU: Die Adresse ist vom Linux-Kernel-Treiber reserviert (z. B. BME280 im IIO-Subsystem). Userspace-Tools können nicht direkt darauf zugreifen.
  • Gesamte Tabelle füllt sich mit Zahlen: Die SDA-Leitung hat einen Kurzschluss gegen Masse.
  • Tabelle bleibt leer oder der Scan läuft extrem zäh: SCL hängt auf Masse oder ist unterbrochen.

Register lesen und schreiben: i2cget & i2cset

Ein 8-Bit-Register auslesen (z. B. Chip-ID aus Register 0xD0 von Sensor 0x76):

# i2cget -y <bus> <device_addr> <reg_addr> [mode]
i2cget -y 1 0x76 0xd0 b
# Rückgabe: 0x60 (bestätigt Bosch BME280)

In ein Konfigurationsregister schreiben:

# Wert 0x27 in Register 0xF4 schreiben
i2cset -y 1 0x76 0xf4 0x27 b

Gesamtes Register-Abbild eines Speichers oder Sensors auslesen:

i2cdump -y 1 0x50

4. Robuste Python-Treiberentwicklung mit smbus2

In Produktionsumgebungen führen einfache Leseaufrufe ohne Fehlerbehandlung unweigerlich zum Absturz, sobald minimale elektromagnetische Störungen ein OSError: [Errno 121] Remote I/O error hervorrufen.

Hier ist eine industrietaugliche Treiberklasse mit automatischem Retry, Verbindungs-Reset und Burst-Read:

#!/usr/bin/env python3
"""
Ausfallsicherer I2C-Client mit automatischem Retry & Error-Handling
Kommuniziert zuverlässig mit einem Bosch BME280 Umgebungssensor.
"""

import time
import sys
import logging
from typing import Optional
from smbus2 import SMBus, i2c_msg

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")

class ResilientI2CDevice:
    def __init__(self, bus_id: int, address: int, max_retries: int = 3):
        self.bus_id = bus_id
        self.address = address
        self.max_retries = max_retries
        self.bus: Optional[SMBus] = None
        self._connect()

    def _connect(self) -> None:
        try:
            if self.bus:
                self.bus.close()
            self.bus = SMBus(self.bus_id)
            logging.info(f"I2C-Bus {self.bus_id} fuer Geraet 0x{self.address:02X} initialisiert.")
        except Exception as err:
            logging.error(f"Fehler beim Oeffnen von Bus {self.bus_id}: {err}")
            raise

    def read_byte_data(self, register: int) -> int:
        """Liest ein einzelnes Byte mit Retry-Schleife bei Fehlern."""
        last_error = None
        for attempt in range(1, self.max_retries + 1):
            try:
                assert self.bus is not None
                return self.bus.read_byte_data(self.address, register)
            except OSError as err:
                last_error = err
                logging.warning(f"Leseversuch {attempt}/{self.max_retries} fehlgeschlagen an 0x{register:02X}: {err}")
                time.sleep(0.02 * attempt)
                self._connect()

        logging.error(f"Dauerhafter I2C-Fehler an Geraet 0x{self.address:02X}")
        raise last_error

    def read_i2c_block(self, start_register: int, length: int) -> list[int]:
        """Fuehrt atomaren Mehrfach-Byte-Burst-Read ueber i2c_msg aus."""
        write_msg = i2c_msg.write(self.address, [start_register])
        read_msg = i2c_msg.read(self.address, length)

        for attempt in range(1, self.max_retries + 1):
            try:
                assert self.bus is not None
                self.bus.i2c_rdwr(write_msg, read_msg)
                return list(read_msg)
            except OSError as err:
                logging.warning(f"Burst-Read Versuch {attempt} fehlgeschlagen: {err}")
                time.sleep(0.05 * attempt)
                self._connect()

        raise RuntimeError(f"Timeout bei Datenblock-Abfrage an 0x{self.address:02X}")

    def close(self) -> None:
        if self.bus:
            self.bus.close()
            self.bus = None

def main() -> None:
    BME280_ADDR = 0x76
    REG_CHIP_ID = 0xD0

    device = ResilientI2CDevice(bus_id=1, address=BME280_ADDR)

    try:
        chip_id = device.read_byte_data(REG_CHIP_ID)
        logging.info(f"Sensor antwortet. Chip-ID = 0x{chip_id:02X}")

        while True:
            # 24 Kalibrierungs-Bytes am Stueck atomar lesen
            calib_data = device.read_i2c_block(0x88, 24)
            logging.info(f"Telemetrie empfangen: {len(calib_data)} Bytes geprueft.")
            time.sleep(2.0)

    except KeyboardInterrupt:
        logging.info("Polling-Schleife beendet.")
    finally:
        device.close()

if __name__ == "__main__":
    main()

5. Adresskonflikte lösen: TCA9548A Multiplexer

Sensoren besitzen häufig feste 7-Bit-Adressen oder maximal zwei wählbare Adressen per Lötjumper (z. B. 0x76 oder 0x77). Wenn eine Industriesteuerung vier identische BME280-Sensoren oder mehrere Displays betreiben soll, wird ein I2C-Multiplexer (TCA9548A) benötigt.

%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    RPI["Raspberry Pi (SDA/SCL)"] -->|"Haupt-I2C-Bus"| MUX["TCA9548A Multiplexer (0x70)"]

    subgraph Channels["Multiplexte virtuelle Busse"]
        direction TB
        S1["Kanal 0: Sensor A (0x76)"]
        S2["Kanal 1: Sensor B (0x76)"]
        S3["Kanal 2: Sensor C (0x76)"]
        S4["Kanal 3: Sensor D (0x76)"]
        S1 --> S2 --> S3 --> S4
    end

    MUX --> Channels

Kernel-Overlay für automatische Bus-Erzeugung

Statt den Multiplexer umständlich im Anwendungscode per Byte-Schreibbefehl umzuschalten, kann der Linux-Kernel angewiesen werden, für jeden Multiplexer-Kanal einen eigenen virtuellen I2C-Bus (/dev/i2c-2 bis /dev/i2c-9) bereitzustellen.

In /boot/firmware/config.txt eintragen:

# TCA9548A an Standardadresse 0x70 auf Bus 1 einbinden
dtoverlay=i2c-mux,pca9548,addr=0x70

Nach einem Neustart die neuen Busse prüfen:

i2cdetect -l

Ausgabe:

i2c-1   i2c         bcm2835 (i2c-bcm2835)               I2C adapter
i2c-2   i2c         i2c-1-mux (chan_id 0)               I2C adapter
i2c-3   i2c         i2c-1-mux (chan_id 1)               I2C adapter
...

Jeder Sensor kann nun vollkommen unabhängig auf seinem eigenen Bus angesprochen werden:

# Sensor an Port 0 abfragen
i2cget -y 2 0x76 0xd0
# Sensor an Port 1 abfragen
i2cget -y 3 0x76 0xd0

6. Bus-Blockaden lösen: Die 9-Takt-Wiederherstellung

Das Problem hängender Slaves (SDA auf LOW gefangen)

Stürzt ein I2C-Sensor mitten in der Übertragung ab, während er gerade ein 0-Bit überträgt, hält sein internes Schieberegister die SDA-Leitung dauerhaft auf Masse (LOW). Da der Raspberry Pi den Takt (SCL) kontrolliert und SDA auf LOW feststeckt, kann kein regulärer STOP-Zustand erzeugt werden. Der gesamte Bus ist lahmgelegt.

Die 9-Takt-Befreiungssequenz

Um den Bus ohne Stromabschaltung zu befreien, taktet der Host die SCL-Leitung 9-mal manuell durch. Dadurch schiebt der blockierte Sensor sein Byte zu Ende und gibt die offene SDA-Leitung wieder frei.

Wiederherstellungs-Skript /usr/local/bin/i2c-bus-recover.sh:

#!/usr/bin/env bash
# I2C-Bus Entstörungs- und Recovery-Skript

set -euo pipefail

CHIP="gpiochip4"  # Auf Pi 4: gpiochip0, auf Pi 5: gpiochip4
SCL_PIN=3
SDA_PIN=2

echo "[*] Entlade Kernel-Treiber..."
echo "fe804000.i2c" > /sys/bus/platform/drivers/i2c-bcm2835/unbind 2>/dev/null || true

echo "[*] Erzeuge 9 Taktimpulse auf SCL..."
for i in {1..9}; do
    gpioset -t 0 "${CHIP}" "${SCL_PIN}=0"
    sleep 0.001
    gpioset -t 0 "${CHIP}" "${SCL_PIN}=1"
    sleep 0.001
done

echo "[*] Erzeuge I2C STOP-Kondition..."
gpioset -t 0 "${CHIP}" "${SDA_PIN}=0"
gpioset -t 0 "${CHIP}" "${SCL_PIN}=1"
gpioset -t 0 "${CHIP}" "${SDA_PIN}=1"

echo "[*] Binde Kernel-Treiber wieder ein..."
echo "fe804000.i2c" > /sys/bus/platform/drivers/i2c-bcm2835/bind 2>/dev/null || true

echo "[+] Bus erfolgreich reaktiviert."

Ausführungsrechte setzen:

sudo chmod +x /usr/local/bin/i2c-bus-recover.sh

Nächste Schritte

Mit stabilem I2C-Wissen geht es weiter zur schnellen SPI-Kommunikation: