Powrót

Kurs Arduino – zegar czasu rzeczywistego RTC DS3231

W poprzednich odcinkach kursu Arduino wykorzystywaliśmy rozmaite czujniki. Teraz w odcinku 9 zaczniemy realizować logger, czyli rejestrator danych. Zajmiemy się też problemem czasu, a w szczególności wykorzystaniem modułów służących do odmierzania czasu – modułów czasu rzeczywistego RTC (Real Time Clock), w szczególności DS3231.

Stopniowo zrealizujemy rejestrator – logger, i to nie jedną wersję, ale wcześniej opanujemy coś innego. Otóż do rejestratora przydałby się, a wręcz konieczny jest jeszcze jeden blok, mianowicie zegar, żeby przy rejestracji podawać nie tylko wartość zmierzonych parametrów, ale i czas dokonania pomiaru. Dlatego w dziewiątym odcinku kursu zajmiemy się problemem czasu, a w szczególności wykorzystaniem modułów służących do odmierzania czasu.

Zliczanie czasu można zrealizować na kilka sposobów. Trzeba wiedzieć, że Arduino bez naszej wiedzy tworzy w procesorze rodzaj zegara, a raczej timera ciągle odmierzającego czas. To zadanie wykonuje jeden z trzech timerów procesora ATmega328. Taki zegar-timer tworzony jest w każdym programie Arduino i to jest jeden z powodów, dla których nawet najprostszy program zajmuje w pamięci procesora więcej miejsca niż podobne „niearduinowe” programy tworzone np. z pomocą AVR Studio czy BASCOM-a. Nie jest to prawdziwy zegar, tylko timer zliczający milisekundy, które upłynęły od chwili włączenia zasilania i rozpoczęcia pracy programu, co można odczytać za pomocą funkcji millis(). Ten licznik-timer jest kasowany do zera przy każdym wyłączeniu zasilania, a ponadto ulega przepełnieniu i liczy od zera po każdych kolejnych 49 dniach. Dokładność odmierzania w ten sposób czasu jest bardzo mała, zarówno z powodu pewnych uproszczeń programowych (wskutek których „arduinowa milisekunda” z założenia nie jest równa „prawdziwej milisekundzie”), jak też z faktu, że główny procesor Arduino nie jest taktowany rezonatorem kwarcowym, tylko dużo mniej stabilnym  ceramicznym. Będziemy do tych dość ważnych szczegółów wracać, w każdym razie z kilku powodów nie ma sensu programowa realizacja „prawdziwego zegara” za pomocą procesora ATmega328 płytki Arduino Uno (inne płytki Arduino zawierają inne procesory, a niektóre z nich mają wbudowany w procesor zegar RTC). W przypadku Arduino Uno i większości innych płytek Arduino do odmierzania czasu zdecydowanie lepiej wykorzystać oddzielny moduł zegara czasu rzeczywistego, czyli RTC (Real Time Clock).

Na rynku jest sporo układów scalonych RTC. Jednym z najpopularniejszych jest, a właściwie był, DS1307 (DS1302). W Internecie do dziś można znaleźć mnóstwo przykładów użycia tych układów scalonych. Na pierwszy rzut oka idealny do naszych celów byłby pokazany na fotografii 1 moduł zawierający RTC w postaci DS1307, baterię podtrzymującą oraz „5-woltowy” czytnik kart SD. Niestety, tylko na pierwszy rzut oka.

Fotografia 1

W praktyce DS1307 okazuje się kiepskim rozwiązaniem. Dokładność zegara jest wyznaczona przez precyzję zastosowanego rezonatora kwarcowego (32768Hz). Dokładność częstotliwości tanich rezonatorów kwarcowych jest mała. Co prawda stałą odchyłkę można byłoby wyeliminować już w kostce DS1307 albo w programie Arduino, jednak nie można wyeliminować dość dużego wpływu temperatury. W praktyce zegary RTC z kostką DS1307 (DS1302) mogą spieszyć się lub późnić kilka minut na miesiąc. Ten sam problem dotyczy układów RTC z innymi kostkami, także z dość popularnymi PCF85xx.

Zdecydowanie większą dokładność od DS1307 mają także bardzo tanie układy RTC tej samej firmy MAXIM, o oznaczeniu DS3231, które dla zwiększenia dokładności mają wewnętrzny czujnik temperatury. Klasyczna wersja DS3231SN ma też rezonator kwarcowy wbudowany wewnątrz obudowy układu scalonego. Na etapie produkcji dany egzemplarz kostki jest kalibrowany temperaturowo. Do pamięci układu są wpisywane dane kalibracyjne. Później podczas normalnej pracy czujnik temperatury mierzy temperaturę i dołącza do tego wewnętrznego rezonatora szereg małych pojemności tak, żeby zmienić częstotliwość rezonatora i by w danej temperaturze utrzymać potrzebną częstotliwość oscylatora. Dołączanie drabinki kondensatorów związane jest z wykorzystaniem dostępnego też dla użytkownika rejestru kalibracyjnego.  Efekt takiej korekcji pokazany jest na rysunku 2, gdzie czerwona krzywa to typową zależność częstotliwości od temperatury dla rezonatora kwarcowego, a linia zielona pokazuje stabilność po korekcji.

Rysunek 2

W każdym razie gwarantowana w temperaturze 0…+40°C dokładność częstotliwości DS3231 to ±2ppm, co oznacza dokładność zegara lepszą niż pięć sekund na miesiąc, co daje maksymalną odchyłkę około minuty na rok (rok ma około 526 000 minut)! W pełnym zakresie temperatury (–45°C…+85°C) wynosi ±3,5ppm.

Ostatnio upowszechnia się też wersja DS3231M, gdzie końcowa litera M wskazuje, że zamiast rezonatora kwarcowego wykorzystany jest generator MEMS, zrealizowany jako mikromechaniczna struktura w płytce krzemowej. Ta wersja też ma obwody kalibracji temperaturowej. Uzyskiwana dokładność jest odrobinę mniejsza niż w DS3231SN i w pełnym zakresie temperatury pracy (–45°C…+85°C) wynosi ±5ppm, czyli błąd nie większy niż 2…3 minuty na rok, co też jest znakomitym rezultatem.

W związku z takimi parametrami moduły DS1307 i wcześniejsze DS1307/02, a także szereg innych układów RTC o podobnych właściwościach wielu elektroników słusznie czy niesłusznie po prostu wyrzuca do kosza i zastępuje je modułami z DS3231, które w kraju kosztują poniżej dziesięciu złotych (a w Chinach około 1 dolara).

Fotografia 3

Fotografia 3 pokazuje trzy takie moduły, który oprócz DS3231 zawierają też pamięć EEPROM 24C32 i koszyk na baterię litową. Różnią się szczegółami. Jeden zawiera kostkę  DS3231M. Co ważne, dwa zawierają diodę i 200-omowy rezystor, a jeden tego obwodu nie ma. To bardzo ważny szczegół.

Rysunek 4

Rysunek 4 pokazuje schemat tego rodzaju modułów. Kostka DS3231 normalnie zasilana jest napięciem 2,3…5,5V i wtedy z baterii nie jest pobierany prąd, a tylko przy braku zasilania modułu, energia pobierana jest z baterii (wtedy możliwa jest nawet realizacja transmisji za pomocą I2C, ale oznacza to znaczne obciążenie dla małej bateryjki). W szczególności gdy moduł jest całkowicie odłączony od zasilania zewnętrznego, wewnętrzne układy zliczają czas przy małym średnim poborze prądu rzędu 1 tylko mikroampera. Do tego co 64 sekundy przeprowadzany jest pomiar temperatury i korekcja częstotliwości, co trwa około 0,2s i pobór prądu wzrasta do około 0,5mA, przez co średni pobór prądu przy zasilaniu bateryjnym wynosi około 3 mikroamperów. Jednorazowa bateria CR2032 powinna wystarczyć na rok ciągłej pracy modułu odłączonego od zewnętrznego zasilania. Jeżeli przerwy zasilania są krótsze, bateria wystarczy na kilka lat.

Co ważne, moduły oprócz kontrolki LED zawierają też obwód ze zwykłą diodą i zaskakująco małym rezystorem (200Ω) (wyróżniony na rysunku 4 różową podkładką i czerwonymi strzałkami na fotografii 3). Obwód, który ma służyć do… ładowania baterii.

Problem w tym, że bardzo popularne jednorazowe baterie litowe CR2032 absolutnie nie nadają się ani do ładowania, ani do podładowywania maleńkim nawet prądem! Zasilanie takiego modułu napięciem 5V skończy się uszkodzeniem, spuchnięciem, rozszczelnieniem, a być może nawet eksplozją baterii CR2032!

W niektórych źródłach jest podane, że ten obwód ładowania nie przeszkadza przy zasilaniu modułu napięciem 3,3V (np. w systemach z Raspberry Pi) – to prawda, ale wtedy też nie jest do niczego potrzebny i nie pełni żadnej funkcji.

Ten obwód ładowania, spotykany też w innych modułach RTC, przewidziany jest na okoliczność zastosowania nie jednorazowego ogniwa CR(2032), tylko litowego akumulatora LIR(2032). I trzeba przyznać, że niektóre moduły RTC dostarczane są właśnie z akumulatorkiem LIR, a nie jednorazowym ogniwem CR. Ale i wtedy słusznie nasuwają się wątpliwości co do prawidłowości takiego rozwiązania. Otóż według kart katalogowych akumulatorków LIR… nominalne napięcie ładowania to 4,2V, a jego maksymalna wartość to 4,25V (rysunek 5).

Rysunek 5

Tymczasem końcowe napięcie ładowania ze zwykłą diodą krzemową według rysunku 4 będzie rzędu 4,4V (napięcie przewodzenia diody przy małych prądach wynosi około 0,6V). A ponadto napięcie zasilania może być trochę wyższe od nominalnego 5,0V – na przykład przy zasilaniu 5,2V, na ładowanym akumulatorku końcowe napięcie ładowania wynosiłoby około 4,6V. A to oznacza ewidentne przeładowanie! Nic dziwnego, że w sieci można znaleźć fotografie tego rodzaju modułów RTC ze „spuchniętym” akumulatorkiem LIR.

Warto dodać, że przy zasilaniu 3,3V napięcie to jest zdecydowanie za małe do naładowania akumulatorka LIR o napięciu nominalnym 3,6…3,7V. Z podanego względu zarówno przy zasilaniu 5V, jak i 3,3V w tego rodzaju modułach RTC z akumulatorkiem LIR należałoby w szereg z zastosowaną tam diodą włączyć jeszcze jedną diodę (zwykłą lub Schottky’ego), by w danym konkretnym przypadku końcowe napięcie ładowania na akumulatorku nie przekroczyło 4,25V. To jest pewien kłopot z kilku powodów, dlatego prościej jest usunąć (wylutować) diodę i rezystor obwodu ładowania oraz stosować jednorazowe ogniwa CR2032. Jeden z modeli z fotografii 3 już fabrycznie nie miał wlutowanej diody i 200-omowego rezystora, a za to od razu miał włożone ogniwo CR3231. To on został dołączony do płytki Arduino wraz z wyświetlaczem LCD oraz modułem czytnika kart microSD według rysunku 6 i fotografii 7.

Rysunek 6

Fotografia 7

Na razie spróbujmy wykorzystać tylko moduł RTC. Komunikuje się on z Arduino za pomocą łącza I2C (TWI). W najprostszym przypadku należałoby zaadresować kostkę DS3231 (jej adres to 0x68) i odczytać zawartość poszczególnych jej rejestrów. Jak pokazuje pochodzący z katalogu DS3231 rysunek 8, w tych rejestrach odmierzany czas (m.in. godziny, minuty sekundy) podany jest w kodzie BCD, to znaczy, że liczby dwucyfrowe już w kostce RTC reprezentowany jest przez dwie liczby 4-bitowe. W niektórych zastosowaniach było to wygodne, ale obecnie coraz częściej nie jest to zaletą.

Rysunek 8

Znając zasady pracy łącza I2C, można byłoby napisać program, który wprost wykorzysta zawartość poszczególnych rejestrów, w tym gotowe wartości poszczególnych cyfr podane w kodzie BCD. Przy odrobinie wysiłku w Internecie można znaleźć takie programy, także dla Arduino.

Ale my w ramach kursu Arduino już przyzwyczailiśmy się do wykorzystywania gotowych bibliotek, które pozwalają zapomnieć o szczegółach (a właściwie nie wymagają poznania szczegółów) i skracają pisanie programu szkicu.

Chcemy więc znaleźć bibliotekę do obsługi DS3231. Do dyspozycji jest co najmniej kilka bibliotek, oferujących różne funkcje i możliwości. Arduinowy menedżer bibliotek po wpisaniu DS3231 oferuje dziesięć propozycji, ale niestety nie pozwala na poznanie i porównanie ich możliwości. Dlatego ja przejrzałem biblioteki DS3231 dla Arduino na stronie www.github.com. Najwięcej gwiazdek ma tam biblioteka DS3232RTC Jacka Christensena (https://github.com/JChristensen/DS3232RTC), która jednak nie obsługuje wszystkich funkcji wykorzystanego modułu. Druga pod względem liczby gwiazdek jest biblioteka MakunaRTC z adresu https://github.com/Makuna/Rtc, która obsługuje też dodatkowe funkcje, a także pamięć EEPROM 24C32 oraz zawiera szereg przykładowych szkiców. Zainstalowałem ją z poziomu ArduinoIDE (rysunek 9), upewniwszy się wcześniej, że jest to najnowsza wersja (2.1.2).

Rysunek 9

Rysunek 10

Jak pokazuje rysunek 10, w bibliotece tej mamy aż sześć plików nagłówkowych .h, ale tylko dwa pliki .cpp, co nie powinno dziwić. Do wstępnych prób wykorzystałem zawarty w katalogu /examples szkic DS3231_Simple, który otworzyłem z poziomu ArduinoIDE – rysunek 11.

Rysunek 11

Podczas tych wstępnych prób został on trochę zmieniony, spolonizowany. Ale ostatecznie zdecydowałem się okroić i maksymalnie uprościć ten program. Ta uproszczona wersja dostępna jest tutaj  = ZIP A09 jako A0901.ino.

Proponuję, żebyś zestawił układ według rysunku 6 (może być bez czytnika kart micro SD) i wgrał do Arduino program A0901.ino.

Na wyświetlaczu LCD od razu zobaczysz działający zegar, pokazujący dzień miesiąca, miesiąc, godziny, minuty i sekundy! U mnie wyglądało to jak na fotografii 12.

Fotografia 12

Zaskakująco krótki program A0901.ino, pokazany w szkicu 1, realizuje najprawdziwszy zegar, a na dodatek w jakiś (na razie) tajemniczy sposób bez naszej wiedzy od razu ustawił w naszym chronometrze właściwy czas!

Szkic 1:

#include <LiquidCrystal.h>
#include <Wire.h> // biblioteka I2C
#include <RtcDS3231.h> // biblioteka dla DS3231
RtcDS3231<TwoWire> zegar(Wire); // tworzymy obiekt zegar
LiquidCrystal wysw (2, 3, 4, 5, 6, 7); //obiekt wysw
 
void setup ()   {   //jednorazowo:
  Serial.begin(9600);  zegar.Begin(); wysw.begin(16, 2);
  RtcDateTime czasKompilacji = RtcDateTime(__DATE__, __TIME__);
  zegar.SetDateTime(czasKompilacji); zegar.SetIsRunning(true);}
 
void loop () { wysw.clear();
 RtcDateTime czasAktualny = zegar.GetDateTime();
 wysw.print(czasAktualny.Hour());wysw.print(„:”);
 wysw.print(czasAktualny.Minute());wysw.print(„:”);
 wysw.print(czasAktualny.Second()); delay(500); }

Oto szczegóły: na początku szkicu A0901.ino dołączamy biblioteki: do obsługi wyświetlacza LCD, łącza I2C oraz kostki DS3231. Następnie tworzymy dwa obiekty o nazwach zegar oraz wysw. Na początku jednorazowej funkcji setup() inicjalizujemy obiekty. W następnych dwóch linijkach mamy tajemniczy sposób ustawiania czasu.

Musimy tu odróżnić dwie sprawy. Jedna sprawa to kompilacja w komputerze, druga to praca programu w procesorze ATmega328 na płytce Arduino. Otóż w funkcji setup() niejako „w locie” tworzymy lokalną zmienną o nazwie czasKompilacji. Tworząc zmienną „w locie”, musimy przed nią podać jej typ. Nie jest to zmienna prosta (np. typu int, char czy float), tylko zmienna złożona, a konkretnie struktura. Ta zmienna strukturalna ma typ RtcDateTime, określony w bibliotece – klasie RtcDS3231. I właśnie do tej stworzonej „w locie” złożonej zmiennej coś wpisuje biblioteczna funkcja – metoda o nazwie RtcDatTime(). Argument przekazywany do tej funkcji ma dziwną postać: __DATE__, __TIME__

Otóż podczas pracy kompilatora, pobierze on informacje o systemowej dacie i czasie naszego komputera, na którym piszemy program, i w zmiennej czasKompilacji umieści informacje ni mniej, ni więcej, tylko o czasie kompilacji programu na naszym komputerze. Oznacza to, że w tworzonym programie kompilator umieści informację o czasie kompilacji.

Rysunek 13

Tak się składa, że gdy w ArduinoIDE klikniemy ikonkę Wgraj (Compile) – rysunek 13, najpierw następuje kompilacja programu i bezpośrednio po tym program wynikowy jest ładowany do procesora płytki Arduino. Gdy program zacznie pracować w procesorze ATmega, polecenie z następnej linijki, czyli

zegar.SetDateTime(czasKompilacji);

w sobie znany sposób wpisze do naszego układu DS3231 zarówno datę, jak i czas kompilacji. Potem (na wszelki wypadek) poda polecenie uruchomienia zegara: zegar.SetIsRunning(true);

Program zostanie załadowany do płytki Arduino i zacznie to realizować kilka sekund po kompilacji, więc do kostki DS3231 czas kompilacji zostanie wpisany z niewielkim opóźnieniem tych kilku sekund, ale to jest bardzo drobna odchyłka.

Jeżeli ktoś chciałby zmniejszyć błąd, mógłby zaobserwować, jaka jest różnica czasu systemowego komputera i czasu na wyświetlaczu zegara, a potem odpowiednio skorygować (do przodu) czas w zmiennej czasKompilacji. Wymagałoby to jednak rozszyfrowania szczegółów działania biblioteki – klasy i sposobu reprezentacji minut i sekund w zmiennych typu RtcDateTime. Na pewno nie jest to zadanie dla początkujących.

W większości przypadków omówiony tu prosty sposób wpisywania do RTC czasu kompilacji jest całkowicie satysfakcjonujący! Oczywiście można też dopisać do programu procedury „ręcznego” ustawiania czasu RTC.

A jeśli chodzi o szkic A0901.ino, w każdym obiegu pętli loop() program najpierw czyści wyświetlacz, a potem do stworzonej „w locie” zmiennej czas­Aktualny, która też jest strukturalnego typu RtcDateTime, funkcja – metoda GetDateTime() wczytuje z naszego zegara RTC aktualny czas.

Następnie metoda print() pokazuje na wyświetlaczu LCD godziny, minuty i sekundy, odczytane z kolejnych pól zmiennej czasAktualny.

Proste, łatwe i przyjemne!

Mamy funkcjonalny zegar, cieszymy się sukcesem, ale… szybko zauważamy, że lepiej byłoby liczby jednocyfrowe (przynajmniej minuty i sekundy) wyświetlać zawsze jako dwucyfrowe, z zerem na początku, np. 21:07:02 zamiast 21:7:2.

Zrealizowane jest to w programie A0902.ino, gdzie jak pokazuje szkic 2, do wyświetlania czasu (a dodatkowo daty) wykorzystana jest funkcja printDateTime(), która nie pochodzi z biblioteki, tylko jest zdefiniowana dalej w programie.

Szkic 2:

void loop () { wysw.clear();
 RtcDateTime czasAktualny = zegar.GetDateTime();
 printDateTime(czasAktualny);  delay(200);}
 
#define countof(a) (sizeof(a) / sizeof(a[0]))
void printDateTime(const RtcDateTime& dt) {
 char datestring[16];snprintf_P(datestring,countof(datestring),
 PSTR(„%02u/%02u %02u:%02u:%02u”), dt.Day(), dt.Month(),
 dt.Hour(),dt.Minute(),dt.Second()); wysw.print(datestring); }

Dla większości Czytelników ta funkcja to zapewne czarna magia… Wszystkich szczegółów nie będziemy analizować, ale z grubsza do funkcji printDateTime() jako argument przekazujemy odczytany z RTC czasAktualny, który w tej funkcji będzie nazywał się dt. Tworzymy też 15-znakową zmienną tekstową datestring[16]. Następnie „wybrane kawałki dt układamy z zmiennej datestring” według wzorca: „%02u/%02u %02u:%02u:%02u„, który określa „dwucyfrowy” sposób wyświetlania liczb i znaki pomiędzy nimi (ukośnik, spacja, dwukropek). Efekt działania programu A0902 widać na fotografii 14.

Fotografia 14

Możemy do tego dorzucić wyświetlanie temperatury, odczytanej z kostki DS3231. Pętlę loop() można zmodyfikować według szkicu 3.

Szkic 3:

void loop () {
 RtcDateTime czasAktualny = zegar.GetDateTime();
 wysw.clear(); printDateTime(czasAktualny);  delay(200);}
 
//funkcja formatująca wyświetlany wynik:
#define countof(a) (sizeof(a) / sizeof(a[0]))
void printDateTime(const RtcDateTime& dt) {
 char datestring[16];snprintf_P(datestring,countof(datestring),
 PSTR(„%02u/%02u %02u:%02u:%02u”), dt.Day(), dt.Month(),
 dt.Hour(),dt.Minute(),dt.Second()); wysw.print(datestring); }

Wersja z pliku A0903.ino daje na wyświetlaczu efekt jak na fotografii 15.

Fotografia 15

Dzięki użyciu DS3231 mamy zaskakująco dokładny zegar z kalendarzem, uwzględniający długość poszczególnych miesięcy, a także lata przestępne. Nie ma tylko automatycznej zmiany na czas letni i nie ma bezpośredniej możliwości zmiany stref czasowych. Dla ścisłości, jest też problem  „przestępnych sekund”, ale nie dotyczy on układu RTC, tylko innego aspektu „komputerowego” zliczania czasu.

Wykorzystujemy tu też bibliotekę – klasę RtcTemperature i w linii

RtcTemperature temp = zegar.GetTemperature();

tworzymy „w locie” zmienną temp, która jest typu RtcTemperature i do tej złożonej zmiennej wpisujemy wartość temperatury z układu RTC za pomocą funkcji – metody .GetTemperature().

Analiza treści biblioteki RtcTemperature.h wskazuje, że zawartość tej zmiennej to po prostu odczytane z DS3231 dwa bajty z rejestrów temperatury (11h, 12h). W programie przenosimy kursor do dolnej linii wyświetlacza LCD i wyświetlamy napis oraz wartość temperatury. Argumentem metody .print() nie jest bezpośrednio zmienna temp, tylko wartość temperatury, przetworzona na liczbę zmiennoprzecinkową z pomocą bibliotecznej metody AsFloatDegC().

Pięknie, ale jest też poważniejszy problem. Jeżeli odłączysz zasilanie od płytki Arduino, układ RTC nadal będzie odliczał czas dzięki rezerwowej baterii CR2032. Gdy jednak ponownie włączysz zasilanie…

Czy już domyślasz się, na czym polega dość istotny problem?

Otóż nasz fantastycznie krótki program przy każdym włączeniu zasilania zaczyna pracować od początku, a to znaczy, że po każdym włączeniu zasilania zrealizuje funkcję setup(), która za każdym razem wpisuje do RTC… zdecydowanie już przestarzały czasKompilacji!

Pomimo że układ RTC może pracować prawidłowo, wskutek niedoróbki programu po każdym włączeniu zasilania nasz zegar niepotrzebnie zaczyna pracę od nieaktualnego już wskazania (od czasu kompilacji).

STOP! Proponuję, żebyś samodzielnie zastanowił się, jak to poprawić.

Nie czytaj dalej!

STOP! Spróbuj rozwiązać problem!

Jeśli Ci się udało – ogromne gratulacje!

Niestety, dla elektronika uczącego się programowania takie problemy wcale nie są łatwe do rozwiązania, głównie z uwagi na inny tok myślenia i brak „programistycznych przyzwyczajeń”. Wróćmy do wspomnianego przykładowego programu DS3231_Simple dołączonego do biblioteki. Możesz przeanalizować oryginał, ale ja proponuję spolonizowaną i zmodyfikowaną wersję, z pliku A0904.ino. W szkicu 4 znajdziesz kluczowe fragmenty pliku A0904.ino.

Szkic 4:

#include <Wire.h> // biblioteka I2C
#include <RtcDS3231.h> // biblioteka dla DS3231
RtcDS3231<TwoWire> zegar(Wire);
 
void setup ()   { // jednorazowo na początku pracy programu:
    … pominięta część programu – funkcji setup() …
 
//odczytujemy czas z RTC do zmiennej „typu zegarowego”:
  RtcDateTime czasAktualny = zegar.GetDateTime();
  Serial.println(„A oto czas z RTC:”);
  printDateTime(czasAktualny); Serial.println();
  if (czasAktualny < czasKompilacji) { // sprawdzamy
   zegar.SetDateTime(czasKompilacji);//wpisujemy czasKompilacji
   Serial.println(„Wpisano do RTC czas kompilacji”);} 
  else if (czasAktualny > czasKompilacji) {
   Serial.println(„Czas odczytany z RTC wyglada prawidlowo”);}
  else if (czasAktualny == czasKompilacji){
   Serial.println(„Dziwny, ale akceptowalny odczyt z RTC”); }
 
… tu pominięta część programu setup() …      }
 
void loop () { //podczas pracy w pętli (co ok. 10 sekund):
 if (!zegar.IsDateTimeValid()) { //gdy jakiś błąd pracy RTC
  Serial.println(„Jakis problem RTC ze zliczaniem czasu!”); }
// tak czy inaczej – odczytaj czas z RTC i wyświetl na konsoli
 RtcDateTime czasAktualny = zegar.GetDateTime();
 printDateTime(czasAktualny);  Serial.println();
// odczytaj i wyświetl temperaturę:
 RtcTemperature temp = zegar.GetTemperature();
  temp.Print(Serial);   Serial.println(„C”);
delay(10000); } // czekaj 10 sekund
 
//…pominięta definicja funkcji formatującej i drukującej…

Program wykorzystuje łącze szeregowe i konsolę monitora w komputerze, a nie wykorzystuje wyświetlacza LCD – możesz go odłączyć.

Co około 10 sekund wyświetla on pełną datę: dzień, miesiąc, rok, a do tego temperaturę DS3231, zmierzoną przez wewnętrzny czujnik. Czerwonym kolorem zaznaczyłem fragment, usuwający wcześniejszą wadę „rozpoczynania po włączeniu zasilania od czasu kompilacji”. Otóż wcześniej w funkcji setup() bezwarunkowo wpisywaliśmy do RTC czas kompilacji poleceniem:

zegar.SetDateTime(czasKompilacji);

Teraz jest inaczej: w funkcji setup() najpierw do stworzonej „w locie” zmiennej  czasAktualny wpisujemy aktualny czas pobrany z układu RTC:

RtcDateTime czasAktualny = zegar.GetDateTime();

Następnie za pomocą warunku if porównujemy czas aktualny z czasem kompilacji (na marginesie: kto mniej zaawansowany wpadnie na to, że można tak porównywać  zawartość złożonych zmiennych „zegarowego typu”?). Jeżeli nasz moduł RTC nigdy nie był używany albo gdy wcześniej na chwilę wyjęto z niego baterie, to pomijając detale, zaczął on liczyć czas „od zera” i jakkolwiek by było, jego stan jest „mały”,  „bliski zera”, choćby dlatego, że zapewne nie zliczył ani jednego pełnego roku. A więc jeśli zegar RTC nie był wcześniej prawidłowo ustawiony albo „zapomniał czas” wskutek chwilowego braku baterii, to czas aktualne jest mniejszy od czasu kompilacji, dokonanej przez nas w ramach kursu Arduino w roku 2018.

I tylko wtedy w funkcji setup() do RTC wpisany zostanie czas kompilacji.

Natomiast gdy już wcześniej, czy to w ramach tego rodzaju ćwiczeń program wpisał do RTC czas kompilacji,  czy moduł RTC był prawidłowo ustawiony w jakikolwiek inny sposób, wtedy RTC liczy nie od zera, tylko niejako kontynuuje liczenie i zawiera prawidłowy bieżący czas. Dzięki obecności baterii liczy czas niezależnie od tego, czy jest zasilany z zewnątrz (z Arduino), czy nie (może być całkiem odłączony, byle miał baterię).

Gdy więc ponownie włączymy zasilanie płytki Arduino z wgranym już wcześniej programem A0903, program najpierw sprawdzi omawiany warunek i stwierdzi, że czas aktualnie pobrany z RTC „nie jest mniejszy” od czasu kompilacji. I nie wpisze ponownie „przestarzałego już” czasu kompilacji.

Co prawda nie sprawdzi, czy wskazania układu RTC są dokładne, ale nie zaingeruje w jego pracę. Aby usunąć omawiany problem, wystarczyłoby napisać krótko:

if (czasAktualny < czasKompilacji) {
 zegar.SetDateTime(czasKompilacji)};

Ale w naszym programie dodatkowo sprawdzamy:

else if (czasAktualny > czasKompilacji) {}

a jeśli tak, nie ingerujemy w RTC, tylko wypisujemy na ekranie konsoli uspokajający komunikat. Autor pierwotnego programu sprawdza jeszcze „równość czasów”:

else if (czasAktualny == czasKompilacji){}

ale taki przypadek absolutnie nie powinien się nigdy zdarzyć, bo nawet przy najszybszym komputerze, między chwilą kompilacji a początkiem pracy Arduino mija więcej niż sekunda.

U mnie po wgraniu  tego programu do Arduino ekran konsoli monitora wyglądał jak na rysunku 16.

Rysunek 16

Podsumowanie

Niestety, nie omówiliśmy wszystkich interesujących zagadnień, ani tych dotyczących różnych aspektów zliczania czasu w programach Arduino, ani tych związanych z układem DS3231, w szczególności alarmów. Nie przejmuj się, jeśli w tym odcinku kursu napotkałeś niezrozumiałe szczegóły. Do szeregu poruszonych tu spraw będziemy wracać, a wiedza z tego odcinka zapewne zachęci zainteresowanych do samodzielnego zbadania różnych zagadnień, być może przy użyciu jeszcze innych bibliotek. A w następnym odcinku UR010 zajmiemy się czujnikiem temperatury DS18B20.

Piotr Górecki