Das Bild in diesem Blogbeitrag wurde von Stable Diffusion erstellt.

Was passiert, wenn man ein Dutzend Uhren nimmt und sie in den Fluss der Zeit wirft?

Der Fluss der Zeit

Wenn man ein Dutzend Uhren in den Fluss der Zeit wirft, werden sie wahrscheinlich fröhlich vor sich hin ticken und langsam voneinander wegdriften. Das kann ganz unterschiedliche Gründe haben. Wenn man es mit Atomuhren zu tun hat, kann es durch relativistische Effekte zu messbaren Unterschieden kommen, wenn man einige von ihnen auf hohe Berge bringt und dann wieder herunter. Wenn man es mit gewöhnlichen Echtzeituhren (RTCs) zu tun hat, gehen diese auch ohne Beeinflussung oder Kenntnis von Einsteins Relativitätstheorie unterschiedliche Wege.

Wie ich in einem meiner letzten Blog-Beiträge erwähnt habe, haben RTCs eine inhärente Ungenauigkeit, die zwischen 1 ppm und 20+ ppm schwankt (und noch schlimmer sein kann). So steht es zumindest in den Datenblättern. Es ist natürlich interessant zu sehen, wie sich das in der Praxis auswirkt.

Andere Leute haben so etwas natürlich auch schon gemacht. Und ich vermute, Hersteller führen auch solche Versuche durch. Pete Stephenson führte ein 5-monatiges Experiment durch, in dem er verschiedene DS3231-Exemplare verglich. Dan Drown hat eine Reihe von Kurzzeitexperimenten über mehrere Tage durchgeführt und sehr genaue Daten gesammelt. Schließlich fand ich diesen Online-Artikel von Francisco Tirado-Andrés, der verschiedene Arten von Uhren theoretisch und empirisch vergleicht.

Mein Plan ist es, etwa ein Dutzend verschiedener RTCs auf die Reise zu schicken, sie rauen Umgebungsbedingungen auszusetzen (so rau wie es hier eben geht) und dabei 12 Monate lang in Ruhe zu lassen (na ja, und regelmäßig zu überprüfen, was sie so tun).

Vorbereitungen auf die Reise

Die Idee klingt konzeptionell einfach, aber ohne ein paar Vorbereitungen geht es nicht.

Bevor es losgeht

Die RTCs sollten eine ganze Weile unbeaufsichtigt laufen, d.h. das gesamte System sollte nicht zu viel Strom verbrauchen. Dann brauchen wir ein bisschen Software, um die Uhren zu Beginn zu kalibrieren, und wir brauchen eine Möglichkeit, ihre Abweichung von der wahren Zeit zu messen. Letzteres geschieht mit DCF77, unter Verwendung der DCF77-Bibliothek von Thijs Elenbaas. Es gibt eine andere DCF77-Bibliothek, die angeblich genauer und robuster ist. Allerdings nimmt sie auch mehr Flash-Speicher in Anspruch. Vielleicht komme ich später darauf zurück.

Und dann gibt es noch ein Dutzend verschiedener RTCs, die ein Dutzend verschiedener Bibliotheken benötigen, um die Zeit zu ermitteln und einzustellen und die Uhren anfangs zu kalibrieren. Was man also gerne hätte, ist so etwas wie eine einheitliche Bibliothek für die Kommunikation mit allen Chips über eine Standardschnittstelle.

Die Bibliothek, die mir dazu einfällt, ist RTClib von Adafruit, ursprünglich von Jeelabs. Diese Bibliothek deckt vier verschiedene populäre RTCs ab, aber sie haben immer noch vier verschiedene Klassen/Schnittstellen. Die gemeinsame Basisklasse enthält nur die grundlegenden Funktionen zum Lesen und Schreiben einzelner Register. Die abgeleiteten Klassen haben manchmal ähnlich benannte Methoden. Aber selbst wenn funktional identische Methoden den gleichen Namen besitzen, kann man sie nicht in einem Kontext verwenden, in dem man den Typ der RTC-Instanz erst zur Laufzeit kennt.

Es gab also nichts von dem, was ich im Sinn hatte. Mit anderen Worten, wir brauchen eine neue Bibliothek.

Eine einheitliche RTC-Bibliothek: RTC_I2C

Wie könnte man eine solche Bibliothek implementieren, und was sollte sie abdecken? Wenn man die verschiedenen Datenblätter durchgeht, stellt man fest, dass die RTCs eine Unmenge verschiedener Möglichkeiten bieten, um den Bedürfnissen eines Benutzers gerecht zu werden. Es gibt jedoch ein paar Funktionen, die fast alle RTCs bieten.

  • Setzen der aktuellen Zeit in der RTC (und ggf. Start der Uhr).
  • Auslesen der aktuellen Zeit.
  • Prüfen, ob die Zeitmessung in der RTC gültig ist.
  • Eine Alarmzeit setzen. Der gemeinsame Nenner scheint zu sein, dass ein Alarm nach Stunde und Minute festgelegt werden kann. Die meisten RTCs unterstützen auch einen stündlichen Alarm zu einer bestimmten Minute.
  • Aktivieren oder Deaktivieren des Alarms. Wenn er aktiviert ist, dient einer der Ausgangspins als Interruptleitung.
  • Erkennen, ob ein Alarm aufgetreten ist.
  • Löschen des Alarms.
  • Aktivieren oder deaktivieren der Ausgabe des Taktsignals, normalerweise die 32,768 kHz Frequenz.
  • Aktivieren oder deaktivieren der Ausgabe eines 1 Hz-Signals.
  • Setzen des Kalibrierungsregisters, das die Drift kompensiert. Einige RTCs erlauben eine Schrittweite von 0,1 ppm, bei anderen ist es 4,5 ppm.
  • Und schließlich unterstützt jede RTC das Setzen und Lesen einzelner Register.

Eine der RTCs bietet keine Alarmfunktion. Andere haben nur sehr spezielle Arten von Alarmen. Ein paar weitere haben kein Kalibrierungsregister. Davon abgesehen, sieht diese Schnittstelle jedoch universell aus.

C++ bietet auch die Werkzeuge, um eine einheitliche Schnittstelle durch die Vererbung von Methoden bereitzustellen. Eine Basisklasse namens RTC deklariert alle Methoden als virtuelle Methoden und implementiert einige wenige, die quasi universell sind, u.a. das Setzen und Auslesen der Uhrzeit. Dann muss für jeden RTC-Typ eine weitere Klasse definiert werden, die von der Basisklasse erbt und die übrigen Methoden implementiert.

Auf dieser Grundlage können Sie ein Array mit Zeigern auf RTCs definieren, wobei jeder Slot einen Zeiger auf eine andere Instanz enthalten kann. Beim Iterieren über das Array ist es dann möglich, jedes Element auf die gleiche Weise zu behandeln. Der folgende Sketch veranschaulicht das.

#include <RTC_I2C.h>
#include <RTC_DS1307.h>
#include <RTC_RV8803.h>
DS1307 rtc0;
RV8803 rtc1;
RTC *rtc[2] = { &rtc0, &rtc1 };

void setup(void) {
  Serial.begin(115200);
  for (byte i=0; i < 2; i++) rtc[i]->begin();
}
void loop(void) {
  time_t t;
  for (byte i=0; i < 2; i++) {
    Serial.print(F("RTC")); Serial.print(i); Serial.print(F(": "));
    Serial.println(rtc[i]->getTime());
  }
  delay(1000);
}

Dies ist tatsächlich das erste Mal, dass ich Vererbung und dynamische Typisierung in C++ verwende. Und es hat in diesem Fall ziemlich gut funktioniert. Es sieht so aus, als ob etwas mehr Flash-Speicher verwendet wird, wahrscheinlich wegen der dynamischen Bindung. Ich werde mir das genauer ansehen, wenn ich mit der Implementierung der Bibliothek fertig bin.

Wer sich für diese Bibliothek interessiert, die inzwischen 12 verschiedene Typen von RTCs abdeckt, kann sie von GitHub herunterladen.

Vermeiden von I2C-Adressierungskonflikten

Im obigen Sketch haben wir zwei verschiedene RTCs, die denselben I2C-Bus verwenden. In diesem Beispiel geht alles gut, weil die RTCs unterschiedliche I2C-Adressen haben. Im Allgemeinen kann es jedoch zu Problemen kommen. Beispielsweise verwenden 5 verschiedene Arten von RTCs 0x68 als I2C-Adresse. Und es gibt keine Möglichkeit, die I2C-Adresse dieser RTCs zu ändern.

Natürlich ist dies ein allgemeines Problem, das immer auftritt, wenn man mehre I2C-Geräte gleichzeitig verwendet. Und deshalb gibt es auch allgemeine Lösungen dafür. Der I2C-Multiplexer TCA9548 ermöglicht es, 8 verschiedene „lokale“ I2C-Busse anzusprechen. Er funktioniert wie folgt. Der Multiplexer hat auch eine I2C-Adresse, die zwischen 0x70 und 0x77 konfiguriert werden kann. Diese Adresse muss unter den Geräten, auf die man über den I2C-Bus zugreifen möchte, eindeutig sein. Der einzige Befehl, den dieser Multiplexer akzeptiert, ist ein Byte, in dem jedes Bit angibt, welche lokalen I2C-Busse mit dem globalen I2C-Bus verbunden werden sollen. Wenn man zum Beispiel das Byte 0x84 an den Multiplexer sendet, werden die lokalen Busse #7 und #2 mit dem globalen Bus verbunden, da die Bits Nummer 7 und 2 gesetzt sind. Nachdem man 0x00 gesendet hat, sind alle lokalen Busse deaktiviert. Der Multiplexer kann gleichzeitig als Pegelwandler verwendet werden, da jeder lokale Bus seine eigenen Pull-up-Widerstände (und seine eigene Versorgungsspannung) hat.

Anstatt einen I2C-Multiplexer zu verwenden, könnte die MCU auch verschiedene I2C-Busse ansteuern. Einige Arduino MCUs, z.B. ESP32 und der Due, haben zwei I2C-Ports und auf den zweiten kann über die Instanzvariable Wire1 zugegriffen werden. Die Instanzvariable, die Sie für eine bestimmte RTC verwenden möchten, muss beim Aufruf der Methode begin der jeweiligen RTC-Instanz angegeben werden.

Im Prinzip sollte es auch möglich sein, verschiedene Software-I2C-Instanzen zu verwenden, um verschiedene I2C-Ports zu erstellen. Zu diesem Zweck könnte zum Beispiel die Bibliothek SoftWire verwendet werden. Da es der Arduino IDE ein wenig an Flexibilität mangelt, was die Angabe der zu berücksichtigenden Bibliotheken angeht, muss man wahrscheinlich einige Verrenkungen machen, damit es funktioniert. Das musste ich jedoch nicht machen. Ich besitze zwei der oben erwähnten I2C-Multiplexer.

Erster Test & Kalibrierung

Nachdem ich die Bibliothek implementiert hatte, musste ich sie auf allen RTCs testen. Dabei machte ich eine ganze Reihe überraschender Entdeckungen (und hier sehen wir einmal von den Programmierfehlern ab, für die ich selbst verantwortlich war).

Testen der Bibliothek auf einer RTC

Zunächst funktionierte das Waveshare PCF8563 Breakout-Board überhaupt nicht. Das Auslesen der Zeitregister ergab immer wieder andere Werte. Ein Blick auf die Signale mit einem Oszilloskop bestätigte, dass auf dem Bus alles in Ordnung war, also muss es tatsächlich am Chip liegen. Also kaufte ich eine andere Breakout-Platine (DIY MORE scheint sie z.B. zu bauen), die in vielerlei Hinsicht besser konstruiert war, und, was noch wichtiger ist, sie funktionierte. Ein ähnliches Problem hatte ich mit dem RV-3028 Breakout-Board von Mikroelektronika, das manchmal funktionierte, aber dann hing die I2C-Kommunikation. Nach einer Weile funktionierte es wieder, keine Ahnung warum. Ein anderes Breakout-Board von Pimoroni mit dem Chip funktioniert jetzt einwandfrei. Vielleicht waren die beiden erwähnten problematischen RTCs Opfer von ESD geworden.

Was die Softwareschnittstellen betrifft, so habe ich zwei Gewinner in der Kategorie „hässlichste Schnittstelle“. Die RS5C372 zeichnet sich durch die Verwendung eines Adressierungsschemas für die internen Register aus, das ich noch nie gesehen habe. Die Registeradressen müssen um 4 Bits nach links verschoben werden! Um die Sache noch komplizierter zu machen, erfordert die I2C-Schnittstelle eine wiederholte Startbedingung zwischen einer I2C-Schreibsequenz, die die Registeradresse festlegt, und der folgenden I2C-Lesesequenz. Der MCP79410 überrascht den Programmierer anders. Viele wichtige Steuerbits sind im Zeitregister gespeichert (um Geld zu sparen?) und ihr Standardwert ist eins. Dadurch unterscheidet sich der Code zum Einstellen der Zeit erheblich von dem, der für andere Uhren verwendet wird. Und dann unterstützt sie nur 6 verschiedene Alarmmodi, aber keiner davon erlaubt es einen Alarm zu einer gegebenen Minute und Stunde auszulösen, ein Modus, den alle anderen RTCs (und sogar mein Nachttischwecker) bieten. Besonders ärgerlich ist, dass beide RTCs keine Möglichkeit bieten, die Teilerkette zur Erzeugung des Sekundensignals zurückzusetzen, was bedeutet, dass es unmöglich ist, den Start der Uhr mit den anderen Uhren zu synchronisieren. Oder anders ausgedrückt, mit diesen RTCs haben Sie eine anfängliche Unsicherheit von einer Sekunde!

Nachdem die Bibliothek mit all den verschiedenen RTCs gearbeitet hatte, habe ich die Uhren kalibriert. Das bedeutet, dass ich mit meinem Frequenzanalysator FA-2, der extrem genau ist, gemessen habe, wie weit das nominale 1-Hz-Signal von 1 Hz abweicht. Daraufhin berechnete ich den Offset-Wert und speicherte diesen in dem Kalibrierungsregister. Ich gehe davon aus, dass dies dazu beitragen wird, die RTCs bei Raumtemperatur einigermaßen genau zu halten. Der Plan ist jedoch, die RTCs auch nach draußen zu stellen, um zu sehen, wie sich die Uhren verhalten. Die Erwartung ist natürlich, dass die RTCs mit TCXOs viel besser abschneiden werden als diejenigen, die keine Temperaturkompensation besitzen.

NameExt.
X
Initial
(ppm)
Calibrated
(ppm)
DS1307 (Elecrow)Y+65.7
DS1307 (Adafruit)Y+18.2
DS1337Y-43.7
DS3231SNN+0.20.0
DS3231MN-4.0-0.2
MCP79410Y-28.0-0.3
PCF8523Y+106.9+1.5
PCF8563Y+13.5
RS5C372Y+12.6+1.5
RV-3028N-2.0+1.5
RV-3032N0.00.0
RV-8523N+2.8-2.3
RV-8803N+1.5-0.3
SD2405N+2.3+2.3

Interessanterweise zeigen einige RTCs mit externen Quarzen eine viel höhere Abweichung, als man erwarten würde, wenn in den Datenblättern angegeben wird, dass die Quarze eine Genauigkeit von ±20 ppm haben. Das Breakout-Board Elecrow DS1307 hat zum Beispiel eine Abweichung von 65,7 ppm. Das Adafruit DS1307 Breakout Board hat dagegen nur eine Abweichung von 18,1 ppm. Das Adafruit-Board ist allerdings auch fast zehnmal so teuer wie das Elecrow-Board.

Andererseits war ich beeindruckt, dass man in allen Fällen, in denen ein Kalibrierungsregister vorhanden ist, eine Genauigkeit von besser als ±3 ppm (kurzfristig und bei Raumtemperatur) erzielen konnte. In jedem Fall sind der DS3231SN, der RV-3032 und der RV-8803 die klaren Favoriten, wenn es um die Genauigkeit geht. Da sie auch über einen TCXO verfügen, gehe ich davon aus, dass sie am Ende die „Gewinner“ sein werden.

Experimenteller Aufbau

Jetzt muss nur noch das Experiment aufgebaut werden. Das bedeutet, dass ich einen Sketch schreiben muss, der die Zeit für alle RTCs setzt. Der Sketch versucht, dies so zu tun, dass der Fehler weniger als 10 ms beträgt. Wie ich oben festgestellt habe, ist das leider unmöglich, also wird die anfängliche Zeitdifferenz aufgezeichnet.

Der Hauptzweck des Sketches ist es, die tägliche Drift zu messen. Da die Temperatur ein Hauptfaktor für die Veränderung der Taktfrequenz ist, wird die Temperatur stündlich mit dem DS18S20 Sensor gemessen. Darüber hinaus wird alle 24 Stunden die Drift aller RTCs gegenüber der DCF77-Zeit gemessen und in einem externen EEPROM gespeichert.

Das Hardware-Setup ist in der Abbildung unten dargestellt. Die Verdrahtung sieht ein wenig chaotisch aus, ist aber sehr regelmäßig. Alle RTCs sind über die I2C-Multiplexer (die violetten Breakout-Boards) mit der MCU verbunden. Die MCU liefert eine schaltbare Versorgungsspannung für die Multiplexer und drei Sätze von RTCs mit separaten GPIO-Leitungen. Die vier RTCs auf der linken Seite verfügen außerdem über eine zusätzliche Backup-Batterie. Diese wird benötigt, weil die Breakouts entweder überhaupt keine Backup-Quelle haben oder, wie im Falle des RS5C372 und des SD2504, nur einen Supercap bzw. eine wiederaufladbare Batterie mit unbekannter Kapazität besitzen. In der oberen rechten Ecke sehen Sie den DCF77-Empfänger, bei dem es sich um ein EM2S-Modul von HKW Elektronik handelt.

Alles verkabelt

Links neben dem DCF77-Modul ist das Arduino-Board zu sehen. In diesem Fall ein Boarduino-Board von Adafruit, das nicht mehr hergestellt wird. Ich hatte die publizierten Eagle-Daten genommen und 3 Boards von OSH Park bestellt. Der Vorteil dieser Boards ist, dass man die Schaltung so aufbauen kann, dass es keinen elektronischen Overhead gibt, was für Low-Power-Projekte wie dieses ja extrem wichtig ist.

Nachdem ich den Sketch getestet hatte, habe ich alles initialisiert, und nun warten wir auf die Ergebnisse.

Die Fortsetzung folgt in ein paar Monaten …

Views: 36