Horloge analogique 24h

De Wiki LOGre
Aller à : navigation, rechercher
Langue : Français  • English

Projet réalisé par : Edgar Bonet.

Le but de ce projet est de construire une horloge analogique, monoaiguille, qui fait un tour en 24h. L'unique aiguille pointe vers le bas à minuit, vers le haut à midi, et parcourt donc le cadran à la manière dont le soleil parcourt le ciel. On peut par la suite égayer ce cadran en y collant une feuille de papier, avec des dessins qui évoquent l'alternance des jours et des nuits.

Clock24 finished.jpg


Électronique et mécanique

Mécanisme d'horloge, circuit de pilotage et alimentation.

L'horloge est basée sur une un mécanisme d'horloge standard, modifié pour être pilotable par un microcontrôleur. Le microcontrôleur est monté sur un circuit de type « stripboard », et alimenté en 3 V par deux piles LR6. Le pilotage du moteur se fait en lui envoyant des impulsions de courant, alternativement dans un sens, puis dans l'autre. Ce principe est bien décrit dans cette page, dont je me suis très largement inspiré pour ce projet :

Controlling a clock with an Arduino, by mahto

Pour avoir une horloge monoaiguille 24h, il suffit de :

  • supprimer les aiguilles des heures et des secondes, pour ne garder que celle des minutes ;
  • ralentir celle-ci d'un facteur 24, pour qu'elle fasse un tour en 24h au lieu de un tour par heure.

Dans une horloge normale, les impulsions sont envoyées au moteur toutes les secondes, nous allons donc envoyer une impulsion toutes les 24 secondes.

Modifications du mécanisme d'horloge

Le mécanisme est facile à ouvrir avec un tournevis. Une fois ouvert, tous les éléments (engrenages, PCB) s'enlèvent sans outil, et se retrouvent même dispersés sur la table à la moindre maladresse. Il faut retirer le PCB, qui est solidaire de la bobine du moteur, puis :

  • dessouder et récupérer le quartz, qui sera utile pour la suite ;
  • couper au cutter au moins l'une des pistes qui vont du circuit intégré (sous la résine bleue dans les photos) à la bobine ;
  • souder deux fils sur les plots du PCB qui sont reliés aux bornes de la bobine ;
  • percer le boîtier afin de pouvoir sortir ces fils.

Voici quelques photos prises lors du remontage du mécanisme :

Électronique

J'étais initialement parti sur l'idée de piloter l'horloge à l'aide d'un Arduino, comme dans l'article de mahto cité plus haut. Mais après avoir fait un premier prototype sur Arduino Uno, j'ai dû y renoncer. D'une part à cause de la consommation électrique, de l'ordre de 40 à 50 mA, qui le rend incompatible avec un fonctionnement sur piles. D'autre part parce que l'Arduino tire sa base de temps d'un résonateur céramique, beaucoup trop imprécis pour faire une horloge.

J'ai décidé alors de faire un circuit autour d'un microcontrôleur nu. J'ai choisi un Atmel AVR ATmega48A car :

  • il est presque identique à l'ATmega328P qui anime l'Arduino Uno, et je viens justement de faire mes premiers pas dans le monde des microcontrôleurs avec un Uno ;
  • il intègre toute l'électronique nécessaire pour faire une horloge.

La seule différence sensible entre le 48A et le 328P est la quantité de mémoire : 4 KiB de flash et 0.5 KiB de RAM sur le 48A, contre 32 et 2 KiB respectivement sur le 328P. Mais le 48A a largement assez de ressources pour ce projet, et il a l'avantage de coûter une bouchée de pain...

La référence indispensable :

Voici le schéma électrique de l'ensemble :

Clock24 circuit.png
Circuit sur stripboard.

Les broches PD0 et PD1 servent à piloter le moteur. La LED connectée sur PC5 sert de témoin de fonctionnement : elle clignote trois fois lors de la mise sous tension du circuit, puis reste éteinte afin de ne pas gaspiller de l'énergie. Le connecteur sur la droite sert à la programmation du microcontrôleur.

Par rapport au schéma de mahto on note deux simplifications :

  • pas de résistances en série avec la bobine, car celle-ci a une résistance interne de 204 Ω qui sous 3 V est suffisante ;
  • pas de diodes de roue-libre car je les ai oubliées le microcontrôleur intègre déjà ces diodes dans chacune de ses sorties (c.f. fig. 14-1, page 75 de la datasheet).

Ci contre des photos des deux faces du circuit. La broche 1 du microcontrôleur est en haut dans les deux photos. Afin de gagner de la place, les trois jumpers (GND, VCC/AVCC et RESET) sont disposés sous le support DIP. Seuls deux sont visibles. On peut voir que le jumper de la ligne RESET passe entre les pattes du support DIP.


Programmation du microcontrôleur

L'AVR est utilisé dans sa configuration par défaut, avec les « fusibles » dans la configuration telle que livré d'usine. En particulier, le circuit est cadencé à 1 MHz à partir de l'oscillateur interne à 8 MHz dont la fréquence est divisée par 8 par le prescaler.

L'utilisation de l'oscillateur interne rend les broches 9 et 10 disponibles pour connecter le quartz au compteur asynchrone. Ce sont les fonctions TOSC1 et TOSC2 associées à ces broches. De plus, à cette fréquence le circuit accepte toute tension d'alimentation entre 1.8 et 5.5 V (fig. 29-1, p. 303 de la datasheet). La limite inférieure à 1.8 V permet de l'alimenter avec deux piles 1.5 V pratiquement jusqu'à épuisement de ces dernières.

Principe de fonctionnement

L'AVR intègre un périphérique appelé « 8-bit Timer/Counter2 with PWM and Asynchronous Operation », décrit en section 18, p. 141, qui fait l'essentiel du travail de l'horloge. Ce périphérique comprend en particulier :

  • un « oscillateur », qui est en fait un amplificateur chargé d'entretenir les oscillations du quartz ;
  • un « prescaler » qui divise la fréquence du signal du quartz par 1, 8, 32, 64, 128, 256 ou 1024, au choix ;
  • un compteur sur 8 bits ;
  • une logique capable de générer une interruption à chaque débordement du compteur ;
  • des registres pour configurer le tout.

C.f. fig. 18-2, p. 142.

L'AVR a plusieurs modes de sommeil (décrits en section 10, p. 39) qui permettent de réduire la consommation électrique. Le mode qui nous intéresse est le mode POWER_SAVE. Ce mode éteint pratiquement tous les circuits du microcontrôleur, à l'exception du Timer/Counter2 qui servira à le réveiller. Dans ce mode la consommation est de l'ordre de seulement 2 µA. Mise à jour (26 octobre 2014) : depuis que cette horloge est en service (début juin 2012, il y a près de 29 mois), elle tourne toujours sur la même paire de piles LR6 qui est aujourd'hui à 2.85 V, c'est à dire encore en début de vie. Ce mode d'économie d'énergie est donc vraiment efficace.

Le quartz a une fréquence nominale de 32768 Hz. En réglant le prescaler sur 1024, on aura un débordement du compteur, et donc une interruption, toutes les 8 secondes. On peut le régler sur 128 si on veut une interruption par seconde.

Le fait que ce compteur fonctionne de façon asynchrone par rapport à l'horloge système impose certaines précautions d'emploi : Après un accès à un registre de configuration, et avant de mettre le microcontrôleur en sommeil, il faut surveiller le registre ASSR (Asynchronous Status Register) et attendre que le bit *UB (pour « Update Busy ») de ASSR qui est associé au registre auquel on a accédé passe à zéro. Dans une routine d'interruption, il faut faire une écriture bidon sur un des registres de configuration et, encore une fois, attendre que le bit *UB passe à zéro. C.f. section 18.9, p. 151.

Pour tenir compte d'un éventuel écart de la fréquence réelle du quartz par rapport à sa fréquence nominale, on procède à un étalonnage, puis on applique l'algorithme suivant, analogue de l'algorithme de Bresenham pour tracer des lignes obliques :

for (each interrupt) {
    unaccounted_microseconds += MICROSECONDS_PER_INTERRUPT;
    if (unaccounted_microseconds >= MICROSECONDS_PER_PULSE) {
        send_pulse_to_motor();
        unaccounted_microseconds -= MICROSECONDS_PER_PULSE;
    }
}

où MICROSECONDS_PER_PULSE vaut 24×10⁶ (pour une impulsion toutes les 24 secondes) et MICROSECONDS_PER_INTERRUPT, qui devrait valoir nominalement 8×10⁶, est ajusté en fonction de la fréquence mesurée du quartz. Ceci assure que la fréquence moyenne des impulsions est correcte.

Programme

/*
 * AVR clock: Drive a watch crystal and a clock movement.
 *
 * Connect:
 *   - power (-) to GND (pins 8, 22)
 *   - power (+) to VCC and AVCC (pins 7, 20)
 *   - watch XTAL to TOSC1/TOSC2 (pins 9, 10)
 *   - clock motor to PD0/PD1 (pins 2, 3)
 *   - LED + resistor to PC5 (pin 28) and GND
 *
 * Alternatively, #define PPS and get a 1PPS signal from PD0 (pin 2).
 *
 * TC2 prescaler is set to 1024, then TC2 will overflow once every
 * (roughly) 8 seconds and trigger an interrupt.
 *
 * If PPS is #defined, then the TC2 prescaler is set to 128 and TC2 will
 * overflow once per second.
 */
 
/* Output 1PPS on pin 2. */
/* #define PPS */
 
#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#define F_CPU 1000000UL  /* 1 MHz */
#include <util/delay.h>
 
#ifdef PPS
#   define MICROSECONDS_PER_INTERRUPT  1000000
#   define MICROSECONDS_PER_PULSE      1000000
#else
/* Nominally 8e6, but quartz was calibrated to be 6.5 ppm fast. */
#   define MICROSECONDS_PER_INTERRUPT  7999948
#   define MICROSECONDS_PER_PULSE     24000000
#endif
 
/*
 * On 3 V supply, seems to work reliably for pulse lengths between
 * 52 and 76 ms, or >= 100 ms
 */
#define PULSE_LENGTH 70  /* ms */
 
/* Interrupt triggered once per second. */
ISR(TIMER2_OVF_vect)
{
    static long unaccounted_microseconds;
    static uint8_t pin_bit = _BV(PD0);
#ifdef PPS
    static const uint8_t pin_mask = 0;
#else
    static const uint8_t pin_mask = _BV(PD0) | _BV(PD1);
#endif
 
    /* Send pulses at the correct average frequency. */
    unaccounted_microseconds += MICROSECONDS_PER_INTERRUPT;
    if (unaccounted_microseconds < MICROSECONDS_PER_PULSE) return;
    unaccounted_microseconds -= MICROSECONDS_PER_PULSE;
 
    /* Set the OCR2AUB flag in ASSR. */
    OCR2A = 0;
 
    /* Pulse motor. */
    PORTD = pin_bit;
    _delay_ms(PULSE_LENGTH);
    PORTD = 0;
    pin_bit ^= pin_mask;  /* next time use the other pin  */
 
    /* Wait end of TOSC1 cycle (30.5 us). */
    while (ASSR & _BV(OCR2AUB)) {/* wait */}
}
 
int main(void)
{
    uint8_t count;
 
    /* Configure PC5, PD0 and PD1 as output. */
    DDRC = _BV(DDC5);
    DDRD = _BV(DDD0) | _BV(DDD1);
 
    /* Blink while waiting for the crystal to stabilize. */
    for (count = 0; count < 3; count++) {
        _delay_ms(167);
        PORTC = _BV(PC5);
        _delay_ms(167);
        PORTC = 0;
    }
 
    /* Save power by sleeping. */
    set_sleep_mode(SLEEP_MODE_PWR_SAVE);
 
    /* Configure Timer/Counter 2. */
    ASSR |= _BV(AS2);               /* asynchronous */
    TCCR2A = 0;                     /* normal counting mode */
#ifdef PPS
    TCCR2B = _BV(CS22) | _BV(CS20); /* prescaler = 128 */
#else
    TCCR2B = _BV(CS22) | _BV(CS21) | _BV(CS20); /* prescaler = 1024 */
#endif
    TCNT2 = 1;                      /* initial value */
    while (ASSR & _BV(TCN2UB)) {/* wait */}
    TIMSK2 = _BV(TOIE2);        /* Timer Overflow Interrupt Enable */
    sei();
 
    for (;;) sleep_mode();
}

Compilation et transfert

Le programme est compilé sous Linux avec gcc-avr. Pour le transférer sur l'AVR, j'utilise mon Arduino Uno en guise de programmateur. Des informations contradictoires circulent sur le Net quand à la possibilité d'utiliser un Uno comme programmateur AVR. Je préfère donc préciser que cela fonctionne parfaitement chez moi moyennant les précautions suivantes :

  • Modifier le programme arduino_isp standard pour une vitesse de transfert de 9600 b/s (dans setup() : Serial.begin(9600)) ;
  • donner cette même vitesse en paramètre à avr-dude ;
  • placer un condensateur de 1 µF entre les contacts +5V et RESET de l'Arduino.

Avec le makefile suivant :

TARGET=clock.hex
CFLAGS=-mmcu=atmega48 -Os -Wall -Wextra
AVRDUDE=avrdude -p m48 -c avrisp -b 9600 -P /dev/ttyACM0
 
all:	$(TARGET)
 
upload:	$(TARGET)
	$(AVRDUDE) -e -U flash:w:$(TARGET)
 
test:
	$(AVRDUDE) -n  -v
 
%.elf:	%.c
	avr-gcc $(CFLAGS) $< -o $@
 
%.hex:	%.elf
	avr-objcopy -j .text -j .data -O ihex $< $@

j'utilise les commandes :

  • make : pour compiler ;
  • make test : pour tester la communication avec l'AVR ;
  • make upload : pour programmer l'AVR.


Étalonnage du quartz

Vue la précision de lecture, il est sans doute un peu futile d'étalonner le quartz d'une horloge 24h, mais je le fais car c'est amusant. ;-) L'étalonnage se fait en compilant le programme en mode PPS (avec #define PPS), et en comparant le signal PPS avec un serveur NTP.

Liaison au PC via Arduino

Je me sers de l'Arduino pour relayer le signal PPS en provenance de l'AVR vers le PC. L'AVR et l'Arduino sont reliés suivant le schéma ci-dessous :

Clock24 arduino link.png

Dans ce schéma, la diode et sa résistance de polarisation de 1 kΩ servent à alimenter l'AVR sous une tension de l'ordre de 2.7 V, afin de simuler les conditions de fonctionnement typiques avec des piles en milieu de vie. Le transistor et sa résistance de base servent à adapter les niveaux de signaux, de 2.7 V à 5 V. Ces quatre composants (diode, transistor et résistances) sont montés sur une plaque d'essai (breadboard).

À noter qu'il est nécessaire d'activer la résistance de pull-up interne sur la broche 2 de l'Arduino. Aussi, cette adaptation de niveaux inverse les signaux, les fronts montants du signal PPS sont donc détectés par l'Arduino comme des fronts descendants. Ces fronts sont utilisés pour déclencher une interruption qui envoie un caractère (un point) sur le port série. Voici le code :

/*
 * Forward a 1PPS signal from a digital input to the serial port.
 *
 * Intended for calibrating a clock.
 *
 * Connect the inverted 1PPS signal to digital pin 2.
 */
 
#include <avr/sleep.h>
 
#define PPS_PIN 2
#define PPS_IRQ 0
 
void forward()
{
  Serial.write('.');
}
 
void setup()
{
  Serial.begin(9600);
  pinMode(PPS_PIN, INPUT);
  digitalWrite(PPS_PIN, HIGH);  // enable pull-up
  attachInterrupt(PPS_IRQ, forward, FALLING);
}
 
void loop()
{
  sleep_mode();
}

Acquisition sous Linux

Le PC, sous Linux, reçoit le signal PPS et le compare à l'heure d'un serveur NTP à l'aide des deux programmes ci-dessous :

  • time-pps.c surveille le port série à l'aide d'une boucle select(). À chaque fois qu'un caractère devient disponible sur le port série, l'heure système est lue avec gettimeofday() et le résultat de la comparaison est enregistré dans un fichier.
  • time-ntp.sh, appelé toutes les 10 minutes par cron, interroge un serveur NTP et enregistre dans un autre fichier l'écart entre l'heure NTP et l'heure du PC.

L'horloge du PC tourne librement, sans asservissement NTP. On enregistre alors dans deux fichiers les différences (PPS − heure_PC) et (heure_NTP − heure_PC). En faisant la différence entre ces deux fichiers, on obtient (PPS − heure_NTP), qui est la donnée qui nous intéresse.

Résultats

Dérive.
Variance d'Allan.

Voici, ci-contre, les mesures :

  • en rouge : (PPS − heure_PC)
  • en vert : (heure_NTP − heure_PC)
  • en bleu : (PPS − heure_NTP), obtenu par différence.

Dans ce graphe, les dérives moyennes ont été estimées par régression linéaire et soustraites des données. Ceci afin de pouvoir voir l'instabilité de la dérive. Pour la série (PPS − heure_NTP), la dérive moyenne mesurée est de +6.5 ppm, soit une avance de quelques 3.4 minutes par an. Une telle dérive est largement tolérable, mais au point où on en est, autant la corriger. On calcule donc la période entre les interruptions, et on définit

#define MICROSECONDS_PER_INTERRUPT 7999948

au lieu de sa valeur nominale de 8000000.

Le deuxième graphe est la (racine carrée de la) variance d'Allan. C'est une mesure des fluctuations de fréquence du quartz en fonction de l'échelle de temps à laquelle on les mesure. Les fortes fluctuations aux courtes durées sont dues à l'important jitter de la méthode de mesure. Sur des plus longues durées, les fluctuations de fréquence tombent en dessous de 10⁻⁷, ce qui est parfaitement satisfaisant.


Cadran

Cadran gravé et découpé au laser.

Le cadran mesure 350 mm de diamètre. Il comporte un trou central 7.5 mm, soit le diamètre du pas de vis qui entoure les axes des aiguilles. Il a été gravé et découpé au Fab Lab, dans du MDF, à la découpeuse laser, à partir de ce fichier SVG :

Ce fichier SVG a lui même été créé par un programme en PHP, afin de pouvoir faire des boucles sur les heures. Voici le code source de ce programme :

On peut ajuster le diamètre du cadran en modifiant le facteur d'échelle global (scale(2.0669)) à la ligne 13 du fichier SVG, ou la ligne 26 du fichier PHP. On veillera alors à ajuster aussi la taille du trou central (le cercle à ligne 16 du SVG, ligne 29 du PHP) pour qu'elle corresponde toujours au pas de vis.