Ein Dot-Matrix-LCD (1602A und 2004A) mit dem Raspberry Pi ansteuern

In meinen letzten Projekt haben wir eine 4-fach-7-Segment-Anzeige benutzt. Damit lassen sich 4 Ziffern (und rudimentär auch Buchstaben) darstellen. Das reicht nur für einfache Anzeigeaufgaben.

Falls mehr darzustellen ist, empfielt sich ein LCD (liquid crytal display, Flüssigkristallanzeige), dass man mit beliebigen Buchstaben füttern kann und dass dann zwischen 8 und 80 Zeichen darstellt. Gebräuchlich sind 16x2-Displays mit 2 Zeile zu 16 Zeichen ( Bauteil 1602A) und 20x4-Displays mit 4 Zeilen zu 20 Zeichen ( Bauteil 2004A), also insgesamt 80 Zeichen, was für die meisten Anwendungsfälle ausreichend sein sollte.

Ich beabsichtige ein schön großes 20 x 4 Display zur Anzeige von wichtigen Informationen, die ich über die WLAN-Verbindung aus dem Internet zusammensuche zu benutzen. Dieses soll immer angeschaltet bleiben und im direkten Blickfeld auf meinem Schreibtisch einen Platz finden. Einen Namen habe ich auch shcon dafür: das Always-On-Display.

Vorsicht: die Bauteile gibt es für 5V und für 3.3V Versorgungsspannung, ohne dass das unbedingt auf dem Bauteil ersichtlich wäre. Ich habe ein 1602A mit 5V, der mit VDD 3.3V nichts tut und ein 2004A, dass prima mit VDD 3.3V läuft (und das ich bisher auch nicht an 5V angeschlossen habe, nicht dass es dadurch noch kaputt geht). Das 1602 läuft auch mit 3.3V an der Hintergrundbeleuchtung, ist dann zwar nicht mehr blendend hell, aber ausreichend. Der Vorteil: die 3.3V-Leitung sind beim Raspi schaltbar und damit können wir dann die Hintergrundbeleuchtung passen ein- und ausschalten, was Strom spart.


Ohne Hintergrundbeleuchtung ist das Display übrigens nicht gut ablesbar.

Rechts ein Foto, bei dem alle Pixel der oberen Zeile eingeschaltet sind und alle der unteren Zeile aus.

Dies ist übrigens der Einschaltzustand des LCDs, wenn noch keine Initialisierung durchgeführt wurde: die erste Zeile ist weiß - beim 2004A sind dies die 1. und 3. Zeile.





Sobald die Hintergrundbeleuchtung eingeschaltet wird, wird das Display sehr gut lesbar, auch im Dunkeln. Die Schrift erscheint weiß auf blau.

Es empfielt sich ein Potentiometer (Drehwiederstand, kurz Poti) zwischen Anschluss A und 3.3V (bzw. 5V) zu schalten, mit dem man die Helligkeit regeln kann. Man möchte das LCD ja auch noch bei ein wenig Sonneneinstrahlung lesen können, wenn es für draußen bestimmt ist bzw. von ihm nicht geblendet werden, wenn man es auch nachts verwendet.


Wenn man Glück hat, sind die Anschlüsspins jeweils beschriftet, es kann aber auch sein, dass man nur die Zahlen 1 bis 16 vorfindet. Dann hilft die folgende Tabelle, die außerdem über die Bedeutung der Anschlusspins Auskunft gibt.

PinBezeichnungPegelBeschreibung
1VSSMasseMasse GND
2VDD3.3V / +5VBetriebsspannung Logik (je nach Model 1 ... 7V)
3V00 ... 5VDisplayspannung max. 5V (VDD-10.0 ... VDD+0.3V)
4RSH/LRegister Select
5RWH/LH:Read / L:Write
6EH.H-LEnable Signal
7D0H/LDatenleitung 0 (LSB)
8D1H/LDatenleitung 1
9D2H/LDatenleitung 2
10D3H/LDatenleitung 3
11D4H/LDatenleitung 4
12D5H/LDatenleitung 5
13D6H/LDatenleitung 6
14D7H/LDatenleitung 7 (MSB)
15A-Hintergrundbeleuchtung +5V (+3.3V)
16K-Hintergrundbeleuchtung GND

Ich habe bei meinem 1602 den Header für die Pins an die Unterseite gelötet, so dass ich es direkt in das Breadboard stecken kann. Da das 2004 doch wesentlich größer ist, habe ich doch den Header oben drauf gelötet, um das Display dann per Flachbandkabel zu verbinden - hier eignen sich die 2-reihigen 34-poligen alten Floppykabel ganz gut. Damit kann ich das Display flexibel aufstellen.

Mein 1602 kam übrigens halb defekt an, rechts im LCD war eine Spalte in einem Zeichen der Dot-Matrix immer aus. Wenn ich aber unten rechts auf den Display-Rahmen drückte, sprang die Spalte wieder an, solange ich drückte, danach war sie wieder aus. Nachdem sich nach reichlich Rumgedrücke kein stabiler Zustand einstellen ließ, beschloss ich der Sache auf den Grund zu gehen.

Wenn man die Klammern an der Unterseite wieder gerade biegt, dann kann man den Metallrahmen samt blauer Scheibe und Kontaktgummis abnehmen. Auf der Platine sollte die Hintergrundbeleuchtung mit Reflektionsfolie kleben bleiben.

Der Fehler war schnell ausgemacht: der linke obere Pin, der eigentlich in das Führungsloch in der Platine sollte, war umgeknickt und verbog das Kontaktgummi an diese Stelle und verhinderte einen zuverlässigen Kontakt. Den Pin gerade gebogen, das Gummi zurechtgerückt und das LCD wieder zusammengebaut, und dann funktionierte es tadellos. Schade, dass man da selbst reparieren muss, gibt es denn keine Qualitätskontrollen mehr?

Schaltung

LCD Raspi VSS Masse VDD 3.3V oder 5V, je nach Modell V0 über 5 kΩ-Poti an GND, für Kontrast RS an BCM 26, bestimmt ob Kommando oder Zeichen RW GND (wir wollen nur schreiben, also immer Low) E an BCM 19 (Enable-Leitung, Pulse, wenn Sendung fertig) D4 an BCM 13 (Datenleitung 1) D5 an BCM 6 (Datenleitung 2) D6 an BCM 5 (Datenleitung 3) D7 an BCM 11 (Datenleitung 4) A an BCM 9 über 470 Ω-Poti (Backlight an/aus, Helligkeit) K GND Experimente mit Widerständen haben ergeben, dass 1 kΩ zu wenig und 10 kΩ zu viel sind, um den Kontrast optimal einzustellen. Ein Poti mit 5 kΩ hat sich dann als optimal zum Einstellen des Kontrastes herausgestellt.

Das Poti an A kann man auch höher dimensionieren, wenn man es noch dunkler wünscht. Die Potis funktionieren für das 1602 und das 2004 optimal, so dass jeweils nur das Display ausgetauscht werden muss (entweder 1602 einstecken oder Flachbandkabel zum 2004).

A kann man auch auf Dauer-High setzen (+3.3V oder auch +5V für blendend hell), wenn man keine schaltbare Hintergrundbeleuchtung möchte, dann spart man sich eine Leitung.

Dem aufmerksamen Leser wird nicht entgangen sein, dass wir D0 bis D3 nicht angeschlossen haben. Das liegt daran, dass wir den 4-Bit-Modus zur Übertragung von Daten gewählt haben. Datenübertragungsmodi gibt es nämlich zwei: 8 Bit parallel, welcher alle 8 Leitungen benötigt, oder eben der 4-Bit-Modus, bei dem die 8 Bit in zwei Portionen zu 4 bit sendet und damit gleich mal 4 Kabel einspart. Da Leitungen immer rar sind, nehmen wir natürlich den 4 Bit-Mode. Das werden wir dem Display später in der Software beim Initialisieren mitteilen.

Das größere 2004A-Display ist übrigens ganz genau so aufgebaut und beschaltet. Es sind hier auch mehr Controller Chips (unter den 5 schwarzen Kleksen). Es hat nur mehr Zeilen, die ein bisschen seltsam angesprochen werden, nämlich wie ein 40x2 Display, dass in der Mitter zerteilt wurde. So gehen die ersten 40 Zeichen über Zeile 1 und 3 und die nächsten über Zeile 2 und 4. Aber das ist kein Manko und wird über die Software geregelt.

Bevor wir zum Source-Code kommen, hier einmal das 1602A-Display:



... und das 2004A-Display in Aktion, wie sie ein paar Aufgaben erledigen:




Der Code ist wie immer mit vielen Kommentaren versehen. Im Anschluss werde ich dann noch auf Besonderheiten oder neue Befehle eingeben.


lcd-1602a-2004a.py (klicken, um diesen Abschnitt aufzuklappen)
# -*- encoding: utf-8 -*- # (C) 2018 by Oliver Kuhlemann # Bei Verwendung freue ich mich über Namensnennung, # Quellenangabe und Verlinkung # Quelle: http://cool-web.de/raspberry/ import RPi.GPIO as GPIO # Funktionen für die GPIO-Ansteuerung laden from time import sleep # damit müssen wir nur noch sleep() statt time.sleep schreiben from sys import exit # um das Programm ggf. vorzeitg zu beenden GPIO.setmode(GPIO.BCM) # die GPIO-Pins im BCM-Modus ansprechen pinRS=26 pinE=19 pinData=[13,6,5,11] # wir benutzen den 4-Pin-Data-Mode für das LCD pinBL=9 pulseDur = 0.0005 delayDur = 0.0005 # Defnitionen für ein 1602-Display #lcdWidth = 16 #lcdHeight= 2 #addrLines=[0x00,0x40] # Defnitionen für ein 2004-Display lcdWidth = 20 lcdHeight= 4 addrLines=[0x00,0x40,0x14,0x54] # Dictionary der Kommandos cmds={"clear":0x01, # löscht alle Zeichen "home":0x02, # springt an den Anfang (line 0, col 0) "off":0b1000, # schaltet LCD aus (merkt sich aber den Text) "on":0b1100, # schaltet LCD wieder ein mit Text (praktisch zum blinken) "curon":0b1111, # cursor an "curoff":0b1100 # cursor wieder aus } # Folgende ASCII-Zeichen werden auf das richtig LCD-Zeichen übersetzt zeichen="°äöüÄÖÜßµ€" ersatz=[223,225,239,245,225,239,245,226,228,227] # GPIO-Ports initialisieren GPIO.setup(pinRS, GPIO.OUT) GPIO.setup(pinE, GPIO.OUT) GPIO.setup(pinBL, GPIO.OUT) for pin in pinData: GPIO.setup(pin, GPIO.OUT) def backlight(onOff): # 1 schaltet die Hintergrundbeleuchtung an GPIO.output(pinBL, onOff) def pulseEnable(): # Einen Pulse auf die Enable-Leitung schicken sleep(delayDur) GPIO.output(pinE, 1) sleep(pulseDur) GPIO.output(pinE, 0) sleep(delayDur) def lcdByte(byte): # ein Byte im 4-Bit-Mode senden # höherwertiges Halbbyte senden for bnr in range (4,8): bit=(byte & 2**bnr) / 2**bnr GPIO.output(pinData[bnr-4], bit) pulseEnable() # niederwertiges Halbbyte senden for bnr in range (0,4): bit=(byte & 2**bnr) / 2**bnr GPIO.output(pinData[bnr], bit) pulseEnable() def lcdCmd(cmd): # ein Kommando ans LCD senden try: byte=cmds[cmd] except KeyError: print "Das Kommando " + char + " ist nicht definiert." return lcdCmdByte(byte) def lcdCmdByte(byte): # Kommando als Byte-Wert senden GPIO.output(pinRS, 0) # 0=command, 1=character lcdByte(byte) def lcdMsg(line, col, msg): # line ist die x. Zeile (gezählt ab 0) # col die Spalte (ab 0) lcdCmdByte(addrLines[line]+0x80+col) # Speicheradresse für Line/Col adressieren GPIO.output(pinRS, 1) # 0=command, 1=character za=col+1 for char in msg: byte=(ord(char)) # ord gibt ASCII-Code eines Zeichens zurück p=zeichen.find(char) # einige zeichen sind nicht ASCII-konform, diese übersetzen if p >-1: byte=ersatz[p] lcdByte(byte) if za % lcdWidth == 0: line+=1 # Zeile zu lang, Zeilenumbruch und in nächster Zeile weiter try: lcdCmdByte(addrLines[line]+0x80) GPIO.output(pinRS, 1) # und wieder zurück in den Zeichenmodus except IndexError: # Display voll break za+=1 def initLCD(): # LCD initialisieren lcdCmdByte(0x33) # 110011 Initialize lcdCmdByte(0x32) # 110010 Initialize lcdCmdByte (0b101000) # Function Set Command # ^----- DL: 4-bit-mode (optional 8-bit-Mode) # ^---- N: 2-line-mode (optional 1-line-mode) # ^--- F: 5x8 font (optional 5x11 font) lcdCmdByte (0b1100) # Display on/off Command # ^----- D: Display on # ^---- C: Cursor off # ^--- B: Cursor Pos. off lcdCmdByte (0b110) # Cursor or Display Shift Command # ^----- S/C: 1=Shift, 0=Cursor # ^---- R/L: 1=Right, 0=Left # lcdCmd("clear") def showZs(): # zeigt die eingebauten Zeichen an lcdCmd("clear") beg=32 c=beg line=0 lcdCmdByte(addrLines[0]+0x80) while c <256: GPIO.output(pinRS, 1) lcdByte (c) c+=1 if (c-beg) % (lcdHeight*lcdWidth) == 0: sleep(3) lcdCmd("clear") line=0 elif (c-beg) % lcdWidth == 0: line+=1 lcdCmdByte(addrLines[line]+0x80) GPIO.output(pinRS, 1) def showZsCGRAM(): # zeigt die selbst definierten Zeichen an lcdMsg(0,0,"own CGRAM chars:") lcdCmdByte(addrLines[1]+0x80) GPIO.output(pinRS, 1) for c in range (0,8): lcdByte (c) for c in range (0,8): lcdByte(32) def saveCharToCGRAM(charNr, arrMuster): # speichert ein eigenes Zeichen lcdCmdByte(charNr*8+0x40) GPIO.output(pinRS, 1) for byte in arrMuster: lcdByte(byte) sleep (delayDur) def loadCustomChars(): # definiert 8 eigene Zeichen (ansprechbar über chr(0) bis chr(7) muster = [ 0b10000 , # Strich Breite 1 0b10000 , 0b10000 , 0b10000 , 0b10000 , 0b10000 , 0b10000 , 0b10000 ] saveCharToCGRAM (0, muster) muster = [ 0b11000 , # Balken Breite 2 0b11000 , 0b11000 , 0b11000 , 0b11000 , 0b11000 , 0b11000 , 0b11000 ] saveCharToCGRAM (1, muster) muster = [ 0b11100 , # Balken Breite 3 0b11100 , 0b11100 , 0b11100 , 0b11100 , 0b11100 , 0b11100 , 0b11100 ] saveCharToCGRAM (2, muster) muster = [ 0b11110 , # Balken Breite 4 0b11110 , 0b11110 , 0b11110 , 0b11110 , 0b11110 , 0b11110 , 0b11110 ] saveCharToCGRAM (3, muster) muster = [ 0b01110 , # Batterie-Symbol 0b11111 , 0b10001 , 0b10001 , 0b10001 , 0b10001 , 0b10001 , 0b11111 ] saveCharToCGRAM (4, muster) muster = [ 0b00000 , # CPU-Symbol 0b01110 , 0b11011 , 0b01010 , 0b11011 , 0b01010 , 0b11011 , 0b01110 ] saveCharToCGRAM (5, muster) muster = [ 0b00000 , # Smiley 0b01010 , 0b01010 , 0b00000 , 0b10001 , 0b10001 , 0b01110 , 0b00000 ] saveCharToCGRAM (6, muster) muster = [ 0b00001 , # Checkmark 0b00001 , 0b00010 , 0b00010 , 0b10100 , 0b10100 , 0b01000 , 0b00000 ] saveCharToCGRAM (7, muster) def loadCustomEuro(): # überschreibt Checkmark mit Euro-Zeichen, wenn geladen muster = [ 0b00011 , # Euro-Zeichen 0b00100 , 0b11111 , 0b01000 , 0b11111 , 0b00100, 0b00011 , 0b00000 ] saveCharToCGRAM (7, muster) def showProzentBalken(line,startCol,prozent): # zeigt einen fein abgestuften Prozent-Balken # benutzt selbstdefinierte Chars if prozent == 0: # es gibt nichts anzuzeigen return(0) breiteGes=lcdWidth*5-startCol*5 breite=int(breiteGes*prozent/100)+1 if breite > breiteGes: breite = breiteGes voll=int(breite/5) rest=breite % 5 # die vollen 5-Balken darstellen (char Nr. 255) lcdCmdByte(addrLines[line]+0x80+startCol) GPIO.output(pinRS, 1) for n in range (0,voll): lcdByte (255) # gefolgt vom Rest if rest > 0: lcdByte(rest-1) # --- Ende Funktionen --- Beginn Hauptprogramm ------------------------------------------- try: backlight(1) # Backlight anschalten initLCD() # wichtig. Ohne diesen Befehl macht das LCD nur Müll. showZs() # eingebauter Zeichensatz sleep (5) lcdCmd("clear") # Anzeige löschen loadCustomChars() # eigene Zeichen definieren und in CGRAM laden showZsCGRAM() # eigene Zeichen anzeigen sleep (5) lcdCmd("clear") lcdMsg (0,0,"Test Übersetzung") lcdMsg (1,0,"°äöüÄÖÜßµ€") sleep (5) lcdCmd("clear") # Animation Prozent-Balken (Bsp. Batt. laden) --- if lcdWidth==16: lcdMsg (0,0," 0% 50% 100%") elif lcdWidth==20: lcdMsg (0,0," 0% 50% 100%") lcdMsg (1,0,chr(4)) # Batterie-Symbol for p in range (0,100): showProzentBalken (1,1,p) sleep (.001) sleep (.5) for i in range (0,3): # ganzes Display blinken lassen lcdCmd("off") sleep (.5) lcdCmd("on") sleep (.5) sleep (3) lcdCmd("clear") # -------------------------------------------------- # automatischer Zeilenumbruch bei Überlänge testen lcdMsg (0,0,"http://cool-web.de/raspberry/") sleep(3) # ganzes Display blinken lassen -------------------- for i in range (0,5): lcdCmd("off") sleep (.5) lcdCmd("on") sleep (.5) sleep (3) # kleine Animation mit Cursor ------------------------ lcdCmd("clear") lcdCmd("home") lcdCmd("curon") sleep (2) col=0 for char in "Greetings Prof.": lcdMsg (0,col,char) sleep (.1) col+=1 col=0 for char in "Falken...": lcdMsg (1,col,char) sleep (.1) col+=1 sleep (2) lcdCmd("clear") col=0 for char in "Shall we play": lcdMsg (0,col,char) sleep (.1) col+=1 col=0 for char in "a game?": lcdMsg (1,col,char) sleep (.1) col+=1 sleep (3) lcdCmd("curoff") # ---------------------------------------------------- except KeyboardInterrupt: pass lcdCmd("clear") lcdCmd("off") GPIO.cleanup() # Programm sauber verlassen und Ressourcen wieder freigeben

Je nach eingesetzem Display-Typ muss die Definition für ein 16x2 oder ein 20x4-Display auskommentiert werden. Das Programm passt sich dann automatisch an die Größe an.

Kommandos werden an das LCD als Byte-Codes gesendet (näheres im Datenblatt zum Display). Da man sich die Byte-Codes nur sehr schlecht merken kann, definiert das Dictionary cmds verständliche Kommandos dafür, die später mit lcdCmd("cmd") ausgeführt werden können.

Da die ASCII-Codes im Display-Zeichensatz an anderen Stellen sind als unter Windows oder Linus definiert zeichen zusammen mit ersatz, welche Sonderzeichen zu welchem Zeichenwert übersetzt werden, damit das richtige Zeichen auf dem Display erscheint.

backlight(1) setzt die entsprechende Leitung auf High und schaltet so die Hintergrundbeleuchtung an.

Wenn ein Halbbyte an das LCD gesendet wurde, dann wird die Enabled-Leitung einmal kurz gepulst als Zeichen, dass etwas anliegt. Das erledigt die Funktion pulseEnable()

lcdByte(byte) übernimmt die eigentlich Kommunikation mit dem LCD. Es teilt das ankommende Byte in zwei Halbbytes auf, und schickt diese an das LCD (durch High/Low-Setzen der 4 Datenleitungen). Durch pulseEnable() werden die 4 Bits dann sozusagen "abgeschickt". Das LCD kann sie verarbeiten. Zuerst wird das höherwertige Halbbyte geschickt, dann das niederwertige: 7 6 5 4 3 2 1 0 1. 5. Leitung D4 2. 6. Leitung D5 3. 7. Leitung D6 4. 8. Leitung D7 Die RS-Leitung gibt an, ob es sich bei dem gesendeten Byte um ein Kommando (Low) oder ein Zeichen (High) handelt. lcdCmdByte(byte) setzt den Kommando-Modus.

Bei lcdMsg(line, col, msg) wird zuerst die richtige Adresse, addiert zum Kommando (0x80) im Kommando-Modus geschickt. Damit weiß das LCD dann, in welchen Bereich es ausgeben soll. Dann folgen die Zeichen im Zeichen-Modus (High auf der RS-Leitung). Außerdem sorgt lcdMsg(...) für die richtige Positionierung über line und col , für die Übersetzung von nicht ASCII-Zeichen und den Zeilenumbruch bei zu öangen Zeilen.

initLCD() schickt ein paar Initalisierungskommandos ans LCD, damit es weiß, dass wir im 4-Bit-Modus arbeiten und wie groß das Display ist etc.

Das LCD hat 256-32-32 = 192 eingebaute Zeichen, die im Bereich von 32 bis 255 liegen. Außerdem kann man 8 Zeichen selbst definieren und diese mit den Zeichen von 0 bis 7 ansprechen. showZs() und showZsCGRAM() zeigen diese Zeichensätze an.

Ich habe ein paar Zeichen selbst definiert, und zwar vertikale Striche mit einer Breite von 1, 2, 3 und 4. Einen kompletten Block finden wird schon im Zeichen Nr. 255. Damit kann man wunderbar sanft fortschreitende Balken anzeigen. Außerdem habe ich noch ein Batterie und CPU-Symbol, ein Smiley, ein Checkmark und ein Euro-Zeichen entworfen. Wenn man das 0b an Anfang der Bitmuster weglässt und die Nullen durch Leerzeichen und die Einsen durch # ersetzt, kann man sich eine Vorstellung machen, wie das Zeichen aussieht. Umgekehrt kann man so auch eigene Zeichen entwerfen. saveCharToCGRAM...() sorgt dafür, dass das Zeichen im CGRAM abgelegt wird.

Custom Characters können beliebig überschrieben werden, aber es können nur gleichzeitig 8 im RAM sein, sprich: pro LCD-Bildschirm können max. 8 unterschiedliche selbstdefinierte Zeichen angezeigt werden.

showProzentBalken(line,startCol,prozent ist eine fertige Funktion für einen Prozent-Balken mit custom chars. Es muss nur die Zeile und die Start-Spalte sowie die Prozentzahl von 0 bis 100 angegeben werden und es wird ein entsprechender Balken bis zum rechten Bildschirmrand angezeigt.

Im Hauptprogramm werden die Funktionen zur Demonstration der Reihe nach aufgerufen, auch wird hier die Verwendung von einigen Kommandos gezeigt. Am besten Kommandos mit dem Ergebnis im Video vergleichen, dann wird alles schnell klar.