Arduino und das I2C/TWI-Protokoll

I2C/TWI ist ein serielles Kommunikationsprotokoll mit niedriger Geschwindigkeit. Ein I2C-Netzwerk besteht aus einem oder mehreren Master-Geräten und einem oder mehreren Slave-Geräten. Das I2C-Protokoll wird nicht für die Datenübertragung über große Entfernungen empfohlen.

Der I2C/TWI-Bus besteht aus zwei Drähten und den dazugehörigen Pull-up-Widerständen. Alle I2C-Geräte werden an dieses Adernpaar angeschlossen. Die Namen der Drähte sind SDA und SCL.

Diese können zum Lernen notwendig sein:

Der Wert der Pull-up-Widerstände hängt von der Spannung des I2C-Busses, seiner Länge und der Anzahl der Geräte am Bus ab. Der Wert des Widerstands sollte zwischen 1K und 10K liegen. Eine bewährte Faustregel, wenn verwenden wir 4,7k Widerstände. Wenn der I2C-Bus lang ist oder mehr Geräte an den Bus angeschlossen sind oder die Busspannung niedriger ist, können wir den Wert der Pull-up-Widerstände reduzieren.

Über die SDA-Datenleitung werden aktuelle Daten hin- und hergeschickt. Die SCL-Leitung überträgt das Taktsignal, das für die Kommunikationszeit-steuerung verwendet wird. Das Taktsignal wird immer vom aktuellen Master generiert.

arduino I2C-Bus

I2C-Geräte können mit unterschiedlichen Spannungspegeln betrieben werden. I2C-Geräte, die mit 3,3 V betrieben werden, können beschädigt werden, wenn sie an den Arduino angeschlossen werden. In einem solchen Fall muss eine Pegelwandler Schaltung verwendet werden.

Arduino BME280 i2c-Bus mit Pegelwandler Schaltung

Master-Geräte können Daten senden und empfangen. Die Slave-Geräte antworten auf die Anfrage des Masters. Beim Senden von Daten auf dem I2C-Bus kann immer nur ein Gerät Daten senden. Jedes Slave-Gerät muss eine eindeutige I2C-Adresse haben, die es auf dem I2C-Bus identifiziert. Eine I2C-Adresse ermöglicht es dem Master, Daten auf dem Bus an ein bestimmtes Slave-Gerät zu senden.

In der folgenden Tabelle können wir sehen, wo sich die I2C/TWI-Pins auf jedem Arduino-Board befinden.

TafelI2C/TWI-Pins
UNO, NanoA4 (SDA), A5 (SCL)
Mega256020 (SDA), 21 (SCL)
Leonardo2 (SDA), 3 (SCL)

Hervorragende Werkzeuge zum Üben:

AZDelivery-ATmega328 Mikrocontroller Board mit USB-Kabel
AZDelivery-ATmega328 Mikrocontroller Board mit USB-Kabel
Werbung

Um den I2C-Bus zu verwenden, benötigen wir die Wire-Bibliothek, diese muss am Anfang des Arduino-Sketch hinzugefügt werden, wie unten gezeigt.

#include <Wire.h>

Die Wire-Bibliothek enthält die folgenden Funktionen. Nehmen wir sie einzeln.

Die Funktion Wire.begin() führt die Wire-Bibliothek ein und startet die Kommunikation. Geräte werden als Controller (Master) oder Peripheriegeräte (Slave) an den I2C-Bus angeschlossen. Jedes Slave-Gerät muss eine 7-Bit-Adresse haben, die vom Master auf dem I2C-Bus identifiziert wird. Der optionale Parameter ist die Adresse des Slave-Geräts. Wird dieser Parameter nicht angegeben, wird das Gerät als Master an den Bus angeschlossen. Optional kann das Mastergerät auch eine Adresse haben.

#include "Wire.h"

void setup()
{
  Wire.begin(); // Wenn kein Parameter ist, das Gerät als Master angeschlossen
}

void loop()
{
  
}
Wire.begin(0x90);   // I2C address

Zurück zum Inhaltsverzeichnis.

Wire.end() stoppt die I2C-Kommunikation und deaktiviert die Wire-Bibliothek. Wenn wir die Wire-Bibliothek erneut verwenden möchten, müssen wir die Funktion Wire.begin() erneut aufrufen. Die Wire.end()-Funktion war in der ursprünglichen Version der Wire-Bibliothek nicht verfügbar und funktioniert möglicherweise nicht in einigen Versionen.

Zurück zum Inhaltsverzeichnis.

Die Funktion Wire.beginTransmission() startet den Schreibvorgang. Es hat einen wichtigen Parameter, die Adresse des Slave-Geräts, mit dem der Master auf dem I2C-Bus sprechen möchte. Bevor der Master schreibt, senden wir die Adresse des Slave-Geräts mit Wire.beginTransmission(). Wenn das Slave-Gerät auf dem I2C-Bus vorhanden ist, kann das Schreiben beginnen. Auf Wire.beginTransmission() folgt Wire.write() und dann Wire.endTransmission() .

Wire.beginTransmission(address);

Zurück zum Inhaltsverzeichnis.

Die Funktion Wire.write() schreibt Daten auf den I2C-Bus. Schreibt Daten von einem Slave-Gerät auf Anforderung des Master-Geräts, oder stellt Bytes zur Übertragung auf dem Master in die Warteschlange, um sie an das Slave-Gerät zu senden. Die Funktion Wire.write() kann einen oder zwei Parameter haben.

Wire.write(value);
Wire.write(string);
Wire.write(data, length);

Der Wert des value-parameter ist ein einzelnes Byte. Der string-parameter ist eine Zeichenkette. Der data-parameter ist das Array der zu sendenden Bytes, der Längenparameter ist die Anzahl der Bytes im Datenarray.

#include <Wire.h>

byte mybyte = 0;

char mystring[] = "mystring";
char mychar[] = {'a','b','c'};

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

void loop()
{
  Wire.beginTransmission(6);
  Wire.write(mybyte);

  Wire.write(mystring);
  Wire.write(mychar, sizeof(mychar));  
  Wire.endTransmission();

  delay(1000);
}

Bevor der Master auf das Slave-Gerät schreibt, starten wir die Kommunikation mit Wire.beginTransmission(). Die Funktion Wire.write() speichert/reiht die zu sendenden Datenbytes in einen Puffer ein. Die Größe des Puffers beträgt 32 Bytes. Wenn die zu sendenden Daten nicht in den Puffer passen, führt dies zu einem Übertragungsfehler.

Auf dem Master-Gerät sendet Wire.endTransmission()  den Inhalt des Puffers und stoppt dann den Schreibvorgang.

Damit das Slave-Gerät senden kann, muss es auf eine Anfrage des Masters warten. Die Wire-Bibliothek enthält eine Funktion für diesen Zweck, Wire.onRequest() .

Zurück zum Inhaltsverzeichnis.

Wire.endTransmission() auf dem Master-Gerät sendet den Inhalt des Puffers, der von der Funktion Wire.write() gespeichert wurde, und stoppt dann den Schreibvorgang.

Wire.endTransmission();

Die Funktion Wire.endTransmission() kann einen optionalen Parameter haben.

Wire.endTransmission(bool stop);

Wenn der Parameter true ist , sendet Wire.endTransmission() nach der Übertragung eine Stop-Nachricht und gibt den I2C-Bus frei. true ist der Standardwert.

Wenn der Parameter false ist , sendet Wire.endTransmission() nach der Übertragung eine Neustartnachricht, sodass der I2C-Bus beschäftigt bleibt. Dadurch kann das aktuelle Master-Gerät zusätzliche Nachrichten senden und verhindert, dass ein anderer Master den I2C-Bus übernimmt.

Der Rückgabewert der Funktion Wire.endTransmission() kann beispielsweise beim Debuggen hilfreich sein. Diese Werte können sein:

Wenn der Rückgabewert „0“ ist, bedeutet dies, dass die Übertragung erfolgreich war. „1“ zeigt an, dass die Daten zu lang sind, um in den Übertragungspuffer zu passen.

Der Wert „2“ ist das empfangene NACK, wenn die Slave-Adresse übertragen wird. Nach jedem gesendeten Byte wird ein Quittungsbit gesendet. Dieses AKC/NACK-Bit wird vom Slave-Gerät verwendet, um anzuzeigen, ob es das vorherige Byte erfolgreich empfangen hat. Wenn der Master die Adresse des Slaves sendet, mit dem er kommunizieren möchte, und ein Slave, der seine Adresse erkennt, als Antwort ein ACK sendet. Dadurch wird dem Master mitgeteilt, dass sich der zu erreichende Slave tatsächlich im Bus befindet.
Wenn kein Slave-Gerät die Adresse erkennt, das Ergebnis NACK wird. In diesem Fall unterbricht der Master die Kommunikation, weil niemand zum Reden da ist.

Wenn der Rückgabewert „3“ ist, zeigt das empfangene NACK einen Datenübertragungsfehler an. Wenn das Slave-Gerät das vorherige Byte erfolgreich empfangen hat, dann sendet er ein ACK. Ein Wert von „4“ bedeutet einen sonstiges Fehler. Bei Timeout wird der Wert „5“ empfangen.

byte error = Wire.endTransmission();
switch (error) 
{
  case 1:
    Serial.println("Success");
    break;
  case 2:
    Serial.println("Data too long");
    break;
  case 3:
    Serial.println("NACK on transmit of address");
    break;
  case 4:
    Serial.println("NACK on transmit of data");
    break;
  case 5:
    Serial.println("Timeout");
    break;

}

Zurück zum Inhaltsverzeichnis.

Die Funktion Wire.requestFrom() wird vom Master-Gerät verwendet, um Bytes von einem Slave-Gerät anzufordern. Die Bytes können dann mit den Funktionen Wire.available() und Wire.read() abgerufen werden . Die Funktion Wire.requestFrom() hat mindestens zwei Parameter.

Wire.requestFrom(address, quantity);

Der erste Parameter ist address , die 7-Bit-Adresse des Slave-Geräts, von dem das Master-Gerät Bytes anfordert. Der zweite Parameter ist Quantity , der die Anzahl der Bytes angibt, die angefordert werden können.

Optional können wir einen dritten Parameter angeben.

Wire.requestFrom(address, quantity, stop);

Der dritte Parameter ist stop . Sein Wert kann true oder false sein . Der Standardwert ist true. Wenn der Wert des Stop-Parameters true ist, sendet requestFrom() nach der Anfrage eine Stop-Nachricht und gibt den I2C-Bus frei.

Wenn der stop-parameter von requestFrom() falsch ist, wird nach der Anfrage eine Neustartnachricht gesendet und der I2C-Bus wird nicht freigegeben, sodass die Verbindung aktiv bleibt. Dadurch wird verhindert, dass ein anderes Master-Gerät eine Kommunikation einleitet, bis der aktuelle Master die Anfrage abgeschlossen hat.

Zurück zum Inhaltsverzeichnis.

Die Wire.read()- Funktion liest das vom Master-Gerät angeforderte Byte von einem Peripheriegerät unter Verwendung der Wire.requestFrom()-Funktion.

char mychar = Wire.read();

Zurück zum Inhaltsverzeichnis.

Die Wire.available()-Funktion gibt die Anzahl der Bytes zurück, die von der Wire.read()-Funktion abgerufen werden können. Die Funktion Wire.available() auf dem Master-gerät nach der Funktion Wire.requestFrom() …

#include <Wire.h>

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

void loop()
{
  Wire.requestFrom(0x90, 5);
  while (Wire.available())
  {
    char mychar = Wire.read();
    Serial.print(mychar);
  }
  delay(1000);
}

… oder es muss innerhalb der von Wire.onReceive() auf dem Slave-Gerät registrierten Event-Handler-Funktion aufgerufen werden.

Zurück zum Inhaltsverzeichnis.

Die Wire.onReceive()-Funktion registriert eine Event-Handler-Funktion, die aufgerufen wird, wenn ein Slave-Gerät eine Anfrage von einem Master-Gerät empfängt. Die Funktion Wire.onReceive() hat einen Parameter.

Wire.onReceive(handler);

Der handler-Parameter ist die Event-Handler-Funktion, die aufgerufen werden muss, wenn das Slave-Gerät Daten empfängt. In diesem Eventhandler wird die Funktion Wire.available() aufgerufen .


#include <Wire.h>

void setup()
{
  Wire.begin(0x90);
  Wire.onReceive(myHandler);
  Serial.begin(9600);
}

void loop() 
{
  delay(1000);
}

void myHandler(readbytes)
{
  while (Wire.available())
  {
    char mychar = Wire.read();
    Serial.print(mychar);
  }
}

Zurück zum Inhaltsverzeichnis.

Die Funktion Wire.onRequest() registriert eine Event-Handler-Funktion auf dem Slave-Gerät, dieser Event-Handler wird ausgeführt, wenn das Master-Gerät Daten anfordert.

#include <Wire.h>

void setup()
{
  Wire.begin(0x90);
  Wire.onRequest(myRequestHandler);
}

void loop()
{
  delay(1000);
}

void myRequestHandler()
{
  Wire.write("from slave");
}

Zurück zum Inhaltsverzeichnis.

Die Funktion Wire.setClock() ändert die Taktfrequenz der I2C-Kommunikation. Die Funktion Wire.setClock() hat einen Parameter, clockFrequency . Dieser Parameter ist der Wert des gewünschten Kommunikationstaktes (in Hertz).

Wire.setClock(clockFrequency);

Die akzeptierten Werte sind: 100000 Hz (normaler Modus) und 400000 Hz (schneller Modus). Einige Geräte unterstützen auch 10000 Hz (Low-Speed-Modus), 1000000 Hz (Fast-Modus plus) und 3400000 Hz (High-Speed-Modus).

Zurück zum Inhaltsverzeichnis.

Die Funktion Wire.setWireTimeout() setzt das Kommunikations-Timeout im Master-Modus. Die Funktion Wire.setWireTimeout() setzt das Standard-Timeout ohne Parameter.

Wire.setWireTimeout();

Wir können der Funktion Wire.setWireTimeout() zwei Parameter geben.

Wire.setWireTimeout(timeout, reset_on_timeout);

Der Parameter „ timeout “ ist der Timeout-Wert in Mikrosekunden, wenn er Null ist, ist die Timeout-Prüfung deaktiviert. Wenn der Parameter „ reset_on_timeout “ true ist, wird die I2C-Hardware bei Zeitüberschreitung automatisch zurückgesetzt und die Kommunikation auf dem I2C-Bus neu gestartet.

Wenn auf dem I2C-Bus ein Fehler auftritt, können während der I2C-Kommunikation Einfrierungen und andere Anomalien auftreten. Diese Funktion kann beim Testen des I2C-Busses nützlich sein.

Zurück zum Inhaltsverzeichnis.

Tritt der Timeout auf, wird ein Flag gesetzt, das mit der Funktion Wire.getWireTimeoutFlag() abgefragt werden kann . Das Flag kann manuell mit der Funktion Wire.clearWireTimeoutFlag() gelöscht werden .

Das Flag wird auch gelöscht, wenn die Funktion Wire.setWireTimeout() aufgerufen wird.

bool myFlag = Wire.getWireTimeoutFlag();

Zurück zum Inhaltsverzeichnis.

Ein Aufruf von Wire.clearWireTimeoutFlag() löscht das Timeout-Flag und gibt den aktuellen Wert des Flags zurück.

bool myFlag = Wire.clearWireTimeoutFlag();

Zurück zum Inhaltsverzeichnis.

Sehen wir uns ein einfaches Beispiel für die Verwendung des I2C-Busses an. Verbinden wir ein PCF8591 ADC/DAC-Modul mit dem Arduino-Board. Die Auflösung des ADC und des DAC beträgt 8 Bit, der Wert kann 0-255 betragen.

Das Modul im Beispiel unten hat einen LDR-Lichtsensor an Bord, dies ist ADC0. Kanal; ein Thermistor, ADC1. Kanal; und ein Potentiometer, ADC3. Kanal; um die Eingänge zu testen. Der ADC2-Kanal enthält kein Peripheriegerät, er wird nur an den Header ausgegeben. An den analogen Ausgang wurde eine LED angeschlossen. Eingänge und Ausgänge können für andere Zwecke verwendet werden. Das Modul enthält zwei 10K-Pull-up-Widerstände für den I2C-Bus.

PCF8591 ADC/DAC-Modul mit dem Arduino-Board

Der folgende Beispielcode zeigt das Testen des DAC-Kanals. Wir können das Ergebnis in der LED auf der Platine PCF8591 sehen.

#include "Wire.h"


const char PCF8591 (0x48);
 
void setup()
{
  Wire.begin();
}
 
void loop()
{  
  for(int i=0; i<256; i++) 
  { 
    Wire.beginTransmission(PCF8591);
    Wire.write(0x40);             // Steuerbyte - DAC starten 
    Wire.write(i);                // An DAC zu sendender Wert
    Wire.endTransmission();
    delay(15);
  }
  
  for(int i=255; i>=0; --i) 
  { 
    Wire.beginTransmission(PCF8591);
    Wire.write(0x40);
    Wire.write(i);
    Wire.endTransmission(); 
    delay(15);
  } 
}

Mit dem zweiten Beispiel können wir die Eingaben testen. Laden Sie es auf das Arduino UNO hoch und öffnen Sie dann den seriellen Monitor …

#include "Wire.h"


const char PCF8591 (0x48);
byte adcvalue0, adcvalue1, adcvalue2, adcvalue3;
 
void setup()
{
  Wire.begin();
  Serial.begin(9600);
}
 
void loop()
{
  Wire.beginTransmission(PCF8591);
  Wire.write(0x04);
  Wire.endTransmission();
  Wire.requestFrom(PCF8591, 5);   // 5-Byte-Datenanforderung von PCF8591
 
  adcvalue0=Wire.read();          // vorherige Messung - veraltete Daten.
  adcvalue0=Wire.read();          // die zweite Datenablesung - frische Messung
  adcvalue1=Wire.read();
  adcvalue2=Wire.read();
  adcvalue3=Wire.read();
 
  Serial.print(adcvalue0);        // LDR
  Serial.print(", ");
  Serial.print(adcvalue1);        // Thermistor
  Serial.print(", ");
  Serial.print(adcvalue2);        // leer
  Serial.print(", ");
  Serial.print(adcvalue3);        // Potentiometer
  Serial.println();
 
  delay(500);
}