L'année est stockée sous la forme d'une valeur BCD à deux chiffres (0 - 99) dans la puce d'horloge en temps réel DS3231. De plus, il y a un indicateur d'un bit de siècle dans le registre du mois. À part de la figure montrant les registres de la puce,
| 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 |
la seule référence à cet indicateur dans la documentation du fabricant est la phrase suivante.
L'indicateur du siècle (le bit 7 du registre du mois) est basculé quand il y a un débordement du registre des année de 99 à 00
(Texte orignal: 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.
À mon avis, cela signifie que le bit siècle n'identifie pas un siècle. Cependant, en regardant beaucoup de code sur le Web, il semble que certains l'interprètent comme suit (YEAR est le contenu du registre 6).
calendar year = (19 + Century)*100 + YEAR
Soit, en pseudo-code,
if (Century == 1) then
calendar year = 2000 + YEAR
else
calendar year = 1900 + YEAR
Ce qui est raisonnable, mais pourquoi ne pas laisser Century == 0 signifié 2000 et Century == 0 signifié 2100 ? C'est plus axé vers l'avenir et il a une meilleure symétrie. Au moins une bibliothèque MD_DS3231 de Marco Colli
(MajicDesigns) permet à l'utilisateur de décider de la signification de l'indicateur. J'ai bricolé un script Python, appelé rtc pouvant lire ou modifier les registres du DS3231, qui implémente l'idée de siècles dynamiques ou d'un siècle fixe. Le script s'exécute dans un environnement virtuel appelé rtcpy dans lequel le module smbus a été installé avec pip. Le Raspberry Pi sur lequel le script est exécuté possède la dernière version de Raspbian Buster qui a été mise à jour et mise à niveau en février 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
Après vérification qu'aucun module I2C ou RTC n'est chargé, le seul module i2c est chargé. Ainsi, en ce qui concerne le système, il n'y a pas d'horloge matérielle présente, ce qui signifie que le service de synchronisation horaire de Linux n'essaiera pas de mettre à jour l'horloge.
(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
La première commande ci-dessous définit la date au 30 avril 2523. La valeur dynamique du siècle est définie sur 2400 (-c 24), de sorte que l'indicateur de siècle sera basculé dans les registres DS3231. Le siècle n'est pas enregistré dans la puce, donc la lecture de la date sans spécifier de siècle ne sera pas correcte comme on peut le voir dans la deuxième commande. Le siècle par défaut a été utilisé et il est de 20 (pour 2000). Puisque l'indicateur de siècle a été basculé, le siècle a été augmenté jusqu'à 21 (pour 2100), d'où l'année 2123. Dans la troisième commande, l'erreur est corrigée en spécifiant le siècle correct -c 24. Bien sûr, si elle avait été fixée à -c 25, la date aurait été décalée de 100 ans, comme le montre la quatrième commande. Dans les trois dernières commandes, l'option -f est définie, ce qui signifie que l'indicateur du siècle sera ignoré. Lorsque la seule option est -f, le siècle par défaut, 20 pour 20000, est utilisé. Et, bien sûr, l'année est décalée de cinq siècles! La commande suivante avec l'option -fet le siècle défini sur 24 donnera la mauvaise année car l'indicateur de siècle est ignoré alors qu'il était défini pour signaler le basculement de 2499 à 2500. Seule la dernière commande est correcte lorsque la bonne valeur du siècle est fixé.
(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
Vérifions que l'horloge DS3231 utilise l'indicateur de siècle pour indiquer un roulement à minuit le dernier jour du siècle.
(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
Espérons que cela montre clairement comment le DS3231 ignore béatement les siècles (sauf vers minuit le dernier jour de xx99). Il se passe quelque chose de similaire avec le registre du jour de la semaine.
Le registre du jour de la semaine augmente à minuit. Les valeurs qui correspondent au jour de la semaine sont définies par l'utilisateur mais doivent être séquentielles (c'est-à-dire si 1 est égal à dimanche, alors 2 est égal à lundi, etc.). (Texte original 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.
En modifiant les données du DS3232 dans un système Linux, il est devenu clair que ce dernier utilise la plage suggérée dans la documentation: le numéro du jour de la semaine est compris entre 1 et 7 avec dimanche égal à 1. Malheureusement, Python utilise un convention différente, soit une plage de 0 à 6 avec lundi égal à 0.
Caveat: Cette convention au sujet de la numérotation des jours de la semaine pourrait être un artefact du pilote de la puce DS3231 et peut-être que d'autres modules d'horloge utilisent une autre convention. D'ailleur un autre service de Linux, cron utilise une convention différente avec dimanche égal à 0 (et 7) et samedi égal à 6.
Voici le 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'])
(Lien pour télécharger le script : rtc.py.)
Deuxième avertissement : comme indiqué ci-dessus, j'ai créé ce script sans trop y réfléchir dans l'unique but de voir comment le DS3231 pourrait être programmé. Il n'y a pas de vérification d'erreur et comme on le verra ci-dessous, il peut définir des heures que le module du noyau ne peut pas gérer. Utilisez-le avec précaution.
Que faitLinux&nbps;? Le système d'exploitation marche à son propre rythme selon l'horloge système qui peut être lue ou réglée avec date ou timedatectl. L'utilitaire hwclock peut lire ou régler l'horloge matérielle, indépendamment de l'horloge système. Il peut également synchroniser les deux horloges. Pour commencer l'expérimentation, le module i2c a été retiré pour être remplacé par les modules I2C et RTC. J'ai ensuite mis à jour l'horloge matérielle selon l'heure actuelle du système. Enfin, j'ai désactivé les mises à jour NTP, sinon le système changera à la fois l'horloge système et l'horloge matérielle alors que nous essayons de jouer avec l'heure.
(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
Notez que date ne définit que l'heure système, tandis que, timedatectl définit l'heure système et l'horloge matérielle si 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
Les heures valides sont limitées sous Linux par la façon de stocker l'heure. L'horloge système est un entier de 32 bits qui contient le nombre de secondes depuis la soi-disant époque qui est 1970-01-01 00:00:00 UTC. L'entier débordera à 03:14:07 le mardi 19 janvier 2038 UTC.
(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’
Sans surprise, des résultats semblables sont obtenus avec 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
En réalité, le DS3231 n'a pas de contraintes de ce genre.
(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
Mais pour hwclock les contraintes imposées par Linux s'appliquent.
(rtcpy) woopi@goldserver:~/rtcpy $ sudo hwclock
hwclock: RTC read returned an invalid value.
Cela a du sens puisque l'utilitaire peut être utilisé pour régler l'heure du système à partir de l'horloge matérielle et vice versa.
Il n'y aura pas de problème Y2038 pour le DS3231. En effet, cette puce pourrait théoriquement fournir un temps précis pendant des siècles jusqu'à ce que le temps soit mesuré selon le système "stardate". En revanche, Linux devra faire quelque chose à moins que les systèmes 32 bits n'existent plus en 2038.