8x8-LED-Matrix als Geek Uhr verwenden (Zeit über Serial)

Mit der 8x8-LED-Matrix aus dem letzten Projekt muss sich doch auch was Nützlicheres anstellen lassen als nur eine sich ständig wiederholende Laufschrift.

Da kam mir gleich eine geeky Uhr in den Sinn. Mein erster Gedanke war, eine analoge Uhr abzubilden, aber als ich die Zeichnungen der Uhrzeiger und die korrespondierenden Bitmuster dazu fertig hatte empfand ich das Ergebnis als zu pixelig, als das man die Uhrzeit hätte gut genug erkennen zu lassen.

Also blieb nur eine Digital-Uhr. Da ich ja schon immer der Meinung war, dass der Tag 24 Stunden hat und eine Uhr deshalb auch 24 Stunden anzeigen soll, waren die ersten drei Zeilen schon mal klar: Hier würde die Stunde angezeigt werden von Null (alles aus) bis 23 (alle bis auf die letzte LED an). Das gefiel mir auch ganz gut, weil die erste Zeile dann für die Schlafphase 00:00 bis 08:00 Uhr steht, die zweite Zeile für die Arbeitsphase von 08:00 bis 16:00 und die dritte Zeile für die Feierabend-Phase (17:00-23:59). Und wenn alle LEDs leuchten ist es so langsam Zeit, ins Bett zu gehen.

Bleiben 40 LEDs. Zu wenig für 60 Minuten. Erst dachte ich: Nun, dann machst du pro 2 Minute eine LED an, aber dann gefiel mir die Idee besser, eine Zeile für eine Viertelstunde anzuschalten. Das lässt sich besser ablesen nach dem Motto: "Aha, zwei Zeilen an und eine dazu, dann ist es kurz nach halb".

Blieb noch eine letzte Zeile für die Sekunden. Nun könnte man natürlich alle 10 Sekunden eine LED anmachen und die letzte LED vielleicht als Sekunden-Flasher. Aber ich habe mich dann für eine binäre Darstellung entschieden - nein, nicht, weil ich da programmtechnisch überhaupt nichts umzurechnen hätte - sondern um den Nerd-Faktor ein wenig zu heben.

Meine Skizze sah also so aus:
oooooooo Stunden 00-08 oooooooo Stunden 09-16 ooooooo. Stunden 17-23 oooooooo Minuten 00-15 oooooooo Minuten 16-30 oooooooo Minuten 31-45 oooooooo Minuten 46-59 ..oooooo Sekunden binär Auf dem Beispiel rechts ist es also 11 Uhr und 10 oder 11 Minuten. Da die Minutenangabe nur auf 2 Minuten genau ist, ist die Sekunden-Anzeige eigentlich nur noch Zierde. Aber nichtsdestotrotz: das wären 2+4+8+32 = 46 Sekunden.

Jetzt bleibt aber noch eine bedeutende Frage offen: Woher weiß der Arduino die Zeit? Ein Raspberry könnte übers WLAN einen NTP-Server danach fragen, aber unser Arduino hat nur mit der seriellen Schnittstelle am USB-Bus eine Verbindung zur Außenwelt. Also muss es darüber gehen, will ich nicht ein paar Taster für Set, Time+ und Time-, wie sie an jedem Reisewecker vorzufinden sind, einbauen. Elegant wird die Lösung dann, wenn der Arduino die Zeit in einem RTC (Realtime Clock) -Modul speichern kann und dank Batteriepufferung auch behalten kann. Aber die bestellten Module sind natürlich noch auf dem Weg aus Fernost.


Den Serial Monitor hatten wir ja schon im Teil Fotowiderstand mit Arduino auslesen und über serielle Schnittstelle darstellen angeschnitten. Außer Daten vom Arduino zum PC zu übermitteln geht es auch in die andere Richtung, also so, dass der Arduino lesend darauf zugreift.

Schicken wir eine Zeile in der Arduino IDE ab, so wird sie an den Arduino gesendet und mit einem Zeilenende-Zeichen (Linefeed, LF) abgeschlossen.

Wenn wir im Arduino auf eine Eingabe warten wollen, dann sammeln wir in einer Schleife alle ankommende Zeichen von der seriellen Schnittstelle ein und warten bis zur Ankunft des LF-Zeichens, womit wir dann wissen, dass die Eingabe abgeschlossen ist und wir die Schleife verlassen können: #define LenInputString 10 boolean stringComplete=false; inputString.reserve(LenInputString); ... while (stringComplete == false) { while (Serial.available()) { char inChar = (char) Serial.read(); // ein Byte von der ser. Schnittstelle holen if (inChar == '\n') { // Eingabe fertig? stringComplete = true; } else { // inputString um Zeichen erweitern if (inputString.length() < LenInputString -1) inputString += inChar; } } } Das ist auch schon alles. Damit hätten wir die genaue Zeit. Für das Weiterzählen missbrauchen wir einfach den Befehl millis(), der uns die Zeit seit dem letzten Reset des Arduinos liefert. Wir merken und die Zeit bei der Eingabe und fragen dann jeweils die aktuelle ab. So wissen wir, wieviele Millisekunden seitdem vergangen sind und können das auf die Eingabezeit draufrechnen, womit wir wieder die aktuelle Zeit hätten.

Wir müssen allerdings im Hinterkopf behalten, dass der Arduino die Millis in einer 32-Bit-Unsigned-Integer-Variablen speichert. Nach 232 Zyklen springt der Zähler wieder auf Null, das sind 49 Tage, 17 Stunden, 2 Minuten und 47 Sekunden, um es mal genau zu sagen. Oder etwas einfacher (und ungenauer) ausgedrückt: Nach 7 Wochen fängt die Uhr das Spinnen an!

Man kann übrigens das USB-Kabel und eine Batterieversorgung oder ein Netzteil gleichzeitig an den Uno anschließen. Zieht man dann das USB-Kabel ab, dann läuft die Uhr weiter. Dann könnte man sie 7 Wochen wo hinstellen, müsste sie aber spätestens dann wieder an den PC anschließen. Es sei denn, man berücksichtigt das im Code, dann sollte es auch ohne gehen. Allerdings stellt sich dann die Frage, wie genau die Millis sind auf einen so langen Zeitraum gesehen. Es bleibt dabei: gescheit wird die Uhr erst, wenn sie noch ein RTC-Modul spendiert bekommt.

Das Ergebnis



Durch die kreisende Animation zeigt der Arduino an, dass er auf eine Eingabe der Zeit auf der seriellen Schnittstelle warten. Ist die erfolgt, wird sie überprüft.

Ist die Eingabe nicht gültig, wird ein X angezeigt und die Eingabe muss wiederholft werden (Kreis). War die Eingabe gültig, erscheint kurz ein Checkmark als Bestätigung und danach die Uhr.

Der Source-Code

//////////////////////////////////////////////////////// // (C) 2018 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// #include <stdio.h> #include <stdint.h> #include <unistd.h> #include <time.h> #include <string.h> #define PinData 2 #define PinClockStore 3 #define PinClockShift 4 #define ClockPulseLen 1 // in Microsekunden #define LenInputString 10 void setup() { // put your setup code here, to run once: pinMode(PinData, OUTPUT); pinMode(PinClockStore, OUTPUT); pinMode(PinClockShift, OUTPUT); // initialize serial: Serial.begin(9600); } void storeTick() { // einen Puls auf die ClockStore-Leitung schicken digitalWrite(PinClockStore, HIGH); delayMicroseconds (ClockPulseLen); digitalWrite(PinClockStore, LOW); delayMicroseconds (ClockPulseLen); } void shiftTick() { // einen Puls auf die ClockShift-Leitung schicken digitalWrite(PinClockShift, HIGH); delayMicroseconds (ClockPulseLen); digitalWrite(PinClockShift, LOW); delayMicroseconds (ClockPulseLen); } void clearDots() { // alle Punkte löschen for (int i = 0; i < 16; i++) { digitalWrite(PinData, LOW); shiftTick(); // nächstes Bit } storeTick(); // alle Bits fertig. Speichern } void lightCol(uint8_t col, uint8_t colByte) { // Vorwiderstände hängen an den Row-Ausgängen, sonst keine gleichmäßige Helligkeit // zuerst das colByte invertiert for (uint8_t b = 0; b < 8; b++) { uint8_t w = (b == (7 - col)); w ^= 1; // invertieren, weil wird müssen auf Low und damit zu GND durchschalten digitalWrite(PinData, w); // w = 0 | 1 shiftTick(); // nächstes Bit } // dann das rowByte einschieben for (uint8_t b = 0; b < 8; b++) { uint8_t w = (colByte & 1); digitalWrite(PinData, w); // w = 0 | 1 shiftTick(); // nächstes Bit colByte = colByte >> 1; } storeTick(); // alle Bits fertig. Speichern } void lightDot(uint8_t x, uint8_t y) { lightCol(x, 1 << (7 - y)); } void lightMatrix(uint8_t *rowArray, unsigned long msecs) { uint8_t colArray[8] = {0, 0, 0, 0, 0, 0, 0, 0}; for (uint8_t col = 0; col < 8; col++) { for (uint8_t row = 0; row < 8; row++) { colArray[col] += (rowArray[7 - row] & 128) / 128 * (1 << row); //printf ("col %d, row %d, bit=%d, macht %d\n",col,row,(rowArray[row] & 128)/128,(rowArray[row] & 128)/128 * (1<<row)); rowArray[7 - row] = rowArray[7 - row] << 1; } // for (int i=0;i<8;i++) printf("col %d: %d\n",col, colArray[col]); } for (unsigned long mStop = millis() + msecs; millis() < mStop; ) { for (uint8_t col = 0; col < 8; col++) { lightCol(col, colArray[col]); delayMicroseconds(500); //clearDots(); //delayMicroseconds(10); } } clearDots(); } // ------------------------------------------------ void loop(void) { String inputString = ""; // a String to hold incoming data boolean stringComplete = false; // whether the string is complete //Speicher für den inputString reservieren inputString.reserve(LenInputString); uint8_t good[8] = { 0b00000000, 0b00000011, 0b00000110, 0b00001100, 0b11011000, 0b11110000, 0b01100000, 0b00000000 }; uint8_t bad[8] = { 0b00000000, 0b11000011, 0b01100110, 0b00111100, 0b00011000, 0b00111100, 0b01100110, 0b11000011 }; uint8_t zeit[8] = { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000 }; clearDots(); // kleine Warte-Animation /* 0 1 2 3 4 5 6 7 . . # # # # . . 0 . # . . . . # . 8 # . . . . . . # 16 # . . . . . . # 24 # . . . . . . # 32 # . . . . . . # 40 . # . . . . # . 48 . . # # # # . . 56 */ uint8_t dots[20] = {2, 3, 4, 5, 14, 23, 31, 39, 47, 54, 61, 60, 59, 58, 49, 40, 32, 24, 16, 9}; boolean okay = false; long h=-1; long m=-1; long s=-1; uint32_t secsOnSet=-1; while (!okay) { // auf Zeiteingabe über Serial Monitor warten // time# und Senden while (stringComplete == false) { for (int i = 0; i < 20; i++) { while (Serial.available()) { char inChar = (char) Serial.read(); // ein Byte von der ser. Schnittstelle holen if (inChar == '\n') { // Eingabe beendet ? stringComplete = true; } else { // inputString um Zeichen erweitern if (inputString.length() < LenInputString -1) inputString += inChar; } } // Warte-aufEingabe-Animation abspielen uint8_t x = dots[i] % 8; uint8_t y = dots[i] / 8; lightDot(x, y); if (stringComplete) break; delay (10); } } Serial.println(inputString); secsOnSet = millis() / 1000; // 20:32:21 // 01234567 // Eingabe überprüfen h=-1; m=-1; s=-1; if (inputString.length() == 8 ) { char t = inputString[0]; // Stunde-Zehner if (t == '0' || t == '1' || t == '2') { h = (t-48) * 10; t = inputString[1]; // Stunde-Einer if (isDigit(t)) { h += (t-48); if (inputString[2] == ':') { t = inputString[3]; // Minute-Zehner if (t == '0' || t == '1' || t == '2' || t == '3' || t == '4' || t == '5') { m = (t-48) * 10; t = inputString[4]; // Minute-Einer if (isDigit(t)) { m += (t-48); if (inputString[5] == ':') { t = inputString[6]; // Sekunde-Zehner if (t == '0' || t == '1' || t == '2' || t == '3' || t == '4' || t == '5') { s = (t-48) * 10; t = inputString[7]; // Sekunde-Einer if (isDigit(t)) { s += (t-48); okay = true; } } } } } } } } } if (!okay) { inputString=""; stringComplete=false; clearDots(); lightMatrix (bad, 2000); clearDots(); } } // und nochmal // checkmark zur Bestätigung clearDots(); lightMatrix (good, 2000); clearDots(); long hours=h; long minutes=m; long seconds=s; Serial.print (hours); Serial.print (":"); Serial.print (minutes); Serial.print (":"); Serial.print (seconds); Serial.println (""); while (1) { // Zeit anzeigen // zum Zeitpunkt secsOnSet war es die übergebene Zeit plus 1 Sekunde (wegen abschicken) long secsSince0 = (hours*3600. + minutes*60. + seconds + 1) + millis()/1000. - secsOnSet; h = secsSince0 / 3600; secsSince0 -= h*3600; h = h % 24; // 24 Uhr ist wieder 0 Uhr m = secsSince0 / 60; secsSince0 -= m*60; s = secsSince0; Serial.print (h); Serial.print (":"); Serial.print (m); Serial.print (":"); Serial.print (s); Serial.println (""); // oooooooo 1-8 h // oooooooo 9-16 h // ooooooo. 17-23h // oooooooo 1-15 m // oooooooo 16-30 m // oooooooo 31-45 m // oooooooo 46-59 m // ..oooooo 60 Sek binär for (int i=0; i<8; i++) { zeit[i]=0; } //stunden for (int i=0; i<8; i++) { if (h > i) zeit[0] |= (1<<(7-i)); } for (int i=0; i<8; i++) { if (h > i+8) zeit[1] |= (1<<(7-i)); } for (int i=0; i<8; i++) { if (h > i+16) zeit[2] |= (1<<(7-i)); } //minuten for (int i=0; i<8; i++) { if (m*8/15. > i+1) zeit[3] |= (1<<(7-i)); } for (int i=0; i<8; i++) { if (m*8/15. > i+9) zeit[4] |= (1<<(7-i)); } for (int i=0; i<8; i++) { if (m*8/15. > i+17) zeit[5] |= (1<<(7-i)); } for (int i=0; i<8; i++) { if (m*8/15. > i+25) zeit[6] |= (1<<(7-i)); } //sekunden zeit[7]=s; clearDots(); lightMatrix (zeit, 1000); } } Die Schaltung und die Erklärung, wie über die Steuerleitungen die beiden 74HC595 angesteuert werden, die wiederum die Matrix ansteuern findet sich auf der Raspberry Projektseite.