Ce document montre comment faire des mesures de fréquence et de temps avec un chronomètre-compteur (Timer-Counter). La mesure de fréquence consiste à compter les fronts montants d'un signal pendant une durée fixée. La mesure de temps consiste à déterminer la durée entre deux fronts montants d'un signal.
Les exemples fonctionnent sur l'Arduino Mega (ATmega 2560) mais sont facilement adaptables à d'autres arduino (ATmega 328 ou ATmega 32u4).
L'utilisation des Timers en mode générateur de signal et d'interruption est expliquée dans Génération d'impulsions et d'interruptions périodiques.
Un Timer peut être utilisé en mode capture. Dans ce mode, le compteur (registre TCNTn) est incrémenté sur les fronts montants ou descendants d'un signal externe. Ce signal est lu sur l'entrée Tn. Pour savoir à quelle borne de l'arduino les entrées Tn sont reliées, il faut consulter la table de correspondance de l'Arduino Mega. Pour les Timers 16 bits (numéros 1,3,4 et 5), seule la sortie T5 est reliée, sur la borne 47. Pour l'arduino UNO, la sortie T1 est reliée à la borne 5. Pour l'arduino Mega, nous allons donc utiliser le Timer 5 pour faire la mesure de fréquence. Il faut faire fonctionner le Timer en mode normal, qui consiste à incrémenter le compteur jursqu'à la valeur maximale 0xFFFF, après quoi il revient à 0. Cette configuration est obtenue en mettant les 4 bits de configuration WGM53, WGM52, WGM51, WGM50 à 0. Les bits WGM53 et WGM52 sont les bits 4 et 3 du registre de configuration TCCR5B, alors que les bits WGM51 et WGM50 sont les bits 1 et 0 du registre WGM50. Voici donc comment se fait la configuration du Timer 5 :
TCCR5A = 0; TCCR5B = 0; TCNT5 = 0; // mise à 0 du compteur
Pour déclencher le compteur, il faut configurer les bits CS2, CS1, CS0 (Clock Select) du registre TCCR5B. Dans le mode générateur, ces bits servent à définir l'horloge interne. Dans le cas présent, l'horloge est externe puisque le compteur est incrémenté à partir d'un signal externe. Pour une horloge externe sur front montant, ces trois bits sont 110. Pour un front descendant, ces trois bits sont 111. Voici donc comment déclencher un compteur de fronts montants :
TCCR5B |= (1 << CS32) | (1 << CS31) | (1 << CS30); // external clock on rising edge
Il faut à présent programmer la durée du comptage. Nous allons pour cela utiliser un autre chronomètre-compteur, le Timer 3, pour générer des interruptions périodiques. La configuration de ce Timer pour cette tâche est expliquée en détail dans Génération d'impulsions et d'interruptions périodiques. Voici la fonction des interruptions déclenchées périodiquement par ce Timer :
ISR(TIMER3_OVF_vect) { // Overflow interrupt count = TCNT5; TCNT5 = 0; }
Elle sauvegarde la valeur du compteur dans une variable 16 bits avant de l'initialiser. Compte tenu du temps d'exécution de la première ligne, de l'ordre de quelques cycles d'holorge, on voit que la précision du temps de comptage est aussi de quelques cycles d'horloges, c'est-à-dire inférieure à la microseconde. C'est bien mieux que ce que l'on peut faire avec les fonctions delay ou delayMicroseconds.
Le programme complet présenté ci-dessous met en œuvre les Timer 5 et 3 comme expliqué plus haut.
#include "Arduino.h" char inputPin = 47; // entrée T5 pour Timer5 uint16_t diviseur[6] = {0,1,8,64,256,1024}; volatile uint16_t count;
La fonction suivante effectue la configuration des deux Timers et les déclenche. La période des interruptions générées par le Timer 3 est fourni en argument, en microsecondes. Ce Timer génère aussi un signal sur la sortie 5 (OC3A), qui servira à vérifier son bon fonctionnement. L'interruption est déclenchée lorsque le compteur atteint sa valeur maximale, donnée par le registre ICR3.
void start_count(uint32_t period) { char clockBits; // Timer 3 : génération d'interruptions périodiques TCCR3A = 0; TCCR3A |= (1 << COM3A1) | (1 << COM3A0); TCCR3B = 1 << WGM13; // phase and frequency correct pwm mode, top = ICR3 uint32_t icr = (F_CPU/1000000*period/2); int d = 1; while ((icr>0xFFFF)&&(d<5)) { d++; icr = (F_CPU/1000000*period/2/diviseur[d]); } clockBits = d; ICR3 = icr; OCR3A = icr >> 1; TIMSK3 = 1 << TOIE3; // overflow interrupt enable TCNT3 = 0; // Timer 5 : compteur d'impulsion TCCR5A = 0; TCCR5B = 0; TCNT5 = 0; count = 0; sei(); TCCR3B |= clockBits; TCCR5B |= (1 << CS32) | (1 << CS31) | (1 << CS30); // external clock on rising edge }
Voici la fonction appelée lors des interruptions :
ISR(TIMER3_OVF_vect) { // Overflow interrupt count = TCNT5; TCNT5 = 0; }
La fonction setup configure le sens des différents bornes utilisées. Un signal PWM est générée sur la sortie 6, laquelle est branchée sur l'entrée 47 pour tester le fréquencemètre. La table de correspondance nous indique que la sortie 6 est reliée à la sortie de Timer OC4A, ce qui signifie qu'elle est pilotée par le Timer 4, que nous n'utilisons pas. On démarre un comptage avec une durée de 1 seconde.
void setup() { Serial.begin(115200); pinMode(47,INPUT); pinMode(6,OUTPUT); pinMode(5,OUTPUT); analogWrite(6,128); // sortie 6 branchée sur l'entrée 47 pour test start_count(1000000); }
La fonction loop lit la valeur de la variable count toute les secondes (environ) et l'affiche sur le port série.
void loop() { delay(1000); Serial.println(count); }
Les valeurs affichées sur la console sont 490 et 491. La fréquence du PWM générée sur la sortie 6 est donc de 490 Hz.
Le mode de capture (input capture mode) permet à un Timer d'enregistrer le nombre de tops d'horloges entre deux fronts montants (ou descendants) d'un signal externe. Les entrées à utiliser pour cela sont les entrées ICPn (Input Capture Pin). Pour l'arduino Mega, on doit utiliser l'entrée ICP5, qui est reliée à la borne 48. Il faut donc utiliser le Timer 5.
Le Timer doit fonctionner en mode normal, c'est-à-dire que le compteur 16 bits doit être incrémenté jusqu'à sa valeur maximale 0xFFFF avant de revenir à 0. Cette configuration est obtenue en mettant les 4 bits de configuration WGM53, WGM52, WGM51, WGM50 à 0. Les bits WGM53 et WGM52 sont les bits 4 et 3 du registre de configuration TCCR5B, alors que les bits WGM51 et WGM50 sont les bits 1 et 0 du registre WGM50. Le bit ICES5 (input capture edge select) du registre TCCR5B doit être mis à 1 si l'on veut un déclenchement sur front montant, à 0 pour un déclenchement sur front descendant. Voici donc la configuration du Timer 5 pour un déclenchement sur front montant :
TCCR5A = 0; TCCR5B = (1 << ICES5);
Il faut activer l'interruption Input capture en activant le bit ICIE5 (input capture interrupt enable) du registre TIMSK5 (Timer Interrupt Mask Register) :
TIMSK5 = (1 << ICIE5); sei(); // activation globale des interruptions
Une interruption est alors déclenchée chaque fois que le signal appliqué sur l'entrée ICP5 (borne 48) présente un front montant. Le déclenchement du compteur se fait en choisissant la fréquence de son horloge (bits CS2,CS1,CS0). Par exemple pour utiliser la fréquence de base (16 MHz sur l'arduino Mega) :
TCCR5B |= 1;
La fonction appelée lors de l'interruption doit être définie de la manière suivante :
ISR(TIMER5_CAPT_vect) { TCNT5 = 0; count = ICR5; // input capture register }
La valeur du compteur juste avant l'interruption est sauvegardée dans le registre ICR5 (input capture register). On recopie donc cette valeur dans une variable 16 bits count. Il faut aussi initialiser le compteur pour qu'il recommence à compter les tops d'horloge jusqu'à la prochaine interruption (le prochain front montant).
Le programme présenté ci-dessous met en œuvre la capture et affiche sur le port série le nombre de tops d'horloges entre deux fronts montants, ce qui permet d'obtenir la période d'un signal périodique.
#include "Arduino.h" char inputPin = 48; // entrée ICP5 pour Timer5 uint16_t diviseur[6] = {0,1,8,64,256,1024}; uint8_t clockBits; uint16_t count; #define BUF_SIZE 256 volatile uint16_t buf[BUF_SIZE]; volatile uint8_t buf_index; float clock_period;
La fonction suivant configure le Timer 5, en lui fournissant le code de l'horloge. Les diviseurs de fréquence sont définies dans le tableau diviseur. Par exemple, le code 2 correspond à la fréquence principale (16 MHz) divisée par 8.
void start_capture(uint8_t clock) { clockBits = clock; buf_index = 0; cli(); TCCR5A = 0; // normal counting TCCR5B = (1 << ICES5); TIMSK5 = (1 << ICIE5); // input capture interrupt enable interrupt sei(); TCCR5B |= clockBits; clock_period = 1.0/F_CPU*diviseur[clock]; }
Voici la fonction d'interruption.
ISR(TIMER5_CAPT_vect) { TCNT5 = 0; buf[buf_index] = ICR5; // input capture register buf_index++; }
La fonction setup configure le sens des entrées-sorties utilisées. Un signal PWM est généré sur la sortie 6 (Timer 4), que nous avons reliée à l'entrée 48. La capture est lancée avec l'horloge à 16 MHz.
void setup() { Serial.begin(115200); pinMode(inputPin,INPUT); pinMode(6,OUTPUT); analogWrite(6,128); start_capture(1); }
La fonction loop affiche le contenu du tampon toutes les 500 millisecondes. Elle affiche aussi la valeur moyenne.
void loop() { int i; float m; delay(500); m = 0.0; for (i=0; i<BUF_SIZE; i++) { Serial.println(buf[i]); m += buf[i]; } m /= BUF_SIZE; Serial.print("Moyenne = "); Serial.println(m); Serial.print("Période (ms) = "); Serial.println(m*clock_period*1000); }
Les valeurs moyennes affichées sur la console sont entre 32606 et 32607. Cela correspond bien à la fréquence de 590 Hz du signal PWM.
Pour des périodes plus grandes, il faut penser à réduire la fréquence de l'horloge en choisissant un diviseur. Le compteur est un registre 16 bits, donc la valeur comptée ne peut dépasser 65535.
Le mode Input Capture décrit ci-dessus permet de mesurer des durées très courtes très précisément, avec une précision égale à la période de l'horloge. Cependant, son utilisation ne peut se faire que sur une seule entrée de la carte Arduino. Dans certains cas, on peut avoir besoin de déclencher un chronomètre sur une entrée et de le stopper sur une autre. Le déclenchement d'un compteur par interruption matérielle permet de faire cela, avec une précision très bonne pour des durées supérieures à une milliseconde.
La configuration du Timer est très simple. On le fait fonctionner en mode normal, dans lequel le compteur 16 bits est incrémenté à chaque top d'horloge jusqu'à sa valeur maximale 0xFFFF, au delà de laquelle il revient à zéro (débordement).
Les interruptions matérielles sont programmées avec la fonction attachInterrupt, qui consiste à attribuer une fonction à une entrée pouvant déclencher une interruption. Les entrées numériques 2 et 3 sont utilisables pour cela, aussi bien sur l'Arduino UNO que sur l'Arduino MEGA.
Si l'on veut que le démarrage et la lecture du compteur soit déclenchés par la même entrée, il n'y a qu'une fonction d'interruption à programmer. Elle doit lire la valeur du compteur et le remettre à zéro.
Si l'on veut que le démarrage du compteur soit déclenché par une entrée et sa lecture par une autre, une première fonction effectue la remise à zéro du compteur, la seconde effectue la lecture du compteur.
Le programme présenté ci-dessous réalise un chronomètre déclenché par un front montant sur une entrée et lu par un front montant sur la même entrée, ou éventuellement sur une autre entrée. La durée en millisecondes est affichée sur la console série. Le diviseur d'horloge le plus élevé (1024) fournit une durée maximale pour le compteur 16 bits de 4194 ms. Pour augmenter cette durée, on active l'interruption de débordement du compteur. La fonction appelée par cette interruption permet d'incrémenter un compteur logiciel. On obtient ainsi un compteur de 32 bits, qui donne une durée maximale de 74 heures pour un diviseur de 1024, avec une précision de 64 μs.
Voici tout d'abord les variables globales. L'entrée 2 est choisie pour la lecture et la remise à zéro du compteur. L'entrée 3 (facultative) peut être utilisée pour la remise à zéro du compteur (sans lecture). On pourra ainsi déclencher le compteur avec un interrupteur optique et lire le compteur avec un autre. La variable high_count stocke les 16 bits de poids fort du compteur logiciel 32 bits.
#include "Arduino.h" char readPin = 2; // lecture et RAZ compteur char startPin = 3; // facultatif : RAZ compteur float clock_period; uint16_t diviseur[6] = {0,1,8,64,256,1024}; volatile int32_t high_count;
La fonction suivante configure et déclenche le Timer 1, qui fonctionne sur Arduino Mega et Uno. On doit lui fournir le code de diviseur d'horloge (entre 1 et 5). L'interruption de dépassement du compteur est activée. Cette interruption se déclenche lorsque le registre TCNT1 du compteur revient à zéro après un débordement.
void config_timer(uint8_t clock) { clock_period = 1.0/F_CPU*diviseur[clock]; cli(); TCCR1A = 0; // normal counting TCCR1B = 0; TIMSK1 = (1<<TOIE1); // Overflow interrupt sei(); high_count = 0; TCCR1B |= clock; }
La fonction appelée lors de l'interruption de débordement du compteur incrémente les 16 bits de poids fort du compteur logiciel 32 bits :
ISR(TIMER1_OVF_vect) { // overflow interrupt high_count++; }
Voici la fonction d'interruption qui remet à zéro le compteur :
void start_counter() { TCNT1 = 0; // RAZ compteur high_count = 0; }
Voici la fonction d'interruption qui lit le compteur puis le remet à zéro. Elle affiche la durée convertie en millisecondes.
void read_counter() { uint32_t count; count = TCNT1; TCNT1 = 0; Serial.println(((high_count << 16) + count)*clock_period*1000); // em ms high_count = 0; }
La lecture se fait avec un léger retard par rapport à l'arrivée du front montant qui déclenche l'interruption. Ce retard, qui devrait être de l'ordre de la microseconde, vient principalement du temps nécessaire à l'appel de la fonction. Pour un diviseur d'horloge de 1024, ce retard est négligeable. Pour mesurer des durées inférieures à la milliseconde, qui nécessitent un diviseur plus petit (1 ou 8), il est préférable d'utiliser le mode Input Capture décrit plus haut, qui fait une lecture du compteur avec un délai très court, égal à la période d'horloge.
La fonction setup configure les interruptions matérielles puis déclenche le compteur. Le déclenchement des interruptions se fait ici sur front montant (RISING), mais on peut modifier pour FALLING ou CHANGE. On peut se contenter d'utiliser l'entrée readPin=2, qui fait en même temps la lecture et le déclenchement du chronomètre. Bien sûr, la première valeur lue n'aura pas de signification.
void setup() { Serial.begin(115200); pinMode(readPin,INPUT); pinMode(startPin,INPUT); attachInterrupt(digitalPinToInterrupt(startPin), start_counter, RISING); attachInterrupt(digitalPinToInterrupt(readPin), read_counter, RISING); config_timer(5); }
La fonction loop ne fait rien.
void loop() { }