Industrielle Fertigung
Industrielles Internet der Dinge | Industrielle Materialien | Gerätewartung und Reparatur | Industrielle Programmierung |
home  MfgRobots >> Industrielle Fertigung >  >> Manufacturing Technology >> Herstellungsprozess

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

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

Präzise Uhr mit Arduino Nano & 16×2 LCD – Ganz ohne RTC-Modul
Präzise Uhr mit Arduino Nano & 16×2 LCD – Ganz ohne RTC-Modul

Herstellungsprozess

  1. Arduino POV-Uhr: Persistence-of-Vision LED-Display selbst bauen
  2. DTMF-Decoder mit Arduino bauen – ganz ohne zusätzlichen IC
  3. Monitor-Ambilight selbst bauen mit Arduino Nano & WS2812B LEDs
  4. Stilvolle Wanduhr mit Adafruit 60-LED Neopixel-Ring & Arduino
  5. DIY Arduino Word Clock – Elegante Wortuhr mit RTC & Schieberegistern
  6. Arduino Echtzeituhr für islamische Gebetszeiten bauen
  7. Einen eigenen LED-Master-Clock mit Alarm bauen – aus 12 Metern Entfernung ablesbar
  8. Intelligentes Voltmeter mit Arduino & Smartphone bauen – Einfaches DIY-Projekt
  9. Echtzeit-IoT-Herzfrequenzmonitor mit Arduino & MAX30100
  10. Arduino UNO WiFi Webserver: LED per Browser steuern