Ce document montre comment (sur un Arduino MEGA ou UNO) un Timer peut être déclenché par une interruption matérielle (ou interruption externe), c'est-à-dire une interruption déclenchée par un changement d'état sur une entrée numérique. Le Timer peut avoir différents usages. Dans l'exemple développé, il est utilisé comme compteur avec déclenchement d'une interruption lorsque le compteur atteint une certaine valeur.
Une application est aussi présentée : la mesure de durées entre les fronts montants d'un signal carré. Le programme permet de mesurer la période d'un signal cycle par cycle, avec une précision inférieure à la microseconde.
L'interruption matérielle est déclenchée par un front montant sur l'entrée D2. Dans le gestionnaire de cette interruption, on déclenche le Timer 1 (16 bits). Celui-ci est programmé pour déclencher une interruption lorsque son compteur atteint une valeur qui correspond à un certain délai en microsecondes (noté DELAI).
La sortie PB5 est utilisée pour contrôler le déroulement des interruptions : elle est mise au niveau haut lors du déclenchement de l'interruption matérielle puis mise à zéro lors de la seconde interruption (déclenchée par le Timer). Le signal émis sur PB5 est donc une impulsion de largeur DELAI qui est déclenchée par l'interruption matérielle.
La sortie PB5 est reliée à la borne 13 sur l'Arduino UNO (voir ATmega168/328P-Arduino Pin Mapping), à la borne 11 sur l'arduino MEGA (voir ATmega2560-Arduino Pin Mapping).
#include <Arduino.h> #define INTERRUPT 2 #define DELAI 100 // microsecondes #define OUT 11 // PB5 (remplacer par 13 sur l'Arduino UNO) uint16_t diviseur[6] = {0,1,8,64,256,1024}; uint8_t clock = 1; // diviseur d'horloge pour Timer 1 bool start;
La fonction suivante programme le Timer 1 (sans le déclencher). Le diviseur d'horloge est 1 : la période d'horloge du compteur est donc 16 MHz. Le temps donné par delai en microsecondes est donc atteint lorsque le compteur atteint la valeur de delai multipliée par 16. Étant donné qu'il s'agit d'un compteur 16 bits, la valeur maximale du délai est de 4096 microsecondes. Pour avoir un délai plus grand, il faut changer le diviseur d'horloge. Par exemple l'utilisation du diviseur 3 (clock=3) donne une fréquence d'horloge de 2 MHz, ce qui permet d'atteindre un délai de 32,8 ms.
void timer1_init(uint16_t delai) { // delai en microsecondes, max 4096 pour clock = 1 TCCR1A = 0; TCCR1B = 0; TCCR1B |= (1 << WGM12); OCR1B = F_CPU/1000000 * delai / diviseur[clock]; TIMSK1 = (1 << OCIE1B); }
La fonction suivante est appelée par interruption lorsque la valeur du compteur (TCNT1) atteint la valeur définie dans OCR1B. Dans cette fonction, on met à l'état bas la sortie PB5 et on stoppe le Timer.
ISR(TIMER1_COMPB_vect) { PORTB &= ~(1<<PORTB5); // digitalWrite(OUT,LOW) TCCR1B = 0; start = false; }
Voici la fonction appelée lors de l'interruption matérielle. Elle remet le compteur à zéro, démarre le Timer 1, puis fait passer la sortie PB5 à l'état haut.
void startTimer() { if (start) return; start = true; TCNT1 = 0; TCCR1B |= clock; PORTB |= (1<<PORTB5); // digitalWrite(OUT,HIGH) }
Dans la fonction setup, on programme l'interruption matérielle déclenchée par un front montant sur l'entrée INTERRUPT. Attention : sur Arduino UNO, seules les entrées 2 et 3 sont utilisables. Sur Arduino MEGA, on peut utiliser les entrées 2,3,18,19,20 et 21.
void setup() { attachInterrupt(digitalPinToInterrupt(INTERRUPT), startTimer, RISING); pinMode(OUT,OUTPUT); digitalWrite(OUT,LOW); start = false; timer1_init(DELAI); } void loop() { }
Pour ce test, on envoie sur l'entrée D2 un signal carré de fréquence 100 Hz, d'amplitude 2,5 V et d'offset 2,5 V. Ce signal permet de déclencher une interruption toutes les 10 ms. Voici le signal obtenu sur la sortie D11 (PB5 sur Arduino MEGA) pour DELAI = 100 :
import numpy as np from matplotlib.pyplot import * [t,u1,u2] = np.loadtxt('signal-PB5-D2-DELAI=100.csv',unpack=True,delimiter=',',skiprows=20) figure(figsize=(12,6)) plot(t*1e6,u2,label='D11') plot(t*1e6,u1,label='D2') xlim(-100,200) xlabel(r"$t\ (\rm \mu s)$",fontsize=14) ylabel(r"$U\ (\rm V)$",fontsize=14) legend(loc='upper right',fontsize=14) grid()figA.pdf
Voici le signal pour DELAI = 10 :
[t,u1,u2] = np.loadtxt('signal-PB5-D2-DELAI=10.csv',unpack=True,delimiter=',',skiprows=20) figure(figsize=(12,6)) plot(t*1e6,u2,label='D11') plot(t*1e6,u1,label='D2') xlim(-10,20) xlabel(r"$t\ (\rm \mu s)$",fontsize=14) ylabel(r"$U\ (\rm V)$",fontsize=14) legend(loc='upper right',fontsize=14) grid()figB.pdf
Le délai entre le front montant du signal qui déclenche l'interruption (D2) et le front montant sur D11 est de 4 microsecondes. La durée de l'impulsion obtenue est plus longue d'environ 1 microseconde que le délai programmé. Cet allongement est facile à corriger dans la mesure où il est reproductible. Une observation attentive à l'oscilloscope montre cependant que la durée de l'impulsion fluctue de temps à autre (toutes les 2 secondes environ mais avec une fréquence aléatoire). Voici par exemple une acquisition ou la durée est plus longue :
[t,u1,u2] = np.loadtxt('signal-PB5-D2-DELAI=10-2.csv',unpack=True,delimiter=',',skiprows=21) figure(figsize=(12,6)) plot(t*1e6,u2,label='D11') plot(t*1e6,u1,label='D2') xlim(-10,20) xlabel(r"$t\ (\rm \mu s)$",fontsize=14) ylabel(r"$U\ (\rm V)$",fontsize=14) legend(loc='upper right',fontsize=14) grid()figC.pdf
On constate aussi, moins fréquemment, des fluctuations du délai entre les deux fronts montants de l'ordre de la microseconde et toujours dans le sens de l'augmentation.
La fréquence des interruptions matérielles peut être de plusieurs kHz. Voici par exemple le signal de la sortie D11 lorsque les interruptions sont déclenchées à une fréquence de 10 kHz :
[t,u1] = np.loadtxt('signal-PB5-DELAI=10-3.csv',unpack=True,delimiter=',',skiprows=19) figure(figsize=(12,6)) plot(t*1e6,u1) xlim(-20,180) xlabel(r"$t\ (\rm \mu s)$",fontsize=14) ylabel(r"$U\ (\rm V)$",fontsize=14) grid()figD.pdf
Un filtrage passe-bas appliqué au signal produit permet d'obtenir sa valeur moyenne. Si T est la période du signal sinusoïdal et τ la durée des impulsions, la valeur moyenne obtenue par filtrage passe-bas est :
Cette tension est donc proportionnelle à la fréquence du signal. Nous plaçons après la sortie D11 un filtre RC passe-bas (R=100 kΩ et C=1 μF). Voici le signal en sortie du filtre pour un signa carré de 1 kHz envoyé sur D2 et DELAI = 100 :
[t,u1,u2] = np.loadtxt('signal-PB5-1kHz-filtre-DELAI=100.csv',unpack=True,delimiter=',',skiprows=20) figure(figsize=(12,6)) plot(t*1e3,u2,label='D11') plot(t*1e3,u1,label='D2') xlabel(r"$t\ (\rm ms)$",fontsize=14) ylabel(r"$U\ (\rm V)$",fontsize=14) legend(loc='upper right',fontsize=14) grid()figE.pdf
Voici le résultat pour un signal de fréquence 5 kHz :
[t,u1,u2] = np.loadtxt('signal-PB5-5kHz-filtre-DELAI=100.csv',unpack=True,delimiter=',',skiprows=20) figure(figsize=(12,6)) plot(t*1e3,u2,label='D11') plot(t*1e3,u1,label='D2') xlabel(r"$t\ (\rm ms)$",fontsize=14) ylabel(r"$U\ (\rm V)$",fontsize=14) legend(loc='upper right',fontsize=14) grid()figF.pdf
Nous obtenons ainsi un convertisseur fréquence-tension, qui convertit en tension (entre 0 et 5 V) une fréquence pouvant varier d'un facteur 10. Pour faire fonctionner ce convertisseur avec un signal d'entrée sinusoïdal (ou d'une autre forme), il faut utiliser un comparateur afin de le transformer en signal carré. Voici le signal en sortie du filtre lorsque la fréquence du signal est modulée à 10 Hz :
[t,u2] = np.loadtxt('signal-PB5-5kHz-modul-filtre-DELAI=100.csv',unpack=True,delimiter=',',skiprows=28) figure(figsize=(12,6)) plot(t*1e3,u2,label='D11') xlabel(r"$t\ (\rm ms)$",fontsize=14) ylabel(r"$U\ (\rm V)$",fontsize=14) legend(loc='upper right',fontsize=14) ylim(0,5) grid()figG.pdf
L'objectif est de mesurer la période du signal carré sur D2 cycle par cycle et stocker les valeurs dans un tampon. La période est en fait la durée entre deux fronts montants du signal. Elle peut varier à chaque cycle, auquel cas le signal n'est pas périodique. Pour mesurer la fréquence d'un signal, on peut l'analyser sur une durée grande devant sa période puis faire soit un comptage des fronts montants, soit une analyse spectrale par FFT. Dans certains cas (en particulier si la période est grande ou si elle varie) il peut être nécessaire de mesurer la période cycle par cycle.
Le principe de la mesure est le suivant. Comme précédemment, un front montant sur D2 déclenche une interruption. Dans le gestionnaire d'interruption, on lit la valeur du compteur du Timer 1, qui indique la durée écoulée depuis le front montant précédent puis on déclenche à nouveau le Timer 1. Les valeurs sont stockées dans un double tampon. Lorsqu'un des deux tampons est plein, il est marqué comme disponible pour le traitement (ou pour la transmission à un PC).
On définit tout d'abord la taille d'un tampon et la période maximale que l'on veut mesurer (en microsecondes). Pour plus de précision, on a intérêt à fixer ce maximum au plus juste, car il détermine la période de l'horloge du compteur (plus il est grand, plus la période est grande). Le Timer 1 étant 16 bits, une période égale à la moitié du maximum sera tout de même mesurée avec une précision de .
#include <Arduino.h> #define INTERRUPT 2 #define OUT 11 // PB5 #define BUFSIZE 100 #define MAXPERIOD 1000000 // période maximale en microsecondes
Voici les variables globales :
uint16_t diviseur[6] = {0,1,8,64,256,1024}; uint8_t clock; // diviseur d'horloge pour Timer 1 uint32_t clock_period; // période d'horloge en microsecondes uint16_t count[2][BUFSIZE]; // double tampon uint16_t nBuf,iBuf; uint8_t flag =0; bool dataReady;
Le double tampon sert à mémoriser les valeurs 16 bits fournies par le compteur. Le booléen dataReady sert à indiquer la fin du remplissage d'un tampon, ce qui rend son contenu disponible pour le traitement. La variable nBuf, vallant 0 ou 1, est l'indice du tampon en cours d'écriture. Le tampon disponible pour le traitement est donc l'autre tampon.
La fonction suivante programme et déclenche le Timer 1. Le diviseur d'horloge est défini par un nombre (dans la variable clock). Les diviseurs correspondants sont dans le tableau diviseur. Par exemple clock=2 correspond à une division de la fréquence de l'horloge principale (16 MHz) par 8. La variable top contient la valeur maximale du compteur qui serait nécessaire pour le diviseur d'horloge défini par clock. Si cette valeur est supérieure à 0xFFFF, il faut augmenter le diviseur d'horloge.
void timer1_init(uint32_t maxperiod) { TCCR1A = 0; TCCR1B = 0; TCCR1B |= (1 << WGM12); clock = 1; uint32_t top = (F_CPU/1000000*maxperiod); while ((top>0xFFFF)&&(clock<5)) { clock++; top = (F_CPU/1000000*maxperiod/diviseur[clock]); } clock_period = diviseur[clock] / (F_CPU/1000000); TCCR1B |= clock; }
La fonction startTimer est appelée par interruption matérielle lorsqu'un front montant se présente sur D2. L'état de la sortie PB5 (D11 sur MEGA ou D13 sur UNO) est changé à chaque appel de la fonction. L'indice du buffer est incrémenté puis remis à zéro lorsqu'il atteint la valeur maximale; le tampon est alors déclaré prêt pour le traitement et on change de tampon.
void startTimer() { count[nBuf][iBuf] = TCNT1; // stockage de la valeur du compteur dans le tampon TCCR1B = 0; // initialisation du compteur TCCR1B |= clock; TCNT1 = 0; if (flag) { PORTB &= ~(1<<PORTB5); // digitalWrite(OUT,LOW) flag = 0; } else { PORTB |= (1<<PORTB5); // digitalWrite(OUT,HIGH) flag = 1; } iBuf++; if (iBuf==BUFSIZE) { // fin du tampon if (nBuf) nBuf = 0; // changement de tampon else nBuf = 1; iBuf = 0; dataReady = true; } }
La fonction setup programme l'interruption matérielle pour l'entrée D2 et programme le Timer.
void setup() { Serial.begin(9600); attachInterrupt(digitalPinToInterrupt(INTERRUPT), startTimer, RISING); pinMode(OUT,OUTPUT); digitalWrite(OUT,LOW); timer1_init(MAXPERIOD); iBuf = nBuf = 0; dataReady = false; }
Dans la fonction loop, lorsqu'un tampon est disponible on affiche son contenu sur la console série. Les durées sont converties en microsecondes.
void loop() { if (dataReady) { dataReady = false; uint8_t n = 0; if (nBuf==0) n = 1; for (int i=0; i<BUFSIZE; i++) { Serial.print(i); Serial.print(" : "); Serial.println(count[n][i]*clock_period); } } }
La boucle d'affichage est relativement lente en raison des appels répétés de Serial.print. Il faut que le temps d'exécution de cette boucle soit inférieur à la durée d'acquisition d'un tampon complet. Dans une application réelle, on ne ferait pas cet affichage mais on transmettrait les données par bloc ou bien on ferait d'autres calculs à partir des périodes.
Voici, pour une fréquence de 100 Hz (le signal est fourni par un générateur numérique), les dix premières valeurs affichées sur la console :
0 : 10000 1 : 10000 2 : 10000 3 : 10000 4 : 10000 5 : 10000 6 : 10000 7 : 10000 8 : 10000 9 : 10000
Pour une fréquence de 10 Hz, le remplissage d'un tampon prend 10 secondes, ce qui est largement plus grand que le temps d'affichage des 256 valeurs. Pour réduire le temps de remplissage (donc le temps d'attente entre deux séries d'affichage), il suffit de réduire la taille des tampons (BUFSIZE). Voici les 19 premières valeurs pour cette fréquence :
0 : 100000 1 : 100000 2 : 100000 3 : 100000 4 : 100000 5 : 100000 6 : 100000 7 : 100000 8 : 100032 9 : 100000 10 : 100000 11 : 100000 12 : 100000 13 : 100000 14 : 100000 15 : 100000 16 : 100000 17 : 100000 18 : 100016
Certaines valeurs sont affectées d'une erreur de 16 microsecondes, d'autres d'une erreur de 32 microsecondes. Il faut déterminer si cette erreur vient du générateur de signaux ou du programme arduino.
Voici les résultats pour une fréquence de 2 Hz :
0 : 499968 1 : 499968 2 : 500096 3 : 499968 4 : 500080 5 : 499968 6 : 500096 7 : 499968 8 : 500080 9 : 499968
L'erreur est plus grande, elle atteint 96 microsecondes. Le fait que l'erreur absolue augmente avec la période fait pencher pour une erreur due au générateur de signaux. Le générateur utilisé pour ce test est celui intégré dans l'Analog Discovery 2. La précision en fréquence de ce générateur n'est pas donnée dans ses spécifications.
Le signal sur D2 peut être délivré par l'Arduino lui-même en programmant un Timer pour qu'il génère un signal PWM. Sur l'arduino MEGA, on ajoute la fonction suivante pour générer un signal PWM de période et de rapport cyclique choisis avec le Timer 3 (16 bits) sur la sortie D5 :
// PWM sur OC3A : sortie D5 void timer3_init(uint32_t period, float rapport) { TCCR3A = (1 << COM3A1) | (1 << COM3A0); TCCR3B = 1 << WGM33; uint32_t top = (F_CPU/1000000*period/2); int clock = 1; while ((top>0xFFFF)&&(clock<5)) { clock++; top = (F_CPU/1000000*period/2/diviseur[clock]); } uint16_t ocra = top*(1.0-rapport); ICR3 = top; OCR3A = ocra; TCCR3B |= clock; }
Pour programmer un signal carré de fréquence 2 Hz, on ajoute aussi dans la fonction setup :
pinMode(5,OUTPUT); timer3_init(500000,0.5);
Voici le résultat :
0 : 500000 1 : 500000 2 : 500000 3 : 500000 4 : 500000 5 : 500000 6 : 500000 7 : 500000 8 : 500000 9 : 500000 10 : 500000 11 : 500000 12 : 500000 13 : 500000 14 : 500000 15 : 500000 16 : 500000 17 : 500000 18 : 500000 19 : 500000
Avec ce signal généré par un Timer du microcontrôleur, il n'y a plus aucune fluctuation des périodes mesurées. Nous avons aussi fait ce test avec le signal carré généré par une autre carte Arduino MEGA : les résultats sont identiques. Nous pouvons en conclure que la mesure de période, c'est-à-dire de durée entre deux fronts montants, a bien une précision inférieure à la microseconde. Les fluctuations observées lorsque le signal est délivré par l'Analog Discovery sont bien dues au générateurs de cette carte.
Voici les résultats avec un signal carré de 2 Hz délivré par un générateur numérique (SIGLENT SDG1025) :
0 : 500000 1 : 500080 2 : 499968 3 : 500048 4 : 500048 5 : 500016 6 : 500080 7 : 500016 8 : 500016 9 : 500064 10 : 500016 11 : 500032 12 : 500016 13 : 500000 14 : 500048 15 : 500048 16 : 500016 17 : 500032 18 : 500032 19 : 500032
Comme pour le générateur utilisé plus haut (Analog Discovery), la durée entre deux fronts montants n'est pas constante. La valeur affichée sur le panneau de générateur est 2.000 000 Hz mais la précision cycle par cycle est loin de la précision de fréquence affichée.