Le microcontrôleur ATMEGA328P est très répandu, puisqu’il est utilisé pour les cartes Arduino Uno.

Il possède trois Timers :

Le Timer0 ;

Le Timer1 ;

Le Timer2.

 

Tout utilisateur de la carte Arduino Uno  a déjà utilisé au moins une fois ces périphériques internes dans son programme ne serait ce que lorsqu’il fait appel aux fonctions prédéfinies :  mcros(),  millis()delay(), ou delayMicroseconds().

 

Cet article d'introduction sur les Timers de l'ATMEGA328P abordera les points suivants:

> Présentation globale des Timers;

> Les interruptions du Timer;

> Des exemples de mise en oeuvre :

            - Faire clignoter une LED toutes les 0,5s en utlisant le Timer en débordement;

            - Faire clignoter une LED toutes les 0,5s en utilisant le Timer en comparaison;

            - Faire tourner deux interruptions en même temps sur le microcontrôleur;

> Une application pratique des interruptions Timer: Mesure de la vitesse de rotation d'un moteur.

 

Présentation globale des Timers:

C’est quoi un Timer ?

Un Timer ou Timer / Counter, est une sorte d’horloge intégrée au microcontrôleur qui permet de mesurer la durée d’un événement. Par exemple pendant combien de temps un port d’entré est resté à l’état haut?  Un Timer peut fournir des fonctions de comptage, de mesure de  durée  ou de génération de signaux, dans un montage.

Ces événements qui font l’objet des mesures, peuvent être internes  ou externes.

Chaque Timer possède des registres de configuration internes, mais nous n’allons pas tous les présenter  ici, il faudra donc se référer à la documentation technique du composant pour des renseignements plus détaillés concernant leur utilisation.

Un certain nombre de ces registres  joue un rôle très important dans la programmation des Timers :

Mais avant de les citer, juste une petite convention d’écriture :  "x" représente le Timer (0, 1, 2),  "y" sera mis pour A ou B.

TCCRx : Timer Counter Control Register ;  c’est dans ce registre que l’on pourra configurer le Timer x.

TCNTx : Timer Counter Register ; c’est ici qu’on retrouve la valeur de comptage actuelle du Timer x.

OCRx :   Output Compare Register.

ICRx :    Input Compare Register.

TIMSKx :Timer Conuter Interrupt Mask.  Ce registre contient des bits qui permettent d’activer ou de désactiver les interruptions.

TIFRx :   Timer Counter Interupt Flag Register.  Ses bits indiquent l’interruption en cours.

 

Le Timer 0 et le Timer 2  fonctionnent sur 8 bits c’est-à-dire qu’ils peuvent compter de 0 à 255 (28 - 1) ; tant dis que le Timer 1 quant à lui est sur 16 bits; il est capable de compter de 0 à 65535 (216 -1).

Lorsqu’un Timer atteint sa valeur maximale de comptage (255 pour le Timer 0 et Timer 2, ou 65535 pour le Timer 1), il est réinitialisé à 0 puis le comptage recommence, jusqu’à sa valeur maximale de nouveau, puis ainsi de suite, le cycle se poursuit.

cycle timer

Grâce à ce mode de fonctionnement, une interruption peut être déclenchée lorsqu’un Timer atteint sa valeur maximale ou alors une valeur intermédiaire fixée par le programmeur.

 

cycle timer 2

Les interruptions du Timer:

Lorsque une interruption survient, le microcontrôleur arrête le programme qu’il était en train d’exécuter, puis traite en priorité la routine d’interruption, avant de poursuivre avec  le programme en cours.

Chaque Timer peut générer un ou plusieurs types d’interruptions.

Pour les Timers 0 et 2 :

Interruption par débordement (Overflow), dont la routine dans l’environnement de développement Arduino (IDE) sera : ISR(TIMERx_OVF_vect).

Interruption par comparaison (Compare Match), dans l’IDE Arduino sera : ISR(TIMERx_COMPy_vect).

Pour le Timer 1 :

On retrouve les mêmes interruptions citées précédemment. Il faut noter que les Timer 0 et 1, on a en plus, l’Interruption par changement d'état d'une broche spécifique, dans l’IDE arduino sera : ISR(TIMERx_CAPT_vect).

Avant de poursuivre, voyons comment fonctionne une interruption par comparaison?

Une valeur de comparaison est chargée dans un registre, lorsque la valeur comptée par le Timer est égale à celle du registre en question, une interruption est générée.

 

Pour qu’une interruption puisse être exécutée, il faut que deux conditions soient réunies :

L’interruption doit être activée ;

Le bit correspondant de l’interruption doit être positionné dans le registre de masque d’interruption.

 

L’horloge ou base de temps du Timer peut être interne c’est-à-dire à partir de l’horloge du microcontrôleur, ou alors externe.

On va se contenter ici de faire fonctionner les Timers  à partir de l'horloge interne au composant.

Un petit mot pour clore cette présentation

Dans l’environnement de développement Arduino,  les fonctions prédéfinies sei() et cli() permettent respectivement d’activer ou de désactiver les interruption. On peut aussi utliser les fonctions prédéfinies interrupts(), ou noInterrupts().

L’usage des fonctions  attachInterrupt() et detachInterrupt(), est réservée aux interruptions externes.

 

Exemples  de mise en œuvre :

Les exemples sur les Timers seront mis en œuvre avec la carte Arduino Uno, la figure ci-après montre l’équivalence entre les broches du microcontrôleur et les connecteurs disponibles de la carte Arduino Uno :

 

Atmega pin mapping

sources: site arduino/cc

Exemple 1 :  on veut écrire un programme qui fait clignoter une LED toutes les 0,5s.

L’horloge utilisée par le microcontrôleur ATMEGA328P dans la carte Arduino est de 16MHz ; ce qui veut dire que chaque impulsion aura une durée de \[\frac{1}{16 000000}s\] Soit 62,5ns ou 62,5x10-9s

Sachant qu’on veut compter jusqu’à 0,5s le Timer doit compter jusqu’à  8000000 avant de déclencher une interruption qui va allumer ou éteindre la diode. Ce chiffre est carrément énorme, car on a vu que la valeur maximale des Timers 0 et 2 était de 255. Même si on prend le Timer 1, lui aussi est limité à une valeur de 65535. La solution à ce problème revient donc à diviser cette fréquence de 16MHz  par 256 (autrement dit à multiplier 62,5ns par 256) ce qui nous donnera 16µs. En divisant 0,5s par 16µs, on obtient 31250. Le Timer aura désormais besoin de compter que jusqu’à 31250, pour faire clignoter la LED toutes les 0,5s.  Le Timer 1 sera plus approprié à cet effet, si on divise d'abord l'horloge par 256.

Heureusement,  le constructeur du microcontrôleur a tout prévu en insérant un pré-diviseur dans la puce du composant.

On a donc besoin de 2 registres principaux pour contrôler les Timers, ils sont nommés  TCCRxA (Timer Counter Control Register A) et TCCRxB.  Ce qui, pour le Timer 1 s’appellera TCCR1A et TCCR1B.

Le registre TCCR1A permet au Timer 1 de fonctionner  en mode PWM (Pulse Wide Modulation ou modulation par largeur d’impulsions),  afin de générer une telle forme de signal sur les broches 9 et 10 du composant (voir figure ATMEGA Pin mapping ci-dessus). 

Pour l’exemple du clignotement de la LED, on va utiliser uniquement le registre TCCR 1B. Les bits CS10, CS11 et CS12 de ce registre permettront de positionner le pré diviseur.  Pour une division par 256, on aura CS12 = 1,  CS11 = 0 et CS10 = 0:

 

Registres Timer

 

Selon cet extrait de la documentation technique, on peut faire une division par 1, 8, 64, 256, 1024.

Les valeurs de comptage sont stockées dans un registre interne nommé TCNT1 ; celui-ci est formé de 2 registres à 8 bits, puisque le Timer 1 fonctionne sur 16 bits. Sa remise à zéro consiste à écrire la valeur 0 dans son contenu.

Un autre registre très important est utile pour le Timer 1, il s’agit du registre TIMSK1. 

registretimsk1atmega328p

 

Il permet de valider les interruptions sur entrée de capture, par comparaison A et B, puis par débordement du Timer 1. Pour plus de détails sur ce registre, il faut se reporter à la documentation technique du composant. Puisque nous voulons déclencher l'interruption lorsque le Timer 1 déborde, seul le bit TOIE1 nous intéresse.

Voici  une idée de code de mise en œuvre du Timer 1, dans laquelle on va se contenter d’allumer une LED, pendant 0,5s puis l’éteindre sur la même durée, en utilisant le fonctionnement  par débordement :

 

/*****using Timer 1 first example – 02/16/2022  by JTBB */
//Variables
bool LED_state = LOW ;
const int LED_pin = 12 ;

//Setup
void setup() {
     pinMode(LED_pin, OUTPUT);
     TCCR1A = 0 ;
     TCCR1B = 0 ;
     TCNT1 = 0 ;
     TCCR1B |= (1 << CS12) ; //prescaler to 256, we can also use binary notation according to the compiler
     TIMSK1  |= (1 << TOIE1) ; //enable Timer 1 overflow

}

void loop(){ //put your main code here

}

ISR(TIMER1_OVF_vect) {
     LED_state = !LED_state ; //invert the state
     digitalWrite(LED_pin, LED_state) ; //of this pin
}

 

Il faut juste noter dans ce programme donné en exemple que l’opérateur  «| » permet d’écrire individuellement des valeurs  « 1 » dans les bits du registre concerné; en revanche si on voulait écrire des « 0 », il faudra utiliser l’opérateur « & ».

 

En exécutant ce programme, on remarque qu'il ne fonctionne pas tout à fait comme on pouvait s’y attendre. En effet la LED reste allumée pendant 1s environ puis s’éteint pendant la même durée. Ce ne sont pas les 0,5s prévu par le programme, malgré un pré – diviseur positionné pour faire des divisions de la base de temps par 256. 

Pourquoi ce résultat ?  En effet notre exemple fonctionne en débordement du Timer 1 ; cela veut dire que le registre TCNT1 commencera le comptage à partir de 0 puis ira jusqu’à 65535, il y aura débordement, donc une interruption ;  puis le cycle recommencera.

Si on fait un petit calcul, déjà effectué dans les lignes précédentes, avec une horloge de 16MHz, si on divise par 256, on aura une impulsion toutes les 16µs.  Ensuite, 16µs x 65535 = 1 ; du moins pas exactement 1, mais 1,04856s

Si on veut faire clignoter la LED toutes 0,5s ; il faudra plutôt stocker le nombre d’impulsions souhaitées dans un registre appelé OCR1A,  puis comparer le contenu de ce registre au contenu du registre TCNT1. En cas d’égalité des deux valeurs une interruption sera exécutée.  C’est le mode Compare/Match ;  on commencera par positionner le bit OCIE1A à 1.

 

Exemple 2: Reprenons l’exemple précédent

/*****using Timer 1 second example – 02/16/2022 edited by JTBB */

//Variables
bool LED_state = LOW ;
const int LED_pin = 12 ;
//Setup

void setup() {

  pinMode(LED_pin, OUTPUT);
  TCCR1A = 0 ;
  TCCR1B = 0 ;
  TCCR1B |= (1 << CS12) ; //prescaler to 256, we can also use binary notation according to the compiler
  TIMSK1 |= (1 << OCIE1A); //enable compare match
  OCR1A = 31250; //set this value to the register to match timer 1

}

void loop(){ //put your main code here

}

ISR(TIMER1_COMPA_vect) {

  TCNT1 = 0; //reset this for next interrupt
  LED_state = !LED_state ; //invert the state
  digitalWrite(LED_pin, LED_state) ; //of this pin

}


En effet nous obtenons un clignotement de la LED toutes les 0,5s.  Dans la routine d’interruption, le contenu de TCNT1 sera remis à 0, afin de relancer le cycle.

 

Exemple 3:

Modifions les exemples précédents en mettant en oeuvre deux interruptions Timer en même temps. Une fera clignoter une LED sur la broche numérique 12, et l'autre fera clignoter une LED sur la broche 10, à une fréquence différente.

Pour cela on va se servir en plus du Timer 2, en plus du Timer 1 déjà mis en œuvre.

 

/*****using Timer 1 third example – 02/16/2022 edited by JTBB */

//Variables
bool LED1_state = LOW;
bool LED2_state = LOW;
const int LED1_pin = 12;
const int LED2_pin = 10;

//Setup

void setup() {

  pinMode(LED1_pin, OUTPUT);
  pinMode(LED2_pin, OUTPUT);
  //for Timer 1 interrupt 0,5s
  TCCR1A = 0 ;
  TCCR1B = 0 ;
  TCCR1B |= (1 << CS12) ; //prescaler to 256, we can also use binary notation according to the compiler
  TIMSK1 |= (1 << OCIE1A); //enable compare match
  OCR1A = 31250; //set this value to the register to match timer 1

  //for Timer 2 interrupt 12,5ms

  TCCR2A = 0;
  TCCR2B = 0;
  TCCR2B |= (1 << CS22)|(1 << CS21)|(1 << CS20); //prescaler = 1024
  TIMSK2 |= (1 << OCIE2B); //enable Timer 2 compare match
  OCR2B = 195;

}

void loop(){ //put your main code here

}

//timer 2 interrupt vector

ISR(TIMER2_COMPB_vect) {

  //
  TCNT2 = 0 ; //reset this for next interrupt
  LED2_state = !LED2_state ; //invert the state
  digitalWrite(LED2_pin, LED2_state) ; //of this pin

}

//timer 1 interrupt vector

ISR(TIMER1_COMPA_vect) {

  TCNT1 = 0; //reset this for next interrupt
  LED1_state = !LED1_state ; //invert the state
  digitalWrite(LED1_pin, LED1_state) ; //of this pin

}

 

Rien de bien significatif au niveau visuel puisque la diode pilotée par le Timer 2 clignote beaucoup trop rapidement. Il faut remarquer que le Timer 2 fonctionne sur 8 bits, donc il ne pourra compter que de 0 à 255. Cela signifie que si par exemple on veut faire clignoter la diode pour 0.125s ; la valeur de pré chargement de 1953, sera illégale pour le registre OCR2B, et de ce fait, en fonction du  compilateur utilisé, celle-ci sera tronquée ; par conséquent le résultat attendu ne sera pas au rendez-vous puisqu’on risque d’obtenir n’importe quoi.  On s’est contenté de mettre la valeur 195 qui en théorie nous donnera un clignotement de 12,5ms environ, et la LED est constamment allumée.  On  ne pourra pas utiliser le Timer 2, pour générer un signal de durée 0.5s pour allumer ou éteindre la LED; à moins de modifier la valeur du quartz de la carte Arduino. 

Pour mettre en œuvre une interruption externe, par exemple un comptage d’objets qui passe devant un capteur, ou la génération d’un signal PWM, pour réguler la vitesse d’un moteur à courant continu, il faudra utiliser les broches dédiées du microcontrôleur.

Application des Timers du microcontrôleur ATMEGA328P:

Voici la problématique :  La structure mécanique d'une mini perçeuse manuelle de circuit imprimés, vieille de plus de vingt ans, ne permettait plus d’assurer un bon perçage (trous voilés, pastilles du circuit imprimé qui sautent, à cause d’un mandrin pourrit, …à moins d’être recalcitrant pour à tout prix l’utiliser, inutile d’aller appeler le voisin au secours après..). 

La perceuse a dont été entièrement démontée, puis le moteur récupéré. Mais aucune caractéristique inscrite sur celui-ci. 

On se propose de mesurer la vitesse de ce moteur à courant continu lorsqu’il est alimenté en +12V. Il est possible de pousser l'étude plus loin afin d'en déterminer d'autres paramètres mais cela ne pourra pas être fait dans cet article.

Pour mesurer la vitesse de rotation d'un moteur, il faut disposer d’un tachymètre.  Mais problème n’avons pas ce type de matériel à portée de main.  Alors il faut en fabriquer un.

Le tachymètre que nous voulons réaliser permettra de mesurer la vitesse de rotation du moteur  en utilisant une fourche optique à rupture de faisceau (la raison de choix ce réside sur la fait c'est ce que nous avons sous la main). On peut très bien utiliser un capteur optique à réflexion à Infrarouge, on devra obtenir le même résultat.

De quoi avons nous besoin?

  • Une carte Arduino Uno;
  • Un capteur optique à rupture de faisceau (un capteur à réflexion serait plus adapté pour ce genre d'utilisation, et pourra fonctionner sans problème avec notre exemple);
  • Une résistance de 180 Ohms;
  • Une résistance de 10 KOhms;
  • Une petite plaque de soudure à trous pastillés.

 

maeriel capteur optique1

 

 

Après sourdure, voici le capteur monté:

face composant capteur optique1

 

Pour mettre en place le dispositif de mesure, on a d'abord collé, puis renforcé avec du stoch transparent, un petit morceau de plastique blanc, sur un coupleur fixé sur l'arbre moteur. Il vaut mieux utiliser le montage tel qu'il fait dans le cas présent pour de faibles vitesses de rotation, pour les grandes vitesses ce morceau de plastique doit obligatoirement être remplacé par un autre système plus rigide, au besoin fixé sur l'arbre moteur par un autre système autre que la colle.

 

moteur a tester obturateur1

 

C'est ce bout de plastique qui passera dans la fourche du capteur. Comme illustré par la photo, ci-après:

capteur monte sur arbre moteur1

 

La figure ci-après montre le dispositif d'essai definitif avec capteur monté :

banc de test du moteur1

 

La sortie d'impulsions du capteur sera connecté sur l'entrée numérique 8 de la carte Arduino Uno.

Le programme:

Principe de la mesure: Lorsqu'une impulsion arrive sur l'entrée numérique 8 de l'Arduino, celle-ci va déclencher une interruption sur son front montant. Aussitôt un compteur interne démarre, puis s'arrête à la prochaine impulsion. Le nombre d'impulsions comptées n'aura de sens que si on connait la durée entre deux impulsions. D'où l'intérêt d'utiliser l' interruption en mode capture. Ainsi si l'on tient compte de la durée d'une impulsion d'horloge (62,5ns), du nombre nombre d'impulsions enregistrées entre deux front montants, on peut calculer la vitesse en tours par secondes, et faire la conversion en tours par minutes.

Le résultat sera donc envoyé par liaison série depuis l'Arduino vers le PC et affiché à l'écran.

Ci dessous, une idée possible du programme:

 

//***********************************************************
// Easy Tachometer                                          *
// Edited: 02/19/2022; by JTBB                              *
// We use Timer1 overflow interrupt to count incoming pulses*
// to Uno Digital Pin 8.                                    *
//***********************************************************

//Variables
 
volatile unsigned long CountOvf;
volatile unsigned long startTime;
volatile unsigned long endofTime;

volatile boolean Start;
volatile boolean readyToGo;

// timer overflows vector
ISR (TIMER1_OVF_vect)
{
   CountOvf++;
}

// timer capture vector
ISR (TIMER1_CAPT_vect)
{
   unsigned int timer1Value;
   timer1Value = ICR1;
   unsigned long CountOvfOld = CountOvf;

// if just missed an overflow
   if ((TIFR1 &(TOV1 == 1)) && timer1Value < 0x7FFF){
        CountOvfOld++;
     }

// wait until we noticed last one
   if (readyToGo){
        return;
     }

   if (Start)
     {
       startTime = (CountOvfOld << 16) + timer1Value;
       Start = false;
       return;
     }

    endofTime = (CountOvfOld << 16) + timer1Value;
    readyToGo = true;
    TIMSK1 = 0; //reset interrupt now
}

void SetupForInterrupts ()
{
    noInterrupts (); //disable interrupts
    Start = true;
    readyToGo = false; //get ready for next time
    //
    TCCR1A = 0;
    TCCR1B = 0;

    TIFR1 |= (1 << ICF1) | (1 << TOV1); // clear flags
    TCNT1 = 0; // Timer1 counter to 0
    CountOvf = 0; //

    // Timer 1 - counts clock pulses
    TIMSK1 |= (1 << TOIE1) | (1 << ICIE1); // Timer 1 overflow and input capture
    // start Timer 1, no prescaler
    TCCR1B |= (1 << CS10) | (1 << ICES1); // Input Capture Edge Select
    interrupts (); //enable interrupts now
}

void setup ()
{
    Serial.begin(115200);
    Serial.println("Vitesse de rotation: ");

    SetupForInterrupts (); // set up for interrupts
}

void loop ()
{
    // wait until a measured value change
    if (!readyToGo){
       return;
     }

    // period is elapsed time
    unsigned long elapsedTime = endofTime - startTime;
    float rpm = round(60*(16000000.00 / float (elapsedTime)));
    // clock 16 MHz

    if (rpm <= 20) {
          Serial.println("Lent ou arrêt");
       }
    else{
          Serial.print ("Vitesse: ");
          Serial.print (rpm);
          Serial.println (" Trs/mn. ");
       }
    // This delay is for Serial print stuff
    delay (500);

    SetupForInterrupts ();
} // end

La vidéo ci-après montre les types d'interruptions abordées ainsi que le montage d'application en action:

 

 

Voici quelques caractéristiques de notre tachymètre à affichage sur l'écran de l'ordinateur:

 

Le tachymètre peut faire des mesures de 60 trs/mn à 600000 trs/mn (en théorie. Sachant que 600000trs/mn c'est déjà très énorme encore faut-il que le temps de réponse du capteur soit au rendez-vous) ;

  • Erreur à 120 trs/mn      +/- 0.2%
  • Erreur à 6000 trs/mn     +/-0.2%
  • Erreur à 60000 trs/mn   +/-0.2%
  • Erreur à 600000 trs/mn +/-0.2%
  • Sensibilité                   6 trs/min.

Ces données n'ont pas été évaluées dans des conditions réelles d'utilisation, mais plutôt à l’aide d’un générateur de fonction fournissant un signal carré 0 – 5V, de rapport cyclique 20%. La sortie du générateur connectée sur la broche d'E/S numérique 8 de l'Arduino.  Il faudra juste noter que le quartz de l’Arduino  lui aussi est entâché d’une erreur dont on peut aussi tenir compte, celle-ci n’a pas été évaluée ici.

D’autres idées de mise en oeuvre existent pour obtenir le même résultat, par exemple on peut se contenter d’utiliser les fonctions prédéfinies comme par exemple millis(), ceci sans passer par la manipulation directe des registre, et des Timers.

Cet article avait pour but une présentation rapide des Timers présents dans nos cartes Arduino Uno, et de montrer comment ceux-ci pouvaient être utilisés sans avoir besoin de faire appel aux fonctions prédéfinies dans l'IDE Arduino.

D'autres fonctionnalités qui peuvent être fournies par ces Timers n'ont pas été abordées non plus. Il convient de se reporter à la documentation technique du constructeur, pour avoir une présentation détaillée sur ces périphériques que sont les Timers.

Pour finir, le montage électronique d'application sur le tachymètre est loin d’être fini, on s'en doute. En tout cas, il nous a permis de mesurer la vitesse de rotation de notre moteur afin d’obtenir quelques unes de ses caractéristiques sans avoir besoin d’acheter un appareil rien que pour ce besoin ponctuel.  Pour être vraiment utile et portable, il faudrait se passer du PC pour afficher les résultats.  On peut remplacer le capteur à fourche par un capteur à réflexion (avantage cela évitera tout contact avec le dispositif fesant l'objet de la mesure), puis on peut ajouter un écran LCD, une alimentation à pile et une carte Arduino compatible au format plus petit. Ainsi, on peut obtenir un joli module compact, comme il en existe dans plein d'articles sur le net et aussi dans le commerce.