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:
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:
Prüfen, ob die Gerätedatei /dev/i2c-1 vorhanden ist:
3. Hardware-Diagnose mit i2c-tools
Das Werkzeugpaket i2c-tools ist das wichtigste Hilfsmittel zur Fehlersuche auf Linux-Systemen.
Installation:
Bus-Scan mit i2cdetect
Mit i2cdetect werden alle 7-Bit-Adressen (von 0x03 bis 0x77) auf Bus 1 gescannt:
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:
Gesamtes Register-Abbild eines Speichers oder Sensors auslesen:
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:
Nach einem Neustart die neuen Busse prüfen:
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:
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:
Nächste Schritte
Mit stabilem I2C-Wissen geht es weiter zur schnellen SPI-Kommunikation:
- SPI-Protokoll & Hochgeschwindigkeits-Peripherie: Megahertz-Streaming für Analog-Digital-Wandler und Displays.
- UART-Seriell & Industrielles RS-485: Störsichere Datenübertragung über lange Strecken in Fabriken.