LED-Matrix Programmieren: Binäre Darstellung & Multiplexing

In diesem Teil der LED-Matrix Programmierung werden wir im Detail sehen, wie die physische Matrix im Programm dargestellt und gesteuert werden kann, wobei ich einen Arduino Mega R3 zur Steuerung verwende. Beginnen wir zunächst mit dem Konzept des Multiplexings, und wie dieses programmatisch umgesetzt wird:

Multiplexing – Zeilenweiser Aufbau

Über die Technik des Multiplexings ist online viel zu lesen. Weniger oft findet man allerdings konkreten Programmcode, durch den die Anzeige einer Matrix gesteuert werden kann.

Ich werde in diesem Beispiel einen Arduino Mega R3 programmieren, der Stromkreis der Zeilen/Spalten wird über Transistoren geschlossen. Wird also der Pin für die jeweilige Zeile/Spalte auf HIGH gesetzt, wird der Stromkreis geschlossen. Die Pins 22, 24, 26, 28, …, 36 steuern die Zeilen. 38, 40, 42, …, 52 steuern die Spalten. Es wird also ab einem Offset (22 für Zeilen bzw. 38 für Spalten) jeder zweite Pin gesteuert. Um das im Programm einfach umzusetzen, hier die Basis des Arduino-Sketches:

// Offsets for rows/cols
const int start_rows = 38;
const int start_cols = 22;

// Amount of rows/cols
const int num_rows = 8;
const int num_cols = 8;

void setup()
{
	// Set up rows
	for(int i = 0; i < num_rows; i++)
	{
		pinMode(start_rows + (2*i), OUTPUT);
		digitalWrite(start_rows + (2*i), LOW);
	}

	// Set up cols
	for(int i = 0; i < num_cols; i++)
	{
		pinMode(start_cols + (2*i), OUTPUT);
		digitalWrite(start_cols + (2*i), LOW);
	}
}

void loop()
{
}

start_rows ist hier das Offset für die Zeilen, i*2 stellt sicher dass jeder zweite Pin gesteuert wird.

Zurück zum Multiplexing: Logisch aufgebaut läuft das Multiplexing etwa so ab:

Der Stromkreis jeder Zeile wird nacheinander geschlossen. Während der Stromkreis einer Zeile geschlossen ist, wird der Stromkreis für diejenigen Spalten geschlossen, deren LEDs in dieser Zeile leuchten sollen.

Im Klartext: Es werden alle Zeilen durchlaufen und „eingeschaltet“ (Anmerkung: im Artikel werde ich folgend „einschalten“ schreiben – dies ist gleichbedeutend mit „Stromkreis schließen“). In jeder Zeile wird jede Spalte durchlaufen und eingeschaltet. Programmiertechnisch klingt das nach… For-Schleifen! Verschachtelte, um genau zu sein:

void loop()
{
	// Repeat displaying a single image
	for(int i = 0; i < 500; i++)
	{
		// Iterate rows
		for(int r = 0; r < num_rows; r++)
		{
			// Switch on a row
			digitalWrite(start_rows + 2*r, HIGH);

			// Iterate Cols
			for(int c = 0; c < num_cols; c++)
			{
				//ToDo: See what columns have to be switched on
			}

			// Iterate Cols
			for(int c = 0; c < num_cols; c++)
			{
				// Switch off cols again
				digitalWrite(start_cols + 2*c, LOW);
			}

			// Switch off row again
			digitalWrite(start_rows + 2*r, LOW);
		}
	}
}

Die äußere Schleife durchläuft alle Zeilen. In jeder Zeile werden durch die inneren Schleifen auch alle Spalten durchlaufen – prima! Eine weitere For-Schleife befindet sich um das gesamte konstrukt, da das Einzelbild nur einige Tausendstel Sekunden angezeigt wird, bevor der loop wiederholt wird. Langfristig brauchen wir eine Möglichkeit um zu steuern, wie lange ein einzelnes Bild angezeigt wird, immerhin werden wir im nächsten Tutorial Animationen (Bildsequenzen) besprechen. Also: Den Code zum Bildaufbau einfach so oft ausführen, bis das Bild für eine gewünschte Zeit angezeigt wird.

Binäre Repräsentation, oder: Welche Pixel sollen leuchten?

Unser Programmgerüst steht. Alle Zeilen werden durchlaufen, für jede Zeile ebenfalls jede Spalte. Was jetzt noch fehlt ist eine Möglichkeit dem Mikrocontroller zu sagen, welche Pixel der Matrix leuchten sollen. Ebenso wie eine Methode um diese Informationen im Programm zu nutzen.

Als erstes kümmern wir uns darum, wie wir Menschen dem Prozessor einfach mitteilen können, was für ein Bild angezeigt werden soll.

Die Physische Matrix besteht aus 8 Zeilen. In jeder Zeile müssen 8 Spalten angesprochen werden. Zu jeder Zeile gehören somit 8 „untergeordnete“ Elemente, die Spalten. Im Programm werden solche Strukturen meistens durch Arrays repräsentiert. Ein Array hat eine beliebige Anzahl an Elementen. Es gibt auch mehrdimensionale Arrays, bei denen jedes Element eine beliebige Anzahl an Unterelementen besitzt. Wir könnten die Matrix durch ein zweidimensionales Array repräsentieren, welches in der ersten Dimension aus 8 Zeilen besteht, welche wiederum aus je 8 Spalten bestehen. Das würde konkret so aussehen:

// Two-dimensional Array represents LEDs of the Matrix
const char image[num_rows][num_cols] = {\
							{1, 1, 1, 1, 1, 1, 1, 1},\
							{1, 0, 0, 0, 0, 0, 1, 1},\
							{1, 0, 0, 0, 0, 1, 0, 1},\
							{1, 0, 0, 0, 1, 0, 0, 1},\
							{1, 0, 0, 1, 0, 0, 0, 1},\
							{1, 0, 1, 0, 0, 0, 0, 1},\
							{1, 1, 0, 0, 0, 0, 0, 1},\
							{1, 1, 1, 1, 1, 1, 1, 1}	};

Und dieses Beispiel ist auch schon im Programm nutzbar. Durch die Indizes der ersten und zweiten Dimension kann gezielt ein Byte in diesem Array angesprochen werden. Dieses Char hat (bei 8 Bit länge = 1 Byte) 256 unterschiedliche Zustände – Dabei brauchen wir nur 2 Zustände, die Repräsentation für „Ein“ und „Aus“ – Klassischerweise 1 und 0.

In der Programmiersprache C können diese Werte auch gleich als Wahrheitswert überprüft werden: Ist eine Zahl genau gleich 1, entspricht dies dem booleschen Wert „wahr“. Ist die Zahl nicht genau gleich 1, also 0, 2, -10, 200… entspricht dies dem booleschen Wert „falsch“. Dies repräsentiert, welche Spalte in welcher Zeile ein oder aus sein soll.

Nur der Vollständigkeit halber: Hier wäre der entsprechende Programmcode, um in die jeweiligen Zeilen einzuschalten. Zum Üben und Lernen empfehle ich, diesen Code bereits einmal abzutippen und auf einer LED-Matrix zu testen.

void loop()
{
	// Repeat displaying a single image
	for(int i = 0; i < 500; i++)
	{
		// Iterate rows
		for(int r = 0; r < num_rows; r++)
		{
			// Switch on a row
			digitalWrite(start_rows + 2*r, HIGH);

			// Iterate Cols
			for(int c = 0; c < num_cols; c++)
			{
				// Check which cols have to be turned on
				if(image[r])
					digitalWrite(start_cols + 2*c, HIGH);
				else
					digitalWrite(start_cols + 2*c, LOW);
			}

			// Iterate Cols
			for(int c = 0; c < num_cols; c++)
			{
				// Switch off cols again
				digitalWrite(start_cols + 2*c, LOW);
			}

			// Switch off row again
			digitalWrite(start_rows + 2*r, LOW);
		}
	}
}

OK, das mag schon funktionieren, aber ganz ehrlich: Da kann noch etwas verbessert werden…

Code optimieren

Wie bereits erwähnt, nutzen wir bisher für jede „Spalte in einer Zeile“ (aka. Repräsentative LED) ein Byte, also 8 Bits, um den Zustand dieser LED zu repräsentieren. Das ist ziemlich ineffizient. Wir benötigen nur 2 „Zustände“ (an und aus), dabei hat ein Byte 256 verschiedene Werte (0 bis 255). Außerdem belegen mehrdimensionale Arrays im Vergleich zur gleich erlernten Methode mehr Speicherplatz im Flash-Speicher sowie im RAM – Suboptimal für Mikrocontroller.

Daher können wir das ganze noch etwas optimieren. Führen wir uns noch einmal vor Augen:

  • Wir brauchen 8 Elemente (Zeilen), also auf jeden Fall ein Array
  • Jedes Element hat wiederum 8 untergeordnete Elemente (Spalten)
  • Diese untergeordneten Elemente repräsentieren den Zustand einer LED im Bild
  • Dieser Zustand ist entweder 0 (aus) oder 1 (ein)

Kennen wir nicht schon ein „Element“, welches 8 „Unterelemente“ besitzt? Einen Datentyp… Ein Byte besteht aus 8 Bits. Jedes dieser Bits ist entweder 0 oder 1. BINGO! Wir können also die zweite Dimension des Arrays darstellen, wir die einzelnen Bits als Unterelemente der ersten Dimension – der Bytes – ansehen.

Bevor jetzt jemand fürchtet, die mühsam zusammengelötete 16×16 Matrix funktioniere nicht, da ein Byte nur 8 Bits hat: Keine Sorge, es gibt Datentypen, die umfassen auch mehr als 8 Bits. Für dieses Beispiel verwende ich als Elemente erster Dimension 8 Chars, diese bestehen auf dem ATMega2560 aus genau 8 Bits. Ihr könnt natürlich auch einen Integer mit 16 Bits nutzen.

Also: Wir nutzen nur noch eine Dimension, die zweite wird durch die Bits im Byte dargestellt. Im Programm sieht so ein „Pseudo-zweidimensionales Array“ wie folgt aus:

// 1-Dimensional Array, Bits represent LEDs
const char image[num_rows] ={B11111111,\
							 B10000011,\
							 B10000101,\
							 B10001001,\
							 B10010001,\
							 B10100001,\
							 B11000001,\
							 B11111111};

Der Nachteil dieser Methode ist allerdings, dass man die Bits einzeln auf deren Wert untersuchen muss, was – gerade als Anfänger – nicht sofort einleuchtend ist. Doch durch etwas Logik findet man schnell eine Lösung.

Unser Problem: Wir können nicht einfach durch einen Index ein einzelnes Bit ansprechen. In C gibt es gar keinen Datentyp für ein einzelnes Bit. Wir können nur mit dem Char (oder Integer, Long, LongLong…) rechnen. Ist dieser Char ungleich 0, also beispielsweise B00000001, ergibt dies ein logisches wahr. Um nun jedes Bit einzeln auszuwählen und somit überprüfen zu können, nutzen wir 2 Techniken:

Die Bits werden mit einer Bitmaske AND-Verknüpft. Jedes Bit, welches einen Pixel darstellt, wird also per logischem „Und“ mit dem entsprechendem Bit der Bitmaske verknüpft. Die Bitmaske heißt so, da wir eine Maske erstellen können, die beliebige Bits „eliminiert“ – Null setzt.
Da wir später binär B00000001 oder B00000000 haben wollen, verknüpfen wir den Char im image-Array mit B00000001 – Somit gibt es nur genau 2 Möglichkeiten

  • Das letzte Bit ist „1“, daher ist das Ergebnis aus dieser AND-Verknüpfung gleich 1 = wahr
  • Das letzte Bit ist „0“, daher ist das Ergebnis der AND-Verknüpfung gleich 0 = falsch

Wir können also durch die AND-Verknüpfung mit der Bitmaske den Wert des letzten Bits bestimmen (und durch eine If-Abfrage bestimmen, ob die LED nun leuchten soll oder nicht).

Jedes andere Bit des Ergebnisses wird 0, da die entsprechenden Bits der Bitmaske 0 sind – Die AND-Verknüpfung resultiert also immer in „0“. Nur das letzte Bit kann „1“ sein, falls das Bit im Char ebenfalls 1 ist.

Mit dieser Methode können wir immerhin das letzte Bit überprüfen. Um auch alle anderen Bits prüfen zu können, kommt die zweite Technik zum Einsatz: Wir verschieben alle Bits im Char nach rechts. Im Programm werden wir beim ersten Schleifendurchlauf (Zähler 0) die Bits um 0 Stellen nach rechts verschieben, beim zweiten Schleifendurchlauf (Zähler 1) alle Bits um 1 Stelle nach rechts verschieben, usw…

Die Technik dazu wird Bitshifting genannt (Bitverschiebung) und ist von Prozessoren sehr schnell ausführbar. Damit können wir jedes einzelne Bit im Char prüfen, angefangen mit dem letzten, danach das vorletzte usw. Der fertige, voll funktionsfähige Programmcode sieht nun so aus:

// Offsets for rows/cols
const int start_rows = 38;
const int start_cols = 22;

// Amount of rows/cols
const int num_rows = 8;
const int num_cols = 8;

// 1-Dimensional Array, Bits represent LEDs
const char image[num_rows] ={B11111111,\
							 B10000011,\
							 B10000101,\
							 B10001001,\
							 B10010001,\
							 B10100001,\
							 B11000001,\
							 B11111111};

// Bitmask for logical AND
char bitmask = B00000001;

void setup()
{
	// Set up rows
	for(int i = 0; i < num_rows; i++)
	{
		pinMode(start_rows + (2*i), OUTPUT);
		digitalWrite(start_rows + (2*i), LOW);
	}

	// Set up cols
	for(int i = 0; i < num_cols; i++)
	{
		pinMode(start_cols + (2*i), OUTPUT);
		digitalWrite(start_cols + (2*i), LOW);
	}
}

void loop()
{
	// Repeat displaying a single image
	for(int i = 0; i < 500; i++)
	{
		// Iterate rows
		for(int r = 0; r < num_rows; r++)
		{
			// Switch on a row
			digitalWrite(start_rows + 2*r, HIGH);

			// Iterate Cols
			for(int c = 0; c < num_cols; c++) 			{ 				// Check which cols have to be turned on 				if((image[r]>>c) & bitmask)
					digitalWrite(start_cols + 2*c, HIGH);
				else
					digitalWrite(start_cols + 2*c, LOW);
			}

			// Iterate Cols
			for(int c = 0; c < num_cols; c++)
			{
				// Switch off cols again
				digitalWrite(start_cols + 2*c, LOW);
			}

			// Switch off row again
			digitalWrite(start_rows + 2*r, LOW);
		}
	}
}

Wichtige Hinweise

instacode led matrix multiplexing

HIPSTER! Der Code zum Multiplexing. Via Instacode

Da wir in der If-Abfrage immer das letzte, vorletzte, drittletzte… Bit prüfen, durch das digitalWrite aber die erste, zweite, dritte… Spalte einschalten, wird das Bild spiegelverkehrt dargestellt! Wir könnten das durch weitere Berechnungen im Programm beheben, allerdings müsste der Mikrocontroller dann noch mehr CPU-Zyklen verbrauchen, nur um ein paar LEDs leuchten zu lassen… Daher werden die Bilder, die auf der Matrix angezeigt werden sollen, einfach spiegelverkehrt abgespeichert. Das ist für uns ein einmaliger „Rechenaufwand“, spart der CPU des Mikrocontrollers aber einige Rechenzyklen.

Und noch ein Wort zum Konstrukt, mit dem wir prüfen, welche LEDs eingeschaltet werden sollen: Es sind 2 Schleifen vorhanden, welche die Zeilen durchlaufen. Die erste Schleife ist dazu da, die entsprechenden LEDs einzuschalten. Ist diese Schleife durchlaufen, wird die zweite Schleife durchlaufen, welche die LEDs alle wieder ausschaltet. Wozu also der „überflüssige“ else-Zweig in der ersten Schleife (in dem die nicht einzuschaltenden LEDs ausgeschaltet werden) wenn später doch sowieso alle LEDs ausgeschaltet werden?

Das Einschalten des Pins für die jeweilige LED kostet Zeit. Die digitalWrite-Methode benötigt eine gewisse Zeit, sowie den Ausdruck „“ zu errechnen benötigt Zeit. Würde diese Zeit nur für die LEDs beansprucht, die auch wirklich leuchten sollen, würden einige Zeilen schneller, andere langsamer aufgebaut. Bei einer winzigen 8×8 Matrix ist das kein Problem, doch bei größeren Matritzen ergeben sich so eventuell hellere und dunklere Zeilen. Außerdem ist das Timing auch bei der Animation entscheidend. Daher wird die digitalWrite-Methode für jeden Pin aufgerufen, unabhängig davon, ob dieser eingeschaltet oder ausgeschaltet werden soll.

Ich hoffe diese Anleitung hilft euch dabei, das Programmieren der Matrix besser zu verstehen. Ich habe versucht meine Gedankengänge verständlich zu beschreiben, damit ihr seht, wie man schritt für schritt das Programm ganz logisch aufbauen kann. Zur weiteren Hilfe habe ich noch ein GitHub-Repo erstellt, in dem die einzelnen Schritte als commits abgearbeitet werden.

Ich würde mich freuen, wenn ihr diesen Artikel kommentiert oder teilt (per Email, auf Twitter…), falls er euch gefallen hat. Für Fragen, Kritik und Ideen bin ich immer offen.

MfG
Damon Dransfeld

Dieser Eintrag wurde veröffentlicht in Arduino
Bookmarken: Permanent-Link Schreibe einen Kommentar oder hinterlasse einen Trackback: Trackback-URL.

4 Kommentare

  1. Erstellt am 17. November 2013 um 14:18 | Permalink zum Kommentar

    Danke! Das hat mir viel geholfen. Ich habe eine Wordclock gebaut, und die Matrix ist eigentlich viel zu schade dort nur die Zeit anzuzeigen. Nur das ganze Bitmusterzeug war immer wie Magie für mich. gleich werde ich mir noch den Laufschrift & Bildsequenz Artikel durchlesen. Es ist echt schwer in Worte zu fassen was man wissen möchte. Bin froh diese Seite gefunden zu haben.
    Gruß, Steffen

  2. robin
    Erstellt am 8. Januar 2014 um 13:18 | Permalink zum Kommentar

    tip topp !!! :)

  3. Pascal
    Erstellt am 7. April 2014 um 12:25 | Permalink zum Kommentar

    Ist echt super erklährt nur blicke ich bei dem Kommando : if(image[r]1) nicht durch.
    Wenn das so geschrieben ist, wie weiss das Programm in welcher Spalte der Cols-Wert aus dem Array ausgelesen werden muss? Müsste dies dann nicht if(image[r]==1) heissen?
    Wäre super wenn mir das jemand Idotensicher erklären kann.

    // Check which cols have to be turned on
    if(image[r]1)
    digitalWrite(start_cols + 2*c, HIGH);
    else
    digitalWrite(start_cols + 2*c, LOW);

Achtung: Wordpress interpretiert bestimmte Zeichenfolgen als Markup und verändert diese. Nutzt für Programmcode lieber Gist oder PasteBin-Services und verlinkt die Code-Schnipsel.

Post a Comment

Ihre E-Mail wird niemals veröffentlicht oder verteilt. Benötigte Felder sind mit * markiert

*
*

Du kannst diese HTML Tags und Attribute verwenden: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>