Publicidade:

terça-feira, 16 de agosto de 2016

Arduino - Data e hora

Nativamente, o Arduino não possui nenhum recurso que permita nos dar a data e hora atuais, apesar de possuir recursos para a contagem de tempo, como a função millis e a função micros, onde a primeira retorna a quantidade de milissegundos que se passaram desde que a placa foi ligada ou resetada, enquanto a segunda a quantidade de microssegundos. Mas essa informação por si só não é capaz de nos dizer se hoje é terça ou quinta, se é abril ou setembro ou se estamos em 1970 ou em 2016.

Para obtermos data e horas atuais no Arduino precisamos contar com algum recurso externo, como por exemplo, um módulo RTC ou através de um servidor de data e hora na internet.

Existem diversas formas de conseguir obter a data e hora atual, a forma mais viável vai depender do projeto em si. Nesse artigo, vamos ver sobre algumas das estratégias que podemos utilizar.


  • Módulo RTC
  • Servidor de Data e Hora
  • Entrada através da Serial
  • Entrada Manual
  • Data e Hora de compilação


RTC

Contar com o um módulo RTC é uma boa saída para obtermos data e hora em nossas aplicações, há alguns modelos disponíveis no mercado, os quais contam com uma bateria de 3V, de modo que, mesmo sem energia na placa, o próprio módulo consegue se manter.

Servidor de Data e Hora

Caso a aplicação conte com algum meio de acesso a internet, ou mesmo acesso a uma rede interna, uma boa saída é acessar um servidor NTP. Nesse artigo falo um pouco como funciona esse recurso:
http://fabianoallex.blogspot.com.br/2015/08/arduino-ntp-acessando-servidor-de-hora.html

É preciso ficar atento ao fato de que o servidor de data e hora pode não retornar a hora no mesmo fuso-horário de onde a aplicação estiver rodando, ficando a cargo do programador fazer os ajustes necessários, além disso, alguns servidores também não retornam a data no horário de verão.

Serial

Um meio simples e eficiente de sincronizarmos a data e hora do Arduino é através da comunicação serial, caso o arduino esteja conectado a outros arduinos ou computadores que possam fornecer a data e hora. Esse tipo de solução pode ser utilizado em aplicações distribuídas, onde um Arduino tem acesso a data e hora, e consegue repassar essa informação através de uma comunicação serial. Por exemplo, se existe um Arduino com um módulo RTC o qual se conecta com outros Arduinos, não tem porque cada um dos Arduinos possuirem um módulo. Basta que o Arduino com o módulo RTC repasse a data e hora atual para os demais.

Manual

Caso não seja possível para uma aplicação contar com nenhum outro meio de obter a data e hora, ainda existe a possibilidade do próprio usuário da aplicação informar a data e hora atual. A partir do momento que o Arduino sabe qual é a data e hora atual, ele é capaz de se manter atualizado enquanto estiver energizado ou não for resetado. Obviamente não é o meio mais confiável de se obter data e hora, mas pode ser a única solução dependendo da aplicação. O único inconveniente, é que será necessário ter uma interface onde o usuário possa dar a entrada manual. Se durante a execução, existir o risco da aplicação ser reiniciada, uma saída paliativa para conseguir recuperar a data e hora, seria gravar a cada tantos segundos a data atual na eeprom (ou outro meio disponível), e caso reiniciar a aplicação, a mesma iniciaria com a última data gravada. O inconveniente deste método, é que cada vez que a aplicação reiniciar, a precisão da hora será perdida. E caso o meio utilizado pra gravar a informação for a eeprom, é preciso ficar atento ao limite de vezes que uma eeprom pode ser gravada, antes de acabar sua vida útil.

Data e Hora de compilação

Outra opção, que é mais aconselhável para ser usada durante a programação, é utilizar a data e hora de compilação do arquivo executável. Ela facilita o programador ter uma forma de testar seu código, sem ter necessariamente que contar com algum recurso externo. Essa forma de obtenção de data e hora só não é viável para se utilizar em produção, pois sempre que a aplicação for resetada, a data inicial será a mesma, então se uma aplicação foi compilada ha dois meses, ao reiniciar a aplicação, a data será a data de dois meses atrás.

Outros

Ainda há outros modos de se obter a data e hora, como por exemplo, através de um módulo de GPS ou GSM, caso a aplicação utilize-os.


No exemplo abaixo mostro um exemplo que sincroniza a data e hora com um módulo RTC, com um servidor NTP, com a entrada serial e com a data e hora de compilação do executável.

Código-fonte:

Inicialmente foi criado uma struct chamada Datetime, para que possamos ter os campos da data separados, o que facilita nosso entendimento. E também foi definido o tipo time_t, que na verdade é um long, de 4 bytes. Esse tipo na verdade é o chamado Unix Time, que nada mais é que a quantidade de segundos que se passaram desde o dia 01/01/1970. Ter a hora armazenada em um tipo de dado único facilita diversas operações com datas, como soma e subtração, por isso, a definição e utilização do tipo de dados time_t.

Como há dois tipos de dados, Datetime (que é uma struct) e time_t (que é um long), sendo que ambos representam a mesma coisa, foram criadas duas funções que converte a data de um tipo para o outro, sendo elas:

unixTimeToDatetime e datetimeToUnixTime

Com essas duas funções, fica fácil alternar entre um e outro tipo de dados. As Demais classes e funções utilizadas para tratar as datas se baseiam nesses tipos de dados definidos e nessas duas funções de conversões.

Para controlar a data atual do Arduino, foi criada uma classe chamada ArduinoDatetime. Essa classe é responsável por nos informar a data e hora atual do Arduino. Mas para isso, antes precisamos informar a data inicial. Isso é feito através do método Sync. Depois disso podemos obter a data e hora atuais através das funções getDatetime(), que retorna a data atual no formato Datetime, e através na função now() que retorna a data e hora atual no formato time_t.

Essa classe armazena duas informações: o millis no momento da sincronização e a data e hora daquele momento. Para a classe retornar a data atual, basta que classe calcule quantos segundos se passaram desde a última sincronização e somar esse valor à data e hora do momento de sincronização.

A princípio, é necessário apenas uma única sincronização para que o Arduino possa nos dar a data e hora atual, mas é aconselhável, dentro de um certo período de tempo, refazer a sincronização. Isso porque o clock do Arduino pode ter algum atraso durante longos períodos de tempo.

Veja que a classe em si não é baseada em nenhum meio específico de obtenção da data e hora, ela apenas depende de uma prévia sincronização de data e hora, sem se preocupar com a origem da data. Mais abaixo no código, temos as diferentes funções que nos dão data e hora com diferentes origens: vindo do módulo DS3231, via NTP, via Serial e ainda da data e hora da compilação. Esses são apenas alguns exemplos. É perfeitamente possível que outras funções sejam incluídas.

No loop, o código fica alternando a sincronização da data com as diversas funções que criamos que podem nos retornar a data inicial. Obviamente que em uma aplicação não será necessário ter mais de uma. O que foi feito aqui é apenas para demonstrar as possibilidades.



#include "Wire.h"
#include <LiquidCrystal_I2C.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include "Dns.h"


LiquidCrystal_I2C lcd(0x3F,20,4);  // set the LCD address to 0x27 for a 16 chars and 2 line display

struct Datetime {                 //sizeof(Datetime) ==>  5 bytes --- sem bitfields teria 8 bytes
  byte second       : 6;          //0..63
  byte minute       : 6;          //0..63
  byte hour         : 5;          //0..31
  byte dayOfWeek    : 3;          //0..7
  byte dayOfMonth   : 5;          //0..31
  byte month        : 4;          //0..15
  unsigned int year : 11;         //0..2047
};

typedef unsigned long time_t;          //variavel de 32 bits. para maior precisão (datas acima do ano de 2038. utilizar a linha abaixo.)
//typedef unsigned long long time_t;   //variavel de 64 bits


/****************************************************************************************
************************************ETHERNET CONFIG/FUNCTIONS****************************
*****************************************************************************************
se for rodar numa rede com firewall, verifique se os ip utilizado está liberado.
Servidores da NTP.br
a.st1.ntp.br 200.160.7.186 e 2001:12ff:0:7::186
b.st1.ntp.br 201.49.148.135
c.st1.ntp.br 200.186.125.195
d.st1.ntp.br 200.20.186.76
a.ntp.br 200.160.0.8 e 2001:12ff::8
b.ntp.br 200.189.40.8
c.ntp.br 200.192.232.8
****************************************************************************************/
byte mac[] = {  0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD};
unsigned int localPort = 8888;           // local port to listen for UDP packets
IPAddress timeServer(200, 192, 232, 8);  // time.nist.gov NTP server (fallback) - segunda tentativa caso a primeira de erro
const int NTP_PACKET_SIZE = 48;         // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE];   // buffer to hold incoming and outgoing packets 
 
//host para a primeira tentativa
const char* host = "a.st1.ntp.br";  // servidor da NTP.br - ver lista acima para todos os servidores da NTP.br
//const char* host = "192.168.200.254";  // servidor interno 01 - caso tenha um servidor de hora interno, pode ser configurado o nome ou ip na variavel host
 
EthernetUDP Udp;
DNSClient Dns;
IPAddress rem_add;
 
unsigned long sendNTPpacket(IPAddress& address) {           // send an NTP request to the time server at the given address 
  memset(packetBuffer, 0, NTP_PACKET_SIZE);                 // set all bytes in the buffer to 0
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode      // Initialize values needed to form NTP request (see URL above for details on the packets)
  packetBuffer[1] = 0;            // Stratum, or type of clock
  packetBuffer[2] = 6;            // Polling Interval
  packetBuffer[3] = 0xEC;         // Peer Clock Precision
  packetBuffer[12] = 49;          // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;
  Udp.beginPacket(address, 123); //NTP requests are to port 123                           // all NTP fields have been given values, now you can send a packet requesting a timestamp: 
  Udp.write(packetBuffer,NTP_PACKET_SIZE);
  Udp.endPacket(); 
}
/****************************************************************************************
************************************FIM ETHERNET CONFIG/FUNCTIONS************************
****************************************************************************************/

/****************************************************************************************
****************************DATA/HORA*****************************************************
****************************************************************************************/
#define LEAP_YEAR(_year) ((_year%4)==0)
static byte monthDays[] = {31, 28, 31, 30 , 31, 30, 31, 31, 30, 31, 30, 31};
#define DS3231_I2C_ADDRESS 0x68
enum WeekDay {WD_SUNDAY=0, WD_MONDAY, WD_TUESDAY, WD_WEDNESDAYM, WD_THURSDAY, WD_FRIDEY, WD_SATURDAY};
 
Datetime unixTimeToDatetime(time_t timep) {
  Datetime dt = {0,0,0,0,0,0,0};
  unsigned long long epoch = timep;
  byte year, month, monthLength;
  unsigned long days;
  dt.second  =  epoch % 60;
  epoch  /= 60; // now it is minutes
  dt.minute  =  epoch % 60;
  epoch  /= 60; // now it is hours
  dt.hour =  epoch % 24;
  epoch  /= 24; // now it is days
  dt.dayOfWeek =  (epoch+4) % 7;
  year = 70;  
  days = 0;
  while((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= epoch) { year++; }
  dt.year = year; // is returned as years from 1900
  days  -= LEAP_YEAR(year) ? 366 : 365;
  epoch -= days; // now it is days in this year, starting at 0
  for (month=0; month<12; month++) {
    monthLength = ( (month==1) && LEAP_YEAR(year) ) ? 29 : monthDays[month];  // month==1 -> february
    if (epoch >= monthLength) { epoch -= monthLength; } else { break; }
  }
  dt.month = month+1;  // jan is month 0
  dt.dayOfMonth = epoch+1;  // day of month
  dt.year += 1900;
  return dt;
}

time_t datetimeToUnixTime(Datetime dt) {                                                                           // note year argument is full four digit year (or digits since 2000), i.e.1975, (year 8 is 2008)
  time_t seconds;
  if(dt.year < 69) { dt.year += 2000; }                                                                                   // seconds from 1970 till 1 jan 00:00:00 this year
  seconds = (dt.year-1970)*(60*60*24L*365);
  for (int i=1970; i<dt.year;  i++)   { if (LEAP_YEAR(i)) { seconds += 60*60*24L; } }                                       // add extra days for leap years
  for (int i=0;    i<dt.month-1; i++) { seconds += (i==1 && LEAP_YEAR(dt.year)) ? 60*60*24L*29 : 60*60*24L*monthDays[i]; }  // add days for this year
  seconds += (dt.dayOfMonth-1)*3600*24L   +   dt.hour*3600L    +    dt.minute*60L    +    dt.second;
  return seconds; 
}

class DS3231{
  private:
    static byte decToBcd(byte val) { return( (val/10*16) + (val%10) ); }  // Convert normal decimal numbers to binary coded decimal
    static byte bcdToDec(byte val) { return( (val/16*10) + (val%16) ); }  // Convert binary coded decimal to normal decimal numbers
  public:
    static void sync(time_t t)    { sync(unixTimeToDatetime(t)); }
    static void sync(Datetime dt) {
      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+1));// set day of week (1=Sunday, 7=Saturday)
      Wire.write(decToBcd(dt.dayOfMonth)); // set date (1 to 31)
      Wire.write(decToBcd(dt.month));      // set month
      Wire.write(2000-decToBcd(dt.year));  // set year (0 to 99)
      Wire.endTransmission();
    }
    static Datetime getDatetime() {
      Datetime dt;
      Wire.beginTransmission(DS3231_I2C_ADDRESS);
      Wire.write(0);                               // set DS3231 register pointer to 00h
      Wire.endTransmission();
      Wire.requestFrom(DS3231_I2C_ADDRESS, 7); 
      dt.second     = bcdToDec(Wire.read() & 0x7f);  // request seven bytes of data from DS3231 starting from register 00h
      dt.minute     = bcdToDec(Wire.read());
      dt.hour       = bcdToDec(Wire.read() & 0x3f);
      dt.dayOfWeek  = bcdToDec(Wire.read())-1;
      dt.dayOfMonth = bcdToDec(Wire.read());
      dt.month      = bcdToDec(Wire.read());
      dt.year       = bcdToDec(Wire.read())+2000;
      return dt;
    }
    static time_t now(){ return datetimeToUnixTime(getDatetime()); }
};

class ArduinoDatetime {
  private:
    static unsigned long _mRef;  //millis de referencia na hora do sincronismo
    static time_t        _time;  //data e hora no momento do último sincronismo
  public:
    static void sync(Datetime dt) { sync(datetimeToUnixTime(dt)); }
    static void sync(time_t t)    { _mRef = millis(); _time = t; }
    static time_t now()           { return _time + (millis() - _mRef)/1000; }
    static Datetime getDatetime() { return unixTimeToDatetime(now()) ;}
};
unsigned long ArduinoDatetime::_mRef = 0;
time_t ArduinoDatetime::_time = 0;

//retorna a data e hora que o programa foi compilado
Datetime compiledDatetime() {
  Datetime dt;
  char s_month[5];
  int day, hour, minute, second, year;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
  sscanf(__DATE__, "%s %d %d", s_month, &day, &year);
  sscanf(__TIME__, "%2d %*c %2d %*c %2d", &hour, &minute, &second);
  dt.second = second;
  dt.minute = minute;
  dt.hour = hour;
  dt.dayOfMonth = day;
  dt.month = (strstr(month_names, s_month) - month_names) / 3 + 1;
  dt.year = year;
  return dt;
}

//retorna a data e hora de um servidor ntp (sem fuso-horário)
Datetime NTPDatetime(){
  if(Dns.getHostByName(host, rem_add) == 1 ){
    Serial.println("DNS resolve...");  
    Serial.print(host);
    Serial.print(" = ");
    Serial.println(rem_add);
    sendNTPpacket(rem_add);
  } else {
    Serial.println("DNS fail...");
    Serial.print("time.nist.gov = ");
    Serial.println(timeServer); // caso a primeira tentativa não retorne um host válido
    sendNTPpacket(timeServer);  // send an NTP packet to a time server
  }
  delay(1000); //aguarda um segundo, para receber os dados enviados.
  if ( Udp.parsePacket() ) {  
    Udp.read(packetBuffer, NTP_PACKET_SIZE);                              // We've received a packet, read the data from it. read the packet into the buffer
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);    // the timestamp starts at byte 40 of the received packet and is four bytes or two words, long. First, esxtract the two words:
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);  
    unsigned long secsSince1900 = highWord << 16 | lowWord;               // combine the four bytes (two words) into a long integer this is NTP time (seconds since Jan 1 1900):
    Serial.print("Segundos desde 1 de Jan. de 1900 = " );
    Serial.println(secsSince1900);     
    const unsigned long seventyYears = 2208988800UL;      // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
    unsigned long epoch = secsSince1900 - seventyYears;  //desconta 70 anos
    return unixTimeToDatetime(epoch);
  }
  return unixTimeToDatetime(0);  //01/01/1970 00:00:00
}

//---------------------funcoes para sincronia da data e hora da Classe ArduinoDatetime
void syncCompiledDatetime(){
  lcd.setCursor(0, 0);
  lcd.clear();
  lcd.print("Sinc pela compilacao");
  ArduinoDatetime::sync(compiledDatetime());
}

void syncDS3231(){
  lcd.setCursor(0, 0);
  lcd.clear();
  lcd.print("Sinc. pelo rtc");
  ArduinoDatetime::sync(DS3231::getDatetime());
}

void syncNTP(){
  lcd.setCursor(0, 0);
  lcd.clear();
  lcd.print("Sinc via NTP");
  ArduinoDatetime::sync(NTPDatetime());
}

void syncSerial() {
  //formato    |DATETIME|DIA|MES|ANO|HORA|MINUTO|SEGUNDO|
  while (Serial.available() ) {
    String str = Serial.readStringUntil('|');
    if (str == F("DATETIME")) {

      lcd.setCursor(0, 0);
      lcd.clear();
      lcd.print("Sinc. pela serial");
      
      Datetime dt;
      dt.dayOfMonth = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.month      = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.year       = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.hour       = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.minute     = Serial.parseInt();
                      Serial.readStringUntil('|');
      dt.second     = Serial.parseInt();
                      Serial.readStringUntil('|');

      ArduinoDatetime::sync(dt);
    }
  }
}

String zero(int a){ if(a>=10) {return (String)a+"";} else { return "0"+(String)a;} }

/****************************************************************************************
****************************FIM DATA/HORA************************************************
****************************************************************************************/

void setup() {
  Serial.begin(9600);
  Wire.begin();
  lcd.init();                      // initialize the lcd 
  lcd.backlight();


  pinMode(4, OUTPUT);      //ANTES DE INICIAR O ETHERNET, DESABILITA O SDCARD
  digitalWrite(4, HIGH);
  if (Ethernet.begin(mac) == 0) { Serial.println("Failed to configure Ethernet using DHCP"); }
  Udp.begin(localPort);
  Dns.begin(Ethernet.dnsServerIP() );

  
  syncNTP();
}

void loop() {

  syncSerial();   //aguarda a entrada de dados via serial pra configurar a data.
  
  Datetime dt = ArduinoDatetime::getDatetime();

  lcd.setCursor(0, 1);
  lcd.print(zero(dt.dayOfMonth));
  lcd.print("/");
  lcd.print(zero(dt.month));
  lcd.print("/");
  lcd.print(dt.year);
  lcd.print(" ");
  lcd.print(zero(dt.hour));
  lcd.print(":");
  lcd.print(zero(dt.minute));
  lcd.print(":");
  lcd.print(zero(dt.second));
  delay(999);

  static unsigned long m = 0;
  static int cont = 0;
  if (millis() - m > 10000){
    m = millis();

    if (cont == 0){ syncDS3231(); }
    if (cont == 1){ syncCompiledDatetime(); }
    if (cont == 2){ syncNTP(); }
    cont++;
    if (cont == 3){ cont = 0; }
  }
}


Vídeo



2 comentários:

  1. Boa noite, Fabiano. Tudo bem?

    Meu nome é Bruno Jordão e gostaria, se for possível, que você me ajudasse.

    Estou desenvolvendo um projeto de um relógio com arduino para minha tese de mestrado, mas tenho que trocar a base de tempo do cristal de 16MHz por uma base de 10MHz de um relógio atômico. Como faço para mudar o FUSE do arduino e programar com essa nova base de tempo?

    ResponderExcluir
  2. Desde já quero parabenizar pelos vídeos que são muito interessantes.

    Fique com DEUS.

    ResponderExcluir