Mit Arduino Mega und einer RGB-64x64-Matrix konstruieren wir jetzt eine Sanduhr. Das wohlbekannte Zeitmessgerät wird bei uns auf der Matrix grafisch
dargestellt, die nach unten fallenden Sandkörner werden durch einzelnen Pixel der Matrix imitiert. Ihre Bewegung steuert der Mikrocontroller. Die Schaltung ergänzen wir
noch mit einem Potentiometer, mit dem die Geschwindigkeit der fallenden Sandkörner verstellt werden kann. Ein aktiver Buzzer signalisiert mit einem Piepton, dass die Zeit
abgelaufen ist. Mit zwei Taster wird die Sanduhr dann zurückgesetzt oder gestartet.
Die Matrix besteht aus 64x64 = 4096 RGB-Leuchtdioden. Die Versorgungsspannung der Matrix beträgt 5 VDC. Damit ist jedoch noch nicht alles gesagt. Das Netzteil,
der die Matrix mit stromversorgt, soll in der Lage sein, bis zu 4A am Strom zu liefern.
Für die Steuerung der Matrix kann nicht jedes Arduino-Modell eingesetzt werden. Neben der Spannungsversorgung müssen insgesamt 16 Anschlüsse der Matrix
bedient werden.
Für die Erzeugung des akustischen Signals kommt ein aktiver Buzzer zum Einsatz. Der kleine Lautsprecher mit eingebauter Elektronik arbeitet mit 5 VDC. Um ein Signal zu
erzeugen, reicht es, das Gerät mit Spannung zu versorgen.
Das Programm besteht grundsätzlich aus zwei Unterprogrammen „Sand_fliesst_ab()“ und „Sand_fliesst_rein()“. Alle weiteren Unterprogramme sind diesen beiden
Hauptsequenzen untergeordnet.
Das Unterprogramm „Sand_fliesst_ab()“ stützt sich bei seinen Berechnungen auf die Tabelle (Array) „Sandkorn_oben[204]“. Die Elemente in der Tabelle werden beim Start
mit festen Werten vorbelegt. Die Felder, wo anschließend Sandkörner positioniert werden, werden mit dem Wert „1“ vorbelegt. Alle anderen Felder haben den Wert „5“. Die Tabelle
könnte man sich
wie folgt vorstellen:
In bestimmten Zeitabständen, die in ms mit „ZeitLang„ festgelegt sind, durchsucht das Programm die Tabelle und sucht die „Sandkörner“ (Felder mit 1, in der
Grafik blaue Felder), die gelöscht werden können, aus. Sie werden in der Liste „Obere_Reihe [Anzahl]“ festgehalten. Gibt es mehrere Sandkörner, die gelöscht werden können,
entscheidet der Zufall, welches Sandkorn dran ist. Das Löschen der Sandkörner imitiert den wegfließenden Sand. Sobald die Entscheidung gefallen ist, welches Sandkorn das
Feld räumen muss, wird in die Liste „fallendes_Sandkorn[x][y]“ ein neuer Eintrag eingefügt. Diese Liste stellt die einzige Schnittstelle zwischen den beiden Haupt-Unterprogrammen
dar. Die Liste enthält die Sandkörner, die sich im freien Fall befinden. Das erste Unterprogramm fügt hier neue Sandkörner ein, das zweite, sobald ein Sandkorn nach dem
Fall seine endgültige Position erreicht hat, löscht sie wieder.
Das zweite Unterprogramm „Sand_fliesst_rein()“ stützt sich auf der Tabelle „Platz_unten[15][24]“. Die Felder der Tabelle (zweidimensionales Array) bleiben zunächst
unbelegt und haben den Wert „0“. Die Tabelle kann wie folgt grafisch dargestellt werden:
Das Programm, ähnlich wie der Vorgänger, arbeitet ebenfalls in bestimmten Zeitabständen. Sie werden mit der Variable „ZeitKurz“ festgelegt. In jedem Durchlauf
untersucht das Programm die Liste der fallenden Sandkörner „fallendes_Sandkorn[x][y]“. Wird hier ein Sandkorn gefunden, wird seine Position um einen Platz nach unten verschoben.
Erreicht ein Sandkorn die Spitze der Sandpyramide, entscheidet wieder der Zufall, ob seine Bewegung nach links oder nach rechts fortgesetzt wird. Wenn das Sandkorn schließlich
eine Position erreicht hat, wo es nicht weiter geht, wird diese Stelle in der Tabelle „Platz_unten[15][24]“ mit „1“ markiert. Das Sandkorn bleibt hier endgültig stehen und sein
Eintrag wird aus der Liste „fallendes_Sandkorn[x][y]“ gestrichen. Die Durchläufe des Programms werden so oft wiederholt, bis alle Sandkörner „zu Hause“ sind.
// ***************************************************************************************************
// Sanduhr mit Arduino und RGB 64x64 Matrix
// Eine Schaltung mit Arduino Mega
// Arduino IDE 2.1.0 (2023)
// ***************************************************************************************************
#include "RGBmatrixPanel.h"
#define CLK 11
#define OE 9
#define LAT 10
#define A A0
#define B A1
#define C A2
#define D A3
#define E A4
RGBmatrixPanel matrix(A, B, C, D, E, CLK, LAT, OE, false, 64);
// Sanduhr Konturen (Linien-Koordinaten)
int Sanduhr[12][4] = { { 33, 8, 33, 20 }, { 34, 21, 40, 27 }, { 51, 8, 51, 20 },
{ 50, 21, 44, 27 }, { 40, 28, 40, 29 }, { 44, 28, 44, 29 },
{ 40, 30, 33, 37 }, { 44, 30, 51, 37 }, { 33, 38, 33, 50 },
{ 51, 38, 51, 50 }, { 33, 7, 51, 7 }, { 33, 51, 51, 51 } };
// Platzbelegung oberer Sanduhrbehälter
int Sandkorn_oben[204] = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5,
5, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5,
5, 5, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5, 5,
5, 5, 5, 5, 5, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 1, 1, 1, 1, 1, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 1, 1, 1, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 5, 5, 5 };
bool Platz_unten[15][24]; // Platzzellen Sanduhrbehälter unten
int fallendes_Sandkorn[10][3]; // Hilfstabelle mit X, Y - Koordinaten
unsigned long MillisAktuell = 0; // Hilfsvariable Milisekunden
unsigned long ZeitLang = 200; // Takt für Abfluss oberer Sanduhrbehälter
unsigned long ZeitLang_Start; // Start Zeitmessung oben
unsigned long ZeitKurz = 30; // Takt für fallende Sandkörner
unsigned long ZeitKurz_Start; // Start Zeitmessung unten
int ZufallStart; // Wert für randomSeed
int Taster_Start = 2; // Taster Sanduhr Start
int SpeedPin = A8; // Analoger Eingang
int Piepser = 3; // Signalgeber
boolean Sanduhr_laeuft = false; // Hilfsvariable Uhr läuft
int Digital_Zaehler = 110; // 109 Sandkörner
// ***************************************************************************************************
void setup() {
matrix.begin(); // Matrix initialisieren
delay(500); // Wartezeit 500 ms
pinMode(Taster_Start, INPUT_PULLUP); // Eingang Start-Taster
pinMode(Piepser, OUTPUT); // Piepser-Ausgang
Sanduhr_zeichnen(); // Sanduhrkonturen zeichnen
Sanduhr_fuellen(); // Sand einfpgen
matrix.setTextSize(1); // textgröße festlegen
zaehler(); // Zählerstand anzeigen;
}
// ***************************************************************************************************
void loop() { // Hauptprogramm
int AnalogWert = analogRead(SpeedPin); // Bestimmung der Geschwindigkeit
ZeitLang = (AnalogWert / 4) * 3 + 250; // Einfache Formeln für Geschwindigkeiten
ZeitKurz = (AnalogWert / 20) * 7 + 50;
MillisAktuell = millis(); // Milisekunden aktuell auslesen
ZufallStart++; // randomSeed-Startvariable inkrementieren
if (ZufallStart > 100) { // randomSeed-Startvariable abfragen
ZufallStart = 20; // ggf. zurücksetzen
}
if (digitalRead(Taster_Start) == LOW) { // Mit Starttaster wird die Uhr gestartet
delay(200); // Prellen abwarten
Sanduhr_laeuft = true; // Hilfsvariable auf TRUE
randomSeed(ZufallStart); // Zufalssgenerator vorbelegen
ZeitLang_Start = ZeitKurz_Start = MillisAktuell;
}
// Taktüberwachung Behälter oben
if (Sanduhr_laeuft && (ZeitLang_Start + ZeitLang) < MillisAktuell) {
digitalWrite(Piepser, LOW);
Sand_fliesst_ab();
ZeitLang_Start = MillisAktuell;
}
// Taktüberwachung Behälter unten
if (Sanduhr_laeuft && (ZeitKurz_Start + ZeitKurz) < MillisAktuell) {
Sand_fliesst_rein();
ZeitKurz_Start = MillisAktuell;
}
}
// Sanduhr Kontur zeichnen ---------------------------------------------------------------------------
void Sanduhr_zeichnen() {
screen_clear(); // Matrix löschen
for (int i = 0; i < 12; i++) { // Linien der Sanduhr zeichnen
matrix.drawLine(Sanduhr[i][0], Sanduhr[i][1], Sanduhr[i][2],
Sanduhr[i][3], matrix.Color333(0, 7, 0));
}
}
// Sanduhr füllen -----------------------------------------------------------------------------------
void Sanduhr_fuellen() {
for (int i = 0; i < 204; i++) { // Sanduhr mit "Sand" füllen
if (Sandkorn_oben[i] == 1) { // Nur Felder mit dem Wert 1
int X = i - ((i / 17) * 17) + 34; // X, Y - Koordinaten für Ausgabe
int Y = (i / 17) + 15; // berechnen
matrix.drawPixel(X, Y, matrix.Color333(0, 0, 7)); // Pixel sichtbar machen
}
}
}
// Sanduhr oben leeren ------------------------------------------------------------------------------
void Sand_fliesst_ab() {
if (Sandkorn_oben[195] == 1) { // Noch mindestens 1 Sandkorn vorhanden?
int Anzahl = 0; // In Frage kommenden Sandkörner suchen
int Obere_Reihe [15]; // Maximal 15 Sandkörner
for (int i=18; i<196; i++) { // Tabelle durchsuchen
if (Sandkorn_oben[i] == 1) { // Sandkorn (irgendein) gefunden
// Nur bestimmte Sandkörner können
// gelöscht werden:
int Wertung = Sandkorn_oben[i-18] + Sandkorn_oben[i-17] + Sandkorn_oben[i-16];
if ((Wertung == 0) or (Wertung == 5) or (Wertung == 15)) {
Anzahl++; // Anzahl gefundenen Sandkörner
Obere_Reihe [Anzahl] = i; // Nummer aus der Haupttabelle festhalten
}
}
}
if (Anzahl > 0) { // Sandkörner, die gelöscht werden können
int Reihe_Nr = random(1, Anzahl); // Sandkorn via Zufall bestimmen
int Nummer = Obere_Reihe [Reihe_Nr]; // Nummer in der Haupttabelle
int X = Nummer - ((Nummer / 17) * 17) + 34; // Matrix-Koordinaten
int Y = (Nummer / 17) + 15;
matrix.drawPixel(X, Y, matrix.Color333(0, 0, 0)); // Sandkorn löschen
Sandkorn_oben[Nummer] = 0; // Platz freimachen
for (int i = 10; i > 0 ; i--) { // Tabelle der fallenden Sandkörner sort.
fallendes_Sandkorn[i][0] = fallendes_Sandkorn[i-1][0];
fallendes_Sandkorn[i][1] = fallendes_Sandkorn[i-1][1];
fallendes_Sandkorn[i][2] = fallendes_Sandkorn[i-1][2];
}
fallendes_Sandkorn[0][0] = 1; // Neues Sandkorn auf Platz 0
fallendes_Sandkorn[0][1] = 7; // X Startkoordinate
fallendes_Sandkorn[0][2] = 1; // Y Startkoordinate
matrix.drawPixel(42, 27, matrix.Color333(0, 0, 7)); // Sandkorn sichtbar machen
zaehler(); // Zählerstand aktualisieren und anzeigen
}
}
}
// Sanduhr unten füllen -----------------------------------------------------------------------------
void Sand_fliesst_rein() {
for (int i = 0; i < 10 ; i++) {
if (fallendes_Sandkorn[i][0] == 1) { // Noch fallende Sandkörner unterwegs?
int Richtung = 0; // 0-Pos,1-Links,2-Rechts,3-runter
int X = fallendes_Sandkorn[i][1]; // Aktuelle Koordinaten
int Y = fallendes_Sandkorn[i][2];
if ((X - 1) > -1 && (Y + 1) < 24 && (X + 1) < 15) { // Weitere Bewegung möglich?
if (X == 7 && Platz_unten[X][Y + 1] == 1) { // senkrecht nach unten geht nicht
if (Platz_unten[X - 1][Y + 1] == 0 && Platz_unten[X + 1][Y + 1] == 0) {
int Zufall = random(2);
Richtung = Zufall + 1; // Zufall entscheidet ob Links/Rechts
}
}
if (Richtung == 0 && Platz_unten[X][Y + 1] == 0 && Y < 23) {
Richtung = 3; // Senkrecht runter
}
if (Richtung == 0 && Platz_unten[X - 1][Y + 1] == 0) {
Richtung = 1; // Links runter
}
if (Richtung == 0 && Platz_unten[X + 1][Y + 1] == 0) {
Richtung = 2; // Rechts runter
}
}
if (Richtung > 0) {
matrix.drawPixel(X + 35, Y + 26, matrix.Color333(0, 0, 0)); // löschen
}
switch (Richtung) {
case 0: { // Endposition erreicht
fallendes_Sandkorn[i][0] = 0; // Raus aus der Fallliste
Platz_unten[X][Y] = 1; // Platz markieren
break; }
case 1: { // Runter Links
fallendes_Sandkorn[i][1] = X - 1; // Neue X Startkoordinate
fallendes_Sandkorn[i][2] = Y + 1; // Neue Y Startkoordinate
matrix.drawPixel(X - 1 + 35, Y + 1 + 26, matrix.Color333(0, 0, 7)); // Zeichnen
break; }
case 2: { // Runter Rechts
fallendes_Sandkorn[i][1] = X + 1; // Neue X Startkoordinate
fallendes_Sandkorn[i][2] = Y + 1; // Neue Y Startkoordinate
matrix.drawPixel(X + 1 + 35, Y + 1 + 26, matrix.Color333(0, 0, 7)); // Zeichnen
break; }
case 3: { // Senkrecht Runter
fallendes_Sandkorn[i][1] = X; // Neue X Startkoordinate
fallendes_Sandkorn[i][2] = Y + 1; // Neue Y Startkoordinate
matrix.drawPixel(X + 35, Y + 1 + 26, matrix.Color333(0, 0, 7)); // Zeichnen
break; }
}
}
}
}
// Matrix Löschen ------------------------------------------------------------------------------------
void screen_clear() {
matrix.fillRect(0, 0, matrix.width(), matrix.height(), matrix.Color333(0, 0, 0));
}
// Digital Zähler anzeigen ---------------------------------------------------------------------------
void zaehler() {
matrix.setTextColor(matrix.Color333(0,0,0)); // Letzte Ausgabe zunächst löschen
matrix.setCursor(8, 45); // Cursor positionieren
matrix.print(Digital_Zaehler); // Alten Wert schreiben
Digital_Zaehler--; // Zähler dekrementieren
matrix.setTextColor(matrix.Color333(7,7,0)); // Textfarbe festlegen
matrix.setCursor(8, 45); // Wieder auf Startposition
matrix.print(Digital_Zaehler); // Neuen Wert schreiben
if (Digital_Zaehler == 0) {
digitalWrite(Piepser, HIGH); // Piepser einschalten
}
}
// ***************************************************************************************************