AVR-Tutorial: LCD

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

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, 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 praktisch immer gleich:

Pin #BezeichnungFunktion
1VssGND
2Vcc5V
3VeeKontrastspannung (0V bis 5V)
4RSRegister Select (Befehle/Daten)
5RWRead/Write
6EEnable
7DB0Datenbit 0
8DB1Datenbit 1
9DB2Datenbit 2
10DB3Datenbit 3
11DB4Datenbit 4
12DB5Datenbit 5
13DB6Datenbit 6
14DB7Datenbit 7
15ALED-Beleuchtung, Anode
16KLED-Beleuchtung, Kathode

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

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 an 5V. Vee kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte muss man ein 10k-Potentiometer zwischen GND und 5V schalten, mit dem Schleifer an Vee:

LCD Vee.gif

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 4 IO-Pins weniger benötigt als beim 8-bit-Modus, weshalb ich mich hier für eine Ansteuerung mit 4bit entschieden habe.

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. Ist RS Low, dann wird das ankommende Byte als Befehl interpretiert, ist RS high, dann wird das Byte auf dem LCD angezeigt.
  • 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 verarbeitetet 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 ein IO-Pin am Controller einspart. Der Nachteil ist, dass man dann das Busy-Flag nicht lesen kann, weswegen man nach jedem Befehl vorsichtshalber ein paar Mikrosekunden 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.

Anschluss an den Controller

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

Pin #-LCDBezeichnung-LCDPin-µC
1VssGND
2Vcc5V
3VeeGND oder Poti (siehe oben)
4RSPD4 am AVR
5RWGND
6EPD5 am AVR
7DB0offen
8DB1offen
9DB2offen
10DB3offen
11DB4PD0 am AVR
12DB5PD1 am AVR
13DB6PD2 am AVR
14DB7PD3 am AVR

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?

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:

<avrasm>

          swap r16               ; vertauscht die beiden Nibbles von r16

</avrasm>

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 PORTB, 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:

<avrasm>

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

</avrasm>

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 setzen (= Daten senden). Um ein Bit in einem normalen Register zu setzen gibt es den Befehl sbr (Set Bit 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:

<avrasm>

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

</avrasm>

An PD4 ist RS 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:

<avrasm>

          out PORTD, r16

</avrasm>

Natürlich muss vorher der Port D auf Ausgang geschalten 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:

<avrasm>

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

</avrasm>

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.

Initialisierung im 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 Nibbel gültig. Durch die Art der Verschaltung (DB4 - DB7 wurde auf dem PORT an PD0 bis PD3 angeschlossen) ergibt sich dadurch 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 umstellt
  • ab jetzt muss für die Übertragung eines Bytes jeweils 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.

Initialisierung im 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.

  • 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

Die Routinen zur Kommunikation mit dem LCD sehen also so aus:

<avrasm>

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


;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
          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

lcd_enable:

          sbi PORTD, 5                 ; Enable high
          nop                          ; 3 Taktzyklen warten
          nop
          nop
          cbi PORTD, 5                 ; Enable wieder low
          ret                          ; Und wieder zurück                     
; Pause nach jeder Übertragung

delay50us:  ; 50us 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  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

</avrasm>

Weitere Funktionen (wie z.B. Cursorposition verändern) sollten mit Hilfe der Befehlscodeliste nicht schwer zu realisieren sein. Einfach den Code in temp 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

<avrasm> .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
          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 </avrasm>

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 <avrasm>

          ldi temp1, 'T'

</avrasm> 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 5 erhält ein 'A' anzeigt, bei einem Ausgabecode von 63 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 Code Tabellen geeinigt, von denen die Verbreitetste sicherlich die ASCII Zuordnung ist.

ASCII

ASCII steht für American Standard Code for Information Interchange und regelt eine standardisierte Code zu Zeichen Umsetzung. Die Code Tabelle sieht hexadezimal dabei wie folgt aus:


.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F
0.NULSOHSTXETXEOTENQACKBELBSHTLFVTFFCRSOSI
1.DLEDC1DC2DC3DC4NAKSYNETBCANEMSUBESCFSGSRSUS
2.SP!"#$%&'()*+,-./
3.0123456789:;<=>?
4.@ABCDEFGHIJKLMNO
5.PQRSTUVWXYZ[\]^_
6.`abcdefghijklmno
7.pqrstuvwxyz{|}~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.

Der Assembler kennt diese Codetabelle und ersetzt die Zeile

<avrasm>

          ldi temp1, 'T'

</avrasm>

durch

<avrasm>

          ldi temp1, $54

</avrasm>

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 Kontroller vom Typ HD44780. Diesen Kontroller versteht eine Reihe von Befehlen, die allesamt mittels lcd_command gesendet werden können. Ein Kommando ist dabei nichts anderes als ein Befehlsbyte, indem die verschiedenen Bits verschiedene Bedeutung besitzen:

0dieses Bit muss 0 sein
1dieses Bit muss 1 sein
xder Zustand dieses Bits ist egal
sonstige Buchstabendas 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 ein 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.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 fest 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, 5x8 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 Adress Zeiger wird nach Ausgabe jeder Pixelzeile (8Bit) 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 32 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

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, daß ü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 muß 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

<avrasm>

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


.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:  ; 50us 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 Bot, 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:

          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

</avrasm>

Mittels .equ werden mit dem Assembler Textersetzungen vereinbart. Der Assembler ersetzt alle Vorkomnisse des Quelltextes durch den zu ersetzenden Text. Dadurch ist es zb. möglich alle Vorkommnisse von PORTD durch LCD_PORT auszutauschen. Wird das LCD an einen anderen Port, zb. PORTB gelegt, dann genügt es, einzig und alleine die Zeilen <avrasm> .equ LCD_PORT = PORTD .equ LCD_DDR = DDRD </avrasm> durch <avrasm> .equ LCD_PORT = PORTB .equ LCD_DDR = DDRB </avrasm> zu ersetzen. Der Assembler sorgt dann dafür, dass diese Portänderung an den relevanten Stellen im Code über die Textersetzungen einfliesst. 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 wiederrum 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, daß 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.

Nehmen wir die Funktion <avrasm>

; Sendet den Befehl zur Löschung des Displays

lcd_clear:

          ldi   temp1, 0b00000001      ; Display löschen
          rcall lcd_command
          rcall delay5ms
          ret

</avrasm>

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

<avrasm>

; 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

</avrasm>

Am besten hält man sich an die Regel: Jede Funktion ist dafür zuständig die Register zu sichern und wiederherzustellen, 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, das 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

<avrasm> delay50us:  ; 50us Pause

          ldi  temp1, $42

delay50us_:

          dec  temp1
          brne delay50us_
          ret                          ; wieder zurück

</avrasm>

(Das vorangestellte $ kennzeichnet die Zahl als Hexadezimalzahl) 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 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 heist dieser Eintrag XTAL:

<avrasm> .equ XTAL = 4000000

...

delay50us:  ; 50us Pause

          ldi  temp1, ( XTAL * 50 / 3 ) / 1000000

delay50us_:

          dec  temp1
          brne delay50us_
          ret                          ; wieder zurück

</avrasm>

An einer anderen Codestelle gibt es weitere derartige magische Zahlen:

<avrasm>

; 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

</avrasm>

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 kommen pro Durchlauf alo 1 + 603 + 1 + 2 = 607 Takte verbraucht. Da die äußere Schleife $21 = 33 mal wiederholt wird, werden 20031 Takte verbraucht. Bei 4Mhz benötigt der Prozessor 20031 / 4000000 = 0.005007 Sekunden, also 5 ms. Wird der Wiederholwert für die innere Schleife bei $C9 belassen, so werden 4000000 / 607 * 5 / 1000 Wiederholungen der äusseren Schleife benötigt. Auch diese Berechnung kann wieder der Assembler übernehmen:

<avrasm>

; 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

</avrasm>

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.

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 holt und ausgibt. Dabei erhebt sich aber eine Fragestellung: Woher weiß die Funktion eigentlich, wie lange 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:

  • 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, 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:

<avrasm>

; Einen konstanten Text aus dem Flash Speicher
; ausgeben. Der Text wird mit einer 0 beendet

lcd_flash_string:

          push  temp1

lcd_flash_string_1:

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

lcd_flash_string_2:

          pop   temp1
          ret

</avrasm>

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 <avrasm>

          lpm   temp1, Z+

</avrasm> 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, daß die vorhergegangene Operation ein 0 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 <avrasm>

          cpi   temp1, 0
          breq  lcd_flash_string_2

</avrasm> das 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:

<avrasm> .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 </avrasm>

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

Zahlen ausgeben

Dezimal ausgeben

<avrasm>

Eine 8 Bit Zahl ohne Vorzeichen ausgeben
Übergabe
Zahl im Register temp1
veränderte Register
keine

lcd_number:

          push  temp2            ; die Funktion verändert 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
          ldi   temp1, '0'

lcd_number_1:

          subi  temp2, 100       ; 100 abziehen
          brcs  lcd_number_2     ; ist dadurch ein Unterlauf entstanden?
          inc   temp1            ; Nein: 1 Hunderter mehr ...
          rjmp  lcd_number_1     ; ... und ab zur nächsten Runde
                                 ; die Hunderterstelle ausgeben

lcd_number_2:

          rcall lcd_data
          subi  temp2, -100      ; 100 wieder dazuzählen, da die
                             ; vorherhgehende Schleife 100 zuviel
                 ; abgezogen hat
                                 ; abzählen wieviele Zehner in
                 ; der Zahl enthalten sind
          ldi   temp1, '0'

lcd_number_3:

          subi  temp2, 10        ; 10 abziehen
          brcs  lcd_number_4     ; ist dadurch ein Unterlauf enstanden?
          inc   temp1            ; Nein: 1 Zehner mehr ...
          rjmp  lcd_number_3     ; ... und ab zur nächsten Runde
                             ; die Zehnerstelle ausgeben

lcd_number_4:

          rcall lcd_data
          subi  temp2, -10       ; 10 wieder dazuzählen, da die
                             ; vorhergehende Schleife 10 zuviel
                 ; abgezogen hat
                                 ; die übrig gebliebenen Einer
                 ; noch ausgeben
          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 dierekt
                                 ; den ASCII Code für die Ziffer
          pop   temp2            ; den gesicherten Inhalt von temp2 wieder herstellen
          ret                    ; und zurück

</avrasm>

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:

<avrasm>

;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

</avrasm>

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 bzw. Zehnerstelle muss lediglich überprüft werden, ob die Entsprechende Ausgabe eine '0' wäre. Ist sie das, so wird die Ausgabe übersprungen. Lediglich in der Einerstelle wird jede Ziffer wie errechnet ausgegeben.

<avrasm>

          ...
                                 ; die Hunderterstelle ausgeben, wenn
                                 ; sie nicht '0' ist

lcd_number_2:

          cpi   temp1, '0'
          breq  lcd_number_2a
          rcall lcd_data

lcd_number_2a:

          subi  temp2, -100      ; 100 wieder dazuzählen, da die
          ...
          ...
                             ; die Zehnerstelle ausgeben, wenn
                 ; sie nicht '0' ist

lcd_number_4:

          cpi   temp1, '0'
          breq  lcd_number_4a
          rcall lcd_data

lcd_number_4a:

          subi  temp2, -10       ; 10 wieder dazuzählen, da die
          ...

</avrasm>


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)

   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.

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.

<avrasm>

Eine 8 Bit Zahl ohne Vorzeichen hexadezimal ausgeben
Übergabe
Zahl im Register temp1
veränderte Register
keine

lcd_number_hex:

          push  temp1
          swap  temp1
          andi  temp1, $0F
          rcall lcd_number_hex_digit
          pop   temp1
          push  temp1
          andi  temp1, $0F
          rcall lcd_number_hex_digit
          pop   temp1
          ret

lcd_number_hex_digit:

          cpi   temp1, 10
          brlt  lcd_number_hex_digit_1
          subi  temp1, -( 'A' - '9' - 1 )

lcd_number_hex_digit_1:

          subi  temp1, -'0'
          call  lcd_data
          ret

</avrasm>

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.

<avrasm>

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
die Zehntausenderstellen abzählen ...
          ldi   temp1, '0'

lcd_number0:

          subi  temp2, low(10000)
          sbci  temp3, high(10000)
          brcs  lcd_number1
          inc   temp1
          rjmp  lcd_number0
.. und ausgeben

lcd_number1:

          rcall lcd_data
          subi  temp2, low(-10000)
          sbci  temp3, high(-10000)
die Tausenderstellen abzählen ...
          ldi   temp1, '0'

lcd_number2:

          subi  temp2, low(1000)
          sbci  temp3, high(1000)
          brcs  lcd_number3
          inc   temp1
          rjmp  lcd_number2
... und ausgeben

lcd_number3:

          rcall lcd_data
          subi  temp2, low(-1000)
          sbci  temp3, high(-1000)
Als nächtes kommt die Hunderterstelle drann
          ldi   temp1, '0'

lcd_number4:

          subi  temp2, low(100)
          sbci  temp3, high(100)
          brcs  lcd_number5
          inc   temp1
          rjmp  lcd_number4
und ausgeben

lcd_number5:

          rcall lcd_data
          subi  temp2, -100
bleiben noch die Zehner
          ldi   temp1, '0'

lcd_number6:

          subi  temp2, 10
          brcs  lcd_number7
          inc   temp1
          rjmp  lcd_number6
ausgeben ...

lcd_number7:

          rcall lcd_data
          subi  temp2, -10
          ldi   temp1, '0'
          add   temp1, temp2
          rcall lcd_data
fertig. Stack wieder aufräumen
          pop   temp1
          pop   temp2
          pop   temp3
          ret

</avrasm>

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 (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.

Download lcd-routines.asm