DOGM163-LCD an Arduino Nano (5V) im 4Bit-Modus betreiben

Das LCD EA DOGM163 hatte ich bereits in diesem Artikel mit einem STM32 und 3.3V benutzt.

Nun will ich ihn auch in meinem 8Bit-Breadboard-Computer-Projekt verwenden. Da dort aber auch die Hardware zur Ansteuerung selbstgedengelt sein wird, will ich das LCD erst einmal mit den im 6502-Projekt verwendeten 5V testen, das geht wohl wohl am besten mit einem Arduino Nano.

Wie das DOGM163-LCD zusammengelötet wird und die sonstigen Grundlagen kann man im alten Artikel nachlesen. Dieser Artikel setzt auf das dort vermittelte Wissen auf.

Wenn das DOGM-Display mit 5V betrieben wird, können ein paar Kondensatoren wegfallen, die die 3.3V auf 5V erhöht haben. Die Schaltung wird also einfacher.

Dafür wird die Programmierung ein wenig komplizierter, weil wir den 4-Bit-Modus benutzen und dort jedes Byte in zwei Schüben übertragen werden musss. Der Vorteil allerdings ist, dass so nur 4 Leitungen für die Datenbytes benötigt werden und ich noch Daten-Leitungen frei habe, die ich beim 6502er-Projekt als Steuerleitungen für RS, R/W und E benutzen kann. So kann ich das LCD leichter an den 8-Bit breiten Datenbus des 6502 anschließen.

Das DOGM163-LCD

Das Schöne an dem DOGM163 sind seine kleinen Abmessungen. Diese betragen nur 55 x 31 mm = 1705 mm2. Ein 1602A Modul hat hingegen 80 x 36 mm = 2880 mm2 und belegt damit fast 70% mehr Grundfläche. Besonders in der Breite kann man beim Breadboard-Aufbau nicht genug sparen. Und eigentlich möchte ich alle Komponenten für ein Breadboard-Computer-IO-Gerät, soweit denn möglich, auf einem einzigen Breadboard unterbringen.

Weiterer Vorteil: Das DOGM163 kann 3 Zeilen à 16 Zeichen, das 1602A nur 2 Zeilen. Das sind 16 Zeichen mehr. Trotzdem ist das DOGM163 noch gut ablesbar.

Trotzdem ist das DOGM163 so hoch, dass es gerade auf ein Breadboard passt. Das läßt keine Pins mehr ober- oder unterhalb frei, um dort Jumperkabel anzuschließen.

Das ich aber die Beinchen des LCD-Moduls (die man hoffentlich sparsam und sauber verlötet hat) in Präzisionsbuchsenleisten (einreihig, die mit den runden Löchern und Beinchen) gesteckt habe, auch um diese besser zu handeln und die empfindliche Beinchen nicht zu verbiegen, ist damit unter dem LCD un ein wenig Platz, um die Jumperkabel hier durchzuführen.

Pinout und Anschließen

Das Datenblatt des DOGM163 ist nicht so sonderlich übersichtlich, aber folgende Infos konnte ich finden:




Zu beachten beim Betrieb bei 5V und Nutzung des 4-Bit-Modus:

Schaltplan

Folgenden Schaltplan habe ich für die Konfiguration 4Bit/5V gefunden.



Die LEDs sind im Schaltplan nicht beschaltet, es gilt das oben Gesagte. Auch sollte man die Warnung im Datenblatt beachten:
Betreiben Sie die Beleuchtung nie direkt an 5V/3,3V; das kann zur sofortigen Zerstörung der LEDs führen

Aufbau auf einem Breadboard



Da das LCD-Modul leider zu groß ist, um noch Platz ober- oder unterhalb auf einem Breadboard zum Einstecken von Jumper-Kabeln, verlege ich die Kabel zuerst und lasse die oberste und unterste Pinreihe frei, damit ich dort später das LCD auf Präzisionssockelreihe einstecken kann.

Um es mir ein bisschen einfacher zu machen, zeichne ich mir mit wasserfestem Stift ein, wo später das Modul stecken wird: Pin 21 ganz rechts oben und Pin 40 ganz links oben.

Laut Schaltplan kommen Pin 23 bis 26 sowie Pin 32 bis 35 an +5V (rot). Diese werden miteinander verbunden und dann unter dem später eingesteckten LCd nach rechts getunnelt.

Dazwischen liegen Pin 27 an Ground (schwarz) und die Datenpins D4 bis D7 (orange), die an die Arduino Pins D4 bis D7 gehen.

Wären noch die Steuerleitungen (blau): Pin 36 an D3 für Enable, Pin 37 an D2 für Read/Write und Pin 39 an D10 für RS (Cmd/Data). Pin 40 für Reset kann man generell auf High, also +5V ziehen oder auch an einen Arduino Pin (hier D11).

Pin 38 (CSB) nicht übersehen und vergessen - so wie ich zuerst - der gehört auch auf Ground.



Die richtige Initialisierungs-Sequenz finden

Es ist nämlich schwierig genug, die richtige Initialisierungs-Sequenz herauszufinden. Die im Datenblatt angebenenen funktionieren nämlich nicht.

Weder die Sequenz beim 4-bit Interface (fosc=380kHz) angegebenen Ablaufdiagramm lcdWriteCmd(0b00110011); lcdWriteCmd(0b00110010); lcdWriteCmd(0b00100001); lcdWriteCmd(0b01110101); lcdWriteCmd(0b01100000); noch die nachfolgend im Beispielcode Initial Program Code Example For 8051 MPU(4 Bit Interface) wieder gegebene Sequenz lcdWriteCmd(0x30); lcdWriteCmd(0x30); lcdWriteCmd(0x30); lcdWriteCmd(0x20); lcdWriteCmd(0x29); lcdWriteCmd(0x14); lcdWriteCmd(0x78); lcdWriteCmd(0x5e); lcdWriteCmd(0x6a); lcdWriteCmd(0x0c); lcdWriteCmd(0x01); lcdWriteCmd(0x06); sind richtig.

Nur durch viel Rumprobieren habe ich eine Initialisierungs-Sequenz gefunden, mit der überhaupt etwas auf dem Bildschirm angezeigt wird.

Durch die anfänglich fehlende Verbindung von Pin 38 auf GND nahm das Display allerdings nach ca. einer halben Sekunde nichts mehr an. Das reichte für die Ausgabe eines Bildschirm wie z. B.



Aber danach war das Display dann eingefroren.

Wie sich das Display noch mit dem fehlenden Pin 38 auf GND verhält, wie ich die richtige Ini-Sequenz suche und weitere wichtige Infos bezüglich des Displays und seines Controllers ST7036 erfahrt ihr in diesem Video:



Mit an Ground verbundenen Pin 38 sah es dann schon gleich viel besser aus. Allerdings war die Übertragung mit den im Datenblatt angegebenen Werte für die Pausen zwischen den Befehlen nicht stabil, so dass auch hier noch herumgeschraubt bzw. probiert werden musste:



Schließlich aber war die richtige Initialisierungs-Sequenz und das richtige Timing gefunden.

Nun konnte ich den Code noch um ein paar Funktionen wie Cursor-Positionierung, Blinken und eigene Sonderzeichen (im Video am Beispiel eines Eszett) erweitern.

Zum einen habe ich schon so eine kleine Bibliothek, falls ich den DOGM163 am Arduino betreiben will und zum zweiten den Code schoneinmal in C geschrieben, so dass das Übertragen in Assembler später einfacher ist. Denn das Debuggen auf meinem Breadboard-Computer in Maschinensprache ist kein Kinderspiel und auch kein Vergnügen.



Sourcecode

Für alle, die wie ich, nach der richtigen Initalisierungs-Sequenz wie ich gesucht haben, hier mein Source-Code.

Aus den Foren weiß ich, dass viele an dem 4-Bit-Mode des DOGM163 verzweifelt sind, weil einfach das Datenbklatt hier falsche Angaben macht.

///////////////////////////////////////////////////////// // (C) 2020 by Oliver Kuhlemann // // Bei Verwendung freue ich mich ueber Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // ///////////////////////////////////////////////////////// #include #define PinD7 7 #define PinD6 6 #define PinD5 5 #define PinD4 4 #define PinE 3 // Enable #define PinRW 2 // Read/Write #define PinRS 10 // Command/Data #define OpCmdTable0 0b00101000 // 4Bit #define OpCmdTable1 0b00101001 // 4Bit #define OpCmdTable2 0b00101010 // 4Bit #define OpCmdTableTest 0b00101011 // 4Bit #define OpClear 0b00000001 #define OpHome 0b00000010 #define OpDispOn 0b00001100 #define OpDispOff 0b00001000 #define OpCursorOn 0b00001111 #define OpCursorOff 0b00001100 #define lcdPulseDelayLong 2000 // erster Befehl #define lcdPulseDelayShort 50 // 27 lt. Datenblatt sind zu wenig bool FirstCommand = true; void pulseEnable() { digitalWrite(PinE, HIGH); delayMicroseconds (FirstCommand ? lcdPulseDelayLong : lcdPulseDelayShort); digitalWrite(PinE, LOW); delayMicroseconds (FirstCommand ? lcdPulseDelayLong : lcdPulseDelayShort); } void transferNibbleToPins(byte nibble) { digitalWrite(PinD7, (nibble & 0b1000) > 0 ? HIGH : LOW); digitalWrite(PinD6, (nibble & 0b0100) > 0 ? HIGH : LOW); digitalWrite(PinD5, (nibble & 0b0010) > 0 ? HIGH : LOW); digitalWrite(PinD4, (nibble & 0b0001) > 0 ? HIGH : LOW); /* Serial.print("send nibble:"); Serial.print((nibble & 0b1000) > 0 ? "1" : "0"); Serial.print((nibble & 0b0100) > 0 ? "1" : "0"); Serial.print((nibble & 0b0010) > 0 ? "1" : "0"); Serial.print((nibble & 0b0001) > 0 ? "1" : "0"); Serial.println(); */ } void lcdByte(char data) // sendet ein Byte zum LCD { // höherwertiges Halbbyte senden byte nibble = (data & 0xF0) >> 4; transferNibbleToPins(nibble); pulseEnable(); // niederwertiges Halbbyte senden nibble = data & 0x0F; transferNibbleToPins(nibble); pulseEnable(); } void lcdWriteCmd(char data){ // Kommando als Byte-Wert senden digitalWrite(PinRW, LOW); // Schreib-Modus digitalWrite(PinRS, LOW); // 0=command, 1=character lcdByte (data); } void lcdWriteChar(char data) { // Zeichen als Byte-Wert senden digitalWrite(PinRW, LOW); // Schreib-Modus digitalWrite(PinRS, HIGH); // 0=command, 1=character lcdByte (data); } void lcdInit() { // LCD initialisieren // lcd ein bisschen Zeit zum Hochfahren geben delay (50); // function set muss im extended mode aufgerufen werden, sonst werden die unteren 3 bits ignoriert // Function Set 0 0 1 DL N DH IS2 IS1 // DL: interface data is 8/4 bits N: number of line is 2/1 // DH: double height font IS[2:1]: instruction table select // Instruction table 0(IS[2:1]=[0,0]): Cursor or Disp Shift & Set CGRAM // Instruction table 1(IS[2:1]=[0,1]): Bias, ICON, PowerCtrl, FollCtrl, Contrast // Instruction table 2(IS[2:1]=[1,0]): Double Height & Reserved // Instruction table 3(IS[2:1]=[1,1]): DO NOT USE FirstCommand=true; lcdWriteCmd(0b00110000); // 8-Bit-Mode einschalten; nötig, um nach deinem Reset die Spur wieder zu finden lcdWriteCmd(0b00110011); // nötig, um nach deinem Reset die Spur wieder zu finden lcdWriteCmd(0b00110010); // nötig, um nach deinem Reset die Spur wieder zu finden lcdWriteCmd(0b00101001); // 4-Bit-Mode einschalten; extended instruction table 1 setzen // ^^^---------- function set // ^--------- DL = 0 = 4-Bit-Mode // ^-------- N = 1 = one-line display mode (1=two-line disp mde), auch für 3-line-displays // ^------- DH = 0 = single height (1=double height) // ^^----- IS = 01 = extended instructions table No. IS // 3 line mode: DH=0, N=1, UD=egal lcdWriteCmd(0b00011101); // ^^^^--------- bias set // ^-------- BS = 1 = bias 1/4 (0=bias 1/5) // ^^------ fest // ^----- FX = 1 = three line application lcdWriteCmd(0b01010011); // ^^^^--------- Power/ICON control/Contrast set // ^-------- I_ON = 0 = ICON display off (1=on) // ^------- B_ON = 0 = booster circuit off (1=on) // ^^----- Contrast = 11 (C5,C4,C3,C2,C1,C0 can only be set when internal follower is used (OPF1=0,OPF2=0)) lcdWriteCmd(0b01101111); // ^^^^--------- Follower control // ^-------- F_ON = 1 = follower circuit on/off (Fon must be set to “Low” if (OPF1, OPF2) is not (0,0)) ???? // ^^^----- Rab2,Rab1,Rab0 = ???. V0 generator amplified ratio. Rab2,Rab1,Rab0 can only be set when internal follower is used (OPF1=0,OPF2=0). They can adjust the amplified ratio of V0 generator lcdWriteCmd(0b01111111); // ^^^^--------- Contrast set(low byte) // ^^^^----- I_ON = 0 = ICON display off (1=on) // C5,C4,C3,C2,C1,C0 can only be set when internal follower is used (OPF1=0,OPF2=0) lcdWriteCmd(0b00101000); // es folgen nur noch Standard-Befehle: normal instruction table 0 setzen, damit Konfiguration nicht überschrieben werden kann druch ÜBertragungsfehler lcdWriteCmd(0b00000110); // ^^^^^^------- Entry Mode Set // ^------ I/D = 1 = Increment / decrement = inc= left to right (1), dec= right to left (0) // ^----- S = Shift, if 1 = Scrolling in direction I/D lcdWriteCmd(OpClear); // clear display lcdWriteCmd(OpDispOn); // display on FirstCommand=false; } void lcdHome() { lcdWriteCmd(OpHome); delayMicroseconds(lcdPulseDelayLong); } void lcdClear() { lcdWriteCmd(OpClear); lcdHome(); } void lcdDispOn() { lcdWriteCmd(OpDispOn); } void lcdDispOff() { lcdWriteCmd(OpDispOff); } void lcdCursorOn() { lcdWriteCmd(OpCursorOn); } void lcdCursorOff() { lcdWriteCmd(OpCursorOff); } void lcdPos(byte x, byte y) { // x= 0...15, y= 0..2 Cursor an Pos. setzen byte cmd=0x80; cmd += y*16 + x; lcdWriteCmd(cmd); } void lcdPrint(char* lines) { char bst; for (byte i=0; i < 48; i++) { bst = lines[i]; if (bst == 0) return; lcdWriteChar (bst); } } void lcdPrint(byte x, byte y, char* lines) { lcdPos(x, y); lcdPrint(lines); } void lcdDefineChar (byte charNum, byte *def) { if (charNum > 7) return; // nur 8 Chars möglich lcdWriteCmd(0x40+8*charNum); // Adresse für custom char // 8 Datenbytes für die Definition übertragen for (byte i=0; i<8; i++) { lcdWriteChar(def[i]); } lcdPos(0,0); // Standard Position DDRAM Adresse } // Selbstdefiniertes Zeichen: ß wie in Straße byte eszett[] = { 0b11110, // #### 0b10001, // # # 0b11110, // #### 0b10001, // # # 0b10001, // # # 0b11110, // #### 0b10000, // # 0b00000 // } ; void setup() { //Serial.begin(115200); pinMode(PinD7, OUTPUT); pinMode(PinD6, OUTPUT); pinMode(PinD5, OUTPUT); pinMode(PinD4, OUTPUT); pinMode(PinE, OUTPUT); pinMode(PinRW, OUTPUT); pinMode(PinRS, OUTPUT); lcdInit(); // Cursor testen lcdCursorOn(); lcdPrint ("Hallo Cursor"); delay (3000); lcdCursorOff(); // Test Positionierung lcdClear(); lcdPos(0,0); lcdPrint("links"); lcdPos(5,1); lcdPrint("mitte"); lcdPos(10,2); lcdPrint("rechts"); lcdPrint(14,0,"or"); lcdPrint(0,2,"ul"); delay (5000); //Alphabet ausgeben und dann blinken lassen lcdClear(); for (int i = 0; i < 48; i++) { lcdWriteChar(65 + i); //Alphabet } delay (2000); for (byte i=0; i < 4; i++) { lcdDispOff(); delay (500); lcdDispOn(); delay (500); } delay(2000); // Test eigener Character und Umlaute lcdClear(); lcdDefineChar (0, eszett); // ß jetzt als chr(0) lcdPrint ("Max Mustermann "); lcdPrint ("Hauptstra"); lcdWriteChar(0); // ß lcdPrint ( "e 42 "); lcdPrint ("90402 N"); lcdWriteChar(0b10000001); // ü, siehe Datenblatt integrierter Zeichensatz lcdPrint ("rnberg "); delay(10000); lcdHome(); lcdPrint ("DOGM163 / ST7036"); lcdPrint ("4-Bit-Mode at 5V"); lcdPrint ("www.cool-web.de "); delay(10000); } void loop() { char buf[10]; lcdClear(); for (int i=0; i<10000; i ++) { lcdHome(); snprintf(buf, 10, "Test %d", i); lcdPrint(buf); delay (100); digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); } } Den Code habe ich in den 3 Videos oben ausführlich erklärt, hier aber trotzdem noch einmal die wichtigsten Funktionen.

lcdByte ist für die Übertragung eines Bytes mit 8 Bit an den LCD-Controller zuständig. Dabei wird es in zwei Hälfte geteilt, das High- und das Low-Nibble. Diese werden nacheinander - zuerst high, dann low - durch die Funktion transferNibbleToPins übertragen. Das macht den 4-Bit-Mode aus.

Immer wenn ein Datenpaket mit einem Nibble fertig in Highs und Lows eingestellt ist, kann es der LCD-Controller abholen. Das wird mit einem kurzen Setzen der Enable-Leitung auf High getan. Dies übernimmt die Funktion pulseEnable.

Es gibt zwei Arten, wie der ST7036-Controller ein Datenbyte interpretieren soll: entweder ist es ein Befehl oder es ist ein Zeichen, dass er darstellen soll. Um was es sich handelt, wird durch ein High oder Low der RS-Leitung signalisiert. Die Funnktion lcdWriteCmd setzt RS entsprechend für einen Befehl und überträgt diesen dann und lcdWriteChar tut das Gleiche für ein Zeichen.

Zu Anfang müssen die richtigen Initialisierungs-Befehle geschickt werden, um dem LCD-Controller mitzuteilen, in welchem Modus er laufen soll. 4- oder 8-Bit-Modus? Oder Übertragung via SPI? Hat das Display 1, 2 oder gar 3 Zeilen? Wie sollen die angesprochen werden? Sind 3.3V oder 5V angeschlossen? Muss der interne Voltage-Booster verwendet werden? Und wenn ja, mit welcher Verstärkung? Wie soll der Kontrast eingestellt werden (dies geschicht beim DOGM163 über einen Befehl, nicht über ein Potentiometer)? Und so weiter und so fort.

Wird nicht das richtige Übertragungsprotkoll gewählt, schlägt die Übertragung der Befehle fehl und das Display bleibt schwarz. Ist nicht die richtige Voltage eingestellt, oder der Follower oder der Booster, bleibt das Display schwarz. Ist der Kontrast nicht richtig eingestellt, bleibt das Display schwarz oder mit etwas Glück kann man eine Schrift beim Schiefdraufschauen erahnen.

Der Herausfinden der richtigen Befehle zur Initalisierung war mehr Raterei als das Befolgen des Datenblattes - denn hier stehen falsche Angaben in den beiden Beispielen für den 4-Bit-Modus. Aber nun ist die richtige Befehlsfolge gefunden und wird mit lcdInit an den Controller geschickt. Danach braucht es nur noch Low-Level-Befehle zur Übertragung von Zeichen oder zum Löschen des Display und dergleichen. Diese erfordern auch spezielle Commands, die aber mit den Funktionen lcdHome, lcdClear, lcdDispOn, lcdDispOff, lcdCursorOn, lcdCursorOff, lcdPos bereits definiert sind. Die Funktionsnamen sollten selbsterklärend sein.

Mit der Funktion lcdPrint können dann Zeichen auf das Display ausgegeben werden. Dabei kann man einfach 48 Zeichen hintereinander schreiben. Für Stellen, die an einem Zeilenende leer bleiben sollen, überträgt man einfach Leerzeichen. Möchte man nicht immer das ganze Display neu schreiben, sondern nur einen Teil davon aktualisieren, benutzt man vorher lcdPos, um hier die X-Position (0 bis 15) und Y-Position (0 bis 2) anzugeben, an der weitergeschrieben werden soll. ODer man benutzt gleich die überlagerte Methode lcdPrint(byte x, byte y, char* lines), um das mit einem Befehl in einem Rutsch zu machen.

Das Display bietet (übrigens wie das 1602A auch) 8 selbstdefinierbare Zeichen. Diese müssen als Array von 8 Byte-Werte definiert und dann an die Funktion lcdDefineChar übergeben werden mitsamt der Nummer (0 bis 7) der Position im CGRAM. Danach kann es dann mit z. B. lcdWriteChar(0); für das erste Zeichen auf dem Display ausgegeben werden. Das Beispiel im Source-Code sollte das klar machen.

Damit sollten alle wichtigen Funktionen, die das DOGM163 bietet, abgedeckt sein.

Fazit

Die Qualität des Datenblattes für das DOGM163 und auch den Contoller ST7036 finde ich einfach nur schlecht. Die Informationen sind über viele Stellen verteilt und ich musste ständig hin- und herblättern (bzw. scrollen), als ich Informationen suchte, wie das LCD anzusprechen ist. Auch dass es zwischen Hoch- und Querformat wechselt und die Vertauschen von Zeilen und Spalten bei der Zeichentabelle gegenüber allgemeinen Gepflogenheiten irritiert.

Das schlimmste allerdings ist, dass die beiden Beispiele für die Ini-Sequenz für den 4-Bit-Mode einfach falsch sind und nicht funktionieren. Wenn man sich schon nicht auf das Datenblatt verlassen kann, was im übertragenen Sinne das Gesetzbuch ist, dann wird es echt haarig.

Zum Glück habe ich dann doch noch die richtige Kombination der Initialisierungs-Befehle gefunden und das LCD läuft jetzt auch im 4-Bit-Modus stabil. Wobei ich die Stromschwankungen an meine USB-Hub (siehe Videos) schon komisch finde. Das kann aber auch das USB-Hub-Netzteil sein, das angeblich 5V bei 2A kann und dicke ausreichen müsste. Evtl. liegt es aber auch am Arduino Nano. Leider habe ich gerade keinen zweiten da, um das auszuschließen.

Ich will aber trotzdem versuchen, den DOGM163 in meinem 6502-Breadboard-Computer zu verwenden, und hoffe darauf, dass die Strom- bzw. Helligkeitsschwankungen dort nicht mehr auftreten.