md
L'indicateur du siècle et le registre du jour de la semaine du DS3231
2020-02-20
<-Module horloge et mémoire EEPROM I2C pour Raspberry Pi --

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,

AddressMSB LSBFunctionRange
76543210
05hCentury0010 MonthMonthMonth
Century
01-12
0-1
06h10 YearYearYear00-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.

<-Module horloge et mémoire EEPROM I2C pour Raspberry Pi --