Liquid Crystal Display (kurz LCD)

Heute nehmen wir uns ein alt bewährtes, und noch sehr oft verbautes Display vor.

Es gibt eine Menge Anwendungen für Displays. Letztens erst war ich wieder an der Tankstelle meines Vertrauens.. dort ist mir aufgefallen, dass man jetzt auch mit Karte über ein Terminal zahlen kann (sprich draußen, wenn die Tanke geschlossen hat). Dort war ein 20 Zeichen a 4 Zeilen Display an dem Bezahlautomaten montiert.. Bei Kartenzahlungen an Terminals, in Geschäften (besonders Kassen), Regel / Steuereinheiten, Temperaturanzeigen usw. An die Displays kommt man schon ziemlich günstig ran.

www.ebay.de oder www.aliexpress.de sind da meine ersten Anlaufstellen.

Nun bekommt man bei der "Arduino IDE" schon einiges oder fast alles an Bibliotheken angeboten, ohne das man überhaupt weiß, was hinter den Kulissen passiert. Das wollen wir uns jetzt mal ein bisschen näher betrachten. Also möchte ich mich hier ein wenig von der „Arduino IDE“ distanzieren. Die Displays müssen mit Informationen versorgt werden, das sollte soweit klar sein?!Dazu kommen wir aber erst später.

Des weiteren muss gewährleistet sein, dass das Display die Informationen auch entsprechend verarbeiten kann. Wie funktioniert die Datenverarbeitung bei den Displays überhaupt? Ganz einfach! Bei den LCD´s haben sich einige Typen von Mikrokontrollern durchgesetzt. Text (Charakter) LCDs verwenden meist den HD44780 oder einen kompatiblen Kontroller (z.B KS0066). Einige Mikrokontroller, welche auf den Displays verbaut sind, unterscheiden sich in einigen physikalischen Merkmalen erheblich. Zum Beispiel ist das Timing vom „HD44780“ zum „KS0066“ deutlich anders. Dieses muss unbedingt beachtet werden, sonst können ab und zu oder auch dauerhaft Probleme auftreten. Datenblatt zum HD44780 Im obigen Link zum Datenblatt sind alle relevanten Informationen über den "HD44780" erwähnt. Der Mikrokontroller auf dem LCD sorgt für die Verarbeitung der ankommenden Daten und der Darstellung auf dem LCD.

Nun muss der LCD Mikrokontroller aber noch genau wissen, was er überhaupt machen soll. Die meisten Mikrokontroller sind untereinander einigermaßen kompatibel. Was die meisten Befehle oder Timings (Verarbeitungszeit der Daten.. etc.) betrifft.

Fangen wir mal an…

Als erstes konfigurieren wir die Ausgänge, die wir für die Kommunikation zwischen MEGA32 und LCD Mikrokontroller verwenden wollen. Dafür schreiben wir in das jeweilige „Daten Direction Register“ (DDRx). Das „x“ steht jeweils, für den von uns benutzen Port. Durch das setzen des Bits ( 1<< PAx), bezwecken wir das genau dieser Pin des MEGA32 als Ausgang fungieren soll.

     DDRA |= ( 1<< PA0 ); // Register Select Pin
     DDRA |= ( 1<< PA1 ); // Read Write Pin
     DDRA |= ( 1<< PA2 ); // Enable Pin
     DDRA |= ( 1<< PA3 ); // Data Bit Pin DB4
     DDRA |= ( 1<< PA4 ); // Data Bit Pin DB5
     DDRA |= ( 1<< PA5 ); // Data Bit Pin DB6
     DDRA |= ( 1<< PA5 ); // Data Bit Pin DB7

Im Folgenden gehen wir davon aus, dass es sich bei dem LCD Mikrokontroller um einen "HD44780" handelt und bei unserem Mikrokontroller um einen Mikrokontroller aus dem Hause Atmel / Microchip (ATMEGA32).

Das Display, was wir nutzen wollen ist ein "16x2" LCD. 16x2 bedeutet, dass wir 16 Zeichen a 2 Zeilen haben. Als aller erstes initialisieren wir den Mikrokontroller auf dem LCD. Dafür verwenden wir jetzt nur 4 Datenpins vom LCD Kontroller zum MEGA32. Dies kann man machen um Pins vom MEGA32 oder generell von irgendwelchen Mikrokontrollern zu sparen, es kann jedoch auch über 8 Leitungen (DB0 – DB7) kommuniziert werden. Das geht deutlich schneller und braucht nicht extra eine Funktion um die Daten über nur 4 Leitungen zu jagen.

Arbeiten tun wir jetzt aber wie eben schon erwähnt mit 4 Datenleitungen (DB4- DB7) die anderen Datenleitungen bzw. Eingänge vom LCD Mikrokontroller werden mithilfe eines PullDown Widerstandes auf „GND“ Potential gezogen, um auftretende Störungen an den Eingängen zu vermeiden. In dem jetzigen Beispiel nutzen wir ausschließlich den „PORTA“ unsers MEGA32. Beginnen wir damit dem Display Mikrokontroller beizubringen das mit 8 Bit Datenbreite gearbeitet  werden soll. Dafür setzen wir jetzt die jeweiligen Steuerpins / Datenpins auf "1"..
Wir gehen natürlich vorher davon aus, dass alle Datenpins die richtigen Pegel haben. Wir verodern ("|") die ganzen Bits. Es kann passieren das einige Pins noch einen anderen State ("high oder "low") haben. Daher ist immer darauf zu achten das wirklich die richtigen Pegel anliegen!.

     PORTA |= (1<<PA3 ); // DB4
     PORTA |= (1<<PA4 ); // DB5

Das ist die erste Vereinbarung, die wir mit dem Kontroller des LCD´s vereinbaren. Die ist so laut Datenblatt vorgegeben. Nun müssen wir ihm noch sagen, dass das Kommando vorhanden ist und er es jetzt einlesen und verarbeiten soll. Dies geschieht, indem wir den Steuerpin / Datenpin  „Enable“ drei Mal von „high“ nach „low“ schalten. Man spricht in der Fachsprache auch von einem „toggeln“.

     uint8_t enCnt = 0x00;
     for(enCnt = 0x00 ; enCnt < 3 ; enCnt++)
     {
          PORTA |= ( 1<<PA2 ); // setzt „Enable“ auf high
          _delay_ms(5); // timing ist vom LCD Kontroller abhänging
          PORTA &= ~( 1<<PA2 ); // setzt „Enable“ wieder auf low
     }

Kommen wir jetzt zu der Datenannahme. Wie bereits oben beschrieben, kann man einmal mit dem LCD Kontroller über 8 Datenleitungen (DB0 – DB7) reden / kommunizieren oder über 4 Datenleitungen (DB4 – DB7). ACHTUNG! Es werden immer die höherwertigen Datenpins (DB4-DB7) benutzt, wenn wir über nur 4 Datenleitungen kommunizieren wollen (Das ist im LCD Mikrokontroller nun mal so festgelegt..).

     PORTA &= ~( 1<<PA3 ): // schaltet den DB4 Datenpin auf logisch „0“
     PORTA |= ( 1<<PA4 ): // schaltet den DB5 Datenpin auf logisch „1“

Wieder sagen wir ihm, dass neue Daten vorhanden sind und er die Übernehmen soll.

     PORTA |= ( 1<<PA2 ); // setzt „Enable“ auf high
     _delay_ms(5); // timing ist vom LCD Kontroller abhänging
     PORTA &= ~( 1<<PA2 ); // setzt „Enable“ wieder auf low

Tipp: Damit wir nicht immer diese Codezeilen schreiben müssen, bauen wir uns für dieses Kommando „Datenübernahme“ einfach eine eigene Funktion die wir später nur noch aufrufen müssen und die das alles für uns übernimmt.

     void lcdToggleEnable(void)
     {
          PORTA |= ( 1<<PA2 ); // setzt „Enable“ auf high
          _delay_ms(5); // timing ist vom LCD Kontroller abhänging
          PORTA &= ~( 1<<PA2 ); // setzt „Enable“ wieder auf low
     }

Kommen wir nun zum spannenden Teil. Wir bauen uns eine Funktion, die uns die ganzen Informationen an den LCD Mikrokontroller sendet. Dies bräuchten wir nicht, wenn wir über 8 Datenleitungen kommunizieren würden. Da könnten wir einfach einen ganzen Port von unserem MEGA32 nutzen um die Daten 1 zu 1 auf den "LCD Bus" zu legen. Das spart uns zwar eine Menge Zeit und ein bisschen an Code aber im Endeffekt sind wir damit nicht mehr ganz so flexibel was die Belegung der Pins angeht. Gerade in einem in Platine eingegossenem Layout (wo die Belegung schon fest steht) kann es manchmal so sein, dass die Pins an verschiede Ausgänge gehen.

Die Funktion für die Datenübertragung von 1 Byte (8 Bits) in jeweils 4 Bit aufgeteilten „Datenpaketen“. Erwartet von uns nun zwei Parameter ->

dataByte = Daten die zu dem LCD Mikrokontroller gesendet werden sollen. cmdOrData = Soll ein Kommando oder sollen Nutzdaten übertragen werden?. Die beiden Parameter sind jeweils 8 Bit breit und können Werte von „0 – 255“ aufnehmen. Unsere Funktion sorgt dann für die jeweilige Aufteilung von „high“ bzw. „low“ Nibble.

     /* ist die variable == 1, wissen wir das wir ein kommando senden wollen */
     if(cmdOrData == 1)
     {
          PORTA |= ( 1<<PA0 ); // register select auf high
     }
      else /* ist es alles andere außer "1", wissen wir das wir daten senden wollen. */
     {
         PORTA &= ~( 1<<PA0 ); // register select auf low
     }

     /* hier konfigurieren wir wieder die ausgänge für die datenpins.. es kann durchaus mal sein
     das man in seinem weiteren code diese pins für was anderes nutzen möchte da werden sie dann evtl. als eingänge
     konfiguriert und später beim schreiben, funktioniert nichts mehr. wenn man sich jedoch sicher ist, dass man die pins
     im weiteren programm nicht mehr anfassen tut, kann man diese zeilen auch raus schmeißen */
     DDRA |= ( 1<< PA3 ); // Data Bit Pin DB4
     DDRA |= ( 1<< PA4 ); // Data Bit Pin DB5
     DDRA |= ( 1<< PA5 ); // Data Bit Pin DB6
     DDRA |= ( 1<< PA5 ); // Data Bit Pin DB7
        
     /* hier legen wir noch mal eine startbedingung fest.. sorgen also dafür das die pins vorher alle auf "0" geschaltet werden,
     damit keine falschen daten übermittelt werden, durch noch falsch geschaltete pins     */
     PORTA &= ~( 1<< PA4 );
     PORTA &= ~( 1<< PA5 );
     PORTA &= ~( 1<< PA6 );
     PORTA &= ~( 1<< PA7 );
    
     /* hier wird das daten byte abgefragt und auf 4 bit aufgeteilt (high nibble)
     da wir ja nur mit 4 bit bus breite arbeiten, müssen wir erst das "high nibble" und dann das "low nibble"
     schicken.. */
     if(dataByte & 0x80) PORTA |= ( 1<< PA4 );
     if(dataByte & 0x40) PORTA |= ( 1<< PA5 );
     if(dataByte & 0x20) PORTA |= ( 1<< PA6 );
     if(dataByte & 0x10) PORTA |= ( 1<< PA7 );  
     
     /* daten stehen bereit und können übernommen werden */
     lcdToggleEnable();
        
     /* hier das gleiche spiel wie oben mit den default pegeln auf dem lcd bus*/
     PORTA &= ~( 1<< PA4 );
     PORTA &= ~( 1<< PA5 );
     PORTA &= ~( 1<< PA6 );
     PORTA &= ~( 1<< PA7 );
    
     /* hier wird der andere teil des daten bytes abgefragt "low nibble" und auf den bus gelegt */
     if(dataByte & 0x08) PORTA |= ( 1<< PA4 );
     if(dataByte & 0x04) PORTA |= ( 1<< PA5 );
     if(dataByte & 0x02) PORTA |= ( 1<< PA6 );
     if(dataByte & 0x01) PORTA |= ( 1<< PA7 );
    
     /* daten stehen bereit und können übernommen werden */
     lcdToggleEnable();    
        
     /* und hier nochmal die gleiche default einstellung wie oben.. damit wir sichergehen können das auf dem
     bus kein blödsinn passiert */
     PORTA |= ( 1<< PA4 );
     PORTA |= ( 1<< PA5 );
     PORTA |= ( 1<< PA6 );
     PORTA |= ( 1<< PA7 );

Das war der erste Schritt um dem Display Nutz bzw. Kommandos zu senden.

Puuh... Wenn du es bis hier hin Verstanden hast, bist du schon mal einen großen Schritt weiter als vorher!

Können wir das LCD jetzt konfigurieren? Ja! Jetzt ist es nicht mehr viel, was wir noch machen müssen.. Übergeben wir unserer Funktion die wir gerade geschrieben haben (diejenige die Daten oder Kommandos verarbeiten kann..) jetzt folgende Parameter.. Ich nenne die Funktion einfach mal void lcdWriteDataOrCmd(uint8_t dataByte, uint8_t cmdOrData)..

     lcdWriteDataOrCmd(0x28,1); // kommando! -> 4 bit mode & 2 lines
     lcdToggleEnable(); // daten übernehmen
     lcdWriteDataOrCmd(0x08,1); // kommando! -> lcd display off
     lcdToggleEnable(); // daten übernehmen
     lcdWriteDataOrCmd(0x00,1); // kommando! -> lcd clear
     lcdToggleEnable(); // daten übernehmen
     lcdWriteDataOrCmd(0x0F,1); // kommando! -> lcd display on & cursor on

In dem Beispiel habe ich jetzt das Steuersignal "RW - ReadWrite" bewusst nicht implementiert. Dies kann man tun, wenn man Wartezeiten vermeiden möchte. Im Allgemeinen möchte man ja nur das dass LCD Daten empfangen kann. Dieses Steuersignal könnte man an einem Pin vom MEGA32 legen (vorher als Eingang konfigurieren..!) um das Flag zu pollen (ständig abzufragen).

Das Signal zeigt einem an, ob der LCD Mikrokontroller gerade noch den Befehl bearbeitet oder ob er damit schon fertig ist. Hier haben sich ein paar Millisekunden so im Bereich von etwa ~50 Millisekunden erwiesen. Damit umgehen wir das Auswerten des Signales und sparen uns einen weiteren Pin ein.

Wer jedoch nicht unnötig lange warten möchte, obwohl der LCD Mikrokontroller schon alle Daten verarbeitet hat, kann das Signal natürlich ständig nach dem Daten senden abfragen und ggf. warten bis es auf "0" gefallen ist. Ist dieses „Bit“ auf „0“ gegangen, können wir weitere Daten senden.

Würden wir nun das ganze noch in einem "C" tauglichem Syntax packen, würden wir damit schon unser LCD in Betrieb nehmen können.

Die Funktionen dafür folgen..!

 

/* Hier schön zu sehen, der Datentransfer */

P.S Wer dies schon getan hat und oder es noch vor hat, was weiß ich.. und es will einfach nichts auf dem LCD erscheinen, außer schwarze Balken.., der hat evtl. die Kontrastspannung nicht richtig eingestellt (betreibt es also außerhalb des erlaubten Bereiches!) oder die Initialisierung ist schief gelaufen.

Sollte die Kontrastspannung so sein, wie laut Datenblatt vorgegeben, schau dir einfach noch mal die Festlegung deiner Pins an.

Dies könnte dann so aussehen.

Es muss aber nicht die Kontrastspannung sein. Es kann auch was in der Initalisierungsphase falsch gelaufen sein. Falsche definition der Ausgänge? Falsche Anschlussbelegung?

 

Hier ist einer von vielen Möglichen Verdrahtungsplänen den man nutzen könnte um solch ein Display an seine eigene Applikation anzubinden.

 

 

Diese LCD Anzeigen und deren Ansteuerung sind aber schon ziemlich "Old School".
Inzwischen gibt es schon "OLED" Anzeigen. Diese bestehen aus Organischen LEDs und benutzen eine ganz andere Technik als die hier erwähnten Crystal Displays.

Doch auch die Technik der normalen LCD Displays ist nicht ganz stehen geblieben. Inzwischen gibt es auch LCD´s die man über einen gängigen SPI oder I2C Bus ansteuern kann. Dies ermöglicht uns weitere Einsparungen an Pins. Nur der Aufwand an Code ist bei manchen Modulen ein wenig mehr geworden.

Hier könnt ihr euch das ganze Geschehen noch einmal detailliert (hoffentlich..) anschauen.

lcdInit_PAP.pdf (75,44 kb)

lcdWrite_PAP.pdf (64,22 kb)

Logische Operatoren

 In den hier gezeigten Beispielen gehen wir mal von folgenden Situationen aus..
Wir nutzen die Variablen x = 5, y = 4, z = 0;

+ Addierungs Operator   (x + y oder x+=y)
- Subtrahierungs Operator  (x - y oder x-=y)
* Multiplikations Operator  (x * y oder x*=y)
/ Divisions Operator  (x / y oder x/=y)
 
Hier muss ich nicht weiter drauf eingehen (glaube ich!)..
 
Hier mal die Übersicht einiger Operatoren die in der Programmiersprache "C" wieder zu finden sind.

Der UND Operator 

0b11110000
0b10001111
&--------------------
0b10000000 // Ergebnis

Wie man sieht, müssen zwei gleiche Zustände oder Bits ("0" oder "1") gleich sein
damit man im Ergebnis auch eine "1" setzen kann. Ist dies nicht der Fall, wird eine
"0" gesetzt.

 

Der ODER Operator 

0b11110001
0b00001111
|--------------------
0b11111111 // Ergebnis

Auf dem ersten Blick erkennen wir, dass mit dem ODER Operator jedes Bit in einem Byte gesetzt werden kann,
unabhängig vom vorherigem Zustand.
  

Der XOR(EXKLUSIV ODER) Operator 

0b10101010
0b01010101
^--------------------
0b11111111 // Ergebnis (bsp. 1)
  
0b10101111
0b01010101
^--------------------
0b11111010 // Ergebnis (bsp. 2)

Der "^" Operator ist das Gegenteil vom "|" Operanten. Hier müssen sich die Zustände unterscheiden, damit ein Bit gesetzt werden kann.

Der NEGATIONS Operator 

0b00000000
~--------------------
0b11111111 // Ergebnis

Der "~" Operator dreht einfach die Werte "um". Ist es vorher eine "1" so ist es nach dem ausführen des Operators eine "0". Es werden die Zustände einfach umgedreht.

Es folgen Scripts zur dynamischen Berechnung!

----------------------------------------------------------------------------

Um bei einer Variablen ein Bit zu setzen gibt es mehrere Möglichkeiten. Hier stelle ich mal einige davon vor..

uint8_t tmp = 0; // eine einfache variable, bestehend aus 8 Bit (1 Byte)
Bits schieben  -> tmp = (1<<7)
Hexadezimal    -> tmp = 0x80
Dezimal        -> tmp = 128 
Binär          -> tmp = 0b10000000

Mit jeder dieser Schreibweisen, kann man zum gleichen Ergebnis kommen. Es wird jeweils das 7 Bit in einem Byte (in diesem Fall "tmp") gesetzt. Die anderen bleiben "0"

Nun haben wir in der Variablen das 7 Bit gesetzt. Wie bekommen wir ein weiteres Bit gesetzt ohne das andere mit überschrieben werden? Das kann passieren! Vorsicht! Dort hatte ich am Anfang auch meine Probleme.

Hier gibt es auch wieder verschiedene Möglichkeiten..

Bits schieben -> tmp |= (1<<6)
Hexadezimal   -> tmp |= 0x40
Dezimal       -> tmp |= 64
Binär         -> tmp |= 0b01000000

In diesem Beispiel wurde der "|" Operator eingesetzt. Damit haben wir, im Endeffekt ein Bit hinzugefügt. Es gibt auch noch andere Schreibweisen. Das ist Geschmackssache. Ich empfehle für den Anfang die "Binär" Schreibweise.

Wie aber lösche ich jetzt ein Bit ohne die anderen in Mitleidenschaft gezogen werden?

Bits schieben  -> tmp &= ~(1<<6)
Hexadezimal    -> tmp &= ~(0x40)
Dezimal        -> tmp &= ~(64)
Binär          -> tmp &= ~(0b01000000)

Hier kommen gleich zwei Operatoren ins Spiel. Einmal der "&" Operator und der "~" Operator.
Als erstes wird das ganze Byte "maskiert". Das bedeutet eigentlich nur das die anderen Bits nicht mit in die Rechnung einbezogen werden, bis auf das was hinter dem "=" Zeichen steht. Das nennt man "maskieren". Das Bit wird jetzt mit der aktuellen Variablen "tmp" verundet (tmp&=..). Da vorher in der Variablen "tmp" das Bit gesetzt war und jetzt auch wieder gesetzt ist (1<<6) bekommen wir in diesem ersten Teilergebnis eine "1" (jetzt mal in Binärer Schreibweise 0bx1xxxxxx). Hinter dem "=" kommt dann direkt der nächste Operator. Hier wird das Bit einfach umgedreht. Das bedeutet im Endeffekt eigentlich nur das wir aus der Logischen "1" eine "0" gemacht haben.

In der Variablen "tmp" steht jetzt folgender Wert ->

tmp = 0b10000000.

Das "6" Bit (1<<6) haben wir maskiert und negiert ohne das die Vorherigen Bits in diesem Fall das "7" Bit (1<<7) gelöscht oder überschrieben wurde.

Ich hoffe das war ein wenig Verständlich?

 

 

Fachbegriffe aus der Mikrocontroller Welt - I2C / TWI

Kommunikation

  • I2C (oder auch TWI - Two Wire Interface)

Der I2C Bus ist ein synchroner serieller 2-Draht Bus, der in den 80er Jahren von Philips entwickelt wurde. I2C gesprochen 'I Quadrat C' kommt von der der Abkürzung IIC und bedeutet Inter-Integrated Circuit. Er wird hauptsachlich dazu benutzt, zwischen Schaltkreisen, die sich auf einer Platine verbinden, Daten auszutauschen. Die beiden Leitungen, die den I2C Bus bilden heißen SCL und SDA. SCL steht für Signal Clock und ist die Taktleitung für den Bus. Deshalb spricht man auch von einem synchronen Bus. SDA steht für Signal Data und ist die Datenleitung. Die Datenübertragungsrate des I2C Busses beträgt 100kHz im Standard Mode, bzw. 400kHz im Fast Mode. Aus Lizenzgründen nennt man das I2C Interface bei Atmel TWI (Two Wire Interface).

Der I2C Bus ist ein Multi Master/Slave Bus. Das bedeutet, es gibt mindestens einen I2C Master und ebenso mindestens einen I2C Slave. Der Master selektiert einen Slave durch seine Slave Adresse, die innerhalb eines Busses eindeutig sein muß. Eine Datenübertragung kann nur durch einen I2C Master initiiert werden. Der Slave bleibt immer passiv und lauscht nur auf die Slave Adresse und vergleicht diese mit seiner eigenen Slave Adresse. Erst wenn er seine Slave Adresse erkennt, greift der Slave auch aktiv in das Busgeschehen ein.

Aus Sicht des I2C Masters unterscheidet man zwischen Read und Write Sequenzen. Bei einer Read Sequenz liest der I2C Master Daten vom I2C Slave. Bei einer Write Sequenz sendet der I2C Master Daten zum Slave.

Wie man sich denken kann, gibt es verschiedene Bus Zustände

Durch die Signal Pegel der beiden I2C Bus Leitungen SCL und SDA ergeben sich verschiedene Zustände, die der Bus einnehmen kann.

 

Zustand Nummer 1.: Bus ist frei

Wenn SCL und SDA dauerhaft HIGH sind, spricht man von 'Bus free'. Ein I2C Master muß diese Bedingung immer zuerst abprüfen, bevor er den Bus belegen darf.

 

Zustand Nummer 2.: Start Bedinung (Übertragung kann beginnen..)

Die Start Bedingung kennzeichnet den Beginn einer Datenübertragung durch einen I2C Master. Der Master zieht die Datenleitung SDA von HIGH auf LOW, während die Taktleitung SCL auf HIGH bleibt.

 

Zustand Nummer 3.: Datenbit (Logische Informationen, Digitale Informationen)

Ein Datenbit kann, wie in der Digitaltechnik üblich 2 Zustände einnehmen '0' oder '1'. Die Daten sind gültig während die Taktleitung SCL auf HIGH liegt. Ein LOW Pegel auf der Datenleitung SDA bedeuted '0', ein HIGH bedeutet '1'

 

Zustand Nummer 4.: Acknowledge

Bei einer Schreib Sequenz quittiert der I2C Slave nach Erkennen seiner Slave Adresse bzw. nach jedem geschriebenen Daten Byte mit einem Acknowledge. Bei einer Lese Sequenz quittiert der I2C Slave nach Erkennen seiner Slave Adresse mit Acknowledge. Nach jedem gelesenen Daten Byte quittiert der I2C Master mit einem Acknowledge dem Slave, dass er bereit ist, weitere Daten zu empfangen. Dabei wird zu einem ebenfalls vom Slave generierten Takt Impuls die Daten Leitung auf LOW gehalten.

 

Zustand Nummer 5.: No Acknowledge

Bei einer Lese Sequenz sendet der I2C Master nach dem Lesen des letzten Daten Byte ein No Acknowledge. Das bedeutet, er möchte keine weiteren Daten mehr lesen. Dabei wird zu einem ebenfalls vom Slave generierten Takt Impuls die Daten Leitung auf HIGH gehalten.

 

Zustand Nummer 6.: Stopp Bedingung

Die Stop Bedingung kennzeichnet das Ende einer Datenübertragung durch einen I2C Master. Der Master zieht die Datenleitung SDA von LOW auf HIGH, während die Taktleitung SCL auf HIGH bleibt.

 

Addressierung

Nun gibt es bei einem "Bus System" ja meistens mehrere Teilnehmer die mit Informationen versorgt werden wollen. Stellen wir uns einmal vor, wir haben auf unserer Leiterplatte einen Temperatursensor und einen Datenspeicher wie zum Beispiel einen "EEProm " oder einen "Flash".

Nun hängen die beiden IC´s an den gleichen Leitungen.. Was nun? Woher weiß jetzt wer, wer was liefern oder speichern soll? Genau dafür gibt es eine Adressierungs Sequenz.

I2C Slave Adressen sind 7-Bit Adressen. Damit lassen sich an einem Bus bis zu 128 Geräte betrieben. Sofern jeder über eine verschiedene I2C Adresse verfügt. Die wird oft mit einem oder mehreren Bits Hardwaremäßig eingestellt. Einige Hersteller bieten dafür Pins mit den Bezeichnungen "A0-An" an. Je nachdem welche Pegel dort anliegen (low oder high), ändert sich die Adresse. Es werden meistens noch Widerstände an die Eingänge angeschlossen. PullUps oder PullDowns in Berreich von einigen kOhm (10 - 100k).

Beim Senden eines I2C Befehls folgt nach der 7-Bit Adresse als letztes und niederwertigstes Bit die Schreib/Lese Kennung. Damit kennzeichnet der I2C Master die Richtung des Datentransfers. Eine '1' steht für Lesen, das heißt der Master will vom Slave lesen. Eine '0' dagegen bedeutet, der Master will schreiben.

 

Hier sieht man wie aus einer 7 Bit Adresse mit einem lese / Schreibbit versehen wird.
Möchten wir jetzt dem Busteilnehmer (nehmen wir einfach mal den Temperatursensor..) sagen, dass er uns doch die aktuellen Werte für die Temperatur übermitteln soll, so legen wir jetzt den sogenannten "read/write Header" auf den Bus. Der beinhaltet die Adresse und das jeweilige Bit für lesen.

Die Adresse von dem Temperatur IC ist "0x09" + das Datenbit zum lesen (0x01).
In C, würde eine von vielen Möglichkeiten so aussehen..

#define I2C_READ                  (0x01)
#define I2C_WRITE                (0x00)
#define TEMP_SENS_ADDR     (0x09 << 1)

#define TEMP_SENS_ADDR_READ     (TEMP_SENS_ADDR + I2C_READ)
#define TEMP_SENS_ADDR_WRITE   (TEMP_SENS_ADDR + I2C_WRITE)

Daten Übertragen.:
******************************************************************

uint8_t receive=0;

i2c_start_wait(TEMP_SENS_ADDR_READ); // sendet den read header
receive = i2c_read_Nack(); // ließt ein byte und sagt dem sensor das er keine weiteren haben möchte..
i2c_stop(); // sagt dem Slave das die übertragung beendet ist
**************************************************************************

Wenn alles geklappt hat, finden wir jetzt unsere Date in der Variablen "receive".

Das alles kleines Beispiel..

Habt ihr Fehler gefunden oder Anmerkungen? Hinterlasst ein Kommentar. Danke!