Projektvorstellung

UHR3 - die dritte Version meiner TEXTUHR auf Arduino-Basis



+++UPDATE+++: Inzwischen gibt es UHR3.1 +++ Infos dazu hier!

Die Idee:


Die Grundidee stammt nicht von mir. Irgendwann habe ich so eine Uhr nach ähnlichem Prinzip im Internet gesehen. Die erste Reaktion war: HABEN WILL! Als ich den Preis von mehreren hundert Euro sah, verfolg diese Begeisterung. Aber ganz vergessen hatte ich dass Ding nicht.

Als eines meiner ersten Arduino-Projekte baute ich um 2012 eine Textuhr (zur Projektbeschreibung) auf Basis eines Arduino nano und Schiebregistern. Diese existiert inzwischen nicht mehr - hat mir nicht mehr gefallen :)

Als nächstes folgte eine kleinere Version 2, die noch läuft und auf einem Standalone ATMega 328 basiert. Ansonsten unterscheidet sie sich nur im Gehäuse. Davon gibt es keine auführliche Projektbeschreibung, aber ein kurzes Video.

UHR3 war eigentlich von der Idee her nicht als Textuhr, sondern als DCF-gesteuerter Adventskranz gedacht. Ursprünglich hätte ein ATTiny45 das DCF77-Signal entschlüsseln, die Adventssonntage berechnen und entsprechend Kerzen anschalten sollen. Bis ich allerdings die manuelle Entschlüsselung des Signals einigermaßen im Griff hatte, war der Advent fast vorbei und ich entschied mich, eine Uhr mit Adventskranz-Addon zu entwickeln.

 



Das Gehäuse:

Ich zähle mich nach wie vor zu den handwerklich eher Minderbegabten. Aber seit ich mir eine CNC-Fräse zugelegt habe (eigentlich nur zum Platinen-Fräsen), ersetzt die Maschine einiges an fehlendem Geschick :-)

Das Gehäse der UHR3 besteht aus einer Plexiglasröhre Ø 90/84mm (aussen/innen) und ca 70cm Länge (original 1m, zugeschnitten und eben geschliffen) Darin sind Polysytrol-Platten mit 1,5mm verbaut, in die von hinten die Texte eingefräst wurden, sodass sie ohne Hintergrundbeleuchtung nicht besonders sichtbar sind. Die Hintergrundbeleuchtung erfolgt durch einen RGB-LED-Stripe mit 60 WS2812-LEDs pro Meter - hier sind es 36 Stück. Jeweils zwei LEDs beleuchten ein Textfeld. Nach hinten sind die Felder durch eine 0,5mm schwarze Polysytrol-Platte geschlossen. Zwischen den einzelnen Feldern sorgen 3mm Sperrholzplatten für Stabilität und Abtrennung, die auf zwei dünnen Stäben geführt werden. Die Bilder rechts sagen hier bestimmt mehr als 1000 Worte.

Im Sockel, der auch zum besseren Stand beiträgt, ist die Elektronik verbaut.

Der Bau erfolgte "Upside-Down" - es wurden von oben nach unten die Anzeigeelemente in die Röhre geschoben und erst am Ende der Sockel montiert. Während der Bauphase lief parallel der Testbetrieb über einen Arduino UNO.


Textelemente und Trenner


Der noch leere Sockel

Zusammenbau der Elemente

Elektronik und Aufbau:

Das Herz der Uhr ist ein ATMega328, der neben Spannungsregler und sonstiger Minimalbeschaltung (Quarz/Kondensatoren/Widerstände) auf einer selbstgefrästen Platine werkelt. Die Platine hat ansonsten nur noch jede Menge Pfostenstecker, über die die gesamte Peripherie angeschlossen ist:


Die Hardware im "Beta-Stadium"

Die Hauptplatine beim Fräsen.

Sicht von unten auf die Elektronik: In der Mitte die Hauptplatine mit dem ATMega, darüber die RTC-Platine. Das dicke schwarze Kabel ist geschirmt und geht zum DCF77-Modul



Der Sketch (das "Programm"):


Der Arduino-Sketch sollte sich eigentlich von selbst erklären - besonders nachdem ich mir diesmal die Mühe gemacht habe, ihn komplett zu kommentieren.

#include <Adafruit_NeoPixel.h>                            // Library für Stripe-Ansteuerung einbinden
#include <Time.h>                                         // Library mit Zeit-Funktionen einbinden
#include \"DCF77.h\"                                        // Library für DCF77-Modul einbinden
#include \"Wire.h\"                                         // Library für I2C (RTC) einbinden
#include \"UC121902-TNARX-A.h\"                             // Library fürs Display einbinden
UC121902_TNARX_A::Display display (6,7,8);                // Display einstellen an Pins 6,7 und 8
#define PIXELPIN A0                                       // Datenpin für Stripe: A0
#define NUMPIXELS 37                                      // Stripe hat 37 LEDs/Pixels
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIXELPIN, NEO_GRB + NEO_KHZ800);    //Stripe-Daten einstllen
#define DS3231_ADDRESS 0x68                               // I2C Adresse für RTC
#define DCF_PIN 2	                                        // Pin für DCF77-Modul festlegen
#define DCF_INTERRUPT 0		                                // Interrupt für DCF77 festlegen
#define hsensor A1                                        // Pin für Helligkeitssensor festlegen
const byte zero = 0x00;                                   // Hilfsvariable für I2C-Kommunikation
time_t DCFtime;                                           // Variable für die DCF-Zeit
DCF77 DCF = DCF77(DCF_PIN,DCF_INTERRUPT);                 // DCF77 einstellen
tmElements_t tm;                                          // zum Berechnen des Adventskalenders
const int advPin[]={0,9,10,11,12};                        // Pins für Adventskerzen
unsigned long lastdcf=0;                                  // Variable zum Speichern, wann letzter DCF-Sync war
unsigned long lastrtc=0;                                  // Variable zum Speichern, wann letzter RTC-Sync war
byte lastsecond;                                          // Variable zum Speichern des letzten Sekundenwertes
byte lastminute;                                          // Variable zum Speichern des letzten Minutenwertes  
byte lastday;                                             // Variable zum Speichern des letzten Tageswertes  
String disp;                                              // String-Variable für Displayinhalt
byte dispcount;                                           // Zähler zum Wechsel des Displaywertes
int hell;                                                 // Helligkeitswert aus dem Sensor
int ledhell=255;                                          // Helligkeit der LEDs
int lasthell;                                             // Variable zum Speichern der letzten LED-Helligkeit
int mehrrot;                                              // Variable für höheren Rot-Wert (abends)
int mehrblau;                                             // Variable für höheren Blau-Wert (morgens)

void setup() {                                            // Initalisierungen (wird nur am Anfang 1x ausgeführt)
  DCF.Start();                                            // Initalisieren von DCF
  Wire.begin();                                           // Initalisieren der RTC-Kommunikation
  display.begin();                                        // Initalisieren des Displays
  pixels.begin();                                         // Initalisieren des Stripes
  delay(30);                                              // kurze Pause
  pinMode(hsensor, INPUT);                                // Pin für Helligkeitssensor als Eingang setzen
  for (int jj=1;jj<5;jj++){
    pinMode(advPin[jj],OUTPUT);                           // Adventskerzen-Pins als Ausgang definieren
  }  
  ledlauf();                                              // alle LEDs durchlaufen lassen
}

void loop() {                                             // Hauptprogramm (läuft endlos)
  if (now()-lastdcf > 90 || lastdcf==0) dcfsync();        // DCFsync starten, wenn noch nicht erfolgt nach 90 Sek 
  if (now()-lastrtc > 360 || lastrtc==0) rtcget();        // RTC-Zeit holen, wenn nicht erfolgt oder nach 6 Minuten
  if(second()!=lastsecond){                               // wenn sich Sekundenwert geändert hat (= 1 Sek vorbei)...
    hell=analogRead(hsensor);                             // Helligkeitswert einlesen
    ledhell=20;                                           // Helligkeit erst auf Minimum setzen und dann...
    if (hell>100) ledhell=30;                             //  
    if (hell>300) ledhell=60;                             //
    if (hell>500) ledhell=80;                             // ...je heller desto höher
    if (hell>800) ledhell=120;                            //
    if (hell>900) ledhell=220;                            //
    if (ledhell!=lasthell){                               // falls sich die Helligkeit geändert hat...
      zeitzuled(hour(),minute());                         // Zeit auf den Stripe ausgeben (mit neuer Helligkeit) und
      lasthell=ledhell;                                   // Helligkeit für nächsten Vergleich merken
    }
    zeitdisplay();                                        // Displayausgabe
    lastsecond=second();                                  // Sekundenwert für nächsten Vergleich merken
  }
  if(minute() != lastminute){                             // wenn sich Minutenwert geändert hat (= 1 Min vorbei)...
    zeitzuled(hour(),minute());                           // Zeit auf den Stripe ausgeben
    lastminute=minute();                                  // Minutenwert für nächsten Vergleich merken
  }
  if(day() != lastday){                                   // wenn sich Tageswert geändert hat (= 1 Tag vorbei)...
    advent();                                             // Adventsberechnung (und ggf -ausgabe) starten
    lastday=day();                                        // Tageswert für nächsten Vergleich merken
  }
}
void zeitdisplay(){                                       // *** Funktion Displayausgabe
  disp=\" \";                                               // Stringvariable für Displayinhalt leeren
  if (dispcount<6){                                       // wenn wengier als 6 Sek vergangen...
    disp += \" \";                                          // Leerzeichen hinzufügen
    if (hour()<10) disp += \"0\";                           // falls Stunde einstellig, Null einfügen
    disp += hour();                                       // Stunde einfügen
    disp += \" \";                                          // Leerzeichen einfügen
    if (minute()<10) disp += \"0\";                         // falls Miunte einstellig, Null einfügen
    disp += minute();                                     // Minute einfügen
    disp += \" \";                                          // Leerzeichen einfügen
    if (second()<10) disp += \"0\";                         // falls Sekunde einstellig, Null einfügen
    disp += second();                                     // Sekunde einfügen
  }
  else{                                                   // ... sonst (6 oder mehr Sek vergangen)
    if (day()<10) disp += \"0\";                            // falls Tag einstellig, Null einfügen
    disp += day();                                        // Tag einfügen
    disp += \" \";                                          // Leerzeichen einfügen
    if (month()<10) disp += \"0\";                          // falls Monat einstellig, Null einfügen
    disp += month();                                      // Monat einfügen
    disp += \" \";                                          // Leerzeichen einfügen
    disp += year();                                       // Jahr einfügen (ist vierstellig)
  }
  display.print(disp);                                    // generierten String auf Display schreiben
  
  if (now()-lastdcf > 180 || lastdcf==0) {                // wenn seit >180 Sekunden kein DCF-Singal...
    display.prog.toggle();                                // ... PROG-Feld des Displays blinken lassen
  }
  else{                                                   // ... sonst ...
    display.prog.turnOff();                               // ... PROG-Feld des Displays aus  
  }
  
  dispcount++;                                            // Zähler um eins erhöhen 
  if (dispcount==10) dispcount=0;                         // bei 10 auf Null setzen (nach 10 Sekunden)
}

void dcfsync(){                                           // *** Funktion DCF-Sync
  DCFtime = DCF.getTime();                                // Zeitinfo von DCF77 holen
  if (DCFtime!=0)                                         // wenn Zeitinfo vorliegt...
  {
    setTime(DCFtime);                                     // Zeit nach DCF-Zeit setzen
    lastdcf=now();                                        // merken, wann letzter Sync war
    Wire.beginTransmission(DS3231_ADDRESS);               // Verbindung zu RTC-Modul herstellen
    Wire.write(zero);                                     // RTC stoppen
    Wire.write(decToBcd(second(DCFtime)));                
    Wire.write(decToBcd(minute(DCFtime)));
    Wire.write(decToBcd(hour(DCFtime)));                  // RTC nach DCF-Zeit stellen
    Wire.write(decToBcd(weekday(DCFtime)));
    Wire.write(decToBcd(day(DCFtime)));
    Wire.write(decToBcd(month(DCFtime)));
    Wire.write(decToBcd(year(DCFtime)-2000));
    Wire.endTransmission();                               // RTC wieder starten
  } 
}

void rtcget(){                                            // *** Funktion RTC-Sync
  Wire.beginTransmission(DS3231_ADDRESS);                 // Verbindung zu RTC-Modul herstellen
  Wire.write(zero);
  Wire.endTransmission();
  Wire.requestFrom(DS3231_ADDRESS, 7);
  int second = bcdToDec(Wire.read());
  int minute = bcdToDec(Wire.read());
  int hour = bcdToDec(Wire.read() & 0b111111);            // Zeitinfo von RTC holen
  int weekDay = bcdToDec(Wire.read()); 
  int monthDay = bcdToDec(Wire.read());
  int month = bcdToDec(Wire.read());
  int year = bcdToDec(Wire.read());
  setTime(hour,minute,second,monthDay,month,year);        // Zeit nach RTC-Zeit setzen
  lastrtc=now();                                          // merken, wann letzter Sync war 
}

void zeitzuled(int h, int m)                              // *** Funktion Stripeausgabe
{
  /* 
    LEDs und Anzeigen
    35->fünf    33->zehn    31->viertel    29->vor   27->nach    25->halb    23->1   21->2   19->3   17->4    15->5    13->6   11->7    9->8   7->9   5->10  3->11   1->12
  */
  pxoff();                                                // Funktion aufrufen, das alle Pixel auf AUS setzt
  mehrblau=0;                                             // Variable für mehr blau auf Null
  mehrrot=0;                                              // Variable für mehr rot auf Null
  if (h<7) mehrblau=20;                                   // Wenn Stunde kleiner als 7, Blauwert um 20 erhöhen
  if (h>18) mehrrot=20;                                   // Wenn Stunde größer als 18, Rotwert um 20 erhöhen
  if (m>17) h+=1;                                         // ab Minute 18 -> 1 Std größer anzeigen  (13:20 -> zehn vor halb ZWEI)
  if (h>12) h-=12;                                        // 12h-Format, also nach 12 Uhr 12 Std abziehen
  if (h==0) h=12;                                         // 0 uhr = 12
  pxon(25-h*2);                                           // Stunde anzeigen (z.B. Std 6 -> Pixel 13)
  if (m>2 && m<8) { pxon(35); pxon(27); }                 // fünf nach
  if (m>7 && m<13) { pxon(33); pxon(27); }                // zehn nach
  if (m>12 && m<18) { pxon(31); pxon(27); }               // viertel nach
  if (m>17 && m<23) { pxon(33); pxon(29); pxon(25); }     // zehn vor halb
  if (m>22 && m<28) { pxon(35); pxon(29); pxon(25); }     // fünf vor halb
  if (m>27 && m<33) { pxon(25); }                         // halb
  if (m>32 && m<38) { pxon(35); pxon(27); pxon(25); }     // fünf nach halb
  if (m>37 && m<43) { pxon(33); pxon(27); pxon(25); }     // zehn nach halb
  if (m>42 && m<48) { pxon(31); pxon(29); }               // viertel vor
  if (m>47 && m<53) { pxon(33); pxon(29); }               // zehn vor
  if (m>52 && m<58) { pxon(35); pxon(29); }               // fünf vor
  pixels.show();                                          // alle Pixel gesetzt, Daten auf Stripe schreiben
}
                                                          // *** Hilfsroutinen für die Umrechnung der Werte für die RTC:
byte bcdToDec(byte val)  {
  return ( (val/16*10) + (val%16) );                      // Binär codierte Dezimalzahl in normale umwandeln
}    
byte decToBcd(byte val){
  return ( (val/10*16) + (val%10) );                      // Normal codierte Dezimalzahl in binäre umwandeln
} 
void pxon(int px){                                        // *** Funktion für Pixelansteuerung
  pixels.setPixelColor(px,pixels.Color(ledhell+mehrrot,ledhell,ledhell+mehrblau));    // Farben (RGB) des Pixel px setzen (Helligkeit und ggf mehr rot/blau)
  pixels.setPixelColor(px+1,pixels.Color(ledhell+mehrrot,ledhell,ledhell+mehrblau));  // dto für px + 1 (jedes Feld besteht aus 2 Pixel)
}
void pxoff(){                                             // *** Funktion für alle Pixel aus
  for (int i=0;i<NUMPIXELS;i++){                          // für jedes Pixel von 0 bis NUMPIXELS (Anzahl der Pixel)...
    pixels.setPixelColor(i,pixels.Color(0,0,0));          // ... setze alle 3 Farben (RGB) auf Null
  }
}
void advent() {
  tm.Second = tm.Hour = tm.Minute = 0;                    // Datum erzeugen: 00:00:00 Uhr am...
  tm.Day = 24;                                            // 24.
  tm.Month = 12;                                          // 12.    
  tm.Year = year()-1970;                                  // ...des aktuellen Jahres (minus 1970, weil Timestamp)
  time_t xmas=makeTime(tm);                               // Timestamp aus Weihnachts-Datum erzeugen
  time_t adv1=xmas-86400*weekday(xmas)-1728000;           // 1. Advent berechnen
  time_t adv2=adv1+604800;                                // 2. Advent = 1. Adv + 7 Tage (=604800 Sek)
  time_t adv3=adv2+604800;                                // 3. Advent
  time_t adv4=adv3+604800;                                // 4. Advent
  for (int jj=1;jj<5;jj++){
    digitalWrite(advPin[jj],0);                           // alle Kerzen aus
  }  
  if (now()<=xmas){                                       // wenn aktueller Timestamp vor Weihnachten...
    if(now()>=adv1) digitalWrite(advPin[1], 1);           // wenn nach 1. Advent -> Kerze 1 an
    if(now()>=adv2) digitalWrite(advPin[2], 1);           // dto 2
    if(now()>=adv3) digitalWrite(advPin[3], 1);           // dto 3
    if(now()>=adv4) digitalWrite(advPin[4], 1);           // dto 4
  }
}
void ledlauf(){                                           // *** Funktion, um alle LEDs durchlaufen zu lassen (Effekt beim Start)
  for (int ii=NUMPIXELS;ii>0;ii--){                       // für jedes Pixel von NUMPIXELS (Anzahl der Pixel) bis absteigend...
    pxoff();                                              // alle Pixel aus
    pxon(ii);                                             // Pixel ii ein 
    pixels.show();                                        // anzeigen 
    delay(100);                                           // 0,1 Sekunden warten
  }
}


Ausblick / ToDo / Dankesworte:

 

Falls jemand Vorschläge hat oder Fehler findet, darf er/sie sich gerne melden!
An dieser Stelle herzlichen Dank für die vielen Rückmeldungen auf die erste Version. Es war immer schön, mit anderen Uhrenbastlern aus ganz Europa zu fachsimpeln und Erfahrungen auszutauschen!
Ein besonderer Gruß geht an phi, Frank und Matthias! ;)


Alle in erwähnten Marken- und Warenzeichen oder Produktnamen sind Eigentum ihrer jeweiligen Inhaber.
Für die Inhalte externer Links sind die jeweiligen Seitenbetreiber verantwortlich.

Keine Haftung für eventuelle Schäden.