Viele Mikrocontroller-Systeme verwenden kleine LC-Displays für die Anzeige von Systemzuständen, Daten, Ereignissen, und vieles anderes. Monochrome Text-Displays basierend auf dem HD44780-Controller haben sich als weit verbreiteter Standard etabliert. Sie sind preisgünstig, in verschiedenen Größen und Farben zu bekommen, sind zurückhaltend im Stromhunger und lassen sich einfach an die meisten Mikrocontroller-Systeme anschließen. Oft kommt aber der Wunsch nach mehr Funktionalität. Für viele Anwendungen sind Graphiken wie z.B. Balkendiagramme, Pegelanzeigen, Kurvenverläufe usw. viel besser geeignet als Text. Solche Diagramme helfen dem Auge, die Daten intuitive zu erfassen.
Der Einsatz von graphischen Displays stellt aber insbesondere kleine Systeme mit beschränkten Ressourcen vor erhebliche Probleme. Hier beschreibe ich ein universelles Interface, das die Anwendung der weit verbreiteten KS0108-Displays sehr einfach macht und das Host-System soweit wie möglich entlastet. Das Interface bietet eine Reihe von Graphik-Funktionen an, die über das universelle I2C-Interface aufgerufen werden. So lassen sich KS0108-Displays durch beliebige Systeme vom 8-Bit Mikrocontroller bis zum Raspberry Pi ohne zusätzliche Treiber ansteuern.
Ausgangspunkt
Auf der Suche nach einem Display, das im einfachen Handling den HD44780-Typen entspricht, jedoch auch graphische Qualitäten hat, bin ich auf die (schon etwas ältere) Familie der KS0108-Controller gestoßen. Schon seit geraumer Zeit gibt es eine Vielzahl von solchen graphischen LCDs in verschiedenen Größen und Farben, meistens mit Auflösungen von 64 * 128 oder 64 * 192 Pixeln. Auch wenn diese eher geringe Auflösung nicht unbedingt auf der Höhe der Zeit ist, so erlauben diese Displays doch genau die Art der Diagramme, die ich bei verschiedenen Projekten einsetzen wollte. Außerdem sind diese Displays sind sehr preisgünstig zu bekommen und sparsam im Stromverbrauch.
Die nähere Beschäftigung mit den Graphik-LCDs zeigte jedoch, dass die Ansteuerung deutlich anspruchsvoller ist als für die bewährten Text LCDs. Der Datentransfer zwischen Host-System und Display ist naturgemäß intensiver und benötigt einen 8-Bit parallelen Port zuzüglich einer Reihe von Steuerleitungen. Damit sind die verfügbaren Ports am Mikrocontroller schnell vergeben. Die mitgebrachte Funktionalität der KS0108-Systeme ist sehr beschränkt und erlaubt nur das byte-weise Setzen oder Löschen von Pixeln. Das Zeichnen von Linien, Kreisen oder Texten benötigt allerlei logische Operationen mit dem Display-Memory, wobei das notwendige Timing exakt eingehalten werden muss. Es gibt zwar gute Bibliotheken für die meisten Zielsysteme (z.B. Referenz 1, 2). Trotzdem ist der Aufwand für das Host-System beträchtlich, da ein großer Teil der Ports, Timer, des Programmspeichers und der Rechenpower durch die Ansteuerung des Graphik-LCD ausgelastet sind. Die verbleibenden Ressourcen für die eigentliche Aufgabe des Host-Systems sind beschränkt.
So entwickelte sich die Idee, einen dedizierten Treiber mit Hard- und Software zu konstruieren, der auf der einen Seite die Komplexität der Graphik-Ansteuerung vollständig übernimmt und auf der anderen Seite dem Host-System ein serielles Interface anbietet, das Graphik-Anweisungen entgegennimmt. Das Smart I2C Graphic LCD Interface war entstanden.
Interfaces
Im Zentrum des Graphik-LCD-Interfaces befindet sich ein ATmega328, der dediziert für die Arbeit mit dem Display abgestellt ist. Der ATmega versorgt die Daten- und Steuerleitungen des Displays. Auf der Eingabe-Seite kommuniziert er mit dem Host System als I2C-Slave. Die Wahl fiel auf die I2C-Schnittstelle, weil die meisten Mikrocontroller-Systeme dieses Protokoll bereits verwenden, um mit Sensoren, Speicherbausteinen oder anderen Komponenten in Verbindung zu treten. Es sollte also relativ einfach sein, das I2C GLCD in ein bestehendes System einzufügen. Hilfreich ist auch die Tatsache, dass die I2C-Adresse über ein entsprechendes Kommando im verfügbaren Adressraum frei gewählt werden kann. Es ist sogar möglich, mehrere Displays an einem Host-System zu betreiben.
Da das I2C-Protokoll ist Plattform-unabhängig ist, arbeitet das Graphik-Display gleichermaßen gut mit AVR, Arduino, STM32 oder dem Raspberry PI zusammen. Der ATmega wird mit 3.3V versorgt, ist aber 5V-tolerant, so dass keine Pegelwandler benötigt werden. Die hier gezeigte Implementierung funktioniert sehr gut mit I2C-Taktgeschwindigkeiten zwischen 100 und 400kHz (Standard-mode und Fast-mode).
Das I2C-Interface nimmt Instruktionen byte-weise entgegen. Das Format ist für alle Instruktionen gleich: Zuerst wird ein Instruktions-Code gesendet, z.B. für das Zeichnen einer Linie oder der Ausgabe eines Textes, gefolgt von verschiedenen Paramater, z.B. Start- und End-Koordinaten der Linie oder die Zeichen des Text-Strings. Im Laufe der Entwicklung des Interfaces hat sich die Liste der verfügbaren Instruktionen stetig entwickelt und ist recht umfangreich geworden. Sie beinhaltet das Zeichnen von durchgezogenen oder gepunkteten Linien, gefüllten oder offenen Kreisen oder Rechtecken, die Ausgabe von Texten in verschiedenen Schrifttypen, und mehr.
Das Dokument Smart I2C GLCD Instruction Set zeigt eine tabellarische Aufstellung aller verfügbaren Instruktionen. Neben Funktionen zum Zeichnen und Löschen von graphischen Elementen gibt es auch Funktionen zur Ausgabe von Zeichen und Texten.
“Rundum sorglos Paket”
Eine Besonderheit ist der interne Datenpuffer, der das Host-System von jeder Sorge bezüglich Timing befreit. Der ATmega ist so programmiert, dass die Anforderungen vom Host über das I2C-Interface einen Interrupt triggern. In der Interrupt-Routine werden die Daten entgegengenommen und in einem Ringpuffer abgelegt. Das Hauptprogramm arbeitet den Puffer sukzessive ab. Die Tatsache, dass manche Anweisung (z.B. die Ausgabe eines längeren Strings) mehr Zeit benötigen als andere (z.B. das Setzen eines Pixels) ist aus Sicht des Host vollständig uninteressant. Solange der Puffer nicht voll ist – was in der Praxis praktisch nicht vorkommt – kann das Host-System die Graphik-Instruktionen zu jeder Zeit absetzten und braucht sich nicht darum zu kümmern, ob das Display bereit ist zur Eingabe oder nicht. Dieses „rundum sorglos Paket“ vereinfacht die Arbeit mit dem Display enorm.
Für Daten-intensive Anwendungen, bei denen ein Puffer-Overflow denkbar wäre, ist ein „Buffer Empty (B/E)“ Signal herausgeführt. Das Host-System kann den Status testen und gegebenenfalls warten, bis der Puffer leer ist. Bislang hat sich bei meinen Anwendungen dafür nur selten Bedarf ergeben.
Instruktions-Umfang
Das Display stellt eine lange Liste von Graphik-Funktionen bereit, z.B. das Setzen und Löschen von einzelnen Pixel, das Zeichnen von Linien, Rechtecken, gefüllten Flächen, Kreisen oder Segmente von Kreisen. Darüber hinaus gibt es die Möglichkeit der Textausgabe als einzelne Zeichen oder längere Strings. Die Textausgabe wird über einen Cursor gesteuert, der beliebig gesetzt werden kann. Es gibt die Möglichkeit automatischen Zeilenumbruch und Textscrollen zu setzen, sodass das Display auch für eine fließende Terminal-Ausgabe verwendet werden kann. Standardmäßig stehen 6 verschiedene Schriftsätze zur Verfügung.
Weiter gibt es Funktionen für die Ausgabe von Daten als Linien- oder Punkte-Diagramme. Diese Funktionen sind z.B. nützlich für die Visualisierung von zeitlichen Abläufen (z.B. Temperatur-Log) oder wissenschaftliche Daten.
Das I2C-Interface wird bi-direktional verwendet und bietet die Möglichkeit, Display-Parameter auszulesen. Zum Beispiel können die aktuelle Cursor-Position, die Länge von Strings in Pixeln oder die Höhe des angewählten Fonts abgefragt werden. Diese Funktionen sind nützlich, um die Grösse von weiteren graphische Elemente anzupassen. Das folgende Beispiel für den Arduino zeigt die Ausgabe eines Textes links oben im Display, gefolgt von der Abfrage der aktuelle Cursor-Position (der steht jetzt am Ende des Textes) und der aktuellen Höhe des Zeichensatzes. Mit diesen Daten wird dann eine Linie direkt unter den Text gezogen.
1 2 3 4 5 6 7 |
my_gd.set_font(4); my_gd.set_cursor(0, 0); my_gd.draw_str("Smart I2C GLCD Demo with Arduino"); delay(40); // ensure display ist ready prior to read request my_gd.get_cursor(&x_cur, &y_cur); font_height = my_gd.get_font_height(); my_gd.draw_line(0, font_height + 1, x_cur - 1, font_height + 1, 1); |
Allerdings gibt es beim Auslesen von Display-Parametern die Einschränkung, dass der Lese-Zugriff vom Host in Echtzeit stattfindet, also nicht gepuffert ist. Das Display muss die Daten aufbereiten, bevor der I2C-Read-Zugriff ausgeführt wird. Ansonsten kommen vorherige Werte aus dem Lese-Puffer über die Leitung. Deshalb empfiehlt es sich, entweder vor dem Ausführen der Abfrage einen Moment zu warten (40 msec wie im obigen Beispiel sind meist ausreichend) oder das Buffer/Empty-Signal abzufragen, bis der Puffer leer ist. Dann klappt es aber reibungslos.
Die vollständige Liste der Instruktionen ist als PDF verfügbar. Smart I2C GLCD Instruction Set_v1-1 Weitere Funktionen werden nach Bedarf hinzu kommen. Input ist auf jeden Fall willkommen.
Weitere nützliche Funktionen
Wenn schon ein I2C-kommunizierender ATmega am Display vorhanden ist, kann er auch noch andere nützliche Aufgaben übernehmen, z.B. die Versorgung der Hintergrund-Beleuchtung des Displays. Dazu erzeugt der ATmega ein PWM-Signal, das über zwei Transistoren die Beleuchtung regelt. Die Helligkeit wird ebenfalls über eine I2C-Instruktion gesteuert. Werte zwischen 0 (aus) und 10 (maximale Leuchtstärke) sind einstellbar. Die Helligkeits-Werte werden mit Hilfe einer logarithmischen Tabelle in die entsprechenden PWM-Wert umgesetzt, so dass eine gleichmäßige Abstufung entsteht.
Schließlich ist das System so ausgelegt, das Schlüssel-Parameter wie die aktuelle I2C-Adresse und Beleuchtungseinstellung im eePROM abgelegt werden, so dass sie beim Power-On wieder hergestellt werden.
Die Hardware
Die Schaltung ist sehr einfach. Ein low-drop Spannungsregler mit zugehörigen Kondensatoren erzeugt 3.3V für den Controller. Das Display benötigt in jedem Fall 5V. Abgesehen vom ATmega gibt es noch zwei Transistoren, die das PWM-Signal für die Beleuchtung verstärken. Je nach Display-Typ können hier bis zu 150 mA fließen. Der maximale Strom wird durch den 22 Ohm Widerstand am BC636 begrenzt. Einige Displays haben bereits eingebaute Vorwiderstände für die LED-Beleuchtung. Gegebenenfalls muss der Widerstand angepasst werden oder kann komplett entfallen. Weiterhin gibt es auf der Platine die Möglichkeit, die I2C-Leitungen mit Pull-Up-Widerständen zu versorgen, hier 4.7 k Ohm, falls das noch nicht an anderer Stelle im System geschehen ist. In der Regel werden diese Widerstände nicht gebraucht.
Der ATmega arbeitet mit seinem internen 8MHz R/C-Oszillator, so dass für die Takterzeugung keine weiteren Maßnahmen notwendig sind, abgesehen von dem Löschen des CLKDIV8-Flag in den Fuses.
Das Interface-Modul ist relativ sparsam. Der ATmega ist schon von sich aus moderat in Bezug auf den Strombedarf. Wenn es nichts zu tun gibt, wird er in den Sleep-Modus versetzt. Bei abgeschalteter Beleuchtung liegt der Eigenstrom-Bedarf unter 1mA.
Kleiner Form-Faktor
Der ATmega mit etwas Peripherie passt gut auf eine kleine Platine, die von hinten auf das Display aufgesteckt werden kann. Die ersten Prototypen wurden auf Lochraster-Platinen aufgebaut. Die Verbindung zum Display geschieht über einreihige 20-polige Stift- und Sockelleisten. Diese Leiste gibt dem Modul einen ausreichenden mechanischen Halt.
Nachdem der Prototyp erfolgreich im Einsatz war, wuchs der Bedarf an Display-Interfaces, so dass sich die Entwicklung und Herstellung einer kleinen Platine lohnte. Die Sockelleiste kommt diese Mal auf die Bestückungsseite. Wenn man niedrige Bauteile für die Bestückung verwendet, verschwindet das komplette Interface-Modul unauffällig hinter dem Display.
Die Verbindung zum Host-System ist an der Seite als gewinkelte Pfoste-Leiste herausgeführt. Normalerweise werden nur 4 Leitungen benötigt: Masse und +5V zur Stromversorgung und SCL und SDA für den I2C-Bus. Außerdem, falls Bedarf besteht, steht dort das oben angesprochene B/E-Signal an einem Pin zur Verfügung.
Das Board wurde für Display-Typ 3 entwickelt (siehe nächsten Absatz). Einen ZIP-Ordner mit den Gerber-Files zum Download gibt es auf der Ressourcen-Seite.
GLCD Typen
Im Laufe meiner Arbeiten sind mir (mindestens) drei verschiedene Typen von KS0108-Displays begegnet. Im Prinzip funktionieren sie alle ähnlich, unterscheiden sich aber in der Anzahl der KS0108 Chips, der daraus resultierenden Auflösung, der Anschluss-Belegung und der Logik für die Auswahl des jeweiligen KS0108-Chips. Die Tabelle zeigt die drei Display-Typen, wobei die Tabelle natürlich keinen Anspruch auf Vollständigkeit erhebt. Diese drei Typen sind in der Software vorgesehen. Der gewünschte Display-Typ muss vor der Compilierung in der Datei ks0108.h ausgewählt werden.
Die Displays haben mehrer KS0108-Panels an Board, wobei ein Panel für jeweils 64 * 64 Pixel zuständig ist. Entsprechend werden also 2 oder 3 Panels eingesetzt. Die Auswahl der Panels geschieht über die Display-Anschlüsse CS1 bis CS3. Display Typ 1 hat nur zwei Chips die mit CS1 und CS2 selektiert werden. Dabei ist “low” der aktive Zustand. Display Typ 3 funktioniert analog mit 3 Chips, also ein linkes, ein mittleres und ein rechtes Panel, und verwendet die Anschlüsse CS1 bis CS3. Display Typ 2 hat ebenfalls 3 Chips, selektiert diese aber mit nur zwei Leitungen CS1 und CS2. Wenn beide Leitungen “low” sind, dann ist das linke Panel angewählt. CS1 auf “low” wählt das mittlere, und CS2 auf “low” das rechte Panel.
In jedem Fall wird die Panel-Selektion von der Software des Interface-Moduls verwaltet – sofern der richtige Display-Typ gewählt wurde – und ist für den Anwender vollständig transparent.
Software
Auch wenn die Hardware überschaubar ist, so ist die Software für dieses Projekt deutlich komplexer. Die Software wurde in C mit dem AtmelStudio(Version 7) entwickelt. Sie besteht aus den folgenden Modulen:
- main.c enthält den System-Initialisierung und Instruktions-Interpreter.
- GLCD_routines.c beinhaltet die höheren Graphik-Funktionen einschließlich Fehler- und Bereich-Checks. Diese Funktionen werden vom Instruktions-Interpreter aufgerufen.
- ks0108.c beinhaltet die Hardware-nahen Graphik-Funktionen, die direkt mit dem KS0108-Chip zusammenarbeiten
- twi_slave.c sorgt sich um das I2C-Interface und verwaltet den Ringpuffer mit den Funktionen zum Auslesen der Daten aus dem Ringpuffer.
- smart_I2C_display.h enthält eine Liste der Instruktion-Codes.
Das Software-Paket hat (zur Zeit) insgesamt 7 Zeichensätze für die Ausgabe von Text, die in eigenen #include-Dateien abgelegt sind. Erweiterungen sind jederzeit möglich.
- Font #0: system.h, 5×8 fixed space
- Font #1: Font_3x5.h
- Font #2: Font_4x8.h
- Font #3: Arial_8.h
- Font #4: Calibri_10.h
- Font #5: Arial_12.h
- Font #6: Arial_bold_14.h
Zum Teil wurden die Fonts mit einem Public Domain GLCDFontCreator erzeugt (Referenz). Die Datenstruktur der Font-Files folgt einem üblichen Standard ist relativ einfach durchschaubar. Es ist durchaus möglich, einen weiteren Zeichensatz zu kreieren, z.B. für Anwendungs-spezifische Symbole, Pfeil, Sterne, usw. Der Programm-Speicher des ATmega biete noch eine Menge Platz für viel Kreativität.
Anwendungsbeispiel:
Smart I2C GLCD-Interface am Raspberry Pi
Das Smart I2C GLCD Interface macht die Anwendung der KS1018-Displays sehr einfach. Sofern das Host-System über ein I2C-Master-Interface verfügt, können die Instruktionen direkt zum Display gesendet werden. Und da der Raspberry Pi diese Schnittstelle aufweist, ist der Anschluss an das GPIO sehr einfach.
Sobald die vier Leitungen angeschlossen sind, sollte sich das GLCD bei der Abfrage mit sudo i2cdetect -y 1 unter der gewählten I2C-Adresse melden.
Der Python-Interpreter erlaubt das interaktive Ausprobieren der einzelnen Instruktionen und macht viel Spaß mit dem Display. Zuerst muss noch der SMBus gestartet werden, und dann kann es losgehen. Die Python-Funktionen write_byte(), write_word_data() und write_i2c_block_data() eignen sich, um das GLCD anzusprechen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import smbus s = smbus.SMBus(1) <span style="color: #ff0000;"># clear screen, instruction code 15</span> s.write_word_data(32, 15, 0) <span style="color: #ff0000;"># dim backlight to 70%, instruction code 3</span> s.write_word_data(32, 3, 7) <span style="color: #ff0000;"># draw line from 1/1 to 190/60, instruction code 17</span> s.write_i2c_block_data(32, 17, [1, 1, 190, 60, 1]) <span style="color: #ff0000;"># draw circle, instruction code 25</span> s.write_i2c_block_data(32, 25, [100, 30, 20, 0xff, 1]) |
Da das Eintippen der Instruktionen auf die Dauer ermüdend ist, habe ich eine Python-Klasse angelegt, die die Anwendung vereinfacht (siehe glcd_module). Sobald die Datei mit der glcd-Klassendefinition im Suchpfad von Python verfügbar ist, genügen die beiden Anweisungen …
1 2 |
>>> import glcd_module >>> gl = glcd_module.glcd(32) |
… um eine Instanz des GLCD Modules zu eröffnen. Jetzt stehen alle Instruktionen zur Verfügung, z.B.:
1 2 3 4 |
>>> gl.clear_screen() >>> gl.set_font(5) >>> gl.set_cursor(96, 25) >>> gl.draw_center_string("Hello World") |
Das kleine Beispielprogramm glcd_demo zeigt einen wandernden Strich auf dem Bildschirm in der Art, wie man es vielleicht von früheren Bildschirmschonern kennt.
Weitere Anwendungen für das GLCD-Interface Modul an ATmega- und Arduino-Host-Systemen folgen in eigenen Beiträgen.
Downloads
Die Seite Ressourcen stellt umfangreiches Material bereit, einschließlich Source-Code im Atmel Studio, Beschreibung der verfügbaren Graphik-Funktionen, Board-Layout für den Display Typ3 3, Bibliotheken und Beispiele für Arduino und Python
Referenzen
- U8glib library:
github.com/olikraus/u8glib/wiki/userreference - ScienceProg: Controlling graphical 128×64 LCD based on KS0108:
scienceprog.com/controlling-graphical-128×64-lcd-based-on-ks0108 - M. Thiele: KS0108 Library and Font Creator
https://www.mikrocontroller.net/articles/KS0108_Library