Looking into How the Century is Stored in the DS3231
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,
| Address | MSB LSB | Function | Range |
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 05h | Century | 0 | 0 | 10 Month | Month | Month Century | 01-12 0-1 |
| 06h | 10 Year | Year | Year | 00-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 way 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.