030 - MicroPython TechNotes: DS3231 RTC

Introduction

In this article, we will learn to use the DS3231 RTC module with ESP32 using MicroPython programming language.

Pinout

  1. GND – for the ground pin.
  2. VCC – for the supply voltage.
  3. SDA – for the i2c serial data pin.
  4. SCL – for the i2c serial clock pin.

Bill Of Materials

  1. ESP32 development board.
  2. Gorillacell ESP32 shield.
  3. 4-pin female-female dupont wires.
  4. DS3231 RTC module.

Hardware Instruction

  1. First, attach the ESP32 board on top of the ESP32 shield and make sure that both USB port are on the same side.
  2. Next, attach the dupont wires to the RTC module by following the color coding which is black for the ground, red for the VCC, yellow for the SDA pin, and white for the SCL pin.
  3. Next, attach the other end of the dupont wires to the ESP32 shield by matching the colors of the wires to the colors of the pin headers which such as black to black, red to red, and yellow and the following colors to the yellow pin headers. For this experiment, I choose GPIO 21 for the SDA pin and GPIO 22 for the SCL pin.
  4. Next, power the ESP32 shield with an external power supply through a type-C USB cable and make sure that the power switch is set to ON state.
  5. Lastly, connect the ESP32 to the computer through a micro USB cable.

Software Instruction

  1. Copy the ds3231.py and save it to ESP32 MicroPython root directory.
  2. Copy and paste to Thonny IDE the example source code, play with it and feel free to modify it adapting according to your needs.
  3. Enjoy and happy tinkering.

Video Demonstration

Call To Action

For any concern, write your message in the comment section.

You might also like to support my journey on Youtube by Subscribing. Click this to Subscribe to TechToTinker.

Thank you and have a good days ahead.

See you,

– George Bantique | tech.to.tinker@gmail.com

Source Code

1. Example # 1, basic of DS3231 RTC:

 1# More details can be found in TechToTinker.blogspot.com 
 2# George Bantique | tech.to.tinker@gmail.com
 3
 4from machine import Pin
 5from machine import SoftI2C 
 6from ds3231 import DS3231
 7
 8i2c = SoftI2C(scl=Pin(22), sda=Pin(21), freq=400000) 
 9ds = DS3231(i2c)
10
11# The following line of codes can be tested using the REPL:
12# 1. Get the current time
13# ds.get_time()
14# Return:
15#        YYYY, MM, DD, HH, mm, ss, WD, YD
16#        YYYY - year
17#          MM - month
18#          DD - day
19#          HH - hour in 24 hour
20#          mm - minutes
21#          ss - seconds
22#          WD - week day: 1=Monday, 7=Sunday
23#          YD - day of the year
24# 2. Set the current time
25# ds.set_time(YYYY, MM, DD, HH, mm, ss, WD, YD)
26# ds.set_time(2021, 04, 20, 08, 30, 00, 02, 00)

2. Example # 2, application of RTC module:

  1# More details can be found in TechToTinker.blogspot.com   
  2 # George Bantique | tech.to.tinker@gmail.com  
  3 from machine import Pin  
  4 from machine import SoftI2C  
  5 from time import sleep_ms  
  6 from time import ticks_ms  
  7 from ds3231 import DS3231  
  8 from i2c_lcd import I2cLcd  
  9 from rotary_irq import RotaryIRQ  
 10 # 1. RTC object instantiation:  
 11 rtc_i2c = SoftI2C(scl=Pin(22), sda=Pin(21), freq=400000)   
 12 ds = DS3231(rtc_i2c)  
 13 # DS3231 tuple:  
 14 #    YYYY, MM, DD, HH, mm, ss, WD, YD  
 15 #    YYYY - year  
 16 #     MM - month  
 17 #     DD - day  
 18 #     HH - hour in 24 hour  
 19 #     mm - minutes  
 20 #     ss - seconds  
 21 #     WD - week day: 1=Monday, 7=Sunday  
 22 #     YD - day of the year  
 23 # 2. Onboard LED object instantiation:  
 24 led = Pin(2, Pin.OUT)  
 25 # 3. LCD object instantiation:  
 26 lcd_i2c = SoftI2C(scl=Pin(22), sda=Pin(21), freq=400000)   
 27 lcd = I2cLcd(lcd_i2c, 0x20, 2, 16)  
 28 # 4. Rotary Encoder object instantiation:  
 29 r = RotaryIRQ(pin_num_clk=32,   
 30        pin_num_dt=33,   
 31        min_val=0,   
 32        max_val=19,   
 33        reverse=True,   
 34        range_mode=RotaryIRQ.RANGE_WRAP)  
 35 rsw = Pin(34, Pin.IN)  
 36 val_old = r.value()  
 37 menus = ['Display Date',  
 38      'Display Time',  
 39      'Display Wkday',  
 40      'Edit Year',  
 41      'Edit Month',  
 42      'Edit Day',  
 43      'Edit Hour',  
 44      'Edit Minute',  
 45      'Edit Wkday']  
 46 wkdays = ['Monday',  
 47      'Tuesday',  
 48      'Wednesday',  
 49      'Thursday',  
 50      'Friday',  
 51      'Saturday',  
 52      'Sunday']  
 53 months = ['January',  
 54      'February',  
 55      'March',  
 56      'April',  
 57      'May',  
 58      'June',  
 59      'July',  
 60      'August',  
 61      'September',  
 62      'October',  
 63      'November',  
 64      'December']  
 65 working_idx = 0 # holds the menu index  
 66 year = 0    # holds the year  
 67 month = 0    # holds the month <1=January, 12=December>  
 68 day = 0     # holds the day  
 69 hour = 0    # holds the hour in 24 hour <0=12AM, 23=11PM>  
 70 minute = 0   # holds the minue <0-59>  
 71 wkday = 0    # holds the day of the week <1=Monday, 7=Sunday>  
 72 isPress = False # flag for the config / normal mode  
 73 isSaved = False # flag for determining if the configuration is save  
 74 # get the current time  
 75 prev_time = ticks_ms()  
 76 # Prints a string of characters in the LCD  
 77 #  text - the string you want to print  
 78 #  x - x position  
 79 #  y - y position  
 80 def print_line(text, x, y):  
 81   # Calculate the start of character display  
 82   start = 8-len(text)//2  
 83   # Calculate the end of character display  
 84   end = start + len(text)  
 85   # Clears characters before the start position  
 86   lcd.move_to(x,y)  
 87   for i in range(x,start,1):  
 88     lcd.putchar(' ')  
 89   # Clears characters after the end position  
 90   lcd.move_to(end,y)  
 91   for i in range(end,15,1):  
 92     lcd.putchar(' ')  
 93   # Print the desired display  
 94   lcd.move_to(start,y)  
 95   lcd.putstr(text)  
 96 # Performs updating the menu display  
 97 # according to the rotary encoder value  
 98 def process_menu(rotary_dir=0):  
 99   # Global variables here if needed to be edited,  
100   # else, no need to declare as global here.  
101   global working_idx  
102   global year  
103   global month  
104   global day  
105   global hour  
106   global minute  
107   global wkday  
108   # If the rotary value is less than -1 ie:-2,-3,etc:  
109   #  rotary value = -1  
110   # If the rotary value is more than 1:  
111   #  rotary value = 1  
112   # Else  
113   #  rotary value is not modified  
114   if rotary_dir < -1:  
115     rotary_dir = -1  
116   elif rotary_dir > 1:  
117     rotary_dir = 1  
118   # DISPLAY ONLY MODE  
119   if isPress==False:  
120     # Calculate the working index  
121     # based on rotary encoder value.  
122     working_idx += rotary_dir  
123     if working_idx < 0:  
124       working_idx = 0  
125     elif working_idx > len(menus) - 1:  
126       working_idx = len(menus) - 1  
127     # Check if there is menu available in left  
128     # If true, display < character  
129     # If false, display none  
130     lcd.move_to(0,0)  
131     if working_idx==0:  
132       lcd.putchar(' ')  
133     else:  
134       lcd.putchar('<')  
135     # Print the menu based on working index value  
136     print_line(menus[working_idx],1,0)  
137     # Checks if there is menu available in right  
138     # If true, display > character  
139     # If false, display none  
140     lcd.move_to(15,0)  
141     if working_idx==len(menus)-1:  
142       lcd.putchar(' ')  
143     else:  
144       lcd.putchar('>')  
145     # ------------------------  
146     # Process menu selections:  
147     if working_idx==0:  
148       # Display Date  
149       t = ds.get_time()  
150       date = '{:04d}-{:02d}-{:02d}'.format(t[0],t[1],t[2])  
151       print_line(date,1,1)  
152     elif working_idx==1:  
153       # Display Time  
154       t = ds.get_time()  
155       time = '{:02d}:{:02d}'.format(t[3],t[4])  
156       print_line(time,1,1)  
157     elif working_idx==2:  
158       # Display Week Day  
159       t = ds.get_time()  
160       day = wkdays[t[6]-1]  
161       print_line(day,1,1)  
162     elif working_idx==3:  
163       # Display year  
164       t = ds.get_time()  
165       year=t[0]  
166       print_line(str(year),1,1)  
167     elif working_idx==4:  
168       # Display month  
169       t = ds.get_time()  
170       month=t[1]  
171       print_line(months[month-1],1,1)  
172     elif working_idx==5:  
173       # Display day  
174       t = ds.get_time()  
175       day=t[2]  
176       print_line(str(day),1,1)  
177     elif working_idx==6:  
178       # Display hour  
179       t = ds.get_time()  
180       hour=t[3]  
181       print_line(str(hour),1,1)  
182     elif working_idx==7:  
183       # Display minute  
184       t = ds.get_time()  
185       minute=t[4]  
186       print_line(str(minute),1,1)  
187     elif working_idx==8:  
188       # Display wkday  
189       t = ds.get_time()  
190       wkday=t[6]  
191       print_line(wkdays[wkday-1],1,1)        
192   # CONFIGURATION MODE  
193   elif isPress==True:  
194     if working_idx==3:  
195       # Edit year  
196       year+=rotary_dir  
197       print_line(str(year),1,1)  
198     elif working_idx==4:  
199       # Edit month  
200       month+=rotary_dir  
201       if month < 1:  
202         month = 1  
203       elif month > 12:  
204         month = 12  
205       print_line(months[month-1],1,1)  
206     elif working_idx==5:  
207       # Edit day  
208       day+=rotary_dir  
209       if day < 0:  
210         day = 0  
211       elif day > 31:  
212         day = 31  
213       print_line(str(day),1,1)  
214     elif working_idx==6:  
215       # Edit hour  
216       hour+=rotary_dir  
217       if hour < 0:  
218         hour = 0  
219       elif hour > 23:  
220         hour = 23  
221       print_line(str(hour),1,1)  
222     elif working_idx==7:  
223       # Edit minute  
224       minute+=rotary_dir  
225       if minute < 0:  
226         minute = 0  
227       elif minute > 59:  
228         minute = 59  
229       print_line(str(minute),1,1)  
230     elif working_idx==8:  
231       # Edit week day  
232       wkday+=rotary_dir  
233       if wkday < 1:  
234         wkday = 1  
235       elif wkday > 7:  
236         wkday = 7  
237       print_line(wkdays[wkday-1],1,1)  
238 # Prints the initial menus  
239 process_menu()  
240 while True:  
241   # Creates 200 ms interval  
242   if ticks_ms() - prev_time >= 200:  
243     # Checks only for the switch when index is more than 2  
244     # If switch is press, toggle the state of isPress variable  
245     # If isPress is True, config for editing  
246     # If isPress is False back again, save the configs.  
247     if rsw.value()==1 and working_idx>2:  
248       isPress = not isPress  
249       if isPress==True:  
250         process_menu()  
251         isSaved=False  
252       if isPress==False and isSaved==False:  
253         isSaved=True  
254         t = ds.get_time()  
255         if working_idx==3:  
256           ds.set_time(year,t[1],t[2],t[3],t[4],t[5],t[6],t[7])  
257         elif working_idx==4:  
258           ds.set_time(t[0],month,t[2],t[3],t[4],t[5],t[6],t[7])  
259         elif working_idx==5:  
260           ds.set_time(t[0],t[1],day,t[3],t[4],t[5],t[6],t[7])  
261         elif working_idx==6:  
262           ds.set_time(t[0],t[1],t[2],hour,t[4],t[5],t[6],t[7])  
263         elif working_idx==7:  
264           ds.set_time(t[0],t[1],t[2],t[3],minute,t[5],t[6],t[7])  
265         elif working_idx==8:  
266           ds.set_time(t[0],t[1],t[2],t[3],t[4],t[5],wkday,t[7])  
267     # Read the rotary encoder values for processing  
268     val_new = r.value()  
269     if val_old != val_new:  
270       if val_old == 0 and val_new == 19:  
271         val_dif = -1  
272       elif val_old == 19 and val_new == 0:  
273         val_dif = 1  
274       else:  
275         val_dif = val_new - val_old  
276       process_menu(val_dif)  
277       val_old = val_new  
278     # Blink the onboard LED during config mode  
279     if isPress:  
280       led.value(not led.value())  
281     else:  
282       led.value(0)  
283     # Save the current timer counter  
284     prev_time = ticks_ms()  

3. Modified ds3231.py driver library:

  1# ds3231_port.py Portable driver for DS3231 precison real time clock.  
  2 # Adapted from WiPy driver at https://github.com/scudderfish/uDS3231  
  3 # Author: Peter Hinch  
  4 # Copyright Peter Hinch 2018 Released under the MIT license.  
  5 import utime  
  6 import machine  
  7 import sys  
  8 DS3231_I2C_ADDR = 104  
  9 try:  
 10   rtc = machine.RTC()  
 11 except:  
 12   print('Warning: machine module does not support the RTC.')  
 13   rtc = None  
 14 def bcd2dec(bcd):  
 15   return (((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f))  
 16 def dec2bcd(dec):  
 17   tens, units = divmod(dec, 10)  
 18   return (tens << 4) + units  
 19 def tobytes(num):  
 20   return num.to_bytes(1, 'little')  
 21 class DS3231:  
 22   def __init__(self, i2c):  
 23     self.ds3231 = i2c  
 24     self.timebuf = bytearray(7)  
 25     if DS3231_I2C_ADDR not in self.ds3231.scan():  
 26       raise RuntimeError("DS3231 not found on I2C bus at %d" % DS3231_I2C_ADDR)  
 27   def get_time(self, set_rtc=False):  
 28     if set_rtc:  
 29       self.await_transition() # For accuracy set RTC immediately after a seconds transition  
 30     else:  
 31       self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) # don't wait  
 32     return self.convert(set_rtc)  
 33   def convert(self, set_rtc=False): # Return a tuple in localtime() format (less yday)  
 34     data = self.timebuf  
 35     ss = bcd2dec(data[0])  
 36     mm = bcd2dec(data[1])  
 37     if data[2] & 0x40:  
 38       hh = bcd2dec(data[2] & 0x1f)  
 39       if data[2] & 0x20:  
 40         hh += 12  
 41     else:  
 42       hh = bcd2dec(data[2])  
 43     wday = data[3]  
 44     DD = bcd2dec(data[4])  
 45     MM = bcd2dec(data[5] & 0x1f)  
 46     YY = bcd2dec(data[6])  
 47     if data[5] & 0x80:  
 48       YY += 2000  
 49     else:  
 50       YY += 1900  
 51     # Time from DS3231 in time.localtime() format (less yday)  
 52     result = YY, MM, DD, hh, mm, ss, wday -1, 0  
 53     if set_rtc:  
 54       if rtc is None:  
 55         # Best we can do is to set local time  
 56         secs = utime.mktime(result)  
 57         utime.localtime(secs)  
 58       else:  
 59         rtc.datetime((YY, MM, DD, wday, hh, mm, ss, 0))  
 60     return result  
 61 #   def save_time(self):  
 62 #     (YY, MM, mday, hh, mm, ss, wday, yday) = utime.localtime() # Based on RTC  
 63 #     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 0, tobytes(dec2bcd(ss)))  
 64 #     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 1, tobytes(dec2bcd(mm)))  
 65 #     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 2, tobytes(dec2bcd(hh))) # Sets to 24hr mode  
 66 #     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 3, tobytes(dec2bcd(wday + 1))) # 1 == Monday, 7 == Sunday  
 67 #     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 4, tobytes(dec2bcd(mday))) # Day of month  
 68 #     if YY >= 2000:  
 69 #       self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM) | 0b10000000)) # Century bit  
 70 #       self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-2000)))  
 71 #     else:  
 72 #       self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM)))  
 73 #       self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-1900)))  
 74   # Modified by George Bantique / April 19, 2021  
 75   # to add manual saving of time.  
 76   def set_time(self, YY, MM, mday, hh, mm, ss, wday, yday):  
 77     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 0, tobytes(dec2bcd(ss)))  
 78     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 1, tobytes(dec2bcd(mm)))  
 79     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 2, tobytes(dec2bcd(hh))) # Sets to 24hr mode  
 80     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 3, tobytes(dec2bcd(wday + 1))) # 1 == Monday, 7 == Sunday  
 81     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 4, tobytes(dec2bcd(mday))) # Day of month  
 82     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM)))  
 83     self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-1900)))  
 84 #   # Wait until DS3231 seconds value changes before reading and returning data  
 85 #   def await_transition(self):  
 86 #     self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf)  
 87 #     ss = self.timebuf[0]  
 88 #     while ss == self.timebuf[0]:  
 89 #       self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf)  
 90 #     return self.timebuf  
 91 #   # Test hardware RTC against DS3231. Default runtime 10 min. Return amount  
 92 #   # by which DS3231 clock leads RTC in PPM or seconds per year.  
 93 #   # Precision is achieved by starting and ending the measurement on DS3231  
 94 #   # one-seond boundaries and using ticks_ms() to time the RTC.  
 95 #   # For a 10 minute measurement +-1ms corresponds to 1.7ppm or 53s/yr. Longer  
 96 #   # runtimes improve this, but the DS3231 is "only" good for +-2ppm over 0-40C.  
 97 #   def rtc_test(self, runtime=600, ppm=False, verbose=True):  
 98 #     if rtc is None:  
 99 #       raise RuntimeError('machine.RTC does not exist')  
100 #     verbose and print('Waiting {} minutes for result'.format(runtime//60))  
101 #     factor = 1_000_000 if ppm else 114_155_200 # seconds per year  
102 #   
103 #     self.await_transition() # Start on transition of DS3231. Record time in .timebuf  
104 #     t = utime.ticks_ms() # Get system time now  
105 #     ss = rtc.datetime()[6] # Seconds from system RTC  
106 #     while ss == rtc.datetime()[6]:  
107 #       pass  
108 #     ds = utime.ticks_diff(utime.ticks_ms(), t) # ms to transition of RTC  
109 #     ds3231_start = utime.mktime(self.convert()) # Time when transition occurred  
110 #     t = rtc.datetime()  
111 #     rtc_start = utime.mktime((t[0], t[1], t[2], t[4], t[5], t[6], t[3] - 1, 0)) # y m d h m s wday 0  
112 #   
113 #     utime.sleep(runtime) # Wait a while (precision doesn't matter)  
114 #   
115 #     self.await_transition() # of DS3231 and record the time  
116 #     t = utime.ticks_ms() # and get system time now  
117 #     ss = rtc.datetime()[6] # Seconds from system RTC  
118 #     while ss == rtc.datetime()[6]:  
119 #       pass  
120 #     de = utime.ticks_diff(utime.ticks_ms(), t) # ms to transition of RTC  
121 #     ds3231_end = utime.mktime(self.convert()) # Time when transition occurred  
122 #     t = rtc.datetime()  
123 #     rtc_end = utime.mktime((t[0], t[1], t[2], t[4], t[5], t[6], t[3] - 1, 0)) # y m d h m s wday 0  
124 #   
125 #     d_rtc = 1000 * (rtc_end - rtc_start) + de - ds # ms recorded by RTC  
126 #     d_ds3231 = 1000 * (ds3231_end - ds3231_start) # ms recorded by DS3231  
127 #     ratio = (d_ds3231 - d_rtc) / d_ds3231  
128 #     ppm = ratio * 1_000_000  
129 #     verbose and print('DS3231 leads RTC by {:4.1f}ppm {:4.1f}mins/yr'.format(ppm, ppm*1.903))  
130 #     return ratio * factor  
131 #   def _twos_complement(self, input_value: int, num_bits: int) -> int:  
132 #     mask = 2 ** (num_bits - 1)  
133 #     return -(input_value & mask) + (input_value & ~mask)  
134 #   def get_temperature(self):  
135 #     t = self.ds3231.readfrom_mem(DS3231_I2C_ADDR, 0x11, 2)  
136 #     i = t[0] << 8 | t[1]  
137 #     return self._twos_complement(i >> 6, 10) * 0.25  

4. Copy rotary.py in the SOURCE CODE section of:

https://techtotinker.com/2021/04/027-micropython-technotes-rotary-encoder/

5. Copy rotary_irq.py in the SOURCE CODE section of:

https://techtotinker.com/2021/04/027-micropython-technotes-rotary-encoder/

References And Credits

  1. Purchase your Gorillacell ESP32 development kit at: https://gorillacell.kr

  2. Peter Hinch DS3231 driver library: https://github.com/peterhinch/micropython-samples/tree/master/DS3231



Posts in this series



No comments yet!

GitHub-flavored Markdown & a sane subset of HTML is supported.