Arduino a obvod reálného času

Před nějakou dobou jsme si popsali obvod reálného času DS1307, který dokáže udržovat aktuální datum a čas a je napájen z baterie. V tomto článku si ukážeme jak RTC obvod použít s Arduinem. Samotné zapojení je velmi jednoduché, a proto se budeme více věnovat programu pro Arduino.

Základní zapojení obvodu

Na obrázku níže je schéma zapojení samotného obvodu DS1307, v dolní části pak naznačeno, ke kterým pinům Arduina je nutné obvod připojit (DS1307 komunikuje po standardní sběrnici I²C). Pokud máte DS1307 již na nějaké hotové desce (například já jsem zakoupil hotový obvod od SparkFun.com), stačí jen připojit napájení a dva I²C vodiče k arduinu.

Schéma zapojení obvodu RTC k desce Arduino Uno.
Schéma zapojení obvodu RTC k desce Arduino Uno.

Čtení obsahu paměti

Jakmile máme vše zapojeno, můžeme přečíst obsah paměti DS1307. V datasheetu nebo v minulém příspěvku se můžete dočíst, že obvod obsahuje celkem 63 bajtů paměti RAM. Spodních 8 bajtů je určeno pro datum a čas, zbytek může uživatel volně použít (to může být užitečné například pro uložení informace o letním/zimním čase, časové zóně, či nějaké důležité události).

Struktura vnitřní paměti (zdroj datasheet).
Struktura vnitřní paměti (zdroj datasheet).

Pojďme si pomocí Arduina přečíst obsah prvních 8 bajtů. Každý bajt je většinou rozdělen na dvě části. Spodní 4 bity určují jednotky a horní 4 (nebo méně) bitů určují desítky.

To znamená, že například hodnota 01001000 na adrese 0x01 není 72 minut, ale 48 minut (0100 — 4 a 1000 — 8). Obdobně je tomu i u ostatních hodnot.

Další důležité upozornění — aby obvod fungoval, je nutné vynulovat (nastaven na nulu) bit CH na adrese 0x00. V opačném případě obvod neběží a tím pádem neudržuje aktuální čas.

Arduino IDE 1.0
Zdrojové kódy pro Arduino jsem psal a odladil pro Arduino IDE 1.0. Pokud máte starší verzi, aktualizujte nebo si musíte příklady vhodně upravit.

Teď už bez dalšího zdržování ukážeme kód pro Arduino, který přečte prvních 8 bajtů paměti a pošle je přes UART do PC. Kód jsem napsal co nejvíce přímočaře, takže snad nepotřebuje žádné další komentáře.

#include <Wire.h>

// Adresa obvodu je napevno nastavená
// na hodnotu 0x68.
#define DS1307_ADDRESS 0x68

void setup() {
  Serial.begin(9600);
  Wire.begin();

  // Sem budeme ukládat příchozí data.
  uint8_t data[8];
  Wire.beginTransmission(DS1307_ADDRESS);
  Wire.write((uint8_t)0x00); // Adresy musí být typu uint8.
  Wire.endTransmission();

  Wire.requestFrom(DS1307_ADDRESS, 8);
  for(int i = 0; i < 8; i++)
    data[i] = Wire.read();

  // Pokud je bit CH vynulován, RTC běží.
  Serial.print("RTC running: ");
  Serial.println((data[0] >> 7) ? "No" : "Yes");

  // Spodní polovina bajtu jsou jednotky,
  // horní pak desítky.
  Serial.print("Seconds: ");
  Serial.print((data[0] & 0x70) >> 4, DEC);
  Serial.println(data[0] & 0x0F, DEC);

  Serial.print("Minutes: ");
  Serial.print(data[1] >> 4, DEC);
  Serial.println(data[1] & 0x0F, DEC);

  // Hodiny mohou být v 12 i 24 hodinovém formátu.
  // Lze mezi nimi volit nastavením šestého bitu na
  // adrese 0x02.
  if(data[2] >> 6) {
    // 12-hodinový formát
    Serial.print("Hours: ");
    Serial.print((data[2] & 0x1F) >> 4, DEC);
    Serial.print(data[2] & 0x0F, DEC);
    // Pátý bit určuje dopoledne/odpoledne.
    Serial.println(((data[2] & 0x20) >> 5) ? "PM" : "AM");
  } else {
    // 24-hodinový formát
    Serial.print("Hours: ");
    Serial.print((data[2] & 0x3F) >> 4, DEC);
    Serial.println(data[2] & 0x0F, DEC);
  }

  Serial.print("Day: ");
  Serial.println(data[3], DEC);

  Serial.print("Date: ");
  Serial.print(data[4] >> 4, DEC);
  Serial.println(data[4] & 0x0F, DEC);

  Serial.print("Month: ");
  Serial.print(data[5] >> 4, DEC);
  Serial.println(data[5] & 0x0F, DEC);

  Serial.print("Year: ");
  Serial.print(data[6] >> 4, DEC);
  Serial.println(data[6] & 0x0F, DEC);

  // Řídící registr. Určuje výstup pinu SQW/OUT.
  // Může být nastaven třeba na 0x10 a pak se na
  // zmíněném pinu objeví obdelníkový signál s
  // frekvencí 1Hz.
  Serial.print("Control: ");
  Serial.println(data[7], BIN);

}

void loop() { }

Pokud jste obvod ještě nespouštěli, může být obsah paměti libovolný, a proto je nejdříve nutné nastavit aktuální datum a čas. Ukažme si jednoduchou funkci, která nastaví obsah libovolného registru v paměti RTC:

/**
 * Funkce uloží hodnotu 'value' na adresu 'address'.
 *
 * Použití může být například:
 *  ds1307_set_register(0x06, 0x12);
 *    Nastaví rok na 2012.
 */
void ds1307_set_register(uint8_t address, uint8_t value) {
  Wire.beginTransmission(DS1307_ADDRESS);
  Wire.write(address);
  Wire.write(value);
  Wire.endTransmission();
}

Tím jsme si ukázali nízkoúrovňovou komunikaci s obvodem. Tento obvod je ale hodně oblíbený, a tak existuje několik již hotových knihoven, které usnadňují použití. Jednou z nich je například RTClib z webu ladyada.net (odkaz na samotnou knihovnu). S touto knihovnou je pak nastavení aktuálního data a času velmi jednoduché. Například:

#include <Wire.h>
#include <RTClib.h>

RTC_DS1307 RTC;

void setup () {
    Serial.begin(9600);
    Wire.begin();
    RTC.begin();

  // Datum a čas nastavíme, pouze pokud obvod neběží.
  if (! RTC.isrunning()) {
    Serial.println("RTC is NOT running!");
    // __DATE__ a __TIME__ jsou konstanty překladače, které
    // jsou nastaveny během překladu programu.
    RTC.adjust(DateTime(__DATE__, __TIME__));
  }
}

void loop() {
  // A budeme stále dokola tisknout aktuální čas.
  DateTime now = RTC.now();

  Serial.print(now.hour(), DEC);
  Serial.print(':');
  Serial.print(now.minute(), DEC);
  Serial.print(':');
  Serial.println(now.second(), DEC);

  delay(500);
}

Samotné Arduino poskytuje také knihovnu Time.h. Arduino samo o sobě sice nemá RTC, ale díky 16bitovému časovači je schopné udržovat si aktuální datum a čas (pouze dokud je připojené k napájení, po odpojení napájení se aktuální čas ztrácí). Je tedy možné po každém spuštění Arduina synchronizovat čas s externím RTC a Arduino si pak bude udržovat aktuální čas samo.

To lze provést například pomocí zmíněné knihovny Time. Pokud si ji stáhnete do adresáře s ostatními Arduino knihovnami, můžete ji použít následovně (předpokládám, že DS1307 je již připojen a nastaven):

#include <Time.h>
#include <Wire.h>
#include <DS1307RTC.h>

void setup()  {
  Serial.begin(9600);

  // Načteme datum a čas z externího RTC a nastavíme Arduino.
  setSyncProvider(RTC.get);
  if(timeStatus()!= timeSet)
     Serial.println("Unable to sync with the RTC");
  else
     Serial.println("RTC has set the system time");
}
void loop() { }

Pak už lze RTC od Arduina odpojit a používat funkce jako hour(), minute(), second(), day(), apod.

Přesnost

Přesnost obvodu do velké míry závisí na přesnosti použitého krystalu, teplotě okolí a napětí baterie. Experimentálně jsem ověřil, že se obvod za necelé tři měsíce opozdil o 286 vteřin. Desku jsem nechal ve skříni, napájenou pouze z malé baterie v cca 20°C.

Závěr

Hotové zapojení s DS1307 od Sparkfun.com.
Hotové zapojení s DS1307 od Sparkfun.com.

Ukázali jsme si, jak používat obvod reálného času DS1307 s Arduinem. Nejdříve jsme si popsali nízkoúrovňovou komunikaci přímo po I²C sběrnici, pak jsme využili existujících knihoven. V závěru jsme si ukázali i možnost udržovat čas pomocí vnitřního časovače Arduina a externí RTC použít jen pro jednorázovou synchronizaci.

Zde bych ještě upozornil na jednu nepříjemnost, která nemusí být na první pohled zřejmá. Arduino si aktuální datum a čas udržuje pomocí funkce millis(), která vrací počet milisekund od spuštění. Tato hodnota se aktualizuje při každém přetečení 16bitového časovače a je typu unsigned long, takže jednoduchým výpočtem zjistíme, že přeteče za přibližně 50 dní. Co se stane s aktuálním časem Arduina pak, netuším. Nevsázel bych ale na jeho přesnost po této době.