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 specifiek (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 wat serieuze toepassingen zoals een permanente klok toepassing wordt deze RTC chip afgeraden. Voor het experimenteren met een RTC is het een prima RTC.

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 niet oplaadbare batterij uit de houder als er externe stroom via de VCC-pin wordt aangevoerd.
  • En/of 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.

Tip: Waarom al deze moeite? Koop een LIR2032! Je hebt dan geen gedoe met beperkingen of risico’s en nog een mooie RTC module ook.


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

Om de dateTime functie van de RTC te kunnen gebruiken hebben we de bibliotheek van Peter Hinch nodig. Ten overvloede heb ik de benodigde bibliotheek hieronder geplaatst.

Kopieer de ds3231_gen.py bibliotheekcode en sla deze via Thonny IDE op in de Raspberry Pi Pico /lib-map als ds3231.py. Als je niet weet hoe dit moet, kijk dan hier.

ds3231_gen.py (Peter Hinch)

# 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

Nu we de bibliotheek in de /lib-map hebben staan kunnen we eindelijk gaan bouwen.

Eerst slaan we de DateTime op in de RTC van de Pico. 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 bouwstappen

Om de actuele tijd uiteindelijk op een LED-display te kunnen laten zien, zijn de volgende stappen nodig.

  1. De (interne) RTC van de Pico van de actuele tijd voorzien.
  2. De tijd van de Pico RTC opslaan in de (externe) DS3231 RTC
    • Controleren of de DS3231 RTC de juiste tijd heeft
  3. Het hoofdprogramma: De tijd van de DS3231 wordt getoond op het LED-display
    • Installatie van de TM1637, het LED-display

Ad 1 – De (interne) RTC van de Pico van de actuele tijd voorzien

Je kunt op twee manieren de RTC van de Pico van de actuele tijd voorzien.

  1. Door met de hand de tijd in te geven
  2. Door de tijd automatisch in te (laten) lezen

Ad 1.1 – Met de hand de datum/tijd ingeven

Het enige dat je hiervoor hoeft te doen is de gewenste tijd in het script in te geven en, zodra het deze tijd is, het script te starten.

from machine import RTC
import time

# Stel hieronder de tijd in van de interne RTC van de Pico
# Voorbeeld: 2025-01-25 15:50:30 UTC

year   = 2025           
month  = 1
day    = 25
hour   = 15
minute = 50
second = 30

# Stel de datum/tijd voor de RTC van de Pico in
print("Time to set {:04}-{:02}-{:02} {:02}:{:02}:{:02}".format(
      year, month, day, hour, minute, second))

rtc = RTC()
rtc.datetime((year, month, day, 0, hour, minute, second, 0))

# Lees de datum/tijd van de RTC van de Pico
year, month, day, hour, minute, second, dow, doy = time.gmtime()

print("Time is now {:04}-{:02}-{:02} {:02}:{:02}:{:02}".format(
      year, month, day, hour, minute, second))

De interne RTC van de Pico heeft nu de ingestelde tijd.

Ad 1.2 – Code om de tijd in de RTC op te slaan (Schrijver: WERKT DIT? NOG NAKIJKEN!)

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.

Deze code wordt zsm gepubliceerd!

Ad 2 – De Pico RTC datum/tijd opslaan in de DS3231 RTC

Nu de RTC van de Pico van de ingestelde (juiste) datum/tijd voorzien is, kan deze in de DS3231 opgeslagen worden. Gebruik hiervoor onderstaan script.

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

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

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

# Maak een DS3231-klasse aan voor de DS3231 RTC
ds = DS3231(i2c)

# Stel de DS3231 RTC in op de huidige systeemtijd van de Pico RTC 
ds.set_time()

print ("De Datum en Tijd is opgeslagen in de RTC (DS3231)")

Ad 3 – Controleren of de DS3231 RTC de juiste tijd heeft

Nu de DateTime zijn opgeslagen in de DS3231 RTC kunnen we deze in de Shell van Thonny laten zien met behulp van het volgende script.

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

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

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

# Maak een DS3231-klasse aan voor de DS3231 RTC 
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

Als de juiste tijd getoond wordt kunnen we verder.

De exacte tijd wordt eenmalig getoond

Ad 4 – Het ‘main’-programma maken dat de tijd toont op basis van de DS3231 RTC-tijd

Toon de tijd op een led-display (TM1637)

Nu we de tijd van de DS3231 RTC 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

Het script (main.py)

Om de tijd op het display te tonen heb ik onderstaan script gemaakt. Het script leest de RTC-tijd van de DS3231 en toont deze in het led-display. In de Thonny Shell zie je direct na het starten van de code de start datum en tijd. Als de code gestart is scrollt “GO” in het display voorbij en verschijnt vervolgens de DS3231 RTC-tijd. Als extraatje laat ik de dubbele punt knipperen zodat je weet dat het programma draait.

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)

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

# Maak een DS3231-klasse aan voor de DS3231 RTC 
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 een dummy-tijd in voor de RTC van de Pico
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]

    # Toon de tijd met de knipperende dubbele punt            
    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.


*