Das Grafik-Display DG12232

Hintergrund

Für einen Programmierkurs in meiner Firma habe ich mich mit dem Grafik-Display DG12232 von Datavision beschäftigt. Das Display ist sehr verbreitet und günstig zu kriegen (meins hatte ich z.B. bei Pollin Electronic inkl. Adapter-Platine für unter 2 Euro bekommen). Es hat eine Auflösung von 122 x 32 Pixel und ist monochrom. Ich hatte mich im Vorfeld darüber informiert, wie biestig es anzusteuern ist, um meinen Kursteilnehmern keine "unlösbare" Aufgabe zu präsentieren. Dabei habe ich viele Foren-Beiträge gefunden, in denen die Ansteuerung als verhältnismäßig kompliziert verschrien wurde. Es gab dann aber doch etliche Projekte, in denen das Display erfolgreich benutzt wurde. Etwas Code-Analyse hat dabei gezeigt, dass alle den Treiber von Radoslaw Kwiecien benutzt haben, allerdings meistens ohne Verweis auf den Urheber und manchmal etwas verstümmelt (Entschuldigung, "customized"). Die teilweise erhalten gebliebenen polnischen Kommentare und natürlich die Funktionsnamen an sich waren jedoch recht eindeutig in Bezug auf die wahre Quelle...

Mit Hilfe des Treibers von Radoslaw und dem SED1520-Datenblatt habe ich mir Aufbau und Funktionsweise des Displays erschlossen und möchte meine Erkenntnisse nun weitergeben. Außerdem habe ich einen eigenen Treiber geschrieben, der perspektivisch Teil meines avr-classes-Projekts werden soll, aber im Augenblick noch auf Integration wartet. Hat aber schon mal funktioniert, könnt ihr gerne vorab benutzen.

Display-Aufbau

Das Display DG12232 besitzt zwei Exemplare des LCD-Controllers SED1520. Der Eine ist für die linke Hälfte zuständig, der Andere für die rechte Hälfte. Dabei steuert jeder Controller 61 x 32 Pixel an, was in Summe 122 x 32 Pixel ergibt. Ein Treiber muss also anhand der X-Position entscheiden, auf welchem Controller das Pixel liegt, und dementsprechend adressieren. Das betrifft einerseits die Verwendung der richtigen Steuerleitungen, andererseits die Umrechnung der Display-Koordinaten, da für jeden der beiden Controller die linke obere Ecke "seines" Displays bei 0;0 liegt.

Der SED1520-Controller ist intern in 4 Seiten mit je 80 Spalten organisiert. Von diesen 80 Spalten sind bei diesem Display jedoch nur 61 angeschlossen. Jede Spalten/Seiten-Kombination adressiert 1 Byte, dessen 8 Bits die Farbe der 8 Pixel steuern. Um den Buchstaben "A" im nebenstehenden Bild zu erzeugen müssen also die Spalten 1 bis 5 der Seite 0 mit dem Bitmuster 0x7E, 0x11, 0x11, 0x11, 0x7E beschrieben werden. Das Beschreiben aufeinander folgender Spalten ist besonders effizient, weil der SED1520 seinen Schreibzeiger mit jedem Schreibzugriff automatisch um eins inkrementiert, d.h. die Schreibzugriffe können direkt aufeinander folgen, ohne dass zwischendrin eine neue Adresse gesetzt werden müsste.

Dadurch, dass jedes Pixel frei angesteuert werden kann, spielt die Aufteilung auf Seiten eine untergeordnete Rolle. Sie bedingt lediglich, dass das Schreiben von "überlappenden" Symbolen ggf. mehr Schreibvorgänge benötigt und somit langsamer ist. Ähnliches gilt auch für Symbole, die auf beide SED1520-Instanzen verteilt sind: die Ansteuerung erfordert etwas mehr Intelligenz, aber die Darstellung erfolgt nahtlos.

Pin-Belegung

Bei der Pin-Belegung orientiere ich mich an der Beschriftung und Anordnung der Adapter-Platine von Pollin, weil ich davon ausgehe, dass die meisten sich diese beschaffen werden. Die Belegung ist im Datenblatt von Pollin beschrieben, welches eine Zusammenfassung des Hersteller-Datenblatts ist. Ich habe eine Weile gebraucht, um sie richtig zu verstehen, und möchte sie an dieser Stelle daher etwas ausführlicher kommentieren.

Ein zentraler Punkt ist im Display-Datenblatt lapidar mit "80 Serial" und "68 Serial" erwähnt, ohne genauer ins Detail zu gehen. Aus dem Controller-Datenblatt wird klar, dass die verschiedenen CPU-Baureihen gemeint sind:

Das ist elementar, weil sie andere Bus-Signale verwenden und damit verbunden unterschiedliche Timings benötigen. Das Display ist offensichtlich dafür gedacht, direkt in ein solches Mikroprozessor-System integriert zu werden (sieht man auch am Ende des SED1520-Datenblatts). Daher auch die mysteriöse Bezeichnung A0 für die Register-Auswahl: das ist das niedrigste Adressbit, d.h. man könnte das Display im Speicher an zwei aufeinander folgenden Adressen einblenden, sodass $BASE_ADDR das Befehlsreigster und $BASE_ADDR + 1 das Datenregister referenziert.

Nr. Symbol Beschreibung Bedeutung
LOW HIGH
1 A0 Niedrigstes Adressbit, wählt zwischen Befehls- und Datenregister. Befehlsregister Datenregister
2 CS2 Chip Select für Controller 2 (rechts). aktiv neutral
3 CS1 Chip Select für Controller 1 (links). aktiv neutral
4 CL Externes Taktsignal. Bei Verwendung der Pollin-Platine mit aktivem Taktgeber kann dieser Pin unbeschaltet bleiben.
5 /RD 80er-Familie: fallende Flanke löst Lesevorgang aus.
E 68er-Familie: Enable-Pin für Lese- oder Schreibvorgang.
6 /WR 80er-Familie: fallende Flanke löst Schreibvorgang aus.
R/W 68er-Familie: Auswahl zwischen Lese- und Schreibrichtung. schreiben lesen
7 VSS 0 Volt, verbunden mit GND-Potenzial.
8-15 DB0-DB7 Datenbus (8 Bit).
16 VDD Spannungsversorgung für LCD-Logik.
17 RES Reset. Der gehaltene Spannungspegel nach erfolgtem Reset gibt vor welches Interface benutzt werden soll. 80er-Familie 68er-Familie
18 VEE Spannungsversorgung für LCD-Segmente.

Das Befehlsregister hat abhängig von der Datenrichtung unterschiedliche Bedeutungen: beim Schreiben nimmt es Befehle an den Display-Controller entgegen, beim Lesen zeigt es den Status des Display-Controllers an. Das Datenregister verhält sich "normal" (was man schreibt liest man auch).

Display-Befehle

Der SED1520-Controller versteht eine Reihe von Befehlen, häufig mit Parametern. Diese haben teilweise subtile Eigenschaften, die sich erst durch das aufmerksame Lesen des Datenblatts erschließen. Ein guter Treiber sollte diese Display-Befehle in sprechende Funktionsnamen kapseln und dem Benutzer keine Möglichkeiten zur Fehlbedienung lassen. Die folgende Tabelle gibt die auf unterster Ebene zur Verfügung stehenden Display-Befehle wieder.

Befehl Code Parameter Beschreibung
Werte Bedeutung
Display ON/OFF 0xAE 0 - 1 1 = ON
0 = OFF
Schaltet die Anzeige ein und aus, unabhängig vom Inhalt des Display-RAMs. Kann genutzt werden um das Schreiben des Display-Inhalts nicht "live" zu zeigen.
Display Start Line 0xC0 0 - 31 Display Start Address Legt fest welche RAM-Zeile der obersten Segment-Zeile zugeordnet werden soll. Vermutung: kann für vertikale Scroll-Effekte genutzt werden? (1)
Set Page Address 0xB8 0 - 3 Page Legt fest auf welche Seite als nächstes zugegriffen wird. Bei Änderung ist ein Dummy-Lesevorgang notwendig. (2)
Set Column Address 0x00 0 - 79 Column Legt fest auf welche Spalte als nächstes zugegriffen wird. Bei Änderung ist ein Dummy-Lesevorgang notwendig. (2)
Select ADC 0xA0 0 - 1 1 = CW (forward)
0 = CCW (reverse)
Invertiert die Zuordnung zwischen Display-RAM-Spalten und Segment-Treiber. CW steht für clockwise und CCW für counter clockwise. (1)
Static Drive ON/OFF 0xA4 0 - 1 1 = Static drive
0 = Normal driving
Energiesparmodus. Wenn aktiv, dann werden alle Segmente statisch angesteuert und erscheinen ausgefüllt, die Treiber verbrauchen dann keine Energie. (1)
Select Duty 0xA8 0 - 1 1 = 1/32
0 = 1/16
Duty Cycle für LCD-Ansteuerung, hat Abhängigkeit zur Controller-Variante. (1)
Read Modify Write 0xE0 Nur Schreibvorgänge inkrementieren den Addresszähler, Lesevorgänge nicht. (3)
End 0xEE Ende des Read-Modify-Write-Modus. Setzt Page/Column Address Counter auf den Zustand zurück, der beim Start vorlag. (3)
Reset 0xE2 Reset von Display Start Line, Column Address Counter und Page Address Counter. Die Daten im Display-RAM bleiben erhalten.

Anmerkungen:

  1. Habe ich noch nicht benutzt und immer auf Default-Werten belassen.
  2. Der SED1520 verfügt über eine 2-stufige Pipeline für das Lesen von Daten. Nach dem Verschieben der Leseposition (Seite oder Spalte) ist das nächste Zeichen in der Pipeline veraltet und muss daher mit einem Dummy-Lesevorgang entsorgt werden.
  3. Hat bei mir nicht wie erwartet funktioniert. Ist zur Erleichterung von Pixel-Manipulationen gedacht, weil hierzu der alte Wert gelesen, das Pixel verändert und der neue Wert zurückgeschrieben werden muss. Der Read-Modify-Write-Modus erspart das ständige Zurücksetzen der Spalten-Adresse.

Beim Lesen des Befehlsregisters erhält man den Controller-Status:

Bit Makse Name Beschreibung Bemerkung
7 0x80 BUSY 1: Interne Operation im Gang
0: Bereit
6 0x40 ADC 1: CW output (forward)
0: CCW output (reverse)
5 0x20 ON/OFF 1: Display aus
0: Display ein
Bedeutung invertiert zu ON/OFF-Befehl!
4 0x10 RESET 1: Reset aktiv
0: Normal

Die unteren vier Bits sind immer 0.

Rastergrafik

Wer ein Grafik-Display mit fester Pixelauflösung betreibt kommt unweigerlich mit dem faszinierenden Gebiet der Rastergrafik in Kontakt. Spätestens wenn man versucht eine schräge Linie auf das Display zu zeichnen ist klar: das ist gar nicht so einfach. Zum Glück beschäftigen sich viele schlaue Leute schon sehr lange mit diesem Thema und es gibt eine Menge an Literatur derer man sich bedienen kann. Da ich selbst noch Novize auf diesem Gebiet bin, möchte ich an dieser Stelle nur ein paar Links teilen:

Von hier aus kann man sich gut einen Abend lang durchs Internet schmökern. Gerade auch englische Suchbegriffe wie "raster graphics algorithms" fördern zahlreiche Vorlesungsskripte und Präsentationen zutage.

Bildschirmpuffer

Mit pixelbasierten Verfahren auf das Display zu zeichnen ist relativ langsam, weil das Verändern eines einzelnen Pixels eine Read-Modify-Write-Operation ist:

  1. Ganzes Byte (8 Pixel) lesen
  2. Das gewünschte Pixel ändern
  3. Ganzes Byte zurückschreiben

Wie schlimm ist es jetzt, auf diese Art das Display zu befüllen? Es folgen ein paar einfache Rechnungen. Mir ist bewusst, dass nicht alle Operationen genau gleich lange dauern bzw. programmintern noch weitere Schritte zur Beschaffung der gewünschten Display-Daten notwendig sind, aber für eine grobe Komplexitätsbetrachtung und den Vergleich verschiedener Ansätze reicht das.

Vorgang Schritte Häufigkeit Summe
Seite auswählen Seite auswählen 8 mal 8
Bestehendes Byte einlesen Zeilen-Adresse setzen 122 * 32 mal 3904
Dummy-Lesevorgang 122 * 32 mal 3904
Lesevorgang 122 * 32 mal 3904
Verändertes Byte schreiben Zeilen-Adresse setzen 122 * 32 mal 3904
Schreibvorgang 122 * 32 mal 3904
Jedes Pixel einzeln setzen 19528

Macht zusammen 19528 Operationen. Nun ist es sehr unwahrscheinlich, dass man das komplette Display pixelweise beschreiben möchte. Ein wahrscheinlicherer Anwendungsfall besteht darin, das Display erst komplett zu löschen und dann vielleicht 20% der Display-Fläche neu zu beschreiben.

Neue Rechnung zum Löschen des Display-Inhalts:

Vorgang Schritte Häufigkeit Summe
Position an Seitenanfang setzen Seite auswählen 8 mal 8
Zeilen-Adresse setzen 8 mal 8
Byte mit Wert 0x00 schreiben Byte schreiben 122 * 4 mal 488
Display komplett löschen 504

Das macht also 504 Operationen für das Löschen und 19528 * 0.2 = 3906 Operationen für das Setzen der Pixel. Immer noch eine Menge, insbesondere wenn man das alles über GPIO-Pins eines Mikrocontrollers erledigt.

Ein viel schnelleres Verfahren erfordert einen Bildschirmpuffer, d.h. man hält die Pixeldaten komplett im lokalen RAM vor. Damit entfallen die lästigen Read-Modify-Write-Vorgänge, weil man ständig das aktuelle Gesamtbild zur Verfügung hat; das Setzen und Löschen einzelner Pixel ist eine einfache Bit-Operation im Arbeitsspeicher. Man muss lediglich zu geeigneter Zeit das Bild zum Display übertragen, was genau so viele Operationen erfordert wie das Löschen: 504. Dabei spielt es auch keine Rolle, ob 20% oder 100% der Display-Fläche verändert wurden (es sei denn man optimiert weiter, indem man nur Teile des Bildschirmpuffers zum Display überträgt).

Für realistische Anwendungsfälle beschleunigt dies das Beschreiben also auf das 8- bis 10-fache. Aber das kommt natürlich zu einem gewissen Preis: für den Bildschirmpuffer werden für dieses Display 122 * 4 = 488 Byte RAM benötigt. Je nach verwendetem Mikrocontroller kann das schon weh tun (Beispiel ATmega32: 2 KByte RAM insgesamt verfügbar!). Wer allerdings noch etwas mehr RAM spendieren kann, dem eröffnen sich zahlreiche weitere Möglichkeiten (Stichworte Blending, Z-Ordnung, Sprites, Kachelgrafik).

Update: eine Idee, die mir erst im Nachhinein kam, erlaubt einen gewissen Mischbetrieb. Man lädt bei Bedarf einen Teil des Display-RAMs in das lokale RAM, zeichnet nach Herzenslust drin herum, und schreibt es dann am Stück zurück. Damit hat man die Vorteile, einerseits das lokale RAM nicht dauerhaft zu blockieren (die eigentliche Datenhaltung erledigt das Display-RAM auf dem Display-Controller), und andererseits auch bei vielem hin-und-her-Gespringe kein Byte mehrfach anfassen zu müssen. Die Nachteile sind der Aufwand die Daten zu holen (ebenfalls 504 Operationen) und bei Kachelung der Display-Bereiche, um den temporären Puffer klein zu halten, die zusätzliche Logik dafür.

Software

Treiber von Radoslaw Kwiecien

Der vermutlich am weitesten verbreitete Treiber für das DG12232 bzw. den SED1520 dürfte von Radoslaw Kwiecien stammen: Universal C library for SED1520/NJU6450 LCD. Dieser ist für AVR-Mikrocontroller formuliert und beinhaltet folgende typischen Dateien, die man oft in anderen Mikrocontroller-Projekten wieder findet:

Neben internen Funktionen besteht der Treiber im Wesentlichen aus diesen Funktionen, die für den Bibliotheksanwender gedacht sind:

Die Abhängigkeit zu den GPIO-Pins wird über #define-Makros in SED1520-AVR.c aufgelöst, was zu sehr kompaktem Code führt. Etwas unglücklich finde ich die Benennung der Schnittstellen-Parameter, denn bei GLCD_GoTo und GLCD_Bitmap steht y für eine Seitenadresse, während GLCD_SetPixel sowie die darauf aufbauenden Funktionen GLCD_Rectangle, GLCD_Circle und GLCD_Line tatsächlich pixelgenau arbeiten.

Mein eigener Treiber

Mein eigener Treiber ist in C++ geschrieben und Teil meines avr-classes-Projekts. Er stellt die Funktionen des Displays in objektorientierter Form zur Verfügung, was meiner Meinung nach recht elegant und vielseitig ist, aber auf der anderen Seite deutlich mehr Ressourcen verschleudert. Hier ein vereinfachtes Diagramm der beteiligten Klassen um einen groben Überblick zu vermitteln:

Die Basis-Klasse Canvas stellt eine Zeichenfläche dar, auf die pixelweise zugegriffen werden kann. Ferner hat sie eine Höhe und eine Breite. Zeichenfunktionen (z.B. für Linien und Kreise) müssen nur die Canvas-Klasse kennen.

Von Canvas abgeleitet sind Bitmap und GraphicDisplay. Bitmap ist ein Canvas-Objekt, das die Pixeldaten in einem Speicherbereich ablegt. GraphicDisplay ist momentan eine leere Klassen-Ebene, könnte aber Methoden enthalten, die typisch für grafische Displays sind (in Abgrenzung zu allen Displays, deren Methoden im Interface IDisplay enthalten sind). Außerdem ist diese Klassen-Ebene dazu gedacht, das Display an Methoden zu übergeben, die irgendein geeignetes Grafik-Display erwarten und wissen müssen, dass es ein Display ist (nicht nur ein beliebiges Canvas-Objekt). Vielleicht um später einmal Double-Buffering zu implementieren oder das Display auszuschalten während aktualisiert wird.

Konkreter wird es bei DG12232, das die Fähigkeiten eines GraphicDisplays beinhaltet, aber spezifische Eigenschaften des DG12232-Displays hat (z.B. die feste Größe von 122 x 32). Eine solche Klasse besitzt zwei Elemente vom Typ SED1520. Die SED1520-Klasse implementiert den Großteil des Treiber-Know-Hows. Die Arbeitsteilung sieht vor, dass DG12232 die Befehle entgegen nimmt und dann anhand der Pixel-Koordinaten auf die entsprechende Instanz von SED1520 abbildet. Die SED1520-Instanzen wiederum kennen nur "ihren" Controller und wissen nichts von ihrer Umgebung.

Eine Variante von DG12232 ist BufferedDG12232, welche einen Bildschirmpuffer für die gesamte Display-Fläche beinhaltet und eine Methode bereitstellt, über die der Puffer auf einen Schlag an das Display übertragen wird.

Alle Zeichenoperationen sind in statischen Hilfsklassen implementiert (hier nicht abgebildet). Die Idee ist, dass der Anwender nur mit Canvas-Objekten arbeitet, von der eines die Besonderheit besitzt mit einem physikalischen Display verknüpft zu sein.

Die Abhängigkeiten zu den GPIO-Pins sind über weitere Interfaces aufgelöst und werden Instanzen der DG12232-Klasse zur Laufzeit bekannt gemacht. Dies macht die Display-Anbindung zwar noch ein gutes Stückchen teurer, aber, wie auf der Projektseite zu avr-classes dargelegt, ist dies Teil des Experiments ("wie viel C++ erträgt ein AVR?").

Leider ist die Entwicklung dieses Treibers zusammen mit der Entwicklung von avr-classes stecken geblieben. Dem Treiber fehlt noch eine Anpassung an das neue GPIO-Interface zur sauberen Integration. Aber er war schon mal in einem Bastelprojekt im Einsatz und sollte für sich genommen funktionieren. Ich habe jetzt (September 2020) beschlossen, die bisherigen Brocken als Vorabversion zu releasen, damit ich endlich diese Seite aus dem Zustand "in Arbeit" entlassen und sie guten Gewissens der Allgemeinheit zugängig machen kann, quasi als stabilen Zwischenstand.

Downloads

Datum Version Datei Beschreibung
2018-12-04 0.1 dg12232-driver-0.1.zip Quellcode (nur Brocken!)

Zurück zur Hauptseite