I²C (vorläufig)

XMega Blockdiagramm

Mit Hilfe des I²C Buses ist es dem XMega möglich mit anderen Geräten zu kommunizieren (z.B. EEPROMs, Sensoren usw.).
Ein Vorteil vom I²C ist, dass er mit nur zwei Leitungen auskommt (SDA und SCL).
Der Bus besteht im Normalfall immer aus einem Master, welcher den Takt vorgibt, und einer beliebigen Anzahl von Slaves (maximal 127).

-> Der I²C Bus:

Bevor ich auf das Programm eingehe, erkläre ich einmal ganz kurz wie der I²C Bus funktioniert.
Der I²C (oder Inter-Integrated Circuit) Bus ist ein bidirektionaler Zweidrahtbus,  was soviel heißt wie das der Bus nur zwei Signalleitungen besitzt und es keine feste Richtung für die Versendung von Daten gibt (nicht so wie beim UART das Tx nur für die Daten vom Sender zum Empfänger ist).
Er besteht aus zwei Leitungen die SCL (Serial Clock) und SDA (Serial Data) genannt werden. Die SCL und SDA Pins eines I²C Devices sind Open-Collector, weswegen es nötig ist den Bus mittels Pull-Up Widerstände auf einen bestimmten Pegel zu ziehen.
Jeder Busteilnehmer besitzt eine eigene Adresse, die sich in eine feste Grundadresse und in eine variable Adresse aufteilt. Bei vielen I²C Devices kann somit die Adresse durch bestimmte Adresspins verändert werden, sodass mehrere ICs mit der selben Grundadresse durch den Bus verbunden werden können.
Eine Adresse ist üblicherweise 7-Bit lang. Dazu kommt dann noch ein 8. Bit welches signalisiert ob vom IC gelesen werden soll oder ob Daten in das IC rein geschrieben werden sollen.
Normalerweise besitzt der Bus einen Master (meistens Mikrocontroller) und bis zu 127 Slaves (EEPROMs, Sensore usw.)., aber es ist auch möglich mehrere Master in einen Bus zu implementieren.
Ein typischer I²C Bus sieht z.B. so aus:

I2C Bus SprutQuelle: Sprut.de

Für alle die etwas mehr über den I²C Bus erfahren wollen, gibt es hier einen Link zu einem passenden Wikipediaartikel.

-> Der XMega als I²C Master:

Als erstes zeige ich euch wie ihr den I²C des XMega so konfiguriert das er als Master arbeitet.
In meinem Beispiel verwende ich dazu einen PCF8574 I²C Portexpander und das Programm schaltet eine LED, die sich am Pin P0 vom Portexpander befindet, ein und aus.

– I²C konfigurieren:

Um den I²C vom XMega zu konfigurieren, verwende ich diese Funktion:

void TWI_MasterInit(TWI_t *twi)
{
   twi->MASTER.BAUD = TWI_BAUDSETTING;
   twi->MASTER.CTRLA = TWI_MASTER_ENABLE_bm |
   TWI_MASTER_INTLVL_HI_gc |
   TWI_MASTER_RIEN_bm |
   TWI_MASTER_WIEN_bm;
   twi->MASTER.STATUS = TWI_MASTER_BUSSTATE_IDLE_gc;
}

Diese Funktion ist so designed, dass beim Funktionsaufruf mitgeteilt wird welcher I²C genutzt werden soll. Den Rest erledigt die Funktion mittels Zeigeroperationen.
Die erste Zeile der Funktion kopiert die gewünschte Baudrate in das Baudratenregister.
Der Wert der in das Register kopiert wird, wird vom Compiler berechnet.
Dies geschieht mittels „#define“:

#define Taktfreuqenz 32000000
#define Takt_TWI 400000
#define TWI_BAUD(F_SYS, F_TWI) ((F_SYS / (2 * F_TWI)) - 5)
#define TWI_BAUDRATE TWI_BAUD(Taktfrequenz, Takt_TWI)

Zuerst definiere ich die Variable „Taktfrequenz“ und gebe ihr die aktuelle Frequenz des Mikrocontrollers (in meinem Fall 32MHz).
Die nächste Zeile definiert die Variable „Takt_TWI“ und sie erhält den Wert der gewünschten Taktfrequenz für den I²C. Dieser ist in meinem Beispiel auf 400kHz gesetzt.
Jetzt wird die Variable „TWI_BAUD“ definiert.
Der Wert dieser Variable berechnet sich auf Grundlage der im Datenblatt angegebenen Formel

((F_SYS / (2 * F_TWI)) - 5)

Die Variablen „F_SYS“ und „F_TWI“ sind zwei funktionsinterne Variablen.
Es wird also eine Funktion definiert die „TWI_BAUD“ heißt und die als Übergabeparameter zwei Werte erwartet die funktionsintern „F_SYS“ und „F_TWI“ heißen.
Die letzte Zeile definiert eine Variable mit dem Namen „TWI_BAUDRATE“ und diese erhält als Wert das Ergebnis aus der Funktion „TWI_Baud“ der die Werte aus der Variable „Taktfrequenz“ und „Takt_TWI“ übergeben werden.
Dieser Wert wird anschließend mittels

twi->MASTER.BAUD = TWI_BAUDRATE;

in das Baudratenregister geschrieben.
Anschließend wird der XMega in den Mastermodus versetzt und sowohl der Read und der Writeinterrupt aktiviert:

twi->MASTER.CTRLA = TWI_MASTER_ENABLE_bm |
TWI_MASTER_INTLVL_HI_gc | TWI_MASTER_RIEN_bm |
TWI_MASTER_WIEN_bm;

Als letztes muss der Bus noch in den „Idle“ Modus versetzt werden. Dies geschieht so:

twi->MASTER.STATUS = TWI_MASTER_BUSSTATE_IDLE_gc;

Das ist ganz wichtig, da der I²C nach einem Reset erst in den „Unknown“ Zustand wechselt und im „Unknown“ Zustand kann über den Bus weder gelesen noch geschrieben werden.
Dieses Bild aus dem Manual veranschaulicht das ganz gut:

I2C Busstate

Nun da der Master konfiguriert ist, erkläre ich euch wie ihr ein Slavedevice ansprechen könnt.

– Daten zu einem PCF8574 übertragen:

Die Kommunikation zwischen Master dem PCF857) geschieht in zwei Schritten.
Zuerst wird die Adresse übermittelt um das passende Slavedevice anzusprechen und dann werden dem Slave die Daten übermittelt.
Die Adresse des PCF8574 habe ich am Anfang des Programmes durch die Zeile

#define PCF8574 0x20 

unter der Variable PCF8574 gespeichert. Dabei handelt es sich aber nur um die 7 Bit der Adresse vom PCF8574. Das R/W Bit ist noch nicht mit dabei. Dieses Bit wird später vom Programm hinzugefügt.
Die Funktion die das Schreiben der Daten in den PCF8574 übernimmt sieht so aus:

void PCF8574_send_byte(TWI_t *twi, char Adresse, char byte)
{
   Send_Address(twi, Adresse, 0);
   _delay_us(500);
   twi->MASTER.DATA = byte;
}

Sie erhält als Übergabeparameter den Namen vom I²C Interface welches verwendet werden soll, die Adresse von dem Slavedevice und 1 Byte an Daten die geschickt werden sollen.
Als erstes wird durch

Send_Address(&TWIF, Adresse, 0);

eine weitere Funktion aufgerufen, die das Senden der Adresse übernimmt.
Diese Funktion erhält den Namen des zu verwendenen I²C Interfaces (&TWIF), die Adresse des PCF8574 (durch den Zeiger „Adresse“ markiert) sowie eine „0“ wenn schreibend auf den Slave zugegriffen wird oder eine „1“ wenn lesend auf den Slave zugegriffen wird.
Betrachten wir die Funktion zum senden der Adresse mal genauer. Sie sieht so aus:

void Send_Address(TWI_t *twi, char Adresse, char RW)
{
   twi->MASTER.ADDR = (Adresse << 1) + RW;
}

Als allererstes wird eine lokale Variable mit dem Namen „Add“ deklariert. Diese soll später die vollständige Adresse des Slaves besitzen.
Anschließend wird das Bitmuster der in die Funktion übergebenen Adresse, welches sich in der Variable „Adresse“ befindet, in die Variable „Add“ kopiert. Das Bitmuster der Variable „Add“ wird im nächsten Schritt um eine Stelle nach links geschoben und anschließend wird geprüft ob das RW Bit gesetzt wurde.
Wenn es gesetzt ist, sprich es soll lesend auf das Device zugegriffen werden, dann wird die Adresse aus der Variable „Add“ mit 0x01 verodert.
Ist das „RW“-Bit nicht gesetzt, weil schreibend auf das Device zugegriffen werden soll, so wird die Variable „Add“ mit dem invertierten Wert von 0x01 verundet..
Dies sieht für den Fall das gelesen werden soll so aus so aus:

Add: 010 0000
Shift nach links
Add: 100 0000
RW:             1

Add: 100 0001

Und für den Fall das Daten in das Device geschrieben werden sollen:

Add: 010 0000
Shift nach links
Add: 100 0000
RW:             1
Add: 100 0000
RW:  111 1110 

Add: 100 0000

Da die Adresse immer mit 0 verundet wird, kann das letzte Bit nie gesetzt werden. So wird dafür gesorgt, dass das R/W Bit beim Schreiben immer 0 ist.
Im Umkehrschluss sorgt ein verodern mit einer 1 dafür, dass das Bit immer gesetzt ist.
Das Resultat ist dann die Adresse des Slaves, zusammen mit dem R/W Bit. Diese Adresse wird anschließend in das Adressregister des gewünschten I²C Interfaces geschoben und anschließend versendet.
Dies sieht so aus:

twi->MASTER.ADDR = Add;

Nachdem die Adresse verschickt wurde springt der Controller zurück in die erste Funktion.
Dort wird das Programm 500µs lang angehalten um dem Controller und dem Device genug Zeit zu geben die Daten zu verarbeiten.
Anschließend wird mit der Zeile

twi->MASTER.DATA = byte;

das übergebene Datenbyte in das durch den Pointer „twi“ markierte I²C Datenregister kopiert und das Byte versendet.

– Daten von einem PCF8574 lesen:

Das Lesen von Daten von dem PCF8574 geschieht ähnlich wie das Schreiben.
Als aller erstes wird die Adresse des Devices übertragen, nur diesmal muss das R/W Bit gesetzt werden, sprich die Adresse erhöht sich um eins.
Auf die Funktion zum senden der Adresse übertragen sieht dies so aus:

Schreiben:
Send_Address(twi, Adresse, 0); 

Lesen:
Send_Address(twi, Adresse, 1);

Mit dieser Funktion werden dann die Daten ausgelesen:

char PCF8574_read_byte(TWI_t *twi, char *Adresse)
{
   Send_Address(twi, Adresse, 1);
   _delay_us(500);
   twi->MASTER.CTRLC = TWI_MASTER_CMD_STOP_gc;
   return twi->MASTER.DATA;
}

Nachdem die Leseadresse übertragen wurde gibt der PCF8574 ein Byte Daten auf den Bus. Diese werden vom Mikrocontroller eingelesen und im Datenregister gespeichert.
Die Zeile

twi->MASTER.CTRLC = TWI_MASTER_CMD_STOP_gc;

übermittelt eine Stopkondition, damit der PCF8574 weiß das die Übertragung zu Ende ist.
Jetzt können die Daten mithilfe der Zeile

return twi->MASTER.DATA;

aus dem Datenregister raus kopiert und aus der Funktion übergeben werden.
Im Hauptprogramm werden sie dann in einer Variable gespeichert, in einen String umgewandelt und per UART an meinen PC geschickt.

Dieser Teil sieht so aus:

Buffer = PCF8574_read_byte(&TWIF);
itoa(Buffer, Text, 10);
Send_UART(Text);
_delay_ms(1000);

Die Zeile

Buffer = PCF8574_read_byte(&TWIF, PCF8574);

ruft die Funktion zum auslesen eines Bytes auf und den Rückgabewert speichert sie in der Variable „Buffer“.
Die Variable „Buffer“ wird anschließend in der nächsten Zeile zu einem String mit dem Namen „Text“ zusammengesetzt. Die „10“ bedeutet, dass Zahlen aus der Variable „Buffer“ in Dezimalzahlen umgewandelt werden und der ganze String wird anschließend durch die Funktion

Send_UART(Text);

versendet.
Danach pausiert das Programm 1 Sekunde.
Die Ausgabe der Daten auf dem Computer sieht dann so aus:

XMega I2C Read PCF8574

Bei meinem PCF8574 sind alle Pins bis auf P1 High. Nur P1 ist auf GND gezogen und so entsteht der im Terminal angezeigt Zahlenwert.

 

-> Zurück zum XMega Tutorial

9 thoughts on “I²C (vorläufig)
  1. Hallo Kampi,
    danke für dein Tutorial! Damit hab ich sehr schnell mein Atxmegaboard zum laufen bekommen! Echt cool! Jeder Port kann I2C! Ein super Embedded Prozzi.

    Ich hab noch ne kleine Verbesserung für dich. Die Funktion
    Send_Address() kann um 8 Byte gekürzt werden und sie ist auch einfach zu verstehen.

    void Send_Address(TWI_t *twi, char Adresse, char RW)
    {
    twi->MASTER.ADDR = (Adresse << 1) + RW;
    }
    Wenn du den Shift Operator nutzt ist lauf C Definition das "freie" Bit immer 0, also braucht man die Maskierung nicht. RW kann nur 0 oder 1 sein da kann man es auch einfach auf die Shift Operation aufaddieren.

    Gruß
    schorsch

    • Hallo Schorsch,

      danke für deine Anmerkung.
      Ich werde es demnächst korrigieren und ich hoffe das ich im nächsten Jahr mal weiter dran arbeiten kann (ist im Moment nur viel zusammen gebasteltes und dementsprechend was vorläufiges).
      Aber freut mich, dass das Tut wieder jemandem was genützt hat :)
      Auf meiner ToDo Liste für die Zeit nach den Klausuren nächstes Jahr steht auf jeden Fall der I²C, SPI, Empfangen beim UART und der USB.

      Gruß
      Daniel

  2. Hi Daniel,

    erstmal Kompliment TOP Seite ich kämpfe seit längerem mit dem XPLAIN Board und nem I2C Sensor, der mir 4 Byte Daten auf den Bus legt… ich bin noch ein ziemlicher Anfänger was uC Programmierung angeht. Mein Gedanke war, ein Array in der Funktion char PCF8574_read_byte(TWI_t *twi, char *Adresse) anzulegen, und dort dann die 4 Bytes abzuspeichern. Leider bekomm ich einen Kompiler Fehler nach dem anderen….könntest Du mir ein Code Beispiel zukommen lassen? Besten Dank
    Grüße Johannes

    • Hallo Johannes,

      schick mir mal bitte einen Screenshot des Fehlers und dein Programm per E-Mail zu.
      Um welchen Sensor handelt es sich?

      Gruß
      Daniel

  3. Hi Daniel,

    Super! Den vorangegangenen Komplimenten kann ich mich nur anschließen.
    Bei mir ist auch fast alles auf anhieb gelaufen. Nur wenn ich mit „PCF8574_read_byte(TWI_t *twi, char Adresse)“ ein Byte lesen möchte, hängt sich die I2C Schnitstelle auf. Die Stop Condition funktioniert bei mir vermutlich nicht. Ich arbeite mit dem xmega32a4 und dem PCF8574. Hast Du ne Ide wo ich einen Fehler gemacht haben könnte?

    Vielen Dank und freundliche Grüße Sebastian

    • Hallo Daniel,

      ich habe den Fehler gefunden. Mein I2C Expander ist ein PCF8574T von NXP. Dieser hat vermutlich einen Bug. Wenn ich den PIN P7 auf 0 setze, bzw über eine Taste auf 0 ziehe, wir der SDA hart auf GND gezogen und somit die Stop Condition verhindert. Ab diesem Moment ist auch kein weiterer Datentransfer auf SDA möglich.

      Gruß
      Sebastian

      • Hallo Sebastian,

        uuuh, das ist ärgerlich.
        Aber schön das du den Fehler gefunden hast (dann muss ich mein AVR Board nicht mehr suchen ;) ).

        Gruß
        Daniel

  4. Hallo Kampi,
    weißt du zufällig, ob die Pins des TWI als Aus-oder Eingang eingestellt werden müssen? Ich denke mal der SCL wird als Ausgang definiert. Aber was ist mit dem SDA?
    Gruß
    Andreas

    • Hallo Andreas,

      so aus dem Kopf weiß ich das leider nicht mehr.
      Da musst du mal im Datenblatt nachschauen, bzw. probier es einfach mal aus ;)

      Gruß
      Daniel

Schreibe einen Kommentar

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

Time limit is exhausted. Please reload CAPTCHA.