Start AVR-KursStartseiteserielles ISP-Interface

14. Eine LCD-Anzeige - Programmbibliotheken nutzen


Quickstart:

Herunterladen und speichern des Hex-Files "uhr.hex" (Rechtsklick und "Ziel speichern unter"). Verbindungen gemäß Schaltskizze herstellen und Hex-File brennen.

Für Genieser und Leute, die alles wissen wollen, bitte weiterlesen.

Bisher haben LEDs den Betriebszustand des Controllers gut vermitteln können. Unter anderen Gesichtspunkten betrachtet, benötigt eine LED im Volllastbetrieb eine Stromstärke von 20 mA. Wird sie im Blinkerbetrieb mit 1% Lastanteil (Dutycycle) eingestetzt, beträgt der mittlere Stromanteil nur noch 0,2 mA. Die Informationswirkung ist in diesem Fall viel besser wie bei Dauerlicht, die Belastung der Energieversorgung aber nur 1% im Vergleich zur Dauerlast.

Wenn allerdings genauere Informationen, zum Beispiel zum Ablauf einer Zeitmessung gefragt sind, dann wird es Zeit eine alphanumerische Anzeigeeinheit einzusetzen. Hier bieten sich die vielfältig angebotenen LC-Displays (LCD) mit Industriestandard an. Die Schnittstelle derartiger LCDs ist genormt und in stets gleicher Weise ansprechbar. Das betrifft sowohl die elektrischen Kontakte wie auch die Art und Weise der Datenübergabe (Handshaking), man nennt dies auch das Protokoll des Datentransfers. Beides wird nachfolgend genau beschrieben. Natürlich folgt auch die Umsetzung in einem Projektbeispiel.

Kernpunkt dieses Projekts ist die Nutzung von Programmbibliotheken. Das sind Unterprogrammsammlungen, die als wesentlichste Information für den Nutzer eine Beschreibung enthalten, wie die einzelnen Proceduren zu verwenden sind, welche Parameter sie erwarten, welche Register sie verändern und welche Werte sie in welcher Weise zurückgeben (bei Funktionen). Ich habe diese Beschreibung in Anlehnung an das verstaubte Turbopascal auch "Interface" genannt. Auf Windowsebene nennt sich so was heute API und füllt ganze Bücher. Für meine Mikros habe ich eine Reihe solcher Bibliotheken, die LCD-Bibliothek ist nur eine davon. Ferner gibt es welche für Arithmetik, die RS232-Schnittstelle, PWM-Steuerung, Timer... Der Vorteil dieser Procedursammlungen ist der, dass sie auf Grund ihrer Struktur in jedes andere Programm (-Projekt) integriert werden können und das in den meisten Fällen sogar ohne großartige Änderungen am Procedurtext.

Im vorliegenden Fall wurde die Integration so vollzogen, dass die gesamte Bibliothek für die LCD-Verwendung als Quelltext komplett in die Datei "uhr.asm" einkopiert wurde. Dabei wurden geringfügige Anpassungen an die vorliegende Anwendung im Kopf des Procedurpakets vollzogen. Ein derartiges Vorgehen bläht zwar den aktuellen Programmtext unmittelbar auf, man sieht aber besser, wie sich die Bibliothek in das "Betriebssystem" des AVR einbettet und dass dafür natürlich Speicherplatz benötigt wird. Eine andere Möglichkeit der Integration von Programmbibliotheken ist durch das "Include" der Bibliothek "arithmetik.asm" angedeutet. Durch die Definition einer Variable mittels der ".equ"-Assemblerdirektive und der Abfrage durch die Direktive ".ifdefined" wird diese Bibliothek bei Bedarf eingebunden. Die in deren Interface beschriebenen Proceduren und Funktionen sind dann überall im Hauptprogramm und in Unterprogrammen genau so aufrufbar, wie wenn der Quelltext im Wortlaut in der Projektdatei aufgeführt wäre. Derartige Include-Dateien befinden sich bei mir in einem speziellen Verzeichnis, das dem Pfad zu einem bestimmten Prozessortyp untergeordnet ist. Wird die Integration einer Bibliothek aktuell, dann gebe ich einfach diesen Pfad in der Menüoption Projekt - Assembleroptions unter "Additional include path" an.


Das vorliegende Beispiel verwendet die LED-Tasten-Platine und die eigens entwickelte Anschlussplatine für das Mini-LCD mit 2 Zeilen zu je 16 ASCII-Zeichen . Das Platinchen wird mit 10 ca. 5cm langen dünnen Litzen mit der LCD-Platine verbunden. Die Leitungen wurden aus einem Flachbandkabel aus einem alten PC (Floppyleitung oder Festplattenanschlussleitung) hergestellt. Das Verlöten auf der Interfaceplatine ist einfach, auf der vorliegenden LCD-Platine haben die Anschlüsse nur 1 mm Abstand, extrem genaues Löten ist erforderlich, damit keine Kurzschlussbrücken entstehen. Die Interfaceplatine enthält neben der Buchsenleiste für den Anschluss an die Experimentierplatine auch noch ein Trimmpoti mit dem der Kontrast der Anzeige einjustiert werden kann.

Zur Funktionskontrolle dient wieder eine LED auf der LED-Tasten-Platine, die mit 50% Dutycycle betrieben wird. Wenn diese LED, die am PortD.4 liegt blinkt, funktioniert auf jeden Fall die Interruptserviceroutine des Zählers Timer/Counter0. Damit wird auch die Zeiterfassung korrekt arbeiten, die für das vorliegende Beispiel "Uhr" gebraucht wird.

gedruckte Interfaceplatine und ...

als Lochstreifen-Raster

 

Zum Ansteuern der LCD-Interfaceplatine sind 8 Leitungen nötig. Zwei davon sind für die Energieversorgung (Pin1: Vss=0V=GND; Pin2: Vcc=+5V), zwei für die Steuerung (Pin4=R/S; Pin6=-E) und 4 sind Datenleitungen (Pin7=D4 bis Pin10=D7). Die Verbindung des Pins R/W des LC-Displays und der Vo-Anschluss zur Kontrasteinstellung werden auf dem Interfaceplatinchen realisiert.

Die Übergabe eines 8-Bit-ACSII-Zeichens erfolgt in zwei Schritten zu je 4 Bit. Für den Benutzer der Procedurbibliothek geschieht dies transparent, das heißt, er bekommt davon nichts mit. Er übergibt den ASCII-Zeichencode an die Bibliotheksroutine, die ein Zeichen auf das LCD ausgibt, ruft diese Routine auf und das Zeichen erscheint in der LCD-Anzeige. Für den Kurs ist es natürlich interessant, wie so eine Bibliothek aufgebaut sein muss, damit sie so einfach und universell einsetzbar ist.

            

Im Schaltplan des Projekts findet man all die Dinge wieder, die eben beschrieben wurden. Der +5V-Anschluss auf der LCD-Platine ist der Anschluss 1, das Pin D7 ist der Anschluss 14. Die Leitungen D0 .. D3 (Anschluss 7 ... 10) sind nicht belegt. Alle LC-Displays mit Industriestandard haben die gleiche Belegung und werden von der Software in gleicher Weise angesprochen. Rechts das Platinchen für den Übergang vom Controlleranschluss oben (Buchsenleiste) zur LCD-Verdrahtung unten. Unten an den beschrifteten Pins gehen die 10 dünnen Leitungen an die LCD-Pins weg.

Nach der Hardware, jetzt zur Behandlung des AVR-Programms, das sich in fünf Teile gliedert.

Declarationen

Pseudobefehle zur Assemblersteuerung haben wir schon einige kennengelernt. Der Befehl ".ifdef arithmetik_asm" sorgt hier dafür, dass, falls die Variable "arithmetik_asm" in der Namensliste geführt wird, also bereits definiert wurde, die Bibliotheksdatei "arithmetik.asm" mit in die Übersetzung eingebunden wird. Die Zeile oberhalb ist durch einen ";" auskommentiert, daher wird die Direktive zur Definition der Variablen "arithmetik_asm" nicht ausgeführt, die Variable steht also nicht in der Namensliste. In diesem Programm wird zu dieser Zeit deshalb die Arithmetikbibliothek nicht eingebunden.

Man beachte ferner, dass die Assemblervariablen lcd_asm und toggle_test_pin sehr wohl definiert werden und mit 1 beziehungsweise 0 belegt werden.

Die Variable "lcd_asm" sorgt als nächstes gleich dafür, dass die Portleitungen, die für den Kontakt zum LCD benötigt werden, definiert werden. So wie hier angegeben, werden die Ports, Datenrichtungsregister und Pins nämlich auch in der entsprechenden UP-Bibliothek referenziert. Man erkennt den Vorteil dieses Verfahrens. Am Anfang des Programms werden die Zuordnungen getroffen, weil hier die Übersicht vorhanden ist. Hier und nirgends mehr sonst werden die LCD-Variablen festgelegt. In der Bibliothek kommen die Variablen vielfältig an verschiedenen Stellen vor. Wollte man Änderungen durchführen, müsste man alle diese Stellen im UP aufsuchen und abändern - das Chaos wäre vorprogrammiert.

Deshalb setzen wir Declarationen einmal an den Anfang des Programms und verwenden fortan nur noch die Aliasnamen der Ports, Register...

Steuervariablen wie "lcd_asm" dienen dazu, Programmteile wie UP-Bibliotheken, Testroutinen etc. einfach in den Programmtext ein- und auszuklinken. Neben den einfachen Abfragen wie "ifdef" (falls definiert) oder "ifndef" (falls nicht definiert) gibt es noch die Vergleichsabfragen, mit denen man auf Bereiche oder Werte prüfen kann.

Ein letztes Beispiel behandelt den folgenden Fall. In einem UP muss eine ganz bestimmte Variable "XTAL" belegt sein. Es könnte aber sein, dass sie schon irgendwo anders einen Wert erhalten hat, also bereits festgelegt ist. Würde man versuchen, die Variable XTAL noch ein weiteres Mal zu definieren, so hätte das einen Fehler zur Folge, der die Assembliereung abbrechen kann.

Das wird hier dadurch umgangen, dass die Declaration nur dann stattfindet, wenn die Variable noch nicht definiert ist.

 

Hauptprogramm

In den Vorbereitungen des Hauptprogramms findet man als bemerkenswerteste Neuerungen die UP-Aufrufe zur Initialisierung des LCD und der Begrüßungsmeldung, 3 Sekunden warten und das Löschen aller Zeitvariablen.

Damit das LC-Display ordentlich arbeitet, muss es initialisiert werden. Alle dafür nötigen Schritte enthält die Routine "lcd_init". Was da alles drinsteht, das betrachten wir später.

Drei Sekunden warten ganz einfach: Sekundenzähler löschen (clr sekunden) und warten bis er auf 3 erhöht wurde, durch die Interruptroutine natürlich, die im Hintergrund arbeitet.

Neben den eigentlichen Zeitvariablen gibt es noch ein Flag, das durch die ISR (Interrupt Service Routine) immer dann gesetzt wird, wenn eine Sekunde vergangen ist. Auch dieses Flag (Flagge = Merker) muss zunächst gelöscht werden.

In der Hauptschleife wird jetzt auf das Setzen dieses Flags gewartet. Während dieser Zeit von 1,58 Hundertstelsekunden könnte man natürlich auch noch andere Sachen tun lassen aber im Moment gibt es keine anderen Sachen. Mit dem Update der Zeitanzeige ist der Prozessor also ca. 1,6% ausgelastet, wirklich genug Zeit für andere Sachen!

Bevor die Zeitangaben neu geschrieben werden, wird jedes Mal das Display gelöscht, der Cursor nach links oben also "home" geschickt und das Cursorblinken abgestellt. Das Ausgeben der Daten erfolgt drei mal in analoger Weise:
Reister temp1 mit dem Zeitwert laden und die Datenausgabe "lcd_number" aufrufen. das UP "lcd_number" erwartet den Binär-Code des auszugebenen Werts, wandelt ihn in ASCII-Zeichen um und schreibt diese in die LC-Anzeige. Dann wird das Flag für volle Sekunden gelöscht.

Um festzustellen, wie lang der Prozessor dazu gebraucht hat, werden jetzt noch die Hunderstelsekunden und Jiffies (1 Jiffie = 1/50 einer Hundertstelsekunde) ausgelesen, zwischengespeichert und in der zweiten Zeile des Displays ausgegeben. Das Ergebnis 1:29, was die oben genannten 1,58 Hundertstelsekunden ergibt.

Interrupt Service Routine

Zu Beginn der ISR wird eine Testleitung, dieses Mal ist es PortD.2 (Siehe Declarationsteil) auf 1 gesetzt, dann die Anzahl der Jiffies erhöht. Die Jobs 2 und 3 werden mangels Notwendigkeit übersprungen, dann erfolgt das Zeitupdate. Nach jeweils 100 - ledan Hundertstelsekunden wird die LED-Leitung (PortD.4) auf 0 gezogen, also LED an. Beim Erreichen einer vollen Sekunde: LED aus durch setzen von PortD.4=1 und setzen des Flags für volle Sekunden.

Was im Einzelnen passiert, erklären die Komentare. Letzte Handlung der ISR, Testpin auf 0 und nach dem Restaurieren wichtiger Register zurück zum unterbrochenen Programm.

 

Betriebssystem - Bibiliothek

Der allgemeinen Beschreibung folgen die Informationen zum Interface. Hier werden die Funktionen der einzelnen UP-Routinen beschrieben und, welche Daten in welchen Registern, Flags oder sonstigen Speicherstellen erwartet werden. Natürlich sind hier auch die Register aufgeführt, in denen evtl. verarbeitete Daten vom UP zurückgeliefert werden. Letzteres ist bei dieser Bibliothek allerdings nicht nötig, da das Ziel des Datentransports ja das LCD ist.

Die Namen sowie die Beschreibungen der Routinen sind was Aufruf und Datenübergabe angeht selbstredend. Wenden wir uns also einigen UPs zu.

Ich warne an dieser Stelle noch einmal ausdrücklich davor, in UPs, speziell in solchen, die für Bibliotheksroutinen vorgesehen sind, absolute Portadressen zu verwenden. "Sprechende" Aliasnamen, die am Programmstart definiert werden, sind der absolute Hit und erleichtern Pflege und Anwendung der Bibliothek.

Zur Parameterübergabe werden ausschließlich globale Speicherstellen, in der Regel Register, verwendet, so wie es auch Burkhard Kainka in seinen AVR-Projekten empfiehlt.

Ausgabe eines ASCII-Zeichens

Das Sichern der Register temp1 bis temp3 und des Statusregisters sreg und das Restaurieren vor Verlassen der Routine verursacht zwar einen erheblichen zeitlichen Aufwand (auch zur Laufzeit), sorgt aber dafür, dass bei der Rückkehr zum aufrufenden Programm alle Register wieder ihren alten Wert haben. Wird dieser Aufwand nicht getrieben, kann es zu sehr unschönen Programmfehlern kommen, die nur sehr schwer zu finden sind.

Deshalb: Alle Register, die in einer UP-Routine verändert werden (das Statusregister wird meist vergessen), müssen auf dem Stapel gerettet und vor Ende restauriert werden.

Nach dem Anlegen einer Kopie des übergebenen Bytes, werden die beiden Nibbles (= halbe Bytes = 4 Bits) vertauscht, weil das High-Nibble (Bits 4-7) zuerst ausgegeben werden müssen.

Die Undierung mit 0b00001111 blendet die Bits 4 bis 7 von temp1 aus. Aus 0b11011110 wird also z.B. 0b00001110.

Der RS-Anschluss der LC-Anzeige wird auf 1 gesetzt, das bedeutet, dass die Übergabe eines Datenbytes erfolgt.

Der LCD_PORT wird nach temp3 eingelesen, das untere Nibble (Bit 0 -3) auf 0 gesetzt, die obere Hälfte des Portregisters bleibt unberührt, weil man ja nicht weiß, was diese Leitungen sonst noch für eine Bedeutung haben. Aus dem gleichen Grund wurden vorhin auch die Bits 4 bis 7 von temp1 gelöscht. Bei der jetzt folgenden Oderierung von temp1 mit temp3 werden die Nibbels der beiden Register gemischt. Das Lownibble stammt vom Datenbyte in temp1, das Highnibble enthält die oberen Werte vom LCD_PORT.

Bei der Ausgabe an den LCD_PORT wird also nur das Lownibble geändert.

Durch den Aufruf von lcd_enable wird ein kurzer Impuls am Anschluss "Enable" des LCD erzeugt, der die Datenübernahme anschließt.

Gleich danach wird das Lownibble des Datenbytes an das LCD übergeben. Für die Verarbeitung braucht das LCD etwas Zeit, wir warten 50 Mikrosekunden. Damit das auch dann funktioniert, wenn keine Timer zur IRQ-Steuerung frei sind, ist die Warteschleife softwaremäßig implementiert.

Ausgabe eines Befehls

Die Ausgabe eines Befehls an das LCD erfolgt analog der Datenausgabe. Der einzige Unterschied ist der, dass die Leitung RS des LCD auf 0 gelegt werden muss statt auf 1. Die Konstante 0b11110000 wurde hier durch die wertgleiche Schreibweise als Hexzahl 0xF0 ersetzt.

Enable-Puls und Warteschleifen

Die oben bereits verwendete Routine lcd_enable erzeugt auf der zu PIN_E gehörenden Leitung des Ports e_PORT einen Impuls, der ca. 3 Takte lang ist. Bei einem 8MHz-Quarz sind das 3 x 0,125µs = 0,375µs. Die Impulslänge, die sich mit einem 20MHz-Quarz ergibt (0,15µs) ist immer noch ausreichend. Die Information am Datenbus wird stets mit der fallenden Flanke des Enable-Pulses in das LCD übernommen.

delay50µs

temp1 wird mit dem Wert t50us belegt und dann in der Schleife heruntergezählt. Der Wert von t50us hängt vom Systemtakt ab, natürlich auch von evtl. eingestellten Vorteilern. Wir verwenden hier 8 MHz ohne Vorteilung, die Schleife benötigt bei einem Durchlauf mit Verzweigung zur Marke delay50us_ 5Takte. Vorspann und Nachspann sowie Zeit für den Aufruf der Routine nicht mitgerechnet, wie oft muss die Schleife durchlaufen werden, welchen Wert muss t50us haben? Die Quarzfrequenz ist 8 MHz. 50 µs mal 8 Takte (pro µs)= 400 Takte. 400 Takte durch 5 Takte = 80. t50us muss den Wert 80 vertreten, die Schleife muss also 80 mal durchlaufen werden. Ein Blick in die map-Datei "uhr.map" (Rechtsklick und "Ziel speichern unter") des Projekts zeigt, dass die Formel nach welcher der Assembler diesen Wert berechnet, korrekt ist:

.equ t50us = ( XTAL * 50 / 5 ) / 1000000

Berechne den Wert t5ms, der in der Routine delay5ms verwendet wird.

OK, also:

Quarzfrquenz = 8 MHz daher 8 Takte / µs (kein Vorteiler für die Systemfrequenz)

5 ms = 5000µs = 40000 Takte

Innere Schleife läuft 200 mal mit 3 Takten, einmal mit 2 Takten = 602 Takte

Von der äußeren Schleife kommen 1 + 1 + 2 Takte = 4 Takte dazu

Die Äußere Schleife muss also rund 40000 durch 606 = 64 mal durchlaufen werden.

 

Zur Initialisierung müssen an das LCD eine Reihe von Befehlen gesendet werden wobei gewisse Wartezeiten einzuhalten sind.

Welche Befehle ein Industrie-LCD versteht, ist schnell gesagt. "*" bedeutet einen beliebigen Wert. Die RS-Leitung muss zur Kommandoübermittlung stets auf Low = 0 liegen.

Befehl
D7
D6
D5
D4
D3
D2
D1
D0
Funktion
Clear Display
0
0
0
0
0
0
0
1
Anzeige löschen (2 ms)
Cursor home
0
0
0
0
0
0
1
*
Cursor nach links oben (1. Zeile, 1, Spalte) (2 ms)
Entry mode
0
0
0
0
0
1
I/D
S

Nach dem Schreiben eines Zeichens: (50 µs)

I/D = 1 vorwärts/rechts schieben
I/D = 0 rückwärts/links schieben
S = 1 Anzeige schieben
S = 0 Cursor schieben

Dislpay on/off
0
0
0
0
1
D
C
B
D = 1/0: Display an/aus (50 µs)
C = 1/0: Unterstrichcursor ein/aus
B = 1/0: Blinkcursor ein/aus
cursor/display shift
0
0
0
1
S/C
R/L
*
*
S/C = 1/0: schiebe Anzeige/Cursor um eine Stelle nach
R/L = 1/0: rechts/links (50 µs)
System set
0
0
1
DL
N
F
G
*

DL = 1/0: 8-Bit /4Bit Datenbusbreite (50 µs)
N = 1/0:2 oder 4/1 Displayzeile(n)
F = 1/0: großer/kleiner Zeichensatz
G = don't care (nur Philips)

Ferner gibt es noch Befehle zur Adressierung des Zeichensatz-RAM und des Datenspeichers, die aber hier nicht das Thema sind. Nach der Initialisierungsprozedur ist das LCD bereit, Daten und Befehle zu empfangen. Weil die folgenden Aktionen häufig vorkommen sind sie wieder als Bibliotheksroutinen verfasst. "lcd_clear" und "lcd_home" werden ohne Parameter aufgerufen.

Für die Display- und Cursoreinstellung müssen die Bits 2:0 des Übergaberegisters temp1 entsprechend gesetzt werden. Die Routine "lcd_position" nutzt den Positionierungsbefehl für das DD-RAM (Display Data Speicher). Die Position der Spalte (temp1) kann je nach Displaytyp die Werte 0...16 (20; 32 ??) annehmen und die Zeile in temp2 mit 0 oder 1 belegt werden.

Einer näheren Betrachtung wert ist noch die im Projekt benutzte Routine, die einen Bytewert in eine ASCII-Zeichenkette umwandelt. Die Kommentare dürften für ein Verständnis ausreichen .

Projektgebundene Unterprogramme

Auch hier ist der Programmtext hoffentlich ausreichend, diesmal auch ohne Kommentare.


So, und wer bis jetzt durchgehalten hat, der darf sich jetzt auch das Assemblerquellprogramm "uhr.asm" (Rechtsklick und "Ziel speichern unter") herunterladen und selber Experimente damit durchführen.

 


Start AVR-KursAn den Seitenbeginnnicht verfügbar