ESP32 mit Arduino programmiert steuert ein 8×8 LED-Matrix-Modul als Daumenkino – Teil 4

Nachdem wir in Teil 3 die Hardware zusammengebaut haben, geht es heute im abschließenden vierten Teil um die Software, mit der wir das Daumenkino zum Leben erwecken. Dazu erstellen wir eine Anwendung für den ESP32-Mikrocontroller in Arduino.

Dabei müssen wir uns erst einmal der Herausforderung stellen, dass wir – wie bereits in Teil 1 angerissen – nicht alle 64 LEDs komplett unabhängig voneinander ansteuern können. Ich habe mich hier für eine Art Software-Multiplexer entschieden, der in einer intervallgesteuerten Interrupt-Routine alle 30 µs immer nur eine der LEDs (je nach Wert in den Daten für das aktuelle Bild) einschaltet und bei jedem Aufruf die nächste LED abarbeitet. So leuchtet also tatsächlich immer nur eine der LEDs, aber durch das schnelle Intervall von 30 µs ist dieser Wechsel zu schnell für das menschliche Auge und das „Flimmern“ fällt nicht auf. Der einzige sichtbare Effekt ist, dass die Matrix nicht in voller Helligkeit zu leuchten scheint.

Die einzelnen Bilder der Filme werden als Byte-Arrays im Code abgespeichert. Ein Bild besteht aus jeweils 9 Byte, nämlich 8 Zeilen als Byte für jeweils 8 Spalten (je 1 Bit) und einer neunten Zeile für die Anzeigedauer des Frames in Zehntel-Sekunden:

   // Erster Frame = Bild "10", Dauer 1 Sekunde (100 * 10tel Sekunde)
    {B00100110,
     B01101001,
     B10101001,
     B00101001,
     B00101001,
     B00101001,
     B00101001,
     B00100110, 100},

Die 8 Datenbyte für die Bilder selbst können wir recht bequem im Online-Tool LED Matrix Editor erstellen („As Byte Arrays“ auswählen) und die generierten Bild-Daten (das sind die 8 Zeilen im binären Zahlenformat „B01001…“ usw.) in unseren Quelltext übernehmen. Die 9. Zeile für die Anzeigedauer müssen wir selbst einfügen.

Der restliche Code beinhaltet weiter keine besonders aufregenden Teile und ist hoffentlich durch die Kommentare ausreichend beschrieben. Wir müssen nur darauf achten, dass wir die PIN-Nummern aus der Tabelle aus Teil 3 gemäß der Beschaltung unseres Matrix-Moduls korrekt in den Code übertragen. So werden aus

Spalte:ABCDEFGH
GPIO ESP32:232221191851716
Zeile:12345678
GPIO ESP32:42153233252627

folgende Definitionen im Quellcode:

// Pins der Zeilen definieren
#define r1 4
#define r2 2
#define r3 15
#define r4 32
#define r5 33
#define r6 25
#define r7 26
#define r8 27

// Pins der Spalten definieren
#define cA 23
#define cB 22
#define cC 21
#define cD 19
#define cE 18
#define cF 5
#define cG 17
#define cH 16


Und hier noch der komplette Quelltext der Software:

// www.stephanys.de  - Code Sample
// Public Domain - 2021 Markus Stephany - V1.0
// 8x8-LED-Matrix - Daumenkino

// Arduino-Platform einbinden
#include <Arduino.h>

// Pins der Zeilen definieren
#define r1 4
#define r2 2
#define r3 15
#define r4 32
#define r5 33
#define r6 25
#define r7 26
#define r8 27

// Pins der Spalten definieren
#define cA 23
#define cB 22
#define cC 21
#define cD 19
#define cE 18
#define cF 5
#define cG 17
#define cH 16

// Arrays mit Zeilen und Spalten-Pins
// Datentyp int wegen Bit-Boundaries bei IRAM
const volatile IRAM_ATTR int rows[8] = {r1, r2, r3, r4, r5, r6, r7, r8};
const volatile IRAM_ATTR int columns[8] = {cA, cB, cC, cD, cE, cF, cG, cH};

// Array mit Daten für die Matrix
// Datentyp int wegen Bit-Boundaries bei IRAM
volatile IRAM_ATTR int matrix[8][8];

// LED 0-63 ein/ausschalten, ausschalten forcieren, einschalten
// je nachdem ob der Punkt im Matrix-Array gesetzt ist
void IRAM_ATTR setLED(int led, bool on)
{
  int col = led & 7;
  int row = led / 8;
  if (!on || matrix[row][col] == 0)
  {
    digitalWrite(rows[row], HIGH);
    digitalWrite(columns[col], LOW);
  }
  else
  {
    digitalWrite(rows[row], LOW);
    digitalWrite(columns[col], HIGH);
  }
}

// Timer für die Interrup-Routine zum schalten der nächsten LED
hw_timer_t *timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// Timer-Routine
void IRAM_ATTR onTimer()
{
  // aktuell geschaltete LED (wird durchgezählt: 0-63, 0-63)
  volatile static int currentLed = 0;
  // vorherige LED (wird wieder ausgeschaltet)
  volatile static int oldLed = 0;
  // Synchronisierung mit Haup-Thread
  portENTER_CRITICAL_ISR(&timerMux);
  // vorherige LED ausschalten
  setLED(oldLed, false);
  // aktuelle LED je nach Wert im Matrix-Array einschalten
  setLED(currentLed, true);
  // LED fürs Auschalten beim nächsten Durchlauf speichern
  oldLed = currentLed;
  // LED-Index weiterzählen
  currentLed++;
  if (currentLed > 63)
    currentLed = 0;
  portEXIT_CRITICAL_ISR(&timerMux);
}

// LED-Wert im Matrix-Attay festlegen
void setDot(int row, int col, int value)
{
  portENTER_CRITICAL_ISR(&timerMux);
  matrix[row][col] = value;
  portEXIT_CRITICAL_ISR(&timerMux);
}

// Zeile im Matrix-Array löschen
void clearRow(int row)
{
  for (int i = 0; i < 8; i++)
    setDot(row, i, 0);
}

// Matrix-Array leeren
void clearMatrix()
{
  for (int i = 0; i < 8; i++)
    clearRow(i);
}

// Wird beim Einschalten einmal ausgeführt
void setup()
{
  // alle 16 LED-Pins auf Ausgabe schalten
  for (int i = 0; i <= 7; i++)
  {
    pinMode(rows[i], OUTPUT);
  }
  for (int i = 0; i <= 7; i++)
  {
    pinMode(columns[i], OUTPUT);
  }

  // Matrix löschen
  clearMatrix();

  // Interrupt-Timer-Routine einschalten
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  // Aufruf alle 30 Mikrosekunden
  timerAlarmWrite(timer, 30, true);
  timerAlarmEnable(timer);
}

// Daumenkino-Framedaten
// jeweils 9 Werte:
// 0-7: Wert pro Zeile
// 8:   Dauer bis zum nächsten Frame (in 100tel Sekunden)
byte frames[][9] = {
    // Erster Frame = Bild "10", Dauer 1 Sekunde (100 * 10tel Sekunde)
    {B00100110,
     B01101001,
     B10101001,
     B00101001,
     B00101001,
     B00101001,
     B00101001,
     B00100110, 100},
    // Zweiter Frame = Bild "9", Dauer 1 Sekunde (100 * 10tel Sekunde)
    {B00000110,
     B00001001,
     B00001001,
     B00000111,
     B00000001,
     B00000001,
     B00001001,
     B00000110, 100},
    // weitere Frames...
    {B00000110,
     B00001001,
     B00001001,
     B00000110,
     B00001001,
     B00001001,
     B00001001,
     B00000110, 100},
    {B00011111,
     B00010001,
     B00000001,
     B00000010,
     B00000100,
     B00000100,
     B00000100,
     B00000100, 100},
    {B00000110,
     B00001001,
     B00001000,
     B00001000,
     B00001110,
     B00001001,
     B00001001,
     B00000110, 100},
    {B00001111,
     B00001001,
     B00001000,
     B00001110,
     B00000001,
     B00000001,
     B00001001,
     B00000110, 100},
    {B00010010,
     B00010010,
     B00010010,
     B00011111,
     B00000010,
     B00000010,
     B00000010,
     B00000010, 100},
    {B00000110,
     B00001001,
     B00000001,
     B00000110,
     B00000001,
     B00000001,
     B00001001,
     B00000110, 100},
    {B00000110,
     B00001001,
     B00001001,
     B00000001,
     B00000010,
     B00000100,
     B00001000,
     B00001111, 100},
    {B00000001,
     B00000011,
     B00000101,
     B00000001,
     B00000001,
     B00000001,
     B00000001,
     B00000001, 100},
    {B00000110,
     B00001001,
     B00001001,
     B00001001,
     B00001001,
     B00001001,
     B00001001,
     B00000110, 255},
    {B00000000,
     B00001001,
     B00001001,
     B00001001,
     B00001001,
     B00001001,
     B00001001,
     B00000000, 4},
    {B00000000,
     B00000000,
     B00001001,
     B00001001,
     B00001001,
     B00001001,
     B00000000,
     B00000000, 4},
    {B00000000,
     B00000000,
     B00000000,
     B00001001,
     B00001001,
     B00000000,
     B00000000,
     B00000000, 4},
    {B00000000,
     B00000000,
     B00000000,
     B00000000,
     B00000000,
     B00000000,
     B00000000,
     B00000000, 255},
    {B00000000,
     B00111100,
     B01011010,
     B10011001,
     B01000010,
     B00111100,
     B00000000,
     B00000000, 100},
    {B00000000,
     B00000000,
     B01111110,
     B10011001,
     B01011010,
     B00111100,
     B00000000,
     B00000000, 4},
    {B00000000,
     B00000000,
     B01111110,
     B10011001,
     B01111110,
     B00000000,
     B00000000,
     B00000000, 4},
    {B00000000,
     B00000000,
     B01111110,
     B11111111,
     B10000001,
     B00000000,
     B00000000,
     B00000000, 20},
    {B00000000,
     B00000000,
     B01111110,
     B10011001,
     B11111111,
     B00000000,
     B00000000,
     B00000000, 6},
    {B00000000,
     B00000000,
     B01111110,
     B10011001,
     B01011010,
     B00111100,
     B00000000,
     B00000000, 6},
    {B00000000,
     B00111100,
     B01011010,
     B10011001,
     B01000010,
     B00111100,
     B00000000,
     B00000000, 100},
    {B00000000,
     B00111100,
     B01011010,
     B10011001,
     B01111110,
     B00000000,
     B00000000,
     B00000000, 6},
    {B00000000,
     B00111100,
     B01001110,
     B10001101,
     B01111110,
     B00000000,
     B00000000,
     B00000000, 70},
    {B00000000,
     B00111100,
     B01011010,
     B10011001,
     B01111110,
     B00000000,
     B00000000,
     B00000000, 6},
    {B00000000,
     B00111100,
     B01110010,
     B10110001,
     B01111110,
     B00000000,
     B00000000,
     B00000000, 70},
    {B00000000,
     B00111100,
     B01011010,
     B10011001,
     B01111110,
     B00000000,
     B00000000,
     B00000000, 100},
    {B00000000,
     B00111100,
     B01011010,
     B10011001,
     B01000010,
     B00111100,
     B00000000,
     B00000000, 100}};

// Byte-Bits in Matrix-Zeile schieben
void setRow(int row, byte data)
{
  for (int i = 0; i < 8; i++)
  {
    matrix[row][7 - i] = (int)(data & 1);
    data = data >> 1;
  }
}

// Matrix mit Frame-Daten (0-7) setzen
void setMatrix(byte data[])
{
  for (int i = 0; i < 8; i++)
    setRow(i, data[i]);
}

// Hauptschleife
void loop()
{
  // StartFrame mit 0 einmal vorbelegen ("static")
  static int startFrame = 0;

  // alle Frames einmal durchlaufen (Countdown + Zwinkern)
  for (int frame = startFrame; frame < sizeof(frames) / 9; frame++)
  {
    setMatrix(frames[frame]);
    delay(frames[frame][8] * 10);
  }

  // Nach einmaligem Durchlauf von Frame 0
  // bis zum Ende StartFrame auf Anfang des Zwinkerns setzen
  // sodass der Countdown nur einmal angezeigt wird
  startFrame = 16;
  // 5-15 Sekunden warten um unregelmäßiges Zwinkern
  // zu simulieren
  sleep(random(5, 15));
}

Ich bin momentan dabei, den weiter oben angesprochenen LED Matrix Editor so in den ESP32 zu integrieren, dass man die „Filme“ bequem über eine Web-Oberfläche erstellen kann.

Damit ist unsere kleine Reihe fertig. Ich hoffe, Ihr habt ein wenig Spaß daran, eigene „Daumenkinos“ zu erstellen und anzeigen zu lassen.