Programmierkurs für PIC-Mikrocontroller in C (CC5X Compiler)
Teil 4 I2C-Bus


Der I2C-Bus unterstützt bidirektionalen Datenaustausch zwischen ICs, auf 2 Leitungen, mit großer Geschwindigkeit. Hierbei werden die Daten seriell, asynchron übertragen. Es sind bis zu 127 Endgeräte adressierbar. Die Datenleitung trägt den Namen SDA (SerialDAta), die Taktleitung den Namen SCL (SerialCLock).

Alle Teilnehmer teilen sich in 2 Klassen: "Master" und "Slave". Der Master initialisiert Datenübertragungen und erzeugt das dazugehörige Taktsignal. Der Slave ist passiv und wartet, bis seine Adresse angewählt wird. Ein PIC mit softwaremäßigem I2C-Interface kann nur Master sein, denn er ist zu langsam um auf einen fremden Takt zu synchronisieren. Jede Datenübertragung startet mit einer Start-Bedingung und endet mit einer Stop-Bedingung.

Am Anfang sind beide Leitungen "1". Start-Bedingung ist, wenn während SCL "1" ist, SDA nach "0" wechselt. Stop-Bedingung ist, wenn SCL "1" ist und SDA nach "1" wechselt. Während SCL "1" ist, darf SDA sich nicht ändern, denn das ist der Zustand, in dem Daten gültig sind und übernommen werden.

Nach der Start-Bedingung wird Bauteiladresse übertagen.

Bits 7...4 der Adresse sind im IC festgelegt(beim seriellen EEPROM (24LC32) ist das beispielsweise "1010" .

Bits 3...1 sind durch Spannungspegeln an den Pins 3,2,1 einstellbar.

Bit 0 teilt dem IC mit, ob gelesen oder geschrieben wird: 1-Lesen, 0-Schreiben.

Es können 8 seriellen EEPROM an einem Bus angeschlossen sein.


Seriellen EEPROM Beschreiben
-Start
-Bauteiladresse senden
-Hochwertige Adresse senden
-Niederwertige Adresse senden
-Datenbyte senden
-Stop.
ACK(Bestätigungsbit) kommt von Slave

Seriellen EEPROM Lesen

-Start
-Bauteiladresse senden
-Hochwertige Adresse senden
-Niederwertige Adresse senden
-Bauteiladresse senden (Bit 0 = 1 zum lesen)
-Datenbyte lesen
-NO ACK senden
-Stop.

Ich stelle hier als Beispiel die Kommunikation mit dem EEPROM(24LC32) vor :

#include <C:\cc5\16F84.H>

#pragma config |= 0b.1111.1111.0010 //Konfigurations-Wort

#pragma bit Opin @ PORTA.0         //Ausgang für serielle Daten definieren

#pragma bit Ipin @ PORTA.1         //Eingang für serielle Daten definieren

#include <Seriel4.c>               //Einbinden Code zur seriellen Kommunikation

#pragma bit sda @ PORTA.3          // Datenleitung SDA

#pragma bit scl @ PORTA.2          // Taktleitung SCL

#pragma bit tsda @ TRISA.3         // Datenleitung SDA IN/AUT - Umschalter

#pragma bit tscl @ TRISA.2         // Taktleitung SCL  IN/AUT - Umschalter

#include <I2C.c>                   //Einbinden Code zur I2C Kommunikation

 

void pausems(uns16 ms)             // Unterprogramm zum Abwarten angegebener Anzahl von Millisekunden

{

while(ms)               // Schleife verlassen, wenn ms=0 ist

      {

    OPTION = 2;               // Vorteiler auf 8 einstellen

    TMR0 = 131;              // 125 * 8 = 1000 (= 1 ms)

    while (TMR0);            //Abwarten einer Millisekunde

    ms--;                    // "ms" mit jeder Millisekunde erniedrigen

    }                   //Ende der Schleife

}

 

void main(void)              //Hauptprogramm

{

TRISA = 0b.1111.1110;        //Bit0 des PortA ist serielle Ausgang

TRISB = 0b.0000.0000;        //PortB eigentlich unbenutzt

char d,komand,adrl,adrh;     //Variablen definieren

#asm

BCF   0x03,RP0               //Speicherbank in Assemblermodus auf Bank 0 umschalten

#endasm

 

anfang:

komand=empfang();       //Empfange seriellen Befehl

switch(komand)          //Befehl auswerten und entsprechend handeln 

{

case '1' :              // 00 Adresse lesen **********************************

bstart();               // Start - Bedingung einstellen

schreib(160);           // Bauteiladresse senden (zum Schreiben)

schreib(0);             // Höhere Adresse übertragen (0..127)

schreib(0);             // Niedrigere Adresse übertragen (0..255)

bstart();               // Nochmals  Start - Bedingung einstellen

schreib(161);           // Bauteiladresse senden (zum Lesen)

d=lesen();              // Byte einlesen

bitaus(1);              // NO ACK

bstop();                // Stop - Bedingung einstellen

senden(d);              //Gelesene Byte zum PC senden

break;

 

      case '2' :                                    // 00 Adresse beschreiben ******************************

d=empfang();            //Zu schreibendes Zeichen vom PC empfangen

bstart();               // Start - Bedingung einstellen

schreib(160);           // Bauteiladresse senden zum Schreiben

schreib(0);             // Höhere Adresse übertragen (0..127)

schreib(0);             // Niedrigere Adresse übertragen (0..255)

schreib(d);             // Byte schreiben

bstop();                // Stop - Bedingung einstellen

break;

 

case '3' :              // Auslesen 50-Byte ab bestimmter Adresse *****************

adrh=empfang();         // High –Adresse von PC empfangen

adrl=empfang();         // Low –Adresse von PC empfangen

bstart();               // Start - Bedingung einstellen

schreib(160);           // Bauteiladresse senden zum Schreiben

schreib(adrh);          // Höhere Adresse übertragen (0..127)

schreib(adrl);          // Niedrigere Adresse übertragen (0..255)

bstart();               // Nochmals  Start - Bedingung einstellen

schreib(161);           // Bauteiladresse senden (zum lesen)

d=lesen();              // Byte einlesen erstes Mal

for (adrl=0;adrl<50;adrl++)  // Schleife 50-mal

      {

      bitaus(0);        // ACK

      senden(d);        // seriell zum PC

      d=lesen();        // Byte einlesen

      }

bitaus(1);              // NO ACK (immer vor dem Stop)

bstop();                // Stop - Bedingung einstellen

break;

 

case '4' :              // Schreiben 8-Byte an bestimmter Adresse **********

adrh=empfang();         // High –Adresse von PC empfangen

adrl=empfang();         // Low –Adresse von PC empfangen

bstart();               // Start - Bedingung einstellen

schreib(160);           // Bauteiladresse senden zum Schreiben

schreib(adrh);          // Höhere Adresse übertragen (0..127)

schreib(adrl);          // Niedrigere Adresse übertragen (0..255)

for (d=0;d<8;d++) schreib(d); // 8 Byte schreiben in dem Fahl 0..8

bstop();                // Stop - Bedingung einstellen

pausems(6);             // Schreibzyklus des EEPROMs 6ms, also warten

break;

 

case '5' :        // Bauteiladressen suchen ******************************

bit ACK;                // Bit Variable

d=2;                    // mit Adresse 2 anfangen

while (d)               // alle Bauteiladressen durchgehen

{

bstart();         // Start - Bedingung einstellen

ACK=schreib(d);   // Bauteiladresse Übertragen, Bestätigungsbit aufnähmen

if (!ACK) senden(d);// Wenn IC auf Adresse mit ACK antwortet, dann diese Adresse seriell senden

bstop();          // Stop - Bedingung einstellen

pausems(16);      // nicht unbedingt notwendig

d +=2;            // Adresszähler um 2 erhöhen

}

      break;

}

goto  anfang;           // Endlosschleife(weitere Befehl holen)

}


Include-datei I2C.c Enthält Funktionen zum Kommunikation auf dem I2C-Bus als Master.
bstart(); Erzeugt Startbedingung auf dem I2C-Bus, keine Parameter
bstop(); Erzeugt Stopbedingung auf dem I2C-Bus, keine Parameter
schreib(10); Schreibt ein Byte, erhält Zeichen zum Senden, gibt zurück Bestätigungsbit ACK des Slave-Geräts.
d=lesen(); Liest ein Byte von I2C-Bus, gibt das eingelesene Zeichen zurück
bitaus(0); Ein Bit ausgeben, dient zum Erzeugen des Bestätigungsbits (ACK) von Master.

Der fünfte Abschnitt des Programms findet automatisch Bauteiladressen aller an I2C-Bus angeschlossener Geräte.


Schaltplan zu diesen Quellcode ist hier. Anstatt 24LC32 kann auch 24L32, 24L64 verwendet werden. Wobei der 24L32 einen schnelleren Schreibzyklus hat (1ms). Bei anderen EEPROMs mit I2C-Interface muss man in den Datenblätter nachlesen, um die Adressbereiche und die Schreibzyklusdauer einzustellen.


PCF8574 ist ein 8-Bit Port für I2C-Bus Mit diesem IC kann man 8-Bit ausgeben oder einlesen. Zum Einlesen müssen alle Ausgänge auf 1 gesteuert sein. Wenn ein Ausgang von aussen auf "0" gezogen wird, dann wird er als "0" eingelesen. Die festgelegte Adresse des PCF8574 ist "0100". Das Schreiben und Lesen ist noch einfacher als beim EEPROM.

Schreiben Lesen
- Start
- Bauteiladresse schreiben, Bit 0 ="0"
- Datenbyte schreiben
- Stop
- Start
- Bauteiladresse schreiben, Bit 0 = "1"
- Datenbyte lesen
- NO ACK
- Stop.


LM75 (TCN75) ist ein Temperatursensor mit I2C-Interface und einer Auflösung von 0,5 °C und einem Messbereich von -55°C bis +125°C. Seine Bauteiladresse lautet "1001". Es werden 2 Bytes ausgelesen: das erste Byte enthält die ganze Zahl (zum Beispiel 22) in Grad, das zweite Byte ist entweder "1" oder "0". Hierbei steht die "1" für 0,5°C und die "0" für 0°C. Der Temperatursensor hat eine Genauigkeit von +-3°C, deswegen muss man im Programm eine Korrektur vorsehen. Der LM75 hat auch einen Schaltausgang und die Möglichkeit die Einschalttemperatur und Ausschalttemperatur einzustellen. Das ist aber bei der Arbeit mit einem Mikrocontroller überflüssig, da diese Aufgaben von dem Mikrocontroller übernommen werden können.

Hier ist ein Beispiel einer Kommunikation mit 8-Bit Port (PCF8574) und Temperatursensor (TCN75).

// Ausprobieren Kommunikation mit I2C-Temperatursensor TCN75(LM75) bei 4 MHz

// PORT-Expander

#include <C:\cc5\16F84.H>

#pragma config |= 0b.1111.1111.0010 // Konfigurations Wort

#pragma bit Opin @ PORTA.0   // Ausgang für serielle Daten definiren

#pragma bit Ipin @ PORTA.1   // Eingang für serielle Daten definiren

#include <Seriel4.c>

#pragma bit sda @ PORTA.3          // Datenleitung SDA

#pragma bit scl @ PORTA.2          // Taktleitung SCL

#pragma bit tsda @ TRISA.3         // Datenleitung SDA IN/AUT - Umschalter

#pragma bit tscl @ TRISA.2         // Taktleitung SCL  IN/AUT - Umschalter

#include <I2C.c>                   // Einbinden Code zur I2C Kommunikation

 

void pausems(uns16 ms)       // Unterprogramm zum Abwarten angegebener Anzahl von Millisekunden

{

while(ms)                    // Schleife verlassen, wenn ms=0 ist

{

OPTION = 2;             // Vorteiler auf 8 einstellen

TMR0 = 131;             // 125 * 8 = 1000 (= 1 ms)

while (TMR0);           // Abwarten einer Millisekunde

ms--;                   // "ms" mit jeder Millisekunde erniedrigen

}

}

 

void main(void)

{

TRISA = 0b.1111.1110;

TRISB = 0b.0000.0000;

char d,komand,k;

#asm

BCF   0x03,RP0

#endasm

anfang:

komand=empfang();

switch(komand)

{

case '1' :              // Temperatur lesen ***********************

bstart();               // Start - Bedingung einstellen

schreib(147);           // Bauteiladresse senden zum Lesen

d=lesen();              // Byte einlesen, Temperatur in Grad

bitaus(0);              // ACK

k=lesen();              // Byte einlesen (0...0,5) 1 Bit

bitaus(1);              // NO ACK

bstop();                          // Stop - Bedingung einstellen

senden(d);

break;

 

case '2' :

d=empfang();            // Port-Expander beschreiben

bstart();               // Start - Bedingung einstellen

schreib(64);            // Bauteiladresse senden zum Schreiben

schreib(d);             // Byte schreiben (0..255)

bstop();                // Stop - Bedingung einstellen

break;

 

case '3' :              // Port-Expander lesen *******************         

bstart();               // Start - Bedingung einstellen

schreib(65);            // Bauteiladresse senden zum Lesen

d=lesen();              // Port-Expander lesen

bstop();                // Stop - Bedingung einstellen

senden(d);

break;

}

goto  anfang;

}

Diese Quellkodes habe ich getestet.

Schaltplan herunterladen zum Programm. Leider nur der Teil mit I2C-ICs, Anschlussplan für PIC16F84 bitte im Schaltplan oben entnehmen.

In allernächster Zukunft plane ich "I2C.c" für Taktfrequenz 20Mhz einzustellen und mit PIC16F871 testen.
Auch sind Experimente geplant mit I2C-Chipkarte 256 Byte (2 kbit) (für 1,55 Euro bei Reichelt ).