Über den Beitrag
In diesem Beitrag möchte ich euch Methoden vorstellen, wie ihr ein LED Matrix Display (engl.: Dot Matrix Display) ansteuern könnt. Das ist natürlich nicht der erste Beitrag zu diesem Thema im Netz, aber vielleicht gibt es doch noch das eine oder andere Neue für euch zu entdecken. Im Einzelnen behandele ich die folgenden Themen:
- Was ist ein LED Matrix Display und wie funktioniert es?
- Direkte Ansteuerung mit einem Arduino
- Ansteuerung über den MAX7219 / MAX7221 (mit Bibliothek)
- Verwendung von Displays mit integriertem MAX7219
- Programmierung einer Laufschrift
- Ansteuerung über den MAX7219 / MAX7229 ohne Bibliothek
- Anhang: Ansteuerung eines zweifarbigen, TA6932-basierten Matrix Displays
Als Appetizer hier schon mal ein kleines Video:
Was ist ein LED Matrix Display und wie funktioniert es?
Ein LED Matrix Display ist ein LED Dioden Array. Jeweils ein Eingangs- bzw. Ausgangspin verbindet die LEDs einer Reihe (Row) bzw. einer Spalte (Column). Ob die Reihen oder die Spalten die Ein- oder Ausgänge sind, variiert von Display zu Display.
Durch Anlegen einer Spannung an einen Eingangspin und durch Verbinden eines Ausgangspins mit Ground bringt ihr genau eine LED am Schnittpunkt zum Leuchten.
Wenn ihr keinen Ansteuerungs-IC wie den MAX7219 oder MAX7221 verwendet, dann müsst ihr Vorwiderstände einsetzen. Die benötigte Größe hängt von den technischen Daten eurer Displays ab. Meistens liegt ihr aber bei 5 Volt mit 330 Ohm schon richtig.
Noch eine Anmerkung zum Schema oben: normalerweise findet man in Datenblättern die Reihen- und Spaltennummern von 1 bis 8. Gewöhnt euch lieber die Nummerierung 0 bis 7 an. Denn dann denkt ihr gleich „richtig“ in Bits und Bytes und das ist bei der Ansteuerung hilfreich.
Und noch etwas: wenn ich eine LED Position (einen Dot) in diesem Beitrag als Wertepaar (a/b) angebe, dann ist a die Reihe und b die Spalte. Also nicht wie x und y im Koordinatensystem.
Woher bekommt Ihr LED Matrix Displays?
LED Matrix Displays gibt es in vielen verschiedenen Ausfertigungen. Dabei variiert die Anzahl der LEDs (Dots), die Größe der Displays und die Farbe der LEDs. Darüber hinaus gibt es sie mit integriertem Ansteuerungs-IC und teilweise auch im Verbund, z.B. als 8 x 8 x 4. Sucht in dem Online-Shop eures Vertrauens nach „LED Matrix Display“ oder „Dot Matrix“. Die Preise variieren recht stark mit der Herkunft und der Menge.
Direkte Ansteuerung per Microcontroller oder Arduino
Ich verwende als Beispiel das oben schematisch abgebildete 8×8 LED Matrix Display mit Reihen-Eingang und Spalten-Ausgang. Vorab: die Verdrahtung macht keinen Spaß, da acht Eingänge, acht Ausgänge und acht Widerstände verbunden werden müssen. Außerdem sind die Reihen und Spalten den Pins ohne erkennbares System zugeordnet. Da müsst ihr ziemlich viel suchen. Teilweise ist auch nicht offensichtlich welches der Pin 1 ist. In diesem Fall müsst ihr erstmal ein wenig ausprobieren.
Der ganze erste Abschnitt dient denn auch mehr der Didaktik als der Praxis. Ansteuerungs-ICs wie der MAX7219 machen das Leben leichter. Und noch komfortabler sind die Displays mit integrierter Ansteuerung. Da komme ich dann später zu. Trotzdem sollte man erstmal das Prinzip verstanden haben.
Beschaltung
Bei 16 benötigten Ein- und Ausgängen müssen auf dem Arduino auch die analogen Pins mit einbezogen werden. Oder ihr verwendet eine Portweiterung wie den MCP23017.
So sieht die Beschaltung schematisch und praktisch aus:
Ein kleiner Beispielsketch
Im folgenden Beispielsketch werden zunächst die Arduino Pins für die Reihen und Spalten definiert. Die Eingänge (Reihen) werden auf LOW gesetzt, die Ausgänge (Spalten) auf HIGH. Damit sind alle LEDs aus. Eine LED an der Stelle „r/c“ wird angeschaltet, wenn die Reihe „r“ auf HIGH gesetzt wird und die Spalte „c“ auf LOW.
Als erste kleine Spielerei gibt es ein Lauflicht. Dabei geht jede LED einmal kurz an. Dann wird die Diagonale von (0/0) bis (7/7) zum Leuchten gebracht.
#define r0 2 // pin for row 0#define r1 3 // pin for row 1#define r2 4 // ...#define r3 5#define r4 6#define r5 7#define r6 8#define r7 9#define c0 10 // pin for column 0#define c1 11 // pin for column 1#define c2 12 // ...#define c3 13#define c4 A0#define c5 A1#define c6 A2#define c7 A3int row[] = {r0, r1, r2, r3, r4, r5, r6, r7};int column[] = {c0, c1, c2, c3, c4, c5, c6, c7}; void setup() { for(int i = 0; i<=7; i++){ pinMode(row[i], OUTPUT); } for(int i = 0; i<=7; i++){ pinMode(column[i], OUTPUT); } clearDisplay();}void loop() { for(int i=0; i<=7; i++){ for(int j=0; j<=7; j++){ switchLED(row[i],column[j],1); delay(100); switchLED(row[i],column[j],0); } }}void clearDisplay(){ for(int i = 0; i<=7; i++){ digitalWrite(row[i],LOW); } for(int i = 0; i<=7; i++){ digitalWrite(column[i], HIGH); } }void switchLED(int r, int c, bool ON){ if(ON){ digitalWrite(r, HIGH); digitalWrite(c, LOW); } else{ digitalWrite(r, LOW); digitalWrite(c, HIGH); }}
Unerwünschte Querbeeinflussung
Ersetzt im letzten Sketch einmal die Sketch Hauptschleife durch die folgende:
void loop() { switchLED(row[0],column[0],1); delay(1000); switchLED(row[0],column[1],1); delay(1000); switchLED(row[1],column[0],1); delay(1000); clearDisplay();}
Eigentlich soll der Sketch die Dots (0/0), (0/1) und (1/0) anschalten. Das tut er zwar auch, aber mit dem Dot (1/0) wird auch (1/1) angeschaltet. Falls euch nicht klar sein sollte, warum das so ist, schaut dazu noch einmal in das Schema des 8×8 Displays. Dann sollte die Problematik offensichtlich sein.
Die Lösung: Multiplexing
Ersetzt nun noch einmal die Hauptschleife durch die folgende:
void loop() { switchLED(row[0],column[0],1); delay(1); switchLED(row[0],column[0],0); switchLED(row[0],column[1],1); delay(1); switchLED(row[0],column[1],0); switchLED(row[1],column[0],1); delay(1); switchLED(row[1],column[0],0);}
In diesem Fall leuchten tatsächlich nur die Dots (0/0), (0/1) und (1/0), da jeder Dot separat für kurze Zeit angeschaltet wird. Ein Flackern sieht man nicht, dafür ist der Wechsel zu schnell.
Die Grenzen des einfachen Multiplexings
Zunehmendes Multiplexing in dieser Form führt zu einer Abnahme der Intensität. Im folgenden Beispiel ist der Dot (7,7) immer an, der Rest der Diagonale ist „gemultiplext“. Dass der Rest von Reihe und Spalte 7 auch leuchten, ist ein Nebeneffekt.
Zu beachten: Die Sketchzeilen 1 und 2 gehören noch ins Setup.
switchLED(row[7],column[7], 1);}void loop() { for(int i=0; i<=6; i++){ switchLED(row[i],column[i],1); delay(1); switchLED(row[i],column[i],0); }}
Auf dem Foto erkennt man die Unterschiede nicht so gut wie in der Realität. LEDs sind nicht einfach zu fotografieren. Dennoch könnt ihr vielleicht erahnen, dass der Dot (7,7) heller ist.
Auch für dieses Problem gibt es eine Lösung. Über Pulsweitenmodulation (PWM, siehe mein Beitrag über Timer) lässt sich die Intensität je nach Anzahl der „gemultiplexten“ LEDs gewichten. Darauf gehe ich aber jetzt nicht näher ein und möchte die bequemere und allgemein übliche Lösung vorstellen.
Ansteuerung mit dem MAX7219 / MAX7221
Alle eben genannten Probleme lassen sich mit den Ansteuerungs-ICs MAX7219 oder MAX7221 lösen. Da beide fast identisch sind, nenne ich ab jetzt nur den MAX7219, meine aber beide.
Primär ist der MAX7219 für die Ansteuerung von 7-Segment Displays konzipiert. Er lässt sich aber auch für die Ansteuerung von LED Matrix Displays oder losen LED Arrangements verwenden. Das ist auch nicht weiter verwunderlich, da die Ansteuerung mehrerer 7-Segment Anzeigen im Grunde dieselbe Herausforderung wie die Ansteuerung eines LED Matrix Displays darstellt.
Pinout und technische Eigenschaften des MAX7219
Der MAX7219 besitzt 24 Pins:
- Die Pins SEG X liefern die positive Spannungsversorgung
- Ob diese mit den Reihen- oder Spaltenpins verbunden werden, hängt davon ab, wie herum die LEDs im Display verbaut sind.
- Die Pins DIG X schalten nach GND.
- Der MAX7219 wird per SPI angesteuert:
- DIN ist der Data Input, also MOSI (Master Out, Slave In).
- Ein MISO (Master In, Slave Out) gibt es nicht; es ist also eine One-Way Kommunikation.
- CLK: Clock Pin.
- LOAD / CS: Chip Select Pin.
- Wenn ihr mehrere Displays verwendet, wird DOUT mit dem DIN des nächsten Displays verbunden.
- An VCC dürfen 4 bis 5.5 Volt verwendet werden
- An ISET wird die Höhe des Stroms für die LEDs eingestellt. Ich habe 10 kOhm verwendet. Im Datenblatt des MAX7219 gibt es eine grafische Darstellung, welcher Strom mit welchem Widerstand bei welcher Spannung bereitgestellt wird.
Verwendung der Bibliothek LedControl
Zur Ansteuerung des MAX7219 verwende ich die Bibliothek LedControl von Eberhard Fahle, die ihr von Github oder direkt über die Bibliotheksverwaltung der Arduino IDE installieren könnt. Eine sehr gute Einführung gibt es hier.
Die Bibliothek erwartet eigentlich den Spannungseingang an den Spalten und den Spannungsausgang an den Reihen. Das ist bei dem von mir verwendeten Modell anders herum. Man kann einfach entsprechend andersherum verkabeln (Segx an die Reihenpins, Digx an die Spaltenpins), muss dann aber nachher noch etwas umdenken.
Der MAX7219 besitzt nicht sonderlich viele Register. Deshalb lässt er sich auch ohne all zu viel Aufwand ohne Bibliothek ansteuern. Wie das geht, zeige ich am Ende dieses Beitrages.
Was die Komplexität der Verkabelung des LED Matrix Displays angeht, bringt der MAX7219 noch keinen großen Vorteil, aber immerhin braucht ihr die Vorwiderstände nicht:
Zu beachten ist, dass die oben abgebildete Verkabelung spezifisch für das von mir verwendete LED Matrix Display ist. Vielleicht sind die Anschlüsse eures Displays gleich, vielleicht aber auch nicht.
Ein kleiner Beispielsketch
#include <LedControl.h>//12= data pin(DIn), 11= CLK Pin, 10= Load/CS Pin, 1 = num of devicesLedControl lc88=LedControl(12,11,10,1); void setup(){ lc88.shutdown(0,false); // Wake up! 0= index of first device; lc88.setIntensity(0,2); lc88.clearDisplay(0); delay(500);}void loop(){ for(int row=0; row<=7; row++){ lc88.setLed(0,row,0,true); delay(250); } for(int col=0; col<=7; col++){ lc88.setLed(0,0,col,true); delay(250); } delay(500); lc88.clearDisplay(0); delay(2000); }
An diesem Beispiel sollten die meisten Funktionen der Bibliothek schon klar werden:
- zunächst wird ein Display Objekt erzeugt und dabei die Anschluss Pins und die Zahl der Einzeldisplays festgelegt
shutdown(display, false)
weckt das Display; mit true geht es schlafensetIntensity(display, value)
legt die Helligkeit der Dots fest mit 0 <= value < 16clearDisplay(display)
schaltet alle Dots eines Displays aussetLed(display, row, col, true/false)
schaltet genau einen Dot in Reihe row und Spalte col an oder aus.
Weitere nützliche Funktionen, die ihr selber ausprobieren könnt, sind:
setRow(display, row, val)
– die Reihe row eines Displays wird mit dem Wert val belegt, z.B. 0b11110000 -> die ersten vier LEDs sind an, die letzten vier aussetColumn(display, col, val)
– wie setRow, nur eben zum Setzen von Spalten- diese Funktion ist erheblich langsamer als die setRow-Funktion, da sie die setLed-Funktion (achtmal pro setColumn) verwendet.
Da ich bei meinem LED Matrix Display die SEG und die DIG Anschlüsse vertauschen musste, sind auch Reihen und Spalten vertauscht. Eigentlich sollte der Sketch erst die Spalte 0 und dann die Reihe 0 füllen. Offensichtlich ist es anders herum:
Auf dem Bild oben seht ihr, dass die obere Reihe schon komplett leuchtet und nun die linke Spalte vervollständigt wird.
Eine Lösung lautet, das Display einfach um 90° gegen den Uhrzeigersinn zu drehen. Dann ist der Dot (0/0) unten links (wie in einem Koordinatenkreuz) und nicht oben links. Ihr müsst dann bei der Zeilennummerierung umdenken.
Verwendung von Displays mit integriertem MAX7219
8x8x1 LED Matrix Display
Wer es bequem haben möchte, dem rate ich zur Verwendung von Displays mit integriertem MAX7219. Die Verkabelung solcher Teile sieht schon deutlich angenehmer aus:
Wo der Nullpunkt (0/0) liegt, kann unter Umständen überraschend sein. Probiert es aus. Dieses LED Matrix Display macht es wie erwartet:
Hinweis: wenn ihr alle LEDs eines 8×8 Matrix Displays gleichzeitig leuchten lasst, dann kann der Strombedarf mehrere 100 mA pro Display betragen. Bei Verwendung mehrerer Displays könnt ihr dann sehr locker die Limits des Arduinos überschreiten. Mehr als 500 mA solltet ihr einem Arduino UNO bzw. eurem USB Anschluss nicht zumuten. Setzt ggf. eine separate Stromquelle ein.
8x8x4 LED Matrix Display
Wollt ihr mehrere Displays aneinanderhängen, kommt für euch vielleicht ein Verbunddisplay infrage. Vor allem sind 8x8x4 Displays weit verbreitet. Auch hier seid ihr unter Umständen überrascht, wo sich der Dot (0,0) befindet. Zum Testen habe ich den folgenden kurzen Sketch verwendet:
#include "LedControl.h"LedControl lc884=LedControl(12,11,10,4);unsigned long delaytime=3000;void setup() { for(int i=0;i<4;i++){ lc884.shutdown(i,false); lc884.setIntensity(i,8); lc884.clearDisplay(i); }}void loop() { lc884.setRow(0,0,B01111111); lc884.setRow(1,1,B00111111); lc884.setRow(2,2,B00011111); lc884.setRow(3,3,B00001111); }
Eigentlich hätte ich gerne das Display 0 links und das Display 4 rechts. Außerdem hätte ich gerne den Nullpunkt oben links und eine Reihenabbildung, die der Reihenfolge der Bits entspricht. D.h. ein 0b00001111 soll vier Dots einer Reihe rechts leuchten lassen und nicht links. Ich habe zwei Displays aus unterschiedlichen Quellen getestet und mit beiden konnte ich nicht alle Wünsche erfüllen. Mit einer umgekehrten Displayreihenfolge konnte ich am ehesten leben:
Display 0 ist hier rechts, dafür ist aber der Rest so wie ich es will. Diese Anordnung verwende ich im Folgenden für die Entwicklung der Laufschrift. Wenn euer Display die Dinge anders abbildet, dann müsst ihr die Sketche entsprechend anpassen.
Programmierung einer Laufschrift (Banner)
Vorbereitung: Kreieren und Darstellen von Buchstaben
Buchstaben, Zahlen und Zeichen sind auf einem Blatt kariertem Papier schnell entwickelt. Dann schreibt ihr das Ergebnis zeilenweise im Byteformat in Arrays. Im folgenden Sketch seht ihr Beispiele für A,r,d,u,i,n und o. Ich habe die Buchstaben recht schmal gemacht, da ich sie später noch „zusammenschieben“ möchte, sodass der ganze Arduino Schriftzug auf einmal auf das Display passt.
Die Buchstaben werden zunächst nacheinander auf dem Display 0 dargestellt. Dann machen wir den ersten Schritt in Richtung Laufschrift, indem wir die Buchstaben Display-weise von rechts nach links durchlaufen lassen. Mein Bezugsdisplay ist dabei das nicht sichtbare, virtuelle Display -1.
#include "LedControl.h"LedControl lc884=LedControl(12,11,10,4);unsigned long delayTime=500;byte a[8]={B00000000,B01100000,B10010000,B10010000,B10010000,B11110000,B10010000,B10010000};byte r[8]={B00000000,B00000000,B00000000,B10100000,B11010000,B10000000,B10000000,B10000000};byte d[8]={B00000000,B00010000,B00010000,B00010000,B01110000,B10010000,B10010000,B01110000};byte u[8]={B00000000,B00000000,B00000000,B10010000,B10010000,B10010000,B10010000,B01110000};byte i[8]={B00000000,B00000000,B01000000,B00000000,B01000000,B01000000,B01000000,B01000000};byte n[8]={B00000000,B00000000,B00000000,B10100000,B11010000,B10010000,B10010000,B10010000};byte o[8]={B00000000,B00000000,B00000000,B01100000,B10010000,B10010000,B10010000,B01100000};void setup() { for(int i=0;i<4;i++){ lc884.shutdown(i,false); lc884.setIntensity(i,8); lc884.clearDisplay(i); }}void loop() { oneMatrix(); fourMatrices(); delay(1000);}void oneMatrix(){ displayCharAndWait(a,0); displayCharAndWait(r,0); displayCharAndWait(d,0); displayCharAndWait(u,0); displayCharAndWait(i,0); displayCharAndWait(n,0); displayCharAndWait(o,0);}void displayCharAndWait(byte* x, byte displayNumber){ lc884.clearDisplay(displayNumber); for(int j=0; j<=7;j++){ lc884.setRow(displayNumber,j,x[j]); } delay(delayTime); lc884.clearDisplay(displayNumber); }void fourMatrices(){ for(int j=0; j<=10; j++){ int currentMatrix = -1; if( ((currentMatrix+j)>=0) && ((currentMatrix+j)<4) ){ displayChar(a,currentMatrix+j); } currentMatrix--; if( ((currentMatrix+j)>=0) && ((currentMatrix+j)<4) ){ displayChar(r,currentMatrix+j); } currentMatrix--; if( ((currentMatrix+j)>=0) && ((currentMatrix+j)<4) ){ displayChar(d,currentMatrix+j); } currentMatrix--; if( ((currentMatrix+j)>=0) && ((currentMatrix+j)<4) ){ displayChar(u,currentMatrix+j); } currentMatrix--; if( ((currentMatrix+j)>=0) && ((currentMatrix+j)<4) ){ displayChar(i,currentMatrix+j); } currentMatrix--; if( ((currentMatrix+j)>=0) && ((currentMatrix+j)<4) ){ displayChar(n,currentMatrix+j); } currentMatrix--; if( ((currentMatrix+j)>=0) && ((currentMatrix+j)<4) ){ displayChar(o,currentMatrix+j); } delay(delayTime); for(int i=0; i<=4; i++){ lc884.clearDisplay(i); } } }void displayChar(byte *x, int displayNumber){ lc884.clearDisplay(displayNumber); for(int j=0; j<=7;j++){ lc884.setRow(displayNumber,j,x[j]); }}
Wie das im Ergebnis aussieht, könnt ihr in dem Video zu Beginn des Beitrages sehen. Hier ein Ausschnitt:
Ein Zwischenschritt: statische Darstellung als unsigned long Array
Das Durchlaufen „Display für Display“ war einfach. Aber es ist auch noch nicht besonders schick. Schöner wäre ein Verschieben „Dot für Dot“. Zugegebenermaßen habe ich mich damit schwerer getan als ich dachte. Ich zeige ein paar Lösungen. Vielleicht denke ich auch zu kompliziert – wenn ihr einfachere Methoden habt, immer her damit!
Für meinen Lösungsweg Nr. 1 kommt hier erst einmal ein Zwischenschritt. Zunächst habe ich die Buchstaben so zusammengefasst, dass sie auf das Viererdisplay passen. Dazu habe ich die Zwischenräume auf ein Mindestmaß reduziert und alle 0ten, 1sten, 2ten….7ten Reihen in jeweils einem unsigned long Wert zusammengefasst. Da eine unsigned long Variable 32 bits hat, entspricht das der Breite des 4er Displays. Bei der späteren Darstellung auf dem Display muss man den Wert dann allerdings wieder in bytes „aufdröseln“. Das macht die Funktion displayBanner()
.
#include "LedControl.h"LedControl lc884=LedControl(12,11,10,4);long delayTime=500;unsigned long banner[8]={0b00000000000000000000000000000000, 0b01100000000001000000000000000000, 0b10010000000001000000100000000000, 0b10010101000001010010001010001100, 0b10010110100111010010101101010010, 0b11110100001001010010101001010010, 0b10010100001001010010101001010010, 0b10010100000111001110101001001100}; void setup() { for(int i=0;i<4;i++){ lc884.shutdown(i,false); lc884.setIntensity(i,8); lc884.clearDisplay(i); }}void loop() { displayBanner(); delay(500);}void displayBanner(){ byte currentMatrix[8]; for(int j=0; j<4; j++){ for(int i=0; i<8; i++){ currentMatrix[i] = (((banner[i])>>(j*8)) & 0b11111111); } displayMatrix(currentMatrix,j); }}void displayMatrix(byte *matrix, int matrixNumber){ for(int i=0; i<=7;i++){ lc884.setRow(matrixNumber,i,matrix[i]); }}
Und so sieht das Ergebnis aus:
Das unsigned long Array dotweise laufen lassen
Der Vorteil der Darstellung als unsigned long array ist, dass die Verschiebung wesentlich leichter über Binäroperationen zu programmieren ist als Byte für Byte. Das Banner wird schrittweise nach links verschoben und auf die Einzeldisplays aufgeteilt. Beim Nachvollziehen des Sketches vergesst nicht, dass sich das Display 0 rechts befindet.
Als kleines Feature habe ich noch eingebaut, dass das Banner kurz stoppt, wenn es vollständig auf dem Display ist. Es wird kurz heller, dann wieder dunkler und läuft dann aus dem Display. Das Ergebnis seht ihr im Video zu Beginn des Beitrages.
#include "LedControl.h"LedControl lc884=LedControl(12,11,10,4);long delayTime=150;unsigned long banner[8]={0b00000000000000000000000000000000, 0b01100000000001000000000000000000, 0b10010000000001000000100000000000, 0b10010101000001010010001010001100, 0b10010110100111010010101101010010, 0b11110100001001010010101001010010, 0b10010100001001010010101001010010, 0b10010100000111001110101001001100}; void setup() { for(int i=0;i<4;i++){ lc884.shutdown(i,false); lc884.setIntensity(i,8); lc884.clearDisplay(i); }}void loop() { calcCurrentBanner(); delay(500);}void calcCurrentBanner(){ unsigned long currentBanner[8]; for(int i=32; i>=0; i--){ for(int j=0; j<8; j++){ currentBanner[j] = (banner[j])>>i; } displayBanner(currentBanner); delay(delayTime); } stopAndHighlight(); for(int i=0; i<33; i++){ for(int j=0; j<8; j++){ currentBanner[j] = (banner[j])<<i; } displayBanner(currentBanner); delay(delayTime); }}void displayBanner(unsigned long *cb){ byte currentMatrix[8]; for(int j=0; j<4; j++){ for(int i=0; i<8; i++){ currentMatrix[i] = (((cb[i])>>(j*8)) & 0b11111111); } displayMatrix(currentMatrix,j); }}void displayMatrix(byte *matrix, int matrixNumber){ for(int i=0; i<=7;i++){ lc884.setRow(matrixNumber,i,matrix[i]); }}void stopAndHighlight(){ for(int i=8; i<16; i++){ for(int j=0;j<4;j++){ lc884.setIntensity(j,i); delay(20); } } delay(1000); for(int i=15; i>=8; i--){ for(int j=0;j<4;j++){ lc884.setIntensity(j,i); delay(20); } }}
Gößere Banner laufen lassen
Nun passt das, was ihr vielleicht als Banner über das Display laufen lassen wollt, nicht unbedingt auf das Display bzw. in ein unsigned long Array. Aber das geht auch. Als Beispiel nehme ich den ungekürzten Arduino Schriftzug, der sich über sieben Einzeldisplays erstreckt. Diesen verteile ich in ein zweidimensionales unsigned long Array „bannerPart[2][8]“. Dabei verschwende ich natürlich ein wenig Speicherplatz, da ich das Array nicht ganz nutze.
Die Übernahme der einzelnen Buchstaben in das Array bannerPart habe ich diesmal automatisiert. Das geschieht im Setup in den ersten beiden for-Schleifen. In calcCurrentBanner()
finden sich drei for-Schleifen, die für drei Phasen stehen. In der ersten Phase wird das erste Bannerteil hineingeschoben. Während der zweiten Phase geht das erste Teil hinaus und die frei werdenden Bereiche werden mit dem zweiten Bannerteil aufgefüllt. In der dritten Phase wird das zweite Bannerteil hinausgeschoben.
#include "LedControl.h"LedControl lc884=LedControl(12,11,10,4);long delayTime=150;byte banner[7][8]={{B00000000,B01100000,B10010000,B10010000,B10010000,B11110000,B10010000,B10010000}, {B00000000,B00000000,B00000000,B10100000,B11010000,B10000000,B10000000,B10000000}, {B00000000,B00010000,B00010000,B00010000,B01110000,B10010000,B10010000,B01110000}, {B00000000,B00000000,B00000000,B10010000,B10010000,B10010000,B10010000,B01110000}, {B00000000,B00000000,B01000000,B00000000,B01000000,B01000000,B01000000,B01000000}, {B00000000,B00000000,B00000000,B10100000,B11010000,B10010000,B10010000,B10010000}, {B00000000,B00000000,B00000000,B01100000,B10010000,B10010000,B10010000,B01100000}};unsigned long bannerPart[2][8];void setup(){ unsigned long b1, b2, b3; for(int i=0; i<8; i++){ unsigned long b1, b2, b3; b1 = ((unsigned long)(banner[0][i]))<<24; b2 = ((unsigned long)(banner[1][i]))<<16; b3 = ((unsigned long)(banner[2][i]))<<8; bannerPart[0][i] = b1 + b2 + b3 + banner[3][i]; } for(int i=0; i<8; i++){ unsigned long b1, b2, b3; b1 = ((unsigned long)(banner[4][i]))<<24; b2 = ((unsigned long)(banner[5][i]))<<16; b3 = ((unsigned long)(banner[6][i]))<<8; bannerPart[1][i] = b1 + b2 + b3; } for(int i=0;i<4;i++){ lc884.shutdown(i,false); lc884.setIntensity(i,8); lc884.clearDisplay(i); }}void loop() { calcCurrentBanner(); delay(500);}void calcCurrentBanner(){ unsigned long currentBanner[8]; for(int i=32; i>=0; i--){ for(int j=0; j<8; j++){ currentBanner[j] = (bannerPart[0][j])>>i; } displayBanner(currentBanner); delay(delayTime); } for(int i=1; i<=32; i++){ for(int j=0; j<8; j++){ currentBanner[j] = ((bannerPart[0][j])<<i) + ((bannerPart[1][j])>>(32-i)); } displayBanner(currentBanner); delay(delayTime); } for(int i=1; i<=24; i++){ for(int j=0; j<8; j++){ currentBanner[j] = (bannerPart[1][j])<<i; } displayBanner(currentBanner); delay(delayTime); }}void displayBanner(unsigned long *cb){ byte currentMatrix[8]; for(int j=0; j<4; j++){ for(int i=0; i<8; i++){ currentMatrix[i] = (((cb[i])>>(j*8)) & 0b11111111); } displayMatrix(currentMatrix,j); }}void displayMatrix(byte *matrix, int matrixNumber){ for(int i=0; i<=7;i++){ lc884.setRow(matrixNumber,i,matrix[i]); }}
Das Ergebnis findet ihr wieder im Video.
Wenn ihr eigene Banner mit einer anderen Breite kreiert, dann müsst ihr einige Werte im Sketch anpassen. Ich hatte offengestanden keine Lust mehr den Sketch zu verallgemeinern.
Weitere Methoden
Das Verschieben mittels Byte Arrays ist etwas komplizierter als man zunächst meinen sollte. Zumindest habe ich mich damit ein wenig schwergetan. Ihr findet den Sketch am Schluss des Beitrages.
Die mit Abstand einfachste Methode wäre ein bool Array, in dem jeder Dot eine separate Variable ist. In dieses Array könnte man auch die vier leeren Displays zu Beginn und die vier leeren Displays am Ende integrieren. Macht 4 + 7 (für das Arduino Banner) + 4 = 15 Einzeldisplays. Daraus würde ein 120 x 8 Array mit 960 bool Werten entstehen. Das Durchlaufen des Banners wäre so sehr einfach zu programmieren. Aber leider belegt jede bool Variable ein ganzes Byte. Diese Verschwendung von Speicherplatz ist mir dann doch zu groß.
LED Matrix Display Ansteuerung ohne MAX7219 Bibliothek
Wie schon erwähnt, hat der MAX7219 nicht übermäßig viele Register und kann deswegen recht leicht auch ohne Bibliothek angesteuert werden. Im nächsten Sketch könnt ihr sehen wie das geht.
Ich möchte den Sketch aber auch nicht im Detail erläutern, da der Beitrag sowieso schon so lang ist. Wenn ihr euch das Datenblatt danebenlegt, sollte er nicht so schwer zu verstehen sein. Wenn ihr trotzdem Fragen habt, fragt! Nur ein paar Anmerkungen:
- Der Sketch verwendet die Standard SPI Anschlüsse für MOSI und CLK (beim UNO Pin 11 bzw. 13). Ihr müsst also ein wenig umstöpseln.
- MISO gibt es nicht, der MAX7219 ist nicht gesprächig.
- Da es kein MISO gibt, lässt sich der Zustand der Dots nicht abfragen.
- Da man bei der setLed Funktion nur einzelne Dots ändern möchte, aber nur ganze Reihen Bytes schreiben kann, muss der Sketch sozusagen Buch führen. Das macht er über das Array dots.
#include <SPI.h>const int slaveSelectPin = 10;const int totalDevs = 4;const int colsPerDev = 8;const int rowsPerDev = 8;byte dots[totalDevs][rowsPerDev] = {0}; // in diesem Array wird der Zustand der Dots gespeichertbyte a[8]={B00000000,B01100000,B10010000,B10010000,B10010000,B11110000,B10010000,B10010000};void setup(){ SPI.begin(); pinMode(slaveSelectPin, OUTPUT); digitalWrite(slaveSelectPin, HIGH); for(int device=0; device<totalDevs; device++){ wakeUp(device); // Normal Operation (shutdown beenden) displayTest(device,500); writeRegister(device,0x09,0x00); // Decode: 00 = Einzeldots, kein decoding setIntensity(device, 6); writeRegister(device,0x0B,0x07); // alle Reihen werden angezeigt clearDevice(device); } delay(500);}void loop(){ setRow(0,0,B11111000); delay(1000); setLED(0,0,5,0); delay(1000); setLED(0,0,7,0); delay(1000); displayChar(1, a);} void clearDevice(int device){ for(int row=0; row<=rowsPerDev; row++){ //alles ausschalten writeRegister(device,byte(row),0x00); }}void setIntensity(int device, int intensity){ writeRegister(device,0x0A,intensity);}void displayTest(int device, int testTime){ writeRegister(device,0x0F,0x01); //Display Test ein delay(testTime); writeRegister(device,0x0F,0x00); //Display Test aus}void wakeUp(int device){ writeRegister(device,0x0C,0x01);}void shutDown(int device){ writeRegister(device,0x0C,0x00);}void setRow(int device, byte row, byte data){ dots[device][row] = data; writeRegister(device, row+1, data); }void setLED(int device, byte row, byte col, bool on){ byte reg = row; byte dataToSend; byte mask; if(on){ mask = (1<<(col)); dots[device][row] |= mask; } else{ mask = ~(1<<(col)); dots[device][row] &= mask; } dataToSend = dots[device][row]; writeRegister(device, reg+1, dataToSend);}void displayChar(int device, byte *character){ for(int i=0; i<=7; i++){ dots[device][i] = character[i]; writeRegister(device,i+1,character[i]); }}void writeRegister(int device, byte reg, byte data){ digitalWrite(slaveSelectPin, LOW); for(int i=0; i<(totalDevs-device); i++){ SPI.transfer(0x00); SPI.transfer(0x00); } SPI.transfer(reg); SPI.transfer(data); for(int i=0; i<device; i++){ SPI.transfer(0x00); SPI.transfer(0x00); } digitalWrite(slaveSelectPin, HIGH);}
Zu guter Letzt…
…noch der Banner Sketch auf Basis der Byte Arrays. Hier habe ich als Denkmodell ein virtuelles Display verwendet, welches das physikalische Display quasi von rechts nach links durchwandert (siehe Schema unten). Ein bisschen wie ein Filmstreifen, der im Projektor hinter dem Objektiv entlang läuft.
Ich habe den Sketch so verfasst, dass ihr ihn leicht auf andere Displaygrößen anpassen könnt. Ich habe ihn z.B. auf zwei hintereinandergeschalteten 8x8x4 Matrix Display laufen lassen (dazu musste lediglich die numberOfPhysicalDisplays von vier auf acht geändert werden:
Der Sketch ist recht kompakt, aber einigermaßen schwer „verdaulich“. Ich habe versucht, ihn durch Kommentare halbwegs verständlich zu gestalten. Viel Spaß beim Nachvollziehen meiner Logik!
#include "LedControl.h"const unsigned int delayTime = 150;const int numberOfCharacters = 7; // = A,r,d,u,i,n,oconst int numberOfPhysicalDisplays = 4; // 4 Matrix Displaysconst int displayWidth = 8; // 8x8 Formatconst int displayHeight = 8;int numberOfSteps; // Anzahl Verschiebeschritteint numberOfVirtualDisplays; // Anzahl virtuelle Displays LedControl lc884=LedControl(12,11,10,numberOfPhysicalDisplays);byte banner[7][8]={{B00000000,B01100000,B10010000,B10010000,B10010000,B11110000,B10010000,B10010000}, {B00000000,B00000000,B00000000,B10100000,B11010000,B10000000,B10000000,B10000000}, {B00000000,B00010000,B00010000,B00010000,B01110000,B10010000,B10010000,B01110000}, {B00000000,B00000000,B00000000,B10010000,B10010000,B10010000,B10010000,B01110000}, {B00000000,B00000000,B01000000,B00000000,B01000000,B01000000,B01000000,B01000000}, {B00000000,B00000000,B00000000,B10100000,B11010000,B10010000,B10010000,B10010000}, {B00000000,B00000000,B00000000,B01100000,B10010000,B10010000,B10010000,B01100000}};void setup(){ numberOfVirtualDisplays = numberOfPhysicalDisplays + numberOfCharacters; // das virtuelle Gesamtdisplay besteht aus 4 leeren Displays + Anzahl der Zeichen numberOfSteps = numberOfVirtualDisplays * displayWidth; for(int i=0;i<numberOfPhysicalDisplays; i++){ lc884.shutdown(i,false); lc884.setIntensity(i,8); lc884.clearDisplay(i); }}void loop(){ for(int i=0; i<= numberOfSteps; i++){ calcVisibleBannerPart(i); }}void calcVisibleBannerPart(int step){ int currentFirstVirtualDisplay = step/displayWidth; // Erstes virtuelles Display, dass auf dem physikalischen Display abgebildet wird int bitPosition = step % displayWidth; // Bit-Position, an der die virtuellen Displays auf zwei benachbarte physikalische Displays verteilt werden byte matrixToDisplay[8]; // abzubildende Matrix for(int currentPhysicalDisplay=0; currentPhysicalDisplay<numberOfPhysicalDisplays; currentPhysicalDisplay++){ // Gehe die physikalischen Displays durch if( (currentFirstVirtualDisplay + currentPhysicalDisplay) == (numberOfPhysicalDisplays - 1)){ for(int i=0; i < displayHeight; i++){ matrixToDisplay[i] = banner[0][i] >> (8 - bitPosition); } displayMatrix(matrixToDisplay,(numberOfPhysicalDisplays-1 - currentPhysicalDisplay)); // mein 0tes Display ist rechts! } if( ((currentFirstVirtualDisplay + currentPhysicalDisplay) > (numberOfPhysicalDisplays - 1)) && (currentFirstVirtualDisplay + currentPhysicalDisplay) < (numberOfCharacters + numberOfPhysicalDisplays - 1) ){ for(int i=0; i < displayHeight; i++){ matrixToDisplay[i] = (((banner[currentFirstVirtualDisplay + currentPhysicalDisplay - numberOfPhysicalDisplays][i]) << bitPosition) + ((banner[currentFirstVirtualDisplay + currentPhysicalDisplay - numberOfPhysicalDisplays+1][i]) >> (8- bitPosition))) ; } displayMatrix(matrixToDisplay,(numberOfPhysicalDisplays-1 - currentPhysicalDisplay)); } if( (currentFirstVirtualDisplay + currentPhysicalDisplay) == (numberOfCharacters + numberOfPhysicalDisplays-1)){ for(int i=0; i < displayHeight; i++){ matrixToDisplay[i] = banner[numberOfCharacters-1][i] << bitPosition; } displayMatrix(matrixToDisplay,(numberOfPhysicalDisplays-1 - currentPhysicalDisplay)); } } delay(delayTime);} void displayMatrix(byte *matrix, int matrixNumber){ for(int i=0; i<=7;i++){ lc884.setRow(matrixNumber,i,matrix[i]); }}
Anhang: Ansteuerung eines zweifarbigen TA6932 basierten Displays
Es gibt auch zweifarbige Displays mit Dots, die rot oder grün leuchten können. Diese werden intern über einen TA6932 Chip gesteuert. Eine gleichnamige Bibliothek gibt es hier auf GitHub oder ihr installiert sie über die Arduino Bibliotheksverwaltung.
Mit der Bibliothek ist die Ansteuerung sehr einfach. Mit displayCache(row) = value
legt ihr fest, welche Dots einer Reihe (row) leuchten sollen. Die roten Dots sind als Reihen 0 bis 7 definiert. Die grünen Dots sprecht ihr als Reihen 8 bis 15 an. Das sollte durch den folgenden Sketch klarer werden.
#include <TA6932.h>#define PIN_TA6932_STB 8#define PIN_TA6932_CLK 9#define PIN_TA6932_DIN 10TA6932 tm(PIN_TA6932_STB, PIN_TA6932_CLK, PIN_TA6932_DIN);void setup() { tm.begin(); tm.displayCache[0] = 0b00000001; // red dot at 0/0 tm.updateDisplay(); delay(2000); tm.displayCache[0] = 0b00000011; // red dots at 0/0 and 1/0 tm.updateDisplay(); delay(2000); tm.displayCache[0] = 0b00011011; // red dots at 0/0,1/0,3/0,4/0 tm.updateDisplay(); delay(2000); tm.displayCache[1] = 0b00000001; // red dot at 0/1; tm.updateDisplay(); delay(2000); tm.displayCache[1] = 0b11110000; // red dots at 4/1,5/1,6/1,7/1 tm.updateDisplay(); delay(2000); tm.displayCache[3] = 0b11110000; // red dots at 4/3,5/3,6/3,7/3 tm.updateDisplay(); delay(2000); tm.displayCache[11] = 0b11110000; // green dots at 4/3,5/3,6/3,7/3 (in addition!) tm.updateDisplay(); delay(2000); tm.displayCache[3] = 0b01010101; // red dots at 6/3,4/3,2/3,0/3 tm.displayCache[11] = 0b10101010; // green dots at 7/3,5/3,3/3,1/3 tm.updateDisplay(); delay(2000); byte value = 0b10101010; for(int i=0; i<8; i++){ tm.displayCache[i] = value; value = ~value; // inverts all bits tm.displayCache[i+8] = value; } tm.updateDisplay();}void loop() {}