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(decToBcd( (byte)(dt.year-2000) ));  // set year (0 to 99)
      //Wire.write(2000-decToBcd(dt.year));  // set year (0 to 99)  --> substituido pela linha acima corrigido 07/06/201
      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



4 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
  3. Este comentário foi removido pelo autor.

    ResponderExcluir
  4. #define LEAP_YEAR(_year) ((_year%4)==0)


    //variaveis globais
    static uint8_t ULTIMODIADOSMES[] = {31, 28, 31, 30 , 31, 30, 31, 31, 30, 31, 30, 31};
    uint8_t SEGUNDO ; //0..63
    uint8_t MINUTO ; //0..63
    uint8_t HORA ; //0..31
    uint8_t DIADASEMANA ; //0..7
    uint8_t DATA ; //0..31
    uint8_t MES ; //0..15
    uint16_t ANO ; //0..2047

    //variaveis globais

    int UNIXT_TO_DATE(uint64_t EPOCH)
    {
    uint8_t MONTH, MONTHLENGTH;
    uint32_t DAYS, YEAR;
    SEGUNDO = 0;
    MINUTO = 0;
    HORA = 0;
    DIADASEMANA = 0;
    DATA = 0;
    MES = 0;
    ANO = 0;
    YEAR = 70;
    DAYS = 0;

    SEGUNDO = EPOCH % 60;
    EPOCH /= 60; // now it is minutes
    MINUTO = EPOCH % 60;
    EPOCH /= 60; // now it is hours
    HORA = EPOCH % 24;
    EPOCH /= 24; // now it is days
    DIADASEMANA = ((EPOCH + 4) % 7)+1;

    while((uint32_t)(DAYS += (LEAP_YEAR(YEAR) ? 366 : 365)) <= EPOCH)
    {
    YEAR++;
    } // ** Acredito que o erro esteja ness laço, mas nao consigo ver *

    ANO = 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 : ULTIMODIADOSMES[MES]; // month==1 -> february
    if (EPOCH >= MONTHLENGTH)
    {
    EPOCH -= MONTHLENGTH;
    }
    else
    {
    break;
    }
    }
    MES = MONTH + 1; // jan is month 0
    DATA = EPOCH + 1; // day of month
    //ANO += 1900;

    }

    ResponderExcluir