Arduino : créer une horloge synchronisée

Dans cet article, nous allons voir comment afficher sur un écran la date et l'heure courantes, avec la possibilité de se synchroniser sur un serveur de temps public (NTP - Network Time Protocol), comme le font tous les PC ou téléphones.

Matériel

Pour cet exemple, il faut vous munir :

  • d'un arduino (ici un DUE)
  • un shield Ethernet
  • un module RTC (Real Time Clock, ici un Gravitech)
  • un écran LCD

Voici un schéma du montage :

L'écran est branché, de manière classique, sur des broches numériques. Le module RTC quant à lui est intégré sur le bus I2C par les broches SDA1 et SCL1.

Logiciel

Nous allons utiliser les librairies :

  • Ethernet
  • Wire1 (pour le bus I2C)
  • LiquidCrystal
  • Time (pour faciliter la décomposition de la date à partir d'un timestamp)

Données importantes :

Selon la spécification du constructeur, l'adresse de communication du module RTC sur le bus I2C est 0x68. Le serveur NTP choisi est celui de l'Observatoire de Paris (ntp-p1.obspm.fr, IP: 145.238.203.14). Le dialogue avec un serveur NTP se fait par échange de paquets par le protocole UDP.

C'est parti!

1. Configuration d'une date par défaut sur le module RTC

Commençons par un premier échange avec ce module :


const int I2C_address = 0x68;
...
Wire1.begin();        // join i2c bus (address optional for master)
Wire1.beginTransmission(I2C_address);// Début de transaction I2C
Wire1.write(0); // Arrête l'oscillateur
Wire1.write(0); // sec
Wire1.write(0x11); // min
Wire1.write(0x20); // heure
Wire1.write(6); // jour de la semaine
Wire1.write(0x20); // jour
Wire1.write(9); // mois
Wire1.write(0x14); // annee
Wire1.write(0); // Redémarre l'oscillateur
Wire1.endTransmission(); // Fin de transaction I2C

Ici nous définissons la date "20/09/2014 20:11:00". Remarquez que les valeurs transmises sont au format hexadécimal.

2. Lecture de la date sur le module RTC

Interrogeons notre module pour récupérer la date :


void getTime(char* tempData) {
 byte i = -1;
 byte error;
 
 Wire1.beginTransmission(I2C_address); // Début de transaction I2C
 Wire1.write(0); // Demande les info à partir de l'adresse 0 (soit toutes les info)
 error = Wire1.endTransmission(); // Fin de transaction I2C
 
 if(error==0) {
  Wire1.requestFrom(I2C_address, 7); // Récupère les info (7 octets = 7 valeurs correspondant à l'heure et à la date courante)
 
  while(Wire1.available())        
  {
   tempData[++i] = Wire1.read(); // receive a byte as character
  }
 } else {
  // le composant RTC n'etait pas joignable, on relance le bus I2C pour la prochaine fois
  Wire1.begin();
 }
}

Cette méthode nous permet de récupérer, dans un tableau de 7 char (tempData), les données du module.

3. Affichage de la date sur l'écran


void displayTime()
{
 char tempchar[7] = {'\0'};
 // recuperation de la date
 getTime(tempchar);
 // affichage
 if(tempchar[0]!='\0') {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(tempchar[4],HEX);
  lcd.write("/");
  lcd.print(tempchar[5],HEX);
  lcd.write("/20");
  lcd.print(tempchar[6],HEX);
  lcd.setCursor(0, 1);
  lcd.print(tempchar[2],HEX);
  lcd.write(":");
  lcd.print(tempchar[1],HEX);
  lcd.write(":");
  lcd.print(tempchar[0],HEX);
 }
}

Avec cette méthode, on envoie à l'écran chaque caractère (toujours au format hexadecimal) retourné par l'appel au module RTC.

Il ne vous reste plus qu'à placer l'appel à cette méthode dans une boucle toutes les secondes pour voir défiler le temps. Essayez même de couper l'alimentation du module RTC : l'affichage de l'heure se fige, jusqu'à nouvelle alimentation du module. Elle sera alors mise à jour avec la date qui a continué à défiler grâce à la pile du module.

4. Interrogation du serveur de temps

Le shield Ethernet va nous permettre de contacter le serveur public et de récupérer les informations de date "de référence".


IPAddress timeServer(145, 238, 203, 14); // ntp-p1.obspm.fr (IP: 145.238.203.14)
const int NTP_PACKET_SIZE= 48; // NTP time stamp is in the first 48 bytes of the message
uint8_t packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
/* Set this to the offset (in seconds) to your local time
   GMT - 2 */
const long timeZoneOffset = -7200L;  
// A UDP instance to let us send and receive packets over UDP
EthernetUDP udpClient;
EthernetClient client;

/**
* Démarrage des librairies
*/
Ethernet.begin(mac);
udpClient.begin(8888); // port UDP local


/**
* Envoi de la requete 
*/
// set all bytes in the buffer to 0
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
packetBuffer[0] = 0b11100011;   // LI, Version, Mode
packetBuffer[1] = 0;     // Stratum, or type of clock
packetBuffer[2] = 6;     // Polling Interval
packetBuffer[3] = 0xEC;  // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer[12]  = 49;
packetBuffer[13]  = 0x4E;
packetBuffer[14]  = 49;
packetBuffer[15]  = 52;

// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
udpClient.beginPacket(timeServer, 123); //NTP requests are to port 123
udpClient.write(packetBuffer,NTP_PACKET_SIZE);
udpClient.endPacket();


/**
* Parcours de la réponse du serveur
*/
unsigned long epoch = 0;
if ( udpClient.parsePacket() ) {
 // We've received a packet, read the data from it
 udpClient.read(packetBuffer,NTP_PACKET_SIZE);  // read the packet into the buffer

 //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 highWord = word(packetBuffer[40], packetBuffer[41]);
 unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
 // combine the four bytes (two words) into a long integer
 // this is NTP time (seconds since Jan 1 1900):
 unsigned long secsSince1900 = highWord << 16 | lowWord;

 // now convert NTP time into everyday time:
 // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
 const unsigned long seventyYears = 2208988800UL + timeZoneOffset;
 // subtract seventy years:
 epoch = secsSince1900 - seventyYears;
}


/**
* Configuration du module RTC avec la nouvelle date
*/
setTime(epoch); // on affecte le timestamp récupéré à la librairie Time
Wire1.beginTransmission(I2C_address);// Début de transaction I2C
Wire1.write(0); // Arrête l'oscillateur
String strval = String(second(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // sec
strval = String(minute(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // min
strval = String(hour(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // heure
Wire1.write(weekday()-1); // jour de la semaine
strval = String(day(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // jour
strval = String(month(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // mois
strval = String(year(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // annee
Wire1.write(0); // Redémarre l'oscillateur
Wire1.endTransmission(); // Fin de transaction I2C

La librairie Time nous permet de décomposer facilement les données de la date. Mais le serveur NTP nous a envoyé la date au format décimal "20/09/2014 20:11:00". Il faut donc convertir les données au format hexa avant de configurer le module RTC.

Ainsi, après l'appel à ce code (à découper en méthodes pour plus de clarté), l'affichage de la date sera automatiquement actualisé avec la date de référence.

Have fun!

Sources :


Fichier(s) joint(s) :



Utiliser les librairies Arduino dans Atmel Studio

Dans cet article je vais vous présenter comment démarrer un projet simple (exemple Blink) avec un Arduino Due, Atmel Studio 6.2 et les librairies du projet Arduino.

Pour commencer, vous pouvez suivre cet article très clair et très simple pour initier l'environnement : http://www.engblaze.com/tutorial-using-atmel-studio-6-with-arduino-projects/

Datant cependant de 2012, il mérite quelques mises à jour : il vous faudra télécharger la dernière version du software Arduino capable de gérer le Due. Les chemins vers les répertoires à inclure seront donc légèrement différents, puisque spécifiques à la plateforme (par ex ".\hardware\arduino\sam\variants\arduino_due_x"). Il faudra également ajouter dans le Linker un lien vers la librairie libsam_sam3x8e_gcc_rel située sous ".\hardware\arduino\sam\variants\arduino_due_x".

Il se peut que vous rencontriez des petites erreurs de compilation, que vous pourrez résoudre à la main en commentant/déplaçant du code.

Voici maintenant la nouvelle version de l'exemple Blink :


#include "sam.h"
#include "Arduino.h"


int led = 53;

/**
 * \brief SysTick_Handler.
 */
void SysTick_Handler(void)
{
 /* Increment counter necessary in delay(). */
 TimeTick_Increment();
}

/**
 * \brief Application entry point.
 *
 * \return Unused (ANSI-C compatibility).
 */
int main(void)
{
    /* Initialize the SAM system */
    SystemInit();
    SysTick_Config(SystemCoreClock / 1000); //1ms per interrupt
 
    pinMode(led, OUTPUT);

    while (1) 
    {
        digitalWrite(led, HIGH);   // turn the LED on (HIGH is the voltage level)
        delay(1000);               // wait for a second
        digitalWrite(led, LOW);    // turn the LED off by making the voltage LOW
        delay(1000);
    }
}

Pour faire fonctionner la méthode delay, il est nécessaire d'ajouter le code activant l'incrémentation du compteur systeme :


void SysTick_Handler(void)
{
 /* Increment counter necessary in delay(). */
 TimeTick_Increment();
}
...
SysTick_Config(SystemCoreClock / 1000); //1ms per interrupt

Ainsi, la méthode d'incrémentation TimeTick_Increment sera appelée par l'handler interne au processeur SysTick_Handler toutes les 1ms comme indiqué par SysTick_Config

Ne reste plus qu'à indiquer la plateforme cible à la compilation (processeur SAM3X8E) et lancer la programmation!

Edit : il est également possible d'appeler la méthode Arduino TimeTick_Configure(SystemCoreClock)


Fichier(s) joint(s) :