Precizie klok met RTC DS3231 (Pico)

Ik kreeg de vraag om een Raspberry Pi Pico van een RTC te voorzien en de actuele tijd op een display te tonen. Met andere woorden: Hoe bouw je een autonome klok die door middel van een RTC de actuele (precizie) tijd op een display weergeeft.

In deze tutorial bouw ik op basis van een Raspberry Pi Pico een klok met vier zeven-segment led displays. Het is de bedoeling dat de klok ergens in huis kan staan, zonder internet nodig te hebben. De actuele tijd wordt vanuit een precisie RTC (Real Time Clock) ingelezen, te weten de DS3231.

Wat bij sommige beginnende hobbyisten even moet doordringen is dat het bij de RTC de DS3231 chip is die op een RTC module verwerkt is en daarmee ieder DS3231 RTC module in de basis aan elkaar gelijk is en op dezelfde wijze aangestuurd wordt. Er is in principe maar één voorwaarde, het I2C adres van de DS3231 chip moet 0x68 zijn. Ik kom daar later op terug.

Daar gaan we…

DS3231 varianten

Er zijn verschillende varianten van de DS3231 module. Hieronder zijn de twee bekendste afgebeeld. Ik bespreek ze allebei.

De twee bekendste DS3231 RTC modules

De RTC modules kenmerken zich doordat de modules een DS3231 chip hebben en daarmee een I²C- en soms ook een SPI-interface kent en, niet onbelangrijk, een (oplaadbare) batterij. De batterij zorgt ervoor dat als de module niet gevoed wordt de actuele tijd wordt bijgehouden. De batterij is in het beste geval oplaadbaar en laadt zich op als er een voeding op aangesloten wordt.

Mini RTC module (DS3231 for PI)

De linker (mini) module is specifiek ontworpen voor de Raspberry Pi en wordt eenvoudig met de oneven headerpinnen 1-9 verbonden. Via een breadbord past de RTC prima op een Pico. De module kent een vast gemonteerde oplaadbare batterij. Het is dan ook verstandig voor gebruik de spanning van de batterij te meten of deze 3V3 levert, zo niet dan kan die de prullenbak in.

Tip: Wacht even een dag na het laden voordat je de spanning meet. Ervaring leert dat bij mij na een paar jaar de helft van deze batterijen het niet meer doen.

Een tijdje geleden heb ik een artikel over dit type RTC geschreven, kijk hier als je er meer over wilt weten.

RTC met EEPROM (ZS-042)

De rechter RTC module is in principe technisch gelijk aan de linker, maar met dit verschil dat de module voorzien is van een een AT24C32 EEPROM. Beide IC’s op de module kunnen middels een apart (hex)adres communiceren via I²C. De EEPROM is niet gerelateerd aan RTC-functies, maar kan dienen als een niet-vluchtig geheugen voor datalogging. Aan de andere kant van de module bevindt zich een knoopcel houder. Ofschoon de RTC soms met een CR2032 (niet oplaadbaar) batterij verkocht worden, wordt sterk afgeraden ze met de module te gebruiken. De module is ontworpen voor de LIR2032 (oplaadbaar). Indien een niet oplaadbare batterij gebruikt wordt dan MOET je op de module een aanpassing doorvoeren anders probeert de module de batterij op te laden, met alle gevolgen van dien. Ik kom hier op terug.

Specificaties van de DS3231-module

  • Bedrijfsspanning: 2,3 tot 5,5 V.
  • Batterij: 2,3 tot 5,5 V. Gebruik een oplaadbare 3V-knoopcelbatterij (zoals de LIR2032-batterij).
  • Maximale actieve voedingsstroom: 300 µA.
  • De realtimeklok telt seconden, minuten, uren, datum van de maand, maand, dag van de week en jaar, met schrikkeljaarcompensatie die geldig is tot het jaar 2100.
  • Nauwkeurigheid: ±2 ppm van 0°C tot +40°C, ±3,5 ppm van -40°C tot +85°C.
  • Snelle (400 kHz) I2C-interface.
  • De module wordt aangestuurd door een 32 kHz temperatuur gecompenseerde kristaloscillator (TCXO). De TCXO helpt de RTC binnen een nauwkeurigheid van ±2 minuten per jaar te houden van -40 °C tot +85 °C.

Kan de DS1302 gebruikt worden?

De DS1302 is een vergelijkbare IC die goedkoper is dan de DS3231, maar enkele tekortkomingen heeft, zoals een lagere nauwkeurigheid, afwezigheid van I²C en geen ingebouwde temperatuurcompensatie. Voor serieuze toepassingen wordt deze RTC chip afgeraden.

De module van stroom voorzien

De rechter DS32321 RTC-module, ook wel ZS-042, bevat een houder voor een losse batterij. Als we de module willen voeden met een externe voeding en een niet-oplaadbare batterij (bijv. CR2032), moeten we een van de volgende stappen uitvoeren:

  • Verwijder de batterij uit de houder terwijl er externe stroom via de VCC-pin wordt aangevoerd.
  • Verwijder de weerstand op de print vlakbij de diode. De weerstand en diode worden gebruikt voor het opladen van een lithium-ion knoopcelbatterij (LIR2032).

Let op! Als je een niet-oplaadbare batterij gebruikt zonder een van deze stappen te volgen, kan de batterij en/of de module beschadigd raken.


Thonny
Ik ga er van uit dat je weet hoe Thonny werkt en hoe je de Raspberry Pi Pico ermee verbindt. Weet je dit niet of twijfel je? Kijk dan hier waar ik uitleg hoe je het moet doen.


RTC met de Pico verbinden

Om de Pico te laten communiceren met de RTC gebruiken we:
– GP4 als SDA (seriële data)
– GP5 als SCL (seriële klok)

Verbind de RTC pinnen (Vcc, GND, SDA, SCL) met de Pico.

Controleer I2C-adres

Na het verbinden van de RTC met de Pico is het raadzaam om te scannen naar het I2C-apparaat en te kijken of de DS3231 wordt gedetecteerd. Hieronder is de code om te scannen naar apparaten die op de I2C-bus zijn aangesloten.

Start Thonny op, verbind de Pico ermee, kopieer onderstaande code in een script en start de code.

import machine

sdaPIN = machine.Pin(4)
sclPIN = machine.Pin(5)

i2c = machine.I2C(0, sda=sdaPIN, scl=sclPIN, freq=400000)

devices = i2c.scan()

if len(devices) == 0:
    print("Geen I2C apparaat gevonden!")
else:
    print('I2C apparaat gevonden:', len(devices))
    for device in devices:
        print("Hexadecimal adres:", hex(device))

De volgende schermafbeelding in Thonny IDE toont de hexadecimale adressen van de I2C-apparaten. Het I2C-adres van de DS3231 wordt gedetecteerd als 0x68 en het I2C-adres van de EEPROM is 0x57. Dit laatste adres zie je als je de RTC met de EEPROM aangesloten hebt.

Het resultaat

De hexadecimale adressen van de RTC met EEPROM
Het hexadecimale adres van de RTC met alleen de DS3231 chip

DS3231 MicroPython-bibliotheek

In deze tutorial gebruiken we de EEPROM functie niet omdat we slechts een klok willen bouwen. Om de dateTime functie van de RTC te kunnen gebruiken hebben we de bibliotheek van Peter Hinch nodig.

Kopieer en plak de volgende DS3231 MicroPython-bibliotheekcode en sla deze via Thonny IDE op in Raspberry Pi Pico /lib als ds3231.py. Kijk hier voor de GitHub pagina van Peter.

# General purpose driver for DS3231 precison real time clock.
# Copyright Peter Hinch 2023 Released under the MIT license.

import time
import machine

_ADDR = const(104)

EVERY_SECOND = 0x0F  # Exported flags
EVERY_MINUTE = 0x0E
EVERY_HOUR = 0x0C
EVERY_DAY = 0x80
EVERY_WEEK = 0x40
EVERY_MONTH = 0

try:
    rtc = machine.RTC()
except:
    print("Warning: machine module does not support the RTC.")
    rtc = None

class Alarm:
    def __init__(self, device, n):
        self._device = device
        self._i2c = device.ds3231
        self.alno = n  # Alarm no.
        self.offs = 7 if self.alno == 1 else 0x0B  # Offset into address map
        self.mask = 0

    def _reg(self, offs : int, buf = bytearray(1)) -> int:  # Read a register
        self._i2c.readfrom_mem_into(_ADDR, offs, buf)
        return buf[0]

    def enable(self, run):
        flags = self._reg(0x0E) | 4  # Disable square wave
        flags = (flags | self.alno) if run else (flags & ~self.alno & 0xFF)
        self._i2c.writeto_mem(_ADDR, 0x0E, flags.to_bytes(1, "little"))

    def __call__(self):  # Return True if alarm is set
        return bool(self._reg(0x0F) & self.alno)

    def clear(self):
        flags = (self._reg(0x0F) & ~self.alno) & 0xFF
        self._i2c.writeto_mem(_ADDR, 0x0F, flags.to_bytes(1, "little"))

    def set(self, when, day=0, hr=0, min=0, sec=0):
        if when not in (0x0F, 0x0E, 0x0C, 0x80, 0x40, 0):
            raise ValueError("Invalid alarm specifier.")
        self.mask = when
        if when == EVERY_WEEK:
            day += 1  # Setting a day of week
        self._device.set_time((0, 0, day, hr, min, sec, 0, 0), self)
        self.enable(True)

class DS3231:
    def __init__(self, i2c):
        self.ds3231 = i2c
        self.alarm1 = Alarm(self, 1)
        self.alarm2 = Alarm(self, 2)
        if _ADDR not in self.ds3231.scan():
            raise RuntimeError(f"DS3231 not found on I2C bus at {_ADDR}")

    def get_time(self, data=bytearray(7)):
        def bcd2dec(bcd):  # Strip MSB
            return ((bcd & 0x70) >> 4) * 10 + (bcd & 0x0F)

        self.ds3231.readfrom_mem_into(_ADDR, 0, data)
        ss, mm, hh, wday, DD, MM, YY = [bcd2dec(x) for x in data]
        YY += 2000
        # Time from DS3231 in time.localtime() format (less yday)
        result = YY, MM, DD, hh, mm, ss, wday - 1, 0
        return result

    # Output time or alarm data to device
    # args: tt A datetime tuple. If absent uses localtime.
    # alarm: An Alarm instance or None if setting time
    def set_time(self, tt=None, alarm=None):
        # Given BCD value return a binary byte. Modifier:
        # Set MSB if any of bit(1..4) or bit 7 set, set b6 if mod[6]
        def gbyte(dec, mod=0):
            tens, units = divmod(dec, 10)
            n = (tens << 4) + units
            n |= 0x80 if mod & 0x0F else mod & 0xC0
            return n.to_bytes(1, "little")

        YY, MM, mday, hh, mm, ss, wday, yday = time.localtime() if tt is None else tt
        mask = 0 if alarm is None else alarm.mask
        offs = 0 if alarm is None else alarm.offs
        if alarm is None or alarm.alno == 1:  # Has a seconds register
            self.ds3231.writeto_mem(_ADDR, offs, gbyte(ss, mask & 1))
            offs += 1
        self.ds3231.writeto_mem(_ADDR, offs, gbyte(mm, mask & 2))
        offs += 1
        self.ds3231.writeto_mem(_ADDR, offs, gbyte(hh, mask & 4))  # Sets to 24hr mode
        offs += 1
        if alarm is not None:  # Setting an alarm - mask holds MS 2 bits
            self.ds3231.writeto_mem(_ADDR, offs, gbyte(mday, mask))
        else:  # Setting time
            self.ds3231.writeto_mem(_ADDR, offs, gbyte(wday + 1))  # 1 == Monday, 7 == Sunday
            offs += 1
            self.ds3231.writeto_mem(_ADDR, offs, gbyte(mday))  # Day of month
            offs += 1
            self.ds3231.writeto_mem(_ADDR, offs, gbyte(MM, 0x80))  # Century bit (>Y2K)
            offs += 1
            self.ds3231.writeto_mem(_ADDR, offs, gbyte(YY - 2000))

    def temperature(self):
        def twos_complement(input_value: int, num_bits: int) -> int:
            mask = 2 ** (num_bits - 1)
            return -(input_value & mask) + (input_value & ~mask)

        t = self.ds3231.readfrom_mem(_ADDR, 0x11, 2)
        i = t[0] << 8 | t[1]
        return twos_complement(i >> 6, 10) * 0.25

    def __str__(self, buf=bytearray(0x13)):  # Debug dump of device registers
        self.ds3231.readfrom_mem_into(_ADDR, 0, buf)
        s = ""
        for n, v in enumerate(buf):
            s = f"{s}0x{n:02x} 0x{v:02x} {v >> 4:04b} {v & 0xF :04b}\n"
            if not (n + 1) % 4:
                s = f"{s}\n"
        return s

Code om de tijd in de RTC op te slaan

We zijn er bijna! Eerst slaan we de DateTime op in de RTC. Als je dit niet doet en deze stap overslaat, is de kans groot dat er een andere tijd (initiële tijd af fabriek) in de RTC staat en straks getoond wordt. De onderstaande code stelt de RTC in op de systeemtijd (van de pc waar Thonny op draait) met behulp van de methode ds.set_time(). DateTime-tupels worden gebruikt om tijdwaarden in te stellen en te lezen. Deze tupels hebben de volgende vorm: (jaar, maand, dag, uur, minuut, seconde, weekdag, jaardag).

Maak in Thonny een bestand aan, kopieer onderstaande er in en start de software.

# Importeer de benodigde modules
from machine import I2C, Pin
from ds3231 import *
import time

# Defineer de pinnen voor de I2C communicatie
sda_pin=Pin(4)
scl_pin=Pin(5)

# Initialiseer de I2C interface met de opgegeven pinnen
i2c = I2C(0, scl=scl_pin, sda=sda_pin)
time.sleep(0.5)

# Maak een instance van de DS3231 klasse om met de DS3231 RTC te verbinden
ds = DS3231(i2c)

# Stel de DS3231 RTC in met de actuele tijd
ds.set_time()

Code om de RTC-tijd te tonen

Nu de DateTime-gegevens zijn opgeslagen, kunnen we deze in de Shell van Thonny tonen met behulp van het volgende script:

from machine import I2C, Pin
from ds3231 import *
import time

sda_pin=Pin(4)
scl_pin=Pin(5)

i2c = I2C(0, scl=scl_pin, sda=sda_pin)
time.sleep(0.5)

ds = DS3231(i2c)

# Toon de actuele datum in het format: maand/dag/jaar
print( "Date={}/{}/{}" .format(ds.get_time()[1], ds.get_time()[2],ds.get_time()[0]) )

# Toon de actuele tijd in het format: uren:minuten:seconden
print( "Time={}:{}:{}" .format(ds.get_time()[3], ds.get_time()[4],ds.get_time()[5]) )

Het resultaat

De exacte tijd wordt eenmalig getoond

Toon de tijd op een led-display (TM1637)

Nu we de RTC de exacte tijd kunnen laten zien, gaan we een stap verder. We sluiten een zeven-segment led display module (TM1637) op de Pico aan en laten de tijd op het display zien.

Display met de Pico verbinden

Verbind de pinnen van het display als volgt met de pinnen van de Pico:
CLK -> GP19
DIO -> GP18
VCC -> 3V3
GND -> GND

De code (main.py)

Om de tijd op het display te tonen heb ik onderstaan script gemaakt. Het script leest de RTC-tijd en toont deze in het led-display. In de Shell zie je direct na het starten van de code eenmalig de tijd. Zo nodig kun je bij de vijfde regel van onderen het hekje (#) verwijderen zodat in de Shell de actuele tijd getoond wordt. Als de code gestart is scrollt “GO” voorbij en verschijnt vervolgens de gesynchroniseerde RTC-tijd. Als extraatje laat ik de dubbele punt knipperen.

Mocht je de Pico ermee op willen starten, kopieer onderstaande code, plak het in Thonny in een nieuw bestand en sla het op de Pico op onder de naam ‘main.py‘.

from machine import I2C, Pin
from ds3231 import *
import tm1637
import time

#Initialiseer DS3231
sda_pin=Pin(4)
scl_pin=Pin(5)

i2c = I2C(0, scl=scl_pin, sda=sda_pin)
time.sleep(0.5)

ds = DS3231(i2c)

# Initialiseer 7-segment LED display
display = tm1637.TM1637(clk=Pin(19), dio=Pin(18)) 
display.brightness(2)
display.scroll("GO",delay=200)

# Stel dummy-tijd in voor RTC startmoment
rtc.datetime((21, 07 ,11 ,0 ,17 ,26 ,0 , 0))

# Toon de actuele datum in het format: maand/dag/jaar
print( "\nDate = {}/{}/{}" .format(ds.get_time()[1], ds.get_time()[2],ds.get_time()[0]) )

# Toon de actuele tijd in het format: uren:minuten:seconden
print( "Time = {:02}:{:02}:{:02}" .format(ds.get_time()[3], ds.get_time()[4],ds.get_time()[5]) )

# Toon de actuele tijd in het display (tm1637)
while True:
    hour = ds.get_time()[3]
    hh   = hour + 0
    min  = ds.get_time()[4]
            
    #print ("De tijd is:",hh,":",min)
    display.numbers(hh,min,True)
    time.sleep(1)
    display.numbers(hh,min,False)
    time.sleep(1)

Ik heb de opstelling met beide DS3231 RTC’s getest. Het enige dat ik moest doen is de GND verbindingsdraad op het breadboard iets opschuiven omdat de pinout van de modules iets verschillen.

De precisie klok op basis van de Raspberry Pi Pico en de RTC DS3231 (type DS3231 for PI)
De precisie klok op basis van de Raspberry Pi Pico en de RTC DS3231 (type ZS-042)

Veel plezier ermee! Mocht je vragen of opmerkingen hebben, laat dan hieronder een reactie achter.

Have A Nice Day!

Geef als eerste een reactie

Laat een reactie achter

Het e-mailadres wordt niet gepubliceerd.


*