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
- GND – for the ground pin.
- VCC – for the supply voltage.
- SDA – for the i2c serial data pin.
- SCL – for the i2c serial clock pin.
Bill Of Materials
- ESP32 development board.
- Gorillacell ESP32 shield.
- 4-pin female-female dupont wires.
- DS3231 RTC module.
Hardware Instruction
- First, attach the ESP32 board on top of the ESP32 shield and make sure that both USB port are on the same side.
- 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.
- 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.
- 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.
- Lastly, connect the ESP32 to the computer through a micro USB cable.
Software Instruction
- Copy the ds3231.py and save it to ESP32 MicroPython root directory.
- Copy and paste to Thonny IDE the example source code, play with it and feel free to modify it adapting according to your needs.
- 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
-
Purchase your Gorillacell ESP32 development kit at: https://gorillacell.kr
-
Peter Hinch DS3231 driver library: https://github.com/peterhinch/micropython-samples/tree/master/DS3231
Posts in this series
- 049 - MicroPython TechNotes: MP3 Player
- 048 - MicroPython TechNotes: Analog Touch Sensor
- 047 - MicroPython TechNotes: E108 GPS
- 046 - MicroPython TechNotes: RF433 Transceivers
- 045 - MicroPython TechNotes: Infrared Transmitter
- 044 - MicroPython TechNotes: Infrared Receiver
- 043 - MicroPython TechNotes: ESP12E WiFi | External WiFi module
- 042 - MicroPython TechNotes: JDY-32 | Bluetooth Low Energy BLE
- 041 - MicroPython TechNotes: Bluetooth HC-06
- 040 - MicroPython TechNotes: Relay
- 039 - MicroPython TechNotes: Electromagnet
- 038 - MicroPython TechNotes: Buzzer
- 037 - MicroPython TechNotes: Servo Motor
- 036 - MicroPython TechNotes: Stepper Motor
- 035 - MicroPython TechNotes: Dual Motor Driver
- 034 - MicroPython TechNotes: DC Motors | Gear Motor and Fan Motor
- 033 - MicroPython TechNotes: TCS34725 RGB Color Sensor
- 032 - MicroPython TechNotes: BMP280 Sensor
- 031 - MicroPython TechNotes: TOF Distance Sensor
- 029 - MicroPython TechNotes: HC-SR04 Ultrasonic Sensor
- 028 - MicroPython TechNotes: DHT11 Temperature and Humidity Sensor
- 027 - MicroPython TechNotes: Rotary Encoder
- 026 - MicroPython TechNotes: Light Dependent Resistor (LDR)
- 025 - MicroPython TechNotes: Joystick
- 024 - MicroPython TechNotes: Slider Switch
- 023 - MicroPython TechNotes: Continuous Rotation Potentiometer
- 022 - MicroPython TechNotes: Potentiometer | Reading an Analog Input
- 021 - MicroPython TechNotes: Color Touch Sensor
- 020 - MicroPython TechNotes: Touch Sensor
- 019 - MicroPython TechNotes: Switch Module
- 018 - MicroPython TechNotes: Button | Reading an Input
- 017 - MicroPython TechNotes: LASER Module
- 016 - MicroPython TechNotes: RGB LED Matrix
- 015 - MicroPython TechNotes: Neopixel 16
- 014 - MicroPython TechNotes: 8x8 Dot Matrix Display (I2C)
- 013 - MicroPython TechNotes: 8x16 Dot Matrix Display (SPI)
- 012 - MicroPython TechNotes: 8x8 Dot Matrix Display (SPI)
- 011 - MicroPython TechNotes: 1.3 OLED Display
- 010 - MicroPython TechNotes: 0.96 OLED Display
- 009 - MicroPython TechNotes: 7 Segment Display
- 008 - MicroPython TechNotes: 16x2 LCD
- 007 - MicroPython TechNotes: RGB LED
- 006 - MicroPython TechNotes: Traffic Light LED Module
- 005 - MicroPython TechNotes: Gorilla Cell LED | MicroPython Hello World
- 004 - MicroPython TechNotes: Gorilla Cell I/O Devices
- 003 - MicroPython TechNotes: Gorillacell ESP32 Shield
- 002 - MicroPython TechNotes: Introduction for Gorillacell ESP32 Dev Kit
- 001 - MicroPython TechNotes: Get Started with MicroPython
- 000 - MicroPython TechNotes: Unboxing Gorillacell ESP32 Development Kit
No comments yet!