Programm zur Datums- und Uhrzeitsynchronisierung

Im Unterschied zum Test-Programm zur Uhrzeitdekodierung (siehe: Uhrzeit Dekodierung) frage ich hier den Zustand des Eingangs-Pins mit dem DCF-Signal nicht zyklisch ab (Polling), sondern reagiere auf Zustandsänderungen am Eingang per "Pin Change Interrupt".

Wie der Name bereits sagt, wird dabei bei jeder Änderung des Eingangsignals, also sowohl bei steigender als auch bei fallender Flanke ein Interrupt ausgelöst. Beim Arduino Uno kann bei jedem Ein-/Ausgangpin ein Pin Change Interrupt ausgelöst werden.


Hier im Programm nutze ich als Eingangspin für das DCF-Signal den Digitalpin 2 und möchte an diesem Beispiel die Programmierung eines Pin Change Interrupt zeigen. Dazu modifiziere ich im Setup nachfolgende 3 Register des Prozessors "ATmega328P" des Arduino Uno. Bei Arduinos mit anderen CPUs sind hier wahrscheinlich Anpassungen erforderlich, die den Beschreibungen der jeweiligen Prozessoren zu entnehmen sind.


Pin Change Interrupt

Im Pin Change Interrupt Control Register (PCICR) wird festgelegt, dass die Pins eines bestimmten Ports (Arduino Uno hat 3 Ports: B, C und D) für Pin Change Interrupts zugelassen sind. Der Digitalpin 2 gehört zum Port D und dieser Port wird mit dem Bit PCIE2 (= Pin Change Interrupt Enable 2) aktiviert.

Dies geschieht mit folgendem Befehl: PCICR |= (1 << PCIE2);

Dazu muss vorausgesagt werden: Der Arduino-Übersetzer kennt alle Register- und Bitnamen wie PCICR oder PCIE2, diese müssen daher vorher nicht deklariert werden. PCIE2 hat den Inhalt "2", entsprechend dem 2. Bit im Register!

Der Befehl "PCICR |= (1 << PCIE2);" shiftet also die Zahl 1 (binär 0000 0001) um 2 Stellen nach links (= 0000 0100) und wird anschließend mit dem Inhalt von PCICR ODER-verknüpft. Dadurch wird das Bit Nummer 2 des PCICR-Registers auf 1 gesetzt, ohne die anderen Bits zu verändern. Der Befehl "PCICR |= B100;" oder leichter lesbar geschrieben "PCICR = PCICR | B100;" bewirkt dasselbe.

Im Pin Change Mask Register 2 (PCMSK2) werden jene Bits maskiert, deren zugeordnete Pins des Ports D eine Interruptauslösung zulassen sollen. Wie im nachfolgende Bild ersichtlich, gehören zum Port D die Digitalpins D7-D0. Nachdem ich, wie gesagt, den Digitalpin 2 als Interrupt-Pin verwende, ist also das Bit PCINT18 (Pin Change Interrupt 18) des PCMSK2-Registers auf "1" zu setzen.

Wie oben beschrieben geschieht das hier in gleicher Weise: PCMSK2 |= (1 << PCINT18);

Hier sieht man auch den Vorteil dieser Schreibweise: Möchte man einen anderen Pin als Interrupt verwenden, braucht man keine Binärzahl zu ermitteln mit der die ODER-Verknüpfung erfolgen soll, sondern ändert nur die Zahl beim PCINTxx-Bit. Möchte man einen zweiten Pin ebenfalls als Interrupt-Eingang verwenden, wird der Befehl mit dem zusätzlichen PCINTxx einfach noch einmal geschrieben.


Als drittes Register muss nun im Status Register (SREG) das I-Bit (Bit 7) auf 1 gesetzt werden. Dieses Bit ist sozusagen das übergeordnete "Interrupt Enable Bit", mit dem alle Interrupts entsperrt bzw. gesperrt werden können.

Dieses Bit wird gesetzt mit dem Befehl: SREG |= 0x80;

oder mit dem Arduino-Befehl: interrupts();



Zusammenfassend also noch einmal die Befehle im Setup:

void setup() {

  .............;

  PCICR |= (1 << PCIE2);
  PCMSK2 |= (1 << PCINT18);
  SREG |= 0x80;

}

Wird nun der Pin Change Interrupt ausgelöst, verzweigt das Programm in eine zu erstellende Serviceroutine "ISR(PCINT2_vect)", wo nun (in Worten) folgende Maßnahmen gesetzt werden:

  • Überprüfen, ob der Interrupt nicht wiederholt durch "Prellen" ausgelöst worden ist (bounce-Time)
  • Speichern des Zeitpunktes der Interruptauslösung (wichtig zur Ermittlung des Telegramminhalts)
  • Extrahieren des Eingangsbit des Digitaleingang 2 aus dem Port D (PIND-Register) zur Ermittlung ob der Interrupt durch eine steigende oder fallende Flanke ausgelöst wurde
  • Setzen von Merkern in Abhängigkeit der Interruptauslösung durch steigende oder fallende Flanke, auf die dann im Hauptprogramm entsprechend reagiert werden kann.


Serviceroutine des vorgestellten Programms

ISR(PCINT2_vect) //Name darf nicht verändert werden!!
{
  unsigned long IRQNowTime;
  byte PortWert;
  byte IRQPinWert;

  IRQNowTime = millis(); //Speichern der aktuellen CPU-Zeit

  //ATmega328P-Register PIND enthält auf Bit 2 den aktuellen Pinwert des Digitaleingang 2

  PortWert = PIND;

  //Maskieren und auf Bit 0-Position verschieben
  IRQPinWert = PortWert & (1 << PIND2); 
  IRQPinWert = IRQPinWert >> PIND2; // IRQPinWert enthält nun Wert von Digitaleingang 2
  if (IRQNowTime - IRQLastTime > bounceTime) //Überwachung auf "Prellen"
  {
    if (IRQAltPinWert == 0 && IRQPinWert == 1) //Steigende Flanke
    {
      IRQAltHighTime = IRQHighTime; //Sichern des Zeitpunktes der letzten steigenden Flanke
      IRQHighTime = IRQNowTime; //Speichern des Zeitpunktes der aktuellen steigenden Flanke
      IRQRising = true; //Setzen des Merkers "Steigende Flanke" zur Reaktion im Hauptprogramm
      digitalWrite(ledPin, 1); // LED einschalten
    }
    if (IRQAltPinWert == 1 && IRQPinWert == 0) //Fallende Flanke
    {
      IRQLowTime = IRQNowTime; //Speichern des Zeitpunktes der aktuellen fallenden Flanke
      IRQFalling = true; //Setzen des Merkers "Fallende Flanke" zur Reaktion im Hauptprogramm
      digitalWrite(ledPin, 0); // LED ausschalten
    }
    IRQAltPinWert = IRQPinWert; //Aktuellen Pinwert sichern
    IRQLastTime = IRQNowTime; //Aktuelle CPU-Zeit speichern
  }
}


Verwendete Libraries

Neben den Standard-Library Wire verwende ich folgende Libraries:

  • LiquidCrystal_I2C2004: Zur Ansteuerung der 20x4 LCD-Anzeige
  • RTClib: Zur Abfrage und Einstellung von Datum und Uhrzeit beim RTC-Modul
  • Metro: Zur zyklischen Bearbeitung von Programmteilen, im nachfolgenden Programm zum Einlesen und Ausgeben von RTC-Datum und Uhrzeit im Sekundentakt

Links zu allen drei Libraries findet ihr unter: Fremd-Libraries


Überprüfungen des Telegramms auf Richtigkeit

Die Zuordnung, ob ein gesendetes Signal als logisch "0" (Impulsdauer 100 ms) oder "1" (Impulsdauer 200 ms) erkannt wird, habe ich sehr großzügig ausgelegt. So wird einem Signal logisch "0" zugeordnet, wenn die Impulsdauer kleiner 150 ms ist und logisch "1" wenn sie größer 150 ms und kleiner 250 ms ist. Eine "engere" Auslegung führt zu einer größeren Anzahl von fehlerhaften Telegrammen. Die auf Basis der Zeitpunkte der Interruptauslösungen gerechneten Impulslängen betragen in den meisten Fällen für logisch "0" ca. 90-120 ms und für logisch "1" ca. 190-220 ms, was ich auch mit meinem "Nichtspeicher"-Oszilloskop beobachten kann. Zwischendurch weichen die Impulslängen jedoch davon merklich ab, wobei ich noch nicht weiß, ob das am gesendeten Signal selbst oder am DCF77-Modul liegt.


Die Entscheidung, ob ein gesendetes Telegramm als richtig erkannt wird, wird derzeit folgend geprüft:

  • Überprüfung der Anzahl der erkannten Impulse pro Telegramm
  • Paritätscheck
  • Überprüfen des Wertebereiches auf Plausibilität


Mit diesen 3 Überprüfungen werden zwar die allermeisten Fehler erkannt, ein "Durchschlüpfen" eines Telegramms, das zwar alle Überprüfungen besteht und trotzdem falsche Werte beinhalten kann, ist aber so noch nicht ausgeschlossen. Weitere Prüfungen, wie z.B. auf Plausibilität gegenüber vorangegangener Telegramme könnten die Sicherheit auf Richtigkeit weiter erhöhen.


Hier nun das aktuelle gesamte Programm in der Version 1.01.

Das Programm befindet sich gerade in der Testphase und kann daher in nächster Zeit immer wieder korrigiert bzw. ergänzt werden:


//DCF77_07.ino
//Code fuer Arduino
//Author Retian
//Version 1.01.


#include <LiquidCrystal_I2C2004.h>
#include <Wire.h>
#include <RTClib.h>
#include <Metro.h>


LiquidCrystal_I2C lcd(0x27, 20, 4);
RTC_DS1307 rtc;

Metro RTCMetro(1000);

byte Jahr, Monat, Tag, Stunde, Minute, Sekunde;

#define signalPin 2
#define ledPin 3

bool dcf77DateTimeOk;
bool syncExecuted = false;
char dcf77Fehler = ' ';

byte impulsLevel; // Impulsdauer 100ms => logisch 0, 200 ms => logisch 1
byte Impuls[59]; // eingehende Impulse
byte impulsZaehler = 0; //Anzahl eingehende Impulse
byte impulsWert[8] = {1, 2, 4, 8, 10, 20, 40, 80}; //Impulswertigkeit für Level logisch 1

byte dcf77Stunde = 0;
byte dcf77Minute = 0;
byte dcf77Tag = 0;
byte dcf77Monat = 0;
byte dcf77Jahr = 0;

byte syncMonat;
byte syncTag;
byte syncStunde;
byte syncMinute;

volatile byte IRQAltPinWert = 1;
volatile byte bounceTime = 50; //Zeit in ms
volatile unsigned long IRQLastTime = 0;
volatile bool IRQFalling = false;
volatile bool IRQRising = false;
volatile unsigned long IRQHighTime;
volatile unsigned long IRQLowTime;
volatile int IRQPulseTime;
volatile unsigned long IRQAltHighTime;


void setup() {
  pinMode(signalPin, INPUT);
  pinMode(ledPin, OUTPUT);

  lcd.init();
  lcd.backlight();
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("RTC:");
  lcd.setCursor(10, 0);
  lcd.print("DCF:");

  rtc.begin();

  PCICR |= (1 << PCIE2);
  PCMSK2 |= (1 << PCINT18);
  SREG |= 0x80;
}


void loop() {
  if (RTCMetro.check())
  {
    leseRTC();
    ausgabeRTC();
    if (Minute == 59) syncExecuted = false;
  }

  if (IRQFalling)
  {
    IRQPulseTime = IRQLowTime - IRQHighTime;
    if (IRQPulseTime < 150) impulsLevel = 0;
    else if (IRQPulseTime < 250) impulsLevel = 1;
    Impuls[impulsZaehler] = impulsLevel;

    lcd.setCursor(16, 0);
    if (impulsZaehler < 10) lcd.print(" ");
    lcd.print(impulsZaehler);
    lcd.setCursor(19, 0);
    lcd.print(impulsLevel);

    impulsZaehler++;
    IRQFalling = false;
  }

  if (IRQRising)
  {
    int IRQSignalTime;

    IRQSignalTime = IRQHighTime - IRQAltHighTime;

    if (IRQSignalTime > 1500)
    {
      if (impulsZaehler == 59 || impulsZaehler == 60)
      {
        dcf77DateTimeOk = dekodiereDatumZeit();
        if (dcf77DateTimeOk)
        {
          //Synchronisiere RTC jede volle Stunde oder wenn Stundensynchronisierung ueberfaellig
          if (Minute == 0) adjustRTC();
          if (Minute != 0 && syncExecuted == false) adjustRTC();

          printDCF77();
        }
      }
      else dcf77Fehler = 'A';
      lcd.setCursor(19, 1);
      lcd.print(dcf77Fehler);
      dcf77Fehler = ' ';
      impulsZaehler = 0;

      for (byte i = 0; i < 60; i++) Impuls[i] = 0;
    }
    IRQRising = false;
  }
}


bool dekodiereDatumZeit()
{
  bool dateTimeOk = true;
  byte paritaetStunde = 0;
  byte paritaetMinute = 0;
  byte paritaetDatum = 0;

  dcf77Stunde = 0;
  dcf77Minute = 0;
  dcf77Tag = 0;
  dcf77Monat = 0;
  dcf77Jahr = 0;

  //Überprüfen der Stundenparitaet
  for (byte i = 29; i < 35; i++) paritaetStunde ^= Impuls[i];
  if (Impuls[35] != paritaetStunde) dateTimeOk = false;

  //Überprüfen der Minutenparitaet
  for (byte i = 21; i < 28; i++) paritaetMinute ^= Impuls[i];
  if (Impuls[28] != paritaetMinute) dateTimeOk = false;

  //Überprüfen der Datumsparitaet
  for (byte i = 36; i < 58; i++) paritaetDatum ^= Impuls[i];
  if (Impuls[58] != paritaetDatum) dateTimeOk = false;

  if (dateTimeOk == false) dcf77Fehler = 'P';

  if (dateTimeOk == true)
  {
    //Zuweisen der Impulswertigkeit
    for (byte i = 21; i < 28; i++) (Impuls[i] == 1 ? dcf77Minute += impulsWert[i - 21] : 0);
    for (byte i = 29; i < 35; i++) (Impuls[i] == 1 ? dcf77Stunde += impulsWert[i - 29] : 0);
    for (byte i = 36; i < 42; i++) (Impuls[i] == 1 ? dcf77Tag += impulsWert[i - 36] : 0);
    for (byte i = 45; i < 50; i++) (Impuls[i] == 1 ? dcf77Monat += impulsWert[i - 45] : 0);
    for (byte i = 50; i < 58; i++) (Impuls[i] == 1 ? dcf77Jahr += impulsWert[i - 50] : 0);

    //Ueberpruefen des Wertebereiches
    if (dcf77Stunde > 23 || dcf77Minute > 59) dateTimeOk = false;
    if (dcf77Tag > 31 || dcf77Monat > 12 || dcf77Jahr < 16 || dcf77Jahr > 99) dateTimeOk = false;
    if (dateTimeOk == false) dcf77Fehler = 'W';
  }

  if (dateTimeOk) return true;
  else return false;
}


void printDCF77()
{
  lcd.setCursor(10, 1);
  if (dcf77Stunde < 10) lcd.print("0");
  lcd.print(dcf77Stunde);
  lcd.print(":");
  if (dcf77Minute < 10) lcd.print("0");
  lcd.print(dcf77Minute);
  lcd.setCursor(10, 2);
  if (dcf77Tag < 10) lcd.print("0");
  lcd.print(dcf77Tag);
  lcd.print(".");
  if (dcf77Monat < 10) lcd.print("0");
  lcd.print(dcf77Monat);
  lcd.print(".");
  lcd.print(dcf77Jahr);
}


void leseRTC()
{
  DateTime now = rtc.now();
  Jahr = now.year() - 2000;
  Monat = now.month();
  Tag = now.day();
  Stunde = now.hour();
  Minute = now.minute();
  Sekunde = now.second();
}


void ausgabeRTC()
{
  lcd.setCursor(0, 1);
  if (Stunde < 10) lcd.print("0");
  lcd.print(Stunde);
  lcd.print(":");
  if (Minute < 10) lcd.print("0");
  lcd.print(Minute);
  lcd.print(":");
  if (Sekunde < 10) lcd.print("0");
  lcd.print(Sekunde);

  lcd.setCursor(0, 2);
  if (Tag < 10) lcd.print("0");
  lcd.print(Tag);
  lcd.print(".");
  if (Monat < 10) lcd.print("0");
  lcd.print(Monat);
  lcd.print(".");
  lcd.print(Jahr);

  lcd.setCursor(0, 3);
  lcd.print("Lastsync.: ");
  if (syncStunde < 10) lcd.print("0");
  lcd.print(syncStunde);
  lcd.print(":");
  if (syncMinute < 10) lcd.print("0");
  lcd.print(syncMinute);

  if (Minute == 59) syncExecuted = false;
}


void adjustRTC()
{
  rtc.adjust(DateTime(dcf77Jahr, dcf77Monat, dcf77Tag, dcf77Stunde, dcf77Minute, 0));
  leseRTC();
  ausgabeRTC();
  RTCMetro.reset();

  syncExecuted = true;

  syncMonat = dcf77Monat;
  syncTag = dcf77Tag;
  syncStunde = dcf77Stunde;
  syncMinute = dcf77Minute;
}


ISR(PCINT2_vect) //Interrupt Service Routine
{
  unsigned long IRQNowTime;
  byte PortWert;
  byte IRQPinWert;

  IRQNowTime = millis();

  PortWert = PIND;
  IRQPinWert = PortWert & (1 << PIND2); //Maskieren
  IRQPinWert = IRQPinWert >> PIND2; //Auf Bit 0 Position verschieben
  if (IRQNowTime - IRQLastTime > bounceTime)
  {
    if (IRQAltPinWert == 0 && IRQPinWert == 1) //Steigende Flanke
    {
      IRQAltHighTime = IRQHighTime;
      IRQHighTime = IRQNowTime;
      IRQRising = true;
      digitalWrite(ledPin, 1);
    }
    if (IRQAltPinWert == 1 && IRQPinWert == 0) //Fallende Flanke
    {
      IRQLowTime = IRQNowTime;
      IRQFalling = true;
      digitalWrite(ledPin, 0);
    }
    IRQAltPinWert = IRQPinWert;
    IRQLastTime = IRQNowTime;
  }
}