Serielle Kommunikation mit mehrern Geräten mit I²C

Warum serielle Kommunikation manchmal nicht ausreicht1)
Serielle Kommunikation mit Serial ist asynchron, es wird also kein Takt übertragen. Dementsprechend müssen sich kommunizierende Geräte vor Beginn der Kommunikation auf eine Übertragungsgeschwindigkeit einigen. Außerdem müssen beide Geräte Taktgeber haben, die ähnlich geringe Abweichungen voneinander haben - zu große Abweichungen sorgen für Fehler bei der Datenübertragung.

Außerdem können mit serieller Kommunikation nur genau zwei Geräte miteinander kommunizieren. Möchte man mit mehreren Geräten kommunizieren, benötigt man ein anderes System, z.B. I²C.

Das Inter-Integrated Circuit (I²C) Protokolll ist dazu gedacht, mehrere Geräte über einen so genannten Bus zu verbinden. Hierbei wird zwischen Peripheriegeräten (Slaves) und Controllern (Master) unterschieden. Außerdem benötigt I²C genau wie serielle Kommunikation nur zwei Kabel, um Informationen auszutauschen. Dabei benutzt I²C ein bestimmtes Protokoll, in dem jeder Slave eine fixe Adresse (eine Zahl zwischen 1 und 127, mehr dazu) hat. Der Beginn der Kommunikation geht hierbei immer vom Master aus:

  • Das Senden von Daten von einem Master an einen bestimmten Slave (Mehr dazu)
  • Das Abfragen von Daten vom Master von einem bestimmten Slave (Mehr dazu)

Mehr zum Protokoll kann hier nachgeschlagen werden.

Genutzt wird das I²C-Protokoll hauptsächlich von OLED-Displays und von digitalen Sensoren (z.B. Potentiometer, Temperatur-, Luftdruck- und Luftfeuchtigkeitssensoren).

Aufgabe 1: Vorbereitung

Unser Ziel ist es, eine Potentiometer-Eingabe von einem Arduino (Reader-Slave) abzufragen und diese an einen anderen Arduino (Blinker-Slave) zu senden, der die OnBoard-LED mit entsprechender Verzögerung blinken lässt. Das ganze soll durch einen dritten Arduino (Master) koordiniert werden. Dazu muss etwas an Vorarbeit geleistet werden.

a) Erstellt die Schaltung unten in TinkerCad unter dem Namen Eingebettete Systeme - 09 - I2C.

b) Programmiert zunächst den Blinker so, dass er die OnBoard-LED auf Pin 13 ein mal pro Sekunde blinkt.

c) Programmiert zunächst den Reader so, dass er das Potentiometer auf Pin A3 ausliest und den Wert auf der Serial-Konsole ausgibt.

Material & Verkabelung

Materialliste

  • 3x Arduino Uno
    • linker Arduino: Master
    • rechter oberer Arduino: Slave 2 (Reader)
    • rechter unterer Arduino: Slave 1 (Blinker)
  • 1x Breadboard
  • 1x Potentiometer

Verkabelung (Siehe Schaltplan rechts)

  • Schwarz: GND auf allen Arduinos verbinden
  • I²C-Verbindung herstellen:
    • Blau: SDA (Serial Data) → A4 auf allen Arduinos verbinden
    • Gelb: SCL (Serial Clock) → A5 auf allen Arduinos verbinden
  • auf dem Reader Potentiometer anschließen
    • Schwarz: Linker Pin zu GND
    • Rot: Rechter Pin zu +5V
    • Grün: Mittlerer Pin zu A3
Schaltplan
Video - Erwartetes Verhalten
Tipps zur Programmierung

In einigen komischen Fällen im TinkerCad-Simulator kann es vorkommen, dass das Potentiometer negative statt positive Werte liefert. Um das abzufangen, bestimmen wir den Betrag des Messwerts mit der abs-Funktion (engl. "absolute" → "Betrag"):

messwert = abs(analogRead(POT_PIN));
Aufgabe 2: Verbindung der Arduinos mit I²C

Programmiert nun Master, Reader und Blinker so, dass:

  1. Der Reader vom Master nach dem Potentiometer-Wert abgefragt werden kann
    • Verwendet hierzu onRequest, siehe unten.
    • Denkt an die Kodierung auf ein einzelnes Byte, siehe unten!
  2. Der Blinker vom Master die Blink-Wartezeit gesendet bekommen kann
    • Verwendet hierzu onReceive, siehe unten.
    • Denkt an die Kodierung von 0-255 auf 100-1000, 100ms bis 1 Sekunde Wartezeit, siehe unten!
  3. Der Master immer wieder einen Wert vom Reader ausliest und diesen an den Blinker sendet.
    • Verwendet hierzu requestFrom und beginTransmission() / endTransmission(), siehe unten.
Hinweise zur Programmierung
  • Wir kodieren einen einzelnen Potentiometer-Wert als ein einziges Byte, obwohl das Potentiometer einen Wert zwischen 0 und 1024 zurückliefert (dafür werden 2 Bytes benötigt). Grund hierfür ist, dass das Übertragen mit I²C dann leichter wird, da wir nur einzelne Bytes übertragen müssen. Die "Übersetzung" von Werten übernimmt die map-Funktion.
byteWert = map(messWert, 0, 1023, 0, 255); // Für den Reader interessant
warteIntervall = map(byteWert, 0, 255, 100, 1000); // Für den Blinker interessant
  • Auf der Arduino-Plattform ist die Wire-Bibliothek für die Kommunikation mit I²C zuständig.
    • Ähnlich zu Serial muss die Kommunikation mit Wire.begin() (für den Master) bzw. Wire.begin(adresse) (für einen Slave mit einer bestimmten Adresse) in der setup()-Funktion initiiert werden.
    • Der Master kann mit Wire.beginTransmission(address, num) damit beginnen, num Bits an Daten an den Slave mit der angegebene addresse zu schicken. Nach Abschluss der Übertragung muss mit Wire.endTransmission() die Übertragung beendet werden. Das Senden eines einzelnen Bytes übernimmt die Wire.write(data)-Funktion. Der Code unten sendet die beiden Bytes 255 und 0 nacheinander vom Master an den Slave mit der Adresse 123.
void setup() {
  Wire.begin();
}
 
void loop() {
  Wire.beginTransmission(123, 2);
  Wire.write(255);
  Wire.write(0);
  Wire.endTransmission();
}
  • Wenn der Master num Bytes an Daten von einem Slave mit der Adresse adress abfragen will, muss dieses er mit Wire.requestFrom(adress, num) initiieren. Ähnlich wie bei Serial kann dann mit Wire.available() abgefragt werden, ob noch ein Byte zum Lesen da ist. Dieses kann mit int byte = Wire.read() eingelesen werden. Der Code unten liest auf dem Master ein Byte vom Slave mit der Adresse 123 aus.
void setup() {
  Wire.begin();
}
 
void loop() {
  Wire.requestFrom(123, 1);
  if (Wire.available()) {
    int byte = Wire.read();
  }
}
  • Slaves können mit Wire.onReceive(receive) eine Methode definieren, die das Empfangen von Daten abhandelt, und mit Wire.onRequest(request) eine, die das Abfragen von Daten abhandelt. Der Code unten liest die vom Master gesendeten Daten in receive() aus und sendet die vom Master abgefragten Daten in request() zurück.
void setup() {
  Wire.begin(123);
  Wire.onReceive(receive);
  Wire.onRequest(request);
}
 
void receive(int num) {
  while (Wire.available()) {
    int byte = Wire.read();
  }
}
 
void request() {
  Wire.write(data);
}
Aufgabe 3: Eigene Ideen

Erweitert die Schaltung, indem ihr auf TinkerCad eine Kopie unter Eingebettete Systeme - 10 - I2C (eigene Schaltung) anlegt.

Ideen, die ihr umsetzen könntet:

  • Statt des Potentiometers könnte auf dem Reader ein Button abgefragt werden, der die LED auf dem Blinker kontrolliert.
  • Es könnte mehrere Blinker geben.
  • … Denkt euch was aus!
Weitere Informationen