Skip to main content

PIC LCD 1602A 4-BIT-Interface-Ansteuerung

An dieser Stelle wird die Ansteuerung eines LCD-Displays mit einem PIC-Mikrokontroller erläutert werden. Der Grund für diese Beschreibung ist der, dass ich bei meiner Suche diesem Thema bezüglich nicht zufriedenstellende Ergebnisse erhalten habe. Für mich wichtig, ist das Verständnis "Warum, Wieso, Weshalb" die Ansteuerung so gemacht wird, wie die gemacht wird. Dies ist durch die verwendeten Libraries ein wenig "verborgen" geblieben.

Ich hoffe, dass ich meine Erfahrung gut verständlich dem Leser rüberbringen kann. Der nachfolgende Text soll kein "Grundlagen-Tutorial" in Mikroprozessor-Programmierung werden. Diese gibt es Zahlreich und in sehr guter Qualität im Internet zu finden.

 Benötigt werden:

  • PIC Mikrokontroller
  • Ein Programmer (hier Verwendet PICKit 3)
  • Entwicklungsumgebung (z.B. MPLAB X von Microchip mit XC8, XC16 oder XC32 Kompiler (abhängig vom Verwendeten Mikrokontroller))
  • Ein Steckbord um "Luftverkabelung" zu vermeiden
  • 5V Spannungsquelle
  • Datenblätter vom PIC und vom Verwendeten Display (hier 1602A welcher in meinem Fall beim Arduino-Kit mitgeliefert wurde)

PIC Konfiguration:
Allgemein ist die Konfiguration davon Abhängig, welcher PIC verwendet wird. In meinem Fall soll der interne Oszillator für den nötigen Takt sorgen. Der Rest ist momentan nicht wichtig und wurde ausgeschaltet.

Um die Konfiguration zu testen, bringen wir (Traditionsgemäß) eine LED zum Blinken. Dies ist am Anfang recht hilfreich, um die benötigten Verzögerungen einstellen zu können. Dafür werden die "__delay_x(y)" Makros verwendet. Diese benötigen die Definition von _XTAL_FREQ Symbols, welches der Frequenz des Oszillators in Hz entspricht. Beim PIC18F4680 ist dies durch die Zeile:
#define _XTAL_FREQ 1000000
Bei der verwendeten Konfiguration erledigt. 

Die LED wird am Pin D4 angeschlossen und kann entweder durch LATDbits.LATD4 angesprochen werden oder durch den Platzhalter "LED". Dies wird wie folgt definiert:
#define LED LATDbits.LATD4

Damit dieser Pin als Ausgang verwendet werden kann, muss dies über das TRIS Register im Programm beschrieben werden. Das gesamte Programm zum Testen der __delay Makros sieht nun so aus:

#include "headerfile.h"
#include <xc.h>
#include "delays.h"
#define _XTAL_FREQ 1000000
#define LED LATDbits.LATD4

void main()
{
TRISDbits.TRISD4 = 0; //Definiert Pin D4 als Output
While(1)
{
LED = ~LED; // Invertiert den Zustand der LED
__delay_ms(500);// Entspricht einer Frequenz von 1Hz (0,5 Sekunden an, 0,5 Sekunden aus 
}
}

Wenn bis hierhin nichts gänzlich schiefgelaufen ist, dann können wir uns nun dem eigentlichen Thema der Display Ansteuerung widmen.
Dafür definieren wir auch hier die Anschlüsse des Displays an den PIC18F4680:
#define DISPLAY_RS LATDbits.LATD5
#define DISPLAY_RW LATDbits.LATD6
#define DISPLAY_E LATDbits.LATD7
#define DISPLAY_DATA LATB

Jetzt befassen wir uns mit dem Programm. Beim Programmstart, führe ich gerne eine "Initialisierung" durch. Darin möchte ich festlegen, welche Pins des PICs verwendet werden und welcher Spannungspegel an diesen anliegen soll. Dies mache ich durch die Funktion PicInit().

void PicInit()
{
TRISD = 0;
TRISB = 0;
LATD = 0;
LATB = 0;
}

Damit sind die Ports B und D als Ausgänge festgelegt und führen einen LOW-Pegel. Jetzt kommt der etwas heiklere Teil, beidem wir nun das Display initialisieren. Dafür nehmen wir uns das Datenblatt zu Hilfe und schauen uns die Initialisierungsprozedur für ein 4-Bit-Interface an:

Und genau an dieser Stelle sollte man sich die Frage stellen: "Woher weiß das Display, wann es den nächsten Befehl erfassen soll? Um diese Frage zu beantworten, müssen wir uns die "Timing Characteristics" des Displays anschauen. Insbesondere den Fall für "Daten Schreiben". Vorweg: Der Pin R/W wird dazu benutzt, um Daten in das Display zu schreiben (R/W = 0) und zum Lesen (R/W = 1). Da wir hier lediglich Daten schreiben wollen, wird dieser Anschluss nicht beachtet und könnte somit auch gegen Masse und nicht an den PIC geschaltet werden. 

Timing Characteristics

Quelle: Datenblatt LCD-1602A

Diesem Auszug aus dem Datenblatt kann man entnehmen, dass die Daten, welche an DB0-DB7 (Bei uns an DB4-DB7) erst erfasst werden, nachdem wir VORHER den "Enable" Pin auf High (1) gesetzt haben.

Stolperfalle

Die Zeit von E=1 ==> Daten ==> E=0 sollte nicht kürzer sein, als im Datenblatt angegeben! In diesem Fall darf für diese Prozedur eine Zeit von (Tw + Th2) 230ns + 10ns = 240ns nicht unterschritten werden. Bei einer Oszillatorfrequenz von 1MHz und bei 4 Taktzyklen für die Abarbeitung eines Befehls, kommen wir somit auf eine Zeit von: (E=1) ==> Daten (4-Takte) ==> E=0 (4 Takte) = (4/1MHz) * 2Befehle=8µs. Damit sollten wir eigentlich auch keine Probleme mit der Zykluszeit (Verstrichene Zeit, bis der nächste Befehl angenommen werden kann) haben. Bei höheren Taktraten des PICs ist an dieser Stelle besondere Vorsicht angesagt, wenn man keine Niederlage hinnehmen möchte. Möglicher Ausweg: Für die Initialisierung delays einbauen und für den Betrieb das Busy-Flag Abfragen.

Timing Tabelle. Quelle: Datenblatt 1602A

Bevor wir uns der Initialisierung widmen, gilt es noch zu klären, wo das Display an Port B angeschlossen ist. In meinem Fall habe ich mich dazu entschieden, die Ausgänge RB0 bis RB3 als Datenausgänge für das Display zu verwenden. Dies hat den Grund, dass der PICKit 3 Programmer an die Pins RB7 und RB6 (PGC/PGD) Angeschlossen wird und ich mir die Option des Debuggens noch offen halte möchte und evtl.  Interaktionen zwischen PICKit 3 und dem Display nicht haben möchte. 
Dies führt dazu, dass wir zuerst die "oberen" 4-Bits und dann die "unteren" 4-Bits übertragen müssen. Wie dies geschieht, werden wir gleich sehen. Beginnen wir nun mit der Initialisierung

void DisplayInit()
{
DISPLAY_RS = 0;
DISPLAY_RW = 0;
DISPLAY_E = 1;
DISPLAY_DATA = 0b00110000 >> 4; //Hier wird das Datenwort 4mal nach rechts verschoben
DISPLAY_E = 0;
__delay_ms(5);        
DISPLAY_E = 1;
DISPLAY_E = 0;
__delay_us(120);
DISPLAY_E = 1;
DISPLAY_E = 0;
DISPLAY_E = 1;
DISPLAY_DATA = 0b00100000 >> 4;
DISPLAY_E = 0;
DISPLAY_E = 1;
DISPLAY_DATA = 0b00101100 >> 4;
DISPLAY_E = 0;
DISPLAY_E = 1;
DISPLAY_DATA = 0b00101100;
DISPLAY_E = 0;
DISPLAY_E = 1;
DISPLAY_DATA = 0b00001100 >>4;
DISPLAY_E = 0;
DISPLAY_E = 1;
DISPLAY_DATA = 0b00001100;
}
Die Initialisierung ist an dieser Stelle erledigt und das Display könnte nun Daten oder Befehle Empfangen.

Damit wir allerdings nicht jedes Mal die Vorgehensweise mit "Rotation nach rechts um 4 Stellen" hinschreiben müssen, erstellen wir eine Funktion dafür, welche das Übernimmt.

void WriteDisplayCommand(unsigned cmd)
{
DISPLAY_E = 1;
DISPLAY_DATA = (cmd >> 4) & 0b00001111;//An dieser Stelle möchte ich erreichen, dass die Pins
von Port B, welche nicht angeschlossen sind, weiterhin ihren LOW-Pegel beibehalten. Deswegen die UND-Verknüpfung.
DISPLAY_E = 0;
DISPLAY_E = 1;
DISPLAY_DATA = cmd & 0b00001111;
DISPLAY_E = 0;
}

Die Funktion WriteDisplayCommand() übernimmt das Schreiben eines Befehls. Jetzt benötigen wir noch eine Funktion um ein "Zeichen" (Buchstaben / Zahlen etc.) zu schreiben:

Void WriteDisplayData(char temp)
{
DISPLAY_RS = 1;
WriteDisplayCommand(temp);
DISPLAY_RS = 0;
}

Was ist hier passiert? Nun: Der Unterschied zwischen dem Schreiben eines Befehls und dem Schreiben von Daten ist der RS Pin, welcher beim Schreiben von Daten aktiv sein sollte. Aus diesem Grund wird hier der RS Pin auf 1 gesetzt und der Wert zum Schreiben an die Funktion WriteDisplayCommand() übergeben.

Was benötigen wir noch? Richtig. Wir benötigen noch eine "Zeichenkette", welche dargestellt werden soll. Dafür erzeugen wir ein Array, welcher aus Zeichen-Elementen bestehen soll.

Char zeile1[]="Hallo Zeile 1";

Und natürlich soll in der zweiten Zeile etwas anderes dargestellt werden.

Char zeile2[]="Hallo Zeile 2";

In der nachfolgenden Abbildung ist die Position im Display und die Position im DDRAM abgebildet. Des Weiteren könnt  ihr der Befehlstabelle aus dem Datenblatt entnehmen, dass wenn man die DDRAM Adresse setzen will, das höchste Bit 1 sein soll. Aus diesem Grund werde ich im Hauptprogramm die DDRAM Adresse mit 0b10000000 ODER verknüpfen. Weil wir hier als "Höhepunkt" lediglich nur das Display ansteuern möchten, machen wir vorher keine zeitaufwendigen Berechnungen nach dem Sinn des Lebens und so weiter. Deswegen müssen wir eine Zeitverzögerung am Anfang des Programms einbauen, damit das Display nach dem Einschalten ein wenig Zeit hat sich selbst zu reseten. Im Datenblatt steht unter dem Punkt Initialisierung, dass man das Display mehr als 15ms nach Einschalten "ruhen" lassen sollte. 

Display Position und DDRAM Adresse

Quelle: Datenblatt 1602A. Display position DDRAM adress.

void main()
{
__delay_ms(500); // 500ms sollten zum "chillen" reichen.
Int i;
PicInit();
DisplayInit();
Char zeile1[] = "Hallo Zeile 1";
Char zeile2[] = "Hallo Zeile 2";
WriteDisplayCommand(0x00 | 0b10000000);//Hier möchten wir uns auf die DDRAM Adresse 00 positionieren.
i=0;
While (i<sizeof(zeile1)-1))
{
       WriteDisplayData(zeile1[i]);
       i++;
}

WriteDisplayCommand(0x40 | 0b10000000);//Hier möchten wir uns auf die DDRAM Adresse 40 positionieren.
i=0;
While(i<sizeof(zeile2)-1))
{
       WriteDisplayData(zeile2[i]);
       i++;
}
While(1)//Die Blinkende LED darf natürlich nicht fehlen
{
LED = ~LED;
__delay_ms(500);
}
}

Anmerkung:

Anmerkung: Wenn Sie einen anderen PIC Verwenden, als hier in diesem Beispiel, so sind die Änderung lediglich in Ihrer PIC-Konfiguration. Ist Ihre Oszillatorfrequenz nicht 1MHz, so muss _XTAL_FREQ ebenfalls angepasst werden. Zu beachten gilt weiter, dass bei höheren Taktraten die Makros __delay_ms() oder __delay_us() nicht beliebig hoch sein können. Darauf wird Sie der Kompiler hinweisen. Eine mögliche Abhilfe (kann) das Aufrufen mehrfacher __delay_ms() Makros sein. Allerdings könnte bei mehrfachem Aufrufen eine Vergrößerung der gesamt Verzögerung entstehen.

Wenn Sie dieses Beispiel zum Einstieg verwenden, so können Sie (Ohne PIC, Programmer, etc) die Simulation in MPLAB dafür verwenden, um zu sehen, was mit den einzelnen Registern im PIC passieren würde. So sieht man, wie der Operator ">>4" oder ">>3" eine BIT-Verschiebung nach rechts um 4 bzw. 3 Stellen verursacht. Oder dass die Funktion sizeof(zeile1) einen Integer Wert rausgibt, der die Länge des Arrays beschreibt.

 

Falls Sie Anfänger sind, so könnten Sie sich zu Übung überlegen, wie das Programm angepasst/erweitert werden muss, damit man die Pins des PortB (RB4 - RB7) dazu verwenden könnte, um das gesamte Display nur über PORTB an zu steuern.

 

SPOILER: Möglicher Lösungsansatz sieht so aus, dass die Daten, die Geschrieben werden sollen, mit 000 1111 "Maskiert" (UND-Verknüpft) werden. Anschließend wird PORTB gelesen (Um den Zustand der "Kontrollleitungen" nicht zu verändern). Der Ausgelesene PORTB wird nun mit 1111 0000 UND-Verknüpft und im nächsten Schritt mit den Daten, die Geschrieben werden müssen, welche jetzt in Form von (0000 xxxx) zu Verfügung stehen ODER-Verknüpft. Dadurch erhält man folgendes Datenwort, welches wir nun an PORTB legen: (yyyy xxxx) Darin ist (yyyy) die 4 Konfigurationsleitungen mit deren Zuständen und xxxx die Daten, die an das Display übermittelt werden.

Muss man nun den E Pin vom Display setzen, so sollte man darauf achten, dass die Daten nicht verändert werden. Dies ist aber kein Problem, wenn man die DISPLAY_E Anspricht und nicht den Gesamten Port um E zu ändern.

 

Kritik an dem Artikel, Formulierung, Bereitstellung der Datenblätter oder sonstige Anregung schreiben Sie bitte in das Kontaktformular dieser Internetseite. Ich werde mich bei Zeiten um Ihr Anliegen kümmern.

Viel Spaß beim Experimentieren, Ausprobieren und Lernen

Michael Kubrin

Alle bei dem Projekt entstandene Dateien