After our ESP32-DS3231-HYBRIDWUCH is already running excellently, we want to grant her to access the official period in Germany today. This is made possible by the long-wave transmitter DCF77, which the PTB (Physikalisch-Technische Bundesanstalt Braunschweig) operates with Mainflingen location near Frankfurt.
Today we will deal with the coding of the times of this transmitter and derive a micropython module from it. This allows us to patter our RTC to a "atomic clock". With our highly accurate RTC (real-time clock) in the DS3231, it is sufficient if we make a comparison with the DCF77 once a day. It would probably be enough once a week, because after the last test, I only found the RTC to be followed by about 3 seconds in a period of about 10 days.
As we can dock on the DCF77 with ESP32 and Micropython, we look at it in this episode, from the line
Micropython on the ESP32 and ESP8266
today
The times of the DCF77
The call sign DCF77 is made up of D for Germany, C for long -wave channels and F for the proximity to Frankfurt. 77.5 kHz is the transmitter frequency of the transmitter, which radiates an output of 30 kW over the antenna.
Every second, the carrier amplitude is lowered to 15% for 100 or 200 milliseconds. A 100MS lowering corresponds to a logical 0, 200ms coding a logical 1. From the 16th second, the bits contain time information. Bit 20 sets the start of time and date codes with a 1, for us we really get down to business with Bit 21.
With each second time, a BIT of a BCD code (binary coded decimal) is transmitted for the minutes, hours and the date including the weekday. In between there are test bits. Such a parity bit is 1 if the number of 1 bits in the data field is odd, and 0 if a number of 1 is contained. We speak of a straight parity or even parity. Figure 1 gives an overview of the nature of a code frame.
Figure 1: DCF77 coding
An amplitude reduction of the wearer on the transmitter corresponds to an impulse of VCC of the same duration on our recipient module. At the output of the DCF77 module, the negated transmitter signal lies.
Figure 2: Bit coding of the DCF77 signal
In order to synchronize ourselves with the transmitter, we have to find the beginning of a time frame, the bit 0. This helps us the 59th second, for which no lowering takes place and therefore no impulse is generated at the module output.
According to the falling flank of the parity bite of the date, there is no rising flank for more than a second. We are waiting for this moment. The next rising flank then heralds the beginning of a new minute and we start scanning the second impulses. We put the bit values in a bytearar. Once all bits have been troubled, we can take the BCD code bitterly from the array and put together to form a time stamp. We synchronize our RTC.
Hardware
The list of the previous hardware from the previous articles (Read RC, Send RC-IR code, PS/2 keyboard on the ESP32, A good RTC, 7-segment display) I only expanded the DCF77 reception module that we need to synchronize our watch.
Figure 3: DCF77 module
Figure 4: DCF77 reception module on ESP32
1 | ESP32 Dev Kit C unpleasant or ESP32 NODEMCU Module WiFi Development Board or Nodemcu-ESP-32S kit |
---|---|
1 | Ky-022 Set IR receiver |
1 | Ky-005 IR Infrarot transmitter transceiver module |
1 | 0.91 inch OLED I2C display 128 x 32 pixels |
1 | Breadboard Kit - 3x Jumper Wire M2M/F2M/F2F + 3 Set MB102 Breadbord Compatible with Arduino and Raspberry Pi - 1x Set |
1 | Ky-004 button module |
various | Jumper Wire cable 3 x 40 pcs |
1 | Real Time Clock RTC DS3231 I2C real -time clock |
1 | TM1637 4 DIGIT 7 segment LED display module |
1 | Ky-018 Photo LDR resistance Photo resistor sensor |
1 | DCF77 recipient module |
2 | NPN transistor BC337 or similar |
1 | Resistance 1.0 kΩ |
1 | Resistance 10 kΩ |
1 | Resistance 330 Ω |
1 | Resistance 47Ω |
1 | Resistance 560Ω |
1 | LED (color at will) |
1 | Adapter PS/2 according to USB or PS/2 socket |
1 | Logic Analyzer |
1 | PS/2 keyboard |
The module can be operated with tensions of 1.2 to 3.3V and only marginal with less than 90µA with less than 90µA.
The reception frequency of 77.5 kHz lies in the area in which switching power supplies work. As a result, energy -saving lamps, for example, can interfere with the reception due to interference. Unfortunately, our 7-segment display also works with a multiplex rate of approx. 45kHz. If the reception of the DCF77 module is disturbed, we should switch off the display during synchronization. The antenna (ferrits tab and spool) must not come close to the connection between module output and ESP32. Apparently this leads to wild consequences by feedback.
Figure 5: interference signals by crossing antenna and signal output
It should look like this:
Figure 6: Regular impulse image
In Figure 7 you will see the entire circuit of the radio clock:
Figure 7: Everything together = radio clock with IR-RC ambitions
The software
For flashing and the programming of the ESP32:
Thonny or
Logic 2 operating software From Saleae
Used firmware for the ESP32:
Used firmware for the ESP8266:
The micropython programs for the project:
tm1637_4.py: API for the 4- and 6-digit 7-segment display with the TM1637
ds3231.py: Driver module for the RTC module
oled.py: OLED-API
SSD1306.PY: OLED hardware driver
dcf77.py: Driver for the DCF77 module
ir_rx-small.zip: Package for the IR reception module
irsend.py: IR broadcast module
timeout.py: Software -timer
Micropython - Language - Modules and Programs
To install Thonny you will find one here detailed instructions (English version). There is also a description of how that Micropython firmware (As of 18.06.2022) on the ESP chip burned becomes.
Micropython is an interpreter language. The main difference to the Arduino IDE, where you always flash entire programs, is that you only have to flash the Micropython firmware once on the ESP32 so that the controller understands micropython instructions. You can use Thonny, µpycraft or ESPTOOL.PY. For Thonny I have the process here described.
As soon as the firmware has flashed, you can easily talk to your controller in a dialogue, test individual commands and see the answer immediately without having to compile and transmit an entire program beforehand. That is exactly what bothers me on the Arduino IDE. You simply save an enormous time if you can check simple tests of the syntax and hardware to trying out and refining functions and entire program parts via the command line before knitting a program from it. For this purpose, I always like to create small test programs. As a kind of macro, they summarize recurring commands. Whole applications then develop from such program fragments.
Autostart
If the program is to start autonomously by switching on the controller, copy the program text into a newly created blanks file. Save this file under boot.py in WorkSpace and upload it to the ESP chip. The program starts automatically the next time the reset or switching on.
Test programs
Programs from the current editor window in the Thonny-IDE are started manually via the F5 button. This can be done faster than the mouse click on the start button, or via the menu run. Only the modules used in the program must be in the flash of the ESP32.
In between, Arduino id again?
Should you later use the controller together with the Arduino IDE, just flash the program in the usual way. However, the ESP32/ESP8266 then forgot that it has ever spoken Micropython. Conversely, any espressif chip that contains a compiled program from the Arduino IDE or AT-Firmware or Lua or ... can be easily provided with the micropython firmware. The process is always like here described.
How does the Micropython DCF77 module work?
As usual, we start with a few imports Pin code from machine, some methods time for the time and to rest and array out of array For the bytearar to remember the bit values.
From machine import Pin code, timer
From time import Ticks_us, Ticks_Diff, sleep, Sleep_ms
From array import array
The DCF77 class has a very extensive constructor. __init__() takes the GPIO numbers for the signal input DCF, the connection sec For the red second time LED and the output wait For the blue LED.
XXXXXXXXXX
def __init__(self,DCF=18,sec=4,wait=19):
self.seconds =array("B",0 for _ in range(60))
self.begin = 0
self.end = 0
self.delay = 0
self.counter = 0
self.triggered = False
self.flash=False
self.sec59=False
self.secled=Pin code(sec,Pin code.OUT, value=0)
self.Waitled=Pin code(wait,Pin code.OUT,value=0)
self.DCF=Pin code(DCF, Pin code.IN)
self.DCF.IRQ(handler = None, trigger=Pin code.Irq_rising)
print("DCF77 initialized")
We declare some instance attributes and create the GPIO objects. Then we report that Interrupt For the signal input of the DCF77 module, however, do not yet assign an ISR (interrupt service routine). Finally, the constructor reports the willingness of the DCF77 object.
We declare the method for flashing signals flashing(). It takes the pulse duration, a subsequent break and the GPIO object of the LED.
XXXXXXXXXX
def flashing(self,Pulse,Break,LED):
LED.on()
Sleep_ms(Pulse)
LED.off()
Sleep_ms(Break)
The LED is switched on, we wait Pulse Milliseconds, switch off the LED and wait Break Milliseconds.
We measure the length of the second pulse with our ISR Stopwatch (). With the parameter pin code we get the GPIO object that the IRQ triggered. We ask the level. If it is at 1, a rising flank preceded, a new second start.
XXXXXXXXXX
def stopwatch(self,pin code):
IF pin code.value()==1:
self.DCF.IRQ(handler = None)
self.begin=Ticks_us()
self.secled.on()
Sleep_ms(10)
self.DCF.IRQ(handler = self.stopwatch,\
trigger=Pin code.Irq_falling)
Else:
self.DCF.IRQ(handler = None)
self.end=Ticks_us()
self.delay=Ticks_Diff(self.end,self.begin)
Sleep_ms(10)
self.DCF.IRQ(handler = self.stopwatch,\
trigger=Pin code.Irq_rising)
self.triggered=True
self.secled.off()
We deactivate the IRQ by on the handler None set. Then we remember the stand of the microsis meter and turn on the red LED. After a short waiting time we switch the IRQ sharply again, but it is now triggered by a falling flank.
The falling flank allows us to calculate the duration of the impulse via the µs counter. Here too, the IRQ will also deactivate. Ticks_Diff() Calculates the pulse width with the two flank times and also takes into account a possible counter overflow. Short waiting time, then we switch back to rising flank. We sit triggered on True, so that the completed time measurement is recognized elsewhere. This calculation is not advisable here because an ISR should be kept as short as possible. The LED is switched off and stopwatch() did your job.
XXXXXXXXXX
def wait(self,pin code):
self.begin=Ticks_us()
self.triggered=True
self.flash=True
Also the method wait() is an ISR that is used to wait for the start of a new minute. It stores the start time of a flank, sets the two attributes triggered and flash on true.
We have to wait for the start of a new minute before we can start recording a time frame. That makes the method Waitforstart(). We are pursuing a different strategy here than with the impulse length measurement, which is why we also use another ISR, namely wait(). The normal second-IRQ is deactivated, we set for this wait() As a handler, we start with a falling flank. The flag flash We put on to blink the blue LED False And go into the While loop.
XXXXXXXXXX
def Waitforstart(self):
self.DCF.IRQ(handler = None)
self.DCF.IRQ(handler = self.wait, trigger=Pin code.Irq_falling)
print("Wait at the start of the minute")
self.flash=False
while 1:
IF self.triggered and Ticks_Diff(Ticks_us(),\
self.begin) > 1200000:
self.DCF.IRQ(handler = None)
self.counter = 0
self.DCF.IRQ(handler = self.stopwatch, \
trigger=Pin code.Irq_rising)
break
IF self.triggered and Ticks_Diff(Ticks_us(),\
self.begin) < 300000 and self.flash:
self.DCF.IRQ(handler = None)
self.flash = False
Sleep_ms(50)
IF self.DCF.value()==0:
self.flashing(20,1,self.Waitled)
self.DCF.IRQ(handler = self.wait,\
trigger=Pin code.Irq_falling)
We found what we were looking for when wait() has been triggered by a falling flank and so far no further flank has occurred for more than 1.2 seconds. The minute counter is set to 0 and the IRQ handler is back up stopwatch() changed with increasing flank. With break Let's leave the While loop and thus the routine. Now the system is ready to register the upcoming rising flank of the zero minute.
Have passed less than 0.3 s and flash is through wait() on True have been set, then we put flash on False Back, let the LED flashing and continue waiting for the next falling flank in about 0.65 seconds.
So it was originally thought. Unfortunately, the LED said by flickering several times that something is wrong. With the DSO (digital memory oscilloscope) I quickly got behind what was the cause. The second impulses were not clean. Sporadically, short interference impulses appeared in the rising and/or also falling flank, as with a button prals. In Figure 8, a flashing is triggered with the first falling flank of the first needle impulse.
Figure 8: Disturbant impulses at the beginning of the second impulse
Figure 9: Disturbance impulses after the falling flank of the second impulse
To switch off needle impulses, I first deactivated the IRQ. This ironed away a possible second trigger event. Then we set flash on False And a break of 50ms ensures that the plump is safely skipping. If the level is then high, the flashing is ignored. In the case of needle impulses at the end of the second impulse, you are only blinked if the level is still low after the 50 ms. Then the IRQ handler is restored and we are waiting for the next falling flank.
To secure the transmission, the data blocks are brought to the transmission on the transmission side. The parity bit is set when an odd number of 1 occurs in the data block minute, hour or date. The block thus reaches a straight number of ones, even Even Parity. The test bit is 0 if the number of 1en in the date part is straight.
XXXXXXXXXX
def check parity(self):
minute=0
for I in range(21,29):
minute += self.seconds[I]
minute %= 2
hours=0
for I in range(29,36):
hours += self.seconds[I]
hours %= 2
date = 0
for I in range(36,59):
date += self.seconds[I]
date %= 2
return (minute,hours,date)
The method check parity() counts the 1 in the block including parity bit. A straight number results in a 0, an odd number of a 1, which reveals an error. The result is in the form of one Tuels returned and should be (0.0.0).
When calculating the date and time, the BCD bits from the array must seconds Fished out, put together to the decimal digits and the decimal number are calculated from them. That makes the function BCD2dec(), the locally in calcdate() is declared.
The starting positions (C and CC) of the one and tens bits, as well as the number of BCD bits (N and M) are handed over. The digits X and XX are set to 0 at the beginning. In the for-loop, a bit value is multiplied by binary importance 2 and added to the previous result. The tens digit times 10 plus that results in the decimal number.
XXXXXXXXXX
def calcdate(self):
def BCD2dec(C,n,CC,M):
X,XX=0,0
for I in range(n):
X += self.seconds[C+I]*(2**I)
for I in range(M):
XX += self.seconds[CC+I]*(2**I)
X=X+XX*10
return X
M=BCD2dec(21,4,25,3)
H=BCD2dec(29,4,33,2)
D=BCD2dec(36,4,40,2)
dow=0
for I in range(3):
dow += self.seconds[42+I]*(2**I)
M=BCD2dec(45,4,49,1)
y=BCD2dec(50,4,54,4)
return y,M,D,dow,H,M
As a result, I now only have to hand over the corresponding parameters that can be removed from Figure 1. The only exception is the day of the week, which is calculated as a single -digit value in a separate loop using the same method. Finally, the tupel is returned (year, month, day, weekday, hour, minute).
The method synchronize() brings together all previous program particles and returns a TIMESTAMP of the PTB standard.
XXXXXXXXXX
def synchronize(self):
self.Waitforstart()
self.DCF.IRQ(handler = self.stopwatch, \
trigger=Pin code.Irq_rising)
while 1:
IF self.triggered and self.delay > 20000:
self.triggered=False
code=(((self.delay//1000) + 20 ) // 100) - 1
print(self.counter,code)
self.seconds[self.counter]=code
self.counter =(self.counter +1) % 59
IF self.counter == 0:
sleep(0.95)
self.sec59=True
# If Counter == 0: Break
IF self.counter == 0 and self.DCF.value() == 0 \
and self.sec59:
self.sec59=False
parity = self.check parity()
IF parity == (0,0,0):
y,M,D,dow,H,M=self.calcdate()
dt=(y+2000,M,D,dow,H,M,0,0)
print(dt)
# RTC count starts with 0 on Monday, Sunday = 6
# RTC calculates the weekday after the date
# DCF starts on Monday, Sunday = 7
self.DCF.IRQ(handler = None)
return dt
Else:
print("Parity:",parity)
self.DCF.IRQ(handler = None)
sleep(2)
self.Waitforstart()
First we wait for the start of a minute and then activate the DCF IRQ with increasing flank. This then solves the call of the ISR stopwatch() out of.
We go into the endless loop. triggered and sec59 are first by the constructor call False pre -assigned, counter With 0. At some point an increasing flank starts the stopwatch, that is, we remember the start time in begin. The following falling flank sets triggered on True and passes the duration of the pulse as a time difference delay.
If a pulse was triggered and it was longer than 20ms, it is considered a second impulse. Shorter pulses are separated as interference impulse.
We sit triggered on false.
The second impulses usually differ slightly from the nominal length, 100 ms or 200 ms, up and down. The outliers down are taken into account when calculating the bit value by the addition of 20 ms. From the microseconds in delay Let's make milliseconds first, then add 20 and then carry out a full number division with 100. If we subtract a value of 1 or 2., we receive the bit value 0 or 1. The impulse duration must therefore be at least 80 ms or 180 ms for secure detection.
The second counter counter and the bit value in code are output in Repl. The bit value ends up in the array seconds with counter As index. The counter is increased modulo 59, i.e. when reaching the 59th second, it is set to 0.
Once this has occurred, we sleep over the rest of the second and set sec59 back on False.
If the counter has the value 0, the level of the DCF pin is on low and which is 59th second, we have all bits for calculating time and date. sec59 go on False And we carry out the parity check. If this is positive, we have the time and date values calculate. We bring the year in four places and put everything together into a tupel. The Timestamp is output. After the IRQ is wicked, the return takes place.
Of course, a transmission error can occur that is recognized using the party bits. In which area this happened you can see on the tupel output.
Here, too, we take off the DCF-IRQ, wait 2 seconds and then add a new search for the start of the minute.
If you reanimate the associated lines (mark and Alt + 4), then you can start the file with the start of the file dcf77.py Create a DCF77 object in the editor window and thus synchronize the RTC.
XXXXXXXXXX
# if __Name__ == "__Main__":
# From Machine Import Pin, Softi2c
# From DS3231 Import DS3231
# From Machine Import Pin, Softi2c
# I2C = Softi2c (SCL = PIN (22), SDA = PIN (23), Freq = 100000)
# Rtc = ds3231 (i2c)
# Dc = dcf77 ()
# Dt = dc.Synchronize ()
# Rtc.dateTime (dt)
This means that we are at the pulse of official times in Germany and have also compiled the various ingredients that make up an alarm clock. This is exactly what we will build together in the next episode. Together with the mobile phone app, which is made in the subsequent post, we then have a radio-synchronized clock with an alarm clock, which can control another device via RC5 code via IR broadcast. The codes of an RC control can read out, save and retrieve our egg-laying wool milk sow. This means that the Accesspoint of the ESP32 commands can be transmitted via the cell phone, which it passes on as IR-RC5 code.
Stay tuned!
2 comments
Andreas Wolter
@Ben: because he can :)
if there is no Internet connection you can use that module.
Best regards,
Andreas Wolter
AZ-Delivery Blog
Ben
Why bother with this? The ESP32 has WiFi, just use NTP.