Get and Set time with DS3231 Plus set Alarm

Hi Everyone,
I have a DS3231 connected to a PicoW

What I’m trying to achieve is…

  1. Manually Set the DS3231 Clock from the Pico clock once when I know the Pico’s clock is correct (set by thony ).
  2. Set the Pico clock from the DS3231 every time it stats up.

I cant figure out the commands to do these two things.
This seems like what everyone with one of this devices connected to a Pico would what to do. Later I want to use an alarm to wake the Pico up but first things first. :slight_smile:

When I run the demo code from WaveShare it seems to work.
Although it just sets the DS3231 clock to a hard coded date time using a format nothing like the Pico’s internal time tuple format and return that date.

I have also been going through the code by Peter Hinch I found here.

That code is way more in depth but it doesn’t run on my hardware and I get the error message

Traceback (most recent call last):
File “”, line 19, in
File “ds3231_gen.py”, line 72, in init
RuntimeError: DS3231 not found on I2C bus at 104

I changed the I2C pins from SCL 16 and SDA 17 in Peter’s code to SDA 20 and SCL 21 which seems to match the pin out specification on WaveShare but I may have misunderstood something.
I would expect to be able to find a standard library for the DS3231 I can include in my code and run something like dateTime=ds3231.getdate() and ds3231.setdate(dateTime) .
Any ideas how I can do this or a pointer in the right direction?
Thanks
David

I think the I2C address that you’re writing in your code is wrong. Please verify the I2C address using I2C scanner.

Then I would recommend sticking with this example. All the library does for you is to conceal some of the detail, but that often makes things harder to figure out when you are getting it to work.

The example uses a date-time format of comma-separated strings to set the time. If you change this string in the code to the current date and time, does it set the clock correctly?

rtc = ds3231(I2C_PORT,I2C_SCL,I2C_SDA)
rtc.set_time('13:45:50,Monday,2021-05-24')

You can ignore the complicated code in the set_time function that extracts these strings and formats the data for the RTC.

Don’t forget that if your example code is setting the time, you want to comment-out that code before runninng it again. Else you will just overwrite the actual time with the hard-coded time again!

It looks like you may need to do the parsing yourself to get from the RTC’s format to the Pico’s format.

Can you post the code you are working on so far? And maybe show what the two time formats are? ie the results from

print('DS3231 time:', ds3231.get_time())
print('RTC time:   ', utime.localtime())

We can have a look at helping you to parse between the two (GPT could also be really helpful here! eg. “write Python code to parse this string [your rtc format here] to this format: [pico's rtc format here]

All good ideas. I’ll post my work in progress.
If I can break it down to an includable library I’ll do that to :cowboy_hat_face:
Thanks
David

1 Like

OK, I have modified the WaveShare example and come up with the following code.
Zip file attached

ds3231.zip (2.1 KB)

This provides two new methods

setDS3231Time() # Set the DS3231 clock from the pico clock
setPicoTime() # Set the pico clock from the DS3231 clock

This way if I’m connected to the pico from Thony by default the pico time will be set correctly from the PC and I can run method setD3231Time() manually to configure the DS3231 accurately.
Each time the pico starts up I can run setPicoTime() so it will have the correct time if running headless and with no Wi-Fi available.

My Next Problem. (this may deserve a new thread)

How to Set a Recurring Alarm
The example code provides a way to set single alarm at a specified date and time.
DS_rtc.set_alarm_time(‘13:45:55,Monday,2021-05-24’)

I basically want to set an alarm to wake the pico at 8:00am and 4:00pm every day.
Then the pico will run for say 2 hours then go to sleep and await the next alarm.
It seems like there are two alarms available on the DS3231 that can be set.
I thought I’d just have to do some time maths to calculate the next alarm but this seems like it is dangerous and I could easily end up with a sleeping beauty :slight_smile:
Micropython also seems to have missed out on the port of any time maths from regular python so I’d have to do that all myself. :frowning:
Adding 16 hours to 6:00pm on the 27th Feb does not sound like much fun.

Have I missed something, is there a way I can set an alarm for 8:00am every day?
Thanks
David

I have added some print statements to set_alarm

Here is the output.

set_alarm_time Start
alarm_time=20:15:00,Saturday,2024-03-30
SSMMHHDD
S=Second, M=Minute, H=Hour, D=Day of Month
now_time=00152030

The odd part is only the day part of the date is included.
This is the line that outputs the alarm to the DS3231

     self.bus.writeto_mem(int(self.address),int(self.alarm1_reg),now_time)

Now if I give it a time in the future nothing happens
I’d expect it to print a message based on this line

        #    set alarm irq
        self.alarm_pin.irq(lambda pin: print("alarm1 time is up"), Pin.IRQ_FALLING)
        #    enable the alarm1 reg
        self.bus.writeto_mem(int(self.address),int(self.control_reg),b'\x05')

Any ideas what I’m missing?
Here is the full code

from machine import Pin, I2C, RTC
import time

import binascii

#    the first version use i2c1
#I2C_PORT = 1
#I2C_SDA = 6
#I2C_SCL = 7

#    the new version use i2c0,if it dont work,try to uncomment the line 14 and comment line 17
#    it should solder the R3 with 0R resistor if want to use alarm function,please refer to the Sch file on waveshare Pico-RTC-DS3231 wiki
#    https://www.waveshare.net/w/upload/0/08/Pico-RTC-DS3231_Sch.pdf

I2C_PORT = 0
I2C_SDA = 20
I2C_SCL = 21

ALARM_PIN = 3

class ds3231(object):
#            13:45:00 Mon 24 May 2021
#  the register value is the binary-coded decimal (BCD) format
#               sec min hour week day month year

    NowTime = b'\x00\x45\x13\x02\x24\x05\x21'
    w  = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
    address = 0x68
    start_reg = 0x00
    alarm1_reg = 0x07
    control_reg = 0x0e
    status_reg = 0x0f
    
    def __init__(self,i2c_port,i2c_scl,i2c_sda):
        self.bus = I2C(i2c_port,scl=Pin(i2c_scl),sda=Pin(i2c_sda))

    def set_time(self,new_time):
        hour = new_time[0] + new_time[1]
        minute = new_time[3] + new_time[4]
        second = new_time[6] + new_time[7]
        week = "0" + str(self.w.index(new_time.split(",",2)[1])+1)
        year = new_time.split(",",2)[2][2] + new_time.split(",",2)[2][3]
        month = new_time.split(",",2)[2][5] + new_time.split(",",2)[2][6]
        day = new_time.split(",",2)[2][8] + new_time.split(",",2)[2][9]
        now_time = binascii.unhexlify((second + " " + minute + " " + hour + " " + week + " " + day + " " + month + " " + year).replace(' ',''))
        self.bus.writeto_mem(int(self.address),int(self.start_reg),now_time)
    
    def read_time(self):
        t = self.bus.readfrom_mem(int(self.address),int(self.start_reg),7)
        a = t[0]&0x7F  #second
        b = t[1]&0x7F  #minute
        c = t[2]&0x3F  #hour
        d = t[3]&0x07  #week
        e = t[4]&0x3F  #day
        f = t[5]&0x1F  #month
        #print("%02x:%02x:%02x,%s,20%x-%02x-%02x" %(t[2],t[1],t[0],self.w[t[3]-1],t[6],t[5],t[4]))
        return("%02x:%02x:%02x,%s,20%x-%02x-%02x" %(t[2],t[1],t[0],self.w[t[3]-1],t[6],t[5],t[4]))
    
    def set_alarm_time(self,alarm_time):
        print("set_alarm_time Start")
        #    init the alarm pin
        self.alarm_pin = Pin(ALARM_PIN,Pin.IN,Pin.PULL_UP)
        #print("alarm_pin=" + str(ALARM_PIN))
        
        #    set alarm irq
        self.alarm_pin.irq(lambda pin: print("alarm1 time is up"), Pin.IRQ_FALLING)
        #    enable the alarm1 reg
        self.bus.writeto_mem(int(self.address),int(self.control_reg),b'\x05')

        #    convert to the BCD format
        print("alarm_time="+ str(alarm_time))
        hour = alarm_time[0] + alarm_time[1]
        minute = alarm_time[3] + alarm_time[4]
        second = alarm_time[6] + alarm_time[7]
        date = alarm_time.split(",",2)[2][8] + alarm_time.split(",",2)[2][9]
        #### Testing ####
        now_time= second + " " + minute + " " + hour +  " " + date
        now_time=now_time.replace(' ','')
        print("SSMMHHDD")
        print("S=Second, M=Minute, H=Hour, D=Day of Month")
        print("now_time=" + now_time)
        #### End ####
        now_time = binascii.unhexlify((second + " " + minute + " " + hour +  " " + date).replace(' ',''))
        #    write alarm time to alarm1 reg
        self.bus.writeto_mem(int(self.address),int(self.alarm1_reg),now_time)
        
def formattedDate(inputTuple):
    dateString=zfl(str(inputTuple[0]),2) + "-" + zfl(str(inputTuple[1]),2) + "-" + zfl(str(inputTuple[2]),2)
    return dateString

# Zero pad the "imputString" with zeroes up to "width"
def zfl(inputString, width):
    padded = '{:0>{w}}'.format(inputString, w=width)
    return padded

def formattedTime(inputTuple):
    timeString=zfl(str(inputTuple[3]),2) + ":" + zfl(str(inputTuple[4]),2) + ":" + zfl(str(inputTuple[5]),2)
    return timeString

def dayOfWeek(inputTuple):
    w  = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]
    #Time Tuple Format (year, month, mday, hour, minute, second, weekday, yearday)
    dayNumber=inputTuple[6]
    dayName=w[dayNumber]
    return dayName

def setPicoTime():
    # Set the Pico Internal Clock from the DS3231 Clock
    print("setPicoTime()")
    print("Pico Time Before" + str(time.localtime()))
    pico_rtc = RTC()
    ds3231DateTime = DS_rtc.read_time()
    DateTimeList=ds3231DateTime.split(",")
    DateList=DateTimeList[2].split("-")
    TimeList=DateTimeList[0].split(":")
    year=int(DateList[0])
    month=int(DateList[1])
    day=int(DateList[2])
    weekday=DateTimeList[1]
    hour=int(TimeList[0])
    minute=int(TimeList[1])
    second=int(TimeList[2])
    pico_rtc.datetime((year,month,day,weekday,hour,minute,second,0))
    print("Pico Time After" + str(time.localtime()))
    print("End setPicoTime()")
    
def setDS3231Time():
    # Set DS3231 Date Time From the Pico Internal Clock
    # Format Required Date Time Values
    print("setDS3231Time()")
    dateTimeTuple = time.localtime()
    formattedDateString = formattedDate(dateTimeTuple)
    formattedTimeString = formattedTime(dateTimeTuple)
    dayName = dayOfWeek(dateTimeTuple)
    picoDateTimeString=formattedTimeString+','+ dayName + ','+ formattedDateString
    print("pico Time Before=" + picoDateTimeString)
    
    # Set DS3231 From Pico Clock
    #DS_rtc = ds3231(I2C_PORT,I2C_SCL,I2C_SDA)
    print("Rs2321 Time Before=" + DS_rtc.read_time())
    DS_rtc.set_time(picoDateTimeString)
    print("Rs2321 Time After=" + DS_rtc.read_time())
    print("End setDS3231Time()")

if __name__ == '__main__':
    DS_rtc = ds3231(I2C_PORT,I2C_SCL,I2C_SDA)
    setDS3231Time()    # Set DS3231 Time from Pico Time
    print()
    #setPicoTime()      # Set Pico Time from DS2321 Time
    
    # Test Alarm <TODO>
    # Set alarm and put the pico to sleep
    # Allow the alarm to wake the pico up.
    # Test alarm 2 also.
    
    # setAlarm(hours,minutes,seconds,days)
    DS_rtc.set_alarm_time('20:15:00,Saturday,2024-03-30')
    # DS_rtc.set_alarm_time('13:45:55,Monday,2021-05-24')



Thanks
David

Hi All
Now I don’t profess to know anything about Pico, Python, Micropython or how you implement a real time clock but I do peruse some of these problems with a bit of interest.

But i am a bit puzzled. If the pico is shut down how does it then restart itself if it is not running.

If the pico is merely put into sleep or some sort of hibernation mode and still ticks over quietly in the background why do you have to set the Pico clock at start up. That way you actually have 2 clocks running. I was of the understanding that is what RTCs are for, provide an accurate battery backed up time all the time. It might be able to provide some alarms but if the Pico is not running there is nothing running to take any action.

Don’t anyone get into too much of a fluster with my query. It is just my curiosity. If I needed to do this I would probably ask some more pertinent questions and do some heavy research but I was just interested at the moment.

My approach would probably be to provide enough battery to keep things ticking over quietly and maybe use the alarms as interrupts to fire up some action somewhere.
Cheers Bob

Hi Bob,
Good question. The pico does have an internal clock but there is no battery backup. I think the reason is price.
So if it is restarted the time needs to be reset every time. I have a DS3231 connected and when the pico starts up it reads in a good time from it. The DS3231 has a battery that lasts for years. There are various sleep modes in the pico and I think it’s called deepsleep that uses the least power. Other modes can be set to set to sleep for a length of time. In Deepsleep basically everything is off except it’s waiting for + voltage signal to appear on a specified pin. This is not to power the device just a signal to wake up. My understanding is the device then boots from scratch. This signal can be from many sources. I have done this with a push button to wake a device from deep sleep. My goal (in this forum post) is to wake up the pico with an alarm signal from the DS3231 twice a day fun for a couple of hours then go back to sleep.
You may wonder why I want to do that. I have two pico’s one is a hand held device and the other is mounted inside the enclosure of a community BBQ.
Once every couple of days a volunteer drives past 200m away and pushes his button on the hand held device. It wakes up his battery powered device and sends a radio message to the BBQ device. If the BBQ device is awake it sends the bbq’s starter battery voltage back to the hand held device to be displayed. If I run the BBQ pico device 24x7 listening for requests it flattens a 12v 12AH battery in about a week (11V). I think the volunteers normally pass by in a 2 hour window in the morning or evening so I only need listen for incoming voltage requests 4x7.
The hand held device can easily run for several weeks on a 3.3v 600mAh battery if you check the voltage twice a day but it’s only awake for about 20 seconds. It’s also a snap to charge from usb. The BBQ battery needs to be removed and recharged elsewhere.
The small battery is smaller than your little finger. The BBQ is like a house brick. :cowboy_hat_face:. It’s also used to light the gas.

That’s what the code says to print.
date = alarm_time.split(",",2)[2][8] + alarm_time.split(",",2)[2][9]
date is the day of the month. For setting the alarm you can use either the day of the month or the day of the week, according to how you set the register value. But AFAICT the debug print you are getting is exactly what you are providing. The next step would be to write a read_alarm_time() function to confirm that the alarm time had been set correctly.

Hi Bob

As David points out the advantage of the DS3231 RTC is the battery lasts for ages. I have been using the pico RTC standalone and have found it runs fine with a 20W solar panel and SLA battery. The following code snippet shows how easy it is. In the example there is a loop which actions every minute exactly on the minute. The time is set to 7:58am to show how it behaves when the 8:00am alarm time comes around. In the real world when the pico boots it will enable wifi and then set the RTC.

import machine
from time import sleep

rtc = machine.RTC()
rtc.datetime((2024, 3, 31, 6, 7, 58, 0, 0)) # set time to 7:58am to test alarm
print('Wait for next minute')

while True:
    rd = rtc.datetime()
    delay = 60 - rd[6]
    sleep(delay) # wait for new minute
    rd = rtc.datetime()
    if rd[4] == 8 and rd[5] == 0: # hours = 8, mins = 0
        print('beep beep it is 8.00am') # your code goes here
    elif rd[4] == 16 and rd[5] == 0: # hours = 16, mins = 0
        print('beep beep it is 4.00pm') # your code goes here
    else:
        print(rtc.datetime(), ' no alarm')

I really like this solution because I am not a proficient coder and this is so simple.

The current Micropython sleep implementation on the pico only provides one method - machine.deepsleep. Provided your script is named main.py the pico simply restarts when it comes out of sleep. With machine.deepsleep the current draw is 25.6mA. With time.sleep the current draw is 25.4mA. So my conclusion is there is no particular advantage using machine.deepsleep and it adds to the complexity.

The needed but not implemented Micropython sleep method is the one which stops all the clocks just leaving the RTC running. This allows the date/time for the wake up to be set. The library is available on github and it is reported the current draw is 1.4mA. This method is already available in Circuitpython and the reported current draw is 7.6mA.

I have an interest in lowering the pico current draw for solar power so it has been interesting reading your comments. Also have an interest in tricks to allow a remote pico to run indefinitely.

Hi Jeff,
Yes I know “date” is what the code says to do. That’s not the odd part.
I’m trying to figure out what the DS3231 will do with it the day of the month. I can see there is some other byte to be set to define if this value is a day of the week or a day of the month.
Currently I can only think it will trigger the alarm only once on that day every month. The example code seems to discard other values passed in like the month and the year.
I’m trying to figure out how to use these features like trigger the alarm every day at a specified time or for example every hour when the minute 15 is reached.
All the possible ranges are specified on page 11 of this document.

This explains all the possible ranges but it leaves a lot of questions about how to use them correctly.
Here is my attempt to read the alarm time back again from the DS3231 as you suggested.

    def read_alarm_time(self):
        # Based on read_time method
        # t = self.bus.readfrom_mem(int(self.address),int(self.start_reg),7)
        # and the the set_alarm_time method
        # self.bus.writeto_mem(int(self.address),int(self.alarm1_reg),now_time)

        print("read_alarm_time Start")
        BCD_alarm_time = self.bus.readfrom_mem(int(self.address),int(self.alarm1_reg),7)
        print("BCD_alarm_time=" + str(BCD_alarm_time))
        t = binascii.hexlify(BCD_alarm_time)
        print("hexlify alarm_time="+str(t))
        print("read_alarm_time Finish")
        return("%02x:%02x:%02x,%s,20%x-%02x-%02x" %(t[2],t[1],t[0],self.w[t[3]-1],t[6],t[5],t[4]))

It kind of works but there are six more 0’s on the string.
Extract
now_time=56341231
hexlify alarm_time=b’56341231000000’
Im having trouble understanding all the conversions going on.

Here is all the output

setDS3231Time()
pico Time Before=22:26:33,Sunday,2024-03-31
Rs2321 Time Before=22:26:33,Sunday,2024-03-31
Rs2321 Time After=22:26:33,Sunday,2024-03-31
End setDS3231Time()

set_alarm_time Start
Passed In alarm_time=12:34:56,Saturday,2024-12-31
SSMMHHDD
S=Second, M=Minute, H=Hour, D=Day of Month
now_time=56341231
unhexlify now_time=b'V4\x121'
set_alarm_time Finish
read_alarm_time Start
BCD_alarm_time=b'V4\x121\x00\x00\x00'
hexlify alarm_time=b'56341231000000'
read_alarm_time Finish
Traceback (most recent call last):
  File "<stdin>", line 173, in <module>
  File "<stdin>", line 99, in read_alarm_time
IndexError: list index out of range

The index out of range error comes from the “return” line

I just went back to read the whole post and correct me if I am wrong but is the purpose of the exercise to measure a battery voltage? Bluetooth Low Energy (BLE) has been available on the Pico W for a while. I have tried it and it seems quite reliable with the only caveat that I am not entirely comfortable copying huge swathes of code. This seems like a neat solution however.

The Pico W would act as a Bluetooth LE peripheral in broadcast mode. This has the advantage that no pairing is necessary and is feasable since the data does not need to be secure.

So all the user needs to do is have the (free) LightBlue app installed on their phone to show the measurement. They need to be within distance and my testing showed that the BLE range is about the same as WiFi.

In the BLE example they broadcast the Pico chip temperature. The code could easily be modified to send a battery voltage.

The Pico W could be turned on by the RTC as above although I will confess I do not really understand how everything works e.g. with no WiFi how is the RTC date/time set? How does pressing a button get a wake up signal to the Pico W?

Thoughts on BLE as a solution anyone? Be gentle!

The options are the eleven that you refer to in that list. They do not include any alarm setting for a month number or the year. The option selected there will determine whether it is the day of the week or the day of the month (5/6, 10/11). The appropriate value (date or day) must be available for the corresponding alarm to work. It is not spelt out in detail, but it appears that day (of the week) should be initialized when the date is set, or it can be done separately.

The error in the print statement will be a mismatch between the format string and the argument list. I’m not sure what you are trying to do here as you have the information read from the registers in the line before. The trailing zeroes are probably the result of passing an incorrect length. Note that when reading the alarm registers you need to insert code to strip out the dy/dt setting from bit 6 of the fourth register read, and if you want to get the alarm type you will need to get bit 7 of all four registers and put them back together into a nibble that means something.

1 Like

I have removed the print statement with the error. You are right I didn’t need that. I also trimmed off the trailing zeroes.
I can read back the alarm time and print it the same format as I stored it so that seems good.

I dug out some previous code I came up with to put the pico into dormant mode using this library and a push button basically to prove I can put it to sleep and wake it up again “manually”.

I execute this line to put the pico into power save mode.

lowpower.dormant_until_pin(DORMANT_PIN)   # Set dormant mode

When I connect DORMANT_PIN (GP1) to Gnd with a push button switch the pico wakes up. This works no problem.

With my alarm I’m expecting the DS3221 to send a similar signal on pin ALARM_PIN (GP3)
command to enter power save mode

lowpower.dormant_until_pin(ALARM_PIN)

From the DS3231 example code

self.alarm_pin = Pin(ALARM_PIN,Pin.IN,Pin.PULL_UP)

The dormant definition accepts parameters like this

def dormant_until_pin(gpio_pin, edge=True, high=True):

Do these PULL_UP and high=True edge=True appear to be around the right way for the alarm event to be detected?
I’m running out of ideas now as the result is simply “Nothing Happens”
Once I get it to trigger once I can figure out the repeating alarm stuff.
Thanks
David

1 Like

If you want to use the existing code then interrupts are already enabled and working for a match on the full time and date. This means that to test the alarm you would need to set the date and time to a minute or two after the current date and time. You can do a read time, add one or to to the minutes and write it back. That might fail no more often than once an hour. When the alarm goes off the program will print “alarm1 time is up”. As this is done with a lambda expression doing something different will be difficult.

A simpler example would be mode 0 of alarm 2 - once per minute at zero seconds. This avoids any need to set dy/dt, and only requires A2M4/3/2 to be all 1 by setting the high bit of the first three bytes of the write string. To enable alarm 2 change the control register write from b’\x05 to b’\x06. INTCN default is interrupt mode so no change is required there. That should give you an interrupt once per minute. Note that the interrupt pin is open drain so a pullup resistor is required - this is listed as included with the board. It is active low, but I can’t see any information on how long it stays low. If it is as long as an alarm match occurs then it would be 1s for the once-per-minute mode and that is long enough to see it on a multimeter. Sleep and wake is far too complex at this point in the development so get rid of the existing irq setup and your dormant code. It is much simpler to use any available GPIO with a callback to detect the interrupt. There is an example here for an active-low interrupt.

1 Like

Hi Jeff,
Thanks for your efforts but its two complicated now and I give up.
I have been trying to read the settings values out of memory and not making much sense of it.
I may post another question so the title matches the problem and reference this thread as “Get and Set Time” is solved.
I found another post from more than 10 years ago on the DS3231 (obviously not integrated with the Pico) and they were going though the same crap trying to get he alarm to work.
I’m an old dog and this is looking a lot like a new trick. :slight_smile:

Hi David
Full marks for trying.

Maybe not such a bad idea after all. Have a supply big enough to run the Pico for say a week then recharge or if time does not allow have a second battery to swap out while charging somewhere else.
Cheers Bob

The battery I’m testing with at the moment is 12 amp hours.
This is a little smaller than a house brick and similar to the one on the BBQ. This will only run my pico for about a week (24x7). If the voltage drops much below 11.5v there is a risk of damaging the battery. The BBQ enclosure does not allow cabling of a small solar panel. The load is mostly from the LoRa module listening for incoming requests.
The battery can power the BBQ starter on its own for over six months or more of normal use unless someone jams the start button in. That will obviously flatten the battery fairly quickly which is what I’m trying to detect. The problem is someone has to walk over to the BBQ open the enclosure to check the battery daily with a DMM. My idea is to remotely monitor the battery when a volunteer drives by on the way to the pub. But I definitely don’t want to make the problem worse.
The time set problem part is solved by the DS3231 module. My other option is to use lightsleep which will still save significant amount of power and the pico can wake itself up from that. I have a device meant to measure power usage on these devices so it will be interesting to compare. I might compare various sleep modes and post the results here. :cowboy_hat_face:
.

Hi David

That result would interest a good many people. Good idea.
Cheers bob