Ein Dot-Matrix-Display direkt (mit selbstgeschriebenem “Treiber”) ansteuern

https://oer-informatik.de/arduino_hardware-treiber-max7219

tl/dr; (ca. 15 min Lesezeit): Gedankenspiel: wenn es die ganze wunderbare Arduino-Community nicht gäbe. Nur uns, den Microcontroller, ein 8x8-Matrix-Display (MAX7219), die eine IDE und ein Datenblatt. Würden wir es dann schaffen, das Display an’s laufen zu bekommen? An diesem Beispiel soll gezeigt werden, dass hardwarenahe Programmierung ohne Bibliotheken - nur mit den Angaben eines Datenblatts - kein Zauberwerk ist…

Die wichtigen Infos aus dem Datenblatt des MAX7219 sammeln:

Bevor wir mit der Programmierung beginnen müssen wir erstmal herausfinden, wie die elektronische Verbindung zwischen Arduino und dem Display aufgebaut werden muss. Mir liegt ein gekapseltes Display vor, dass bereits mit dem MAX7219 verbaut wurde. Somit ist klar, dass ich die Pins VCC, GND, DIN, CS und CLK zur Verfügung habe.

Der Schaltplan: Hardware / Pins

Das Datenblatt des von den Displays genutzten Chipts MAX7219 legt auf Seite 5 folgende Rahmenbedingungen für die Pinkontakte des MAX7219 fest:

  • Pin 19 des MAX7219: V+ Positive Supply Voltage. Connect to +5V.

  • Pin 4, 9 des MAX7219: GND Ground. Both GND pins must be connected. (Betrifft die Kontakte des Chips, nicht die des Dispays.)

  • Pin 1 des MAX7219: DIN Serial-Data Input. Data is loaded into the internal 16-bit shift register on CLK’s rising edge

  • Pin 12 des MAX7219: LOAD (\overline{CS}) (MAX7219) Load-Data Input. The last 16 bits of serial data are latched on LOAD’s rising edge.

  • Pin 13: CLK Serial-Clock Input. 10MHz maximum rate. On CLK’s rising edge, data is shifted into the internal shift register. On CLK’s falling edge, data is clocked out of DOUT. On the MAX7221, the CLK input is active only while CS is low.

Ok, arduinoseitig kann die Pin-Belegung frei gewählt werden. Ich habe mich hier für die Belegung gemäß der internen SPI-Schnittstelle entschieden und notiere das erstmal in alter Gewohnheit als Liste der Ein- und Ausgabeelemente am Kopf des Arduino-Sketches. Bei anderen MCU (z.B. nodeMCU mit ESP32 oder ESP8266) müssen natürlich andere, als Ausgang zulässige Pins für LOAD, CLK und DIN gewählt werden.

Fritzing-Entwurf der Schaltung, DIN-Pin12, CLK-Pin13, CS-Pin10
Fritzing-Entwurf der Schaltung, DIN-Pin12, CLK-Pin13, CS-Pin10

Hardwareseitig gibt es noch eine weitere Festlegung:

  • VCC sollte zwischen 4.0V und 5V liegen

  • Die Clock darf nicht schneller als 10MHz werden, also ein Zyklus 0,1 ns. Müssen wir später mal nachrechnen, ob das kritisch wird…

  • Die Logiklevel liegen leider über 3,3 V - der Betrieb an MCU mit 3,3V (etwa nodeMCU) ist ohne zusätzliche Schaltungen also Glückssache (geht aber i.d.R.):

MIN MAX
V_{IH} 3.5
V_{IL} 0.8
V_{OH} 3.5
V_{OL} 0.4

Soweit zur Elektronik. Aber welche Nachrichten verarbeitet das Display?

Das erwartete Nachrichtenformat

Datenblatt des MAX7219, Seite 6:

For the MAX7219, serial data at DIN, sent in 16-bit packets,
is shifted into the internal 16-bit shift register with each
rising edge of CLK regardless of the state of LOAD.

Mit jeder steigenden Flanke der Clock wird ein Bit an DIN eingelesen (und im Schieberegister gespeichert), in 16 Bit Blöcken.

The data is then latched into either the digit or control registers
on the rising edge of LOAD/CS. LOAD/CS must go high concurrently with
or after the 16th rising clock edge, but before the next rising clock
edge or data will be lost.

Nach 16 Bit (also nach oder mit der 16. steigenden Flanke der CLK) muss LOAD auf HIGH wechseln, damit die Daten aus dem Schieberegister gespeichert/verarbeitet werden.

Data bits are labeled D0–D15 (Table 1). D8–D11 contain the register address.
D0–D7 contain the data, and D12–D15 are “don’t care” bits.
The first received is D15, the most significant bit (MSB).

Die Daten werden in 16-Bit-Paketen versendet: mit jeder steigenden Flanke der Clock ein Bit. Dabei besteht die Nachricht aus drei Teilen:

  • die erste Hälfte des Address-Bytes (vier Bit) sind “don’t care”-Positionen. Sie werden nicht interpretiert (weil lediglich 14 Befehle im Max7219 implementiert sind).

  • die zweite Hälfte des Address-Bytes gibt die Befehle an, die der Max7219 ausführen soll (dazu unten mehr)

  • danach folgen 8 Daten-Bit, die die Parameter der Befehle darstellen.

Als Impulsdiagramm darf man sich die Aufteilung der 16 Bit etwa so vorstellen:

Impulsdiagramm einer Nachricht des Max7219
Impulsdiagramm einer Nachricht des Max7219

Die oberen drei Graphen (Clock, DIN, LOAD) sind die eigentlichen gesendeten Werte. Die unteren drei ist nur Kommentare/Interpretationen.

Auf geht’s an die Software

Voraussetzungen

Bevor Daten gesendet werden, müssen zunächst die Ausgänge korrekt gesetzt werden. Sicherheitshalber setzen wir direkt alle Ausgänge auf LOW:

Zusammensetzung eines ersten Befehls

Der einfachste Befehl aus dem Datenblatt (Seite 7, Tabelle “Table 2: Register Address Map”) ist “Display Test”. Wir müssen dazu zunächst 8-Bit für die Befehlsadresse senden, also an DIN bei steigenden Flanken der CLK nacheinander: LOW, LOW, LOW, LOW, HIGH, HIGH, HIGH, HIGH (wobei die ersten vier Bit beliebig sein können (don’t care positions). Danach werden die Daten gesendet: sieben beliebige Bit an DIN bei steigender CLK-Flanke und ein HIGH. Damit sollte das Display im Test-Modus sein und alle LED leuchten. Danach muss einmal LOAD auf HIGH gesetzt werden, um den Befehl auszuführen. Das müssen wir jetzt nur noch programmieren:

Befehle senden in zwei 8-Bit-Blöcken

Die Daten werden in zwei 8-Bit Blöcken gesendet: 8 Bit Befehlsadresse, 8 Bit Daten (Parameter der Befehle). Bei den Arduino Uno handelt es sich um 8-Bit-Microcontroller, daher ist es nicht unüblich, in dieser Paketgröße Daten zu senden. Der Übersicht halber habe ich eine Funktion erstellt, die einen 8-Bit-Block (also ein Byte) an DIN mit CLK-Flanke ausgibt.

Gemäß der obigen Beschreibung wird erst das Datenbit auf den gewünschten Wert gesetzt, danach taktet die Clock einmal.

Die 8-Bit übergeben wir als Byte-Parameter an diese neue Funkton shiftByte(inputByte). Wir notieren den Byte-Wert in binärer Schreibweise, dann erkennen wir auch die einzelnen Bit, z.B. für die Befehlsadresse: shiftByte(0b00001111). Das ist deutlich leichter zu interpretieren als das gleichwertige shiftByte(15).

Wir lesen die einzelnen Bit aus dem Byte mit der Funktion bitRead(byte, pos). Diese Funktion ist Bestandteil der Arduino IDE und etwas leichter verdaulich, als die bitshift-Operatoren <<. Wichtig noch: wir fangen beim Index 7 an und arbeiten uns bis zur 0 vor - die Reihenfolge ist also umgekehrt, als das viele erwarten würden:

Der MAX7219 erlaubt laut Datenblatt Taktraten der Clock bis zu 10\ \mathrm{MHz} (siehe oben). Bei langsamen Microcontrollern wie dem Arduino ist das kein Problem: dessen ATMega 328P Microcontroller taktet selbst nur 1\ \mathrm{MHz}. Bei den deutlich schnelleren ESP-Varianten ESP8266 (80\ \mathrm{MHz}) und ESP32 ( bis zu 240\ \mathrm{MHz}) oder anderen schnellen Microcontrollern sollten wir sicherstellen, dass an der Clock des MAX7219 nicht mehr als 10\ \mathrm{MHz} anliegen. Aus dieser Frequenz folgt eine Periodendauer von T = \frac{1}{f} = \frac{1}{10\ \mathrm{MHz}} = 0{,}1\ \mathrm{\mu s}. Es ist also völlig hinreichend, wenn wir jeweils nach dem HIGH und LOW an der Clock 1\ \mathrm{\mu s} warten. Ich definiere das global (ganz oben im Code, vor den Funktionen):

int delaytime_us = 1;.

Die eben erstellte Funktion shiftByte() müssen wir zweimal aufrufen: einmal für den Befehl, den wir an das Grafik IC (MAX7219) senden wollen, einmal für dessen Parameter. Erst dann haben wir die 16 Bit geschrieben.

Im Anschluss an diese 16-Bit muss LOAD kurz auf HIGH gezogen werden, damit der Befehl verarbeitet wird. Eine Funktion, die Befehl und Parameter schreibt und dann per LOAD ausführt, wäre z.B.:

Ich habe den Namen der Funktion shiftBinaryWord(address, data) bewusst mit word gewählt. Als ein word wird im Microcontrollerumfeld die kleinste Einheit verstanden, die Prozessoren zur Adressierung verwenden. Bei einem 8-Bit MCU wie dem Arduino also 8 Bit (1 Byte). Im Sinne unserer 16 Bit-Nachrichten definiere ich ein word hier als eine Nachricht von der Länge 2 Byte: address+data.

Eine Funktion zum Testen des Displays

Nun ist es mühsam, die einzelnen Befehle einzeln aufzurufen. Daher sollten wir für alle Befehle, die unser MAX7219 bietet, eigene Funktionen schaffen, die jeweils den Befehl ausführen. Im Fall des Displaytests habe ich beispielsweise eine Funktion erstellt, die über den Parameter isTestOn den Displaytest einschaltet (testMatrix(true)) und ausschaltet (testMatrix(false)):

Relevante Befehle laut Datenblatt

Das Datenblatt verrät uns eine ganze Reihe von Befehlen, die wir in ähnlicher weise zu Funktionen zusammentragen sollten. Bevor wir damit anfangen könne, an die einzelnen Zeilen des Displays Bitmuster zu senden, sollten wir aber zunächst das Display in einen Zustand versetzten, in dem es Daten empfängt und korrekt darstellt.

Name Register-
adresse
Daten
Beispiele
Beschreibung
Zeile 1-8 xxxx0001
xxxx0010
xxxx0011
xxxx0100
xxxx0101
xxxx0110
xxxx0111
xxxx1000
00111100
01000010
10100101
10100101
10011001
10000001
01000010
00111100
Setzt das übertragene Bitmuster in die jeweilige Spalte (bei Decode off)
Helligkeit
Intensity
xxxx1010 xxxx0000: 0%
xxxx1111: 100%
Ausprobieren: die LEDs sind ganz schön hell, es ist augenschonen, die etwas zu dimmen.
Anzahl der genutzten Zeilen
Scan Limit
xxxx1011 xxxxx000: 1. Zeile
xxxxx111: 1.-8. Zeile
Wir wollen ja immer alle 8 Spalten anzeigen, das muss also entsprechend eingestellt werden
Ausschalten
Shutdown
xxxx1100 xxxxxxxx: shutdown
xxxxxxx1: normal
Display Test xxxx1111 xxxxxxx0: normal
xxxxxxx1: Test
Nur zum debuggen relevant - vielleicht beim Start kurz aktivieren, dann deaktivieren.
Matrix-Dummy
No-Op
xxxx0000 xxxxxxxx Dummy zum weiterschieben von 16 Bit im Schieberegister, falls mehr als
Decode Mode xxxx1001 00000000 off
11111111 on
Schaltet das Dekodieren von Zahlen für 7-Segment-Displays an/aus. (bei Dot-Matix: off)

Es müssen also eine Reihe von neuen Funktionen geschaffen werden, die diese Befehle ausführen. Diese Funktionen müssen dann am Ende der Setup-Funktion ausführt werden. Beispielsweise könnten die Aufrufe so aussehen (die Funktionen selbst müssen natürlich analog zu testMatrix() implementiert werden):

Ein Bitmuster auf das Display zaubern

Jetzt kommt der spannende Teil: in gleicher Weise, wie wir einzelne Befehle mit Daten an das Display gesendet haben, können wir jetzt auch Bitmuster für jede Spalte mit 8 LED senden. Da jede Spalte einen eigenen Befehl hat, könnten wir natürlich acht Funktionen erstellen. Dass das Blödsinn wäre, ist aber klar. Stattdessen wäre ein Befehl gut, dem wir nur das Bitmuster und die Spaltennnummer übergeben. Praktischerweise entspricht die Spaltennummer genau den Befehl, der die jeweilige Spalte beschreibt (0b00000010 = 2 schreibt die zweite Spalte).

Damit lässt sich jede Zeile schon einmal super beschreiben. Jetzt fehlt noch eine Funktion, die alle Spalten auf einmal beschreibt.

Hierzu müssen wir in die Array-Trickkiste greifen. Wir benötigen insgesamt acht Bitmuster à acht Bit. Eine einfache Struktur, die mehrere gleichartige Werte speichern kann, ist das Array.

Ein Array mit acht Byte kann also ein komplettes Bild auf dem Dot-Matrix-Display speichern. Ein Kreis sähe etwa so aus:

Auf die einzelnen Elemente des Arrays kann ich per Index zugreifen, wobei Arrays “Null-Index-basiert” sind, d.h. die Zählung der Indizes fängt bei 0 an. bitMuster[0] gibt also die erste Spalte, bitMuster[7] die letzte Spalte aus.

Jetzt fehlt nur noch eine Funktion, die dieses Array als Parameter übergeben bekommt und jedes Element des Arrays an unsere Funktion displayByte(...) mit aufsteigender Spaltennummer übergibt.

Vorsicht ist hier aber geboten: der Index der ersten Spalte im Array ist 0, der Befehl für die erste Spalte ist aber 0b00000001 (also 1). Die Indizes sind also immer um eins kleiner als die Spaltennummern.

Wenn das obige Array erstellt wurde, reicht schon ein einfacher Aufruf, um den Kreis auf dem Display auszugeben.

Fazit, wie geht’s weiter?

Es ist also kein Hexenwerk, mit Hilfe eines Datenblatts ein eigenes Programm zu schreiben, das ohne fremde Treiber auskommt.

Wie könnte es jetzt weiter gehen? Es liegt auf der Hand, jetzt eine eigene Schrift zu erstellen, Grafiken zu zeichnen und und und…

Irgendwann sollten wir nur den Absprung wieder bekommen, denn schließlich gibt es diese ganzen Bibliotheken bereits (eine Suche nach “Max7219” in den Arduino-Bibliotheken hat unzählige Treffer). Uns ging es hier ja nur darum, die Infos in Datenblättern zu entschlüsseln.

Eins haben wir dabei aus Versehen noch gelernt: Die Grundlagen für das SPI-Protokoll. Das schauen wir uns in einem nächsten Schritt mal genauer an…

Quellen und offene Ressourcen (OER)

Die Ursprungstexte (als Markdown), Grafiken und zugrunde liegende Diagrammquelltexte finden sich (soweit möglich in weiterbearbeitbarer Form) in folgendem git-Repository:

https://gitlab.com/oer-informatik/mcu/arduino-esp.

Sofern nicht explizit anderweitig angegeben sind sie zur Nutzung als Open Education Resource (OER) unter Namensnennung (H. Stein, oer-informatik.de) freigegeben gemäß der Creative Commons Namensnennung 4.0 International Lizenz (CC BY 4.0).

Creative Commons Lizenzvertrag

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: