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)