Der I2C-Bus


Prof. Jürgen Plate

Der I2C-Bus

Allgemeines

In Embedded-Systemen sind Sensoren und Aktoren oft mit dem I2C-Bus angebunden. Linux auf dem Raspberry Pi unterstützt dies mit einem eigenen Subsystem. I2C (Aussprache: "i-quadrat-c", machmal auch "i-zwei-c")ist ein serieller Master-Slave-Datenbus, der für die Kommunikation über kurze Distanzen konzipiert wurde, also hauptsächlich innerhalb von Platinen oder Geräten. Die Technik stammt aus den frühen 1980er-Jahren und wurde ursprünglich von Philips (heute NXP Semiconductors) für den Einsatz in der Unterhaltungselektronik entwickelt. Die Datenübertragung erfolgt synchron über zwei Leitungen: eine Datenleitung (SDA) und eine Taktleitung (SCL), auf denen ein einfaches Übertragungsprotokoll abläuft (siehe auch Serielle Schnittstelle, USB, SPI, I2C, 1-Wire).

Beide Leitungen werden durch Pullup-Widerstände auf ein positives Potenzial gezogen, die I2C-Chips haben Open-Collector-Ausgänge. Ein Bus-Master gibt Takt und Betriebsmodus vor und initiiert die byteweise erfolgende Kommunikation. Der I2C-Master kann bis zu 112 Geräte (Slaves) mit einer 7-Bit-Adresse ansprechen (eine Erweiterung des Busses verwendet 10 Bit für bis zu 1136 Geräte). Für den Bus existieren auf dem Markt die unterschiedlichsten Devices, darunter Temperatursensoren, Echtzeituhr, Portexpander, A/D- und D/A-Wandler sowie Bausteine der Unterhaltungselektronik.

Eine Abwandlung von I2C ist der SMBus, der System Management Bus. Er ist hardwaretechnisch identisch mit I2C, definiert darauf aber ein anderes Übertragungsprotokoll. Er findet sich auf fast allen modernen PC-Boards, um z. B. die Temperatur der CPU zu messen. Gelegentlich wird der I2C-Bus als Two-Wire-Interface oder TWI bezeichnet.

Die I2C-Devices (Clients) nehmen Daten vom Master entgegen und schicken abhängig vom übertragenen Kommando eine Antwort zurück. Kommandos, Parameter und Antworten sind für jedes Device unterschiedlich - insofern gibt es da auch kein einheitliches Programmierschema. Der Arduino unterstützt den I2C-Bus mit seiner Wire-Bibliothek. Diese Bibliothek ermöglicht einem Arduino, mit Chips zu kommunizieren, die das I2C-Protokoll verwenden. Normalerweise besitzen die I2C-Devices intern zahlreiche Register, auf die entweder geschrieben wird (Konfiguration, Speichern von Werten) oder von denen gelesen wird (Messwerte, Speicherinhalt). Eine Anfrag an ein Device besteht meist aus einer Schreiboperation, be der das gewünschte Register spezifiziert wird, und einer anschließenden Leseoperation für den Registerinhalt.

Man kann nicht nur spezielle I2C-Devices, sondern auch mehrere Arduinos an den I2C-Bus anschließen und darüber untereinander kommunizieren lassen. Jedes am Bus angeschlossene Gerät erhält eine eigene Adresse. Da die Adresse 7 Bit breit ist, können bis zu 112 Geräte an einen I2C-Bus angeschlossen werden (16 der 128 möglichen Adressen sind für spezielle Zwecke reserviert).

Der I2C-Bus benötigt zwei Daten-Leitungen: SCL für das Taktsignal und SDA für das Datensignal. Die früheren Arduinos verwendeten die Leitungen A4 (SDA) und A5 (SCL) für den Busanschluß. Auf neueren Uno-Boards sind diese beiden Leitungen auf separate Anschlüsse der gegenüberliegenden Buchsenleiste herausgeführt. Diese sind also keine "neuen" I2C-Anschlüsse! Deshalb beachten:

SDA und SCL sind lediglich weitergeführte A4- bzw. A5-Anschlüsse! Wenn ein Arduino an einen I2C-Bus angeschlossen ist, stehen die analogen Eingänge A4 und A5 nicht mehr zur Verfügung.

Die anderen Arduino-Board verwenden verschiedene Pins für den I2C-Bus:

BoardI2C/TWI Pins
Uno, EthernetA4 (SDA), A5 (SCL)
Mega256020 (SDA), 21 (SCL)
Leonardo2 (SDA), 3 (SCL)
Due20 (SDA), 21 (SCL), SDA1, SCL1

Die Busleitungen sind mit Pullup-Widerständen gegen +5 V abgeschlossen. Die Taktfrequenz auf dem Bus beträgt standardmäßig 100 kHz, viele Bausteine können aber auch mit 400 kHz betrieben werden.

Wie schon erwähnt, gibt es eine Bubliothek für den Betrieb der Bus-Komponenten. Um diese Bibliothek in eigenem Code einzubinden, wird im Programm die folgende Headerdatei aufgeführt:

#include <Wire.h>

Die Wire-Bibliothek stellt folgende Befehle zur Verfügung:

Dazu einige Anmerkungen.

Beispiele

I2C-Scanner

Manchmal tritt das Problem auf, dass man ein I2C-Gerät hatte, leider aber nicht weiss, welche Adresse es hat. Oder man will überprüfen, ob es einen Fehler in der Verkabelung gibt. Der folgende I2C-Scanner ist ein Arduino-Programm, das alle Adressen scannt und die angeschlossenen I2C-Defices im Serial Monitor anzeigt. Dazu wird die Verbindung mit Wire.beginTransmission() begonnen und mit Wire.endTransmission() gleich wieder beendet. Ließ sich ein Device ansprechen liefert die Funktion Wire.endTransmission() eine Null zurück, andernfalls einen Fehlercode. Das Programm überprüft den Adressbereich von 0x01 bis 0x7F auf ICs zuerst im Normalmodus (100 kHz). Anschließend wird der gleiche Bereich noch mit dem höheren Bustakt von 400 kHz gescannt. Im seriellen Monitor werden die gefundenen I2C-Adressen und die Anzahl der ansprechbaren Bauteile für jeden Modus ausgegeben. Die Funktion loop() bleibt leer, da nur einmal gescannt werden soll.

#include <Wire.h>

void setup() 
  {  
  int count = 0;       /* Anzahl der gefundenen I2C-Geraete */
  
  Serial.begin (9600);
  Wire.begin();
  Serial.println ("I2C-Bus-Scanner");

  Wire.setClock(100000L);
  Serial.println("Scan mit 100 kHz");
  scan();
  Wire.setClock(400000L);
  Serial.println("Scan mit 400 kHz");
  scan();
  }
  
void scan(void)
  {
  int count = 0;
  /* alle in Frage kommenden IDs scannen */
  for (int i = 0; i < 128; i++)
    {
    Wire.beginTransmission(i);
    /* Kommunikation mit Geraet(i) testen */
    if (Wire.endTransmission () == 0)
      { /* gefunden */
      Serial.print ("ID = ");
      Serial.print (" 0x");      
      Serial.println(i, HEX);
      count++;    
      }
    delay (20);
    }
  Serial.print (count);
  Serial.println (" Devices gefunden\n");   
  }
  
void loop()
  {
  // nix
  }

Master-Slave-Kommunikation

Ein Master schaltet per Bus die Leuchtdiode D13 beim Slave, einem zweiten Arduino, um. Dazu sendet der Master einfach ein beliebiges Byte im Sekundentakt an den Slave. Wenn beim Slave ein Byte ankommt dreht er den Status der Leitung um. Der Master hat keine Busadresse. Der Slave braucht dagegen eine Adresse um auf dem Bus identifiziert zu werden. Im Beispiel wird hier 0x55 genommen. Der Master ist recht einfach zu programmieren. In setup() wird der Bus initialisiert und in loop() jeweils die Übertragung angestoßen.

Da der Master nicht parallel an mehrere Slaves senden kann, ist für jeden Slave ein Block aus Wire.beginTransmission(), Interaktion und Wire.endTransmission() nötig. Die tatsächliche Übertragung findet erst mit dem Aufruf von Wire.endTransmission() statt, die Daten werden vorher in einem 32 Byte großen Puffer gespeichert. Über den Rückgabewert von Wire.endTransmission() kann kontrolliert werden, ob der Slave die Daten angenommen hat. Es kann jedoch nicht festgestellt werden, ob alle Daten auch fehlerfrei angekommen sind.

#include <Wire.h>

void setup()
  {
  Wire.begin(); 
  Serial.begin(9600);
  }

void loop()
  {
  boolean OK;

  /* LED beim Slave ausschalten */
  Wire.beginTransmission(0x55); 
  Wire.write(0);             
  OK = Wire.endTransmission();    
  Serial.println("aus");
  delay(1000);
  
  /* LED beim Slave einschalten */
  Wire.beginTransmission(0x55); 
  Wire.write(1);             
  OK = Wire.endTransmission();    
  Serial.println("ein");
  delay(1000);
  }

Beim Slave ist das Programm etwas länger, weil eine der oben aufgeführten ereignisorientierten Methoden verwendet wird. Es muss sich nicht mehr um den Aufruf der Empfangsroutine gekümmert weden, die Empfangsroutine wird aktiviert, sobald Daten vorliegen. Im Setup wird dem Wire.onReceive()-Ereignis eine Callback-Funktion zugewiesen, weiter unten ist die Funktion dann deklariert. Wichtig dabei ist, dass der Datentyp des Übergabeparameters vom Typ int ist. Treffen Daten ein, wird die Callback-Routine receiveEvent() aufgerufen, in der die Daten gelesen und entsprechend reagiert wird.

#include <Wire.h>

void setup()
  {
  pinMode(13,OUTPUT);
  Wire.begin(55); 
  Wire.onReceive(receiveEvent);
  }

void loop()
  {
  //nix 
  }

void receiveEvent(int anzahl)
  {
  byte State;
  
  while(Wire.available())
    { State = Wire.read(); }  
  digitalWrite(13, State);
  }

Auslesen des Temperatur- und Feuchtesensors HDC1008

Der HDC1008 von Texas Instruments ist ein digitaler Luftfeuchtigkeitssensor mit integriertem Temperatursensor, der eine exzellente Messgenauigkeit bei sehr geringer Leistungsaufnahme bietet (wenige Mikroampere). Die Messung der Luftfeuchtigkeit erfolgt mithilfe eines neuartigen kapazitiven Sensors. Die Luftfeuchtigkeits- und Temperatursensoren sind werkseitig kalibriert. Mit ihrem ultrakompakten Gehäuse ermöglicht die innovative WLCSP-Ausführung (Wafer-Level-CSP-Gehäuse) eine einfachere Auslegung. Das Sensorelement des HDC1008 befindet sich im unteren Teil der Komponente, wodurch der HDC1008 besser vor Schmutz, Staub und anderen Umweltschadstoffen geschützt ist. Der HDC1008 ist für Temperaturbereiche von -40 °C bis +125 °C spezifiziert. Er wird über ein I2C-Interface angebunden. Der Sensor kann an 3,3-V- oder 5-V-Systemen genutzt werden.


Aufbau des HDC1008

Bei Watterott oder Adafruit wird ein sogenanntes Breakout-Board angeboten, auf dem der Sensor aufgelötet ist und das den Anschluss über eine Stiftleiste oder dergleichen ermöglicht. Über Jumper kann die 2-Bit-Adresse des Sensors im Bereich von 0x40 bis 0x43 eingestellt werden. Ohne I2C-Multiplexer oder Abschaltung einzelner Sensoren können also bis zu vier Sensoren parallel betrieben werden. Für den Arduino bietet Adafruit eine Bibliothek an, die nur heruntergeladen und installiert werden muss - aber die ist nicht unbedingt nötig, denn der Chip hat nur drei Register.

Die Umrechnung der 14-Bit-Werte des Sensors in Temperatur und Feuchte erfolgt mit den folgenden Gleichungen, wobei das [15:00] nichts weiter bedeutet, als dass es sich um eine 16-Bit-Wert (Bits 15 bis 0) handelt:

Um an die Messwerte zu kommen, werden nur zwei Funktionen benötigt:

In der Programmschleife werden die Temperatur- und Feuchtewerte alle drei Sekunden abgerufen und auf der seriellen Konsole ausgegeben.
#include <Wire.h>

#define HDC1008_I2CADDR       0x43

// HDC1008-Register
#define HDC1008_TEMP          0x00
#define HDC1008_HUMID         0x01
#define HDC1008_CONFIG        0x02

// Konfigurationswort
#define HDC1008_CONFIG_RST    0x9000

float temp;
float humid;
uint16_t data;

void hdc_begin(uint8_t i2caddr, uint16_t config) 
  {
  Wire.begin();
  Wire.beginTransmission(i2caddr);
  Wire.write(config >> 8);
  Wire.write(config & 0xFF);
  Wire.endTransmission();
  delay(15);
  }

uint16_t hdc_read16(uint8_t i2caddr, uint8_t reg) 
  {
  uint16_t res;
  Wire.beginTransmission(i2caddr);
  Wire.write(reg);
  Wire.endTransmission();
  delay(65);
  Wire.requestFrom((uint8_t)i2caddr, (uint8_t)2);
  res = Wire.read();
  res <<= 8;
  res |= Wire.read();
  delay(15);
  return res;
  }


void setup() 
  {
  Serial.begin(9600);
  Serial.println("TI HDC1008 Sensor");
  hdc_begin(HDC1008_I2CADDR, HDC1008_CONFIG_RST);
  }

void loop()
  {
  data = hdc_read16(HDC1008_I2CADDR, HDC1008_TEMP);
  temp = ((float)data/65536.0)*165.0 - 40.0;
  data = hdc_read16(HDC1008_I2CADDR, HDC1008_HUMID);
  humid = ((float)data/65536)*100;
  Serial.print("Temperatur[°C]: ");
  Serial.println(temp);
  Serial.print("Luftfeuchte[%]: ");
  Serial.println(humid); 
  delay(3000);
  }

Weitere Beispiele finden sich unter den Links am Ende der Seite, z. B. die Ansteuerung eines Echtzeit-Uhrenchips und eines LCD.

Links

Copyright © Hochschule München, FK 04, Prof. Jürgen Plate
Letzte Aktualisierung: , Webseite: