Arduino - Een digitale clock
Een digitale clock module biedt Arduino de mogelijkheid om de echte datum en tijd uit te lezen. De term waar je op moet zoeken voor zo'n module is RTC - Real Time Clock. Een dergelijke module heeft een eigen batterij waarmee de klok oneindig doorloopt, ook als de stroom wordt uitgeschakeld. De interne klok draait om een kristal (TCXO - temperature compensated crystal oscillator) met een heel precies bekende eigen frequentie, waardoor de klok vrij nauwkeurig de tijd kan volgen. In dit voorbeeld gaan we uit van een populaire module, de DS3231, die voor minder dan €3,00 te koop wordt aangeboden. De specs hiervan geven een maximale afwijking van 0.5 seconde per dag. De module beschikt over een I2C bus waarmee hij met Arduino kan praten. Een andere prettige eigenschap is dat deze module zowel op 3.3V kan werken als op 5V. Het ding heeft ook nog een temperatuurmeting aan boord, maar die heeft slechts een nauwkeurigheid van ±3˚C en dient vooral voor de interne temperatuurcompensatie.
|
| Een DS3231 RTC module |
De schakeling
Extra benodigdheden:
- 1 DS3231 module
|
| Schakeling voor een RTC module voor een Arduino UNO |
De SDA aansluiting van de module moet aan analoge pin A4, terwijl de SCL aansluiting naar analoge pin A5 moet. De keuze van deze pin connecties is niet vrij; de I2C Wire interface vereist deze connecties. Het programma bevat dan ook geen regels om de pin-connecties in te stellen. Pas wel op: op een Arduino Mega gaat het voor de SDA om pin 20 en voor de SCL aansluiting om pin 21. Voor andere Arduino's kun je de juiste aansluitingen hier opzoeken.
Overigens lijkt de afgebeelde RTC module geen batterij te hebben en heeft hij een dubbele set aansluitingen. De aansluitingen die we nodig hebben zijn VCC (rood, 5 Volt), GND (zwart, 0 Volt), SCL (groen) en SDA (oranje). Eventuele andere aansluitingen op de module kunnen we negeren.
Het programma
#include "Wire.h"
#define DS3231_I2C_ADDRESS 0x68 // vervangt "DS3231_I2C_ADDRESS" steeds door de byte "0x68"
char DayOfWeek[][4] = {"Mon","Tue","Wed","Thu","Fri","Sat","Sun"}; // DayOfWeek[0]=maandag en DayOfWeek[6]=zondag
struct DateTime {
byte year;
byte month;
byte day;
byte hour;
byte minute;
byte second;
byte dayOfWeek;
};
byte decToBcd(byte val)
// Convert normal decimal numbers to binary coded decimal
{
return( (val/10*16) + (val%10) );
}
byte bcdToDec(byte val)
// Convert binary coded decimal to normal decimal numbers
{
return( (val/16*10) + (val%16) );
}
void setDS3231time(DateTime dt)
{
// sets time and date data to DS3231
Wire.beginTransmission(DS3231_I2C_ADDRESS);
Wire.write(0); // set next input to start at the seconds register
Wire.write(decToBcd(dt.second)); // set seconds
Wire.write(decToBcd(dt.minute)); // set minutes
Wire.write(decToBcd(dt.hour)); // set hours
Wire.write(decToBcd(dt.dayOfWeek)); // set day of week (1=Sunday, 7=Saturday)
Wire.write(decToBcd(dt.day)); // set date (1 to 31)
Wire.write(decToBcd(dt.month)); // set month
Wire.write(decToBcd(dt.year)); // set year (0 to 99)
Wire.endTransmission();
}
void readDS3231time(DateTime &dt) // dt by reference
{
Wire.beginTransmission(DS3231_I2C_ADDRESS);
Wire.write(0); // set DS3231 register pointer to 00h
Wire.endTransmission();
Wire.requestFrom(DS3231_I2C_ADDRESS, 7);
// request seven bytes of data from DS3231 starting from register 00h
dt.second = bcdToDec(Wire.read() & 0x7f);
dt.minute = bcdToDec(Wire.read());
dt.hour = bcdToDec(Wire.read() & 0x3f);
dt.dayOfWeek = bcdToDec(Wire.read());
dt.day = bcdToDec(Wire.read());
dt.month = bcdToDec(Wire.read());
dt.year = bcdToDec(Wire.read());
}
void displayDateTime(DateTime dt)
{
Serial.print(DayOfWeek[dt.dayOfWeek]);
Serial.print(' ');
if (dt.day<10) Serial.print('0');
Serial.print(dt.day, DEC);
Serial.print('-');
if (dt.month<10) Serial.print('0');
Serial.print(dt.month, DEC);
Serial.print("-20"); // "year" bestaat uit slechts een of twee decimalen
if (dt.year<10) Serial.print('0');
Serial.print(dt.year, DEC);
Serial.print(' ');
if (dt.hour<10) Serial.print('0');
Serial.print(dt.hour, DEC);
Serial.print(':');
if (dt.minute<10) Serial.print('0');
Serial.print(dt.minute, DEC);
Serial.print(':');
if (dt.second<10) Serial.print('0');
Serial.print(dt.second, DEC);
Serial.println();
}
void setup()
{
Wire.begin();
Serial.begin(9600);
// als nodig stel je hier de datum en tijd opnieuw in
// DateTime dt;
// dt.year=17; // = 2017
// dt.month=6;
// dt.day=8;
// dt.hour=10;
// dt.minute=3;
// dt.second=0;
// dt.dayOfWeek=3; // 3=donderdag
// setDS3231time(dt);
}
void loop()
{
DateTime dt;
readDS3231time(dt);
displayDateTime(dt);
delay(1000); // elke seconde
}
Uitleg
Het programma gebruikt weer de library Wire.h die het I2C protocol implementeerd. De tweede regel legt het adres vast van de DS3231 module op de I2C bus. Dat adres is 0x68 maar in de code kan DS3231_I2C_ADDRESS worden gebruikt.
#define DS3231_I2C_ADDRESS 0x68
Deze #define instructie zorgt er in feite voor dat overal waar DS3231_I2C_ADDRESS staat 0x68 wordt gelezen. Overigens is de schrijfwijze 0x68 een hexadecimale representatie van de byte. De decimale waarde ervan is 104.
Vervolgens wordt een lijst met strings gedefinieerd waarin we de namen van de weekdagen opslaan. Het is handig om dit in een lijst te hebben omdat we later nummers van weekdagen moeten vertalen naar hun respectievelijke namen.
Dan volgt er een stukje waarin een struct wordt gedefinieerd. Een struct is een variabele type dat meerdere elementen kan bevatten. Hier gaat het om een struct die we DateTime noemen, met daarin 7 bytes die we de namen geven year, month, day, hour, minute, second en dayOfWeek. Elke variabele van het type DateTime bevat vanaf nu 7 bytes die via hun namen beschikbaar zijn.
Er volgen twee functies voor het omzetten van bytes van Bcd naar decimale codering en andersom. Dit heeft te maken met enigszins afwijkende de wijze waarop de gegevens van en naar de clock module worden gestuurd: Bcd format.
De functie setDS3231time kan gebruikt worden om de datum en tijd in te stellen op de klok-module. Dit hoeft maar zelden te gebeuren, maar als je een nieuwe module hebt is het vaak wel nodig. Merk op dat deze functie een parameter mee krijgt van het type DateTime. De werking van de functie is alsvolgt. Via de I2C verbinding wordt een transmissie gestart. Daarna wordt er een '0' weggeschreven. Dit zorgt ervoor dat de module de gegevens die hij gaat ontvangen wegschrijft op de juikste plek (beginnend vanaf positie 0). Daarna worden de elementen van datum en tijd in de juiste volgorde weggeschreven naar de module. Kennelijk staat intern in de DS3231 seconde op positie 0, minute op positie 1, en zo voort. Als alles is opgestuurd wordt de transmissie afgesloten.
De functie readDS3231time heeft ook een parameter van het type DateTime. Bij de parameternaam dt staat nu echter het & teken. Dit wijst erop dat de parameter by reference wordt doorgegeven: bij het doorgeven wordt geen kopie gemaakt van de waarde van de variabele (dat is standaard) maar wordt de variabele zelf doorgegeven. Daardoor kan de functie de inhoud van de variabele aanpassen en dat is natuurlijk wat deze read functie moet doen. De functie begint met een write opdracht binnen een transmissie. Dit zorgt ervoor dat de read opdrachten die volgen beginnen op positie 0. De eerste byte staat dan voor second. Zonder die eerste write is het onduidelijk welke byte de module zal doorgeven en krijg je dus rommel. De feitelijke leesopdracht is:
Wire.requestFrom(DS3231_I2C_ADDRESS, 7);
Hierbij worden 7 bytes gelezen (daarvoor is de parameter 7). Deze worden in de Wire bibliotheek opgeslagen en zijn beschikbaar om uit te lezen. Dat uitlezen gebeurt vervolgens in de 7 volgende statements.
De functie displayDateTime() verwerkt de opgegeven datum en schrijft die naar de Serial Monitor. Daarbij wordt ervoor gezorgt dat alle getallen met twee cijfers worden weggeschreven. 3 januarie 2001 wordt dan 03-01-2001 en niet 3-1-201. Merk op dat deze functie de lijst met weekdagen gebruikt om het dagnummer dayOfWeek om te zetten in een tekst.
De setup() functie hoeft geen pin modi in te stellen omdat de I2C bus altijd via de A4 en A5 pins lopen (op een Arduino UNO). Het enige dat hier hoeft te gebeuren is het starten van de I2C verbinding en de normale seriele verbinding.
Wire.begin(); Serial.begin(9600);
Verder kun je hier de datum en tijd van de module opnieuw instellen. Als die al goed staat moet je dit niet doen natuurlijk, want elke keer dat je programma start wordt de setup() doorlopen en wordt de datum/tijd opnieuw ingesteld op hetgeen je hier opgeeft.
Ook de loop() functie is nu erg eenvoudig. De datum/tijd wordt uitgelezen uit de module middels de readDS3231time() functie, waarna die wordt weggeschreven naar de Serial Monitor via displayDateTime(). Dan wacht de functie een seconde voor het zich kan herhalen.
Beetje spelen
Mochten datum en tijd verkeerd zijn ingesteld, en die kans is natuurlijk vrij groot, dan kun je juiste instellingen voor datum en tijd in de setup() zetten en de code daarvoor activeren (door de "//" ervoor weg te halen). Let op dat je dit stuk van de code zo precies mogelijk op het juiste moment uitvoert en vooral dat je ervoor waakt de code later niet te herstarten. Elke keer als setup() wordt uitgevoerd wordt de datum en tijd van de module namelijk weer teruggezet naar hetgeen in het programma staat. Om dat te voorkomen kun je het beste de betreffende regels, meteen na het succesvol uitvoeren ervan, weer veranderen in commentaar en de code opnieuw naar je Arduino sturen. Vanaf dat moment zou de RTC datum en tijd correct moeten onthouden en mee laten lopen met de echte tijd. Het is tenslotte een Real Time Clock. Desnoods breidt je je project uit met een knop waarmee datum en tijd wordt ingesteld. Dan time je met een goede klok het moment om op de knop te drukken. De uitdaging is om de klok op de seconde af correct ingesteld te krijgen.

