md
The DS3231 Century Flag and Day of Week Register
2020-02-20
<-I2C clock and EEPROM Memory Module for Raspberry Pi --

The year is stored as a two-digit BCD value (0 - 99) in the DS3231 real-time clock chip. Additionally there is a century flag or bit in the month register. Beside the figure showing the timekeeping registers,

AddressMSB LSBFunctionRange
76543210
05hCentury0010 MonthMonthMonth
Century
01-12
0-1
06h10 YearYearYear00-99

the only reference to the century bit in the manufacturer documentation is the following sentence.

The century bit (bit 7 of the month register) is toggled when the years register overflows from 99 to 00.
Maxim Integrated Products, Inc (2015) DS3231 Extremely Accurate I 2 C-Integrated RTC/TCXO/Crystal, p. 12.

To me that means that the century bit does not identify a century. Looking at a lot of code on the Web, it seems some interpret it as follows (YEAR is the content of register 6).

  calendar year = (19 + Century)*100 + YEAR

Or if you prefer pseudo-code:

  if (Century == 1) then
    calendar year = 2000 + YEAR
  else
    calendar year = 1900 + YEAR

Which is reasonable, but why not let Century == 0 mean 2000 and Century == 1 mean 2100? That's more forward looking and it has a rather more pleasing symmetry. At least one library MD_DS3231 by Marco Colli (MajicDesigns) lets the user decide what the flag means. I cobbled together a Python script, called rtc, that can set or read the DS3231 time registers which implements the idea of dynamic centuries or a fixed century. The script runs in a virtual environment called rtcpy in which the smbus module was installed with pip. The Raspberry Pi on which the script is run has the latest version of Raspbian Buster which was updated and upgraded in February 2020.

woopi@goldserver:~ $ ve rtcpy (rtcpy) woopi@goldserver:~ $ cd rtcpy (rtcpy) woopi@goldserver:~/rtcpy$ pip install smbus Collecting smbus ... Successfully installed smbus-1.1.post2 (rtcpy) woopi@goldserver:~/rtcpy $ pip freeze pkg-resources==0.0.0 smbus==1.1.post2

After making sure that no I2C and RTC kernel modules are not loaded, the i2c1 module only is loaded. Thus as far as the system is concerned, there is no real time clock present, which means the Linux time synchronization daemon will not be trying to update the RTC.

(rtcpy) woopi@goldserver:~/rtcpy $ ls /dev/i2c* /dev/rtc* ls: cannot access '/dev/i2c*': No such file or directory ls: cannot access '/dev/rtc*': No such file or directory (rtcpy) woopi@goldserver:~/rtcpy $ sudo dtoverlay i2c1 (rtcpy) woopi@goldserver:~/rtcpy $ ls /dev/i2c* /dev/rtc* ls: cannot access '/dev/rtc*': No such file or directory /dev/i2c-1

The first command below sets the date to April 30, 2523. The century value is set to 2400 (-c 24) so the century bit will be set in the DS3231 registers. The century is not saved in the chip, so reading back the date without specifying a century, will not be correct as can be seen in the second command. The default century was used and it is 20 (for 2000). Since the century bit was set, the century was bumped up to 21 (for 2100), hence the year 2123. In the third command, the error is fixed by specifying the correct century -c 24. Of course if -c 25 had been set, then the date would have been off by 100 years as seen in the fourth command. In the last three command, the -f option is set which means that the century flag will be ignored. When only the -f option is given, the default century, 20 for 20000, is used. And, of course, the year is off by five centuries! The following command with the -f option and the century set to 24 will give the wrong year because the century flag is ignored while it was set to signal the rollover from 2499 to 2500. Only the last command is correct when the -f option is accompanied with the correct base century.

(rtcpy) woopi@goldserver:~/rtcpy $ ./rtc -c 24 -s '2523-04-30 13:21:08' (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc Mon Apr 30 13:21:45 2123 (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc -c 24 Mon Apr 30 13:21:16 2523 (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc -c 25 Mon Apr 30 13:21:38 2623 (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc -f Sun Apr 30 13:21:50 2023 (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc -f -c 24 Mon Apr 30 13:21:57 2423 (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc -f -c 25 Mon Apr 30 13:22:08 2523

Let's verify that the RTC does use the century bit to indicate a rollover at midnight on the last day of the century.

(rtcpy) woopi@goldserver:~/rtcpy $ ./rtc -c 24 -s '2423-12-31 23:58:50' (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc Sun Dec 31 23:58:55 2023 ... after a couple of minutes: (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc Mon Jan 1 00:01:03 2024

Hopefully, this makes it clear how the DS3231 blissfully ignores centuries (except around midnight on the last day of xx99). There is something similar going on with the day of week register.

The day-of-week register increments at midnight. Values that correspond to the day of week are user-defined but must be sequential (i.e., if 1 equals Sunday, then 2 equals Monday, and so on).
Maxim Integrated Products, Inc (2015) DS3231 Extremely Accurate I 2 C-Integrated RTC/TCXO/Crystal, p. 12.

By changing the data of the DS3232 in a Linux system it became clear that the latter uses the range suggested in the documentation: the day of the week number is in the range of 1 to 7 with Sunday equal to 1. Unfortunately, Python uses a different convention: range of 0 to 6 with Monday equal to 0.

Caveat: This supposed Linux day of week numbering convention could actually be an artifact of the DS3231 kernel module and perhaps other clock modules use a different convention. The cron convention is different with Sunday equal to 0 (and 7) and Saturday equal to 6.

Here is the script.

#!/usr/bin/env python ''' Python 3 script to get or set the DS3231 RTC date and time References # https://github.com/switchdoclabs/RTC_SDL_DS3231/blob/master/SDL_DS3231.py # http://wiki.erazor-zone.de/wiki:linux:python:smbus:doc?do=show # https://github.com/NorthernWidget/DS3231/blob/master/DS3231.cpp # https://github.com/sleemanj/DS3231_Simple/blob/master/DS3231_Simple.h # https://github.com/ayoy/upython-aq-monitor/blob/master/lib/ds3231.py # https://github.com/adafruit/Adafruit_CircuitPython_Register/blob/master/adafruit_register/i2c_bcd_datetime.py # https://github.com/kriswiner/DS3231RTC/blob/master/DS3231RTCBasicExample.ino # https://github.com/MajicDesigns/MD_DS3231 # http://www.intellamech.com/RaspberryPi-projects/rpi_RTCds3231 ''' # default I2C bus and RTC address # I2C_BUS = 1 RTC_ADDR = 0x68 # default century # FIXED_CENTURY = 0 # if FIXED_CENTURY = 1 then calendar year = YEAR + BASE_CENTURY*100 BASE_CENTURY = 20 # if FIXED_CENTURY = 0 then calendar year = YEAR + (BASE_CENTURY + Century_bit)*100 # ========================================================= import smbus import time import calendar import os.path from subprocess import check_call import argparse CURRENT_TIME_SECONDS = 0 CURRENT_TIME_MINUTES = 1 CURRENT_TIME_HOUR = 2 CURRENT_TIME_DAY = 3 CURRENT_TIME_DATE = 4 CURRENT_TIME_MONTH = 5 CURRENT_TIME_YEAR = 6 verbose = False utcTime = False i2cBus = I2C_BUS rtcAddr = RTC_ADDR fixedCentury = FIXED_CENTURY baseCentury = BASE_CENTURY # Unusual was to convert from bcd to int and back # from SDL_DS3231.py by SwitchDoc Labs 12/19/2014 # https://github.com/switchdoclabs/RTC_SDL_DS3231 def bcdToInt(abyte, n=2): return int(('%x' % abyte)[-n:]) def intToBcd(abyte, n=2): return int(str(abyte)[-n:], 0x10) def getCurrentTime(bus, addr): data = bus.read_i2c_block_data(addr, 0x00, 7) second = bcdToInt(data[CURRENT_TIME_SECONDS]) minute = bcdToInt(data[CURRENT_TIME_MINUTES]) if data[CURRENT_TIME_HOUR] & 0x40 == 0x40: hour = bcdToInt(data[CURRENT_TIME_HOUR] & 0x1f) if data[CURRENT_TIME_HOUR] & 0x20 == 0x20: hour += 12 else: hour = bcdToInt(data[CURRENT_TIME_HOUR] & 0x3F) day = bcdToInt(data[CURRENT_TIME_DAY] & 0x07) - 2 if day < 0: day = 6 date = bcdToInt(data[CURRENT_TIME_DATE] & 0x3f) month = bcdToInt(data[CURRENT_TIME_MONTH] & 0x1f ) year = bcdToInt(data[CURRENT_TIME_YEAR]) + baseCentury*100 if not fixedCentury and data[CURRENT_TIME_MONTH] & 0x80 == 0x80: year += 100 ts = time.struct_time((year, month, date, hour, minute, second, day, 1, -1)) # 1 is probably the wrong day of year (tm_yday) while # -1 means the time zone is unknown # Cheat: convert ts to epoch seconds with time.mktime and then convert # seconds back to a time structure and tm_yday will be corrected if verbose: print(ts) try: if utcTime: # no conversion required so just pass on the time structure nts = ts # time.localtime(time.mktime(ts)) # cheat to fix day of year else: nts = time.localtime(calendar.timegm(ts)) if verbose: print(nts) return(nts) except (OverflowError, ValueError): # this can occur if mktimes and in caledar.timegm ?? if verbose: print("Time overflow error, day of year not calculated") return(ts) def setCurrentTime(bus, addr, utc_s): # setting DS3231 with UTC time if not utcTime: time_s = time.gmtime(time.mktime(utc_s)) utc_s = time_s buf = bytearray(CURRENT_TIME_YEAR+1) buf[CURRENT_TIME_SECONDS] = intToBcd(utc_s.tm_sec) & 0x7f buf[CURRENT_TIME_MINUTES] = intToBcd(utc_s.tm_min) buf[CURRENT_TIME_HOUR] = intToBcd(utc_s.tm_hour) wday = utc_s.tm_wday + 2 if wday > 7: wday = 1 buf[CURRENT_TIME_DAY] = intToBcd(wday) buf[CURRENT_TIME_DATE] = intToBcd(utc_s.tm_mday) century = 0 if fixedCentury: if verbose: print("Fixed century, century flag unchanged") else: if utc_s.tm_year >= (baseCentury+1)*100: century = 0x80 if verbose: print("Century flag set") else: if verbose: print("Century flag not set") buf[CURRENT_TIME_MONTH] = intToBcd(utc_s.tm_mon) | century buf[CURRENT_TIME_YEAR] = intToBcd(utc_s.tm_year % 100) bus.write_i2c_block_data(addr, 0x00, list(buf)) def auto_int(x): return int(x, 0) parser = argparse.ArgumentParser() parser.add_argument("-s", "--set", help="set date (ex. -s '2022-02-20 15:45:12' - quotes necessary") parser.add_argument("-f", "--fixed", help="If specified then year has a one century range otherwise two centuries", action="store_true") parser.add_argument("-c", "--century", type=int, choices=range(17, 50), metavar="17 - 50", help="Start of century (default 20 for 2000)") parser.add_argument("-u", "--utc", help="Use UTC time, else the local time will be used", action="store_true") parser.add_argument("-i", "--i2c", type=int, help="I2C bus (1 default)") parser.add_argument("-a", "--addr", type=auto_int, help="RTC address (0x68 default)") parser.add_argument("-v", "--verbose", action="store_true") args = parser.parse_args() if not args.i2c is None: i2cBus = args.i2c if args.addr: rtcAddrs = args.addr if args.fixed: fixedCentury = args.fixed if args.century: baseCentury = args.century if args.utc: utcTime = True if args.verbose: print("verbosity turned on") verbose = True if args.set: action = 'Setting' else: action = 'Reading' print("{0} real-time clock at address {1} (0x{1:x}) on I2C bus {2}".format(action, RTC_ADDR, I2C_BUS)) # This is ugly but to work with hwclock etc, the /dev/rtc device must be # in place and it will be taken over by the kernel rtc module, so... # remove the rtc module for a little while rtcexists = os.path.exists('/dev/rtc') if rtcexists: if verbose: print("Disabling RTC module") check_call(['sudo', 'rmmod', 'rtc_ds1307']) if verbose: print("Using i2c-{}".format(i2cBus)) bus = smbus.SMBus(i2cBus) if args.set: time_s = time.strptime(args.set, "%Y-%m-%d %H:%M:%S") setCurrentTime(bus, rtcAddr, time_s) else: time_s = getCurrentTime(bus, rtcAddr) if verbose: print(time_s) print(time.asctime(time_s)) print() # Reload the rtc module if it was removed if rtcexists: if verbose: print("Enabling RTC module") check_call(['sudo', 'modprobe', 'rtc_ds1307'])

(Download the script here: rtc.py.)

Second Warning: As stated above, I slapped this script together without much thought just to see how the DS3231 could be programmed. There is no error checking and as will be seen below, it can set times that the kernel module cannot handle. Use with care.

What does Linux do? The operating system marches to its own beat, the system clock which can be read or set with date or timedatectl. The utility hwclock can be used to read or set the hardware clock, independently of system clock. It can also synchronize both clocks. To star experimenting, the i2c1 module was removed to be replace by the I2C and RTC modules. Then I updated the RTC to the current system time. Finally, I turned off NTP updates, otherwise the system will be changing both the system clock and the hardware clock as we are trying to play with the time.

(rtcpy) woopi@goldserver:~/rtcpy $ sudo dtoverlay -r 0 (rtcpy) woopi@goldserver:~/rtcpy $ ls /dev/i2c* /dev/rtc* ls: cannot access '/dev/i2c*': No such file or directory ls: cannot access '/dev/rtc*': No such file or directory (rtcpy) woopi@goldserver:~/rtcpy $ sudo dtoverlay i2c-rtc ds3231 (rtcpy) woopi@goldserver:~/rtcpy $ ls /dev/i2c* /dev/rtc* /dev/i2c-1 /dev/rtc /dev/rtc0 (rtcpy) woopi@goldserver:~/rtcpy $ sudo hwclock -w (rtcpy) woopi@goldserver:~/rtcpy $ sudo timedatectl set-ntp false (rtcpy) woopi@goldserver:~/rtcpy $ sudo timedatectl show; sudo hwclock; ./rtc Timezone=America/Moncton LocalRTC=no CanNTP=yes NTP=no NTPSynchronized=yes TimeUSec=Fri 2020-02-21 14:07:56 AST RTCTimeUSec=Fri 2020-02-21 14:07:56 AST 2020-02-21 14:07:56.920560-04:00 Fri Feb 21 14:07:58 2020

Note that date sets the system time only, whereas timedatectl sets the system time and the hardware clock if possible.

(rtcpy) woopi@goldserver:~/rtcpy $ sudo date -s "2002-08-20 12:48:00" Tue 20 Aug 12:48:00 ADT 2002 (rtcpy) woopi@goldserver:~/rtcpy $ sudo hwclock 2020-02-21 22:02:39.407799-04:00 RTC not updated (rtcpy) woopi@goldserver:~/rtcpy $ sudo timedatectl set-time "2002-08-20 12:48:00" (rtcpy) woopi@goldserver:~/rtcpy $ sudo hwclock 2002-08-20 12:48:05.346738-03:00 RTC updated (rtcpy) woopi@goldserver:~/rtcpy $ sudo hwclock --set --date "2008-09-12 08:34:00" (rtcpy) woopi@goldserver:~/rtcpy $ date Tue 20 Aug 13:06:16 ADT 2002 System time not updated (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc Fri Sep 12 05:35:05 2008

Valid times are constrained in Linux by the mechanism used to store time. The system clock is a 32 bit integer which holds the number of seconds since the so-called epoch which is 1970-01-01 00:00:00 UTC. The integer will overflow at 03:14:07 on Tuesday, 19 January 2038 UTC (see https://en.wikipedia.org/wiki/Year_2038_problem.

(rtcpy) woopi@goldserver:~/rtcpy $ sudo date -u -s '1969-12-31 07:19:01' date: cannot set date: Invalid argument Wed 31 Dec 07:19:01 UTC 1969 (rtcpy) woopi@goldserver:~/rtcpy $ sudo date -u -s "1970-01-01 05:38" Thu 1 Jan 05:38:00 UTC 1970 (rtcpy) woopi@goldserver:~/rtcpy $ date Thu 1 Jan 02:38:08 AST 1970 (rtcpy) woopi@goldserver:~/rtcpy $ sudo date -u -s '2038-01-19 02:14:08' Tue 19 Jan 02:14:08 UTC 2038 (rtcpy) woopi@goldserver:~/rtcpy $ sudo date -u -s '2038-01-19 03:14:08' date: invalid date ‘2038-01-19 03:14:08’

Not surprisingly, similar results obtain with timedatectl.

(rtcpy) woopi@goldserver:~/rtcpy $ sudo timedatectl set-time '1970-01-01 01:38' Failed to set time: Failed to set local time: Invalid argument (rtcpy) woopi@goldserver:~/rtcpy $ sudo timedatectl set-time '1970-01-01 02:38' (rtcpy) woopi@goldserver:~/rtcpy $ sudo timedatectl set-time '2038-01-18 22:15:10' (rtcpy) woopi@goldserver:~/rtcpy $ sudo timedatectl set-time '2038-01-18 23:14:10' Failed to parse time specification '2038-01-18 23:14:10': Invalid argument

Of course, the DS3231 does not really care about such limits.

(rtcpy) woopi@goldserver:~/rtcpy $ ./rtc -u -s '2582-01-19 03:14:08' (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc -f -c 25 Sat Jan 19 03:14:18 2582 (rtcpy) woopi@goldserver:~/rtcpy $ ./rtc Sat Jan 19 03:14:37 2182

But hwclock does.

(rtcpy) woopi@goldserver:~/rtcpy $ sudo hwclock hwclock: RTC read returned an invalid value.

That makes sense since the utility can be used to set the system time from the hardware clock and vice-versa.

There will not be a Y2038 problem for the DS3231. Indeed, it could theoretically provide accurate time for centuries until time keeping changes over to the "stardate" system. Linux, on the other hand, will have to do something unless 32 bit systems no longer exist in 2038.

<-I2C clock and EEPROM Memory Module for Raspberry Pi --