AVR-Tutorial: LCD: Unterschied zwischen den Versionen

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche
Keine Bearbeitungszusammenfassung
K (→‎Anschluss an den Controller: alternative Bezeichnungen Vdd und V0 ergänzt, Kleinigkeiten)
 
(39 dazwischenliegende Versionen von 10 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
Kaum ein elektronisches Gerät kommt heutzutage noch ohne ein LCD daher. Ist doch auch praktisch, Informationen im Klartext anzeigen zu können, ohne irgendwelche LEDs blinken zu lassen. Kein Wunder also, dass die häufigste Frage in Mikrocontroller-Foren ist: "Wie kann ich ein LCD anschließen?"
Kaum ein elektronisches Gerät kommt heutzutage noch ohne ein LCD daher. Ist doch auch praktisch, Informationen im Klartext anzeigen zu können, ohne irgendwelche LEDs blinken zu lassen. Kein Wunder also, dass die häufigste Frage in Mikrocontroller-Foren ist: „Wie kann ich ein LCD anschließen?


==Das LCD und sein Controller==


<gallery>
Die meisten Text-LCDs verwenden den Controller '''[[HD44780]]''' oder einen kompatiblen (z.&nbsp;B. KS0070) und haben 14 oder 16 Pins.
C:\Users\it131-flores\Desktop\unbenannt.png
</gallery>
==Fliegenpilz==


Der Fliegenpilz '''''(Amanita muscaria)''''' ist eine Pilzart aus der Familie der Wulstlingsverwandten. Er ist giftig. Die auch als Roter Fliegenpilz bezeichnete Spezies erscheint in Mitteleuropa von Juni bis zum Winter, hauptsächlich von Juli bis Oktober.
Die Pinbelegung ist meist (Ausnahme z.&nbsp;B. TC1602E (Pollin 120420): V<sub>DD</sub> und V<sub>SS</sub> vertauscht) folgendermaßen:


Es existieren mehrere anerkannte Varietäten ''(Varianten)'': Der Fliegenpilz im eigentlichen Sinn ist die Typusvarietät muscaria. Dagegen hat die Varietät aureola häufig keine Flocken auf dem Hut, aber eine häutige Scheide. Die var. formosa besitzt einen orangegelben Hut mit spärlichen gelben Flocken. Beide zuletzt genannten Varietäten verursachen die gleichen Vergiftungserscheinungen wie die Typusvarietät des Fliegenpilzes. Der braune Königs-Fliegenpilz '''''(Amanita regalis)''''' wird von einigen Autoren nicht als eigenständige Art anerkannt, sondern ebenfalls als Varietät des Roten Fliegenpilzes geführt[1] – die Fruchtkörper ähneln äußerlich sehr stark dem Pantherpilz.
<span style="color:red">ACHTUNG: Es gibt Displays mit abweichender Anschluss-Belegung, falscher Anschluss kann zur Zerstörung führen! Daher immer das zugehörige Datenblatt zu Rate ziehen!</span>
 
==New topic==
lel
nicht so lel
 
 
 
 
 
Einzelheiten unter [http://www.mikrocontroller.net/articles/HD44780 Artikel zum Controller HD44780]


Einzelheiten im [[HD44780|Artikel zum Controller HD44780]].


<center>
<center>
Zeile 29: Zeile 18:
|align="center" |  1
|align="center" |  1
|align="center" |  V<sub>SS</sub> (selten: V<sub>DD</sub>)
|align="center" |  V<sub>SS</sub> (selten: V<sub>DD</sub>)
||  GND (Amina: +10 V)
||  GND (selten: +5 V)
|-
|-
|align="center" |  2
|align="center" |  2
|align="center" |  V<sub>DD</sub> (selten: V<sub>SS</sub>)
|align="center" |  V<sub>DD</sub> (selten: V<sub>SS</sub>)
||  +5 V (Lipp: GND)
||  +5 V (selten: GND)
|-
|-
|align="center" |  3
|align="center" |  3
Zeile 41: Zeile 30:
|align="center" |  4
|align="center" |  4
|align="center" |  RS
|align="center" |  RS
||  Rene Kersges Select (0=Befehl/Status 1=Daten)
||  Register Select (0=Befehl/Status 1=Daten)
|-
|-
|align="center" |  5
|align="center" |  5
Zeile 94: Zeile 83:
</center>
</center>


Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.
Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin-1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.


Bei der DIL-Version (2x7, 2x8 Kontakte) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4  usw. vertauscht. Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es NICHT kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.
Bei der DIL-Version (Wannenstecker mit 2×7 oder 2×8 Kontakten) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4  usw. vertauscht (so z.&nbsp;B. beim ANAG VISION AV1623YFTY-SJW, Pollin 121714). Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es ''nicht'' kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.


Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.
Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.


V<sub>SS</sub> wird ganz einfach an GND angeschlossen und V<sub>CC</sub>=V<sub>DD</sub> an +5 V. V<sub>EE</sub> = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10k&Omega;-Potentiometer zwischen GND und 5 V schalten, mit dem Schleifer an V<sub>EE</sub>. Meist kann man den +5 V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:
V<sub>SS</sub> wird ganz einfach an GND angeschlossen und V<sub>CC</sub>=V<sub>DD</sub> an +5&nbsp;V. V<sub>EE</sub> = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10-kΩ-Potentiometer zwischen GND und 5&nbsp;V schalten, mit dem Schleifer an V<sub>EE</sub>. Meist kann man den +5-V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:


[[Bild:LCD_Vee.gif|framed|center| Gewinnung der Kontrastspannung]]
[[Bild:LCD_Vee.gif|framed|center|Gewinnung der Kontrastspannung]]


Wenn der Kontrast zu schwach sein sollte (z.B. bei tiefen Temperaturen oder bei Betrieb mit 3.3V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis -5 V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.
Wenn der Kontrast zu schwach sein sollte (z.&nbsp;B. bei tiefen Temperaturen oder bei Betrieb mit 3.3&nbsp;V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis −5&nbsp;V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.


Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den '''8-Bit-''' und den '''4-Bit-'''Modus.
Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den '''8-Bit'''- und den '''4-Bit'''-Modus.
* Für den '''8-Bit-Modus''' werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
* Für den '''8-Bit-Modus''' werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
* Der '''4-Bit-Modus''' verwendet nur die oberen vier Datenleitungen ('''DB4-DB7'''). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige '''"Nibble"''' (= 4 Bits), also Bit 4 bis Bit 7 übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z.&nbsp;B. vom KS0070).
* Der '''4-Bit-Modus''' verwendet nur die oberen vier Datenleitungen ('''DB4–DB7'''). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige ''Nibble'' (= 4 Bits), also Bit 4 bis Bit 7, übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z.&nbsp;B. vom KS0070).


Der 4-Bit-Modus hat den Vorteil, dass man 4 IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.
Der 4-Bit-Modus hat den Vorteil, dass man vier IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.


Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse '''RS''', '''RW''' und '''E''' benötigt.  
Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse '''RS''', '''RW''' und '''E''' benötigt.


* Über '''RS''' wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS Low, dann wird das ankommende Byte als Befehl interpretiert; Ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).  
* Über '''RS''' (Register Select) wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS low, dann wird das ankommende Byte als Befehl interpretiert; ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).
* '''RW''' legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das '''Busy-Flag''' an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z.&nbsp;B. den Inhalt des Displays lesen - was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch ein IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50 µs (beim Return Home 2 ms, beim Clear Display 20 ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
* '''RW''' (Read/Write) legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das '''Busy-Flag''' an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z.&nbsp;B. den Inhalt des Displays lesen was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch einen IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50&nbsp;µs (beim Return Home 2&nbsp;ms, beim Clear Display 20&nbsp;ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
* Der '''E''' Anschluss schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten von den Datenleitungen bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten / Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.
* Der Anschluss '''E''' (Enable) schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten bzw. den Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.


==Heute gehts um Penis==
== Anschluss an den Controller ==


Jetzt, da wir wissen, welche Anschlüsse das LCD benötigt, können wir das LCD mit dem Mikrocontroller verbinden:


lel
<span style="color:red">ACHTUNG: Es gibt Displays mit abweichender Anschluss-Belegung (z.&nbsp;B. TC1602E, Pollin 120420: Vdd und Vss vertauscht), falscher Anschluss kann zur Zerstörung führen! Daher immer das zugehörige Datenblatt zu Rate ziehen.</span>
 
 
 
 
 
Einzelheiten unter [http://www.mikrocontroller.net/articles/HD44780 Artikel zum Controller HD44780]


Einzelheiten im [[HD44780|Artikel zum Controller HD44780]].


<center>
<center>
{| {{Tabelle}}
{| {{Tabelle}}
|-  style="background-color:#ffddcc"
|-  style="background-color:#ffddcc"
! Pin # || Bezeichnung || Funktion
!Pinnummer<br />LCD || Bezeichnung || Anschluss
|-
|-
|align="center" | 1
| align="center" |1 || V<sub>SS</sub> || GND (beim TC1602E: V<sub>CC</sub>)
|align="center" | V<sub>SS</sub> (selten: V<sub>DD</sub>)
||  GND (selten: +5 V)
|-
|-
|align="center" | 2
| align="center" |2 || V<sub>DD</sub> = V<sub>CC</sub> || +5 V (beim TC1602E: Gnd)
|align="center" | V<sub>DD</sub> (selten: V<sub>SS</sub>)
|| +5 V (selten: GND)
|-
|-
|align="center" | 3
| align="center" |3 || V0 = V<sub>EE</sub> || GND, [[Potentiometer|Poti]] oder [[Pulsweitenmodulation|PWM]] am AVR
|align="center" |  V<sub>EE</sub>, V0, V5
|| Kontrastspannung (-5 V / 0 V bis 5 V)
|-
|-
|align="center" | 4
| align="center" |4 || RS || PD4 am AVR
|align="center" | RS
|| Register Select (0=Befehl/Status 1=Daten)
|-
|-
|align="center" | 5
| align="center" |5 || RW || GND
|align="center" | RW
|| 1=Read 0=Write
|-
|-
|align="center" | 6
| align="center" |6 || E || PD5 am AVR
|align="center" | E
|| 0=Disable 1=Enable
|-
|-
|align="center" | 7
| align="center" |7 || DB0 || nicht angeschlossen
|align="center" | DB0
|| Datenbit 0
|-
|-
|align="center" | 8
| align="center" |8 || DB1 || nicht angeschlossen
|align="center" | DB1
|| Datenbit 1
|-
|-
|align="center" | 9
| align="center" |9 || DB2 || nicht angeschlossen
|align="center" | DB2
|| Datenbit 2
|-
|-
|align="center" | 10
| align="center" |10 || DB3 || nicht angeschlossen
|align="center" | DB3
|| Datenbit 3
|-
|-
|align="center" | 11
| align="center" |11 || DB4 || PD0 am AVR
|align="center" | DB4
|| Datenbit 4
|-
|-
|align="center" | 12
| align="center" |12 || DB5 || PD1 am AVR
|align="center" | DB5
|| Datenbit 5
|-
|-
|align="center" | 13
| align="center" |13 || DB6 || PD2 am AVR
|align="center" | DB6
|| Datenbit 6
|-
|-
|align="center" | 14
| align="center" |14 || DB7 || PD3 am AVR
|align="center" | DB7
|| Datenbit 7
|-
|-
|align="center" | 15
| align="center" |15 || A || Vorsicht! Meistens nicht direkt an +5 V anschließbar,<br /> sondern nur über einen Vorwiderstand, der an die Daten<br />der Hintergrundbeleuchtung angepasst werden muss.
|align="center" | A
|| LED-Beleuchtung, meist Anode
|-
|align="center" |  16
|align="center" |  K
||  LED-Beleuchtung, meist Kathode
|-
|-
| align="center" |16 || K || GND
|}
|}
</center>
</center>


Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.
OK. Alles ist verbunden. Wenn man jetzt den Strom einschaltet, sollten ein oder zwei schwarze Balken auf dem Display angezeigt werden.


Bei der DIL-Version (2x7, 2x8 Kontakte) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4  usw. vertauscht. Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es NICHT kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.
Doch wie bekommt man jetzt die Befehle und Daten in das Display? Dazu muss das LCD initialisiert werden und man muss Befehle (Commands) und seine Daten an das LCD senden. Weil die Initialisierung ein Spezialfall der Übertragung von Befehlen ist, im Folgenden zunächst die Erklärung für die Übertragung von Werten an das LCD.


Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.
== Ansteuerung des LCDs im 4-Bit-Modus ==


V<sub>SS</sub> wird ganz einfach an GND angeschlossen und V<sub>CC</sub>=V<sub>DD</sub> an +5 V. V<sub>EE</sub> = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10k&Omega;-Potentiometer zwischen GND und 5 V schalten, mit dem Schleifer an V<sub>EE</sub>. Meist kann man den +5 V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:
Um ein Byte zu übertragen, muss man es erstmal in die beiden Nibbles zerlegen, die getrennt übertragen werden. Da das obere Nibble (Bit 4 … Bit 7) als erstes übertragen wird, die vier Datenleitungen jedoch an die vier unteren Bits des Port D angeschlossen sind, muss man die beiden Nibbles des zu übertragenden Bytes erstmal vertauschen. Der AVR kennt dazu praktischerweise einen eigenen Befehl:


[[Bild:LCD_Vee.gif|framed|center| Gewinnung der Kontrastspannung]]
<syntaxhighlight lang="asm">
          swap r16              ; vertauscht die beiden Nibbles von r16
</syntaxhighlight>


Wenn der Kontrast zu schwach sein sollte (z.B. bei tiefen Temperaturen oder bei Betrieb mit 3.3V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis -5 V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.
Aus 0b00100101 wird so z.&nbsp;B. 0b01010010.


Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den '''8-Bit-''' und den '''4-Bit-'''Modus.
Jetzt sind die Bits für die erste Phase der Übertragung an der richtigen Stelle. Trotzdem wollen wir das Ergebnis nicht einfach so mit <code>out PORTD, r16</code> an den Port geben. Um die Hälfte des Bytes, die jetzt nicht an die Datenleitungen des LCDs gegeben wird, auf null zu setzen, verwendet man folgenden Befehl:
* Für den '''8-Bit-Modus''' werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
* Der '''4-Bit-Modus''' verwendet nur die oberen vier Datenleitungen ('''DB4-DB7'''). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige '''"Nibble"''' (= 4 Bits), also Bit 4 bis Bit 7 übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z.&nbsp;B. vom KS0070).


Der 4-Bit-Modus hat den Vorteil, dass man 4 IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.
<syntaxhighlight lang="asm">
          andi r16, 0b00001111  ; Nur die vier unteren (mit 1 markierten)
                                  ; Bits werden übernommen, alle anderen werden null.
</syntaxhighlight>


Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse '''RS''', '''RW''' und '''E''' benötigt.  
Also: Das obere Nibble wird erst mit dem unteren vertauscht, damit es unten ist. Dann wird das obere (das wir jetzt noch nicht brauchen) auf null gesetzt.


* Über '''RS''' wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS Low, dann wird das ankommende Byte als Befehl interpretiert; Ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).
Jetzt müssen wir dem LCD noch mitteilen, ob wir Daten oder Befehle senden wollen. Das machen wir, indem wir das Bit, an dem RS angeschlossen ist (PD4), auf 0 (Befehl senden) oder auf 1 (Daten senden) setzen. Um ein Bit in einem normalen Register zu setzen, gibt es den Befehl <code>sbr</code> (Set Bits in Register). Dieser Befehl unterscheidet sich jedoch von <code>sbi</code> (das nur für IO-Register gilt) dadurch, dass man nicht die Nummer des zu setzenden Bits angibt, sondern eine Bitmaske. Das geht so:
* '''RW''' legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das '''Busy-Flag''' an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z.&nbsp;B. den Inhalt des Displays lesen - was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch ein IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50 µs (beim Return Home 2 ms, beim Clear Display 20 ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
* Der '''E''' Anschluss schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten von den Datenleitungen bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten / Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.


==Heute gehts um Penis==
<syntaxhighlight lang="asm">
          sbr r16, 0b00010000    ; Bit 4 setzen, alle anderen Bits bleiben gleich.
</syntaxhighlight>


RS ist an PD4 angeschlossen. Wenn wir r16 an den Port D ausgeben, ist RS jetzt also high und das LCD erwartet Daten anstatt von Befehlen.


lel
Das Ergebnis können wir jetzt endlich direkt an den Port D übergeben:


<syntaxhighlight lang="asm">
          out PORTD, r16
</syntaxhighlight>


Natürlich muss vorher der Port D auf Ausgang geschaltet werden, indem man 0xFF ins Datenrichtungsregister DDRD schreibt.


Um dem LCD zu signalisieren, dass es das an den Datenleitungen anliegende Nibble übernehmen kann, wird die E-Leitung (Enable, an PD5 angeschlossen) auf high und kurz darauf wieder auf low gesetzt. Ein Puls an dieser Leitung teilt also dem LCD mit, dass die restlichen Leitungen jetzt ihren vom Programm gewollten Pegel eingenommen haben und gültig sind.


<syntaxhighlight lang="asm">
          sbi PORTD, 5              ; Enable high
          nop                      ; 3 Taktzyklen warten ("nop" = nichts tun)
          nop
          nop
          cbi PORTD, 5              ; Enable wieder low
</syntaxhighlight>


Einzelheiten unter [http://www.mikrocontroller.net/articles/HD44780 Artikel zum Controller HD44780]
Die eine Hälfte des Bytes wäre damit geschafft! Die andere Hälfte kommt direkt hinterher: Alles, was an der obenstehenden Vorgehensweise geändert werden muss, ist, das „swap“ (Vertauschen der beiden Nibbles) wegzulassen.


== Initialisierung des Displays ==


<center>
Allerdings gibt es noch ein Problem. Wenn ein LCD eingeschaltet wird, dann läuft es zunächst im 8-Bit-Modus. Irgendwie muss das Display initialisiert und auf den 4-Bit-Modus umgeschaltet werden, und zwar nur mit den 4 zur Verfügung stehenden Datenleitungen.
{| {{Tabelle}}
 
|- style="background-color:#ffddcc"
Wenn es Probleme gibt, dann meistens an diesem Punkt. Die „kompatiblen“ Kontroller sind gelegentlich doch nicht 100 % identisch. Es lohnt sich, das Datenblatt (siehe Weblinks im Artikel [[LCD]]) genau zu lesen, in welcher Reihenfolge und mit welchen Abständen (Delays) die Initialisierungsbefehle gesendet werden. Eine weitere Hilfe können Ansteuerungsbeispiele in Forenbeiträgen geben, z.&nbsp;B.
! Pin # || Bezeichnung || Funktion
 
|-
* [https://www.mikrocontroller.net/topic/79609#664268 (A) KS0066U oder Ähnliche --- LCD Treiber]
|align="center" |  1
 
|align="center" |  V<sub>SS</sub> (selten: V<sub>DD</sub>)
=== Initialisierung für 4-Bit-Modus ===
||  GND (selten: +5 V)
 
|-
Achtung: Im Folgenden sind alle Bytes aus Sicht des LCD-Kontrollers angegeben! Da LCD-seitig nur die Leitungen DB4–DB7 verwendet werden, ist daher immer nur das höherwertige Nibble gültig. Durch die Art der Verschaltung (DB4–DB7 wurde auf dem PORT an PD0 bis PD3 angeschlossen) ergibt sich eine Verschiebung, so dass das am Kontroller auszugebende Byte nibblemäßig vertauscht ist!
|align="center" |  2
 
|align="center" |  V<sub>DD</sub> (selten: V<sub>SS</sub>)
Die Sequenz, aus Sicht des Kontrollers, sieht so aus:
||  +5 V (selten: GND)
 
|-
* Nach dem Anlegen der Betriebsspannung muss eine Zeit von mindestens ca. 15&nbsp;ms gewartet werden, um dem LCD-Kontroller Zeit für seine eigene Initialisierung zu geben.
|align="center" |  3
* $3 ins Steuerregister schreiben (RS = 0)
|align="center" |  V<sub>EE</sub>, V0, V5
* Mindestens 4,1&nbsp;ms warten
||  Kontrastspannung (-5 V / 0 V bis 5 V)
* $3 ins Steuerregister schreiben (RS = 0)
|-
* Mindestens 100&nbsp;µs warten
|align="center" |  4
* $3 ins Steuerregister schreiben (RS = 0)
|align="center" |  RS
* $2 ins Steuerregister schreiben (RS = 0), dadurch wird auf 4-Bit-Daten umgestellt.
||  Register Select (0=Befehl/Status 1=Daten)
* Ab jetzt muss für die Übertragung eines Bytes jeweils zuerst das höherwertige Nibble und dann das niederwertige Nibble übertragen werden, wie oben beschrieben.
|-
* Mit dem Konfigurier-Befehl $20 das Display konfigurieren (4-Bit, 1 oder 2 Zeilen, 5×7-Format)
|align="center" |  5
* Mit den restlichen Konfigurierbefehlen die Konfiguration vervollständigen: Display ein/aus, Cursor ein/aus, etc.
|align="center" |  RW
 
||  1=Read 0=Write
Eine Begründung, warum die ersten Befehle dreifach geschickt werden sollen, findet sich [https://www.mikrocontroller.net/topic/158983#1508510 im Forum] und in der [https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller#Mode_selection englischen Wikipedia].
|-
 
|align="center" |  6
=== Initialisierung für 8-Bit-Modus ===
|align="center" |  E
 
||  0=Disable 1=Enable
Der Vollständigkeit halber hier noch die notwendige Initialisierungssequenz für den 8-Bit-Modus. Da hier die Daten komplett als 1 Byte übertragen werden können, sind einige Klimmzüge wie im 4-Bit-Modus nicht notwendig. Begründung für die anfänglichen Wiederholungen siehe oben.
|-
 
|align="center" |  7
* Nach dem Anlegen der Betriebsspannung muss eine Zeit von mindestens ca. 15&nbsp;ms gewartet werden, um dem LCD-Kontroller Zeit für seine eigene Initialisierung zu geben.
|align="center" |  DB0
* $30 ins Steuerregister schreiben (RS = 0)
||  Datenbit 0
* Mindestens 4,1&nbsp;ms warten
|-
* $30 ins Steuerregister schreiben (RS = 0)
|align="center" |  8
* Mindestens 100&nbsp;µs warten
|align="center" |  DB1
* $30 ins Steuerregister schreiben (RS = 0)
||  Datenbit 1
* Mit dem Konfigurier-Befehl 0x30 das Display konfigurieren (8-Bit, 1 oder 2 Zeilen, 5×7-Format)
|-
* Mit den restlichen Konfigurierbefehlen die Konfiguration vervollständigen: Display ein/aus, Cursor ein/aus, etc.
|align="center" |  9
 
|align="center" |  DB2
== Routinen zur LCD-Ansteuerung im 4-Bit-Modus ==
||  Datenbit 2
 
|-
Im Folgenden werden die bisherigen Grundroutinen zur LCD-Ansteuerung im 4-Bit-Modus zusammengefasst und kommentiert. Die darin enthaltenen Symbole (temp1, PORTD, …) müssen in einem dazugehörenden Hauptprogramm definiert werden. Dies wird nächsten Abschnitt ''Anwendung'' weiter erklärt.
|align="center" |  10
 
|align="center" |  DB3
<syntaxhighlight lang="asm">
||  Datenbit 3
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|-
;;                LCD-Routinen                ;;
|align="center" |  11
;;                ============                ;;
|align="center" |  DB4
;;              (c)andreas-s@web.de            ;;
||  Datenbit 4
;;                                            ;;
|-
;; 4bit-Interface                              ;;
|align="center" |  12
;; DB4-DB7:      PD0-PD3                      ;;
|align="center" |  DB5
;; RS:            PD4                          ;;
||  Datenbit 5
;; E:            PD5                          ;;
|-
;;                                            ;;
|align="center" | 13
;; Takt:          4 MHz                        ;;
|align="center" | DB6
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|| Datenbit 6
 
|-
   
|align="center" |  14
   
|align="center" |  DB7
  ;sendet ein Datenbyte an das LCD
||  Datenbit 7
lcd_data:
|-
          mov temp2, temp1            ; "Sicherungskopie" für
|align="center" |  15
                                        ; die Übertragung des 2. Nibbles
|align="center" |  A
          swap temp1                  ; Vertauschen
||  LED-Beleuchtung, meist Anode
          andi temp1, 0b00001111      ; oberes Nibble auf Null setzen
|-
          sbr temp1, 1<<4              ; entspricht 0b00010000 (Anm. 1)
|align="center" |  16
          out PORTD, temp1            ; ausgeben
|align="center" |  K
          rcall lcd_enable            ; Enable-Routine aufrufen
||  LED-Beleuchtung, meist Kathode
                                        ; 2. Nibble, kein swap, da es schon
|-
                                        ; an der richtigen Stelle ist
|}
          andi temp2, 0b00001111      ; obere Hälfte auf Null setzen
</center>
          sbr temp2, 1<<4              ; entspricht 0b00010000
          out PORTD, temp2            ; ausgeben
          rcall lcd_enable            ; Enable-Routine aufrufen
          rcall delay50us              ; Delay-Routine aufrufen
          ret                          ; zurück zum Hauptprogramm


Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.
; sendet einen Befehl an das LCD
lcd_command:                            ; wie lcd_data, nur RS=0
          mov temp2, temp1
          swap temp1
          andi temp1, 0b00001111
          out PORTD, temp1
          rcall lcd_enable
          andi temp2, 0b00001111
          out PORTD, temp2
          rcall lcd_enable
          rcall delay50us
          ret


Bei der DIL-Version (2x7, 2x8 Kontakte) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4 usw. vertauscht. Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es NICHT kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.
; erzeugt den Enable-Puls
;
; Bei höherem Takt (>= 8 MHz) kann es notwendig sein,  
; vor dem Enable High 1-2 Wartetakte (nop) einzufügen.
  ; Siehe dazu https://www.mikrocontroller.net/topic/81974#685882
lcd_enable:
          sbi PORTD, 5                ; Enable high
          nop                          ; mindestens 3 Taktzyklen warten
          nop
          nop
          cbi PORTD, 5                ; Enable wieder low
          ret                          ; Und wieder zurück                   


Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.
; Pause nach jeder Übertragung
 
delay50us:                              ; 50µs Pause (bei 4 MHz)
V<sub>SS</sub> wird ganz einfach an GND angeschlossen und V<sub>CC</sub>=V<sub>DD</sub> an +5 V. V<sub>EE</sub> = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10k&Omega;-Potentiometer zwischen GND und 5 V schalten, mit dem Schleifer an V<sub>EE</sub>. Meist kann man den +5 V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:
          ldi  temp1, $42
delay50us_:dec  temp1
          brne delay50us_
          ret                          ; wieder zurück


[[Bild:LCD_Vee.gif|framed|center| Gewinnung der Kontrastspannung]]
; Längere Pause für manche Befehle
 
delay5ms:                               ; 5ms Pause (bei 4 MHz)
Wenn der Kontrast zu schwach sein sollte (z.B. bei tiefen Temperaturen oder bei Betrieb mit 3.3V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis -5 V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.
          ldi  temp1, $21
 
WGLOOP0:  ldi  temp2, $C9
Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den '''8-Bit-''' und den '''4-Bit-'''Modus.
WGLOOP1:  dec  temp2
* Für den '''8-Bit-Modus''' werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
          brne WGLOOP1
* Der '''4-Bit-Modus''' verwendet nur die oberen vier Datenleitungen ('''DB4-DB7'''). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige '''"Nibble"''' (= 4 Bits), also Bit 4 bis Bit 7 übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z.&nbsp;B. vom KS0070).
          dec  temp1
 
          brne WGLOOP0
Der 4-Bit-Modus hat den Vorteil, dass man 4 IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.
          ret                          ; wieder zurück
 
Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse '''RS''', '''RW''' und '''E''' benötigt.
 
* Über '''RS''' wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS Low, dann wird das ankommende Byte als Befehl interpretiert; Ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).
* '''RW''' legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das '''Busy-Flag''' an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z.&nbsp;B. den Inhalt des Displays lesen - was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch ein IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50 µs (beim Return Home 2 ms, beim Clear Display 20 ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
* Der '''E''' Anschluss schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten von den Datenleitungen bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten / Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.
 
==Heute gehts um Penis==


; Initialisierung: muss ganz am Anfang des Programms aufgerufen werden
lcd_init:
          ldi  temp3,50
powerupwait:
          rcall  delay5ms
          dec  temp3
          brne powerupwait
          ldi temp1, 0b00000011        ; muss 3mal hintereinander gesendet
          out PORTD, temp1            ; werden zur Initialisierung
          rcall lcd_enable            ; 1
          rcall delay5ms
          rcall lcd_enable            ; 2
          rcall delay5ms
          rcall lcd_enable            ; und 3!
          rcall delay5ms
          ldi temp1, 0b00000010        ; 4bit-Modus einstellen
          out PORTD, temp1
          rcall lcd_enable
          rcall delay5ms
          ldi temp1, 0b00101000        ; 4Bit / 2 Zeilen / 5x8
          rcall lcd_command
          ldi temp1, 0b00001100        ; Display ein / Cursor aus / kein Blinken
          rcall lcd_command
          ldi temp1, 0b00000110        ; Cursor inkrementieren / kein Scrollen
          rcall lcd_command
          ret


lel
; Sendet den Befehl zur Löschung des Displays
 
lcd_clear:
          ldi temp1, 0b00000001  ; Display löschen
          rcall lcd_command
          rcall delay5ms
          ret


; Sendet den Befehl: Cursor Home
lcd_home:
          ldi temp1, 0b00000010  ; Cursor Home
          rcall lcd_command
          rcall delay5ms
          ret
</syntaxhighlight>
Anm. 1: Siehe [[Bitmanipulation]]
Weitere Funktionen (wie z.&nbsp;B. Cursorposition verändern) sollten mit Hilfe der [[AVR-Tutorial: LCD#Welche Befehle versteht das LCD.3F|Befehlscodeliste]] nicht schwer zu realisieren sein. Einfach den Code in <code>temp1</code> laden, <code>lcd_command</code> aufrufen und ggf. eine Pause einfügen.
Natürlich kann man die LCD-Ansteuerung auch an einen anderen Port des Mikrocontrollers „verschieben“: Wenn das LCD z.&nbsp;B. an Port&nbsp;B angeschlossen ist, dann reicht es, im Programm alle „PORTD“ durch „PORTB“ und „DDRD“ durch „DDRB“ zu ersetzen.
Wer eine höhere Taktfrequenz als 4&nbsp;MHz verwendet, der sollte daran denken, die Dauer der Verzögerungsschleifen anzupassen.


==Anwendung==


Ein Programm, das diese Routinen zur Anzeige von Text verwendet, kann z.&nbsp;B. so aussehen (die Datei lcd-routines.asm muss sich im gleichen Verzeichnis befinden). Nach der Initialisierung wird zuerst der Displayinhalt gelöscht. Um dem LCD ein Zeichen zu schicken, lädt man es in <code>temp1</code> und ruft die Routine <code>lcd_data</code> auf. Das folgende Beispiel zeigt das Wort „Test“ auf dem LCD an.


Einzelheiten unter [http://www.mikrocontroller.net/articles/HD44780 Artikel zum Controller HD44780]
[https://www.mikrocontroller.net/sourcecode/tutorial/lcd-test.asm Download lcd-test.asm]


<syntaxhighlight lang="asm">
.include "m8def.inc"


<center>
; .def definiert ein Synonym (Namen) für ein µC-Register
{| {{Tabelle}}
.def temp1 = r16
|- style="background-color:#ffddcc"
.def temp2 = r17
! Pin # || Bezeichnung || Funktion
.def temp3 = r18
|-
 
|align="center" | 1
          ldi temp1, LOW(RAMEND)      ; LOW-Byte der obersten RAM-Adresse
|align="center" | V<sub>SS</sub> (selten: V<sub>DD</sub>)
          out SPL, temp1
|| GND (selten: +5 V)
          ldi temp1, HIGH(RAMEND)    ; HIGH-Byte der obersten RAM-Adresse
|-
          out SPH, temp1
|align="center" | 2
 
|align="center" | V<sub>DD</sub> (selten: V<sub>SS</sub>)
          ldi temp1, 0xFF    ; Port D = Ausgang
|| +5 V (selten: GND)
          out DDRD, temp1
|-
 
|align="center" | 3
          rcall lcd_init    ; Display initialisieren
|align="center" | V<sub>EE</sub>, V0, V5
          rcall lcd_clear    ; Display löschen
|| Kontrastspannung (-5 V / 0 V bis 5 V)
 
|-
          ldi temp1, 'T'    ; Zeichen anzeigen
|align="center" | 4
          rcall lcd_data
|align="center" | RS
 
|| Register Select (0=Befehl/Status 1=Daten)
          ldi temp1, 'e'    ; Zeichen anzeigen
|-
          rcall lcd_data
|align="center" | 5
         
|align="center" | RW
          ldi temp1, 's'    ; Zeichen anzeigen
|| 1=Read 0=Write
          rcall lcd_data
 
          ldi temp1, 't'    ; Zeichen anzeigen
          rcall lcd_data
 
loop:
          rjmp loop
 
.include "lcd-routines.asm"            ; LCD-Routinen werden hier eingefügt
</syntaxhighlight>
 
Für längere Texte ist die Methode, jedes Zeichen einzeln in das Register zu laden und <code>lcd_data</code> aufzurufen natürlich nicht sehr praktisch. Dazu später aber mehr.
 
Bisher wurden in Register immer irgendwelche Zahlenwerte geladen, aber in diesem Programm kommt plötzlich die Anweisung
<syntaxhighlight lang="asm">
          ldi temp1, 'T'
</syntaxhighlight>
vor. Wie ist diese zu verstehen? Passiert hier etwas grundlegend anderes als beim Laden einer Zahl in ein Register?
 
Die Antwort darauf lautet: Nein. Auch hier wird letztendlich nur eine Zahl in ein Register geladen. Der Schlüssel zum Verständnis beruht darauf, dass zum LCD, so wie zu allen Ausgabegeräten, für die Ausgabe von Texten immer nur Zahlen übertragen werden, sogenannte Codes. Zum Beispiel könnte man vereinbaren, dass ein LCD, wenn es den Ausgabecode 65 erhält, ein „A“ anzeigt, bei einem Ausgabecode von 66 ein „B“ usw. Naturgemäß gibt es daher viele verschiedene Code-Buchstaben-Zuordnungen. Damit hier etwas Ordnung in das potentielle Chaos kommt, hat man sich bereits in der Steinzeit der Programmierung auf bestimmte Codetabellen geeinigt, von denen die verbreitetste sicherlich die ASCII-Zuordnung ist.
 
==ASCII==
 
ASCII steht für ''American Standard Code for Information Interchange'' und ist ein standardisierter Code zur Zeichenumsetzung. Die Codetabelle sieht hexadezimal dabei wie folgt aus:
 
<center>
{| {{Tabelle}} style="text-align:center"
|- style="background-color:#fdc"
! style="min-width:3em"|   ||style="min-width:3em"|x0 ||style="min-width:3em"|x1 ||style="min-width:3em"|x2 ||style="min-width:3em"|x3 ||style="min-width:3em"|x4 ||style="min-width:3em"|x5 ||style="min-width:3em"|x6 ||style="min-width:3em"|x7 ||style="min-width:3em"|x8 ||style="min-width:3em"|x9 ||style="min-width:3em"|xA ||style="min-width:3em"|xB ||style="min-width:3em"|xC ||style="min-width:3em"|xD ||style="min-width:3em"|xE ||style="min-width:3em"|xF
|-
|-
|align="center" | 6
!style="background-color:#fdc"| 0x
|align="center" | E
|NUL||SOH||STX||ETX||EOT||ENQ||ACK||BEL||BS||HT||LF||VT||FF||CR||SO||SI
|| 0=Disable 1=Enable
|-
|-
|align="center" | 7
!style="background-color:#fdc"| 1x
|align="center" | DB0
|DLE||DC1||DC2||DC3||DC4||NAK||SYN||ETB||CAN||EM||SUB||ESC||FS||GS||RS||US
|| Datenbit 0
|-
|-
|align="center" | 8
!style="background-color:#fdc"| 2x
|align="center" | DB1
|SP||!||"||#||$||%||&||'||(||)||*||+||,||-||.||/
|| Datenbit 1
|-
|-
|align="center" | 9
!style="background-color:#fdc"| 3x
|align="center" | DB2
|0||1||2||3||4||5||6||7||8||9||:||;||&lt;||=||&gt;||?
|| Datenbit 2
|-
|-
|align="center" | 10
!style="background-color:#fdc"| 4x
|align="center" | DB3
|@||A||B||C||D||E||F||G||H||I||J||K||L||M||N||O
|| Datenbit 3
|-
|-
|align="center" | 11
!style="background-color:#fdc"| 5x
|align="center" | DB4
|P||Q||R||S||T||U||V||W||X||Y||Z||[||\||]||^||_
|| Datenbit 4
|-
|-
|align="center" | 12
!style="background-color:#fdc"| 6x
|align="center" | DB5
|`||a||b||c||d||e||f||g||h||i||j||k||l||m||n||o
|| Datenbit 5
|-
|-
|align="center" | 13
!style="background-color:#fdc"| 7x
|align="center" | DB6
|p||q||r||s||t||u||v||w||x||y||z||{||&#124;||}||~||DEL
|| Datenbit 6
|}
|-
|align="center" | 14
|align="center" | DB7
|| Datenbit 7
|-
|align="center" | 15
|align="center" | A
|| LED-Beleuchtung, meist Anode
|-
|align="center" | 16
|align="center" | K
|| LED-Beleuchtung, meist Kathode
|-
|}
</center>
</center>


Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.
Die ersten beiden Zeilen enthalten die Codes für einige Steuerzeichen, ihre vollständige Beschreibung würde hier zu weit führen. Das Zeichen '''SP''' steht für ein ''Space'', also ein Leerzeichen. '''BS''' steht für ''Backspace'', also ein Zeichen zurück. '''DEL''' steht für ''Delete'', also das Löschen eines Zeichens. '''CR''' steht für ''Carriage Return'', also wörtlich: der Wagenrücklauf (einer Schreibmaschine), während '''LF''' für ''Line Feed'', also einen Zeilenvorschub, steht.


Bei der DIL-Version (2x7, 2x8 Kontakte) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4  usw. vertauscht. Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es NICHT kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.
Der Assembler kennt diese Codetabelle und ersetzt die Zeile


Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.
<syntaxhighlight lang="asm">
          ldi temp1, 'T'
</syntaxhighlight>


V<sub>SS</sub> wird ganz einfach an GND angeschlossen und V<sub>CC</sub>=V<sub>DD</sub> an +5 V. V<sub>EE</sub> = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10k&Omega;-Potentiometer zwischen GND und 5 V schalten, mit dem Schleifer an V<sub>EE</sub>. Meist kann man den +5 V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:
durch


[[Bild:LCD_Vee.gif|framed|center| Gewinnung der Kontrastspannung]]
<syntaxhighlight lang="asm">
          ldi temp1, $54
</syntaxhighlight>


Wenn der Kontrast zu schwach sein sollte (z.B. bei tiefen Temperaturen oder bei Betrieb mit 3.3V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis -5 V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.
was letztendlich auch der Lesbarkeit des Programmes zugute kommt. Funktional besteht kein Unterschied zwischen den beiden Anweisungen. Beide bewirken, dass das Register <code>temp1</code> mit dem Bitmuster 01010100 (=&nbsp;hexadezimal 54, =&nbsp;dezimal 84 oder eben der ASCII-Code für „T“) geladen wird.


Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den '''8-Bit-''' und den '''4-Bit-'''Modus.
Das LCD wiederum kennt diese Code-Tabelle ebenfalls und wenn es über den Datenbus die Codezahl $54 zur Anzeige empfängt, dann schreibt es ein „T“ an die aktuelle Cursorposition. Genauer gesagt, weiß das LCD nichts von einem „T“. Es sieht einfach in seinen internen Tabellen nach, welche Pixel beim Empfang der Codezahl $54 auf schwarz zu setzen sind. „Zufällig“ sind das genau jene Pixel, die für uns Menschen ein „T“ ergeben.
* Für den '''8-Bit-Modus''' werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
* Der '''4-Bit-Modus''' verwendet nur die oberen vier Datenleitungen ('''DB4-DB7'''). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige '''"Nibble"''' (= 4 Bits), also Bit 4 bis Bit 7 übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z.&nbsp;B. vom KS0070).


Der 4-Bit-Modus hat den Vorteil, dass man 4 IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.
==Welche Befehle versteht das LCD?==


Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse '''RS''', '''RW''' und '''E''' benötigt.
Auf dem LCD arbeitet ein Controller vom Typ HD44780. Dieser Kontroller versteht eine Reihe von Befehlen, die allesamt mittels <code>lcd_command</code> gesendet werden können. Ein Kommando ist dabei nichts anderes als ein Befehlsbyte, in dem die verschiedenen Bits verschiedene Bedeutungen haben:


* Über '''RS''' wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS Low, dann wird das ankommende Byte als Befehl interpretiert; Ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).
<center>
* '''RW''' legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das '''Busy-Flag''' an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z.&nbsp;B. den Inhalt des Displays lesen - was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch ein IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50 µs (beim Return Home 2 ms, beim Clear Display 20 ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
{| {{Tabelle}}
* Der '''E''' Anschluss schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten von den Datenleitungen bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten / Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.
|-  style="background-color:#ffddcc"
! Bitwert  || Bedeutung
|-
|align="center" |  0
||dieses Bit muss 0 sein
|-
|align="center" |  1
||dieses Bit muss 1 sein
|-
|align="center" |  x
||der Zustand dieses Bits ist egal
|-
|align="center" | sonstige Buchstaben
||das Bit muss je nach gewünschter Funktionalität gesetzt werden.<br />Die mögliche Funktionalität des jeweiligen Bits geht aus der Befehlsbeschreibung hervor.
|}
</center>


==Heute gehts um Penis==
Beispiel: Das Kommando „ON/OFF Control“ soll benutzt werden, um das Display einzuschalten, der Cursor soll eingeschaltet werden und der Cursor soll blinken.
Das Befehlsbyte ist so aufgebaut:
  0b00001dcb
Aus der Befehlsbeschreibung entnimmt man:
* ''Display ein'' bedeutet, dass an der Bitposition d eine 1 stehen muss.
* ''Cursor ein'' bedeutet, dass an der Bitposition c eine 1 stehen muss.
* ''Cursor blinken'' bedeutet, dass an der Bitposition b eine 1 stehen muss.
Das dafür zu übertragende Befehlsbyte hat also die Gestalt 0b00001111 oder in hexadezimaler Schreibweise $0F.


===Clear display: 0b00000001===


Die Anzeige wird gelöscht und der Ausgabecursor kehrt an die Home Position (links, erste Zeile) zurück.


Ausführungszeit: 1,64&nbsp;ms


===Cursor home: 0b0000001x===


Der Cursor kehrt an die Home Position (links, erste Zeile) zurück. Ein verschobenes Display wird auf die Grundeinstellung zurückgesetzt.


Ausführungszeit: 40&nbsp;µs bis 1,64&nbsp;ms


Einzelheiten unter [http://www.mikrocontroller.net/articles/HD44780 Artikel zum Controller HD44780]
===Entry mode: 0b000001is===


Legt die Cursor-Richtung sowie eine mögliche Verschiebung des Displays fest:
* i = 1, Cursorposition bei Ausgabe eines Zeichens erhöhen
* i = 0, Cursorposition bei Ausgabe eines Zeichens vermindern
* s = 1, Display wird gescrollt, wenn der Cursor das Ende bzw. den Anfang, je nach Einstellung von i, erreicht hat.
Ausführungszeit: 40&nbsp;µs
===On/off control: 0b00001dcb===
Display insgesamt ein-/ausschalten; den Cursor ein-/ausschalten; Cursor-Blinken ein-/ausschalten. Wenn das Display ausgeschaltet wird, geht der Inhalt des Displays nicht verloren. Der vorher angezeigte Text wird nach Wiedereinschalten erneut angezeigt.
Ist der Cursor eingeschaltet, aber Blinken ausgeschaltet, so wird der Cursor als Cursorzeile in Pixelzeile&nbsp;8 dargestellt. Ist Blinken eingeschaltet, wird der Cursor als blinkendes ausgefülltes Rechteck dargestellt, welches abwechselnd mit dem Buchstaben an dieser Stelle angezeigt wird.
* d = 0, Display aus
* d = 1, Display ein
* c = 0, Cursor aus
* c = 1, Cursor ein
* b = 0, Cursor-Blinken aus
* b = 1, Cursor-Blinken ein
Ausführungszeit: 40&nbsp;µs


<center>
===Cursor/Scrollen: 0b0001srxx===
{| {{Tabelle}}
 
|-  style="background-color:#ffddcc"
Bewegt den Cursor oder scrollt das Display um eine Position entweder nach rechts oder nach links.
! Pin # || Bezeichnung || Funktion
 
|-
* s = 1, Display scrollen
|align="center" |  1
* s = 0, Cursor bewegen
|align="center" |  V<sub>SS</sub> (selten: V<sub>DD</sub>)
* r = 1, nach rechts
||  GND (selten: +5 V)
* r = 0, nach links
|-
 
|align="center" |  2
Ausführungszeit: 40&nbsp;µs
|align="center" |  V<sub>DD</sub> (selten: V<sub>SS</sub>)
 
||  +5 V (selten: GND)
===Konfiguration: 0b001dnfxx===
|-
 
|align="center" |  3
Einstellen von Interface-Art, Modus, Font
|align="center" |  V<sub>EE</sub>, V0, V5
* d = 0, 4-Bit-Interface
||  Kontrastspannung (-5 V / 0 V bis 5 V)
* d = 1, 8-Bit-Interface
|-
* n = 0, 1-zeilig
|align="center" |  4
* n = 1, 2-zeilig
|align="center" |  RS
* f = 0, 5×7 Pixel
||  Register Select (0=Befehl/Status 1=Daten)
* f = 1, 5×11 Pixel
|-
 
|align="center" |  5
Ausführungszeit: 40&nbsp;µs
|align="center" |  RW
 
||  1=Read 0=Write
===Character RAM Address Set: 0b01aaaaaa===
|-
 
|align="center" |  6
Mit diesem Kommando werden maximal 8 selbst definierte Zeichen definiert. Dazu wird der Character-RAM-Zeiger auf den Anfang des Character Generator (CG) RAM gesetzt und das Zeichen durch die Ausgabe von 8&nbsp;Byte definiert. Der Adresszeiger wird nach Ausgabe jeder Pixelspalte (8&nbsp;Bit) vom LCD selbst erhöht. Nach Beendigung der Zeichendefinition muss die Schreibposition explizit mit dem Kommando „Display RAM Address Set“ wieder in den DD-RAM-Bereich gesetzt werden.
|align="center" |  E
 
||  0=Disable 1=Enable
aaaaaa = 6-bit CG-RAM-Adresse
|-
 
|align="center" |  7
Ausführungszeit: 40&nbsp;µs
|align="center" |  DB0
 
||  Datenbit 0
===Display RAM Address Set: 0b1aaaaaaa===
|-
 
|align="center" | 8
Den Cursor neu positionieren. Display Data (DD) RAM ist vom Character Generator (CG) RAM unabhängig. Der Adresszeiger wird bei Ausgabe eines Zeichens ins DD-RAM automatisch erhöht. Das Display verhält sich so, als ob eine Zeile immer aus 40 logischen Zeichen besteht, von der, je nach konkretem Displaytyp (16 Zeichen, 20 Zeichen) immer nur ein Teil sichtbar ist.
|align="center" | DB1
 
|| Datenbit 1
aaaaaaa = 7-bit DD-RAM-Adresse. Auf 2-zeiligen Displays (und den meisten 16×1-Displays) kann die Adressangabe wie folgt interpretiert werden:
|-
 
|align="center" |  9
1laaaaaa
|align="center" |  DB2
* l = Zeilennummer (0 oder 1)
||  Datenbit 2
* a = 6-Bit Spaltennummer
|-
 
|align="center" |  10
  --------------------------------
|align="center" |  DB3
  DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0
||  Datenbit 3
  --- --- --- --- --- --- --- ---
|-
  1   A  A  A  A  A  A  A
|align="center" |  11
 
|align="center" |  DB4
Setzt die DDRAM-Adresse:
||  Datenbit 4
 
|-
Wenn N = 0 (1 line display)
|align="center" |  12
    AAAAAAA = "00h" - "4Fh"
|align="center" |  DB5
 
||  Datenbit 5
Wenn N = 1 (2 line display) ((1x16))
|-
    AAAAAAA = "00h" - "27h" Zeile 1. (0x80)
|align="center" |  13
    AAAAAAA = "40h" - "67h" Zeile 2. (0xC0)
|align="center" |  DB6
 
||  Datenbit 6
Ausführungszeit: 40&nbsp;µs
|-
 
|align="center" |  14
==Einschub: Code aufräumen==
|align="center" |  DB7
 
||  Datenbit 7
Es wird Zeit, sich einmal etwas kritisch mit den bisher geschriebenen Funktionen auseinander zu setzen.
|-
 
|align="center" |  15
===Portnamen aus dem Code herausziehen===
|align="center" |  A
||  LED-Beleuchtung, meist Anode
|-
|align="center" |  16
|align="center" |  K
||  LED-Beleuchtung, meist Kathode
|-
|}
</center>


Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.
Wenn wir die LCD-Funktionen einmal genauer betrachten, dann fällt sofort auf, dass über die Funktionen verstreut immer wieder das '''PORTD''' sowie einzelne Zahlen für die Pins an diesem Port auftauchen. Wenn das LCD an einem anderen Port betrieben werden soll, oder sich die Pin-Belegung ändert, dann muss an all diesen Stellen eine Anpassung vorgenommen werden. Dabei darf keine einzige Stelle übersehen werden, ansonsten würden die LCD-Funktionen nicht oder nicht vollständig funktionieren.


Bei der DIL-Version (2x7, 2x8 Kontakte) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4  usw. vertauscht. Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es NICHT kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.
Eine Möglichkeit, dem vorzubeugen, ist es, diese immer gleichbleibenden Dinge an den Anfang der LCD-Funktionen vorzuziehen. Anstelle von PORTD wird dann im Code ein anderer Name benutzt, den man frei vergeben kann. Dem Assembler wird nur noch mitgeteilt, dass dieser Name für PORTD steht. Muss das LCD an einen anderen Port angeschlossen werden, so wird nur diese Zuordnung geändert und der Assembler passt dann im restlichen Code alle davon abhängigen Anweisungen an:


Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.
<syntaxhighlight lang="asm">
 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
V<sub>SS</sub> wird ganz einfach an GND angeschlossen und V<sub>CC</sub>=V<sub>DD</sub> an +5 V. V<sub>EE</sub> = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10k&Omega;-Potentiometer zwischen GND und 5 V schalten, mit dem Schleifer an V<sub>EE</sub>. Meist kann man den +5 V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:
;;                LCD-Routinen                ;;
 
;;                ============               ;;
[[Bild:LCD_Vee.gif|framed|center| Gewinnung der Kontrastspannung]]
;;             (c)andreas-s@web.de            ;;
;;                                            ;;
;; 4-Bit-Interface                            ;;
;; DB4..DB7:     PD0..PD3                    ;;
;; RS:            PD4                          ;;
;; E:             PD5                          ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


Wenn der Kontrast zu schwach sein sollte (z.B. bei tiefen Temperaturen oder bei Betrieb mit 3.3V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis -5 V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.
; .equ definiert ein Symbol und dessen Wert
.equ LCD_PORT = PORTD
.equ LCD_DDR  = DDRD
.equ PIN_E    = 5
.equ PIN_RS  = 4


Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den '''8-Bit-''' und den '''4-Bit-'''Modus.
; sendet ein Datenbyte an das LCD
* Für den '''8-Bit-Modus''' werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
lcd_data:
* Der '''4-Bit-Modus''' verwendet nur die oberen vier Datenleitungen ('''DB4-DB7'''). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige '''"Nibble"''' (= 4 Bits), also Bit 4 bis Bit 7 übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z.&nbsp;B. vom KS0070).
          mov temp2, temp1            ; "Sicherungskopie" für
                                        ; die Übertragung des 2. Nibbles
          swap temp1                  ; Nibbles vertauschen
          andi temp1, 0b00001111      ; oberes Nibble auf null setzen
          sbr temp1, 1<<PIN_RS        ; entspricht 0b00010000
          out LCD_PORT, temp1          ; ausgeben
          rcall lcd_enable            ; Enable-Routine aufrufen
                                        ; 2. Nibble, kein swap, da es schon
                                        ; an der richtigen Stelle ist
          andi temp2, 0b00001111      ; obere Hälfte auf null setzen
          sbr temp2, 1<<PIN_RS        ; entspricht 0b00010000
          out LCD_PORT, temp2          ; ausgeben
          rcall lcd_enable            ; Enable-Routine aufrufen
          rcall delay50us              ; Delay-Routine aufrufen
          ret                          ; zurück zum Hauptprogramm


Der 4-Bit-Modus hat den Vorteil, dass man 4 IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.
; sendet einen Befehl an das LCD
 
Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse '''RS''', '''RW''' und '''E''' benötigt.
 
* Über '''RS''' wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS Low, dann wird das ankommende Byte als Befehl interpretiert; Ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).
* '''RW''' legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das '''Busy-Flag''' an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z.&nbsp;B. den Inhalt des Displays lesen - was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch ein IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50 µs (beim Return Home 2 ms, beim Clear Display 20 ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
* Der '''E''' Anschluss schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten von den Datenleitungen bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten / Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.
 
==Heute gehts um Penis==


lcd_command:                            ; wie lcd_data, nur RS=0
          mov temp2, temp1
          swap temp1
          andi temp1, 0b00001111
          out LCD_PORT, temp1
          rcall lcd_enable
          andi temp2, 0b00001111
          out LCD_PORT, temp2
          rcall lcd_enable
          rcall delay50us
          ret


lel
; erzeugt den Enable-Puls
lcd_enable:
          sbi LCD_PORT, PIN_E          ; Enable high
          nop                          ; 3 Taktzyklen warten
          nop
          nop
          cbi LCD_PORT, PIN_E          ; Enable wieder low
          ret                          ; Und wieder zurück


; Pause nach jeder Übertragung
delay50us:                              ; 50µs Pause
          ldi  temp1, $42
delay50us_:dec  temp1
          brne delay50us_
          ret                          ; wieder zurück


; Längere Pause für manche Befehle
delay5ms:                              ; 5ms Pause
          ldi  temp1, $21
WGLOOP0:  ldi  temp2, $C9
WGLOOP1:  dec  temp2
          brne WGLOOP1
          dec  temp1
          brne WGLOOP0
          ret                          ; wieder zurück


; Initialisierung: muss ganz am Anfang des Programms aufgerufen werden
lcd_init:
          ldi  temp1, 0xFF            ; alle Pins am Ausgabeport auf Ausgang
          out  LCD_DDR, temp1


          ldi  temp3, 6
powerupwait:
          rcall delay5ms
          dec  temp3
          brne  powerupwait
          ldi  temp1,    0b00000011  ; muss 3mal hintereinander gesendet
          out  LCD_PORT, temp1        ; werden zur Initialisierung
          rcall lcd_enable            ; 1
          rcall delay5ms
          rcall lcd_enable            ; 2
          rcall delay5ms
          rcall lcd_enable            ; und 3!
          rcall delay5ms
          ldi  temp1, 0b00000010      ; 4-Bit-Modus einstellen
          out  LCD_PORT, temp1
          rcall lcd_enable
          rcall delay5ms
          ldi  temp1, 0b00101000      ; 4 Bit, 2 Zeilen, 5x7 Pixel
          rcall lcd_command
          ldi  temp1, 0b00001100      ; Display on, Cursor off
          rcall lcd_command
          ldi  temp1, 0b00000110      ; Cursor inkrementieren, kein Scrollen
          rcall lcd_command
          ret


Einzelheiten unter [http://www.mikrocontroller.net/articles/HD44780 Artikel zum Controller HD44780]
; Sendet den Befehl zur Löschung des Displays
 
 
<center>
{| {{Tabelle}}
|-  style="background-color:#ffddcc"
! Pin # || Bezeichnung || Funktion
|-
|align="center" |  1
|align="center" |  V<sub>SS</sub> (selten: V<sub>DD</sub>)
||  GND (selten: +5 V)
|-
|align="center" |  2
|align="center" |  V<sub>DD</sub> (selten: V<sub>SS</sub>)
||  +5 V (selten: GND)
|-
|align="center" |  3
|align="center" |  V<sub>EE</sub>, V0, V5
||  Kontrastspannung (-5 V / 0 V bis 5 V)
|-
|align="center" |  4
|align="center" |  RS
||  Register Select (0=Befehl/Status 1=Daten)
|-
|align="center" |  5
|align="center" |  RW
||  1=Read 0=Write
|-
|align="center" |  6
|align="center" |  E
||  0=Disable 1=Enable
|-
|align="center" |  7
|align="center" |  DB0
||  Datenbit 0
|-
|align="center" |  8
|align="center" |  DB1
||  Datenbit 1
|-
|align="center" |  9
|align="center" |  DB2
||  Datenbit 2
|-
|align="center" |  10
|align="center" |  DB3
||  Datenbit 3
|-
|align="center" |  11
|align="center" |  DB4
||  Datenbit 4
|-
|align="center" |  12
|align="center" |  DB5
||  Datenbit 5
|-
|align="center" |  13
|align="center" |  DB6
||  Datenbit 6
|-
|align="center" |  14
|align="center" |  DB7
||  Datenbit 7
|-
|align="center" |  15
|align="center" |  A
||  LED-Beleuchtung, meist Anode
|-
|align="center" |  16
|align="center" |  K
||  LED-Beleuchtung, meist Kathode
|-
|}
</center>
 
Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.
 
Bei der DIL-Version (2x7, 2x8 Kontakte) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4  usw. vertauscht. Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es NICHT kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.
 
Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.
 
V<sub>SS</sub> wird ganz einfach an GND angeschlossen und V<sub>CC</sub>=V<sub>DD</sub> an +5 V. V<sub>EE</sub> = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10k&Omega;-Potentiometer zwischen GND und 5 V schalten, mit dem Schleifer an V<sub>EE</sub>. Meist kann man den +5 V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:
 
[[Bild:LCD_Vee.gif|framed|center| Gewinnung der Kontrastspannung]]
 
Wenn der Kontrast zu schwach sein sollte (z.B. bei tiefen Temperaturen oder bei Betrieb mit 3.3V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis -5 V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.
 
Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den '''8-Bit-''' und den '''4-Bit-'''Modus.
* Für den '''8-Bit-Modus''' werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
* Der '''4-Bit-Modus''' verwendet nur die oberen vier Datenleitungen ('''DB4-DB7'''). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige '''"Nibble"''' (= 4 Bits), also Bit 4 bis Bit 7 übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z.&nbsp;B. vom KS0070).
 
Der 4-Bit-Modus hat den Vorteil, dass man 4 IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.
 
Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse '''RS''', '''RW''' und '''E''' benötigt.
 
* Über '''RS''' wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS Low, dann wird das ankommende Byte als Befehl interpretiert; Ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).
* '''RW''' legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das '''Busy-Flag''' an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z.&nbsp;B. den Inhalt des Displays lesen - was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch ein IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50 µs (beim Return Home 2 ms, beim Clear Display 20 ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
* Der '''E''' Anschluss schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten von den Datenleitungen bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten / Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.
 
==Heute gehts um die Beziehung zwischen Kersges und Kurtz; Oh ja schleck mal==
 
Kersges und Kurtz sind zwei homophobe Wesen die nichts mehr wollen als sich offen zu lieben.
 
 
 
 
 
Einzelheiten unter [http://www.mikrocontroller.net/articles/HD44780 Artikel zum Controller HD44780]
 
 
<center>
{| {{Tabelle}}
|-  style="background-color:#ffddcc"
! Pin # || Bezeichnung || Funktion
|-
|align="center" |  1
|align="center" |  V<sub>SS</sub> (selten: V<sub>DD</sub>)
||  GND (selten: +5 V)
|-
|align="center" |  2
|align="center" |  V<sub>DD</sub> (selten: V<sub>SS</sub>)
||  +5 V (selten: GND)
|-
|align="center" |  3
|align="center" |  V<sub>EE</sub>, V0, V5
||  Kontrastspannung (-5 V / 0 V bis 5 V)
|-
|align="center" |  4
|align="center" |  RS
||  Register Select (0=Befehl/Status 1=Daten)
|-
|align="center" |  5
|align="center" |  RW
||  1=Read 0=Write
|-
|align="center" |  6
|align="center" |  E
||  0=Disable 1=Enable
|-
|align="center" |  7
|align="center" |  DB0
||  Datenbit 0
|-
|align="center" |  8
|align="center" |  DB1
||  Datenbit 1
|-
|align="center" |  9
|align="center" |  DB2
||  Datenbit 2
|-
|align="center" |  10
|align="center" |  DB3
||  Datenbit 3
|-
|align="center" |  11
|align="center" |  DB4
||  Datenbit 4
|-
|align="center" |  12
|align="center" |  DB5
||  Datenbit 5
|-
|align="center" |  13
|align="center" |  DB6
||  Datenbit 6
|-
|align="center" |  14
|align="center" |  DB7
||  Datenbit 7
|-
|align="center" |  15
|align="center" |  A
||  LED-Beleuchtung, meist Anode
|-
|align="center" |  16
|align="center" |  K
||  LED-Beleuchtung, meist Kathode
|-
|}
</center>
 
Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.
 
Bei der DIL-Version (2x7, 2x8 Kontakte) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4  usw. vertauscht. Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es NICHT kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.
 
Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.
 
V<sub>SS</sub> wird ganz einfach an GND angeschlossen und V<sub>CC</sub>=V<sub>DD</sub> an +5 V. V<sub>EE</sub> = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10k&Omega;-Potentiometer zwischen GND und 5 V schalten, mit dem Schleifer an V<sub>EE</sub>. Meist kann man den +5 V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:
 
[[Bild:LCD_Vee.gif|framed|center| Gewinnung der Kontrastspannung]]
 
Wenn der Kontrast zu schwach sein sollte (z.B. bei tiefen Temperaturen oder bei Betrieb mit 3.3V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis -5 V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.
 
Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den '''8-Bit-''' und den '''4-Bit-'''Modus.
* Für den '''8-Bit-Modus''' werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
* Der '''4-Bit-Modus''' verwendet nur die oberen vier Datenleitungen ('''DB4-DB7'''). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige '''"Nibble"''' (= 4 Bits), also Bit 4 bis Bit 7 übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z.&nbsp;B. vom KS0070).
 
Der 4-Bit-Modus hat den Vorteil, dass man 4 IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.
 
Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse '''RS''', '''RW''' und '''E''' benötigt.
 
* Über '''RS''' wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS Low, dann wird das ankommende Byte als Befehl interpretiert; Ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).
* '''RW''' legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das '''Busy-Flag''' an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z.&nbsp;B. den Inhalt des Displays lesen - was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch ein IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50 µs (beim Return Home 2 ms, beim Clear Display 20 ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
* Der '''E''' Anschluss schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten von den Datenleitungen bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten / Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.
 
==Heute gehts um Penis==
 
 
lel
 
 
 
 
 
Einzelheiten unter [http://www.mikrocontroller.net/articles/HD44780 Artikel zum Controller HD44780]
 
 
<center>
{| {{Tabelle}}
|-  style="background-color:#ffddcc"
! Pin # || Bezeichnung || Funktion
|-
|align="center" |  1
|align="center" |  V<sub>SS</sub> (selten: V<sub>DD</sub>)
||  GND (selten: +5 V)
|-
|align="center" |  2
|align="center" |  V<sub>DD</sub> (selten: V<sub>SS</sub>)
||  +5 V (selten: GND)
|-
|align="center" |  3
|align="center" |  V<sub>EE</sub>, V0, V5
||  Kontrastspannung (-5 V / 0 V bis 5 V)
|-
|align="center" |  4
|align="center" |  RS
||  Register Select (0=Befehl/Status 1=Daten)
|-
|align="center" |  5
|align="center" |  RW
||  1=Read 0=Write
|-
|align="center" |  6
|align="center" |  E
||  0=Disable 1=Enable
|-
|align="center" |  7
|align="center" |  DB0
||  Datenbit 0
|-
|align="center" |  8
|align="center" |  DB1
||  Datenbit 1
|-
|align="center" |  9
|align="center" |  DB2
||  Datenbit 2
|-
|align="center" |  10
|align="center" |  DB3
||  Datenbit 3
|-
|align="center" |  11
|align="center" |  DB4
||  Datenbit 4
|-
|align="center" |  12
|align="center" |  DB5
||  Datenbit 5
|-
|align="center" |  13
|align="center" |  DB6
||  Datenbit 6
|-
|align="center" |  14
|align="center" |  DB7
||  Datenbit 7
|-
|align="center" |  15
|align="center" |  A
||  LED-Beleuchtung, meist Anode
|-
|align="center" |  16
|align="center" |  K
||  LED-Beleuchtung, meist Kathode
|-
|}
</center>
 
Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.
 
Bei der DIL-Version (2x7, 2x8 Kontakte) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4  usw. vertauscht. Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es NICHT kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.
 
Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.
 
V<sub>SS</sub> wird ganz einfach an GND angeschlossen und V<sub>CC</sub>=V<sub>DD</sub> an +5 V. V<sub>EE</sub> = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10k&Omega;-Potentiometer zwischen GND und 5 V schalten, mit dem Schleifer an V<sub>EE</sub>. Meist kann man den +5 V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:
 
[[Bild:LCD_Vee.gif|framed|center| Gewinnung der Kontrastspannung]]
 
Wenn der Kontrast zu schwach sein sollte (z.B. bei tiefen Temperaturen oder bei Betrieb mit 3.3V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis -5 V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.
 
Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den '''8-Bit-''' und den '''4-Bit-'''Modus.
* Für den '''8-Bit-Modus''' werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
* Der '''4-Bit-Modus''' verwendet nur die oberen vier Datenleitungen ('''DB4-DB7'''). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige '''"Nibble"''' (= 4 Bits), also Bit 4 bis Bit 7 übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z.&nbsp;B. vom KS0070).
 
Der 4-Bit-Modus hat den Vorteil, dass man 4 IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.
 
Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse '''RS''', '''RW''' und '''E''' benötigt.
 
* Über '''RS''' wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS Low, dann wird das ankommende Byte als Befehl interpretiert; Ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).
* '''RW''' legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das '''Busy-Flag''' an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z.&nbsp;B. den Inhalt des Displays lesen - was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch ein IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50 µs (beim Return Home 2 ms, beim Clear Display 20 ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
* Der '''E''' Anschluss schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten von den Datenleitungen bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten / Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.
 
== Anschluss an den Controller ==
 
Jetzt, da wir wissen, welche Anschlüsse das LCD benötigt, können wir das LCD mit dem Mikrocontroller verbinden:
 
<span style="color:red">ACHTUNG: Es gibt Displays mit abweichender Anschluss-Belegung (z. B. TC1602E (Pollin 120420): Vdd und Vss vertauscht), falscher Anschluss kann zur Zerstörung führen! Daher immer das zugehörige Datenblatt zu Rate ziehen.</span>
 
Einzelheiten unter [[HD44780|Artikel zum Controller HD44780]]
 
<center>
{| {{Tabelle}}
|-  style="background-color:#ffddcc"
!Pinnummer<BR>LCD || Bezeichnung || Anschluss
|-
| align="center" |1 || V<sub>SS</sub> || GND (beim TC1602E: V<sub>CC</sub>)
|-
| align="center" |2 || V<sub>CC</sub> || +5 V (beim TC1602E: Gnd)
|-
| align="center" |3 || V<sub>EE</sub> || GND , [[Potentiometer | Poti]] oder [[Pulsweitenmodulation | PWM]] am AVR
|-
| align="center" |4 || RS || PD4 am AVR
|-
| align="center" |5 || RW || GND
|-
| align="center" |6 || E || PD5 am AVR
|-
| align="center" |7 || DB0 || nicht angeschlossen
|-
| align="center" |8 || DB1 || nicht angeschlossen
|-
| align="center" |9 || DB2 || nicht angeschlossen
|-
| align="center" |10 || DB3 || nicht angeschlossen
|-
| align="center" |11 || DB4 || PD0 am AVR
|-
| align="center" |12 || DB5 || PD1 am AVR
|-
| align="center" |13 || DB6 || PD2 am AVR
|-
| align="center" |14 || DB7 || PD3 am AVR
|-
| align="center" |15 || A || Vorsicht! Meistens nicht direkt an +5 V anschließbar,<br /> sondern nur über einen Vorwiderstand, der an die Daten<br />der Hintergrundbeleuchtung angepasst werden muss.
|-
| align="center" |16 || K || GND
|}
</center>
 
Ok. Alles ist verbunden. Wenn man jetzt den Strom einschaltet, sollten ein oder zwei schwarze Balken auf dem Display angezeigt werden.
 
Doch wie bekommt man jetzt die Befehle und Daten in das Display? Dazu muss das LCD initialisiert werden und man muss Befehle (Commands) und seine Daten an das LCD senden. Weil die Initialisierung ein Spezialfall der Übertragung von Befehlen ist, im Folgenden zunächst die Erklärung für die Übertragung von Werten an das LCD.
 
== Ansteuerung des LCDs im 4-Bit-Modus ==
 
Um ein Byte zu übertragen, muss man es erstmal in die beiden Nibbles zerlegen, die getrennt übertragen werden. Da das obere Nibble (Bit 4 - Bit 7) als erstes übertragen wird, die 4 Datenleitungen jedoch an die vier unteren Bits des Port D angeschlossen sind, muss man die beiden Nibbles des zu übertragenden Bytes erstmal vertauschen. Der AVR kennt dazu praktischerweise einen eigenen Befehl:
 
<syntaxhighlight lang="avrasm">
          swap r16              ; vertauscht die beiden Nibbles von r16
</syntaxhighlight>
 
Aus 0b00100101 wird so z.&nbsp;B. 0b01010010.
 
Jetzt sind die Bits für die erste Phase der Übertragung an der richtigen Stelle. Trotzdem wollen wir das Ergebnis nicht einfach so mit '''out PORTD, r16''' an den Port geben. Um die Hälfte des Bytes, die jetzt nicht an die Datenleitungen des LCDs gegeben wird auf null zu setzen, verwendet man folgenden Befehl:
 
<syntaxhighlight lang="avrasm">
          andi r16, 0b00001111  ; Nur die vier unteren (mit 1 markierten)
                                  ; Bits werden übernommen, alle anderen werden null
</syntaxhighlight>
 
Also: Das obere Nibble wird erst mit dem unteren vertauscht, damit es unten ist. Dann wird das obere (das wir jetzt noch nicht brauchen) auf null gesetzt.
 
Jetzt müssen wir dem LCD noch mitteilen, ob wir Daten oder Befehle senden wollen. Das machen wir, indem wir das Bit, an dem RS angeschlossen ist (PD4), auf 0 (Befehl senden) oder auf 1 (Daten senden) setzen. Um ein Bit in einem normalen Register zu setzen, gibt es den Befehl sbr (Set Bits in Register). Dieser Befehl unterscheidet sich jedoch von sbi (das nur für IO-Register gilt) dadurch, dass man nicht die Nummer des zu setzenden Bits angibt, sondern eine Bitmaske. Das geht so:
 
<syntaxhighlight lang="avrasm">
          sbr r16, 0b00010000    ; Bit 4 setzen, alle anderen Bits bleiben gleich
</syntaxhighlight>
 
RS ist an PD4 angeschlossen. Wenn wir r16 an den Port D ausgeben, ist RS jetzt also high und das LCD erwartet Daten anstatt von Befehlen.
 
Das Ergebnis können wir jetzt endlich direkt an den Port D übergeben:
 
<syntaxhighlight lang="avrasm">
          out PORTD, r16
</syntaxhighlight>
 
Natürlich muss vorher der Port D auf Ausgang geschaltet werden, indem man 0xFF ins Datenrichtungsregister DDRD schreibt.
 
Um dem LCD zu signalisieren, dass es das an den Datenleitungen anliegende Nibble übernehmen kann, wird die E-Leitung (Enable, an PD5 angeschlossen) auf high und kurz darauf wieder auf low gesetzt. Ein Puls an dieser Leitung teilt also dem LCD mit, das die restlichen Leitungen jetzt ihren vom Programm gewollten Pegel eingenommen haben und gültig sind.
 
<syntaxhighlight lang="avrasm">
          sbi PORTD, 5              ; Enable high
          nop                      ; 3 Taktzyklen warten ("nop" = nichts tun)
          nop
          nop
          cbi PORTD, 5              ; Enable wieder low
</syntaxhighlight>
 
Die eine Hälfte des Bytes wäre damit geschafft! Die andere Hälfte kommt direkt hinterher: Alles, was an der obenstehenden Vorgehensweise geändert werden muss, ist, das "swap" (Vertauschen der beiden Nibbles) wegzulassen.
 
== Initialisierung des Displays ==
 
Allerdings gibt es noch ein Problem. Wenn ein LCD eingeschaltet wird, dann läuft es zunächst im 8 Bit Modus. Irgendwie muss das Display initialisiert und auf den 4 Bit Modus umgeschaltet werden, und zwar nur mit den 4 zur Verfügung stehenden Datenleitungen.<br>
 
Wenn es Probleme gibt, dann meistens an diesem Punkt. Die "kompatiblen" Kontroller sind gelegentlich doch nicht 100% identisch. Es lohnt sich, das Datenblatt (siehe Weblinks im Artikel [[LCD]]) genau zu lesen, in welcher Reihenfolge und mit welchen Abständen (Delays) die Initialisierungbefehle gesendet werden. Eine weitere Hilfe können Ansteuerungsbeispiele in Forenbeiträgen geben z.&nbsp;B.
 
* [http://www.mikrocontroller.net/topic/79609#664268 (A) KS0066U oder Ähnliche --- LCD Treiber]
 
=== Initialisierung für 4 Bit Modus ===
 
Achtung: Im Folgenden sind alle Bytes aus Sicht des LCD-Kontrollers angegeben! Da LCD-seitig nur die Leitungen DB4 - DB7 verwendet werden, ist daher immer nur das höherwertige Nibble gültig. Durch die Art der Verschaltung (DB4 - DB7 wurde auf dem PORT an PD0 bis PD3 angeschlossen) ergibt sich eine Verschiebung, so dass das am Kontroller auszugebende Byte nibblemässig vertauscht ist!
 
Die Sequenz, aus Sicht des Kontrollers, sieht so aus:
 
* Nach dem Anlegen der Betriebsspannung muss eine Zeit von mindestens ca. 15ms gewartet werden, um dem LCD-Kontroller Zeit für seine eigene Initialisierung zu geben
* $3 ins Steuerregister schreiben (RS = 0)
* Mindestens 4.1ms warten
* $3 ins Steuerregister schreiben (RS = 0)
* Mindestens 100µs warten
* $3 ins Steuerregister schreiben (RS = 0)
* $2 ins Steuerregister schreiben (RS = 0), dadurch wird auf 4 Bit Daten umgestellt
* Ab jetzt muss für die Übertragung eines Bytes jeweils zuerst das höherwertige Nibble und dann das niederwertige Nibble übertragen werden, wie oben beschrieben
* Mit dem Konfigurier-Befehl $20 das Display konfigurieren (4-Bit, 1 oder 2 Zeilen, 5x7 Format)
* Mit den restlichen Konfigurierbefehlen die Konfiguration vervollständigen: Display ein/aus, Cursor ein/aus, etc.
 
Eine Begründung, warum die ersten Befehle dreifach geschickt werden sollen, findet sich [http://www.mikrocontroller.net/topic/158983#1508510 im Forum].
 
=== Initialisierung für 8 Bit Modus ===
 
Der Vollständigkeit halber hier noch die notwendige Initialiserungssequenz für den 8 Bit Modus. Da hier die Daten komplett als 1 Byte übertragen werden können, sind einige Klimmzüge wie im 4 Bit Modus nicht notwendig. Begründung für die anfänglichen Wiederholungen siehe oben.
 
* Nach dem Anlegen der Betriebsspannung muss eine Zeit von mindestens ca. 15ms gewartet werden, um dem LCD-Kontroller Zeit für seine eigene Initialisierung zu geben
* $30 ins Steuerregister schreiben (RS = 0)
* Mindestens 4.1ms warten
* $30 ins Steuerregister schreiben (RS = 0)
* Mindestens 100µs warten
* $30 ins Steuerregister schreiben (RS = 0)
* Mit dem Konfigurier-Befehl 0x30 das Display konfigurieren (8-Bit, 1 oder 2 Zeilen, 5x7 Format)
* Mit den restlichen Konfigurierbefehlen die Konfiguration vervollständigen: Display ein/aus, Cursor ein/aus, etc.
 
== Routinen zur LCD-Ansteuerung im 4-Bit-Modus ==
 
Im Folgenden werden die bisherigen Grundroutinen zur LCD-Ansteuerung im 4-Bit-Modus zusammengefasst und kommentiert. Die darin enthaltenen Symbole (temp1, PORTD,...) müssen in einem dazugehörenden Hauptprogramm definiert werden. Dies wird nächsten Abschnitt ''Anwendung'' weiter erklärt.
 
<syntaxhighlight lang="avrasm">
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                LCD-Routinen                ;;
;;                ============                ;;
;;              (c)andreas-s@web.de            ;;
;;                                            ;;
;; 4bit-Interface                              ;;
;; DB4-DB7:      PD0-PD3                      ;;
;; RS:            PD4                          ;;
;; E:            PD5                          ;;
;;                                            ;;
;; Takt:          4 MHz                        ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
;sendet ein Datenbyte an das LCD
lcd_data:
          mov temp2, temp1            ; "Sicherungskopie" für
                                        ; die Übertragung des 2.Nibbles
          swap temp1                  ; Vertauschen
          andi temp1, 0b00001111      ; oberes Nibble auf Null setzen
          sbr temp1, 1<<4              ; entspricht 0b00010000 (Anm.1)
          out PORTD, temp1            ; ausgeben
          rcall lcd_enable            ; Enable-Routine aufrufen
                                        ; 2. Nibble, kein swap da es schon
                                        ; an der richtigen stelle ist
          andi temp2, 0b00001111      ; obere Hälfte auf Null setzen
          sbr temp2, 1<<4              ; entspricht 0b00010000
          out PORTD, temp2            ; ausgeben
          rcall lcd_enable            ; Enable-Routine aufrufen
          rcall delay50us              ; Delay-Routine aufrufen
          ret                          ; zurück zum Hauptprogramm
 
; sendet einen Befehl an das LCD
lcd_command:                            ; wie lcd_data, nur RS=0
          mov temp2, temp1
          swap temp1
          andi temp1, 0b00001111
          out PORTD, temp1
          rcall lcd_enable
          andi temp2, 0b00001111
          out PORTD, temp2
          rcall lcd_enable
          rcall delay50us
          ret
 
; erzeugt den Enable-Puls
;
; Bei höherem Takt (>= 8 MHz) kann es notwendig sein,
; vor dem Enable High 1-2 Wartetakte (nop) einzufügen.
; Siehe dazu http://www.mikrocontroller.net/topic/81974#685882
lcd_enable:
          sbi PORTD, 5                ; Enable high
          nop                          ; mindestens 3 Taktzyklen warten
          nop
          nop
          cbi PORTD, 5                ; Enable wieder low
          ret                          ; Und wieder zurück                   
 
; Pause nach jeder Übertragung
delay50us:                              ; 50µs Pause (bei 4 MHz)
          ldi  temp1, $42
delay50us_:dec  temp1
          brne delay50us_
          ret                          ; wieder zurück
 
; Längere Pause für manche Befehle
delay5ms:                              ; 5ms Pause (bei 4 MHz)
          ldi  temp1, $21
WGLOOP0:  ldi  temp2, $C9
WGLOOP1:  dec  temp2
          brne WGLOOP1
          dec  temp1
          brne WGLOOP0
          ret                          ; wieder zurück
 
; Initialisierung: muss ganz am Anfang des Programms aufgerufen werden
lcd_init:
          ldi  temp3,50
powerupwait:
          rcall  delay5ms
          dec  temp3
          brne powerupwait
          ldi temp1, 0b00000011        ; muss 3mal hintereinander gesendet
          out PORTD, temp1            ; werden zur Initialisierung
          rcall lcd_enable            ; 1
          rcall delay5ms
          rcall lcd_enable            ; 2
          rcall delay5ms
          rcall lcd_enable            ; und 3!
          rcall delay5ms
          ldi temp1, 0b00000010        ; 4bit-Modus einstellen
          out PORTD, temp1
          rcall lcd_enable
          rcall delay5ms
          ldi temp1, 0b00101000        ; 4Bit / 2 Zeilen / 5x8
          rcall lcd_command
          ldi temp1, 0b00001100        ; Display ein / Cursor aus / kein Blinken
          rcall lcd_command
          ldi temp1, 0b00000100        ; inkrement / kein Scrollen
          rcall lcd_command
          ret
 
; Sendet den Befehl zur Löschung des Displays
lcd_clear:
          ldi temp1, 0b00000001  ; Display löschen
          rcall lcd_command
          rcall delay5ms
          ret
 
; Sendet den Befehl: Cursor Home
lcd_home:
          ldi temp1, 0b00000010  ; Cursor Home
          rcall lcd_command
          rcall delay5ms
          ret
</syntaxhighlight>
 
Anm.1: Siehe [[Bitmanipulation]]
 
Weitere Funktionen (wie z.&nbsp;B. Cursorposition verändern) sollten mit Hilfe der [[AVR-Tutorial:_LCD#Welche_Befehle_versteht_das_LCD.3F|Befehlscodeliste]] nicht schwer zu realisieren sein. Einfach den Code in temp laden, lcd_command aufrufen und ggf. eine Pause einfügen.<br>
Natürlich kann man die LCD-Ansteuerung auch an einen anderen Port des Mikrocontrollers "verschieben": Wenn das LCD z.&nbsp;B. an Port B angeschlossen ist, dann reicht es, im Programm alle "PORTD" durch "PORTB" und "DDRD" durch "DDRB" zu ersetzen.<br>
Wer eine höhere Taktfrequenz als 4 MHz verwendet, der sollte daran denken, die Dauer der Verzögerungsschleifen anzupassen.
 
==Anwendung==
 
Ein Programm, das diese Routinen zur Anzeige von Text verwendet, kann z.&nbsp;B. so aussehen (die Datei lcd-routines.asm muss sich im gleichen Verzeichnis befinden). Nach der Initialisierung wird zuerst der Displayinhalt gelöscht. Um dem LCD ein Zeichen zu schicken, lädt man es in temp1 und ruft die Routine "lcd_data" auf. Das folgende Beispiel zeigt das Wort "Test" auf dem LCD an.
 
[http://www.mikrocontroller.net/sourcecode/tutorial/lcd-test.asm Download lcd-test.asm]
 
<syntaxhighlight lang="avrasm">
.include "m8def.inc"
 
; .def definiert ein Synonym (Namen) für ein µC Register
.def temp1 = r16
.def temp2 = r17
.def temp3 = r18
 
          ldi temp1, LOW(RAMEND)      ; LOW-Byte der obersten RAM-Adresse
          out SPL, temp1
          ldi temp1, HIGH(RAMEND)    ; HIGH-Byte der obersten RAM-Adresse
          out SPH, temp1
 
          ldi temp1, 0xFF    ; Port D = Ausgang
          out DDRD, temp1
 
          rcall lcd_init    ; Display initialisieren
          rcall lcd_clear    ; Display löschen
 
          ldi temp1, 'T'    ; Zeichen anzeigen
          rcall lcd_data
 
          ldi temp1, 'e'    ; Zeichen anzeigen
          rcall lcd_data
         
          ldi temp1, 's'    ; Zeichen anzeigen
          rcall lcd_data
 
          ldi temp1, 't'    ; Zeichen anzeigen
          rcall lcd_data
 
loop:
          rjmp loop
 
.include "lcd-routines.asm"            ; LCD-Routinen werden hier eingefügt
</syntaxhighlight>
 
Für längere Texte ist die Methode, jedes Zeichen einzeln in das Register zu laden und "lcd_data" aufzurufen natürlich nicht sehr praktisch. Dazu später aber mehr.
 
Bisher wurden in Register immer irgendwelche Zahlenwerte geladen, aber in diesem Programm kommt plötzlich die Anweisung
<syntaxhighlight lang="avrasm">
          ldi temp1, 'T'
</syntaxhighlight>
vor. Wie ist diese zu verstehen? Passiert hier etwas grundlegend anderes als beim Laden einer Zahl in ein Register?
 
Die Antwort darauf lautet: Nein. Auch hier wird letztendlich nur eine Zahl in ein Register geladen. Der Schlüssel zum Verständnis beruht darauf, dass zum LCD, so wie zu allen Ausgabegeräten, für die Ausgabe von Texten immer nur Zahlen übertragen werden, sog. Codes. Zum Beispiel könnte man vereinbaren, dass ein LCD, wenn es den Ausgabecode 65 erhält, ein 'A' anzeigt, bei einem Ausgabecode von 66 ein 'B' usw. Naturgemäß gibt es daher viele verschiedene Code-Buchstaben Zuordnungen. Damit hier etwas Ordnung in das potentielle Chaos kommt, hat man sich bereits in der Steinzeit der Programmierung auf bestimmte Codetabellen geeinigt, von denen die verbreitetste sicherlich die ASCII-Zuordnung ist.
 
==ASCII==
 
ASCII steht für ''American Standard Code for Information Interchange'' und ist ein standardisierter Code zur Zeichenumsetzung. Die Codetabelle sieht hexadezimal dabei wie folgt aus:
 
<center>
{| {{Tabelle}}
|-  style="background-color:#ffddcc"
!  ||x0||x1||x2||x3||x4||x5||x6||x7||x8||x9||xA||xB||xC||xD||xE||xF
|-
!style="background-color:#ffddcc"| 0x
|NUL||SOH||STX||ETX||EOT||ENQ||ACK||BEL||BS||HT||LF||VT||FF||CR||SO||SI
|-
!style="background-color:#ffddcc"| 1x
|DLE||DC1||DC2||DC3||DC4||NAK||SYN||ETB||CAN||EM||SUB||ESC||FS||GS||RS||US
|-
!style="background-color:#ffddcc"| 2x
|SP||!||"||#||$||%||&||'||(||)||*||+||,||-||.||/
|-
!style="background-color:#ffddcc"| 3x
|0||1||2||3||4||5||6||7||8||9||:||;||<||=||>||?
|-
!style="background-color:#ffddcc"| 4x
|@||A||B||C||D||E||F||G||H||I||J||K||L||M||N||O
|-
!style="background-color:#ffddcc"| 5x
|P||Q||R||S||T||U||V||W||X||Y||Z||[||\||]||^||_
|-
!style="background-color:#ffddcc"| 6x
|`||a||b||c||d||e||f||g||h||i||j||k||l||m||n||o
|-
!style="background-color:#ffddcc"| 7x
|p||q||r||s||t||u||v||w||x||y||z||{|| &#124; ||}||~||DEL
|}
</center>
 
Die ersten beiden Zeilen enthalten die Codes für einige Steuerzeichen, ihre vollständige Beschreibung würde hier zu weit führen. Das Zeichen '''SP''' steht für ein ''Space'', also ein Leerzeichen. '''BS''' steht für ''Backspace'', also ein Zeichen zurück. '''DEL''' steht für ''Delete'', also das Löschen eines Zeichens. '''CR''' steht für ''Carriage Return'', also wörtlich: der Wagenrücklauf (einer Schreibmaschine), während '''LF''' für ''Line feed'', also einen Zeilenvorschub steht.
 
Der Assembler kennt diese Codetabelle und ersetzt die Zeile
 
<syntaxhighlight lang="avrasm">
          ldi temp1, 'T'
</syntaxhighlight>
 
durch
 
<syntaxhighlight lang="avrasm">
          ldi temp1, $54
</syntaxhighlight>
 
was letztendlich auch der Lesbarkeit des Programmes zugute kommt. Funktional besteht kein Unterschied zwischen den beiden Anweisungen. Beide bewirken, dass das Register temp1 mit dem Bitmuster 01010100 ( = hexadezimal 54, = dezimal 84 oder eben der ASCII Code für '''T''') geladen wird.
 
 
Das LCD wiederrum kennt diese Code-Tabelle ebenfalls und wenn es über den Datenbus die Codezahl $54 zur Anzeige empfängt, dann schreibt es ein '''T''' an die aktuelle Cursorposition. Genauer gesagt, weiss das LCD nichts von einem '''T'''. Es sieht einfach in seinen internen Tabellen nach, welche Pixel beim Empfang der Codezahl $54 auf schwarz zu setzen sind. 'Zufällig' sind das genau jene Pixel, die für uns Menschen ein '''T''' ergeben.
 
==Welche Befehle versteht das LCD?==
 
Auf dem LCD arbeitet ein Controller vom Typ HD44780. Dieser Kontroller versteht eine Reihe von Befehlen, die allesamt mittels lcd_command gesendet werden können. Ein Kommando ist dabei nichts anderes als ein Befehlsbyte, in dem die verschiedenen Bits verschiedene Bedeutungen haben:
 
<center>
{| {{Tabelle}}
|-  style="background-color:#ffddcc"
! Bitwert  || Bedeutung
|-
|align="center" |  0
||dieses Bit muss 0 sein
|-
|align="center" |  1
||dieses Bit muss 1 sein
|-
|align="center" |  x
||der Zustand dieses Bits ist egal
|-
|align="center" | sonstige Buchstaben
||das Bit muss je nach gewünschter Funktionalität gesetzt werden.<br />Die mögliche Funktionalität des jeweiligen Bits geht aus der Befehlsbeschreibung hervor.
|}
</center>
 
Beispiel: Das Kommando 'ON/OFF Control' soll benutzt werden, um das Display einzuschalten, der Cursor soll eingeschaltet werden und der Cursor soll blinken.
Das Befehlsbyte ist so aufgebaut:
  0b00001dcb
Aus der Befehlsbeschreibung entnimmt man:
* Display ein bedeutet, dass an der Bitposition d eine 1 stehen muss.<br>
* Cursor ein bedeutet, dass an der Bitposition c ein 1 stehen muss.<br>
* Cursor blinken bedeutet, dass an der Bitposition b eine 1 stehen muss.<br>
Das dafür zu übertragende Befehlsbyte hat also die Gestalt 0b00001111 oder in hexadezimaler Schreibweise $0F.
 
===Clear display: 0b00000001===
 
Die Anzeige wird gelöscht und der Ausgabecursor kehrt an die Home Position (links, erste Zeile) zurück.
 
Ausführungszeit: 1.64ms
 
===Cursor home: 0b0000001x===
 
Der Cursor kehrt an die Home Position (links, erste Zeile) zurück. Ein verschobenes Display wird auf die Grundeinstellung zurückgesetzt.
 
Ausführungszeit: 40µs bis 1.64ms
 
===Entry mode: 0b000001is===
 
Legt die Cursor Richtung sowie eine mögliche Verschiebung des Displays fest
* i = 1, Cursorposition bei Ausgabe eines Zeichens erhöhen
* i = 0, Cursorposition bei Ausgabe eines Zeichens vermindern
* s = 1, Display wird gescrollt, wenn der Cursor das Ende/Anfang, je nach Einstellung von i, erreicht hat.
 
Ausführungszeit: 40µs
 
===On/off control: 0b00001dcb===
 
Display insgesamt ein/ausschalten; den Cursor ein/ausschalten; den Cursor auf blinken schalten/blinken aus. Wenn das Display ausgeschaltet wird, geht der Inhalt des Displays nicht verloren. Der vorher angezeigte Text wird nach wiedereinschalten erneut angezeigt.
Ist der Cursor eingeschaltet, aber Blinken ausgeschaltet, so wird der Cursor als Cursorzeile in Pixelzeile 8 dargestellt. Ist Blinken eingeschaltet, wird der Cursor als blinkendes ausgefülltes Rechteck dargestellt, welches abwechselnd mit dem Buchstaben an dieser Stelle angezeigt wird.
 
* d = 0, Display aus
* d = 1, Display ein
* c = 0, Cursor aus
* c = 1, Cursor ein
* b = 0, Cursor blinken aus
* b = 1, Cursor blinken ein
Ausführungszeit: 40µs
 
===Cursor/Scrollen: 0b0001srxx===
 
Bewegt den Cursor oder scrollt das Display um eine Position entweder nach rechts oder nach links.
 
* s = 1, Display scrollen
* s = 0, Cursor bewegen
* r = 1, nach rechts
* r = 0, nach links
 
Ausführungszeit: 40µs
 
===Konfiguration: 0b001dnfxx===
 
Einstellen der Interface Art, Modus, Font
* d = 0, 4-Bit Interface
* d = 1, 8-Bit Interface
* n = 0, 1 zeilig
* n = 1, 2 zeilig
* f = 0, 5x7 Pixel
* f = 1, 5x11 Pixel
 
Ausführungszeit: 40µs
 
===Character RAM Address Set: 0b01aaaaaa===
 
Mit diesem Kommando werden maximal 8 selbst definierte Zeichen definiert. Dazu wird der Character RAM Zeiger auf den Anfang des Character Generator (CG) RAM gesetzt und das Zeichen durch die Ausgabe von 8 Byte definiert. Der Adresszeiger wird nach Ausgabe jeder Pixelspalte (8 Bit) vom LCD selbst erhöht. Nach Beendigung der Zeichendefinition muss die Schreibposition explizit mit dem Kommando "Display RAM Address Set" wieder in den DD-RAM Bereich gesetzt werden.
 
aaaaaa 6-bit CG RAM Adresse
 
Ausführungszeit: 40µs
 
===Display RAM Address Set: 0b1aaaaaaa===
 
Den Cursor neu positionieren. Display Data (DD) Ram ist vom Character Generator (CG) Ram unabhängig. Der Adresszeiger wird bei Ausgabe eines Zeichens ins DD Ram automatisch erhöht. Das Display verhält sich so, als ob eine Zeile immer aus 40 logischen Zeichen besteht, von der, je nach konkretem Displaytyp (16 Zeichen, 20 Zeichen) immer nur ein Teil sichtbar ist.
 
aaaaaaa 7-bit DD RAM Adresse. Auf 2-zeiligen Displays (und den meisten 16x1 Displays), kann die Adressangabe wie folgt interpretiert werden:
 
1laaaaaa
* l = Zeilennummer (0 oder 1)
* a = 6-Bit Spaltennummer
 
--------------------------------
DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0
--- --- --- --- --- --- --- ---
  1  A  A  A  A  A  A  A
 
Setzt die DDRAM Adresse:
 
Wenn N = 0 (1 line display)
    AAAAAAA = "00h" - "4Fh"
 
Wenn N = 1 (2 line display) ((1x16))
    AAAAAAA = "00h" - "27h" Zeile 1. (0x80)
    AAAAAAA = "40h" - "67h" Zeile 2. (0xC0)
 
 
Ausführungszeit: 40µs
 
==Einschub: Code aufräumen==
 
Es wird Zeit, sich einmal etwas kritisch mit den bisher geschriebenen Funktionen auseinander zu setzen.
 
 
 
===Portnamen aus dem Code herausziehen===
 
Wenn wir die LCD-Funktionen einmal genauer betrachten, dann fällt sofort auf, dass über die Funktionen verstreut immer wieder das '''PORTD''' sowie einzelne Zahlen für die Pins an diesem Port auftauchen. Wenn das LCD an einem anderen Port betrieben werden soll, oder sich die Pin-Belegung ändert, dann muss an all diesen Stellen eine Anpassung vorgenommen werden. Dabei darf keine einzige Stelle übersehen werden, ansonsten würden die LCD-Funktionen nicht oder nicht vollständig funktionieren.
 
Eine Möglichkeit, dem vorzubeugen, ist es, diese immer gleichbleibenden Dinge an den Anfang der LCD-Funktionen vorzuziehen. Anstelle von PORTD wird dann im Code ein anderer Name benutzt, den man frei vergeben kann. Dem Assembler wird nur noch mitgeteilt, das dieser Name für PORTD steht. Muss das LCD an einen anderen Port angeschlossen werden, so wird nur diese Zurodnung geändert und der Assembler passt dann im restlichen Code alle davon abhängigen Anweisungen an:
 
<syntaxhighlight lang="avrasm">
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                LCD-Routinen                ;;
;;                ============                ;;
;;              (c)andreas-s@web.de            ;;
;;                                            ;;
;; 4bit-Interface                              ;;
;; DB4-DB7:      PD0-PD3                      ;;
;; RS:            PD4                          ;;
;; E:            PD5                          ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; .equ definiert ein Symbol und dessen Wert
.equ LCD_PORT = PORTD
.equ LCD_DDR  = DDRD
.equ PIN_E    = 5
.equ PIN_RS  = 4
 
;sendet ein Datenbyte an das LCD
lcd_data:
          mov temp2, temp1            ; "Sicherungskopie" für
                                        ; die Übertragung des 2.Nibbles
          swap temp1                  ; Vertauschen
          andi temp1, 0b00001111      ; oberes Nibble auf Null setzen
          sbr temp1, 1<<PIN_RS        ; entspricht 0b00010000
          out LCD_PORT, temp1          ; ausgeben
          rcall lcd_enable            ; Enable-Routine aufrufen
                                        ; 2. Nibble, kein swap da es schon
                                        ; an der richtigen stelle ist
          andi temp2, 0b00001111      ; obere Hälfte auf Null setzen
          sbr temp2, 1<<PIN_RS        ; entspricht 0b00010000
          out LCD_PORT, temp2          ; ausgeben
          rcall lcd_enable            ; Enable-Routine aufrufen
          rcall delay50us              ; Delay-Routine aufrufen
          ret                          ; zurück zum Hauptprogramm
 
; sendet einen Befehl an das LCD
 
lcd_command:                            ; wie lcd_data, nur RS=0
          mov temp2, temp1
          swap temp1
          andi temp1, 0b00001111
          out LCD_PORT, temp1
          rcall lcd_enable
          andi temp2, 0b00001111
          out LCD_PORT, temp2
          rcall lcd_enable
          rcall delay50us
          ret
; erzeugt den Enable-Puls
lcd_enable:
          sbi LCD_PORT, PIN_E          ; Enable high
          nop                          ; 3 Taktzyklen warten
          nop
          nop
          cbi LCD_PORT, PIN_E          ; Enable wieder low
          ret                          ; Und wieder zurück                   
; Pause nach jeder Übertragung
delay50us:                              ; 50µs Pause
          ldi  temp1, $42
delay50us_:dec  temp1
          brne delay50us_
          ret                          ; wieder zurück
; Längere Pause für manche Befehle
delay5ms:                              ; 5ms Pause
          ldi  temp1, $21
WGLOOP0:  ldi  temp2, $C9
WGLOOP1:  dec  temp2
          brne WGLOOP1
          dec  temp1
          brne WGLOOP0
          ret                          ; wieder zurück
; Initialisierung: muss ganz am Anfang des Programms aufgerufen werden
lcd_init:
          ldi  temp1, 0xFF            ; alle Pins am Ausgabeport auf Ausgang
          out  LCD_DDR, temp1
 
          ldi  temp3,6
powerupwait:
          rcall delay5ms
          dec  temp3
          brne  powerupwait
          ldi  temp1,    0b00000011  ; muss 3mal hintereinander gesendet
          out  LCD_PORT, temp1        ; werden zur Initialisierung
          rcall lcd_enable            ; 1
          rcall delay5ms
          rcall lcd_enable            ; 2
          rcall delay5ms
          rcall lcd_enable            ; und 3!
          rcall delay5ms
          ldi  temp1, 0b00000010      ; 4bit-Modus einstellen
          out  LCD_PORT, temp1
          rcall lcd_enable
          rcall delay5ms
          ldi  temp1, 0b00101000      ; 4 Bit, 2 Zeilen
          rcall lcd_command
          ldi  temp1, 0b00001100      ; Display on, Cursor off
          rcall lcd_command
          ldi  temp1, 0b00000100      ; endlich fertig
          rcall lcd_command
          ret
; Sendet den Befehl zur Löschung des Displays
lcd_clear:
lcd_clear:
           ldi  temp1, 0b00000001      ; Display löschen
           ldi  temp1, 0b00000001      ; Display löschen
Zeile 1.497: Zeile 729:
           ret
           ret


; Sendet den Befehl: Cursor Home
; Sendet den Befehl: Cursor Home
lcd_home:
lcd_home:
           ldi  temp1, 0b00000010      ; Cursor Home
           ldi  temp1, 0b00000010      ; Cursor Home
Zeile 1.505: Zeile 737:
</syntaxhighlight>
</syntaxhighlight>


Mittels '''.equ''' werden mit dem Assembler Textersetzungen vereinbart. Der Assembler ersetzt alle Vorkomnisse des Quelltextes durch den zu ersetzenden Text. Dadurch ist es z.&nbsp;B. möglich, alle Vorkommnisse von '''PORTD''' durch '''LCD_PORT''' auszutauschen. Wird das LCD an einen anderen Port, z.&nbsp;B. '''PORTB''' gelegt, dann genügt es, die Zeilen
Mittels '''<code>.equ</code>''' werden mit dem Assembler Textersetzungen vereinbart. Der Assembler ersetzt alle Vorkommnisse des linken Bezeichners durch den Bezeichner rechts des Gleichheitszeichens. Dadurch ist es z.&nbsp;B. möglich, alle Vorkommnisse von <code>PORTD</code> durch <code>LCD_PORT</code> auszutauschen. Wird das LCD an einen anderen Port, z.&nbsp;B. <code>PORTB</code>, gelegt, dann genügt es, die Zeilen
<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.equ LCD_PORT = PORTD
.equ LCD_PORT = PORTD
.equ LCD_DDR  = DDRD
.equ LCD_DDR  = DDRD
</syntaxhighlight>
</syntaxhighlight>
durch
durch
<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.equ LCD_PORT = PORTB
.equ LCD_PORT = PORTB
.equ LCD_DDR  = DDRB
.equ LCD_DDR  = DDRB
Zeile 1.523: Zeile 755:
Beachtet werden muss dabei natürlich, dass es zu keinen Überschneidungen kommt. Solange nur jede Funktion jeweils für sich betrachtet wird, ist das kein Problem. In 20 oder 30 Code-Zeilen kann man gut verfolgen, welches Register wofür benutzt wird. Schwieriger wird es, wenn Funktionen wiederum andere Funktionen aufrufen, die ihrerseits wieder Funktionen aufrufen usw. Jede dieser Funktionen benutzt einige Register und mit zunehmender Programmgröße wird es immer schwieriger, zu verfolgen, welches Register zu welchem Zeitpunkt wofür benutzt wird.
Beachtet werden muss dabei natürlich, dass es zu keinen Überschneidungen kommt. Solange nur jede Funktion jeweils für sich betrachtet wird, ist das kein Problem. In 20 oder 30 Code-Zeilen kann man gut verfolgen, welches Register wofür benutzt wird. Schwieriger wird es, wenn Funktionen wiederum andere Funktionen aufrufen, die ihrerseits wieder Funktionen aufrufen usw. Jede dieser Funktionen benutzt einige Register und mit zunehmender Programmgröße wird es immer schwieriger, zu verfolgen, welches Register zu welchem Zeitpunkt wofür benutzt wird.


Speziell bei Basisfunktionen wie diesen LCD-Funktionen, ist es daher oft ratsam, dafür zu sorgen, dass jede Funktion die Register wieder in dem Zustand hinterlässt, indem sie sie auch vorgefunden hat. Wir benötigen dazu wieder den Stack, auf dem die Registerinhalte bei Betreten einer Funktion zwischengespeichert werden und von dem die Register bei Verlassen einer Funktion wiederhergestellt werden.
Speziell bei Basisfunktionen wie diesen LCD-Funktionen ist es daher oft ratsam, dafür zu sorgen, dass jede Funktion die Register wieder in dem Zustand hinterlässt, in dem sie sie auch vorgefunden hat. Wir benötigen dazu wieder den Stack, auf dem die Registerinhalte bei Betreten einer Funktion zwischengespeichert werden und von dem die Register bei Verlassen einer Funktion wiederhergestellt werden.


Nehmen wir die Funktion
Nehmen wir die Funktion
<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
  ; Sendet den Befehl zur Löschung des Displays
  ; Sendet den Befehl zur Löschung des Displays
lcd_clear:
lcd_clear:
Zeile 1.535: Zeile 767:
</syntaxhighlight>
</syntaxhighlight>


Diese Funktion verändert das Register temp1. Um das Register abzusichern, schreiben wir die Funktion um:
Diese Funktion verändert das Register <code>temp1</code>. Um das Register abzusichern, schreiben wir die Funktion um:


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
  ; Sendet den Befehl zur Löschung des Displays
  ; Sendet den Befehl zur Löschung des Displays
lcd_clear:
lcd_clear:
Zeile 1.548: Zeile 780:
</syntaxhighlight>
</syntaxhighlight>


Am besten hält man sich an die Regel: Jede Funktion ist dafür zuständig, die Register zu sichern und wieder herzustellen, die sie auch selbst verändert. '''lcd_clear''' ruft die Funktionen '''lcd_command''' und '''delay5ms''' auf. Wenn diese Funktionen selbst wieder Register verändern (und das tun sie), so ist es die Aufgabe dieser Funktionen, sich um die Sicherung und das Wiederherstellen der entsprechenden Register zu kümmern. '''lcd_clear''' sollte sich nicht darum kümmern müssen. Auf diese Weise ist das Schlimmste, das einem passieren kann, dass ein paar Register unnütz gesichert und wiederhergestellt werden. Das kostet zwar etwas Rechenzeit und etwas Speicherplatz auf dem Stack, ist aber immer noch besser als das andere Extrem: Nach einem Funktionsaufruf haben einige Register nicht mehr den Wert, den sie haben sollten, und das Programm rechnet mit falschen Zahlen weiter.
Am besten hält man sich an die Regel: Jede Funktion ist dafür zuständig, die Register zu sichern und wieder herzustellen, die sie auch selbst verändert. <code>lcd_clear</code> ruft die Funktionen <code>lcd_command</code> und <code>delay5ms</code> auf. Wenn diese Funktionen selbst wieder Register verändern (und das tun sie), so ist es die Aufgabe dieser Funktionen, sich um die Sicherung und das Wiederherstellen der entsprechenden Register zu kümmern. <code>lcd_clear</code> sollte sich nicht darum kümmern müssen. Auf diese Weise ist das Schlimmste, das einem passieren kann, dass ein paar Register unnütz gesichert und wiederhergestellt werden. Das kostet zwar etwas Rechenzeit und etwas Speicherplatz auf dem Stack, ist aber immer noch besser als das andere Extrem: Nach einem Funktionsaufruf haben einige Register nicht mehr den Wert, den sie haben sollten, und das Programm rechnet mit falschen Zahlen weiter.


===Lass den Assembler rechnen===
===Lass den Assembler rechnen===
Betrachtet man den Code genauer, so fallen einige konstante Zahlenwerte auf (Das vorangestellte $ kennzeichnet die Zahl als Hexadezimalzahl):
Betrachtet man den Code genauer, so fallen einige konstante Zahlenwerte auf (Das vorangestellte $ kennzeichnet die Zahl als Hexadezimalzahl):


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
delay50us:                              ; 50µs Pause
delay50us:                              ; 50µs Pause
           ldi  temp1, $42
           ldi  temp1, $42
Zeile 1.562: Zeile 794:
</syntaxhighlight>
</syntaxhighlight>


Der Code benötigt eine Warteschleife, die mindestens 50µs dauert. Die beiden Befehle innerhalb der Schleife benötigen 3 Takte: 1 Takt für den '''dec''' und der '''brne''' benötigt 2 Takte, wenn die Bedingung zutrifft, der Branch also genommen wird. Bei 4 Mhz werden also 4000000 / 3 * 50 / 1000000 = 66.6 Durchläufe durch die Schleife benötigt, um eine Verzögerungszeit von 50µs (0.000050 Sekunden) zu erreichen, hexadezimal ausgedrückt: $42.
Der Code benötigt eine Warteschleife, die mindestens 50&nbsp;µs dauert. Die beiden Befehle innerhalb der Schleife benötigen 3 Takte: 1 Takt für den <code>dec</code> und der <code>brne</code> benötigt 2 Takte, wenn die Bedingung zutrifft, der Branch also genommen wird. Bei 4&nbsp;MHz werden also 4000000 / 3 * 50 / 1000000 ≈ 67 Durchläufe durch die Schleife benötigt, um eine Verzögerungszeit von 50&nbsp;µs (0,00005 Sekunden) zu erreichen, hexadezimal ausgedrückt: $42 ''(eigentlich $43)''.


Der springende Punkt ist: Bei anderen Taktfrequenzen müsste man nun jedesmal diese Berechnung machen und den entsprechenden Zahlenwert einsetzen. Das kann aber der Assembler genausogut erledigen. Am Anfang des Codes wird ein Eintrag definiert, der die Taktfrequenz festlegt. Traditionell heißt dieser Eintrag <i>XTAL</i>:
Der springende Punkt ist: Bei anderen Taktfrequenzen müsste man nun jedesmal diese Berechnung machen und den entsprechenden Zahlenwert einsetzen. Das kann aber der Assembler genausogut erledigen. Am Anfang des Codes wird ein Eintrag definiert, der die Taktfrequenz festlegt. Traditionell heißt dieser Eintrag ''XTAL'':


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.equ XTAL  = 4000000
.equ XTAL  = 4000000                   ; Taktfrequenz in Hz


...
...
Zeile 1.579: Zeile 811:
</syntaxhighlight>
</syntaxhighlight>


An einer anderen Codestelle gibt es weitere derartige magische Zahlen:
An einer anderen Codestelle gibt es weitere derartige „magische Zahlen“:


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
  ; Längere Pause für manche Befehle
  ; Längere Pause für manche Befehle
delay5ms:                              ; 5ms Pause
delay5ms:                              ; 5ms Pause
Zeile 1.594: Zeile 826:


Was geht hier vor?
Was geht hier vor?
Die innere Schleife benötigt wieder 3 Takte pro Durchlauf. Bei $C9 = 201 Durchläufen werden also 201 * 3 = 603 Takte verbraucht. In der äußeren Schleife werden pro Durchlauf also 603 + 1 + 2 = 606 Takte verbraucht und einmal 605 Takte (weil der brne nicht genommen wird). Da die äußere Schleife $21 = 33 mal wiederholt wird, werden 32 * 606 + 605 = 19997 Takte verbraucht. Noch 1 Takt mehr für den allerersten ldi und 4 Takte für den ret, macht 20002 Takte. Bei 4Mhz benötigt der Prozessor 20002 / 4000000 = 0.0050005 Sekunden, also rund 5 ms. Die 7. nachkommastelle kann man an dieser Stelle getrost ignorieren. Vor allen Dingen auch deshalb, weil auch der Quarz nicht exakt 4000000 Schwingungen in der Sekunde durchführen wird.
Die innere Schleife benötigt wieder 3 Takte pro Durchlauf. Bei $C9 = 201 Durchläufen werden also 201 * 3 = 603 Takte verbraucht. In der äußeren Schleife werden pro Durchlauf also 603 + 1 + 2 = 606 Takte verbraucht und einmal 605 Takte (weil der <code>brne</code> nicht genommen wird). Da die äußere Schleife $21 = 33-mal wiederholt wird, werden 32 * 606 + 605 = 19997 Takte verbraucht. Noch 1 Takt mehr für den allerersten <code>ldi</code> und 4 Takte für den <code>ret</code>, macht 20002 Takte. Bei 4&nbsp;MHz benötigt der Prozessor 20002 / 4000000 = 0.0050005 Sekunden, also rund 5&nbsp;ms. Die 7. Nachkommastelle kann man an dieser Stelle getrost ignorieren. Vor allen Dingen auch deshalb, weil auch der Quarz nicht exakt 4000000 Schwingungen in der Sekunde durchführen wird.
Wird der Wiederholwert für die innere Schleife bei $C9 belassen, so werden 4000000 / 607 * 5 / 1000 Wiederholungen der äusseren Schleife benötigt. (Die Berechnung wurde hier etwas vereinfacht, die nicht berücksichtigten Takte fallen zeitmässig nicht weiter ins Gewicht bzw. wurden dadurch berücksichtigt, dass mit 607 anstelle von 606 gerechnet wird). Auch diese Berechnung kann wieder der Assembler übernehmen:
Wird der Wiederholwert für die innere Schleife bei $C9 belassen, so werden 4000000 / 607 * 5 / 1000 Wiederholungen der äußeren Schleife benötigt. (Die Berechnung wurde hier etwas vereinfacht, die nicht berücksichtigten Takte fallen zeitmäßig nicht weiter ins Gewicht bzw. wurden dadurch berücksichtigt, dass mit 607 anstelle von 606 gerechnet wird). Auch diese Berechnung kann wieder der Assembler übernehmen:


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
  ; Längere Pause für manche Befehle
  ; Längere Pause für manche Befehle
delay5ms:                              ; 5ms Pause
delay5ms:                              ; 5ms Pause
Zeile 1.609: Zeile 841:
</syntaxhighlight>
</syntaxhighlight>


Ein kleines Problem kann bei der Verwendung dieses Verfahrens entstehen: Bei hohen Taktfrequenzen und großen Wartezeiten kann der berechnete Wert größer als 255 werden und man bekommt die Fehlermeldung "Operand(s) out of range" beim Assemblieren. Dieser Fall tritt zum Beispiel für obige Konstruktion bei einer Taktfrequenz von 16 MHz ein (genauer gesagt ab 15,3 MHz), während darunter XTAL beliebig geändert werden kann. Als einfachste Lösung bietet es sich an, die Zahl der Takte pro Schleifendurchlauf durch das Einfügen von '''nop''' zu erhöhen und die Berechnungsvorschrift anzupassen.
Ein kleines Problem kann bei der Verwendung dieses Verfahrens entstehen: Bei hohen Taktfrequenzen und großen Wartezeiten kann der berechnete Wert größer als 255 werden und man bekommt die Fehlermeldung „Operand(s) out of range“ beim Assemblieren. Dieser Fall tritt zum Beispiel für obige Konstruktion bei einer Taktfrequenz von 16&nbsp;MHz ein (genauer gesagt ab 15,3&nbsp;MHz), während darunter XTAL beliebig geändert werden kann. Als einfachste Lösung bietet es sich an, die Zahl der Takte pro Schleifendurchlauf durch das Einfügen von <code>nop</code>-Befehlen zu erhöhen und die Berechnungsvorschrift anzupassen.


== Ausgabe eines konstanten Textes ==
== Ausgabe eines konstanten Textes ==


Weiter oben wurde schon einmal ein Text ausgegeben. Dies geschah durch Ausgabe von einzelnen Zeichen. Das können wir auch anders machen. Wir können den Text im Speicher ablegen und eine Funktion schreiben, die die einzelnen Zeichen aus dem Speicher liest und aus gibt. Dabei stellt sich Frage: Woher 'weiß' die Funktion eigentlich, wie lang der Text ist? Die Antwort darauf lautet: Sie kann es nicht wissen. Wir müssen irgendwelche Vereinbarungen treffen, woran die Funktion erkennen kann, dass der Text zu Ende ist. Im Wesentlichen werden dazu 2 Methoden benutzt:
Weiter oben wurde schon einmal ein Text ausgegeben. Dies geschah durch Ausgabe von einzelnen Zeichen. Das können wir auch anders machen. Wir können den Text im Speicher ablegen und eine Funktion schreiben, die die einzelnen Zeichen aus dem Speicher liest und ausgibt. Dabei stellt sich die Frage: Woher „weiß“ die Funktion eigentlich, wie lang der Text ist? Die Antwort darauf lautet: Sie kann es nicht wissen. Wir müssen irgendwelche Vereinbarungen treffen, woran die Funktion erkennen kann, dass der Text zu Ende ist. Im Wesentlichen werden dazu zwei Methoden benutzt:
* Der Text enthält ein spezielles Zeichen, welches das Ende des Textes markiert
* Der Text enthält ein spezielles Zeichen, welches das Ende des Textes markiert.
* Wir speichern nicht nur den Text selbst, sondern auch die Länge des Textes
* Wir speichern nicht nur den Text selbst, sondern auch die Länge des Textes.
Mit einer der beiden Methoden ist es der Textausgabefunktion dann ein Leichtes, den Text vollständig auszugeben.
Mit einer der beiden Methoden ist es der Textausgabefunktion dann ein Leichtes, den Text vollständig auszugeben.


Zeile 1.622: Zeile 854:
Den Text selbst speichern wir im Flash-Speicher, also dort, wo auch das Programm gespeichert ist:
Den Text selbst speichern wir im Flash-Speicher, also dort, wo auch das Programm gespeichert ist:


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
  ; Einen konstanten Text aus dem Flash Speicher
  ; Einen konstanten Text aus dem Flash-Speicher
  ; ausgeben. Der Text wird mit einer 0 beendet
  ; ausgeben. Der Text wird mit einer 0 beendet.
lcd_flash_string:
lcd_flash_string:
           push  temp1
           push  temp1
Zeile 1.644: Zeile 876:
</syntaxhighlight>
</syntaxhighlight>


Diese Funktion benutzt den Befehl '''lpm''', um das jeweils nächste Zeichen aus dem Flash Speicher in ein Register zur Weiterverarbeitung zu laden. Dazu wird der sog. '''Z-Pointer''' benutzt. So nennt man das Registerpaar '''R30''' und '''R31'''. Nach jedem Ladevorgang wird dabei durch den Befehl
Diese Funktion benutzt den Befehl <code>lpm</code>, um das jeweils nächste Zeichen aus dem Flash-Speicher in ein Register zur Weiterverarbeitung zu laden. Dazu wird der sogenannte '''Z-Pointer''' benutzt. So nennt man das Registerpaar '''R30''' und '''R31'''. Nach jedem Ladevorgang wird dabei durch den Befehl
<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
           lpm  temp1, Z+
           lpm  temp1, Z+
</syntaxhighlight>
</syntaxhighlight>
dieser Z-Pointer um 1 erhöht. Mittels '''cpi''' wird das in das Register '''temp1''' geladene Zeichen mit 0 verglichen. '''cpi''' vergleicht die beiden Zahlen und merkt sich das Ergebnis in einem speziellen Register in Form von Status Bits. '''cpi''' zieht dabei ganz einfach die beiden Zahlen voneinander ab. Sind sie gleich, so kommt da als Ergebnis 0 heraus und '''cpi''' setzt daher konsequenter Weise das Zero-Flag, das anzeigt, dass die vorhergegangene Operation eine 0 als Ergebnis hatte.'''breq''' wertet diese Status-Bits aus. Wenn die vorhergegangene Operation ein 0-Ergebnis hatte, das Zero-Flag also gesetzt ist, dann wird ein Sprung zum angegebenen Label durchgeführt. In Summe bewirkt also die Sequenz
dieser Z-Pointer um 1 erhöht. Mittels <code>cpi</code> wird das in das Register <code>temp1</code> geladene Zeichen mit 0 verglichen. <code>cpi</code> vergleicht die beiden Zahlen und merkt sich das Ergebnis in einem speziellen Register in Form von Status-Bits. <code>cpi</code> zieht dabei ganz einfach die beiden Zahlen voneinander ab. Sind sie gleich, so kommt da als Ergebnis 0 heraus und <code>cpi</code> setzt daher konsequenter Weise das Zero-Flag, das anzeigt, dass die vorhergegangene Operation eine 0 als Ergebnis hatte. <code>breq</code> wertet diese Status-Bits aus. Wenn die vorhergegangene Operation ein 0-Ergebnis hatte, das Zero-Flag also gesetzt ist, dann wird ein Sprung zum angegebenen Label durchgeführt. In Summe bewirkt also die Sequenz
<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">  
           cpi  temp1, 0
           cpi  temp1, 0
           breq  lcd_flash_string_2
           breq  lcd_flash_string_2
</syntaxhighlight>
</syntaxhighlight>
dass das gelesene Zeichen mit 0 verglichen wird und falls das gelesene
dass das gelesene Zeichen mit 0 verglichen wird und falls das gelesene
Zeichen tatsächlich 0 war, an der Stelle lcd_flash_string_2 weiter gemacht wird. Im anderen Fall wird die bereits geschriebene Funktion '''lcd_data''' aufgerufen, welche das Zeichen ausgibt. '''lcd_data''' erwartet dabei das Zeichen im Register '''temp1''', genau in dem Register, in welches wir vorher mittels '''lpm''' das Zeichen geladen hatten.
Zeichen tatsächlich 0 war, an der Stelle <code>lcd_flash_string_2</code> weiter gemacht wird. Im anderen Fall wird die bereits geschriebene Funktion <code>lcd_data</code> aufgerufen, welche das Zeichen ausgibt. <code>lcd_data</code> erwartet dabei das Zeichen im Register <code>temp1</code>, genau in dem Register, in welches wir vorher mittels <code>lpm</code> das Zeichen geladen hatten.


Das verwendende Programm sieht dann so aus:
Das verwendende Programm sieht dann so aus:


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.include "m8def.inc"
.include "m8def.inc"
   
   
Zeile 1.677: Zeile 909:
           ldi ZH, HIGH(text*2)        ; Z-Pointer laden
           ldi ZH, HIGH(text*2)        ; Z-Pointer laden


           rcall lcd_flash_string      ; Unterprogramm gibt String aus der
           rcall lcd_flash_string      ; Unterprogramm gibt String aus, der
                                       ; durch den Z-Pointer adressiert wird
                                       ; durch den Z-Pointer adressiert wird
loop:
loop:
Zeile 1.689: Zeile 921:
</syntaxhighlight>
</syntaxhighlight>


Genaueres über die Verwendung unterschiedlicher Speicher findet sich im Kapitel [[AVR-Tutorial:_Speicher|Speicher]]
Genaueres über die Verwendung unterschiedlicher Speicher findet sich im Kapitel [[AVR-Tutorial: Speicher|Speicher]].


==Zahlen ausgeben==
==Zahlen ausgeben==
Um Zahlen, die beispielsweise in einem Register gespeichert sind, ausgeben zu können, ist es notwendig sich eine Textrepräsentierung der Zahl zu generieren. Die Zahl 123 wird also in den Text "123" umgewandelt welcher dann ausgegeben wird. Aus praktischen Gründen wird allerdings der Text nicht vollständig generiert (man müsste ihn ja irgendwo zwischenspeichern) sondern die einzelnen Buchstaben werden sofort ausgegeben, sobald sie bekannt sind.
Um Zahlen, die beispielsweise in einem Register gespeichert sind, ausgeben zu können, ist es notwendig, sich eine Textrepräsentierung der Zahl zu generieren. Die Zahl 123 wird also in den Text „123“ umgewandelt, welcher dann ausgegeben wird. Aus praktischen Gründen wird allerdings der Text nicht vollständig generiert (man müsste ihn ja irgendwo zwischenspeichern), sondern die einzelnen Buchstaben werden sofort ausgegeben, sobald sie bekannt sind.


===Dezimal ausgeben===
===Dezimal ausgeben===
Das Prinzip der Umwandlung ist einfach. Um herauszufinden wieviele Hunderter in der Zahl 123 enthalten sind, genügt es in einer Schleife immer wieder 100 von der Zahl abzuziehen und mitzuzählen wie oft dies gelang, bevor das Ergebnis negativ wurde. In diesem Fall lautet die Antwort: 1 mal, denn 123 - 100 macht 23. Versucht man erneut 100 anzuziehen, so ergibt sich eine negative Zahl.
Das Prinzip der Umwandlung ist einfach. Um herauszufinden, wieviele Hunderter in der Zahl 123 enthalten sind, genügt es, in einer Schleife immer wieder 100 von der Zahl abzuziehen und mitzuzählen, wie oft dies gelang, bevor das Ergebnis negativ wurde. In diesem Fall lautet die Antwort: 1-mal, denn 123 100 macht 23. Versucht man erneut 100 abzuziehen, so ergibt sich eine negative Zahl.
Also muss eine '1' ausgeben werden. Die verbleibenden 23 werden weiter behandelt, indem festgestellt wird wieviele Zehner darin enthalten sind. Auch hier wiederum: In einer Schleife solange 10 abziehen, bis das Ergebnis negativ wurde. Konkret geht das 2 mal gut, also muss das nächste auszugebende Zeichen ein '2' sein. Damit verbleiben noch die Einer, welche direkt in das entsprechende Zeichen umgewandelt werden können. In Summe hat man also an das Display die Zeichen '1' '2' '3' ausgegeben.
Also muss eine '1' ausgegeben werden. Die verbleibenden 23 werden weiter behandelt, indem festgestellt wird, wieviele Zehner darin enthalten sind. Auch hier wiederum: In einer Schleife solange 10 abziehen, bis das Ergebnis negativ wurde. Konkret geht das 2-mal gut, also muss das nächste auszugebende Zeichen eine '2' sein. Damit verbleiben noch die Einer, welche direkt in das entsprechende Zeichen umgewandelt werden können. In Summe hat man also an das Display die Zeichen '1' '2' '3' ausgegeben.


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
;**********************************************************************
;**********************************************************************
;
;
; Eine 8 Bit Zahl ohne Vorzeichen ausgeben
; Eine 8-Bit-Zahl ohne Vorzeichen ausgeben
;
;
; Übergabe:            Zahl im Register temp1
; Übergabe:            Zahl im Register temp1
Zeile 1.707: Zeile 939:
;
;
lcd_number:
lcd_number:
           push  temp1            ; die Funktion verändert temp1 und temp2,
           push  temp1            ; Die Funktion verändert temp1 und temp2,
           push  temp2            ; also sichern wir den Inhalt, um ihn am Ende
           push  temp2            ; also sichern wir den Inhalt, um ihn am Ende
                                   ; wieder herstellen zu können
                                   ; wieder herstellen zu können.


           mov  temp2, temp1    ; das Register temp1 frei machen
           mov  temp2, temp1    ; Das Register temp1 frei machen
                                   ; abzählen wieviele Hunderter
                                   ; Abzählen, wieviele Hunderter
                                   ; in der Zahl enthalten sind
                                   ; in der Zahl enthalten sind
;** Hunderter **  
;** Hunderter **  
Zeile 1.720: Zeile 952:
                                   ; Durchlauf eine '0' in temp1)
                                   ; Durchlauf eine '0' in temp1)
           subi  temp2, 100      ; 100 abziehen
           subi  temp2, 100      ; 100 abziehen
           brcc  lcd_number_1    ; ist dadurch kein Unterlauf entstanden?
           brcc  lcd_number_1    ; Ist dadurch kein Unterlauf entstanden?
                                   ; nein, dann zurück zu lcd_number_1
                                   ; nein, dann zurück zu lcd_number_1
           subi  temp2, -100      ; 100 wieder dazuzählen, da die
           subi  temp2, -100      ; 100 wieder dazuzählen, da die
                                   ; vorherhgehende Schleife 100 zuviel
                                   ; vorhergehende Schleife 100 zuviel
                                   ; abgezogen hat
                                   ; abgezogen hat
           rcall lcd_data        ; die Hunderterstelle ausgeben
           rcall lcd_data        ; die Hunderterstelle ausgeben
Zeile 1.733: Zeile 965:
                                   ; Durchlauf eine '0' in temp1)
                                   ; Durchlauf eine '0' in temp1)
           subi  temp2, 10        ; 10 abziehen
           subi  temp2, 10        ; 10 abziehen
           brcc  lcd_number_2    ; ist dadurch kein Unterlauf enstanden?
           brcc  lcd_number_2    ; Ist dadurch kein Unterlauf enstanden?
                                   ; nein, dann zurück zu lcd_number_2
                                   ; nein, dann zurück zu lcd_number_2
           subi  temp2, -10      ; 10 wieder dazuzählen, da die
           subi  temp2, -10      ; 10 wieder dazuzählen, da die
Zeile 1.741: Zeile 973:
   
   
;** Einer **         
;** Einer **         
           ldi  temp1, '0'      ; die Zahl in temp2 ist jetzt im Bereich
           ldi  temp1, '0'      ; Die Zahl in temp2 ist jetzt im Bereich
           add  temp1, temp2    ; 0 bis 9. Einfach nur den ASCII Code für
           add  temp1, temp2    ; 0 bis 9. Einfach nur den ASCII-Code für
           rcall lcd_data        ; '0' dazu addieren und wir erhalten dierekt
           rcall lcd_data        ; '0' dazu addieren und wir erhalten direkt
                                   ; den ASCII Code für die Ziffer
                                   ; den ASCII-Code für die Ziffer.




           pop  temp2            ; den gesicherten Inhalt von temp2 und temp1
           pop  temp2            ; Den gesicherten Inhalt von temp2 und temp1
           pop  temp1            ; wieder herstellen
           pop  temp1            ; wieder herstellen
           ret                    ; und zurück
           ret                    ; und zurück
</syntaxhighlight>
</syntaxhighlight>


Beachte: Diese Funktion benutzt wiederrum die Funktion '''lcd_data'''. Anders als bei den bisherigen Aufrufen ist '''lcd_number''' aber darauf angewiesen, dass '''lcd_data''' das Register '''temp2''' unangetastet lässt. Falls sie es noch nicht getan haben, dann ist das jetzt die perfekte Gelegenheit, '''lcd_data''' mit den entsprechenden '''push''' und '''pop''' Befehlen zu versehen. Sie sollten dies unbedingt zur Übung selbst machen. Am Ende muß die Funktion dann wie diese hier aussehen:
Beachte: Diese Funktion benutzt wiederum die Funktion <code>lcd_data</code>. Anders als bei den bisherigen Aufrufen ist <code>lcd_number</code> aber darauf angewiesen, dass <code>lcd_data</code> das Register <code>temp2</code> unangetastet lässt. Falls Sie es noch nicht getan haben, dann ist das jetzt die perfekte Gelegenheit, <code>lcd_data</code> mit den entsprechenden <code>push</code>- und <code>pop</code>-Befehlen zu versehen. Sie sollten dies unbedingt zur Übung selbst machen. Am Ende muß die Funktion dann wie diese hier aussehen:


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
  ;sendet ein Datenbyte an das LCD
  ;sendet ein Datenbyte an das LCD
lcd_data:
lcd_data:
           push  temp2
           push  temp2
           mov  temp2, temp1          ; "Sicherungskopie" für
           mov  temp2, temp1          ; "Sicherungskopie" für
                                         ; die Übertragung des 2.Nibbles
                                         ; die Übertragung des 2. Nibbles
           swap  temp1                  ; Vertauschen
           swap  temp1                  ; Vertauschen
           andi  temp1, 0b00001111      ; oberes Nibble auf Null setzen
           andi  temp1, 0b00001111      ; oberes Nibble auf Null setzen
Zeile 1.791: Zeile 1.023:
</syntaxhighlight>
</syntaxhighlight>


Kurz zur Funktionsweise der Funktion '''lcd_number''': Die Zahl in einem Register bewegt sich im Wertebereich 0 bis 255. Um herauszufinden, wie die Hunderterstelle lautet, zieht die Funktion einfach in einer Schleife immer wieder 100 von der Schleife ab, bis bei der Subtraktion ein Unterlauf, angezeigt durch das Setzen des Carry-Bits bei der Subtraktion, entsteht. Die Anzahl wird im Register '''temp1''' mitgezählt. Da dieses Register mit dem ASCII Code von '0' initialisiert wurde, und dieser ASCII Code bei jedem Schleifendurchlauf um 1 erhöht wird, können wir das Register '''temp1''' direkt zur Ausgabe des Zeichens für die Hunderterstelle durch die Funktion '''lcd_data''' benutzen. Völlig analog funktioniert auch die Ausgabe der Zehnerstelle.
Kurz zur Funktionsweise der Funktion <code>lcd_number</code>: Die Zahl in einem Register bewegt sich im Wertebereich 0 bis 255. Um herauszufinden, wie die Hunderterstelle lautet, zieht die Funktion einfach in einer Schleife immer wieder 100 von der Schleife ab, bis bei der Subtraktion ein Unterlauf, angezeigt durch das Setzen des Carry-Bits bei der Subtraktion, entsteht. Die Anzahl wird im Register <code>temp1</code> mitgezählt. Da dieses Register mit dem ASCII-Code von '0' initialisiert wurde, und dieser ASCII-Code bei jedem Schleifendurchlauf um 1 erhöht wird, können wir das Register <code>temp1</code> direkt zur Ausgabe des Zeichens für die Hunderterstelle durch die Funktion <code>lcd_data</code> benutzen. Völlig analog funktioniert auch die Ausgabe der Zehnerstelle.


===Unterdrückung von führenden Nullen===
===Unterdrückung von führenden Nullen===


Diese Funktion gibt jede Zahl im Register '''temp1''' immer mit 3 Stellen aus. Führende Nullen werden nicht unterdrückt. Möchte man dies ändern, so ist das ganz leicht möglich: Vor Ausgabe der Hunderterstelle muss lediglich überprüft werden, ob die Entsprechende Ausgabe eine '0' wäre. Ist sie das, so wird die Ausgabe übersprungen. Ist es allerdings eine Zahl 1..9, so muss sie der Zehner Stelle signalisieren, daß eine Prüfung auf eine '0' nicht stattfinden darf. Und dazu wird das T-Flag im SREG genutzt. Lediglich in der Einerstelle wird jede Ziffer wie errechnet ausgegeben.
Diese Funktion gibt jede Zahl im Register <code>temp1</code> immer mit 3 Stellen aus. Führende Nullen werden nicht unterdrückt. Möchte man dies ändern, so ist das ganz leicht möglich: Vor Ausgabe der Hunderterstelle muss lediglich überprüft werden, ob die entsprechende Ausgabe eine '0' wäre. Ist sie das, so wird die Ausgabe übersprungen. Ist es allerdings eine Zahl 1…9, so muss sie der Zehnerstelle signalisieren, daß eine Prüfung auf eine '0' nicht stattfinden darf. Und dazu wird das T-Flag im SREG genutzt. Lediglich in der Einerstelle wird jede Ziffer wie errechnet ausgegeben.


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
           ...
           ...
                                   ; die Hunderterstelle ausgeben, wenn
                                   ; die Hunderterstelle ausgeben, wenn
Zeile 1.805: Zeile 1.037:
           breq  lcd_number_1a
           breq  lcd_number_1a
           rcall lcd_data        ; die Hunderterstelle ausgeben
           rcall lcd_data        ; die Hunderterstelle ausgeben
           set                    ; T-Flag im SREG setzen da 100er Stelle eine
           set                    ; T-Flag im SREG setzen, da 100er-Stelle eine
                                   ; 1..9 war
                                   ; 1..9 war


Zeile 1.813: Zeile 1.045:
           ...
           ...
           brts  lcd_number_2a    ; Test auf '0' überspringen, da 100er eine
           brts  lcd_number_2a    ; Test auf '0' überspringen, da 100er eine
                                   ; 1..9 war (unbedingt anzeigen
                                   ; 1..9 war (unbedingt anzeigen,
                                   ; auch wenn der Zehner eine '0' ist)
                                   ; auch wenn der Zehner eine '0' ist)
           cpi  temp1, '0'      ; ansonsten Test auf '0'
           cpi  temp1, '0'      ; ansonsten Test auf '0'
Zeile 1.824: Zeile 1.056:
</syntaxhighlight>
</syntaxhighlight>


 
Das Verfahren, die einzelnen Stellen durch Subtraktion zu bestimmen, ist bei kleinen Zahlen eine durchaus gängige Alternative. Vor allem dann, wenn keine hardwaremäßige Unterstützung für Multiplikation und Division zur Verfügung steht. Ansonsten könnte man die einzelnen Ziffern auch durch Division bestimmen. Das Prinzip ist folgendes (beispielhaft an der Zahl 52783 gezeigt):
Das Verfahren, die einzelnen Stellen durch Subtraktion zu bestimmen, ist bei kleinen Zahlen eine durchaus gängige Alternative. Vor allem dann, wenn keine hardwaremäßige Unterstützung für Multiplikation und Division zur Verfügung steht. Ansonsten könnte man die die einzelnen Ziffern auch durch Division bestimmen. Das Prinzip ist folgendes (beispielhaft an der Zahl 52783 gezeigt)


<pre>
<pre>
   52783 / 10          -> 5278
   52783 / 10          -> 5278
   52783 - 5278 * 10  ->          3
   52783 5278 * 10  ->          3


   5278 / 10          -> 527
   5278 / 10          -> 527
   5278 - 527 * 10    ->          8
   5278 527 * 10    ->          8


   527 / 10            -> 52
   527 / 10            -> 52
   527 - 52 * 10      ->          7
   527 52 * 10      ->          7


   52 / 10            -> 5
   52 / 10            -> 5
   52 - 5 * 10        ->          2
   52 5 * 10        ->          2


   5 / 10              -> 0
   5 / 10              -> 0
   5 - 0 * 10          ->          5
   5 0 * 10          ->          5
</pre>
</pre>


Das Prinzip ist also die Restbildung bei einer fortgesetzten Division durch 10, wobei die einzelnen Ziffern in umgekehrter Reihenfolge ihrer Wertigkeit entstehen. Dadurch hat man aber ein Problem: Damit die Zeichen in der richtigen Reihenfolge ausgegeben werden können, muß man sie meistens zwischenspeichern um sie in der richtigen Reihenfole ausgeben zu können. Wird die Zahl in einem Feld von immer gleicher Größe ausgegeben, dann kann man auch die Zahl von rechts nach links ausgeben (bei einem LCD ist das möglich).
Das Prinzip ist also die Restbildung bei einer fortgesetzten Division durch 10, wobei die einzelnen Ziffern in umgekehrter Reihenfolge ihrer Wertigkeit entstehen. Dadurch hat man aber ein Problem: Damit die Zeichen in der richtigen Reihenfolge ausgegeben werden können, muß man sie meistens zwischenspeichern, um sie in der richtigen Reihenfolge ausgeben zu können. Wird die Zahl in einem Feld von immer gleicher Größe ausgegeben, dann kann man auch die Zahl von rechts nach links ausgeben (bei einem LCD ist das möglich).


===Hexadezimal ausgeben===
===Hexadezimal ausgeben===


Zu guter letzt hier noch eine Funktion, die eine Zahl aus dem Register '''temp1''' in hexadezimaler Form ausgibt. Die Funktion weist keine Besonderheiten auf und sollte unmittelbar verständlich sein.
Zuguterletzt hier noch eine Funktion, die eine Zahl aus dem Register <code>temp1</code> in hexadezimaler Form ausgibt. Die Funktion weist keine Besonderheiten auf und sollte unmittelbar verständlich sein.


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
;**********************************************************************
;**********************************************************************
;
;
; Eine 8 Bit Zahl ohne Vorzeichen hexadezimal ausgeben
; Eine 8-Bit-Zahl ohne Vorzeichen hexadezimal ausgeben
;
;
; Übergabe:            Zahl im Register temp1
; Übergabe:            Zahl im Register temp1
Zeile 1.869: Zeile 1.100:
           cpi  temp1, 10
           cpi  temp1, 10
           brlt  lcd_number_hex_digit_1
           brlt  lcd_number_hex_digit_1
           subi  temp1, -( 'A' - '9' - 1 ) ; es wird subi mit negativer
           subi  temp1, -( 'A' - '9' - 1 ) ; Es wird subi mit negativer
                                           ; Konstante verwendet,
                                           ; Konstante verwendet,
                                           ; weil es kein addi gibt
                                           ; weil es kein addi gibt
lcd_number_hex_digit_1:
lcd_number_hex_digit_1:
           subi  temp1, -'0'              ; ditto
           subi  temp1, -'0'              ; dito
           rcall  lcd_data
           rcall  lcd_data
            
            
Zeile 1.881: Zeile 1.112:


===Binär ausgeben===
===Binär ausgeben===
Um die Sache komplett zu machen; Hier eine Routine mit der man eine 8 Bit-Zahl binär auf das LC-Display ausgeben kann:
Um die Sache komplett zu machen; hier eine Routine, mit der man eine 8-Bit-Zahl binär auf das LC-Display ausgeben kann:
<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
;**********************************************************************
;**********************************************************************
;
;
; Eine 8 Bit Zahl ohne Vorzeichen binär ausgeben
; Eine 8-Bit-Zahl ohne Vorzeichen binär ausgeben
;
;
; Übergabe:            Zahl im Register temp1
; Übergabe:            Zahl im Register temp1
Zeile 1.927: Zeile 1.158:
</syntaxhighlight>
</syntaxhighlight>


===Eine 16-Bit Zahl aus einem Registerpärchen ausgeben===
===Eine 16-Bit-Zahl aus einem Registerpärchen ausgeben===


Um eine 16 Bit Zahl auszugeben wird wieder das bewährte Schema benutzt die einzelnen Stellen durch Subtraktion abzuzählen. Da es sich hierbei allerdings um eine 16 Bit Zahl handelt, müssen die Subtraktionen als 16-Bit Arithmetik ausgeführt werden.
Um eine 16-Bit-Zahl auszugeben wird wieder das bewährte Schema benutzt, die einzelnen Stellen durch Subtraktion abzuzählen. Da es sich hierbei allerdings um eine 16-Bit-Zahl handelt, müssen die Subtraktionen als 16-Bit-Arithmetik ausgeführt werden.


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
;**********************************************************************
;**********************************************************************
;
;
; Eine 16 Bit Zahl ohne Vorzeichen ausgeben
; Eine 16-Bit-Zahl ohne Vorzeichen ausgeben
;
;
; Übergabe:            Zahl im Register temp2 (low Byte) / temp3 (high Byte)
; Übergabe:            Zahl im Register temp2 (low Byte) / temp3 (high Byte)
Zeile 1.998: Zeile 1.229:
</syntaxhighlight>
</syntaxhighlight>


===Eine BCD Zahl ausgeben===
===Eine BCD-Zahl ausgeben===


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
;**********************************************************************
;**********************************************************************
;
;
; Übergabe:            BCD Zahl in temp1
; Übergabe:            BCD-Zahl in temp1
; veränderte Register: keine
; veränderte Register: keine
;
;
Zeile 2.027: Zeile 1.258:
[[Bild:LCD_Character_Grid.png | framed | right| Zeichenraster für 1 Zeichen]]
[[Bild:LCD_Character_Grid.png | framed | right| Zeichenraster für 1 Zeichen]]


Das LCD erlaubt für spezielle Zeichen, welche sich nicht im Zeichensatz finden, eigene Zeichen zu definieren. Dazu werden die ersten 8 ASCII Codes reserviert, auf denen sich laut ASCII Tabelle spezielle Steuerzeichen befinden, die normalerweise keine sichtbare Anzeige hervorrufen sondern zur Steuerung von angeschlossenen Geräten dienen. Da diese Zeichen auf einem LCD keine Rolle spielen, können diese Zeichen benutzt werden um sich selbst Sonderzeichen zu erzeugen, die für die jeweilige Anwendung massgeschneidert sind.
Das LCD erlaubt für spezielle Zeichen, welche sich nicht im Zeichensatz finden, eigene Zeichen zu definieren. Dazu werden die ersten 8 ASCII-Codes reserviert, auf denen sich laut ASCII-Tabelle spezielle Steuerzeichen befinden, die normalerweise keine sichtbare Anzeige hervorrufen, sondern zur Steuerung von angeschlossenen Geräten dienen. Da diese Zeichen auf einem LCD keine Rolle spielen, können diese Zeichen benutzt werden, um sich selbst Sonderzeichen zu erzeugen, die für die jeweilige Anwendung maßgeschneidert sind.


Das LCD stellt für jedes Zeichen eine 8*5 Matrix zur Verfügung. Um sich selbst massgeschneiderte Zeichen zu erstellen, ist es am einfachsten sich zunächst auf einem Stück karriertem Papier zu erstellen.
Das LCD stellt für jedes Zeichen eine 8×5-Matrix zur Verfügung. Um sich selbst maßgeschneiderte Zeichen zu erstellen, ist es am einfachsten, sie zunächst auf einem Stück kariertem Papier zu erstellen.


[[Bild:BellCharacter.png | framed | right| Zeichenraster für ein Glockensymbol]]
[[Bild:BellCharacter.png | framed | right| Zeichenraster für ein Glockensymbol]]


In diesem Raster markiert man sich dann diejenigen Pixel, die im fertigen Zeichen dunkel erscheinen sollen. Als Beispiel sei hier ein Glockensymbol gezeichnet, welches in einer Telefonapplikation zb als Kennzeichnung für einen Anruf dienen könnte.
In diesem Raster markiert man sich dann diejenigen Pixel, die im fertigen Zeichen dunkel erscheinen sollen. Als Beispiel sei hier ein Glockensymbol gezeichnet, welches in einer Telefonapplikation z.&nbsp;B. als Kennzeichnung für einen Anruf dienen könnte.


Eine Zeile in diesem Zeichen repräsentiert ein an das LCD zu übergebendes Byte, wobei nur die Bits 0 bis 4 relevant sind. Gesetzte Pixel stellen ein 1 Bit dar, nicht gesetzte Pixel sind ein 0-Bit. Das niederwertigste Bit einer Zeile befindet sich rechts. Auf diese Art wird jede Zeile in eine Binärzahl übersetzt, und 8 Bytes repräsentieren ein komplettes Zeichen. Am Beispiel des Glockensymboles: Die 8 Bytes, welches das Symbol repräsentiern, lauten: 0x00, 0x04, 0x0A, 0x0A, 0x0A, 0x1F, 0x04, 0x00,
Eine Zeile in diesem Zeichen repräsentiert ein an das LCD zu übergebendes Byte, wobei nur die Bits 0 bis 4 relevant sind. Gesetzte Pixel stellen ein 1-Bit dar, nicht gesetzte Pixel sind ein 0-Bit. Das niederwertigste Bit einer Zeile befindet sich rechts. Auf diese Art wird jede Zeile in eine Binärzahl übersetzt, und 8 Bytes repräsentieren ein komplettes Zeichen. Am Beispiel des Glockensymboles: Die 8 Bytes, welche das Symbol repräsentieren, lauten: 0x00, 0x04, 0x0A, 0x0A, 0x0A, 0x1F, 0x04, 0x00.


Dem LCD wird die neue Definition übertragen, indem man dem LCD die 'Schreibposition' mittels des Kommandos ''Character RAM Address Set'' in den Zeichensatzgenerator verschiebt. Danach werden die 8 Bytes ganz normal als Daten ausgegeben, die das LCD damit in seine Zeichensatztabelle schreibt.
Dem LCD wird die neue Definition übertragen, indem man dem LCD die „Schreibposition“ mittels des Kommandos ''Character RAM Address Set'' in den Zeichensatzgenerator verschiebt. Danach werden die 8 Bytes ganz normal als Daten ausgegeben, die das LCD damit in seine Zeichensatztabelle schreibt.


Durch die Wahl der Speicheradresse definiert man, welches Zeichen (0 bis 7) man eigentlich durch eine eigene Definition ersetzen will.
Durch die Wahl der Speicheradresse definiert man, welches Zeichen (0 bis 7) man eigentlich durch eine eigene Definition ersetzen will.
{| {{Tabelle}}
{| {{Tabelle}}
|-  style="background-color:#ffddcc"
|-  style="background-color:#ffddcc"
! ASCII Code || Zeichensatzadresse
! ASCII-Code || Zeichensatzadresse
|-
|-
| 0 || 0x00
| 0 || 0x00
Zeile 2.061: Zeile 1.292:
|}
|}


Nach erfolgter Definition des Zeichens, muss die Schreibposition wieder explizit in den DDRAM-Bereich gesetzt werden.
Nach erfolgter Definition des Zeichens muss die Schreibposition wieder explizit in den DDRAM-Bereich gesetzt werden.
Danach kann ein entsprechendes Zeichen mit dem definierten ASCII Code ausgegeben werden, wobei das LCD die von uns definierte Pixelform zur Anzeige benutzt.
Danach kann ein entsprechendes Zeichen mit dem definierten ASCII-Code ausgegeben werden, wobei das LCD die von uns definierte Pixelform zur Anzeige benutzt.


Zuerst müssen natürlich erstmal die Zeichen definiert werden.
Zuerst müssen natürlich erstmal die Zeichen definiert werden.
Dieses geschieht einmalig durch den Aufruf der Routine "lcd_load_user_chars"
Dieses geschieht einmalig durch den Aufruf der Routine <code>lcd_load_user_chars</code>
unmittelbar nach der Initialisierung des LCD-Displays.
unmittelbar nach der Initialisierung des LC-Displays.


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
           .
           .
           .
           .
           rcall lcd_init              ; Display initialisieren
           rcall lcd_init              ; Display initialisieren
           rcall lcd_load_user_chars  ; User Zeichen in das Display laden
           rcall lcd_load_user_chars  ; User-Zeichen in das Display laden
           rcall lcd_clear            ; Display löschen
           rcall lcd_clear            ; Display löschen
           .
           .
Zeile 2.079: Zeile 1.310:


Durch diesen Aufruf werden die im Flash definierten Zeichen in den
Durch diesen Aufruf werden die im Flash definierten Zeichen in den
GC-Ram übertragen. Diese Zeichen werden ab Adresse 0 im GC-Ram
CG-RAM übertragen. Diese Zeichen werden ab Adresse 0 im CG-RAM
gespeichert und sind danach wie jedes andere Zeichen nutzbar.
gespeichert und sind danach wie jedes andere Zeichen nutzbar.


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
           .
           .
           .
           .
Zeile 2.105: Zeile 1.336:
</syntaxhighlight>
</syntaxhighlight>


Jetzt sollte der Schriftzug "AVR-MEGA"
Jetzt sollte der Schriftzug „AVR-MEGA“
verkehrt herum (180 Grad gedreht) erscheinen.
verkehrt herum (180° gedreht) erscheinen.


Es fehlt natürlich noch die Laderoutine:
Es fehlt natürlich noch die Laderoutine:


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
;**********************************************************************
;**********************************************************************
;
;
; Lädt User Zeichen in den GC-Ram des LCD bis Tabellenende (0xFF)
; Lädt User-Zeichen in den CG-RAM des LCD, bis Tabellenende (0xFF)
; gelesen wird. (max. 8 Zeichen können geladen werden)
; gelesen wird (max. 8 Zeichen können geladen werden).
;
;
; Übergabe:            -   
; Übergabe:            -   
Zeile 2.158: Zeile 1.389:
</syntaxhighlight>
</syntaxhighlight>


... und die Zeichendefinition:
und die Zeichendefinition:


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
ldc_user_char:
ldc_user_char:
                               ;    Zeichen  
                               ;    Zeichen  
Zeile 2.212: Zeile 1.443:
==Der überarbeitete, komplette Code==
==Der überarbeitete, komplette Code==


Hier also die komplett überarbeitete Version der LCD Funktionen.
Hier also die komplett überarbeitete Version der LCD-Funktionen.


Die für die Benutzung relevanten Funktionen
Die für die Benutzung relevanten Funktionen
* '''lcd_init'''
* '''<code>lcd_init</code>'''
* '''lcd_clear'''
* '''<code>lcd_clear</code>'''
* '''lcd_home'''
* '''<code>lcd_home</code>'''
* '''lcd_data'''
* '''<code>lcd_data</code>'''
* '''lcd_command'''
* '''<code>lcd_command</code>'''
* '''lcd_flash_string'''
* '''<code>lcd_flash_string</code>'''
* '''lcd_number'''
* '''<code>lcd_number</code>'''
* '''lcd_number_hex'''
* '''<code>lcd_number_hex</code>'''
sind so ausgeführt, dass sie kein Register (ausser dem Statusregister '''SREG''') verändern. Die bei manchen Funktionen notwendige Argumente werden immer im Register '''temp1''' übergeben, wobei '''temp1''' vom Usercode definiert werden muss.
sind so ausgeführt, dass sie kein Register (außer dem Statusregister '''SREG''') verändern. Die bei manchen Funktionen notwendigen Argumente werden immer im Register '''<code>temp1</code>''' übergeben, wobei <code>temp1</code> vom Usercode definiert werden muss.


[[Media:lcd-routines.asm|Download lcd-routines.asm]]
[[Media:lcd-routines.asm|Download lcd-routines.asm]]

Aktuelle Version vom 4. Februar 2023, 10:15 Uhr

Kaum ein elektronisches Gerät kommt heutzutage noch ohne ein LCD daher. Ist doch auch praktisch, Informationen im Klartext anzeigen zu können, ohne irgendwelche LEDs blinken zu lassen. Kein Wunder also, dass die häufigste Frage in Mikrocontroller-Foren ist: „Wie kann ich ein LCD anschließen?“

Das LCD und sein Controller

Die meisten Text-LCDs verwenden den Controller HD44780 oder einen kompatiblen (z. B. KS0070) und haben 14 oder 16 Pins.

Die Pinbelegung ist meist (Ausnahme z. B. TC1602E (Pollin 120420): VDD und VSS vertauscht) folgendermaßen:

ACHTUNG: Es gibt Displays mit abweichender Anschluss-Belegung, falscher Anschluss kann zur Zerstörung führen! Daher immer das zugehörige Datenblatt zu Rate ziehen!

Einzelheiten im Artikel zum Controller HD44780.

Pin # Bezeichnung Funktion
1 VSS (selten: VDD) GND (selten: +5 V)
2 VDD (selten: VSS) +5 V (selten: GND)
3 VEE, V0, V5 Kontrastspannung (-5 V / 0 V bis 5 V)
4 RS Register Select (0=Befehl/Status 1=Daten)
5 RW 1=Read 0=Write
6 E 0=Disable 1=Enable
7 DB0 Datenbit 0
8 DB1 Datenbit 1
9 DB2 Datenbit 2
10 DB3 Datenbit 3
11 DB4 Datenbit 4
12 DB5 Datenbit 5
13 DB6 Datenbit 6
14 DB7 Datenbit 7
15 A LED-Beleuchtung, meist Anode
16 K LED-Beleuchtung, meist Kathode

Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist das Pin-1-Pad eckig oder daneben eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen.

Bei der DIL-Version (Wannenstecker mit 2×7 oder 2×8 Kontakten) auch darauf achten, auf welcher Platinen-Seite der Stecker montiert wird: auf der falschen (meist hinteren) Seite sind dann die Flachbandleitungen 1 und 2, 3 und 4 usw. vertauscht (so z. B. beim ANAG VISION AV1623YFTY-SJW, Pollin 121714). Das kann man kompensieren, indem man es auf der anderen Kabelseite genauso permutiert oder es auf dem Layout bewusst so legt (Stecker auf der Bottom-Seite plazieren). Man kann es nicht kompensieren, indem man das Flachbandkabel auf der anderen Seite in den Stecker führt.

Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen. Die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist.

VSS wird ganz einfach an GND angeschlossen und VCC=VDD an +5 V. VEE = V0 = V5 kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte, muss man ein 10-kΩ-Potentiometer zwischen GND und 5 V schalten, mit dem Schleifer an VEE. Meist kann man den +5-V-Anschluss am Poti weglassen, da im Display ein Pull-up-Widerstand ist:

Gewinnung der Kontrastspannung

Wenn der Kontrast zu schwach sein sollte (z. B. bei tiefen Temperaturen oder bei Betrieb mit 3.3 V), kann man anstelle von GND eine negative Spannung ans Kontrast-Poti legen. Diese kann bis −5 V gehen und kann leicht aus einem Timerpin des µC, einem Widerstand, zwei Dioden und zwei Kondensatoren erzeugt werden. So wird auch ein digital einstellbarer Kontrast mittels PWM ermöglicht. ACHTUNG: Es gibt jedoch auch Displaycontroller wie den Epson SED1278, die zwar Software-kompatibel sind, aber keine negativen Kontrastspannung verkraften. Wird der Kontrast also bei negativer Spannung schlechter oder geht das Display ganz aus, ist davon auszugehen, dass der Controller diesen Betriebsmodus nicht unterstützt.

Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den 8-Bit- und den 4-Bit-Modus.

  • Für den 8-Bit-Modus werden (wie der Name schon sagt) alle acht Datenleitungen zur Ansteuerung verwendet, somit kann durch einen Zugriff immer ein ganzes Byte übertragen werden.
  • Der 4-Bit-Modus verwendet nur die oberen vier Datenleitungen (DB4–DB7). Um ein Byte zu übertragen, braucht man somit zwei Zugriffe, wobei zuerst das höherwertige Nibble (= 4 Bits), also Bit 4 bis Bit 7, übertragen wird und dann das niederwertige, also Bit 0 bis Bit 3. Die unteren Datenleitungen des LCDs, die beim Lesezyklus Ausgänge sind, lässt man offen (siehe Datasheets, z. B. vom KS0070).

Der 4-Bit-Modus hat den Vorteil, dass man vier IO-Pins weniger benötigt als beim 8-Bit-Modus. 6 bzw. 7 Pins (eines Portes) reichen aus.

Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse RS, RW und E benötigt.

  • Über RS (Register Select) wird ausgewählt, ob man einen Befehl oder ein Datenbyte an das LCD schicken möchte. Beim Schreiben gilt: ist RS low, dann wird das ankommende Byte als Befehl interpretiert; ist RS high, wird das Byte auf dem LCD angezeigt (genauer: ins Data-Register geschrieben, kann auch für den CG bestimmt sein).
  • RW (Read/Write) legt fest, ob geschrieben oder gelesen werden soll. High bedeutet lesen, low bedeutet schreiben. Wenn man RW auf lesen einstellt und RS auf Befehl, dann kann man das Busy-Flag an DB7 lesen, das anzeigt, ob das LCD den vorhergehenden Befehl fertig verarbeitet hat. Ist RS auf Daten eingestellt, dann kann man z. B. den Inhalt des Displays lesen – was jedoch nur in den wenigsten Fällen Sinn macht. Deshalb kann man RW dauerhaft auf low lassen (= an GND anschließen), so dass man noch einen IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl ca. 50 µs (beim Return Home 2 ms, beim Clear Display 20 ms) warten sollte, um dem LCD Zeit zum Ausführen des Befehls zu geben. Dummerweise schwankt die Ausführungszeit von Display zu Display und ist auch von der Betriebsspannung abhängig. Für professionellere Sachen also lieber den IO-Pin opfern und Busy abfragen.
  • Der Anschluss E (Enable) schließlich signalisiert dem LCD, dass die übrigen Datenleitungen jetzt korrekte Pegel angenommen haben und es die gewünschten Daten bzw. Kommandos von den Datenleitungen übernehmen kann. Beim Lesen gibt das Display die Daten bzw. den Status so lange aus, wie E high ist. Beim Schreiben übernimmt das Display die Daten mit der fallenden Flanke.

Anschluss an den Controller

Jetzt, da wir wissen, welche Anschlüsse das LCD benötigt, können wir das LCD mit dem Mikrocontroller verbinden:

ACHTUNG: Es gibt Displays mit abweichender Anschluss-Belegung (z. B. TC1602E, Pollin 120420: Vdd und Vss vertauscht), falscher Anschluss kann zur Zerstörung führen! Daher immer das zugehörige Datenblatt zu Rate ziehen.

Einzelheiten im Artikel zum Controller HD44780.

Pinnummer
LCD
Bezeichnung Anschluss
1 VSS GND (beim TC1602E: VCC)
2 VDD = VCC +5 V (beim TC1602E: Gnd)
3 V0 = VEE GND, Poti oder PWM am AVR
4 RS PD4 am AVR
5 RW GND
6 E PD5 am AVR
7 DB0 nicht angeschlossen
8 DB1 nicht angeschlossen
9 DB2 nicht angeschlossen
10 DB3 nicht angeschlossen
11 DB4 PD0 am AVR
12 DB5 PD1 am AVR
13 DB6 PD2 am AVR
14 DB7 PD3 am AVR
15 A Vorsicht! Meistens nicht direkt an +5 V anschließbar,
sondern nur über einen Vorwiderstand, der an die Daten
der Hintergrundbeleuchtung angepasst werden muss.
16 K GND

OK. Alles ist verbunden. Wenn man jetzt den Strom einschaltet, sollten ein oder zwei schwarze Balken auf dem Display angezeigt werden.

Doch wie bekommt man jetzt die Befehle und Daten in das Display? Dazu muss das LCD initialisiert werden und man muss Befehle (Commands) und seine Daten an das LCD senden. Weil die Initialisierung ein Spezialfall der Übertragung von Befehlen ist, im Folgenden zunächst die Erklärung für die Übertragung von Werten an das LCD.

Ansteuerung des LCDs im 4-Bit-Modus

Um ein Byte zu übertragen, muss man es erstmal in die beiden Nibbles zerlegen, die getrennt übertragen werden. Da das obere Nibble (Bit 4 … Bit 7) als erstes übertragen wird, die vier Datenleitungen jedoch an die vier unteren Bits des Port D angeschlossen sind, muss man die beiden Nibbles des zu übertragenden Bytes erstmal vertauschen. Der AVR kennt dazu praktischerweise einen eigenen Befehl:

           swap r16               ; vertauscht die beiden Nibbles von r16

Aus 0b00100101 wird so z. B. 0b01010010.

Jetzt sind die Bits für die erste Phase der Übertragung an der richtigen Stelle. Trotzdem wollen wir das Ergebnis nicht einfach so mit out PORTD, r16 an den Port geben. Um die Hälfte des Bytes, die jetzt nicht an die Datenleitungen des LCDs gegeben wird, auf null zu setzen, verwendet man folgenden Befehl:

           andi r16, 0b00001111   ; Nur die vier unteren (mit 1 markierten)
                                  ; Bits werden übernommen, alle anderen werden null.

Also: Das obere Nibble wird erst mit dem unteren vertauscht, damit es unten ist. Dann wird das obere (das wir jetzt noch nicht brauchen) auf null gesetzt.

Jetzt müssen wir dem LCD noch mitteilen, ob wir Daten oder Befehle senden wollen. Das machen wir, indem wir das Bit, an dem RS angeschlossen ist (PD4), auf 0 (Befehl senden) oder auf 1 (Daten senden) setzen. Um ein Bit in einem normalen Register zu setzen, gibt es den Befehl sbr (Set Bits in Register). Dieser Befehl unterscheidet sich jedoch von sbi (das nur für IO-Register gilt) dadurch, dass man nicht die Nummer des zu setzenden Bits angibt, sondern eine Bitmaske. Das geht so:

           sbr r16, 0b00010000     ; Bit 4 setzen, alle anderen Bits bleiben gleich.

RS ist an PD4 angeschlossen. Wenn wir r16 an den Port D ausgeben, ist RS jetzt also high und das LCD erwartet Daten anstatt von Befehlen.

Das Ergebnis können wir jetzt endlich direkt an den Port D übergeben:

           out PORTD, r16

Natürlich muss vorher der Port D auf Ausgang geschaltet werden, indem man 0xFF ins Datenrichtungsregister DDRD schreibt.

Um dem LCD zu signalisieren, dass es das an den Datenleitungen anliegende Nibble übernehmen kann, wird die E-Leitung (Enable, an PD5 angeschlossen) auf high und kurz darauf wieder auf low gesetzt. Ein Puls an dieser Leitung teilt also dem LCD mit, dass die restlichen Leitungen jetzt ihren vom Programm gewollten Pegel eingenommen haben und gültig sind.

           sbi PORTD, 5              ; Enable high
           nop                       ; 3 Taktzyklen warten ("nop" = nichts tun)
           nop
           nop
           cbi PORTD, 5              ; Enable wieder low

Die eine Hälfte des Bytes wäre damit geschafft! Die andere Hälfte kommt direkt hinterher: Alles, was an der obenstehenden Vorgehensweise geändert werden muss, ist, das „swap“ (Vertauschen der beiden Nibbles) wegzulassen.

Initialisierung des Displays

Allerdings gibt es noch ein Problem. Wenn ein LCD eingeschaltet wird, dann läuft es zunächst im 8-Bit-Modus. Irgendwie muss das Display initialisiert und auf den 4-Bit-Modus umgeschaltet werden, und zwar nur mit den 4 zur Verfügung stehenden Datenleitungen.

Wenn es Probleme gibt, dann meistens an diesem Punkt. Die „kompatiblen“ Kontroller sind gelegentlich doch nicht 100 % identisch. Es lohnt sich, das Datenblatt (siehe Weblinks im Artikel LCD) genau zu lesen, in welcher Reihenfolge und mit welchen Abständen (Delays) die Initialisierungsbefehle gesendet werden. Eine weitere Hilfe können Ansteuerungsbeispiele in Forenbeiträgen geben, z. B.

Initialisierung für 4-Bit-Modus

Achtung: Im Folgenden sind alle Bytes aus Sicht des LCD-Kontrollers angegeben! Da LCD-seitig nur die Leitungen DB4–DB7 verwendet werden, ist daher immer nur das höherwertige Nibble gültig. Durch die Art der Verschaltung (DB4–DB7 wurde auf dem PORT an PD0 bis PD3 angeschlossen) ergibt sich eine Verschiebung, so dass das am Kontroller auszugebende Byte nibblemäßig vertauscht ist!

Die Sequenz, aus Sicht des Kontrollers, sieht so aus:

  • Nach dem Anlegen der Betriebsspannung muss eine Zeit von mindestens ca. 15 ms gewartet werden, um dem LCD-Kontroller Zeit für seine eigene Initialisierung zu geben.
  • $3 ins Steuerregister schreiben (RS = 0)
  • Mindestens 4,1 ms warten
  • $3 ins Steuerregister schreiben (RS = 0)
  • Mindestens 100 µs warten
  • $3 ins Steuerregister schreiben (RS = 0)
  • $2 ins Steuerregister schreiben (RS = 0), dadurch wird auf 4-Bit-Daten umgestellt.
  • Ab jetzt muss für die Übertragung eines Bytes jeweils zuerst das höherwertige Nibble und dann das niederwertige Nibble übertragen werden, wie oben beschrieben.
  • Mit dem Konfigurier-Befehl $20 das Display konfigurieren (4-Bit, 1 oder 2 Zeilen, 5×7-Format)
  • Mit den restlichen Konfigurierbefehlen die Konfiguration vervollständigen: Display ein/aus, Cursor ein/aus, etc.

Eine Begründung, warum die ersten Befehle dreifach geschickt werden sollen, findet sich im Forum und in der englischen Wikipedia.

Initialisierung für 8-Bit-Modus

Der Vollständigkeit halber hier noch die notwendige Initialisierungssequenz für den 8-Bit-Modus. Da hier die Daten komplett als 1 Byte übertragen werden können, sind einige Klimmzüge wie im 4-Bit-Modus nicht notwendig. Begründung für die anfänglichen Wiederholungen siehe oben.

  • Nach dem Anlegen der Betriebsspannung muss eine Zeit von mindestens ca. 15 ms gewartet werden, um dem LCD-Kontroller Zeit für seine eigene Initialisierung zu geben.
  • $30 ins Steuerregister schreiben (RS = 0)
  • Mindestens 4,1 ms warten
  • $30 ins Steuerregister schreiben (RS = 0)
  • Mindestens 100 µs warten
  • $30 ins Steuerregister schreiben (RS = 0)
  • Mit dem Konfigurier-Befehl 0x30 das Display konfigurieren (8-Bit, 1 oder 2 Zeilen, 5×7-Format)
  • Mit den restlichen Konfigurierbefehlen die Konfiguration vervollständigen: Display ein/aus, Cursor ein/aus, etc.

Routinen zur LCD-Ansteuerung im 4-Bit-Modus

Im Folgenden werden die bisherigen Grundroutinen zur LCD-Ansteuerung im 4-Bit-Modus zusammengefasst und kommentiert. Die darin enthaltenen Symbole (temp1, PORTD, …) müssen in einem dazugehörenden Hauptprogramm definiert werden. Dies wird nächsten Abschnitt Anwendung weiter erklärt.

 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                 LCD-Routinen                ;;
;;                 ============                ;;
;;              (c)andreas-s@web.de            ;;
;;                                             ;;
;; 4bit-Interface                              ;;
;; DB4-DB7:       PD0-PD3                      ;;
;; RS:            PD4                          ;;
;; E:             PD5                          ;;
;;                                             ;;
;; Takt:          4 MHz                        ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

 
 
 ;sendet ein Datenbyte an das LCD
lcd_data:
           mov temp2, temp1             ; "Sicherungskopie" für
                                        ; die Übertragung des 2. Nibbles
           swap temp1                   ; Vertauschen
           andi temp1, 0b00001111       ; oberes Nibble auf Null setzen
           sbr temp1, 1<<4              ; entspricht 0b00010000 (Anm. 1)
           out PORTD, temp1             ; ausgeben
           rcall lcd_enable             ; Enable-Routine aufrufen
                                        ; 2. Nibble, kein swap, da es schon
                                        ; an der richtigen Stelle ist
           andi temp2, 0b00001111       ; obere Hälfte auf Null setzen
           sbr temp2, 1<<4              ; entspricht 0b00010000
           out PORTD, temp2             ; ausgeben
           rcall lcd_enable             ; Enable-Routine aufrufen
           rcall delay50us              ; Delay-Routine aufrufen
           ret                          ; zurück zum Hauptprogramm

 ; sendet einen Befehl an das LCD
lcd_command:                            ; wie lcd_data, nur RS=0
           mov temp2, temp1
           swap temp1
           andi temp1, 0b00001111
           out PORTD, temp1
           rcall lcd_enable
           andi temp2, 0b00001111
           out PORTD, temp2
           rcall lcd_enable
           rcall delay50us
           ret

 ; erzeugt den Enable-Puls
 ;
 ; Bei höherem Takt (>= 8 MHz) kann es notwendig sein, 
 ; vor dem Enable High 1-2 Wartetakte (nop) einzufügen. 
 ; Siehe dazu https://www.mikrocontroller.net/topic/81974#685882
lcd_enable:
           sbi PORTD, 5                 ; Enable high
           nop                          ; mindestens 3 Taktzyklen warten
           nop
           nop
           cbi PORTD, 5                 ; Enable wieder low
           ret                          ; Und wieder zurück                     

 ; Pause nach jeder Übertragung
delay50us:                              ; 50µs Pause (bei 4 MHz)
           ldi  temp1, $42
delay50us_:dec  temp1
           brne delay50us_
           ret                          ; wieder zurück

 ; Längere Pause für manche Befehle
delay5ms:                               ; 5ms Pause (bei 4 MHz)
           ldi  temp1, $21
WGLOOP0:   ldi  temp2, $C9
WGLOOP1:   dec  temp2
           brne WGLOOP1
           dec  temp1
           brne WGLOOP0
           ret                          ; wieder zurück

 ; Initialisierung: muss ganz am Anfang des Programms aufgerufen werden
lcd_init:
           ldi  temp3,50
powerupwait:
           rcall  delay5ms
           dec  temp3
           brne powerupwait
           ldi temp1, 0b00000011        ; muss 3mal hintereinander gesendet
           out PORTD, temp1             ; werden zur Initialisierung
           rcall lcd_enable             ; 1
           rcall delay5ms
           rcall lcd_enable             ; 2
           rcall delay5ms
           rcall lcd_enable             ; und 3!
           rcall delay5ms
           ldi temp1, 0b00000010        ; 4bit-Modus einstellen
           out PORTD, temp1
           rcall lcd_enable
           rcall delay5ms
           ldi temp1, 0b00101000        ; 4Bit / 2 Zeilen / 5x8
           rcall lcd_command
           ldi temp1, 0b00001100        ; Display ein / Cursor aus / kein Blinken
           rcall lcd_command
           ldi temp1, 0b00000110        ; Cursor inkrementieren / kein Scrollen
           rcall lcd_command
           ret

 ; Sendet den Befehl zur Löschung des Displays
lcd_clear:
           ldi temp1, 0b00000001   ; Display löschen
           rcall lcd_command
           rcall delay5ms
           ret

 ; Sendet den Befehl: Cursor Home
lcd_home:
           ldi temp1, 0b00000010   ; Cursor Home
           rcall lcd_command
           rcall delay5ms
           ret

Anm. 1: Siehe Bitmanipulation

Weitere Funktionen (wie z. B. Cursorposition verändern) sollten mit Hilfe der Befehlscodeliste nicht schwer zu realisieren sein. Einfach den Code in temp1 laden, lcd_command aufrufen und ggf. eine Pause einfügen.

Natürlich kann man die LCD-Ansteuerung auch an einen anderen Port des Mikrocontrollers „verschieben“: Wenn das LCD z. B. an Port B angeschlossen ist, dann reicht es, im Programm alle „PORTD“ durch „PORTB“ und „DDRD“ durch „DDRB“ zu ersetzen.

Wer eine höhere Taktfrequenz als 4 MHz verwendet, der sollte daran denken, die Dauer der Verzögerungsschleifen anzupassen.

Anwendung

Ein Programm, das diese Routinen zur Anzeige von Text verwendet, kann z. B. so aussehen (die Datei lcd-routines.asm muss sich im gleichen Verzeichnis befinden). Nach der Initialisierung wird zuerst der Displayinhalt gelöscht. Um dem LCD ein Zeichen zu schicken, lädt man es in temp1 und ruft die Routine lcd_data auf. Das folgende Beispiel zeigt das Wort „Test“ auf dem LCD an.

Download lcd-test.asm

.include "m8def.inc"

; .def definiert ein Synonym (Namen) für ein µC-Register
.def temp1 = r16
.def temp2 = r17
.def temp3 = r18

           ldi temp1, LOW(RAMEND)      ; LOW-Byte der obersten RAM-Adresse
           out SPL, temp1
           ldi temp1, HIGH(RAMEND)     ; HIGH-Byte der obersten RAM-Adresse
           out SPH, temp1

           ldi temp1, 0xFF    ; Port D = Ausgang
           out DDRD, temp1

           rcall lcd_init     ; Display initialisieren
           rcall lcd_clear    ; Display löschen

           ldi temp1, 'T'     ; Zeichen anzeigen
           rcall lcd_data

           ldi temp1, 'e'     ; Zeichen anzeigen
           rcall lcd_data
           
           ldi temp1, 's'     ; Zeichen anzeigen
           rcall lcd_data

           ldi temp1, 't'     ; Zeichen anzeigen
           rcall lcd_data

loop:
           rjmp loop

.include "lcd-routines.asm"            ; LCD-Routinen werden hier eingefügt

Für längere Texte ist die Methode, jedes Zeichen einzeln in das Register zu laden und lcd_data aufzurufen natürlich nicht sehr praktisch. Dazu später aber mehr.

Bisher wurden in Register immer irgendwelche Zahlenwerte geladen, aber in diesem Programm kommt plötzlich die Anweisung

           ldi temp1, 'T'

vor. Wie ist diese zu verstehen? Passiert hier etwas grundlegend anderes als beim Laden einer Zahl in ein Register?

Die Antwort darauf lautet: Nein. Auch hier wird letztendlich nur eine Zahl in ein Register geladen. Der Schlüssel zum Verständnis beruht darauf, dass zum LCD, so wie zu allen Ausgabegeräten, für die Ausgabe von Texten immer nur Zahlen übertragen werden, sogenannte Codes. Zum Beispiel könnte man vereinbaren, dass ein LCD, wenn es den Ausgabecode 65 erhält, ein „A“ anzeigt, bei einem Ausgabecode von 66 ein „B“ usw. Naturgemäß gibt es daher viele verschiedene Code-Buchstaben-Zuordnungen. Damit hier etwas Ordnung in das potentielle Chaos kommt, hat man sich bereits in der Steinzeit der Programmierung auf bestimmte Codetabellen geeinigt, von denen die verbreitetste sicherlich die ASCII-Zuordnung ist.

ASCII

ASCII steht für American Standard Code for Information Interchange und ist ein standardisierter Code zur Zeichenumsetzung. Die Codetabelle sieht hexadezimal dabei wie folgt aus:

x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF
0x NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI
1x DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US
2x SP ! " # $ % & ' ( ) * + , - . /
3x 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
4x @ A B C D E F G H I J K L M N O
5x P Q R S T U V W X Y Z [ \ ] ^ _
6x ` a b c d e f g h i j k l m n o
7x p q r s t u v w x y z { | } ~ DEL

Die ersten beiden Zeilen enthalten die Codes für einige Steuerzeichen, ihre vollständige Beschreibung würde hier zu weit führen. Das Zeichen SP steht für ein Space, also ein Leerzeichen. BS steht für Backspace, also ein Zeichen zurück. DEL steht für Delete, also das Löschen eines Zeichens. CR steht für Carriage Return, also wörtlich: der Wagenrücklauf (einer Schreibmaschine), während LF für Line Feed, also einen Zeilenvorschub, steht.

Der Assembler kennt diese Codetabelle und ersetzt die Zeile

           ldi temp1, 'T'

durch

 
           ldi temp1, $54

was letztendlich auch der Lesbarkeit des Programmes zugute kommt. Funktional besteht kein Unterschied zwischen den beiden Anweisungen. Beide bewirken, dass das Register temp1 mit dem Bitmuster 01010100 (= hexadezimal 54, = dezimal 84 oder eben der ASCII-Code für „T“) geladen wird.

Das LCD wiederum kennt diese Code-Tabelle ebenfalls und wenn es über den Datenbus die Codezahl $54 zur Anzeige empfängt, dann schreibt es ein „T“ an die aktuelle Cursorposition. Genauer gesagt, weiß das LCD nichts von einem „T“. Es sieht einfach in seinen internen Tabellen nach, welche Pixel beim Empfang der Codezahl $54 auf schwarz zu setzen sind. „Zufällig“ sind das genau jene Pixel, die für uns Menschen ein „T“ ergeben.

Welche Befehle versteht das LCD?

Auf dem LCD arbeitet ein Controller vom Typ HD44780. Dieser Kontroller versteht eine Reihe von Befehlen, die allesamt mittels lcd_command gesendet werden können. Ein Kommando ist dabei nichts anderes als ein Befehlsbyte, in dem die verschiedenen Bits verschiedene Bedeutungen haben:

Bitwert Bedeutung
0 dieses Bit muss 0 sein
1 dieses Bit muss 1 sein
x der Zustand dieses Bits ist egal
sonstige Buchstaben das Bit muss je nach gewünschter Funktionalität gesetzt werden.
Die mögliche Funktionalität des jeweiligen Bits geht aus der Befehlsbeschreibung hervor.

Beispiel: Das Kommando „ON/OFF Control“ soll benutzt werden, um das Display einzuschalten, der Cursor soll eingeschaltet werden und der Cursor soll blinken. Das Befehlsbyte ist so aufgebaut:

  0b00001dcb

Aus der Befehlsbeschreibung entnimmt man:

  • Display ein bedeutet, dass an der Bitposition d eine 1 stehen muss.
  • Cursor ein bedeutet, dass an der Bitposition c eine 1 stehen muss.
  • Cursor blinken bedeutet, dass an der Bitposition b eine 1 stehen muss.

Das dafür zu übertragende Befehlsbyte hat also die Gestalt 0b00001111 oder in hexadezimaler Schreibweise $0F.

Clear display: 0b00000001

Die Anzeige wird gelöscht und der Ausgabecursor kehrt an die Home Position (links, erste Zeile) zurück.

Ausführungszeit: 1,64 ms

Cursor home: 0b0000001x

Der Cursor kehrt an die Home Position (links, erste Zeile) zurück. Ein verschobenes Display wird auf die Grundeinstellung zurückgesetzt.

Ausführungszeit: 40 µs bis 1,64 ms

Entry mode: 0b000001is

Legt die Cursor-Richtung sowie eine mögliche Verschiebung des Displays fest:

  • i = 1, Cursorposition bei Ausgabe eines Zeichens erhöhen
  • i = 0, Cursorposition bei Ausgabe eines Zeichens vermindern
  • s = 1, Display wird gescrollt, wenn der Cursor das Ende bzw. den Anfang, je nach Einstellung von i, erreicht hat.

Ausführungszeit: 40 µs

On/off control: 0b00001dcb

Display insgesamt ein-/ausschalten; den Cursor ein-/ausschalten; Cursor-Blinken ein-/ausschalten. Wenn das Display ausgeschaltet wird, geht der Inhalt des Displays nicht verloren. Der vorher angezeigte Text wird nach Wiedereinschalten erneut angezeigt. Ist der Cursor eingeschaltet, aber Blinken ausgeschaltet, so wird der Cursor als Cursorzeile in Pixelzeile 8 dargestellt. Ist Blinken eingeschaltet, wird der Cursor als blinkendes ausgefülltes Rechteck dargestellt, welches abwechselnd mit dem Buchstaben an dieser Stelle angezeigt wird.

  • d = 0, Display aus
  • d = 1, Display ein
  • c = 0, Cursor aus
  • c = 1, Cursor ein
  • b = 0, Cursor-Blinken aus
  • b = 1, Cursor-Blinken ein

Ausführungszeit: 40 µs

Cursor/Scrollen: 0b0001srxx

Bewegt den Cursor oder scrollt das Display um eine Position entweder nach rechts oder nach links.

  • s = 1, Display scrollen
  • s = 0, Cursor bewegen
  • r = 1, nach rechts
  • r = 0, nach links

Ausführungszeit: 40 µs

Konfiguration: 0b001dnfxx

Einstellen von Interface-Art, Modus, Font

  • d = 0, 4-Bit-Interface
  • d = 1, 8-Bit-Interface
  • n = 0, 1-zeilig
  • n = 1, 2-zeilig
  • f = 0, 5×7 Pixel
  • f = 1, 5×11 Pixel

Ausführungszeit: 40 µs

Character RAM Address Set: 0b01aaaaaa

Mit diesem Kommando werden maximal 8 selbst definierte Zeichen definiert. Dazu wird der Character-RAM-Zeiger auf den Anfang des Character Generator (CG) RAM gesetzt und das Zeichen durch die Ausgabe von 8 Byte definiert. Der Adresszeiger wird nach Ausgabe jeder Pixelspalte (8 Bit) vom LCD selbst erhöht. Nach Beendigung der Zeichendefinition muss die Schreibposition explizit mit dem Kommando „Display RAM Address Set“ wieder in den DD-RAM-Bereich gesetzt werden.

aaaaaa = 6-bit CG-RAM-Adresse

Ausführungszeit: 40 µs

Display RAM Address Set: 0b1aaaaaaa

Den Cursor neu positionieren. Display Data (DD) RAM ist vom Character Generator (CG) RAM unabhängig. Der Adresszeiger wird bei Ausgabe eines Zeichens ins DD-RAM automatisch erhöht. Das Display verhält sich so, als ob eine Zeile immer aus 40 logischen Zeichen besteht, von der, je nach konkretem Displaytyp (16 Zeichen, 20 Zeichen) immer nur ein Teil sichtbar ist.

aaaaaaa = 7-bit DD-RAM-Adresse. Auf 2-zeiligen Displays (und den meisten 16×1-Displays) kann die Adressangabe wie folgt interpretiert werden:

1laaaaaa

  • l = Zeilennummer (0 oder 1)
  • a = 6-Bit Spaltennummer
--------------------------------
DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0
--- --- --- --- --- --- --- ---
 1   A   A   A   A   A   A   A 

Setzt die DDRAM-Adresse:

Wenn N = 0 (1 line display)

   AAAAAAA = "00h" - "4Fh"

Wenn N = 1 (2 line display) ((1x16))

   AAAAAAA = "00h" - "27h" Zeile 1. (0x80) 
   AAAAAAA = "40h" - "67h" Zeile 2. (0xC0)

Ausführungszeit: 40 µs

Einschub: Code aufräumen

Es wird Zeit, sich einmal etwas kritisch mit den bisher geschriebenen Funktionen auseinander zu setzen.

Portnamen aus dem Code herausziehen

Wenn wir die LCD-Funktionen einmal genauer betrachten, dann fällt sofort auf, dass über die Funktionen verstreut immer wieder das PORTD sowie einzelne Zahlen für die Pins an diesem Port auftauchen. Wenn das LCD an einem anderen Port betrieben werden soll, oder sich die Pin-Belegung ändert, dann muss an all diesen Stellen eine Anpassung vorgenommen werden. Dabei darf keine einzige Stelle übersehen werden, ansonsten würden die LCD-Funktionen nicht oder nicht vollständig funktionieren.

Eine Möglichkeit, dem vorzubeugen, ist es, diese immer gleichbleibenden Dinge an den Anfang der LCD-Funktionen vorzuziehen. Anstelle von PORTD wird dann im Code ein anderer Name benutzt, den man frei vergeben kann. Dem Assembler wird nur noch mitgeteilt, dass dieser Name für PORTD steht. Muss das LCD an einen anderen Port angeschlossen werden, so wird nur diese Zuordnung geändert und der Assembler passt dann im restlichen Code alle davon abhängigen Anweisungen an:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                 LCD-Routinen                ;;
;;                 ============                ;;
;;              (c)andreas-s@web.de            ;;
;;                                             ;;
;; 4-Bit-Interface                             ;;
;; DB4..DB7:      PD0..PD3                     ;;
;; RS:            PD4                          ;;
;; E:             PD5                          ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; .equ definiert ein Symbol und dessen Wert
.equ LCD_PORT = PORTD
.equ LCD_DDR  = DDRD
.equ PIN_E    = 5
.equ PIN_RS   = 4

; sendet ein Datenbyte an das LCD
lcd_data:
           mov temp2, temp1             ; "Sicherungskopie" für
                                        ; die Übertragung des 2. Nibbles
           swap temp1                   ; Nibbles vertauschen
           andi temp1, 0b00001111       ; oberes Nibble auf null setzen
           sbr temp1, 1<<PIN_RS         ; entspricht 0b00010000
           out LCD_PORT, temp1          ; ausgeben
           rcall lcd_enable             ; Enable-Routine aufrufen
                                        ; 2. Nibble, kein swap, da es schon
                                        ; an der richtigen Stelle ist
           andi temp2, 0b00001111       ; obere Hälfte auf null setzen
           sbr temp2, 1<<PIN_RS         ; entspricht 0b00010000
           out LCD_PORT, temp2          ; ausgeben
           rcall lcd_enable             ; Enable-Routine aufrufen
           rcall delay50us              ; Delay-Routine aufrufen
           ret                          ; zurück zum Hauptprogramm

; sendet einen Befehl an das LCD

lcd_command:                            ; wie lcd_data, nur RS=0
           mov temp2, temp1
           swap temp1
           andi temp1, 0b00001111
           out LCD_PORT, temp1
           rcall lcd_enable
           andi temp2, 0b00001111
           out LCD_PORT, temp2
           rcall lcd_enable
           rcall delay50us
           ret

; erzeugt den Enable-Puls
lcd_enable:
           sbi LCD_PORT, PIN_E          ; Enable high
           nop                          ; 3 Taktzyklen warten
           nop
           nop
           cbi LCD_PORT, PIN_E          ; Enable wieder low
           ret                          ; Und wieder zurück

; Pause nach jeder Übertragung
delay50us:                              ; 50µs Pause
           ldi  temp1, $42
delay50us_:dec  temp1
           brne delay50us_
           ret                          ; wieder zurück

; Längere Pause für manche Befehle
delay5ms:                               ; 5ms Pause
           ldi  temp1, $21
WGLOOP0:   ldi  temp2, $C9
WGLOOP1:   dec  temp2
           brne WGLOOP1
           dec  temp1
           brne WGLOOP0
           ret                          ; wieder zurück

; Initialisierung: muss ganz am Anfang des Programms aufgerufen werden
lcd_init:
           ldi   temp1, 0xFF            ; alle Pins am Ausgabeport auf Ausgang
           out   LCD_DDR, temp1

           ldi   temp3, 6
powerupwait:
           rcall delay5ms
           dec   temp3
           brne  powerupwait
           ldi   temp1,    0b00000011   ; muss 3mal hintereinander gesendet
           out   LCD_PORT, temp1        ; werden zur Initialisierung
           rcall lcd_enable             ; 1
           rcall delay5ms
           rcall lcd_enable             ; 2
           rcall delay5ms
           rcall lcd_enable             ; und 3!
           rcall delay5ms
           ldi   temp1, 0b00000010      ; 4-Bit-Modus einstellen
           out   LCD_PORT, temp1
           rcall lcd_enable
           rcall delay5ms
           ldi   temp1, 0b00101000      ; 4 Bit, 2 Zeilen, 5x7 Pixel
           rcall lcd_command
           ldi   temp1, 0b00001100      ; Display on, Cursor off
           rcall lcd_command
           ldi   temp1, 0b00000110      ; Cursor inkrementieren, kein Scrollen
           rcall lcd_command
           ret

; Sendet den Befehl zur Löschung des Displays
lcd_clear:
           ldi   temp1, 0b00000001      ; Display löschen
           rcall lcd_command
           rcall delay5ms
           ret

; Sendet den Befehl: Cursor Home
lcd_home:
           ldi   temp1, 0b00000010      ; Cursor Home
           rcall lcd_command
           rcall delay5ms
           ret

Mittels .equ werden mit dem Assembler Textersetzungen vereinbart. Der Assembler ersetzt alle Vorkommnisse des linken Bezeichners durch den Bezeichner rechts des Gleichheitszeichens. Dadurch ist es z. B. möglich, alle Vorkommnisse von PORTD durch LCD_PORT auszutauschen. Wird das LCD an einen anderen Port, z. B. PORTB, gelegt, dann genügt es, die Zeilen

.equ LCD_PORT = PORTD
.equ LCD_DDR  = DDRD

durch

.equ LCD_PORT = PORTB
.equ LCD_DDR  = DDRB

zu ersetzen. Der Assembler sorgt dann dafür, dass diese Portänderung an den relevanten Stellen im Code über die Textersetzungen einfließt. Selbiges natürlich mit der Pin-Zuordnung.

Registerbenutzung

Bei diesen Funktionen mussten einige Register des Prozessors benutzt werden, um darin Zwischenergebnisse zu speichern bzw. zu bearbeiten.

Beachtet werden muss dabei natürlich, dass es zu keinen Überschneidungen kommt. Solange nur jede Funktion jeweils für sich betrachtet wird, ist das kein Problem. In 20 oder 30 Code-Zeilen kann man gut verfolgen, welches Register wofür benutzt wird. Schwieriger wird es, wenn Funktionen wiederum andere Funktionen aufrufen, die ihrerseits wieder Funktionen aufrufen usw. Jede dieser Funktionen benutzt einige Register und mit zunehmender Programmgröße wird es immer schwieriger, zu verfolgen, welches Register zu welchem Zeitpunkt wofür benutzt wird.

Speziell bei Basisfunktionen wie diesen LCD-Funktionen ist es daher oft ratsam, dafür zu sorgen, dass jede Funktion die Register wieder in dem Zustand hinterlässt, in dem sie sie auch vorgefunden hat. Wir benötigen dazu wieder den Stack, auf dem die Registerinhalte bei Betreten einer Funktion zwischengespeichert werden und von dem die Register bei Verlassen einer Funktion wiederhergestellt werden.

Nehmen wir die Funktion

 ; Sendet den Befehl zur Löschung des Displays
lcd_clear:
           ldi   temp1, 0b00000001      ; Display löschen
           rcall lcd_command
           rcall delay5ms
           ret

Diese Funktion verändert das Register temp1. Um das Register abzusichern, schreiben wir die Funktion um:

 ; Sendet den Befehl zur Löschung des Displays
lcd_clear:
           push  temp1                  ; temp1 auf dem Stack sichern
           ldi   temp1, 0b00000001      ; Display löschen
           rcall lcd_command
           rcall delay5ms
           pop   temp1                  ; temp1 vom Stack wiederherstellen
           ret

Am besten hält man sich an die Regel: Jede Funktion ist dafür zuständig, die Register zu sichern und wieder herzustellen, die sie auch selbst verändert. lcd_clear ruft die Funktionen lcd_command und delay5ms auf. Wenn diese Funktionen selbst wieder Register verändern (und das tun sie), so ist es die Aufgabe dieser Funktionen, sich um die Sicherung und das Wiederherstellen der entsprechenden Register zu kümmern. lcd_clear sollte sich nicht darum kümmern müssen. Auf diese Weise ist das Schlimmste, das einem passieren kann, dass ein paar Register unnütz gesichert und wiederhergestellt werden. Das kostet zwar etwas Rechenzeit und etwas Speicherplatz auf dem Stack, ist aber immer noch besser als das andere Extrem: Nach einem Funktionsaufruf haben einige Register nicht mehr den Wert, den sie haben sollten, und das Programm rechnet mit falschen Zahlen weiter.

Lass den Assembler rechnen

Betrachtet man den Code genauer, so fallen einige konstante Zahlenwerte auf (Das vorangestellte $ kennzeichnet die Zahl als Hexadezimalzahl):

delay50us:                              ; 50µs Pause
           ldi  temp1, $42
delay50us_:
           dec  temp1
           brne delay50us_
           ret                          ; wieder zurück

Der Code benötigt eine Warteschleife, die mindestens 50 µs dauert. Die beiden Befehle innerhalb der Schleife benötigen 3 Takte: 1 Takt für den dec und der brne benötigt 2 Takte, wenn die Bedingung zutrifft, der Branch also genommen wird. Bei 4 MHz werden also 4000000 / 3 * 50 / 1000000 ≈ 67 Durchläufe durch die Schleife benötigt, um eine Verzögerungszeit von 50 µs (0,00005 Sekunden) zu erreichen, hexadezimal ausgedrückt: $42 (eigentlich $43).

Der springende Punkt ist: Bei anderen Taktfrequenzen müsste man nun jedesmal diese Berechnung machen und den entsprechenden Zahlenwert einsetzen. Das kann aber der Assembler genausogut erledigen. Am Anfang des Codes wird ein Eintrag definiert, der die Taktfrequenz festlegt. Traditionell heißt dieser Eintrag XTAL:

.equ XTAL  = 4000000                    ; Taktfrequenz in Hz

...

delay50us:                              ; 50µs Pause
           ldi  temp1, ( XTAL * 50 / 3 ) / 1000000
delay50us_:
           dec  temp1
           brne delay50us_
           ret                          ; wieder zurück

An einer anderen Codestelle gibt es weitere derartige „magische Zahlen“:

 ; Längere Pause für manche Befehle
delay5ms:                               ; 5ms Pause
           ldi  temp1, $21
WGLOOP0:   ldi  temp2, $C9
WGLOOP1:   dec  temp2
           brne WGLOOP1
           dec  temp1
           brne WGLOOP0
           ret                          ; wieder zurück

Was geht hier vor? Die innere Schleife benötigt wieder 3 Takte pro Durchlauf. Bei $C9 = 201 Durchläufen werden also 201 * 3 = 603 Takte verbraucht. In der äußeren Schleife werden pro Durchlauf also 603 + 1 + 2 = 606 Takte verbraucht und einmal 605 Takte (weil der brne nicht genommen wird). Da die äußere Schleife $21 = 33-mal wiederholt wird, werden 32 * 606 + 605 = 19997 Takte verbraucht. Noch 1 Takt mehr für den allerersten ldi und 4 Takte für den ret, macht 20002 Takte. Bei 4 MHz benötigt der Prozessor 20002 / 4000000 = 0.0050005 Sekunden, also rund 5 ms. Die 7. Nachkommastelle kann man an dieser Stelle getrost ignorieren. Vor allen Dingen auch deshalb, weil auch der Quarz nicht exakt 4000000 Schwingungen in der Sekunde durchführen wird. Wird der Wiederholwert für die innere Schleife bei $C9 belassen, so werden 4000000 / 607 * 5 / 1000 Wiederholungen der äußeren Schleife benötigt. (Die Berechnung wurde hier etwas vereinfacht, die nicht berücksichtigten Takte fallen zeitmäßig nicht weiter ins Gewicht bzw. wurden dadurch berücksichtigt, dass mit 607 anstelle von 606 gerechnet wird). Auch diese Berechnung kann wieder der Assembler übernehmen:

 ; Längere Pause für manche Befehle
delay5ms:                               ; 5ms Pause
           ldi  temp1, ( XTAL * 5 / 607 ) / 1000
WGLOOP0:   ldi  temp2, $C9
WGLOOP1:   dec  temp2
           brne WGLOOP1
           dec  temp1
           brne WGLOOP0
           ret                          ; wieder zurück

Ein kleines Problem kann bei der Verwendung dieses Verfahrens entstehen: Bei hohen Taktfrequenzen und großen Wartezeiten kann der berechnete Wert größer als 255 werden und man bekommt die Fehlermeldung „Operand(s) out of range“ beim Assemblieren. Dieser Fall tritt zum Beispiel für obige Konstruktion bei einer Taktfrequenz von 16 MHz ein (genauer gesagt ab 15,3 MHz), während darunter XTAL beliebig geändert werden kann. Als einfachste Lösung bietet es sich an, die Zahl der Takte pro Schleifendurchlauf durch das Einfügen von nop-Befehlen zu erhöhen und die Berechnungsvorschrift anzupassen.

Ausgabe eines konstanten Textes

Weiter oben wurde schon einmal ein Text ausgegeben. Dies geschah durch Ausgabe von einzelnen Zeichen. Das können wir auch anders machen. Wir können den Text im Speicher ablegen und eine Funktion schreiben, die die einzelnen Zeichen aus dem Speicher liest und ausgibt. Dabei stellt sich die Frage: Woher „weiß“ die Funktion eigentlich, wie lang der Text ist? Die Antwort darauf lautet: Sie kann es nicht wissen. Wir müssen irgendwelche Vereinbarungen treffen, woran die Funktion erkennen kann, dass der Text zu Ende ist. Im Wesentlichen werden dazu zwei Methoden benutzt:

  • Der Text enthält ein spezielles Zeichen, welches das Ende des Textes markiert.
  • Wir speichern nicht nur den Text selbst, sondern auch die Länge des Textes.

Mit einer der beiden Methoden ist es der Textausgabefunktion dann ein Leichtes, den Text vollständig auszugeben.

Wir werden uns im Weiteren dafür entscheiden, ein spezielles Zeichen, eine 0 (den Wert 0, nicht das Zeichen '0'), dafür zu benutzen. Die Ausgabefunktionen werden dann etwas einfacher, als wenn bei der Ausgabe die Anzahl der bereits ausgegebenen Zeichen mitgezählt werden muss.

Den Text selbst speichern wir im Flash-Speicher, also dort, wo auch das Programm gespeichert ist:

 ; Einen konstanten Text aus dem Flash-Speicher
 ; ausgeben. Der Text wird mit einer 0 beendet.
lcd_flash_string:
           push  temp1
           push  ZH
           push  ZL

lcd_flash_string_1:
           lpm   temp1, Z+
           cpi   temp1, 0
           breq  lcd_flash_string_2
           rcall  lcd_data
           rjmp  lcd_flash_string_1

lcd_flash_string_2:
           pop   ZL
           pop   ZH
           pop   temp1
           ret

Diese Funktion benutzt den Befehl lpm, um das jeweils nächste Zeichen aus dem Flash-Speicher in ein Register zur Weiterverarbeitung zu laden. Dazu wird der sogenannte Z-Pointer benutzt. So nennt man das Registerpaar R30 und R31. Nach jedem Ladevorgang wird dabei durch den Befehl

           lpm   temp1, Z+

dieser Z-Pointer um 1 erhöht. Mittels cpi wird das in das Register temp1 geladene Zeichen mit 0 verglichen. cpi vergleicht die beiden Zahlen und merkt sich das Ergebnis in einem speziellen Register in Form von Status-Bits. cpi zieht dabei ganz einfach die beiden Zahlen voneinander ab. Sind sie gleich, so kommt da als Ergebnis 0 heraus und cpi setzt daher konsequenter Weise das Zero-Flag, das anzeigt, dass die vorhergegangene Operation eine 0 als Ergebnis hatte. breq wertet diese Status-Bits aus. Wenn die vorhergegangene Operation ein 0-Ergebnis hatte, das Zero-Flag also gesetzt ist, dann wird ein Sprung zum angegebenen Label durchgeführt. In Summe bewirkt also die Sequenz

 
           cpi   temp1, 0
           breq  lcd_flash_string_2

dass das gelesene Zeichen mit 0 verglichen wird und falls das gelesene Zeichen tatsächlich 0 war, an der Stelle lcd_flash_string_2 weiter gemacht wird. Im anderen Fall wird die bereits geschriebene Funktion lcd_data aufgerufen, welche das Zeichen ausgibt. lcd_data erwartet dabei das Zeichen im Register temp1, genau in dem Register, in welches wir vorher mittels lpm das Zeichen geladen hatten.

Das verwendende Programm sieht dann so aus:

.include "m8def.inc"
 
.def temp1 = r16
.def temp2 = r17
.def temp3 = r18
 
 
           ldi temp1, LOW(RAMEND)      ; LOW-Byte der obersten RAM-Adresse
           out SPL, temp1
           ldi temp1, HIGH(RAMEND)     ; HIGH-Byte der obersten RAM-Adresse
           out SPH, temp1
 
           rcall lcd_init              ; Display initialisieren
           rcall lcd_clear             ; Display löschen
 
           ldi ZL, LOW(text*2)         ; Adresse des Strings in den
           ldi ZH, HIGH(text*2)        ; Z-Pointer laden

           rcall lcd_flash_string      ; Unterprogramm gibt String aus, der
                                       ; durch den Z-Pointer adressiert wird
loop:
           rjmp loop

text:
           .db "Test",0                ; Stringkonstante, durch eine 0
                                       ; abgeschlossen  

.include "lcd-routines.asm"            ; LCD Funktionen

Genaueres über die Verwendung unterschiedlicher Speicher findet sich im Kapitel Speicher.

Zahlen ausgeben

Um Zahlen, die beispielsweise in einem Register gespeichert sind, ausgeben zu können, ist es notwendig, sich eine Textrepräsentierung der Zahl zu generieren. Die Zahl 123 wird also in den Text „123“ umgewandelt, welcher dann ausgegeben wird. Aus praktischen Gründen wird allerdings der Text nicht vollständig generiert (man müsste ihn ja irgendwo zwischenspeichern), sondern die einzelnen Buchstaben werden sofort ausgegeben, sobald sie bekannt sind.

Dezimal ausgeben

Das Prinzip der Umwandlung ist einfach. Um herauszufinden, wieviele Hunderter in der Zahl 123 enthalten sind, genügt es, in einer Schleife immer wieder 100 von der Zahl abzuziehen und mitzuzählen, wie oft dies gelang, bevor das Ergebnis negativ wurde. In diesem Fall lautet die Antwort: 1-mal, denn 123 − 100 macht 23. Versucht man erneut 100 abzuziehen, so ergibt sich eine negative Zahl. Also muss eine '1' ausgegeben werden. Die verbleibenden 23 werden weiter behandelt, indem festgestellt wird, wieviele Zehner darin enthalten sind. Auch hier wiederum: In einer Schleife solange 10 abziehen, bis das Ergebnis negativ wurde. Konkret geht das 2-mal gut, also muss das nächste auszugebende Zeichen eine '2' sein. Damit verbleiben noch die Einer, welche direkt in das entsprechende Zeichen umgewandelt werden können. In Summe hat man also an das Display die Zeichen '1' '2' '3' ausgegeben.

;**********************************************************************
;
; Eine 8-Bit-Zahl ohne Vorzeichen ausgeben
;
; Übergabe:            Zahl im Register temp1
; veränderte Register: keine
;
lcd_number:
           push  temp1            ; Die Funktion verändert temp1 und temp2,
           push  temp2            ; also sichern wir den Inhalt, um ihn am Ende
                                  ; wieder herstellen zu können.

           mov   temp2, temp1     ; Das Register temp1 frei machen
                                  ; Abzählen, wieviele Hunderter
                                  ; in der Zahl enthalten sind
;** Hunderter ** 
           ldi   temp1, '0'-1     ; temp1 mit ASCII '0'-1 vorladen
lcd_number_1:
           inc   temp1            ; ASCII erhöhen (somit ist nach dem ersten
                                  ; Durchlauf eine '0' in temp1)
           subi  temp2, 100       ; 100 abziehen
           brcc  lcd_number_1     ; Ist dadurch kein Unterlauf entstanden?
                                  ; nein, dann zurück zu lcd_number_1
           subi  temp2, -100      ; 100 wieder dazuzählen, da die
                                  ; vorhergehende Schleife 100 zuviel
                                  ; abgezogen hat
           rcall lcd_data         ; die Hunderterstelle ausgeben

;** Zehner  **
           ldi   temp1, '0'-1     ; temp1 mit ASCII '0'-1 vorladen
lcd_number_2:
           inc   temp1            ; ASCII erhöhen (somit ist nach dem ersten
                                  ; Durchlauf eine '0' in temp1)
           subi  temp2, 10        ; 10 abziehen
           brcc  lcd_number_2     ; Ist dadurch kein Unterlauf enstanden?
                                  ; nein, dann zurück zu lcd_number_2
           subi  temp2, -10       ; 10 wieder dazuzählen, da die
                                  ; vorherhgehende Schleife 10 zuviel
                                  ; abgezogen hat
           rcall lcd_data         ; die Zehnerstelle ausgeben
 
;** Einer **        
           ldi   temp1, '0'       ; Die Zahl in temp2 ist jetzt im Bereich
           add   temp1, temp2     ; 0 bis 9. Einfach nur den ASCII-Code für
           rcall lcd_data         ; '0' dazu addieren und wir erhalten direkt
                                  ; den ASCII-Code für die Ziffer.


           pop   temp2            ; Den gesicherten Inhalt von temp2 und temp1
           pop   temp1            ; wieder herstellen
           ret                    ; und zurück

Beachte: Diese Funktion benutzt wiederum die Funktion lcd_data. Anders als bei den bisherigen Aufrufen ist lcd_number aber darauf angewiesen, dass lcd_data das Register temp2 unangetastet lässt. Falls Sie es noch nicht getan haben, dann ist das jetzt die perfekte Gelegenheit, lcd_data mit den entsprechenden push- und pop-Befehlen zu versehen. Sie sollten dies unbedingt zur Übung selbst machen. Am Ende muß die Funktion dann wie diese hier aussehen:

 ;sendet ein Datenbyte an das LCD
lcd_data:
           push  temp2
           mov   temp2, temp1           ; "Sicherungskopie" für
                                        ; die Übertragung des 2. Nibbles
           swap  temp1                  ; Vertauschen
           andi  temp1, 0b00001111      ; oberes Nibble auf Null setzen
           sbr   temp1, 1<<PIN_RS       ; entspricht 0b00010000
           out   LCD_PORT, temp1        ; ausgeben
           rcall lcd_enable             ; Enable-Routine aufrufen
                                        ; 2. Nibble, kein swap da es schon
                                        ; an der richtigen stelle ist
           andi  temp2, 0b00001111      ; obere Hälfte auf Null setzen 
           sbr   temp2, 1<<PIN_RS       ; entspricht 0b00010000
           out   LCD_PORT, temp2        ; ausgeben
           rcall lcd_enable             ; Enable-Routine aufrufen
           rcall delay50us              ; Delay-Routine aufrufen
           pop   temp2
           ret                          ; zurück zum Hauptprogramm
 
 ; sendet einen Befehl an das LCD
lcd_command:                            ; wie lcd_data, nur ohne RS zu setzen
           push  temp2
           mov   temp2, temp1
           swap  temp1
           andi  temp1, 0b00001111
           out   LCD_PORT, temp1
           rcall lcd_enable
           andi  temp2, 0b00001111
           out   LCD_PORT, temp2
           rcall lcd_enable
           rcall delay50us
           pop   temp2
           ret

Kurz zur Funktionsweise der Funktion lcd_number: Die Zahl in einem Register bewegt sich im Wertebereich 0 bis 255. Um herauszufinden, wie die Hunderterstelle lautet, zieht die Funktion einfach in einer Schleife immer wieder 100 von der Schleife ab, bis bei der Subtraktion ein Unterlauf, angezeigt durch das Setzen des Carry-Bits bei der Subtraktion, entsteht. Die Anzahl wird im Register temp1 mitgezählt. Da dieses Register mit dem ASCII-Code von '0' initialisiert wurde, und dieser ASCII-Code bei jedem Schleifendurchlauf um 1 erhöht wird, können wir das Register temp1 direkt zur Ausgabe des Zeichens für die Hunderterstelle durch die Funktion lcd_data benutzen. Völlig analog funktioniert auch die Ausgabe der Zehnerstelle.

Unterdrückung von führenden Nullen

Diese Funktion gibt jede Zahl im Register temp1 immer mit 3 Stellen aus. Führende Nullen werden nicht unterdrückt. Möchte man dies ändern, so ist das ganz leicht möglich: Vor Ausgabe der Hunderterstelle muss lediglich überprüft werden, ob die entsprechende Ausgabe eine '0' wäre. Ist sie das, so wird die Ausgabe übersprungen. Ist es allerdings eine Zahl 1…9, so muss sie der Zehnerstelle signalisieren, daß eine Prüfung auf eine '0' nicht stattfinden darf. Und dazu wird das T-Flag im SREG genutzt. Lediglich in der Einerstelle wird jede Ziffer wie errechnet ausgegeben.

           ...
                                  ; die Hunderterstelle ausgeben, wenn
                                  ; sie nicht '0' ist
           clt                    ; T-Flag löschen
           cpi   temp1, '0'
           breq  lcd_number_1a
           rcall lcd_data         ; die Hunderterstelle ausgeben
           set                    ; T-Flag im SREG setzen, da 100er-Stelle eine
                                  ; 1..9 war

lcd_number_1a:
           ...

           ...
           brts  lcd_number_2a    ; Test auf '0' überspringen, da 100er eine
                                  ; 1..9 war (unbedingt anzeigen,
                                  ; auch wenn der Zehner eine '0' ist)
           cpi   temp1, '0'       ; ansonsten Test auf '0'
           breq  lcd_number_2b
lcd_number_2a:        
           rcall lcd_data
lcd_number_2b:
           ...

Das Verfahren, die einzelnen Stellen durch Subtraktion zu bestimmen, ist bei kleinen Zahlen eine durchaus gängige Alternative. Vor allem dann, wenn keine hardwaremäßige Unterstützung für Multiplikation und Division zur Verfügung steht. Ansonsten könnte man die einzelnen Ziffern auch durch Division bestimmen. Das Prinzip ist folgendes (beispielhaft an der Zahl 52783 gezeigt):

   52783 / 10          -> 5278
   52783 − 5278 * 10   ->          3

   5278 / 10           -> 527
   5278 − 527 * 10     ->          8

   527 / 10            -> 52
   527 − 52 * 10       ->          7

   52 / 10             -> 5
   52 − 5 * 10         ->          2

   5 / 10              -> 0
   5 − 0 * 10          ->          5

Das Prinzip ist also die Restbildung bei einer fortgesetzten Division durch 10, wobei die einzelnen Ziffern in umgekehrter Reihenfolge ihrer Wertigkeit entstehen. Dadurch hat man aber ein Problem: Damit die Zeichen in der richtigen Reihenfolge ausgegeben werden können, muß man sie meistens zwischenspeichern, um sie in der richtigen Reihenfolge ausgeben zu können. Wird die Zahl in einem Feld von immer gleicher Größe ausgegeben, dann kann man auch die Zahl von rechts nach links ausgeben (bei einem LCD ist das möglich).

Hexadezimal ausgeben

Zuguterletzt hier noch eine Funktion, die eine Zahl aus dem Register temp1 in hexadezimaler Form ausgibt. Die Funktion weist keine Besonderheiten auf und sollte unmittelbar verständlich sein.

;**********************************************************************
;
; Eine 8-Bit-Zahl ohne Vorzeichen hexadezimal ausgeben
;
; Übergabe:            Zahl im Register temp1
; veränderte Register: keine
;
lcd_number_hex:
           swap  temp1
           rcall lcd_number_hex_digit
           swap  temp1

lcd_number_hex_digit:
           push  temp1

           andi  temp1, $0F
           cpi   temp1, 10
           brlt  lcd_number_hex_digit_1
           subi  temp1, -( 'A' - '9' - 1 ) ; Es wird subi mit negativer
                                           ; Konstante verwendet,
                                           ; weil es kein addi gibt
lcd_number_hex_digit_1:
           subi  temp1, -'0'               ; dito
           rcall  lcd_data
           
           pop   temp1
           ret

Binär ausgeben

Um die Sache komplett zu machen; hier eine Routine, mit der man eine 8-Bit-Zahl binär auf das LC-Display ausgeben kann:

;**********************************************************************
;
; Eine 8-Bit-Zahl ohne Vorzeichen binär ausgeben
;
; Übergabe:            Zahl im Register temp1
; veränderte Register: keine

; eine Zahl aus dem Register temp1 binär ausgeben
lcd_number_bit:
	   push temp1		  ; temp1 gesichert
           push temp2
	   push temp3

	   mov temp2, temp1;

	   ldi temp3, 8;      ; 8 Bits werden ausgelesen
lcd_number_loop:           
	   dec temp3;
	   rol temp2;         ; Datenbits ins Carry geschoben ...
	   brcc lcd_number_bit_carryset_0; 
	   brcs lcd_number_bit_carryset_1;
           rjmp lcd_number_loop;

lcd_number_bit_carryset_0:	 
	   ldi temp1, '0'     ; Bit low ausgeben
           rcall lcd_data
	   tst temp3;
	   breq lcd_number_ende;
	   rjmp lcd_number_loop;

lcd_number_bit_carryset_1:
           ldi temp1, '1'     ; Bit high ausgeben
           rcall lcd_data
           tst temp3;
	   breq lcd_number_ende;
	   rjmp lcd_number_loop;

lcd_number_ende:
	   pop temp3
	   pop temp2
	   pop temp1
	   ret

Eine 16-Bit-Zahl aus einem Registerpärchen ausgeben

Um eine 16-Bit-Zahl auszugeben wird wieder das bewährte Schema benutzt, die einzelnen Stellen durch Subtraktion abzuzählen. Da es sich hierbei allerdings um eine 16-Bit-Zahl handelt, müssen die Subtraktionen als 16-Bit-Arithmetik ausgeführt werden.

;**********************************************************************
;
; Eine 16-Bit-Zahl ohne Vorzeichen ausgeben
;
; Übergabe:            Zahl im Register temp2 (low Byte) / temp3 (high Byte)
; veränderte Register: keine
;
lcd_number16:
           push  temp1
           push  temp2
           push  temp3

; ** Zehntausender **
           ldi   temp1, '0'-1
lcd_number1:
           inc   temp1
           subi  temp2, low(10000)
           sbci  temp3, high(10000)
           brcc  lcd_number1
           subi  temp2, low(-10000)
           sbci  temp3, high(-10000)
           rcall lcd_data

; ** Tausender **
           ldi   temp1, '0'-1
lcd_number2:
           inc   temp1
           subi  temp2, low(1000)
           sbci  temp3, high(1000)
           brcc  lcd_number2
           subi  temp2, low(-1000)
           sbci  temp3, high(-1000)
           rcall lcd_data

; ** Hunderter **
           ldi   temp1, '0'-1
lcd_number3:
           inc   temp1
           subi  temp2, low(100)
           sbci  temp3, high(100)
           brcc  lcd_number3
           subi  temp2, -100             ; + 100 High-Byte nicht mehr erforderlich
           rcall lcd_data

; ** Zehner **
           ldi   temp1, '0'-1
lcd_number4:
           inc   temp1
           subi  temp2, 10
           brcc  lcd_number4
           subi  temp2, -10
           rcall lcd_data

; ** Einer **
           ldi   temp1, '0'
           add   temp1, temp2
           rcall lcd_data

; ** Stack aufräumen **
           pop   temp3
           pop   temp2
           pop   temp1

           ret

Eine BCD-Zahl ausgeben

;**********************************************************************
;
; Übergabe:            BCD-Zahl in temp1
; veränderte Register: keine
;
lcd_bcd:
           push  temp2
          
           mov   temp2, temp1           ; temp1 sichern
           swap  temp1                  ; oberes mit unterem Nibble tauschen
           andi  temp1, 0b00001111      ; und "oberes" ausmaskieren
           subi  temp1, -0x30           ; in ASCII umrechnen
           rcall lcd_data               ; und ausgeben
           mov   temp1, temp2           ; ... danach unteres
           andi  temp1, 0b00001111
           subi  temp1, -0x30
           rcall lcd_data
           mov   temp1, temp2           ; temp1 rekonstruieren

           pop   temp2
           ret

Benutzerdefinierte Zeichen

Zeichenraster für 1 Zeichen

Das LCD erlaubt für spezielle Zeichen, welche sich nicht im Zeichensatz finden, eigene Zeichen zu definieren. Dazu werden die ersten 8 ASCII-Codes reserviert, auf denen sich laut ASCII-Tabelle spezielle Steuerzeichen befinden, die normalerweise keine sichtbare Anzeige hervorrufen, sondern zur Steuerung von angeschlossenen Geräten dienen. Da diese Zeichen auf einem LCD keine Rolle spielen, können diese Zeichen benutzt werden, um sich selbst Sonderzeichen zu erzeugen, die für die jeweilige Anwendung maßgeschneidert sind.

Das LCD stellt für jedes Zeichen eine 8×5-Matrix zur Verfügung. Um sich selbst maßgeschneiderte Zeichen zu erstellen, ist es am einfachsten, sie zunächst auf einem Stück kariertem Papier zu erstellen.

Zeichenraster für ein Glockensymbol

In diesem Raster markiert man sich dann diejenigen Pixel, die im fertigen Zeichen dunkel erscheinen sollen. Als Beispiel sei hier ein Glockensymbol gezeichnet, welches in einer Telefonapplikation z. B. als Kennzeichnung für einen Anruf dienen könnte.

Eine Zeile in diesem Zeichen repräsentiert ein an das LCD zu übergebendes Byte, wobei nur die Bits 0 bis 4 relevant sind. Gesetzte Pixel stellen ein 1-Bit dar, nicht gesetzte Pixel sind ein 0-Bit. Das niederwertigste Bit einer Zeile befindet sich rechts. Auf diese Art wird jede Zeile in eine Binärzahl übersetzt, und 8 Bytes repräsentieren ein komplettes Zeichen. Am Beispiel des Glockensymboles: Die 8 Bytes, welche das Symbol repräsentieren, lauten: 0x00, 0x04, 0x0A, 0x0A, 0x0A, 0x1F, 0x04, 0x00.

Dem LCD wird die neue Definition übertragen, indem man dem LCD die „Schreibposition“ mittels des Kommandos Character RAM Address Set in den Zeichensatzgenerator verschiebt. Danach werden die 8 Bytes ganz normal als Daten ausgegeben, die das LCD damit in seine Zeichensatztabelle schreibt.

Durch die Wahl der Speicheradresse definiert man, welches Zeichen (0 bis 7) man eigentlich durch eine eigene Definition ersetzen will.

ASCII-Code Zeichensatzadresse
0 0x00
1 0x08
2 0x10
3 0x18
4 0x20
5 0x28
6 0x30
7 0x38

Nach erfolgter Definition des Zeichens muss die Schreibposition wieder explizit in den DDRAM-Bereich gesetzt werden. Danach kann ein entsprechendes Zeichen mit dem definierten ASCII-Code ausgegeben werden, wobei das LCD die von uns definierte Pixelform zur Anzeige benutzt.

Zuerst müssen natürlich erstmal die Zeichen definiert werden. Dieses geschieht einmalig durch den Aufruf der Routine lcd_load_user_chars unmittelbar nach der Initialisierung des LC-Displays.

           .
           .
           rcall lcd_init              ; Display initialisieren
           rcall lcd_load_user_chars   ; User-Zeichen in das Display laden
           rcall lcd_clear             ; Display löschen
           .
           .

Durch diesen Aufruf werden die im Flash definierten Zeichen in den CG-RAM übertragen. Diese Zeichen werden ab Adresse 0 im CG-RAM gespeichert und sind danach wie jedes andere Zeichen nutzbar.

           .
           .
           ldi   temp1, 0              ; Ausgabe des User-Char "A"
           rcall lcd_data
           ldi   temp1, 6              ; Ausgabe des User-Char "G"
           rcall lcd_data
           ldi   temp1, 5              ; Ausgabe des User-Char "E"
           rcall lcd_data
           ldi   temp1, 4              ; Ausgabe des User-Char "M"
           rcall lcd_data
           ldi   temp1, 3              ; Ausgabe des User-Char "-"
           rcall lcd_data
           ldi   temp1, 2              ; Ausgabe des User-Char "R"
           rcall lcd_data
           ldi   temp1, 1              ; Ausgabe des User-Char "V"
           rcall lcd_data
           ldi   temp1, 0              ; Ausgabe des User-Char "A"
           rcall lcd_data
           .
           .

Jetzt sollte der Schriftzug „AVR-MEGA“ verkehrt herum (180° gedreht) erscheinen.

Es fehlt natürlich noch die Laderoutine:

;**********************************************************************
;
; Lädt User-Zeichen in den CG-RAM des LCD, bis Tabellenende (0xFF)
; gelesen wird (max. 8 Zeichen können geladen werden).
;
; Übergabe:            -   
; veränderte Register: temp1, temp2, temp3, zh, zl
; Bemerkung:           ist einmalig nach lcd_init aufzurufen
;       

lcd_load_user_chars:
        ldi    zl, LOW (ldc_user_char * 2) ; Adresse der Zeichentabelle
        ldi    zh, HIGH(ldc_user_char * 2) ; in den Z-Pointer laden
        clr    temp3                       ; aktuelles Zeichen = 0 

lcd_load_user_chars_2:
        clr    temp2                       ; Linienzähler = 0

lcd_load_user_chars_1:
        ldi    temp1, 0b01000000           ; Kommando:    0b01aaalll
        add    temp1, temp3                ; + akt. Zeichen  (aaa)
        add    temp1, temp2                ; + akt. Linie       (lll)
        rcall  lcd_command                 ; Kommando schreiben

        lpm    temp1, Z+                   ; Zeichenline laden 
        rcall  lcd_data                    ; ... und ausgeben

        ldi    temp1, 0b01001000           ; Kommando:    0b01aa1lll         
        add    temp1, temp3                ; + akt. Zeichen  (aaa)       
        add    temp1, temp2                ; + akt. Linie       (lll)
        rcall  lcd_command

        lpm    temp1, Z+                   ; Zeichenline laden
        rcall  lcd_data                    ; ... und ausgeben 
        
        inc    temp2                       ; Linienzähler + 1
        cpi    temp2, 8                    ; 8 Linien fertig?
        brne   lcd_load_user_chars_1       ; nein, dann nächste Linie 
		
        subi   temp3, -0x10                ; zwei Zeichen weiter (addi 0x10)
        lpm    temp1, Z                    ; nächste Linie laden
        cpi    temp1, 0xFF                 ; Tabellenende erreicht? 
        brne   lcd_load_user_chars_2       ; nein, dann die nächsten
                                           ; zwei Zeichen
        ret

… und die Zeichendefinition:

ldc_user_char:
                              ;    Zeichen 
                              ;   0       1
       .db 0b10001, 0b00100   ; @   @ ,   @
       .db 0b10001, 0b01010   ; @   @ ,  @ @
       .db 0b11111, 0b10001   ; @@@@@ , @   @
       .db 0b10001, 0b10001   ; @   @ , @   @
       .db 0b10001, 0b10001   ; @   @ , @   @
       .db 0b10001, 0b10001   ; @   @ , @   @
       .db 0b01110, 0b10001   ;  @@@  , @   @
       .db 0b00000, 0b00000   ;       , 

                              ;    Zeichen
                              ;   2       3
       .db 0b10001, 0b00000   ; @   @ , 
       .db 0b01001, 0b00000   ;  @  @ , 
       .db 0b00101, 0b00000   ;   @ @ , 
       .db 0b11111, 0b11111   ; @@@@@ , @@@@@ 
       .db 0b10001, 0b00000   ; @   @ , 
       .db 0b10001, 0b00000   ; @   @ , 
       .db 0b01111, 0b00000   ;  @@@@ , 
       .db 0b00000, 0b00000   ;       ,  

                              ;    Zeichen
                              ;   4       5
       .db 0b10001, 0b11111   ; @   @ , @@@@@  
       .db 0b10001, 0b00001   ; @   @ ,     @
       .db 0b10001, 0b00001   ; @   @ ,     @
       .db 0b10001, 0b01111   ; @   @ ,  @@@@ 
       .db 0b10101, 0b00001   ; @ @ @ ,     @
       .db 0b11011, 0b00001   ; @@ @@ ,     @
       .db 0b10001, 0b11111   ; @   @ , @@@@@
       .db 0b00000, 0b00000   ;       ,  

                              ;    Zeichen
                              ;   6       7
       .db 0b11110, 0b11111   ; @@@@  , @@@@@  
       .db 0b10001, 0b01010   ; @   @ ,  @ @ 
       .db 0b10001, 0b00100   ; @   @ ,   @
       .db 0b11101, 0b01110   ; @@@ @ ,  @@@
       .db 0b00001, 0b00100   ;     @ ,   @
       .db 0b10001, 0b01010   ; @   @ ,  @ @
       .db 0b01110, 0b11111   ;  @@@  , @@@@@
       .db 0b00000, 0b00000   ;       ,  

       ; End of Tab
       .db 0xFF, 0xFF

Der überarbeitete, komplette Code

Hier also die komplett überarbeitete Version der LCD-Funktionen.

Die für die Benutzung relevanten Funktionen

  • lcd_init
  • lcd_clear
  • lcd_home
  • lcd_data
  • lcd_command
  • lcd_flash_string
  • lcd_number
  • lcd_number_hex

sind so ausgeführt, dass sie kein Register (außer dem Statusregister SREG) verändern. Die bei manchen Funktionen notwendigen Argumente werden immer im Register temp1 übergeben, wobei temp1 vom Usercode definiert werden muss.

Download lcd-routines.asm