LED Pixel Display Uhr

Vor ein paar Tagen habe ich Euch in meinem Artikel ‚Super günstiges programmierbares 16 x 16 LED Display!‘ ein programmierbares 16 x 16 LED Display vorgestellt und gegen Ende des Artikels erwähnt, dass man darauf neben Kunst auch Daten aus dem Internet darstellen kann.

Nach der Veröffentlichung des oben angegebenen Artikels habe ich dass dann ausprobiert und es hat auch geklappt, aber es gab eine kleine Hürde und ein kleines Problem. Hier will ich Euch zeigen wie die Hürde zu überspringen ist und Euch dann noch einen Tipp geben, wie das Problem zu lösen ist.

Um an Daten aus dem Internet zu kommen benötigt der ESP32-C3 Super Mini eine WiFi Verbindung zu Eurem Rooter, die mit folgenden paar Zeilen C Code realisiert werden kann:

const char *ssid = "SSID";
const char *password = "PASSWORD";

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
}
Serial.println("WiFi verbunden");

Das ist soweit Standard und wird in hunderten von Programmen so gemacht. Jetzt könnt Ihr mit der Funktion ‚configTime‘ aus der Bibliothek ‚ ESP32Time‘ die Echtzeituhr des ESP32-C3 Super Mini mit der aktuellen Zeit setzen.

#include "ESP32Time.h"

const char *ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 3600;      // Lokaler GMT-Offset
const int daylightOffset_sec = 3600;  // Lokaler Sommerzeit-Offset

configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

Um jetzt die Zeit anzuzeigen müsst Ihr in regelmäßigen Abständen die Echtzeituhr auslesen und die Zeit wie von Euch gewünscht formatieren und auf dem Display ausgeben. Ich habe mich entschieden erst einmal nur Stunde und Minute anzuzeigen und mir dazu die Funktion ‚getFormattedTime‘ gebastelt, die die Zeit in 3 verschiedenen Variablen fertig mit bei Bedarf führenden Nullen ablegt :

char timeStr[6];
char hourStr[4];
char minStr[4];

const char *getFormattedTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    return "N/A";
  }
  snprintf(timeStr, sizeof(timeStr), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
  snprintf(hourStr, sizeof(timeStr), "%02d", timeinfo.tm_hour);
  snprintf(minStr, sizeof(timeStr), "%02d", timeinfo.tm_min);
  return timeStr;
}

Und jetzt kommen wir zur Hürde. Denn wie kann man jetzt die Strings auf dem LED Display ausgeben?

Was ist ein Zeichensatz?

Die meisten werden es wahrscheinlich wissen und können diesen Absatz überspringen. Aber vielleicht gibt es ein paar Anfänger, die gerne die technischen Grundlagen dazu verstehen möchten. Grundsätzlich geht es darum einzelne LEDs des Displays einzuschalten und andere aus, so dass durch die geschickte Anordnung von ein- und ausgeschalteten LEDs ein Zeichen sichtbar wird! Der String ist ja eine Kette von einzelnen Zahlen, bei denen jede Zahl für ein bestimmtes Zeichen steht. Die Zahl selbst enthält aber überhaupt keine Informationen darüber wie das Zeichen aussehen soll.

Diese Informationen stehen üblicherweise (Es gibt auch Vektorzeichensätze, aber das ist ein anderes Thema) in einem Zeichensatz, einer Datei, die normalerweise durch eine Vielzahl von einzelnen Bits die Erscheinungsform vieler einzelner Zeichen bestimmt. In so einer Datei sind also für viele der üblichen Zeichen Muster enthalten, die deren Aussehen bestimmen. Die Zahl im String ist ein Index auf eine bestimmte Position innerhalb der Zeichensatz Datei. An dieser Position beginnt das Muster aus einzelnen gesetzten und umgesetzten Bits, die die Form des durch die Zahl indizierten Zeichens bestimmt.

Es gibt viele verschiedene Zeichensätze mit unterschiedlichem Aussehen der einzelnen Zeichen oder unterschiedlicher Auflösung. Unter Windows kann man mit dem Begriff ‚Schriftarten‘ danach suchen und sie sich anzeigen lassen. Damit man bei verschiedenen Zeichensätzen immer weiß an welcher Position sich das Muster für ein bestimmtes Zeichen findet, verwendet man einen gemeinsamen Code, wie zum Beispiel den ASCII Code.

Im Falle von unserem Display habe ich mich dazu entschieden, nur mit diesem Wissen ausgestattet, das Zeichnen der Zeichen selbst zu realisieren. Allerdings hatte ich keine Lust alle Muster selbst zu codieren und habe darum im Internet nach passenden Zeichensätzen gesucht. Wenn ich nur 16 x 16 Pixel zur Verfügung habe und mehrere Zeichen nebeneinander zeigen möchte, benötige ich einen Zeichensatz, der mit extrem wenig Pixeln in der Lage ist alle wichtigen Zeichen darzustellen. Ich habe einen gefunden, der dies mit nur 4 x 6 Pixeln kann. Eigentlich sogar nur 3 x 5 Pixel, aber für die Abgrenzung voneinander benötigt man halt pro Richtung noch einen weiteren Pixel. Mit diesem Zeichensatz kann ich 4 Zeichen nebeneinander auf das Display zeichnen! Das reicht gerade so für Stunde und Minute. Ihr findet ihn hier. Dort gibt es noch weitere Zeichensätze, die zum Beispiel interessant sind, wenn man nur 2 Zeichen oder gar nur eines auf das Display zeichnen möchte. Für dieses Projekt habe ich mich aber nicht weiter damit beschäftigt.

Ich habe leider nicht heraus gefunden für welche Bibliothek diese Zeichensätze gemacht wurden und sie darum geringfügig für meine Zwecke angepasst. Dazu habe ich alles in der Datei ‚font-4×6.c‘ vor:

unsigned char console_font_4x6[] = {

gelöscht und eine Datei ‚font-4×6.h‘ angelegt, in der nur eine Zeile steht:

extern unsigned char console_font_4x6[];

Die Datei font-4×6.h habe ich mit ‚#include “font-4×6.h”‘ inkludiert und kann nun von meinem Programm aus auf die Daten des Zeichensatzes zugreifen.

Damit ich eventuell später auch einen anderen Zeichensatz verwenden kann, habe ich die beiden Konstanten:

#define CHAR_HEIGHT 6
#define CHAR_WIDTH 4

eingeführt, in der die Größe eines einzelnen Zeichens festgelegt wird. Mit diesen Vorbereitungen habe ich mir nun eine Funktion geschrieben, die mit meiner schon vorhandenen Funktion ‚setPixel()‘ ein einzelnes Zeichen des Zeichensatzes an einer bestimmten Position des Displays mit einer bestimmten Farbe ausgibt:

void drawChar(int x, int y, char c, uint32_t color) {
  int charIndex;
  charIndex = c;
  charIndex *= CHAR_HEIGHT;
  for (int row = 0; row < CHAR_HEIGHT; row++) {
    unsigned char charRow = console_font_4x6[charIndex + row];
    for (int col = 0; col < CHAR_WIDTH; col++) {
      if (charRow & (0b10000000 >> col)) {
        setPixel(x + col, y + row, color);
      } else {
        setPixel(x + col, y + row, 0x000000);
      }
    }
  }
}

Diese Funktion berechnet aufgrund des ASCII Codes des übergebenen Zeichens eine Position in der Zeichensatz Datei, liest ab da hintereinander 6 (CHAR-HEIGHT) Bytes aus und prüft für jedes Byte welche Bits darin 0 oder 1 sind. Für 0 wird an der entsprechenden Stelle der Farbcode 0x000000 gesetzt und für 1 die übergebene Farbe.

Tipp

Ihr seht in der Zeichensatz Datei einzelne Bytes in folgender Schreibweise (zum Beispiel):

0x50

Dies könnt Ihr in der Arduino IDE aber auch so schreiben:

0b01010000

Wenn Ihr alle Bytes mit der dargestellten binären Schreibweise notiert, könnt Ihr das Muster des Zeichens direkt im Code erkennen! Das kann nützlich sein, wenn Ihr zum Beispiel ein einzelnes Zeichen geringfügig umkodieren wollt.

Und jetzt zum Problem.

Nachdem ich alles soweit fertig programmiert hatte und auch alles wie gewünscht funktionierte, viel mir auf, das beim Aktualisieren des Displays mit der Zeit es gelegentlich zu seltsamen Farberscheinungen kam. Anfangs dachte ich natürlich an einen Fehler in meinem Programm, konnte aber nichts entdecken.
Diese Farberscheinungen hatte ich bei meinem animierten Feuer und den anderen Animationen für Weihnachten nicht beobachtet, obwohl diese meiner Meinung nach viel aufwendiger sind und einen deutlich schnelleren Zugriff auf das Display besaßen.

Nachdem ich bei der Analyse des Problems so nicht weiter kam, las ich kreuz und quer durch die vielen Informationen zu RGB LEDs im Internet und stieß auf einer Seite auf einen ersten Hinweis: Der ESP32-C3 Super Mini besitzt an seinen GPIOs nur 3,3 Volt und die WS2812b ICs erwarten aber 5 Volt. Es stand dort aber auch, dass das meistens kein Problem ist und diese Information deckte sich ja auch mit meinen Erfahrungen.

Aber es deutete alles auf ein Problem mit der Stromversorgung hin und nach ein bisschen weiterem Nachdenken kam ich darauf, dass das einzige was anders zu den Weihnachtsanimationen ist, die Verwendung der WIFi Funktionalität des ESP32-C3 Super Mini ist. Wie ich durch die Entwicklung anderer Projekte weiß, kann WiFi zeitweise sehr viel Strom ziehen und möglicherweise dadurch die Spannung am GPIO geringfügig beeinflussen. Aufgrund dieser Theorie baute ich mein Programm so um, dass der Zugriff aufs Internet nur jede Stunde einmal zur Synchronisation der eingebauten Echtzeituhr des ESP32-C3 Super Mini verwendet und anschließend abgeschaltet wird. Diese Maßnahme löste das Problem erfolgreich!

Das Programm:

#include <Adafruit_NeoPixel.h>
#include "font-4x6.h"

#include <WiFi.h>
#include "ESP32Time.h"

#define SCREENWIDTH 16
#define SCREENHEIGHT 16

#define SYNCHRONIZE 3600000

#define CHAR_HEIGHT 6
#define CHAR_WIDTH 4

#define PIN 3
#define N_LEDS 256

Adafruit_NeoPixel strip = Adafruit_NeoPixel(N_LEDS, PIN, NEO_GRB + NEO_KHZ800);

ESP32Time rtc(3600);  // offset in seconds GMT+1

const char *ssid = "SSID";
const char *password = "PASSWORD";

const char *ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 3600;      // Lokaler GMT-Offset
const int daylightOffset_sec = 3600;  // Lokaler Sommerzeit-Offset

char hourStr[4];
char minStr[4];
char timeStr[6];

long lastSynchronize = millis() - SYNCHRONIZE;

void setup() {
  Serial.begin(115200);
  strip.begin();
  strip.setBrightness(255);
  strip.clear();
  randomSeed(analogRead(37));
}

void loop() {
  if (millis() - lastSynchronize > SYNCHRONIZE) {
    synchronizeTime();
    lastSynchronize = millis();
  }
  getFormattedTime();
  drawTimeString(0, 5, hourStr, 0x070707);
  setPixel(7, 7, 0x000200);
  setPixel(8, 7, 0x000200);
  drawTimeString(9, 5, minStr, 0x070707);
  strip.show();
  delay(5000);  // Warte eine Sekunde
  for (int x = 0; x < SCREENWIDTH; x++) {
    for (int y = 5; y < 11; y++) {
      setPixel(x, y, 0x000000);
      delay(10);
      strip.show();
    }
  }
}

void synchronizeTime() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi verbunden");
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  int maxRetries = 60;
  time_t now = time(nullptr);
  while (now < 24 * 3600 && maxRetries--) {
    delay(500);
    now = time(nullptr);
  }
  if (now > 24 * 3600) {
    Serial.println("Zeit erfolgreich synchronisiert");
  } else {
    rtc.setTime(0, 0, 12, 1, 2, 2024);  
    Serial.println("Zeit-Synchronisierung fehlgeschlagen");
  }
  WiFi.disconnect();
}

const char *getFormattedTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    return "N/A";
  }
  snprintf(timeStr, sizeof(timeStr), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
  snprintf(hourStr, sizeof(timeStr), "%02d", timeinfo.tm_hour);
  snprintf(minStr, sizeof(timeStr), "%02d", timeinfo.tm_min);
  return timeStr;
}

void drawTimeString(int x, int y, const char *string, uint32_t color) {
  int currentX = x;
  while (*string) {
    drawTimeChar(currentX, y, *string, color);
    string++;
    currentX += CHAR_WIDTH;
  }
}

void drawTimeChar(int x, int y, char c, uint32_t color) {
  int charIndex;
  charIndex = c;
  charIndex *= CHAR_HEIGHT;
  for (int row = 0; row < CHAR_HEIGHT; row++) {
    unsigned char charRow = console_font_4x6[charIndex + row];
    for (int col = 1; col < CHAR_WIDTH; col++) {
      if (charRow & (0b10000000 >> col)) {
        setPixel(x + col - 1, y + row, color);
      } else {
        setPixel(x + col - 1, y + row, 0x000000);
      }
      delay(10);
      strip.show();
    }
  }
}

void setPixel(int x, int y, uint32_t color) {
  int index;
  int invertedX = SCREENWIDTH - 1 - x;
  if (x < SCREENWIDTH && y < SCREENHEIGHT) {
    if (y % 2 == 0) {
      index = y * SCREENWIDTH + invertedX;
    } else {
      index = (y + 1) * SCREENWIDTH - 1 - invertedX;
    }
    strip.setPixelColor(index, color);
  }
}

unsigned int getPixel(int x, int y) {
  int index;
  int invertedX = SCREENWIDTH - 1 - x;
  if (y % 2 == 0) {
    index = y * SCREENWIDTH + invertedX;
  } else {
    index = (y + 1) * SCREENWIDTH - 1 - invertedX;
  }
  return strip.getPixelColor(index);
}

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert