[APL] Une horloge

Horloge sur Arduino

L'objectif de ce projet était de réaliser, avec la carte Arduino, une horloge avec un écran LCD récupéré sur un fax hors-service.

Quelques précisions tout d'abord :

  • l'écran LCD que j'ai récupéré est un écran présentant 1 ligne de 16 caractères qui est décomposée « physiquement » en 2 lignes de 8 caractères côte à côte. Si vous voulez réaliser cette horloge ou vous inspirer de ce programme, adaptez si nécessaire le code à l'écran LCD que vous utiliserez ;
  • l'objectif était de réaliser une vraie horloge avec une précision correcte et c'est le cas, mais je n'ai inclus dans le code aucun réglage de précision prenant en compte la précision réelle du quartz, je suppose dans ce code que le quartz à une précision exacte de 16 MHz et je néglige le temps de RAZ du compteur, de branchement sur l'interruption, ? Petites approximations, mais le code pourrait inclure une correction plus précise encore ;
  • le code présenté demande une certaine connaissance de l'environnement de développement d'Arduino pour être mis en ?uvre, si vous n'êtes pas sûr de vous c'est à vos risques et périls que vous entreprendrez cette entreprise. Le risque c'est que vos autres programmes ne compilent plus et que vous ayez à ré-installer l'environnement de développement d'Arduino.

Les difficultés rencontrées

1 - l'environnement de développement Arduino ne gère pas les interruptions, ce qui est très ennuyeux pour générer des appels à des fonctions à intervalles très réguliers ;

2 - je n'ai pas réussi à utiliser les librairies comme « timer.c », ? directement dans le programme, j'ai donc du modifier certains fichiers de l'environnement de développement ;

3 - la routine d'interruption devant nécessairement être placée dans le fichier « wiring.c » j'ai donc du modifier certains fichiers de l'environnement de développement pour communiquer via une variable entre les déférentes fonctions du programme ;

4 - la taille du programme dépassait la taille mémoire disponible dans l'environnement de développement configuré par défaut, j'ai donc du modifier certains fichiers de l'environnement de développement pour réduire la taille totale du programme.

Brancher l'écran LCD

Pour connecter l'écran LCD à la carte, j'ai réalisé un ridicule connecteur avec des pattes de résistances soudées sur une plaque d'essais à bandes. J'en ai profité pour y fixé le potentiomètre de réglage du contraste.

Deux fils « volants » rouge et noir sont destinés à l'alimentation de l'écran (fil noir 0V, GND et fil rouge 5V, 5V).

Pour le brochage, voir la photo suivante :

Brancher les boutons

J'ai connecté 3 boutons poussoirs à aux broches 9, 10 et 11 pour pouvoir mettre à l'heure l'horloge. Lisez l'article ici [ http://www.arduino.cc/en/Tutorial/Pushbutton ] pour savoir comment connecter des boutons à la carte.

Commentaires sur le programme

Je vous passe l'écriture sur l'écran LCD vous trouverez de nombreuses explications un peu partout concernant le protocole de communication. Pour ma part, j'utilise dans cet exemple un écran LCD WM-C1601R-1TNNc [ www.gaw.ru/pdf/lcd/lcm/Wintek/char/WM-C1601M-1TNNc.pdf ] équipé d'un contrôleur ST7066U-0A [ ST7066 Datasheet(PDF) - List of Unclassifed Manufacturers ]. Le programme est très facilement adaptable pour un écran LCD équipé d'un contrôleur type HD44780 (peut être ferais-je un prochain billet sur ce sujet).

Le plus complexe et le plus intéressant, c'est la gestion du temps ! J'utilise donc le Timer1 présent dans l'ATMEGA8 qui gère habituellement les fonctions PWM de la carte. Il ne vous sera donc plus possible d'utiliser correctement la fonction analogWrite !

Comment obtenir un intervalle de temps régulier ? Et bien il faut utiliser une interruption interne qui va appeler une fonction à intervalle de temps fixe.

Pour ce faire, il faut activer l'interruption qui est déclenchée quand le compteur du Timer1, TCNT1, arrive à la valeur de comparaison stockée dans le registre de comparaison OCR1A. Il faut aussi indiquer quelle fonction sera appelée à chaque déclenchement de cette interruption (ici la fonction TicTac).
En clair, à chaque fois que le compteur TCNT1 incrémenté de 1 à chaque tic du Timer1 arrive à la valeur de OCR1A, la fonction TicTac est appelée.
C'est ce qui est réalisé dans la fonction initHorloge qui programme tout cela pour que la fonction TicTac soit appelée une fois par seconde.

Je vous passe la gestion de l'heure, je pense que la lecture du code est suffisante pour comprendre.

Mais voilà, il faut ruser ! ! !

1 - Les fonctions initHorloge et TicTac doivent être dans le fichier wiring.c pour pouvoir avoir accès aux fonctions de la librairie timer.c. Il vous faudra donc modifier ce fichier. Le fichier wiring.c se trouve dans le dossier « arduino-0004\lib\targets\arduino » :

Faites en une copie de sauvegarde avant de le modifier en ajoutant les fonctions initHorloge et TicTac et le code d'initialisation.

2 - Je n'ai pas trouvé d'autre moyen pour faire passer l'information du code du fichier wiring.c au programme Arduino classique que de déclarer une variable HORLOGE_secondes dans le fichier wiring.h. Cette variable et modifiée essentiellement dans la fonction TicTac et lue dans la fonction afficheHeure du programme classique. Il vous faudra donc aussi modifier le code du fichier wiring.h. La encore, faites en une sauvegarde avant de le modifier.

3 - Le code modifié dans les fichiers wiring.c et wiring.h est encadré par :
/--------------------------------------/
/* ---- MODIFICATIONS POUR HORLOGE ---- /
/
--------------------------------------/
et
/
--- FIN MODIFICATIONS POUR HORLOGE ---*/
pour l'identifier facilement.

4 - Tout irait pour le mieux si le code généré lors de la compilation ne dépassait pas la mémoire disponible sur le microcontrôleur ! ! ! J'ai donc du supprimer quelques morceaux de codes dans le fichier wiring.c pour que ça passe comme par exemple la fonction pulseIn.

Pour plus d'infos, je reste à votre disposition?

Le code principal

#define E  6 // broche où est connectée l'entrée E du LCD
#define RS 8 // broche où est connectée l'entrée RS du LCD
#define RW 7 // broche où est connectée l'entrée RW du LCD

#define DB4 5 // broche où est connectée l'entrée DB4 du LCD
#define DB5 4 // broche où est connectée l'entrée DB5 du LCD
#define DB6 3 // broche où est connectée l'entrée DB6 du LCD
#define DB7 2 // broche où est connectée l'entrée DB7 du LCD

#define E_MIN_HIGH_US 600 // temps minimum à l'état haut de l'horloge appliquée à la broche E
#define E_MIN_COMPLETE_US 1500 // temps minimum entre 2 commandes envoyée au LCD

#define BT_H 9 // broche où est connectée l'entrée E du LCD
#define BT_M 10 // broche où est connectée l'entrée E du LCD
#define BT_S 11 // broche où est connectée l'entrée E du LCD

char h=0; // contient les heures
char m=0; // " minutes
char s=0; // " secondes

// FONCTIONS GESTION LCD

void topE()
{
  digitalWrite(E, HIGH);
  delayMicroseconds(E_MIN_HIGH_US);
  digitalWrite(E, LOW);
  delayMicroseconds(E_MIN_COMPLETE_US - E_MIN_HIGH_US + 60); // + un poil de marge
}

void envoiOctet (unsigned char b)
{
  digitalWrite(RW, LOW); // passe en mode ecriture
  digitalWrite(E, LOW); // on s'assure d'être à E bas

  // bits de poids fort d'abord
  digitalWrite(E, HIGH);
  digitalWrite(DB7, b & 0x80); // 7
  digitalWrite(DB6, b & 0x40); // 6
  digitalWrite(DB5, b & 0x20); // 5
  digitalWrite(DB4, b & 0x10); // 4
  // top écriture
  topE();

  // bits de poids faible
  digitalWrite(E, HIGH);
  digitalWrite(DB7, b & 0x08); // 3
  digitalWrite(DB6, b & 0x04); // 2
  digitalWrite(DB5, b & 0x02); // 1
  digitalWrite(DB4, b & 0x01); // 0
  // top écriture
  topE();
}

void initDisplay ()
{
  // on s'assure d'être en mode ecriture commande
  digitalWrite(RS, LOW);
 
    // "function set" 8 bits
    digitalWrite(DB7, LOW);
    digitalWrite(DB6, LOW);
    digitalWrite(DB5, HIGH);
    digitalWrite(DB4, HIGH);
    topE();
    delayMicroseconds (50);
 
    // "function set" transmission de données sur 4 bits / 2 lignes / police 5*8 pixels
    envoiOctet (0x28);
    delayMicroseconds (50);
 
    // "function set" transmission de données sur 4 bits / 2 lignes / police 5*8 pixels
    envoiOctet (0x28);
    delayMicroseconds (50);
 
    // "function set" Ecran ACTIF / Curseur ACTIF / Clignotement curseur ACTIF
    envoiOctet (0x0F);
    delayMicroseconds (50);
 
    // "function set" Effecement écran
    envoiOctet (0x01);
    delay (5);

    // "entry mode set" Incrément / Déplacment
    envoiOctet (0x06);
    delay(5);
}

// FONCTIONS GESTION HORLOGE

byte derniere_seconde = 0;
byte derniere_minute = 0;
byte dizaines;
byte unites;

void afficheHeure()
{
  // inutile d'afficher en permanence
  // on ne continu que s'il y a eut un
  // changement de seconde
  if (derniere_seconde == HORLOGE_secondes)
    return;
  derniere_seconde = HORLOGE_secondes;
  
  // gestion des boutons
  
  if (digitalRead(BT_H)==0) h++;
  if (digitalRead(BT_M)==0) m++;
  if (digitalRead(BT_S)==0)
  {
    derniere_seconde = HORLOGE_secondes = 0;
  };
  
  // gestion de l'heure
  
  // secondes
  s = derniere_seconde;
  
  // minutes
  if (s == 0) m++;
  if (m >= 60) m = 0;
  
  // heures
  if (derniere_minute == 59 && m == 0) h++;
  if (h >= 24) h = 0;
  
  derniere_minute = m;
     
  // affichage LCD
  
  digitalWrite(RS, LOW); // mode commandes
  
  envoiOctet (0xC0); // seconde ligne
  delayMicroseconds (50);  
  
  digitalWrite(RS, HIGH); // mode caractères

  dizaines = h / 10;
  unites = h - dizaines * 10;
  envoiOctet (0x30+dizaines);
  envoiOctet (0x30+unites);

  envoiOctet (':');

  dizaines = m / 10;
  unites = m - dizaines * 10;
  envoiOctet (0x30+dizaines);
  envoiOctet (0x30+unites);

  envoiOctet (':');
    
  dizaines = s / 10;
  unites = s - dizaines * 10;
  envoiOctet (0x30+dizaines);
  envoiOctet (0x30+unites);
}

// FONCTIONS WIRING

void setup()
{
  pinMode(E, OUTPUT);
  pinMode(RS, OUTPUT);
  pinMode(RW, OUTPUT);
  pinMode(DB4, OUTPUT);
  pinMode(DB5, OUTPUT);
  pinMode(DB6, OUTPUT);
  pinMode(DB7, OUTPUT);

  pinMode(13, OUTPUT);
  
  pinMode(BT_H, INPUT);
  pinMode(BT_M, INPUT);
  pinMode(BT_S, INPUT);
 
  digitalWrite(E, LOW);
  digitalWrite(RS, LOW);
  digitalWrite(RW, LOW);
  
  // Affichage LCD
  // initialisation puis
  // affichage de "Horloge"
  // sur la première ligne.
  
  initDisplay();
  initDisplay();
     
  digitalWrite(RS, LOW); // mode commandes

  envoiOctet (0x02); // début 1ere ligne (home)
  delayMicroseconds (50);
  
  digitalWrite(RS, HIGH); // mode caractères
  
  envoiOctet ('H');
  envoiOctet ('o');
  envoiOctet ('r');
  envoiOctet ('l');
  envoiOctet ('o');
  envoiOctet ('g');
  envoiOctet ('e');
}

void loop()
{
  afficheHeure();
}

Le code du fichier wiring.c

ATTENTION, NE MODIFIEZ PAS LE FICHIER WIRING.C SI VOUS NE SAVEZ PAS EXACTEMENT CE QUE VOUS FAITES ET SURTOUT FAITES UNE SAUVEGARDE DU FICHIER ORIGINAL !

Extrait de code : (pas de place pour tout mettre)

/*... CODE DU FICHIER D'ORIGINE SANS LA FONCTION pulseIn ...*/

/*--------------------------------------*/
/* ---- MODIFICATIONS POUR HORLOGE ---- */
/*--------------------------------------*/

// valeur à comparer :
// calcul : fréquence quartz arduino / pre-diviseur = nombre d'incréments du compteur par seconde
// calcul : 16 000 000 / 1024 = 15 625 incréments du compteur par seconde
// nous aurons donc une interruption tous les 15 625 incréments soit 1 interruption par seconde
#define MAX_COMPARE_HORLOGE 15625

/**********************************/
// Fonction appelée à chaque
// une fois par seconde
/**********************************/
int etatLED = 0;
void TicTac(void)
{
  // RAZ compteur
  TCNT1 = 0;

  // indicateur de fonctionnement
  digitalWrite (13, etatLED);
  etatLED = !etatLED;

  // gestion HORLOGE_secondes
  if (HORLOGE_secondes >= 59)
    HORLOGE_secondes = 0;
  else
    HORLOGE_secondes++;

}

/**********************************/
// FONCTION D'INITIALISATION
// DE L'INTERUPTION DE L'HORLOGE
/**********************************/
void initHorloge()
{
  // arrêt des fonctions PWM
  timer1PWMOff();
  
  // liaison de l'interruption TIMER1OUTCOMPAREA_INT
  // avec la fonction TicTac
  timerAttach(TIMER1OUTCOMPAREA_INT, TicTac);
  
  // RAZ compteur
  TCNT1 = 0;

  // affectation de la valeur de comparaison
  // pour le déclenchement interruption
  OCR1A = MAX_COMPARE_HORLOGE;

  // activation de l'interruption de comparaison
  // de la valeur du timer 1 au compteur TCNT1
  sbi(TIMSK, OCIE1A);

  // réglage du pré-diviseur de fréquence
  // TCNT1 sera incrémenté tous les 1024 tics
  // de l'horloge du micro-controlleur
  timer1SetPrescaler(TIMER_CLK_DIV1024);
}

/*--- FIN MODIFICATIONS POUR HORLOGE ---*/


int main(void)
{
      // this needs to be called before setup() or some functions won't
        // work there
      sei();
      
      // timer 0 is used for millis() and delay()
      timer0Init();

      // timers 1 & 2 are used for the hardware pwm
      timer1Init();
      //timer1SetPrescaler(TIMER_CLK_DIV1);
      timer1PWMInit(8);
      
      timer2Init();

      // configure timer 2 for phase correct pwm
      // this is better for motors as it ensures an even waveform
      // note, however, that fast pwm mode can achieve a frequency of up
      // 8 MHz (with a 16 MHz clock) at 50% duty cycle
      cbi(TCCR2, WGM21);
      sbi(TCCR2, WGM20);

      // set a2d reference to AVCC (5 volts)
      cbi(ADMUX, REFS1);
      sbi(ADMUX, REFS0);

      // set a2d prescale factor to 128
      // 16 MHz / 128 = 125 KHz, inside the desired 50-200 KHz range.
      // XXX: this will not work properly for other clock speeds, and
      // this code should use F_CPU to determine the prescale factor.
      sbi(ADCSRA, ADPS2);
      sbi(ADCSRA, ADPS1);
      sbi(ADCSRA, ADPS0);

      // enable a2d conversions
      sbi(ADCSRA, ADEN);

      /*--------------------------------------*/
      /* ---- MODIFICATIONS POUR HORLOGE ---- */
      /*--------------------------------------*/
      initHorloge();
      /*--- FIN MODIFICATIONS POUR HORLOGE ---*/
      
      setup();
      
      for (;;)
            loop();
            
      return 0;
}

Code du fichier wiring.h

ATTENTION, NE MODIFIEZ PAS LE FICHIER WIRING.C SI VOUS NE SAVEZ PAS EXACTEMENT CE QUE VOUS FAITES ET SURTOUT FAITES UNE SAUVEGARDE DU FICHIER ORIGINAL !

/*
  wiring.h - Wiring API Partial Implementation
  Part of Arduino / Wiring Lite

  Copyright (c) 2005 David A. Mellis

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General
  Public License along with this library; if not, write to the
  Free Software Foundation, Inc., 59 Temple Place, Suite 330,
  Boston, MA  02111-1307  USA

  $Id: wiring.h 145 2006-04-14 11:53:56Z mellis $
*/

#ifndef Wiring_h
#define Wiring_h

#include <avr/io.h>

#ifdef __cplusplus
extern "C"{
#endif

#define HIGH 0x1
#define LOW  0x0

#define INPUT 0x0
#define OUTPUT 0x1

#define true 0x1
#define false 0x0

#define PI 3.14159265
#define HALF_PI 1.57079
#define TWO_PI 6.283185

#define SERIAL  0x0
#define DISPLAY 0x1

#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
#define abs(x) ((x)>0?(x):-(x))
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
#define radians(deg) ((deg)*DEG_TO_RAD)
#define degrees(rad) ((rad)*RAD_TO_DEG)
#define sq(x) ((x)*(x))

#define NOT_A_PIN 0
#define NOT_A_PORT -1

typedef uint8_t boolean;
typedef uint8_t byte;

void pinMode(int, int);
void digitalWrite(int, int);
int digitalRead(int);
int analogRead(int);
void analogWrite(int, int);

void beginSerial(long);
void serialWrite(unsigned char);
int serialAvailable();
int serialRead();
void printMode(int);
void printByte(unsigned char c);
void printNewline();
void printString(char *s);
void printInteger(long n);
void printHex(unsigned long n);
void printOctal(unsigned long n);
void printBinary(unsigned long n);

unsigned long millis(void);
void delay(unsigned long);
void delay_ms(unsigned short ms);
void delayMicroseconds(unsigned int us);
unsigned long pulseIn(int pin, int state);

void setup(void);
void loop(void);

// XXX: internal definitions, should be moved to another header file
typedef struct {
      int port;
      int bit;
} pin_t;

extern int port_to_mode[];
extern int port_to_input[];
extern int port_to_output[];
extern pin_t *digital_pin_to_port;
extern pin_t *analog_in_pin_to_port;
extern pin_t *analog_out_pin_to_port;

/*--------------------------------------*/
/* ---- MODIFICATIONS POUR HORLOGE ---- */
/*--------------------------------------*/
byte HORLOGE_secondes;
/*--- FIN MODIFICATIONS POUR HORLOGE ---*/

#ifdef __cplusplus
} // extern "C"
#endif

#endif

Salut!

Il a l'air génial ton projet! En plus il est très complèt.
Je pense que ça mérite d'être dans le playground de arduino!
Tu peux démander d'être contributeur, ça serait bien d'ouvrir une rubrique Français pour que l'on puisse poster les projets francophones.
Qu'est-ce que t'en penses??

Bravo encore!

A+!

Cristina

Bonjour Cristina,

Sans pb, je vais tenter d'ajouter cela au playground, je crois déjà être déclaré comme contributeur, il faut que je retrouve mon log/pass...

Mais, ce projet n'étant pas strictement du Wiring et n'étant donc pas entièrement réalisable dans l'environement de dev. Arduino, est-il opportunt de le publier dans le Playground ?

Pour le rendre vraiment "bidouille free" j'ai fait un post ici : http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1152313475 qui est resté sans réponse. Si qqun de l'équipe Arduino proposait une solution, je pourrai alors proposer un projet plus "propre".

Dans tous les cas si tu penses que même en l'état l'horloge peut être présenté dans le Playground, je le ferai...

Cordialement,
Benoît ROUSSEAU

Bonsoir Benoît,

Je trouve bien compliqué de vouloir réinventer l'eau chaude.

Il existe un circuit horloge autoalimenté spécialisé sur toutes les cartes mères de PC depuis le début des XT.
C'est pas les épaves de pc qui manquent, on le reconnait à sa hauteur anormale pour un DIP

(la hauteur est là car une pile au lithium ronde est placée au dessus du circuit.. On peut la changer en fraisant dans le plastique)

En revanche établir un dialogue avec cette puce avec arduino : ça c'est une réponse qui m'intéresse.

Cordialement

Bonsoir fr,

Je trouve bien interessant de comprendre comment faire chauffer de l'eau. Et d'ailleurs ce n'est pas le sujet réel du post. Le sujet réel étant l'utilisation d'une interruption générée sur dépassement de compteur. Je passe sur l'intérêt des TPs scolaires, ....

Bon, ceci dit je regarderai demain si j'ai un de ces composants sur une vielle carte mère. Par contre je ne vois pas quel est ton problème, tu donnes toi même l'@ de la datasheet. Tu veux savoir comment faire quoi exactement ?

Bonjour Benoît,

Si c'est l'utilisation des interruptions le but alors oui le projet est intéressant.
J'avais pensé que le but était de connaître l'heure, je suis trop terre à terre.
On a souvent besoin de cela dans tous les projets répétitifs à heure fixe.

Enfin pour ceux qui ne sont pas à l'aise en anglais (lecture du datasheet) et en C comme moi ton aide pour avoir une brique disponible à intégrer dans son projet est utile.

Toujours dans l'esprit associatif, on pourrait imaginer l'utilisation des interruptions avec un module "arrêt d'urgence" un changement d'état sur une pin est pris en compte à la volée.

Cordialement.

Bonjour,

Contrairement à ce que je pensais, je ne vais pas avoir le temps de regarder mes vielles cartes mères aujourd'hui, je te tiens au courant si je rouve un DSxxx. Auquel cas on fera ça ensemble pas-à-pas.

Pour les interruptions externes interessantes dans le cas des arrêt d'urgence (quoiqu'un arrêt d'urgence au sens strict est toujours un système non "programmé"), elles sont directement accessible dans la version 7 de l'environement de dev. Voir les fonctions suivantes dans le guide de référence :


External Interrupts

These functions allow you to trigger a function when the input to a pin changes value.

attachInterrupt(interrupt, function, mode)
detachInterrupt(interrupt)


Il y a en plus dans ce projet l'utilisation d'une interruption interne qui permet d'avaoir un déclenchement à intervales extrements précis.

Ce projet est en partie obsolète car il n'est pas utilisable sur un ATMega168, je prépare une mise-à-jour mais je manque de temps.

Bonjour,

J'ai regardé sur une 20aine de vielles cartes mères et je n'ai pas trouvé de DSxxx. Donc désolé, mais je ne pourrai pas fournir d'exemples.

Bonjour Benoît,

Je n'ai pas autant de cartes mères sous la main, j'ai donc seulement regardé ce que j'avais.

Je peux t'envoyer un http://www.st.com/stonline/books/pdf/docs/2412.pdf.

Par ailleurs il ne faut pas chercher dans les cartes mères récentes par exemple celles qui ont une pile bouton de sauvegarde.
Il y a bien une puce horloge mais elle n'est pas aussi facile à identifier puisque la batterie est séparée.

J'ai deux épaves de Imac, je vais voir dedans quel composant est utilisé si j'y arrive

Cordialement.

Bonsoir,

Finalement comme je trainais chez maxim pour autre chose je me suis commandé quelques puces:
DS1821 Mixed-signal and digital signal processing ICs | Analog Devices
DS18S20-PAR Mixed-signal and digital signal processing ICs | Analog Devices
DS1904L-F5 Mixed-signal and digital signal processing ICs | Analog Devices
DS28EA00 Mixed-signal and digital signal processing ICs | Analog Devices

Une petite horloge en I button avec identification unique et quelques capteurs de température...Une manière sympa de tester arduino et mes capacités en C.

J'ai vu aussi que l'Atmega168 était dispo chez farnell.

Cordialement.

Salut,

Tu en es où fr avec les horloges (RTC) ? Ca fonctionne ?