3.7 I2C Schnittstelle

[MezData: Links-I2C] [Portexpander HLF 8574] [Temperatursensor LM75A] [Druck- und Temperatursensor BMP280] [MezData: Discovery-Room][stm32-I2C-Lib][Elektrische Gedanken]

Einlesen in die Theorie der Schnittstelle

  1. Mit 2 Leitungen viele Komponenten anschließen. Lesen: [Wikipedia: I2C]
  2. Wozu und wer hat I2C-Schnittstelle “erfunden”?
  3. Wie lauten die Bezeichnungen der Bus-Leitungen und wofür stehen die Abkürzungen?
  4. Zeichnen Sie ein Blockschaltbild mit einem Controller und zwei Targets.
  5. Erklären Sie OpenCollector- bzw. OpenDrain-Ausgänge und warum PullUp-Widerstände nötig sind.
  6. Was ist ein “Wired-AND”?
  7. Bei welchen Zustand von SCL werden die Daten von SDA übernommen?
  8. Arduino-Wire-Operationen

Mit Portexpander HLF8574T zwei LED über I2C im Wechsel leuchten lassen

Die LED-Anoden sind VDD (5V) verbunden. P1 und P0 des PCF8574-Moduls sind über 330Ω Widerstände an die Kathoden angeschlossen. Somit leuchten die LED, wenn an den P-Ausgängen eine 0 anliegt. Hintergrund: Die Ausgänge des HLF8574T liefern nur kurz bei einem Schreibvorgang höhere Ströme und sonst nur 100µA bei log. 1 (siehe Datenblatt HLF8574T). An den Bus können mehrere Targets angeschlossen werden. Damit sie unterscheidbar sind bekommt jedes Target eine eigene Adresse [RN-Wissen: I2C-Adressierung]. Bei unserem Modul sind die Bit A6..A3 fest (0b0100000 = 0x20). Mit den Jumpern (die blauen Dinger unter A2) können die Bit A2..0 von uns eingestellt werden. Somit ist für uns die Targetadresse von 0b0100000 = 0x20 bis 0b0100111 = 0x27 wählbar. Zunächst sei sie 0x20.

Versuchsaufbau
Versuchsaufbau mit HLF8574T
#include <Wire.h> // I2C-Library
#define ADRESSE 0x20 // Adresse des Bausteins im I2C-Bus, siehe Vorgabe des Herstellers

void setup() {
  Wire.begin();  //Alternatives PinMapping: Wire.begin(SDA, SCL);
}

void loop() {
  Wire.beginTransmission(ADRESSE); // beginnt mit dem Slave-Gerät zu sprechen
  Wire.write (0xfe); // 0b1111 1110 übertragen: Rote LED soll leuchten
  Wire.endTransmission(); // hört auf, mit dem Gerät zu sprechen
  delay(500);
  Wire.beginTransmission(ADRESSE); // beginnt mit dem Slave-Gerät zu sprechen
  Wire.write (0xfd); // 0b1111 1101 übertragen: Grüne LED soll leuchten
  Wire.endTransmission(); // hört auf, mit dem Gerät zu sprechen
  delay(500);
}

Bei der Übertragung mit Oszilloskop zuschauen

I2C Oszillogramm rote LED leuchtet
I2C Oszillogramm rote LED leuchtet

Die Geschichte zu dem Bild: SCL und SDA sind high. Ein Master will senden und zieht SDA auf GND, dies ist die Startbedingung. Um A6 zu übertragen zieht der Master SCL auf GND und legt den Wert von A6 auf SDA. Dann lässt er SCL los und dessen Pegel geht durch den PullUp-Widerstand auf high, jetzt wissen die Slaves, dass das Datenbit auf SDA gültig ist und übernehmen A6. Das Spiel wiederholt sich bis A0 und R/!W-Bit. Wenn bei einem Slave die gewünschte Adresse mit der eingestellen Adresse übereinstimmt zieht er die SDA-Leitung auf GND und gibt sein Ack=OK: Ich bin bereit für Daten. Der Master sendet nun 8 Bit Daten, der Slave quittiert dies mit weiteren Ack=OK Antworten. Hier wurde nur ein Byte zum Slave übertragen. Am Ende lässt der Master SCL und dann SDA los, damit zeigt er das Ende der Übertragung an.
Im nächsten Bild wird die Übertragung für grüne LED leuchten lassen gezeigt.

I2C Oszillogramm grüne LED leuchtet
I2C Oszillogramm grüne LED leuchtet

Was passiert, wenn kein Slave antwortet? Im Programm die Adresse von 0x20 auf 0x21 geändert. Kein Modul fühlt sich nun angesprochen und gibt sein Ack=OK:

I2C Oszillogramm keiner fühlt sich angesprochen
I2C Oszillogramm keiner fühlt sich angesprochen

Wird nun am Modul A0 auf + gejumpert fühlt es sich wieder angesprochen:

Modul mit Adresse 0x21 fühlt sich angesprochen
Modul mit Adresse 0x21 fühlt sich angesprochen

I2C-LCD ansteuern

Auf unserem Multifunction-Board befindet sich ein LCD-Modul, das über I2C angesprochen wird, leider an PA11 und PA12, die kein I2C können, daher müssen Drahtbrücken gesetzt werden.

Wenn man das LCD-Modul umdreht findet sich ein Bekannter, der Portexpander HLF8574T. Was macht der da? Tatsächlich sind die meisten preiswerten LCD-Module mit dem Chip HD44780 ausgestattet und der braucht mindestens 6 Leitungen. Daher wird zum sparen gerne ein I2C-Port-Expander verwendet [BLIT2008-Board-LCD]. Beim LCD-Modul gibt es keine Jumper sondern Lötbrücken gegen GND. Sind sie alle offen ist die Target-Adresse 0x27.

Es gibt einen Unterschied bei der Festadresse zwischen HLF8574T und HLF8574A:
HLF8574T: 0x20 HLF8574A: 0x38

Um auf dem LCD was auszugeben gibt es praktische Librarys:

#include <Wire.h> // Wire Bibliothek einbinden
#include <LiquidCrystal_PCF8574.h>
LiquidCrystal_PCF8574 lcd (0x27); // LCD-Adresse auf 0x27
void setup() {
   lcd.begin(16, 2); // initialize the lcd
   lcd.clear();
   lcd.setBacklight(255);
   lcd.setCursor(0,0); // erstes Zeichen, erste Zeile
   lcd.print("MezMedia.de");
}

void loop() { 
  
}
I2C-LCD

Den I2C-Bus abscannen um alle Targets zu finden

Oft gibt es das Problem, dass man ein Target verbindet, es aber scheinbar nicht antwortet. Hier ein kurzes Programm, dass alle Adressen abscannt und alle Targets auflistet. Modifizierter Code von dieser Seite: [i2c_scanner]

#include <Wire.h>
void setup(){
  Wire.begin();
  Serial.begin(9600);
  Serial.println("\nI2C Scanner");
}
void loop(){
  byte error, address;
  int nDevices;
  Serial.println("Scanning...");
  nDevices = 0;
  for(address = 1; address < 127; address++ ){
    Wire.beginTransmission(address); // Target anfunken
    error = Wire.endTransmission();  // hat es geantwortet?
    if (error == 0){ // kein Fehler also Antwort
      Serial.print("I2C Target gefunden bei Adresse 0x");
      if (address<16) Serial.print("0");
      Serial.println(address,HEX);
      nDevices++;
    }
    else if (error==4){ // Fehler
      Serial.print("Unbekannter Fehler bei Adresse 0x");
      if (address<16) Serial.print("0");
      Serial.println(address,HEX);
    }    
  }
  if (nDevices == 0) Serial.println("Keine I2C Targets gefunden\n");
  else Serial.println("fertig\n");
  delay(5000);
}

Temperatur mit LM75A messen und auf LCD ausgeben

Nun habe ich die Formelsammlung gelesen und darin den LM75 entdeckt, dabei soll ein Byte übertragen und als Antwort zwei Byte empfangen werden. Also so ein Modul, allerdings mit einen LM75A bestellt: [amazon]

Angeschossen, den Scanner angeworfen und unter Adresse 0x48 antwortet es.
Allerdings wenn man auf der Unterseite mit dem Finger über die Kontakte für die einstellbare Adresse kommt ändert sie sich, die Eingänge A2..A0 hingen in der “Luft”. Ok, mit dem Lötkolben Lötbrücken setzen. Aus Neugier habe ich geprüft, ob die VCC-Kontakte der Brücken mit VCC verbunden sind: Leider nein, untereinander haben sie zwar Kontakt, aber eine Verbindung zu VCC konnte ich nicht feststellen. Die GNDs sind miteinander verbunden. Falls eine andere Adresse als 0x48 eingestellt werden sollte würde es mich nicht wundern, wenn es spinnt…

Gut zu erkennen sind die PullUp-Widerstände von 10kΩ für SDA und SCL auf der Platine.

LM75A-Modul
LM75A-Modul
LM75A-Modul

Also den LM75A ohne Library selber auslesen können, damit wir uns intensiver mit I2C-Kommunikation beschäftigen -RTFM: [Temperatursensor LM75A]
In der Forsa wird der LM75 dokumentiert, der kann nur 9 Bit Auflösung der Temperatur, beim LM75A sind es 11 Bit.

Der LM75 hat interne Register?

Beim HLF8574 war es einfach, es gab nur ein 1 Byte Register, das gelesen oder beschrieben werden konnte: Target-Adresse auswählen und 1 Byte transferieren.
Beim LM75 gibt es 4 interne Register. Ein “Pointer”-Register merkt sich welches dieser Register gemeint ist. Beim Schreiben muss immer der Pointerwert für Zielregister 0..3 gesendet werden. Beim Lesen wird einfach das zuletzt eingestellte Register verwendet. Will man ein anderes Register lesen muss zuerst der Pointer geändert werden. Steht alles mit Beispielen in der Doku. Muss ich das alles wissen um einfach die Temperatur auslesen zu können? Nein!
Voreingestellt ist Register 0 und dort steht in zwei Bytes die Temperatur: Zwei Bytes lesen und gut ist. Hier die Kodierung der Temperatur in den zwei Bytes:

MSByteLSByte
Bit7654321076543210
LM75D8D7D6D5D4D3D2D1D0xxxxxxx
LM75AD10D9D8D7D6D5D4D3D2D1D0xxxxx
Temperaturwert LM75(A)

Es ist total einfach: Im MSByte steht die Temperatur mit Vorzeichen (Bit 7) in °C, will man es genauer haben kriegt man beim LM75 noch die halben Grade im LSByte und beim LM75A die achtel Grade geliefert. Hier mein Test-Programm:

#include <Wire.h> // I2C-Library
#include <LiquidCrystal_PCF8574.h> // LCD
#define ADRESSE 0x48  //0b01001000
LiquidCrystal_PCF8574 lcd (0x27); 
char byteH, byteL;
float temp;

void setup() {
  Wire.begin();  //Alternatives PinMapping: Wire.begin(SDA, SCL);
  lcd.begin(16, 2); // initialize the lcd
  lcd.clear();
  lcd.setBacklight(255);
  lcd.setCursor(0,0); // erstes Zeichen, erste Zeile
  lcd.print("LM75A TempSensor");
}

void loop() {
  /*Wire.beginTransmission(ADRESSE); // Nicht notwendig, wenn man am Pointer nix verstellt hat
  Wire.write(0x00); // Pointer auf 0 setzen um Temp auslesen zu können
  Wire.endTransmission();
  */
  Wire.requestFrom(ADRESSE, 2); // 2 Bytes lesen
  Wire.available(); // MSByte
  byteH = Wire.read(); // receive a byte
  Wire.available(); // LSByte
  byteL = Wire.read(); // receive a byte
  Wire.endTransmission();
  temp=byteH; // einfach nur Grad Celsius
  //temp = ((byteH<<3) + (byteL>>5))*0.125; // LM75A mit 0.125 Grad Auflösung
  lcd.setCursor(0, 1);
  lcd.print(temp);
  delay(1000);
}

Bei 0.125 °C Auflösung ist ein wenig Bit-Shift-Woodo notwendig 😎.
Hab noch ne Dose Kältespray gefunden und siehe da: Negative Temperaturen stimmen nicht! Ok also Hirn ganz einschalten:

#include <Wire.h> // I2C-Library
#include <LiquidCrystal_PCF8574.h> // LCD
#define ADRESSE 0x48//0x92
LiquidCrystal_PCF8574 lcd (0x27); 
signed char byteH; // wichtig ist das signed!
char byteL;        // signed führt zu Fehler
float temp;

void setup() {
  Wire.begin();  //Alternatives PinMapping: Wire.begin(SDA, SCL);
  lcd.begin(16, 2); // initialize the lcd
  lcd.clear();
  lcd.setBacklight(255);
  lcd.setCursor(0,0); // erstes Zeichen, erste Zeile
  lcd.print("LM75A TempSensor");
}

void loop() {
  Wire.requestFrom(ADRESSE, 2); // 2 Bytes lesen
  Wire.available(); // MSByte
  byteH = Wire.read(); // receive a byte
  Wire.available(); // LSByte
  byteL = Wire.read(); // receive a byte
  Wire.endTransmission();
  //byteH=0b11111111; // zum Testen ob -0.125 laut Doku rauskommt
  //byteL=0b11100000;
  temp = ((byteH<<3) + (byteL>>5))*0.125; // volle Auflösung bitte
  //temp=byteH;
  lcd.setCursor(0, 1);
  lcd.print(temp);
  delay(1000);
}

Zum Kältespray: Von wegen bis -50°C kam auf minimal -36 °C.
Zum Code: Ich hab an den Datentypen rumgespielt: char byteH ist offensichtlich unsigned, erst als ich signed davor schrieb ging es in die Negativen. byteL darf nicht signed sein, sonst stimmt der Wert für -0.125 °C nicht. Spannend wäre nun den Code mit anderen µC zu testen..

Luftdruck und Temperatursensor BPM280

Siehe –>3.5 Sensoren. Der Lerneffekt bzgl. I2C-Bus ist dabei nicht besonders hoch, der Sensor ist komplex und es wird auch geraten die Library dafür zu verwenden.
Für ein Projekt, das sich mit dem Sensor beschäftigt aber brauchbar.

Aufgaben und abprüfbares Wissen

Sobald man alte und einfache Module verlässt wird es kompliziert, Details werden in die Nutzung von Library zu den Sensoren verlagert.
Falls man eigene Targets entwickelt siehe MezData: Discovery-Game sieht es schon spannender aus.

Schnipsel und ToDo

  • Liste von Fragen zu I2C-Bus erstellen.
  • Grösse von PullUp und Serienwiderstand -> Link. Wann müssen wir uns um die PullUps selber kümmern?
  • ¡Selbst gebaute Targets siehe Discovery-Game! Event-Programmierung bei Bus-Action.
  • Alternative Ports für I2C verwenden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert