Präzise Uhr mit Arduino Nano & 16×2 LCD – Ganz ohne RTC-Modul
Komponenten und Materialien
| Bauteil | Menge | Hinweise |
|---|---|---|
| Arduino Nano R3 | 1 | Funktioniert mit jedem Arduino |
| Alphanumerisches LCD 16×2 | 1 | z.B. Amazon B00N8K2BYM |
| Taster (Tactile Switch) | 3 | |
| Trimmer-Potentiometer 10 kΩ | 1 | Für LCD-Kontrast |
| Jumper-Kabel | 1 Set |
Projektbeschreibung
Als akademische Übung begonnen, entstand eine sehr genaue Uhr. Nach 5 Tagen Laufzeit hatte sie keine Sekunde abgewichen.
Das Problem: Ungenaue interne Taktung
Der interne Takt des Arduino ist nicht 100 % genau. Verlässt man sich auf millis(), läuft die Uhr vor oder nach. Mein Ansatz: Die Ungenauigkeit meines Arduinos messen und stündlich korrigieren.
Zweite Hürde: millis() läuft nach ~50 Tagen über und lässt sich nicht manipulieren. Lösung: Eigenen Millisekunden-Zähler per Timer-Interrupt, der täglich bei Mitternacht zurückgesetzt wird.
Ungenauigkeit messen
Annahme: PC-Uhr (Processing millis()) ist genau. Arduino sendet alle 2 Sekunden seinen millis()-Stand an Processing. Processing vergleicht mit eigenem millis(). Nach 1 Stunde zeigt sich die Abweichung in Millisekunden – dieser Wert wird als speedCorrection in der Uhr verwendet.
Codes für Arduino-Testprogramm und Processing-Skript sind unten angegeben.
Processing: processing.org
Der Uhren-Code – Kernbereiche
Eigener Timer-Interrupt (1 ms)
// Timer0 für eigenen ms-Zähler nutzen (millis() & delay() funktionieren dann nicht mehr!)
TCCR0A = (1 << WGM01); // CTC Modus
OCR0A = 0xF9; // 249 = 1 ms bei 16 MHz / 64 Prescaler
TIMSK0 |= (1 << OCIE0A); // Compare Match Interrupt aktivieren
TCCR0B |= (1 << CS01) | (1 << CS00); // Prescaler 64
TCNT0 = 0;
sei(); // Globale Interrupts an
Die ISR wird jede Millisekunde aufgerufen:
volatile unsigned long currentTime = 0; // ms seit Mitternacht
volatile unsigned long elapsed = 0; // Zähler für Stunde/Delay
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
volatile zwingt den Compiler, die Variable jedes Mal neu zu lesen (nicht cachen).
Tages- und Stunden-Handling
const unsigned long msPerDay = 24UL * 60 * 60 * 1000;
const unsigned long msPerHour = 60UL * 60 * 1000;
// Tageswechsel
if (currentMode == SHOW_TIME && currentTime > msPerDay) {
noInterrupts();
currentTime -= msPerDay;
interrupts();
currentDate++; // Julianisches Datum
}
// Stündliche Korrektur
if (elapsed >= msPerHour) {
noInterrupts();
currentTime += speedCorrection; // z.B. +3545 ms wenn Arduino langsam
elapsed = 0;
interrupts();
}
Interrupts werden während der Manipulation kurz deaktiviert, um Race Conditions zu vermeiden.
Datum als Julianisches Datum
Das Datum wird als Julianisches Datum (Tage seit 1.1.-4713 v.Chr.) gespeichert. Routinen zur Umrechnung in Gregorianisch sind implementiert (funktioniert bis Jahr 20.000).
float JulianDate(int d, int m, int y) {
if (m < 3) { m += 12; y -= 1; }
unsigned long t1 = (153 * m - 457) / 5;
unsigned long t2 = 365 * y + y/4 - y/100 + y/400;
return 1721118.5 + d + t1 + t2;
}
void GregorianDate(float jd, int &d, int &m, int &y) {
unsigned long f = jd + 68569.5;
unsigned long e = (4 * f) / 146097;
unsigned long g = f - (146097 * e + 3) / 4;
unsigned long h = 4000 * (g + 1) / 1461001;
unsigned long t = g - (1461 * h / 4) + 31;
unsigned long u = (80 * t) / 2447;
unsigned long v = u / 11;
y = 100 * (e - 49) + h + v;
m = u + 2 - 12 * v;
d = t - 2447 * u / 80;
}
Bedienung über 3 Tasten
- Mode (Pin 2): Wechselt Modus: Zeit anzeigen → Zeit stellen → Jahr stellen → Tag/Monat stellen → Speed-Adjust → zurück
- Up/Stunde/Tag (Pin 3): Erhöht Stunde/Tag/Jahr/Speed
- Down/Minute/Monat (Pin 4): Erhöht Minute/Monat, verringert Jahr/Speed
Im laufenden Betrieb: Bei Gangabweichung in den Speed-Adjust-Modus wechseln und in 5-ms-Schritten korrigieren.
Code
Hauptprogramm: Präzise Uhr mit Datum
// Paul Brace - Feb 2021
// Einfache Uhr mit Datum, nur Arduino, kein RTC
// Zeitkorrektur kompensiert ungenauen internen Takt
// In Test: 5 Tage ohne Abweichung
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 13, 6, 7, 8, 9); // RS, EN, D4, D5, D6, D7
#define MODE_BUTTON 2
#define HOUR_BUTTON 3
#define UP_BUTTON 3
#define DAY_BUTTON 3
#define MINUTE_BUTTON 4
#define DOWN_BUTTON 4
#define MONTH_BUTTON 4
#define SHOW_TIME 1
#define SET_TIME 2
#define SET_YEAR 3
#define SET_DATE 4
#define SET_SPEED_ADJ 5
int speedCorrection = 3545; // ms/Stunde (mein Nano läuft langsam)
// negativ = läuft schnell
volatile unsigned long currentTime = 0;
unsigned long lastTime = -1000;
volatile unsigned long elapsed = 0;
unsigned long msPerDay = 24UL * 60 * 60 * 1000;
unsigned long msPerHour = 60UL * 60 * 1000;
int currentMode = SHOW_TIME;
float currentDate = 0.0;
float lastDate = 0.0;
int currentDay, currentMonth, currentYear;
char *dayArray[] = { "Tue. ", "Wed. ", "Thur. ", "Fri. ", "Sat. ", "Sun. ", "Mon. " };
void setup() {
// Timer0 Interrupt 1 ms
TCCR0A = (1 << WGM01);
OCR0A = 0xF9;
TIMSK0 |= (1 << OCIE0A);
TCCR0B |= (1 << CS01) | (1 << CS00);
TCNT0 = 0;
sei();
pinMode(MINUTE_BUTTON, INPUT_PULLUP);
pinMode(HOUR_BUTTON, INPUT_PULLUP);
pinMode(MODE_BUTTON, INPUT_PULLUP);
currentDate = JulianDate(1, 1, 2021);
currentMode = SHOW_TIME;
lcd.begin(16, 2);
lcd.noAutoscroll();
lcd.display();
lcd.clear();
ShowTime(currentTime);
}
void loop() {
// Tageswechsel
if (currentMode == SHOW_TIME && currentTime > msPerDay) {
noInterrupts();
currentTime -= msPerDay;
interrupts();
currentDate++;
}
// Stündliche Korrektur
if (elapsed >= msPerHour) {
noInterrupts();
currentTime += speedCorrection;
elapsed = 0;
interrupts();
}
CheckButtons();
switch (currentMode) {
case SHOW_TIME: ShowTime(currentTime); ShowDate(currentDate); break;
case SET_TIME: ShowTimeSet(currentTime); break;
case SET_YEAR: ShowYearSet(currentDate); break;
case SET_DATE: ShowDDMMSet(currentDate); break;
case SET_SPEED_ADJ: ShowSpeedSet(); break;
}
Wait(150);
}
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
// ... (JulianDate, GregorianDate, SplitTime, SetTime, Wait, CheckButtons, FormatNumber, ShowTime, ShowDate, ShowDDMMSet, ShowYearSet, ShowTimeSet, ShowSpeedSet)
// Alle Funktionen wie im Original, vollständig implementiert.
// Siehe Originalcode für Details.
Arduino Timer-Testprogramm (für Kalibrierung)
// Sendet alle 2 Sekunden verstrichene ms an Serial
int inByte = 0;
unsigned long firstReading = 100000;
void setup() {
Serial.begin(9600);
sayHello();
}
void loop() {
if (Serial.available() > 0) {
inByte = Serial.read();
Serial.print(millis() - firstReading);
Serial.print('E');
delay(2000);
}
}
void sayHello() {
while (Serial.available() <= 0) {
Serial.print('Z');
delay(200);
}
firstReading = millis();
}
Processing-Skript (Timer-Test)
// Vergleicht Arduino millis() mit Processing millis() (PC-Uhr)
import processing.serial.*;
Serial theSerialPort;
int[] serialBytesArray = new int[15];
int bytesCount = 0;
boolean init = false;
long mills = 0, first = 0, now, firstReading = 100000, DiffPerHour = 0;
void setup() {
size(500, 500);
printArray(Serial.list());
theSerialPort = new Serial(this, Serial.list()[1], 9600);
}
void draw() {
background(70);
fill(255);
textSize(25);
text(hour() + ":" + minute() + ":" + second(), 50, 50);
text("Incoming elapsed: " + mills, 50, 100);
text("Local elapsed: " + (now - firstReading), 50, 150);
text("Diff: " + (mills - (now - firstReading)), 50, 200);
if ((now - firstReading) >= 3600000 && DiffPerHour == 0) {
DiffPerHour = mills - (now - firstReading);
}
text("Diff after 1 hour: " + DiffPerHour, 50, 300);
}
void serialEvent(Serial myPort) {
int inByte = myPort.read();
if (!init) {
if (inByte == 'Z') {
myPort.clear(); init = true; myPort.write('Z');
if (first == 0) first = millis();
}
} else {
if (inByte != 69 && bytesCount < 14) {
serialBytesArray[bytesCount++] = inByte;
}
if (inByte == 69) {
now = millis();
mills = 0;
for (int i = 1; i <= bytesCount; i++)
mills += (serialBytesArray[i-1] - 48) * pow(10, bytesCount - i);
if (firstReading == 100000) firstReading = now;
myPort.write('Z');
bytesCount = 0;
}
}
}
Schaltpläne
Herstellungsprozess
- Arduino POV-Uhr: Persistence-of-Vision LED-Display selbst bauen
- DTMF-Decoder mit Arduino bauen – ganz ohne zusätzlichen IC
- Monitor-Ambilight selbst bauen mit Arduino Nano & WS2812B LEDs
- Stilvolle Wanduhr mit Adafruit 60-LED Neopixel-Ring & Arduino
- DIY Arduino Word Clock – Elegante Wortuhr mit RTC & Schieberegistern
- Arduino Echtzeituhr für islamische Gebetszeiten bauen
- Einen eigenen LED-Master-Clock mit Alarm bauen – aus 12 Metern Entfernung ablesbar
- Intelligentes Voltmeter mit Arduino & Smartphone bauen – Einfaches DIY-Projekt
- Echtzeit-IoT-Herzfrequenzmonitor mit Arduino & MAX30100
- Arduino UNO WiFi Webserver: LED per Browser steuern