RTC und NTP in MicroPython mit ESP8266 und ESP32 - Teil 1 - AZ-Delivery

In der Blogfolge zum Advents-Kranz-Kalender funktionierte die Zeitmessung über einen NTP-Server im Web oder, falls kein WLAN zur Verfügung stand, über die RTC (aka Real Time Clock) auf dem ESP8266, der damals zum Einsatz kam. Ich hatte keine Untersuchungen zur Ganggenauigkeit der Borduhr angestellt. Jetzt im Nachhinein habe ich genau das getan. Dabei kamen erstaunliche Ergebnisse zu Tage, die ich Ihnen nicht vorenthalten will. Folgen Sie mir auf eine Zeitreise mit

MicroPython auf dem ESP32 und ESP8266

heute

Uhren, RTC, NTP und Co.

Also gleich vorweg, die RTC des ESP8266 ist gelinde gesagt Schrott, was die Ganggenauigkeit angeht. Untersucht habe ich einen ESP8266 D1 mini pro mit einem ESP8266 EX Controller. Die Abweichung gegen die NTP-Zeit aus dem Netz beträgt knapp 2 Minuten pro Stunde, das ist eine Dreiviertelstunde am Tag, die die RTC schneller läuft. Die RTC eines ESP8266 12F (Node MCU V3) geht am Tag um knapp eine halbe Stunde vor.

Weil das kein Zustand sein kann, habe ich auch noch eine externe RTC mit Batteriepufferung in meine Experimente mit aufgenommen. Am ESP8266 bringt das Vor- und Nachteile. Ein verfügbares DS1302-BOB (Break Out Board) hört leider nicht am I2C-Bus mit, sondern braucht einen eigenen seriellen Bus mit drei Leitungen, die natürlich drei GPIOs belegen und die sind am ESP8266 ja nicht gerade üppig vorhanden. Natürlich brauchte ich für den Chip somit einen dedizierten Hardwaretreiber, den ich in Form des Moduls ds1302.py gebaut habe.

Dafür wartet der DS1302 aber mit einer viel besseren Ganggenauigkeit auf. Hier sprechen wir von 8 bis 10 Sekunden pro Tag, welche die RTC des Chips vorgeht. Das macht etwa 50 Minuten im Jahr aus, entspricht der Genauigkeit einer Quarzuhr und ist erträglich.

Natürlich wollte ich auch wissen, wie sich ein ESP32 in Sachen Zeit verhält. Ich habe also dieselben Programme sowohl auf dem ESP8266 als auch auf dem ESP32 laufen lassen. Was hierbei herausgekommen ist, verrate ich weiter unten. Werfen wir zuerst mal einen Blick auf die in den Versuchen verwendete Hard- und Software.

Hardware

1

ESP32 Dev Kit C unverlötet oder

ESP32 Dev Kit C V4 unverlötet oder

ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder

NodeMCU-ESP-32S-Kit

1

NodeMCU Lua Amica Modul V2 ESP8266 ESP-12F WIFI oder

D1 Mini NodeMcu mit ESP8266-12F WLAN Modul oder

NodeMCU Lua Lolin V3 Module ESP8266 ESP-12F WIFI

1

0,91 Zoll OLED I2C Display 128 x 32 Pixel

1

KY-004 Taster Modul

1

Breadboard Kit - 3x Jumper Wire m2m/f2m/f2f + 3er Set MB102 Breadbord kompatibel mit Arduino und Raspberry Pi - 1x Set

1 DS1302 Serial Real Time Clock RTC Echtzeituhr Clock Modul

Die Software

Fürs Flashen und die Programmierung des ESP32:

Thonny oder

µPyCraft

Verwendete Firmware für einen ESP32:

MicropythonFirmware

v1.19.1 (2022-06-18) .bin

Verwendete Firmware für einen ESP8266:

v1.19.1 (2022-06-18) .bin

Die MicroPython-Programme zum Projekt:

ds1302.py Hardwaretreiber für die DS1302

ssd1306.py Hardwaretreiber für das OLED-Display

oled.py API für das OLED-Display

DS1302-set+get.py Programm für den Uhrenvergleich NTP-RTC-DS1302

rtc-set+get.py Programm für den Uhrenvergleich NTP-RTC

MicroPython - Sprache - Module und Programme

Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.

MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.

Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.

Autostart

Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.

Programme testen

Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.

Zwischendurch doch mal wieder Arduino-IDE?

Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.

Die Zeitformate von NTP, RTC und DS1302

Lokale Zeit

Auf dem ESP32/ESP8266 erfolgt die Zeitrechnung in Sekunden seit Beginn der Epoche. Das ist beim ESP8266 der 01.01.2000. Sie können das verifizieren, indem Sie im Terminalfenster von Thonny aus dem Modul time die Funktion localtime() mit 0 Sekunden aufrufen.

>>> import time
>>> time.localtime(0)
(2000, 1, 1, 0, 0, 0, 5, 1)

Diese Funktion gibt das Datum, 2000-01-01, und die Uhrzeit (00:00:00) zurück. Es folgen der Wochentag (5 = Samstag) und die Nummer des Tages im Jahr (1). Die Werte sind in einem Tupel (eng. Tuple) zusammengefasst. Die Reihenfolge der einzelnen Angaben ist also wie folgt und entspricht auch derjenigen des NTP-Formats. DOW steht für Day of Week, DOY für Day of Year.

Jahr, Monat, Tag, Stunden, Minuten, Sekunden, DOW, DOY

Dazu noch ein Beispiel, das die Zusammenhänge verdeutlicht. Die Funktion time() des Moduls time liefert die Anzahl Sekunden seit der Epoche. Die Funktion localtime() wandelt diesen Wert in die Datumswerte des Tupels um.

>>> time.time()
723460001
>>> time.localtime(723460001)
(2022, 12, 4, 9, 6, 41, 6, 338)

Es ist also der 04.12.2022, 09:06:41 Uhr. Der 338. Tag im Jahr ist ein Sonntag. Die Wochentage werden von 0 = Montag bis 6 = Sonntag durchgezählt.

Die Real Time Clock – das Modul RTC

Die Klasse RTC wohnt im Modul machine.

>>> from machine import RTC
>>> RTC
<class 'RTC'>

Sie stellt die Methode datetime() zur Verfügung, welche beim Aufruf ohne Parameter ebenfalls ein Tupel mit Datums- und Zeitwerten zurückgibt. Die Reihenfolge der Parameter weicht allerdings von der der Funktion time.localtime() ab. Der Wochentag folgt unmittelbar auf das Datum und der letzte Parameter ist nicht der Tag im Jahr, sondern liefert die Sekundenbruchteile in Millisekunden.

>>> rtc=RTC()
>>> rtc.datetime()
(2022, 12, 4, 6, 9, 50, 50, 215)

Übergibt man der Methode datetime() dagegen ein Tupel dieser Form, dann wird die Angabe in einen Sekunden-Timestamp umgeformt und die RTC danach gestellt. Wochentag und Sekundenbruchteile werden als 0 übergeben.

>>> rtc.datetime((2022, 12, 4, 0, 9, 6, 41, 0))
>>> rtc.datetime()
(2022, 12, 4, 6, 9, 6, 42, 295)

Um das RTC-Format dem von time und NTP benutzten anzugleichen, habe ich zwei Funktionen geschrieben, die ich in meinen Programmen einsetze. Sie setzen die Existenz eines RTC-Objekts mit dem Namen rtc voraus und übernehmen die Umcodierung der Zeitformate.

def getRtcTime():
       yr,mon,day,dow,hor,minute,sec,ms=rtc.datetime()
       return(yr,mon,day,hor,minute,sec,dow,ms)
   
def setRtcTime(dt):
   y,mo,day,h,m,s,dow,doy=dt
   rtc.datetime((y,mo,day,0,h,m,s,0))
   # synchronisiert die RTC automatisch mit UTC + Zeitzone
   print("RTC time set to",rtc.datetime())

getRtcTime() holt mit rtc.datetime() ein Tupel von der RTC, das ich in die Einzelteile zerlege. Dieser Vorgang heißt entpacken und kann mit Tupeln und Listen durchgeführt werden. Danach setze ich die Werte wieder zu einem Standard-Tupel zusammen, das zurückgegeben wird.

Der Funktion setRtcTime() übergebe ich ein Standard-Tupel, das ebenfalls zuerst in die einzelnen Werte entpackt wird. Ein Tupel mit veränderter Reihenfolge übergebe ich an die Methode rtc.datetime() und stelle damit die Borduhr ein.

Das Network Time Protocol NTP

Um NTP zu nutzen, brauche ich einen Server im Netz, welcher dieses Protokoll unterstützt. Davon gibt es viele. Alle verwalten die Zeit, ähnlich wie das Modul time in Sekunden seit Beginn der Epoche. Diese begann aber nicht mit dem 01.01.2000, sondern mit dem 01.01.1900. Dem trägt die Konstante RTC.NTP_DELTA = 3155673600 Rechnung, welche in der Klasse ntptime deklariert ist. Beim Aufruf von ntptime.time() wird dieser Wert von dem beim NTP-Server abgefragten Wert abgezogen, um auf die Epoche der ESP-Controller zu kommen. Durch den Aufruf von time.localtime(ntptime.time()) wird der Sekundenwert schließlich in das Tupel im Standardformat umgerechnet.

Welche Objekte das Modul ntptime zur Verfügung stellt, kann man sich im Object Manager von Thonny anschauen. Dort findet man auch die URL, mit der sich der ESP8266/ESP32 verbindet: pool.ntp.org.

Abbildung 1: Objektinspektor aktivieren

Abbildung 1: Objektinspektor aktivieren

Abbildung 2: Objekte des Moduls ntptime

Abbildung 2: Objekte des Moduls ntptime

Das Modul ds1302

Die Register des DS1302 stellen die Tages- und Zeit-Daten im BCD Format dar. Die Informationen werden seriell ausgelesen oder gesetzt. Wie das funktioniert, erfahren Sie in diesem Kapitel.

Um den DS1302 in meine Versuche mit einzubeziehen habe ich anhand des Datenblatts des DS1302 ein Modul erstellt, ds1302.py. Es liefert für alle wesentlichen Aufgaben die entsprechenden Methoden. Nicht umgesetzt wurde die Eigenschaft, Pufferakkuzellen zu laden, weil auf dem BOB nur eine Lithiumbatterie eingesetzt wird, die man nicht laden kann.

Ich beginne mit dem Import der Klasse Pin aus dem Modul machine. Den Registernummern des DS1302 weise ich Namen zu, das vereinfacht die Handhabung. Als Konstanten werden sie im Flash des ESP8266/ESP32 abgelegt. Die Liste tage enthält die Tageszahlen der Monate Januar bis Dezember. Diese Liste brauche ich später, um, wie beim NTC-Zeitformat, die Nummer des aktuellen Tages im Jahreslauf als achten Parameter zu berechnen.

Dann deklariere ich die Klasse DS1302, deren Konstruktor __init__() mit den übergebenen Pin-Nummern die Pin-Objekte für die Leitungen DIO, CLK und CS instanziiert.

class DS1302:
   def __init__(self, clk, dio, cs):
       self.clk = Pin(clk,Pin.OUT)
       self.dio = Pin(dio)
       self.cs = Pin(cs,Pin.OUT)

Die beiden Methoden dec2BCD() und bcd2dec() erledigen die Umcodierung vom Dezimalsystem ins BCD-Format und umgekehrt. Die Hintergründe erfahren Sie im Glossar.

    def dec2bcd(self, data):
       return (data//10)<<4 | (data % 10)

Die Zehnerziffer des Arguments data erhalte ich durch Ganzzahldivision data // 10. Ich schiebe sie um vier Bitpositionen ins High-Nibble des Ergebnisses. Der Teilungsrest modulo 10 liefert die Einerziffer. Diese wird mit dem bisherigen Ergebnis oderiert und steht im Low-Nibble des Ergebnis-Bytes.

    def bcd2dec(self, data):
       return (data >>4)*10 + (data & 0x0F)

Das High-Nibble schiebe ich um 4 Bitpositionen nach rechts und erhalte so die Zehnerziffer, die für das Ergebnis mit 10 multipliziert wird. Die Einerziffer erhalte ich durch Ausmaskieren des High-Nibbles beim Undieren mit der Maske 0b00001111 = 0x0F.

Die Funktion, Verzeihung, wollte sagen Methode, denn wir befinden uns ja innerhalb einer Klasse und da sagt man zu Funktionen Methode. Beim OOP (OOP aka Objekt Orientiertes Programmieren) nennt man Variablen auch Attribute und die Objekte werden auch unter der Bezeichnung Instanz gehandelt.

Also, die Methode writeByte() bedient die unterste Schicht der Übertragung. Sie schiebt die einzelnen Bits über die Datenleitung, das LSBit (Least Significant Bit = niederwertigstes Bit) zuerst. Die Datenleitung dio wird als Ausgang geschaltet, das Datenbyte wird um 0 bis 7 Bitpositionen nach rechts geschoben und dann jeweils das LSBit isoliert. Das macht das Undieren mit der Maske 0b00000001. Danach wird die Taktleitung clk kurz auf 1 und sofort danach wieder auf 0 gebracht. Das geschieht achtmal.

    def writeByte(self, data):
       self.dio.init(Pin.OUT)
       for i in range(8):
           self.dio.value((data >> i) & 1)
           self.clk.value(1)
           self.clk.value(0)

readByte() holt ein Byte vom DS1302 ab. Die Empfangsvariable data setze ich auf 0 und schalte die Datenleitung dio auf Eingang. Dann lese ich den Zustand der Leitung ein und schiebe ihn als Bit um 0 bis 8 Positionen nach links. Das erste Bit landet daher auf Position 0, das letzte als MSbit (Most significant Bit = hochwertigstes Bit) an Position 7. Ein Takt auf der Leitung clk fordert das nächste Bit vom DS1302 an.

    def readByte(self):
       data = 0
       self.dio.init(Pin.IN)
       for i in range(8):
           data = data | (self.dio.value() << i)
           self.clk.value(1)
           self.clk.value(0)
       return data

Die nächst höhere Übertragungsschicht setzt den Rahmen für einen Transfer. Damit die Übertragung beginnen kann, muss die cs-Leitung (chip select) auf 1 gesetzt werden. Dann sende ich die Adresse des anzusprechenden Registers, die der Methode readReg() als Argument übergeben wird. Sofort danach überträgt der DS1302 den Registerinhalt, den ich mit readByte() abhole. Den Takt liefert stets der Controller, er ist der Chef.

    def readReg(self, reg):
       self.cs.value(1)
       self.writeByte(reg)
       reg = self.readByte()
       self.cs.value(0)
       return reg

Die Methode writeReg() arbeitet ähnlich nur wird, anstatt ein Byte zu empfangen, eines gesendet. In reg und data werden Registernummer und Datenbyte übergeben.

    def writeReg(self, reg, data):
       self.cs.value(1)
       self.writeByte(reg)
       self.writeByte(data)
       self.cs.value(0)

Die oberste Übertragungsschicht regelt den Schreibzugriff auf den DS1302. Damit ein Register auf dem Chip beschrieben werden kann, muss das Write-Protect-Bit in Register Rwp auf 0 gesetzt werden. Danach sende ich die Registernummer und das Datenbit, um in Folge den Schreibschutz wieder zu aktivieren.

    def write(self, reg, data):
       self.writeReg(Rwp, 0)
       self.writeReg(reg, data)
       self.writeReg(Rwp, 0x80)

Abbildung 3 zeigt die Aufteilung der RTC-Register des DS1302. Man findet die Tabelle auf Seite 9 des Datenblatts.

Abbildung 3: Registerlandschaft des DS1302

Abbildung 3: Registerlandschaft des DS1302

CH (Clock Hold), das Bit 7 des Sekundenregisters, stoppt die Uhr, wenn es gesetzt ist. Die Ansteuerung übernehmen die beiden folgenden Methoden start() und stop().

    def start(self):
       h = self.readReg(Rsekunde | 1)
       self.write(Rsekunde, h & 0x7f)

   def stop(self):
       h = self.readReg(Rsekunde | 1)
       self.write(Rsekunde, h | 0x80)

Die meisten der weiteren Methoden lesen die entsprechenden Register für Sekunde, Minute etc. aus, wenn sie ohne Argument aufgerufen werden. Wird dagegen eine Zahl übergeben, dann wird diese als entsprechender Zeit- oder Datumswert interpretiert, geprüft oder auf gültige Werte eingegrenzt und an den DS1302 gesendet. Zum Lesen vom DS1302 muss das R/-W-Bit im Adress-Byte auf 1 gesetzt sein, zum Schreiben auf 0. Die Uhr wird angesprochen, wenn außerdem das Bit 6 RAM/-CK auf 0 steht. Wird es auf 1 gesetzt, dann wird eines der 31 RAM-Bytes adressiert. Dazu später mehr.

    def sekunde(self, second=None):
       if second == None:
           return self.bcd2dec(self.readReg(Rsekunde | 1))%60
       else:
           self.write(Rsekunde, self.dec2bcd(second % 60))

Die Methode dateTime(), ohne Argument aufgerufen, gibt ein Datum-Zeit-Tupel im Standardformat zurück. Dazu werden die einzelnen Register abgefragt und zum Tupel zusammengesetzt. Für die Berechnung der Tagesnummer im Jahreslauf werden die Tageszahlen der Monate vor dem aktuellen Monat aufsummiert und dann das Monatsdatum addiert. Im Falle eines Schaltjahres wird die Summe um 1 erhöht, wenn die Monatsnummer 3 oder höher ist. Ob ein Schaltjahr vorliegt, überprüft die Methode schaltjahr(), dann wird von dieser True zurückgegeben.

    def schaltjahr(self, jahr):
       sj = ((jahr % 400) == 0) or \
            ((jahr % 4) == 0 and (jahr % 100) != 0)
       return sj
       

   def dateTime(self, data=None, **weitere):
       if data == None:
           j=self.jahr()
           m=self.monat()
           d=self.mtag()
           doy = 0
           for n in range(m-1):
               doy += tage[n]
           doy = doy + d + 1 if self.schaltjahr(j)and m>2 \
                             else doy+d
                   self.stunde(),
                   self.minute(),
                   self.sekunde(),
                   self.dow(),doy]
       else:
           self.jahr(data[0])
           self.monat(data[1])
           self.mtag(data[2])
           self.stunde(data[3])
           self.minute(data[4])
           self.sekunde(data[5])
           self.dow(data[6])

Der DS1302 stellt neben der RTC auch 31 Bytes ebenfalls batterie-gepufferten RAM-Speicher zur Verfügung. Der Inhalt bleibt also auch bei ausgeschaltetem Controller erhalten.

An dieser Stelle gehe ich zur näheren Erläuterung auf den Aufbau des Adressbytes des DS1302 ein. Man findet eine Darstellung auf Seite 5 des Datenblatts.

Abbildung 4: Das Adress-Byte des DS1302

Abbildung 4: Das Adress-Byte des DS1302

Neben der Adresse selbst beinhaltet das Byte zwei Steuerbits. Bit 0 entscheidet über einen Lesezugriff (RD/-WR = 1) oder einen Schreibzugriff (RD/-WR = 0). Mit Bit 6 kann ich den Zugriff auf das RAM (RAM/-CK = 1) oder auf die RTC (RAM/-CK = 0) steuern. Bit 7 ist immer auf 1 zu setzen.

Die Adressen der Register sind bereits durch die Konstanten am Beginn des Modul-Listings so angegeben, dass sich für einen den RTC-Schreib-Zugriff auf das Sekundenregister, das eigentlich das Register 0 wäre, 0x80 = 0b10000000 ergibt und für einen Lesezugriff 0x81 = 0b10000001. Beim Zugriff auf das RAM muss Bit 6 gesetzt sein. Die erste RAM-Speicherzelle hat daher die Adresse 0xC0 = 0b11000000 bei einem Schreibzugriff und 0xC1 = 0b11000001 beim Lesen. Während bei der RTC die Registernummern eindeutig einem Namen zugewiesen sind, müssen die RAM-Bytes individuell adressiert werden. Das geschieht dadurch, dass zur Basisadresse 0xC0 die Zellen-Adresse von 0x00 bis 0x1F hinzugefügt werden muss, aber nicht als Bit 0 bis 4, sondern als Bit 1 bis 5, wie es in Abbildung 4 dargestellt ist. Die Zellennummer muss also um eine Bitposition nach links geschoben werden. Aus 0x14 wird beispielsweise somit 0x28. Außerdem muss für einen Lesezugriff noch eine 1 addiert werden, 0x29. Weil das Schieben als arithmetische Operation eine niedrigere Priorität besitzt, als die Addition, muss das Schieben um 1 komplett geklammert werden. Das alles berücksichtigt die Methode ram(). In reg wird die Nummer der Speicherzelle übergeben, nicht deren Adresse! Fehlt das Argument data beim Aufruf, dann wird der Inhalt der Zelle abgerufen, andernfalls wird die Zelle mit dem Wert in data beschrieben.

    def ram(self, reg, data=None):
       if data == None:
           adr=RramStart + ((reg % 31)<<1)  + 1
           print(hex(RramStart),hex(adr))
           return self.readReg(adr)
       else:
           self.write(RramStart + ((reg % 31)<<1) + 0, data)

Mit dem RAM-Speicher im DS1302 haben wir also die Möglichkeit Daten über eine (fast) beliebige Zeitdauer hinwegzuretten. Der Chip erleidet nur dann eine Amnesie, wenn die Batterie leer ist, oder entfernt wird.

Einen NTP-Server abfragen

Für den Kontakt zu einem NTP-Server ist ein WLAN-Zugang erforderlich. Zwei Module müssen wenigstens importiert werden, network und ntptime, dazu noch ein paar Funktionen. Das Programm ntp_test.py demonstriert den Zugriff auf einen NTP-Server, hier pool.ntp.org. Diese URL ist im Modul ntptime bereits vorgegeben. Für den recht simplen Zugriff auf die Netzzeit durch ntptime.time(), der uns die Weltzeit UTC in Sekunden holt, ist ein vergleichsweise riesiger Overhead erforderlich. Denn es muss das Station-Interface des ESP8266/ESP32 eingerichtet und eine WLAN-Verbindung aufgebaut werden. Hier das Listing:

# ntp_test.py
import ntptime as ntp
import network
from time import sleep, localtime

timeZone=1

# **************WLAN-Zugriff definieren*******************
#
mySSID="here goes your SSID"
myPass="here goes your password"
myPort=9009

connectStatus = {
   1000: "STAT_IDLE",
   1001: "STAT_CONNECTING",
   1010: "STAT_GOT_IP",
   202:  "STAT_WRONG_PASSWORD",
   201:  "NO AP FOUND",
   5:    "UNKNOWN",
   0: "STAT_IDLE",
   1: "STAT_CONNECTING",
   5: "STAT_GOT_IP",
   2:  "STAT_WRONG_PASSWORD",
   3:  "NO AP FOUND",
   4:  "STAT_CONNECT_FAIL",
  }

# **************Funktionen definieren*********************
#
def TimeOut(t):
   start=ticks_ms()
   def compare():
       return int(ticks_ms()-start) >= t
   return compare

def hexMac(byteMac):
 """
Die Funktion hexMAC nimmt die MAC-Adresse im Bytecode und
bildet daraus einen String fuer die Rueckgabe
"""
 macString =""
 for i in range(0,len(byteMac)):     # Fuer alle Bytewerte
   macString += hex(byteMac[i])[2:]  # String ab 2 bis Ende
   if i <len(byteMac)-1 :            # Trennzeichen
     macString +="-"                 # bis auf letztes Byte
 return macString

nic = network.WLAN(network.AP_IF)  # AP-Interface-Objekt
nic.active(False)                  # sicher ausschalten
nic = network.WLAN(network.STA_IF) # WiFi-Objekt erzeugen
nic.active(True)                   # STA-Objekt nic ein
#
MAC = nic.config('mac')   # binaere MAC-Adresse abrufen und  
myMac=hexMac(MAC)         # in eine Hexziffernfolge umwandeln
print("STATION MAC: \t"+myMac+"\n") # ausgeben
#
# Verbindung mit AP im lokalen Netzwerk aufnehmen,
# falls noch nicht verbunden
if not nic.isconnected():
 # Zum AP im lokalen Netz verbinden und Status anzeigen
 nic.connect(mySSID, myPass)

 # warten bis die Verbindung zum Accesspoint steht
 print("connection status: ", nic.isconnected())

 while (nic.status() != network.STAT_GOT_IP):
   print(".",end='')
   sleep(1)

# Wenn verbunden, zeige Verbindungsstatus und Config-Daten
nicStatus=nic.status()
print("\nVerbindungsstatus: ",connectStatus[nicStatus])
STAconf = nic.ifconfig()
print("STA-IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",STAconf[1],\
     "\nSTA-GATEWAY:\t",STAconf[2] ,sep='')
sleep(3)

while 1:
   try:
       tag=localtime(ntp.time()+timeZone*3600)
       print("NTP-Time       ",tag)
   except:
       print("Uebertragungsfehler")
   sleep(1)

Nach dem Start des Programms bekommen wir die MAC-Adresse des STA-Interfaces des Controllers mitgeteilt. Diese Adresse muss im WLAN-Router eingetragen werden, damit dessen Türsteher den ESP32/ESP8266 auch ins Etablissement einlässt. Wer nicht bekannt ist, bleibt draußen. Es ist übrigens keine gute Idee, den MAC-Filter im WLAN-Router auszuschalten, eben weil dann Hinz und Kunz ohne weiteres einloggen können. Wo und wie Sie die MAC eintragen können, verrät Ihnen Ihr Router-Handbuch.

STATION MAC:    d8-bf-c0-14-ba-6c

Läuft irgendwo im LAN ein DHCP-Server, meist ist das der Router selbst, der den Dienst bereitstellt, dann bekommt der Controller von diesem eine IP-Adresse zugewiesen und wir starten in die Hauptschleife. Dort wird jede Sekunde der NTP-Server kontaktiert. Zum empfangenen Sekundenwert addieren wir den Versatz durch die Zeitzone und lassen uns das Tupel ausgeben.

Verbindungsstatus:  STAT_GOT_IP
STA-IP: 10.0.1.215
STA-NETMASK: 255.255.255.0
STA-GATEWAY: 10.0.1.20
NTP-Time       (2022, 12, 4, 14, 32, 41, 6, 338)
NTP-Time       (2022, 12, 4, 14, 32, 42, 6, 338)
NTP-Time       (2022, 12, 4, 14, 32, 43, 6, 338)
NTP-Time       (2022, 12, 4, 14, 32, 44, 6, 338)
NTP-Time       (2022, 12, 4, 14, 32, 45, 6, 338)

Der Uhrenvergleich

Dieses Basisprogramm können wir nun beliebig erweitern. Ich habe das dahingehend getan, dass ich zunächst eine Option eingebaut habe, durch welche eine Synchronisation der Bord-RTC des ESP32/ESP8266 und auch die RTC des DS1302 mit der NTP-Zeit durchgeführt wird. Dazu verwende ich die Flashtaste des ESP32/ESP8266 – NodeMCU. Der ESP8266 D1 mini pro besitzt keine Flashtaste, deshalb musste ich ihm ein Tastenmodul spendieren. Damit ist er ganz glücklich der Kleine.

Auf Tastendruck wird also synchronisiert, ohne Tastendruck startet das Programm ohne Synchronisation. Die Aufforderung zur Tastenbetätigung kommt über das OLED-Display. Nun kann ich jeweils in einem bestimmten, durch sleep() einstellbaren Intervall, in der Hauptschleife die NTP-Zeit abrufen und mit der Bord-RTC und dem DS1302 vergleichen. So habe ich die eingangs beschriebenen Zeitabweichungen feststellen können. Was ich absolut erstaunlich fand war die Tatsache, dass die RTC des ESP32 lediglich eine Gangabweichung von nur +2 Sekunden pro Tag aufwies!

Ganz anders verhält es sich beim ESP8266. Das Programm DS1302-set+get.py habe ich um 11:40 Uhr mit Synchronisation gestartet. Eine Stunde später ergab sich bereits eine Abweichung von +68 Sekunden! Während der DS1302 nicht signifikant um nur -1 Sekunde von der NTP-Zeit abweicht.

NTP-Time: 723382804
NTP-Zeit       (2022, 12, 3, 12, 40, 4, 5, 337)
RTC             (2022, 12, 3, 12, 41, 12, 5, 967)
DS1302 Zeit     [2022, 12, 3, 12, 40, 3, 5, 337]

Hier die Schaltungen für das Experiment.

Abbildung 5: ESP8266 D1 mini pro in der Testschaltung

Abbildung 5: ESP8266 D1 mini pro in der Testschaltung

Abbildung 6: ESP32 in der Testschaltung

Abbildung 6: ESP32 in der Testschaltung

Und hier ist das Listing des Programms DS1302-set+get.py. Es ist für ESP32 und ESP8266 gleichermaßen ohne Änderung verwendbar. Es gibt auch noch eine abgemagerte Version rtc-set+get.py, in der die Interaktionen mit dem DS1302 herausgenommen wurden. Damit lässt sich nur die RTC im Vergleich zur NTP-Zeit untersuchen.

# DS1302-set+get.py
# Pintranslator fuer ESP8266-Boards
# LUA-Pins     D0 D1 D2 D3 D4 D5 D6 D7 D8
# ESP8266 Pins 16 5 4 0 2 14 12 13 15
#                 SC SD

from machine import Pin, SoftI2C, RTC, deepsleep
from time import sleep, time, localtime, ticks_ms
import ntptime as ntp
from oled import OLED
import network
import sys
from ds1302 import DS1302


if sys.platform == "esp8266":
   i2c=SoftI2C(scl=Pin(5),sda=Pin(4))
elif sys.platform == "esp32":
   i2c=SoftI2C(scl=Pin(22),sda=Pin(21))
else:
   raise RuntimeError("Unknown Port")

d=OLED(i2c,heightw=64) # 128x32-Pixel-Display
d.clearFT(0,0,15,2)
d.writeAt("UHRENVERGLEICH",0,0)
d.writeAt("NTP-ZEIT",3,1)
d.writeAt("RTC+DS1302-ZEIT",0,2)
sleep(1)
d.clearFT(0,0,15,2)

timeZone=1
ds= DS1302(clk=13,dio=14,cs=12)

rtc=RTC()

taste=Pin(0,Pin.IN, Pin.PULL_UP)

# **************WLAN-Zugriff definieren*******************
#
mySSID="here goes your SSID"
myPass="here goes your password"
myPort=9009

connectStatus = {
   1000: "STAT_IDLE",
   1001: "STAT_CONNECTING",
   1010: "STAT_GOT_IP",
   202:  "STAT_WRONG_PASSWORD",
   201:  "NO AP FOUND",
   5:    "UNKNOWN",
   0: "STAT_IDLE",
   1: "STAT_CONNECTING",
   5: "STAT_GOT_IP",
   2:  "STAT_WRONG_PASSWORD",
   3:  "NO AP FOUND",
   4:  "STAT_CONNECT_FAIL",
  }

# **************Funktionen definieren*********************
#
def TimeOut(t):
   start=ticks_ms()
   def compare():
       return int(ticks_ms()-start) >= t
   return compare

def hexMac(byteMac):
 """
Die Funktion hexMAC nimmt die MAC-Adresse im Bytecode und
bildet daraus einen String fuer die Rueckgabe
"""
 macString =""
 for i in range(0,len(byteMac)):     # Fuer alle Bytewerte
   macString += hex(byteMac[i])[2:]  # String ab 2 bis Ende
   if i <len(byteMac)-1 :            # Trennzeichen
     macString +="-"                 # bis auf letztes Byte
 return macString

   
def getRtcTime():
       yr,mon,day,dow,hor,minute,sec,ms=rtc.datetime()
       return(yr,mon,day,hor,minute,sec,dow,ms)
   
def setRtcTime(dt):
   y,mo,day,h,m,s,dow,doy=dt
   rtc.datetime((y,mo,day,0,h,m,s,0))
   # synchronisiert die RTC automatisch mit UTC + Zeitzone
   print("RTC time set to",rtc.datetime())

# ******************* Station einrichten ********************
# Netzwerk-Interface-Instanz erzeugen
# und ESP32-Stationmodus aktivieren;
#
nic = network.WLAN(network.AP_IF)  # AP-Interface-Objekt
nic.active(False)                  # sicher ausschalten
nic = network.WLAN(network.STA_IF) # WiFi-Objekt erzeugen
nic.active(True)                   # STA-Objekt nic ein
#
MAC = nic.config('mac')   # binaere MAC-Adresse abrufen und  
myMac=hexMac(MAC)         # in eine Hexziffernfolge umwandeln
print("STATION MAC: \t"+myMac+"\n") # ausgeben
#
# Verbindung mit AP im lokalen Netzwerk aufnehmen,
# falls noch nicht verbunden
if not nic.isconnected():
 # Zum AP im lokalen Netz verbinden und Status anzeigen
 nic.connect(mySSID, myPass)
 # warten bis die Verbindung zum Accesspoint steht
 print("connection status: ", nic.isconnected())
 n=0
 line="..........."
 while (nic.status() != network.STAT_GOT_IP) and (n < 10):
   n+=1
   print(".",end='')
   d.writeAt(line[0:n],0,2)
   sleep(1)
# Wenn verbunden, zeige Verbindungsstatus und Config-Daten
nicStatus=nic.status()
print("\nVerbindungsstatus: ",connectStatus[nicStatus])
#STAconf = nic.ifconfig((myIP,"255.255.255.0",myGW,myDNS))
STAconf = nic.ifconfig()
print("STA-IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",STAconf[1],\
     "\nSTA-GATEWAY:\t",STAconf[2] ,sep='')
d.clearFT(0,0,15,2)
if nicStatus in (5,1010):
   d.writeAt(STAconf[0],0,0)
   d.writeAt(STAconf[1],0,1)
   d.writeAt(STAconf[2],0,2)
else:
   d.writeAt("STATION CONNECT",0,0)
   d.writeAt("FAILED",4,1)
   d.writeAt("USING RTC",2,2)
sleep(3)

d.clearAll()
d.writeAt("FLASH-TASTE",0,0)
d.writeAt("ZUM SETZEN DER",0,1)
d.writeAt("RTC-ZEIT",0,2)
sleep(3)
if taste.value() == 0:
   try:
       tag=localtime(ntp.time()+timeZone*3600)
       print("NTP-Time       ",tag)
       y,mo,day,h,m,s,dow,doy=tag
       rtc.datetime((y,mo,day,0,h,m,s,0))
       # synchronisiert die RTC mit UTC + Zeitzone
       print("RTC Synchonized\n",getRtcTime())
       ds.dateTime(tag)
       dsTime=ds.dateTime()
       print("DS1302 Zeit synchronized\n",dsTime)
   except:
       print("RTC-Time not set",rtc.datetime())
d.clearAll()
d.writeAt("FLASH-TASTE",0,0)
d.writeAt("LOSLASSEN",0,1)
sleep(3)
d.clearAll()

# Systemzeit-Format und DS1302-Format:
# jahr, Monat, Tag, Stunden, Minuten, Sekunden, DOW, DOY

while 1:
   try:
       # NTP-Zeit-Format
       # jahr,Monat,Tag,Stunden,Minuten, Sekunden, DOW, DOY
       ntpSec=ntp.time()
       if ntpSec > 0:
           print("NTP-Time:",ntpSec)
           dt=localtime(ntpSec+timeZone*3600)
       else:
           print("NTP-Zeit - Fehler")
           d.writeAt("NTP TIMED OUT",0,1)
           dt=(2022,12,17,14,46,31,0,0) # dummy Zeitstempel
       print("NTP-Zeit       ",dt)
       dh=dt[3]; dm=dt[4]; dsec=dt[5]
       d.writeAt("NTP__{:02}:{:02}:{:02} ".\
                 format(dh,dm,dsec),0,0)
       
       # RTC-Zeit:
       # jahr, Monat, Tag, DOW, Stunden,Minuten,Sekunden, ms
       rtcTime=rtc.datetime()
       rh=rtcTime[4]; rm=rtcTime[5]; rs=rtcTime[6]
       print("RTC           ",getRtcTime())
       d.writeAt("RTC_ {:02}:{:02}:{:02} ".\
                 format(rh,rm,rs),0,1)

       # DS1302-Zeit-Format
       # jahr,Monat,Tag,Stunden,Minuten, Sekunden, DOW, DOY
       dsTime=ds.dateTime()
       print("DS1302 Zeit   ",ds.dateTime())
       dh=dsTime[3]; dm=dsTime[4]; dsec=dsTime[5]
       d.writeAt("DS__ {:02}:{:02}:{:02}".\
                 format(dh,dm,dsec),0,2)
       print("")
   except OSError as e:
       print("Error:",e)
       d.clearFT(0,0,15,2)
       d.writeAt("OS-ERROR",0,1)
   sleep(10)

Die Ausgabe der Uhrzeit erfolgt im Terminalfenster und auf dem OLED-Display. Dadurch kann man den Test auch ohne PC fahren, wenn man das jeweilige Programm unter dem Namen main.py auf den ESP32/ESP8266 schickt. Nach einem Reset startet das Programm autonom.

Abbildung 7: Die RTC-Zeit des ESP32 nach mehr als 24 Stunden

Abbildung 7: Die RTC-Zeit des ESP32 nach mehr als 24 Stunden

Abbildung 8: ESP8266-NodeMCU mit DS1302-RTC

Abbildung 8: ESP8266-NodeMCU mit DS1302-RTC

Ich denke, sie haben jetzt einen guten Überblick über die Zeiterfassung auf einem Microcontroller. Das RTC-Modul und ein DS1302 lassen sich aber auch noch zu anderen Zwecken nutzen. Das erzähle ich in einer neuen Blogfolge - Teil 2 der Blog-Reihe. Also – bleiben Sie dran!

DisplaysEsp-32Esp-8266Projekte für fortgeschritteneSmart home

Kommentar hinterlassen

Alle Kommentare werden von einem Moderator vor der Veröffentlichung überprüft

Empfohlene Blogbeiträge

  1. ESP32 jetzt über den Boardverwalter installieren - AZ-Delivery
  2. Internet-Radio mit dem ESP32 - UPDATE - AZ-Delivery
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1 - AZ-Delivery
  4. ESP32 - das Multitalent - AZ-Delivery