Heute zeige ich ein Arduino-Projekt, das ich auf MakerWorld.com entdeckt habe. Es handelt sich um eine Uhr, die angetrieben wird mit nur einen Schrittmotor. Faszinierend dabei ist, das der Stundenzeiger scheinbar magnetisch gesteuert wird und somit der Eindruck entsteht das dieser scheinbar in der Luft schwebt.
Es ist ein Remix des Projekts namens “Hollow Clock 4” von Shiura.

Was mich an diesem Projekt aber auch begeistert, ist der durchdachte Antrieb. Auf der Antriebswelle, die mit dem Schrittmotor verbunden ist, sitzt ein Schneckenrad und zusätzlich ein Zahnrad, die gleichzeitig den Stunden- und Minutenzeiger antreiben. Im Video zeige ich detaillierter, wie die Bauteile zusammen gebaut werden und funktionieren.
Hardware:
Nano Board
Stepper Motor 28BYJ-48 5V mit ULN2003 Treiberplatine / Datenblatt
2 Draht USB-C Buchse (Funktioniert mit USB-A Netzteil mit entsprechenden Kabel)
Irgend ein übrig gebliebenes USB-A Netzteil mit Kabel
Selbstschneidende Schraube M2.6×12
Magnete 8x3mm
Ein paar Details zum Schrittmotor 28BYJ-48:
Der Motor hat vier Spulen. Er benötigt 8 Halbschritte pro Umdrehung, was 45° pro Schritt entspricht.
Der Motor hat ein integriertes Getriebe, das die Präzision so verfeinert, das 64 Teilschritte an der Ausgangswelle pro Umdrehung notwendig sind.
In diesem Projekt wird der Motor mit einem ULN2003A-Treibermodul gesteuert, das die Steuerung mit dem Arduino Nano ermöglicht. Das Modul aktiviert die Spulen in der vom Skript vorgegebenen Reihenfolge, so dass sich der Rotor Schritt für Schritt in die gewünschte Richtung bewegt.
Warum USB-A Netzteil:
Da bei der verwendeten USB-C-Buchse nur GND und VBUS verdrahtet wurden, würde ein USB-C-Netzteil auf Pin CC1 oder CC2 kein Signal erhalten, um zu erkennen, dass das angeschlossene Gerät Strom benötigt und die Uhr würde nicht starten.
Das USB-A-Netzteil mit seinen vier Adern liefert den benötigten Strom einfach ohne dieses Startsignal.
Wer es mit einem USB-C Netzteil betreiben will, sollte es mit dieser 6 Draht Buchse versuchen, muss aber zusätzlich den CC-Pins (CC1 oder CC2) mit einem Widerstand (meistens 5.1kΩ) nach GND verbinden.
3D Druck Dateien:
Hollow Clock 4 von shiura Lösung original
+
Hollow Clock 4 Remix Remix und Neugestaltung von rotor- und rear-cover, case mit cap (ohne Verschraubung)
oder
optional das Case Gehäuse mit Öffnung für USB-C Buchse

Begonnen hatte ich damit, mir die zu Verfügung gestellten Dateien anzusehen und mir somit einen ersten Überblick zu verschaffen. Sehr erfreut war ich, als ich sah, dass auch mehrere Sketche für die Arduino IDE mit dabei lagen.
Ein wichtiger Teil dieses Projekts ist der Stundenzeiger, der mit einem Magneten ausgestattet ist. Ich habe festgestellt, dass der Zeiger bei drei und neun Uhr manchmal nach unten fiel, da der Magnet zu schwach war. Daher habe ich den auch beiliegenden 5mm längeren Stundenzeiger nachgedruckt und schon war das Problem beseitigt.
Nachdem alle Teile gedruckt waren, ging es an den Zusammenbau.
Verdrahtung:

Wie man bei der Verdrahtung sehen kann, wird der Nano und das Treiber-Modul direkt von dem USB Netzteil mit Strom versorgt. Das geht in meinem Fall da der eingesetzte Schrittmotor mit 5 Volt läuft.
Nicht in Versuchung geraten, das Treiber-Modul vom Entwicklungsboard zu versorgen, das würde dem Nano nicht gut tun.
Dann habe ich den Schrittmotor mit dem beiliegenden Sketch getestet.
Um Platz im Gehäuse zu erhalten, habe ich das Kabel des Motors deutlich gekürzt.
Und da ich schon mal am löten war, wurden auch gleich die Kabel vom Treiber-Modul zum Arduino mit gekürzt, sowie die zusätzlich Kabel für die externe Stromversorgung hinzugefügt.

Mit den gekürzten Kabeln konnte ich alles kompakter gestalten und besser in das kleine Gehäuse einbauen. Stromkabel geht zu Arduino und Treiber-Modul.

Nachdem ich den Uhrenaufbau montiert und einen kleinen Deckel auf der Rückseite angebracht hatte, sah das Ganze schon sehr schick aus. Siehe erstes Bild oben.
Arduino-Sketch:
Zum Schluss habe ich das Ganze mit dem beiliegenden Test-Sketch bespielt, der den Minutenzeiger jede Sekunde statt alle 60 Sekunden weiterlaufen lässt. Dies ermöglichte eine schnelle Überprüfung der Funktion. (und funktionierte tadellos)
Test-Quellcode:
// wait for a single step of stepper
int delaytime = 1;
// ports used to control the stepper motor
// if your motor rotate to the opposite direction,
// change the order as {5, 4, 3, 2};
int port[4] = {2, 3, 4, 5};
// sequence of stepper motor control
int seq[8][4] = {
{ LOW, HIGH, HIGH, LOW},
{ LOW, LOW, HIGH, LOW},
{ LOW, LOW, HIGH, HIGH},
{ LOW, LOW, LOW, HIGH},
{ HIGH, LOW, LOW, HIGH},
{ HIGH, LOW, LOW, LOW},
{ HIGH, HIGH, LOW, LOW},
{ LOW, HIGH, LOW, LOW}
};
void rotate(int step) {
static int phase = 0;
int i, j;
int delta = (step > 0) ? 1 : 7;
step = (step > 0) ? step : -step;
for(j = 0; j < step; j++) {
phase = (phase + delta) % 8;
for(i = 0; i < 4; i++) {
digitalWrite(port[i], seq[phase][i]);
}
delay(delaytime);
}
// power cut
for(i = 0; i < 4; i++) {
digitalWrite(port[i], LOW);
}
}
void setup() {
pinMode(port[0], OUTPUT);
pinMode(port[1], OUTPUT);
pinMode(port[2], OUTPUT);
pinMode(port[3], OUTPUT);
}
void loop() {
for(int i = 0; i < 10; i++) {
rotate(-512*11);
}
// delay(1000);
}Das finale Original-Sketch, veranlasst den Motor etwa alle 60 Sekunden kurz anzulaufen. Jetzt muss sich zeigen wie genau die Uhr dann läuft.
Finaler Quellcode:
// Hollow Clock 4 from Shiura - thingiverse
// Please tune the following value if the clock gains or loses.
// Theoretically, standard of this value is 60000.
#define MILLIS_PER_MIN 60000 // milliseconds per a minute
// Motor and clock parameters
// 4096 * 90 / 12 = 30720
#define STEPS_PER_ROTATION 30720 // steps for a full turn of minute rotor
// wait for a single step of stepper
int delaytime = 2;
// ports used to control the stepper motor
// if your motor rotate to the opposite direction,
// change the order as {5, 4, 3, 2};
int port[4] = {2, 3, 4, 5};
// sequence of stepper motor control
int seq[8][4] = {
{ LOW, HIGH, HIGH, LOW},
{ LOW, LOW, HIGH, LOW},
{ LOW, LOW, HIGH, HIGH},
{ LOW, LOW, LOW, HIGH},
{ HIGH, LOW, LOW, HIGH},
{ HIGH, LOW, LOW, LOW},
{ HIGH, HIGH, LOW, LOW},
{ LOW, HIGH, LOW, LOW}
};
void rotate(int step) {
static int phase = 0;
int i, j;
int delta = (step > 0) ? 1 : 7;
int dt = 20;
step = (step > 0) ? step : -step;
for(j = 0; j < step; j++) {
phase = (phase + delta) % 8;
for(i = 0; i < 4; i++) {
digitalWrite(port[i], seq[phase][i]);
}
delay(dt);
if(dt > delaytime) dt--;
}
// power cut
for(i = 0; i < 4; i++) {
digitalWrite(port[i], LOW);
}
}
void setup() {
pinMode(port[0], OUTPUT);
pinMode(port[1], OUTPUT);
pinMode(port[2], OUTPUT);
pinMode(port[3], OUTPUT);
rotate(-20); // for approach run
rotate(20); // approach run without heavy load
rotate(STEPS_PER_ROTATION / 60);
}
void loop() {
static long prev_min = 0, prev_pos = 0;
long min;
static long pos;
min = millis() / MILLIS_PER_MIN;
if(prev_min == min) {
return;
}
prev_min = min;
pos = (STEPS_PER_ROTATION * min) / 60;
rotate(-20); // for approach run
rotate(20); // approach run without heavy load
if(pos - prev_pos > 0) {
rotate(pos - prev_pos);
}
prev_pos = pos;
}Warum hier kein delay() sondern millis() eingesetzt wird:
In der finalen Version der Uhr wird millis() verwendet, weil die Uhr minutengenau arbeiten muss, ohne dass der Arduino durch blockierende Wartezeiten ausgebremst wird.
Ein delay(60000) würde die gesamte Loop für eine Minute anhalten – damit könnte der Sketch keine Zwischenberechnungen durchführen, keine Positionsdifferenzen ermitteln und keine Korrekturläufe ausführen.
Mit millis() lässt sich dagegen präzise feststellen, wann eine neue Minute begonnen hat, ohne den Programmablauf zu stoppen.
Der Code prüft lediglich, ob millis() / MILLIS_PER_MIN einen neuen Minutenwert ergibt, und führt dann die notwendigen Motorbewegungen aus.
Dadurch bleibt die Loop jederzeit reaktionsfähig, die Uhr läuft stabiler, und der Motor kann vor jedem Schritt die kurzen „Approach‑Runs“ durchführen, die im Sketch vorgesehen sind.
Video:
Update nach 4 Wochen:
Die Uhr lief bisher 4 Wochen ohne Probleme und zeigte immer die richtige Uhrzeit an.
Heute jedoch hatte die Antriebswelle das lose liegende Antriebsrad nach oben gedrückt und die Zeiger blieben stehen.
Ich hatte alle Bauteile die alle samt am 3D Drucker mit einer 0,4 mm Düse (Nozzles) gedruckt wurden, nun mit einer 0,2 mm Düse für den Antrieb neu gedruckt.

Bild: Deutlich zu sehen, links die feinere Oberfläche.
Update nach 49 Tagen:
Der Motor der Uhr hat anfangen, sich stundenlang unkontrolliert zu drehen.
Hier sind die Fakten dazu, warum das passiert und wie du das beheben kannst:
1. Der Variablen-Überlauf (Integer Overflow)
Das Skript berechnet die Position des Zeigers fortlaufend über die vergangenen Minuten. Dafür nutzt es Variablen vom Typ long (eine Datenform für große Zahlen). Auf einem Arduino Nano kann diese Variable maximal den Wert 2.147.483.647 speichern.
Nach 48,5 Tagen Dauerbetrieb erreicht die Uhr Minute 69.906. Die Berechnung im Code lautet dann: 30720 * 69.906. Das ergibt rund 2,147 Milliarden. Genau hier sprengt das Skript die Kapazitätsgrenze. Die Variable „läuft über“ und springt plötzlich in einen riesigen Minusbereich. Das Programm denkt auf einmal, der Zeiger befände sich extrem weit im Rückwärtsgang.
2. Der eigentliche millis()-Überlauf
Die Funktion millis() (welche die Millisekunden seit dem Start des Mikrocontrollers zählt) erreicht nach exakt 49,7 Tagen ihren absoluten Maximalwert und springt wieder auf 0 zurück.
Wenn das passiert, rechnet der Code die aktuelle Position (die durch den Reset der Zeit nun wieder 0 ist) abzüglich der fehlerhaften Minus-Zahl aus dem ersten Problem aus. Das Ergebnis: Die Funktion rotate() erhält die Differenz und damit plötzlich den Befehl, den Motor um rund 34 Millionen Schritte vorwärts zu bewegen.
Die Folge: Der Schrittmotor dreht sich für die nächsten 19 Stunden ununterbrochen (das entspricht über 1.100 Umdrehungen des Minutenzeigers), bis er die vermeintliche Position wieder eingeholt hat.
3. Die Lösung für das Arduino-Skript
Der ursprüngliche Entwickler der Uhr hat diese Berechnung eingebaut, um mögliche Rundungsfehler zu vermeiden, falls sich die Anzahl der Motorschritte nicht glatt durch 60 teilen lässt.
Bei deinem Setup ist der Wert STEPS_PER_ROTATION aber auf exakt 30.720 eingestellt. Teilt man das durch 60, erhält man exakt 512 Schritte pro Minute. Es gibt keine Kommastellen und somit auch keine Rundungsfehler.
Daher kannst du den Fehler komplett entfernen und auf die klassische Zeitmessung umsteigen. Ersetze deine loop()-Funktion einfach durch diese saubere Variante:
void loop() {
// unsigned long = Variablen-Typ ausschließlich für positive Zahlen
static unsigned long vorherigeMillisekunden = 0;
const unsigned long intervall = 60000; // 60.000 Millisekunden = 1 Minute
unsigned long aktuelleMillisekunden = millis();
// Prüfen, ob eine Minute vergangen ist
if (aktuelleMillisekunden - vorherigeMillisekunden >= intervall) {
vorherigeMillisekunden = aktuelleMillisekunden;
rotate(-20); // kurzes Zurücksetzen für Anlauf
rotate(20); // Anlauf ohne Last
// Motor exakt 512 Schritte (30720 / 60) weiterbewegen
rotate(STEPS_PER_ROTATION / 60);
}
}Mit dieser Methode zieht das Skript immer die alte Zeit von der neuen Zeit ab. Selbst wenn millis() nach 49,7 Tagen wieder bei 0 anfängt, stimmt diese Differenz mathematisch weiterhin (durch die Besonderheiten des Datentyps „unsigned long“).
Die Uhr läuft so endlos fehlerfrei weiter. Ende gut alles gut 🙂
