Kampis Elektroecke

XMega – I2C

XMega Blockdiagramm

In diesem Teil des Tutorials möchte ich zeigen, wie das Two Wire Interface (TWI), ein I2C-kompatibler Nachbau von Atmel, eines XMega-Mikrocontrollers genutzt werden kann. Texas Instruments hat ein sehr schönes Dokument veröffentlicht, in dem die Funktionsweise des I2C-Bus sehr detailiert erklärt wird, falls es beim Verständnis des I2C-Bus noch die ein oder andere Lücke geben sollte.

XMega als I2C-Master:

Den Anfang möchte ich mit dem XMega als I2C-Master machen. Es sollen drei Register eines beliebigen Chips (hier eine DS1307) beschrieben und wieder ausgelesen werden. Das Schreiben, bzw. das Lesen soll sowohl mit, als auch ohne Interrupts erfolgen.

I2C-Master im normalen Modus:

Beginnen möchte ich mit der Realisierung eines I2C-Master ohne Interrupts. Die I2C-Schnittstelle der XMega-Familie ist, meiner Meinung nach, deutlich einfacher aufgebaut, als die der AVR8-Familie (wie z. B. beim Mega32). Intern arbeitet das I2C-Modul als Zustandsautomat und ist dank geschicktem Design mit sehr wenig Aufwand zu betreiben.

Das Modul ist zudem in ein Master- und ein Slave-Modul aufgeteilt, wodurch die einzelnen Betriebsmodi softwareseitig sehr gut voneinander getrennt werden können.

Die Konfiguration des Moduls erfolgt über das CTRLA– und das CTRLB-Register.

In diesem Beispiel werden keine Interrupts und keine zusätzlichen Modi genutzt, wodurch sich die Konfiguration auf die Aktivierung des Master-Modus reduziert:

Über das BAUD-Register wird anschließend die Busfrequenz definiert. Die Busfrequenz muss sich dabei im Bereich zwischen 100 kHz und 400 kHz bewegen. Der Wert für das BAUD-Register lässt sich über die gegebene Formel berechnen:

BAUD = \frac{Clock}{2\cdot f_{TWI}} - 5

Für eine Busfrequenz von 100 kHz bei 2 MHz Taktfrequenz ergibt sich ein Wert von 5 für das BAUD-Register.

Nach einem Reset befindet sich das I2C-Modul im Zustand Unknown befindet und muss vor dem ersten Betrieb in den Zustand Idle gesetzt werden. Der aktuelle Zustand des Moduls ist über die ersten beiden Bits des STATUS-Registers einsehbar.


Hinweis:

Der Zustand des I2C-Moduls kann vom Anwender nicht geändert werden! Die einzige Ausnahme ist der Wechsel in den Idle-Modus!


Damit ist das I2C-Modul einsatzbereit. Zeit um die ersten Daten zu versenden…

Das Verhalten des Moduls wird über das ADDR-, das DATA– und das CTRLC-Register gesteuert. Sobald eine Adresse in das ADDR-Register geschrieben wird, erzeugt das I2C-Modul eine Startbedingung auf dem Bus und überträgt die Adresse.

Für einen Schreibzugriff sieht das ganze dann folgendermaßen aus.

Über das STATUS-Register kann dann überprüft werden ob das Addressbyte korrekt verwendet worden ist. Für einen Schreibzugriff muss das WIF-Bit und für einen Lesezugriff das RIF-Bit abgefragt werden.


Hinweis:

Da beide Bits beim Beschreiben des ADDR-Registers gelöscht werden, kann eine allgemeine Funktion zum Übertragen der Adresse gemacht werden, in der beide Bits abgefragt werden. Je nachdem welche Adresse in das ADDR-Register geschrieben und übertragen wird, wird entweder das WIF– oder das RIF-Bit gesetzt.


Jetzt können die Daten übertragen werden. Dazu werden die einzelnen Datenbytes nacheinander in das DATA-Register kopiert und darauf gewartet, dass das jeweilige Byte übertragen worden ist.

Zum Abschluss muss eine Stop-Kondition erzeugt werden, indem das entsprechende Kommando in das CTRLC-Register geschrieben wird.

Mit diesen paar Befehlen ist nun ein Byte per I2C übertragen worden. Schauen wir uns nun mal an, wie ein Datenbyte ausgelesen werden kann.

Damit ein Datenbyte ausgelesen werden kann, muss als erstes die entsprechende Leseadresse verschickt werden. Dies geschieht wieder über das ADDR-Register.

Während der Übertragung der Adresse muss das RIF-Bit im STATUS-Register abgefragt werden um ein Übertragungsende zu erkennen.

Über das RIF-Bit kann anschließend erkannt werden, wann ein neues Datenbyte empfangen worden ist. Dieses Datenbyte kann dann über das DATA-Register ausgelesen werden.

Der Empfang muss jetzt noch bestätigt werden, indem der Master ein ACK erzeugt. Dazu wird der entsprechende Befehl in das CTRLC-Register geschrieben.

Falls es sich bei dem empfangenen Datenbyte um das letzte Datenbyte einer Übertragung handelt (z. B. weil der Puffer voll ist) muss der Master ein NACK erzeugen. Die Art der Bestätigung wird über das CTRLC-Register gesteuert.

Bei einem NACK wird zudem kein Bestätigungsbefehl in das CTRLC-Register geschrieben. Abgeschlossen wird die Übertragung mit einer Stop-Kondition und einem NACK.

Für den Empfang ergibt sich damit der folgende Ablauf:

Damit ist der pollende Betrieb des I2C-Moduls auch schon abgeschlossen und das Modul einsatzbereit. Im nächsten Schritt zeige ich, wie man das Modul ausschließich über Interrupts nutzen kann.

I2C-Master im Interruptbetrieb:

Auch hier wird das Modul zuerst konfiguriert, wobei die Konfiguration vom ersten Beispiel übernommen und um das Setzen der Bits für die Interrupts erweitert werden kann:

Für mein Beispiel verwende ich die Interruptpriorität Low. Als nächstes müssen global die Interrupts aktiviert und die verwendeten Interruptprioritäten freigeschaltet werden.

Für die Übertragung habe ich ein I2C-Nachrichtenobjekt erstellt, welches alle wichtigen Informationen beinhaltet:

Das Objekt beinhaltet folgende Werte:

Member Funktion
Device Adresse der verwendeten Schnittstelle
DeviceAddress Zieladresse des I2C-Slaves
Register Registeradresse des I2C-Slaves
BytesWrite Anzahl der zu schreibenden Bytes
IndexWrite Aktueller Schreibindex
BytesRead Anzahl der zu lesenden Bytes
IndexRead Aktueller Leseindex
BufferWrite Zeiger auf einen Schreibpuffer
BufferRead Zeiger auf einen Lesepuffer
Status Aktueller Zustand des Zustandautomaten

Die Einträge BytesRead, IndexRead und BufferRead werden später für den lesenden Zugriff verwendet. Der Eintrag Register beinhaltet die Adresse des ersten Registers auf dem I2C-Slave.

Eine Transaktion (wieder zuerst Schreiben) wird gestartet, indem das Nachrichtenobjekt mit Werten gefüllt und die Adresse des I2C-Slaves in das ADDR-Registers geschrieben wird.

Sobald die Startbedingung erzeugt und die Adresse übertragen worden ist, wird der entsprechende Interruptvektor für den Master-Interrupt vom genutzten TWI aufgerufen. In diesem wird als erstes das STATUS-Register ausgelesen, damit die Art des Interrupts bestimmt werden kann.

Es können nun drei Fälle eintreten, die über die Statusbits dargestellt werden:

  • Fehler bei der Übertragung – ARBLOST-Bit wird gesetzt
  • Schreibzugriff auf den Slave – WIF-Bit wird gesetzt
  • Lesezugriff auf den Slave – RIF-Bit wird gesetzt

Wenn das ARBLOST-Bit gesetzt ist, ist ein Übertragungsfehler aufgetreten. Die Übertragung wird somit direkt beendet und ein Fehlerzustand eingenommen.

In diesem Fall wird das WIF-Bit gesetzt, da eine Schreibadresse übertragen und damit ein Schreibzugriff initiiert worden ist.

Die Funktion TWIM_WriteHandler implementiert einen Zustandsautomaten, der die Übertragung der Daten übernimmt. Dazu wird als erstes überprüft, ob der Slave ein NACK gesendet hat. Wenn ja, wird die Übertragung abgebrochen und ein Fehlerzustand eingenommen.

Falls kein Fehler aufgetreten ist, wird der Zustandsautomat abgearbeitet. Als erstes wird geprüft ob sich der Automat im Zustand TWI_MASTER_REGISTER befindet. Wenn dies der Fall ist, soll als erstes eine Registeradresse übertragen werden. Dazu wird die Registeradresse in das DATA-Register des I2C-Moduls kopiert.

Da es sich beim Versenden der Registeradresse immer um einen Schreibzugriff handelt, entfällt eine zusätzliche Acknowledge-Logik. Der nächste Zustand ist dann davon abhängig, ob es sich um einen schreibenden oder um einen lesenden Zugriff handeln soll. In diesem Fall ist es ersteres, weshalb der Zustand TWI_MASTER_WRITE eingenommen wird.

Sobald die Registeradresse übertragen worden ist, wird der Interruptvektor erneut aufgerufen und der Inhalt des STATUS-Registers ein weiteres mal überprüft. Das Programm springt erneut in die Funktion TWIM_WriteHandler und arbeitet den Zustandsautomaten weiter ab. In diesem Durchlauf wird der Zustand TWI_MASTER_WRITE bearbeitet:

In diesem Zustand werden die einzelnen Datenbytes versendet und der Indexzeiger inkrementiert. Wenn alle Daten versendet worden sind, wird eine Stop-Kondition ausgelöst und der Zustand auf TWI_MASTER_SEND geändert. Damit ist die Übertragung abgeschlossen.

Der lesende Zugriff läuft ähnlich ab. Zuerst wird das I2C-Nachrichtenobjekt wieder mit Werten gefüllt:

Wichtig ist hier, dass zuerst die Schreibadresse in das ADDR-Registers geschrieben wird, da der Adresszeiger des I2C-Slaves noch gesetzt werden muss. Falls nur gelesen werden soll, ohne das der Adresszeiger geändert wird, werden folgen Werte verwendet:

Durch das Beschreiben des ADDR-Registers wird dann wieder die Übertragung gestartet. Falls eine Registeradresse übertragen werden soll, geschieht dies analog zum schreibenden Zugriff, außer das nach dem Senden der Registeradresse der Zustand TWI_MASTER_ADDRESS eingenommen wird, damit nach dem Senden der Registeradresse eine weitere Start-Kondition mit der Adresse für einen lesenden Zugriff gesendet wird.

Sobald die Leseadresse versendet worden ist, springt das I2C-Modul erneut in den Interruptvektor. Dieses mal wird allerdings das RIF-Bit gesetzt und die Funktion TWIM_ReadHandler aufgerufen:

Die Funktion TWIM_ReadHandler liest nun die einzelnen Datenbytes aus und erzeugt die entsprechende Bestätigung für den I2C-Slave.

Als erstes wird geprüft ob der aktuelle Leseindex größer ist als die Länge des Lesepuffers. Wenn dies der Fall ist, wird ein NACK mit einer Stop-Kondition gesendet und der Master in einen Fehlerzustand versetzt. Andernfalls wird das aktuelle Datenbyte aus dem DATA-Register ausgelesen, gespeichert und der Index inkrementiert. Der Empfang wird mit einem ACK quitiert. Sobald alle Daten empfangen worden sind, wird statt dem ACK ein NACK und eine Stop-Kondition gesendet und der Zustand TWI_MASTER_RECEIVED eingenommen. Damit ist die Übertragung abgeschlossen.

XMega als I2C-Slave:

Da der Master-Modus nun komplett ist, wollen wir uns mit dem Slave-Modus beschäftigen. Dieser Modus kann ebenfalls mit und ohne Interrupts realisiert werden. Da ich aber eine Realisierung ohne Interrupts für unnötig halte – warum sollte der Mikrocontroller so lange nichts tun, bis Daten an ihn gesendet werden? – zeige ich hier lediglich eine Implementierung des I2C-Slave über Interrupts. Das Prinzip kann dann, falls Bedarf besteht, auf eine Realisierung ohne Interrupts übertragen werden.

Für das Beispiel als I2C-Slave nutze ich beide I2C-Module des XMega, wobei das Modul TWIC als I2C-Master und das Modul TWIE als I2C-Slave konfiguriert wird.

Beide Module werden ausschließlich über Interrupts gesteuert und die Adresse des Slave-Moduls soll 0x40 lauten. Diese Adresse muss in das ADDR-Register des Slave-Moduls geschrieben werden. Über das ADDRMASK-Register können bestimmte Bits bei einem Vergleich der Adresse ausgeschaltet werden, sodass das Modul z. B. auf einen bestimmten Adressbereich reagiert. Diese Funktion soll hier nicht genutzt werden, weshalb das Register geleert wird.

Damit ist die Konfiguration des Slave-Moduls abgeschlossen. Auch für das Slave-Modul habe ich eine Verarbeitung mittels Zustandautomaten realisiert und in einem entsprechenden Objekt hinterlegt:

Dieses Objekt wird vor der ersten Nutzung initialisiert:

Sobald etwas auf dem Bus passiert wird der entsprechende Interruptvektor für den I2C-Slave aufgerufen. Auch hier wird zuerst das STATUS-Register des I2C-Moduls ausgelesen.

Für den Fall das ein Fehler auf dem I2C-Bus, eine Kollision oder sonstige Zustände auftreten wird ein Fehlerzustand eingenommen.

Wenn das APIF-Bit und das AP-Bit gesetzt sind, hat das Slave-Modul eine gültige Adresse empfangen. In diesem Fall wird der TWIS_AddressMatchHandler abgearbeitet, in dem die Zähler für die empfangenen und übertragenden Bytes auf null gesetzt und die Anfrage für den Master quitiert wird.

Sobald die Adresse des Masters bestätigt worden ist, beginnt der Master mit dem Transfer der Daten. Bei jedem Datenbyte wird das DIF-Flag im STATUS-Register gesetzt und der TWIS_DataHandler aufgerufen.

Diese Funktion ließt das STATUS-Register aus und bestimmt anhand des DIR-Bits ob es sich, seitens des Masters, um einen schreibenden oder einen lesenden Zugriff handelt. Wenn es sich um einen schreibenden Zugriff handelt, ist das Bit nicht gesetzt. Dann werden die empfangenen Bytes in den Datenpuffer vom Slave-Modul geschrieben und der entsprechende Empfangszähler erhöht.

Jedes empfangene Datenbyte wird durch ein Response-Kommando mit einem ACK bestätigt. Falls der Master zu viele Daten sendet und dadurch ein Pufferüberlauf entsteht, wird ein NACK gesendet, sodass der Master die Kommunikation beendet. Zudem wird ein entsprechender Fehlerstatus eingenommen.

Wenn es sich um einen lesenden Zugriff handelt (DIR = 1), werden die einzelnen Datenbytes aus dem Puffer in das DATA-Register des Moduls kopiert und versendet. Falls der Master mit einem NACK bestätigt hat (RXACK = 1) wird die Übertragung beendet. Auch hier wird wieder ein entsprechender Fehler ausgelöst, wenn es zu einem Pufferüberlauf kommt.

Sobald alle Daten vom Master gesendet, bzw. empfangen worden sind (oder es zu einem Fehler kommt), sendet dieser eine Stop-Kondition. In diesem Fall wird der TWIS_StopHandler aufgerufen, der in diesem Fall nur das entsprechende Interruptbit löscht:

Damit ist der I2C-Slave komplett implementiert und einsatzbereit. Das Programm kann sehr bequem mit einem XMega mit zwei I2C-Modulen getestet werden, da man lediglich eine Brücke zwischen SDA und SCL beider Module bilden muss (Pull-Up Widerstände nicht vergessen!).

Das komplette Projekt inkl. mehrerer Beispielanwendungen für den Master- und den Slave-Betrieb steht in meinem GitLab-Repository zum Download bereit.

Last Updated on

Schreibe einen Kommentar

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