[TUTORIEL] Les interruptions - Partie 2

[TUTORIEL] Les interruptions - Partie 2

Message non lude Laetitia » Jeu 13 Aoû 2015 17:52

Bonjour,

Aujourd'hui nous allons plonger un peu plus au cœur du micro-contrôleur et explorer des fonctionnalités un peu avancées ! Il s'agit de la suite du tutoriel sur les interruptions externes : nous allons nous pencher sur les timers et leur utilisation.

Ce tutoriel est une traduction, l'original est disponible ici.

Est-ce que votre programme fait trop de choses à la fois ? Utilisez-vous beaucoup de delay() ou de boucles while() qui ralentissent le reste ? Si oui, votre projet est un bon candidat pour l'utilisation de timers. Dans ce tutoriel, nous allons parler d'AVR et de timers Arduino, et de comment les utiliser pour écrire du code plus performant.

Dans le tutoriel précédent, nous avons vu les bases des interruptions et comment utiliser les interruptions externes, déclenchées par un changement d'état sur une broche ou un événement similaire. Je vous conseille de le relire si vous avez besoin de vous rafraîchir la mémoire sur les interruptions en général.

Ce tutoriel explore les interruptions liées au timers et leurs applications dans les projets Arduino ou les circuits AVR en général. Quasiment toutes les cartes Arduino sont pilotées par des micro-contrôleurs AVR 8 bits, donc quelle que soit votre plateforme de prédilection vous pourrez utiliser les mêmes méthodes.

Qu'est-ce qu'un timer ?

Vous connaissez déjà probablement le concept général d'un timer : quelque chose que l'on utilise pour mesurer un intervalle de temps donné. Dans les microcontrôleurs, c'est le même principe. Vous pouvez paramétrer un timer pour déclencher une interruption à un moment donné. Lorsque ce moment arrive, l'interruption peut servir à déclencher une alerte, exécuter un certain code, ou modifier l'état d'une broche. C'est un peu comme un réveil pour votre AVR.

L'avantage des timers, tout comme les interruptions externes, est de fonctionner de manière asynchrone, indépendamment de votre programme principal. Plutôt que d'exécuter une boucle ou d'appeler millis() régulièrement, vous pouvez laisser un timer faire le travail à votre place pendant que votre code fait autre chose.

Par exemple, imaginons que vous vouliez construire un robot de sécurité. Pendant qu'il parcourt les pièces, vous voulez qu'il fasse clignoter une LED toutes les deux secondes pour faire savoir à de potentiels intrus qu'ils seront vaporisés au moindre faux mouvement. Avec un code classique, vous devriez initialiser une variable pour fixer le délai entre deux clignotements puis vérifier constamment si celui-ci s'est écoulé. Au lieu de ça, vous pouvez paramétrer une interruption sur timer, puis mettre en route ce dernier. Votre LED clignotera parfaitement dans les temps, même lorsque votre programme exécute sa longue routine executerIntrus().

Comment fonctionnent les timers ?

Les timers fonctionnent en incrémentant un compteur (an anglais counter register). Ce registre peut compter jusqu'à un certain nombre, suivant sa taille. Le timer incrémente ce compteur jusqu'à ce qu'il atteigne sa valeur maximale, ce qui provoque un débordement de capacité (an anglais overflow), et se réinitialise à zéro. Normalement le timer active un bit particulier (un drapeau, an anglais flag) pour vous faire savoir qu'il y a eu débordement. Vous pouvez vérifier l'état de ce bit manuellement, ou vous pouvez paramétrer l'interruption pour qu'elle ait lieu dès que le flag est activé. Comme pour n'importe quelle autre interruption, vous pouvez spécifier une routine d'interruption (ISR) pour exécuter le code de votre choix lorsque le timer déborde. L'ISR réinitialisera le flag en même temps qu'elle s'exécutera, c'est donc la solution la plus simple et la plus rapide.

Pour incrémenter le compteur à intervalles réguliers, le timer doit avoir accès à une horloge. Celle-ci génère un signal se répétant périodiquement. À chaque fois que le timer détecte ce signal, il incrémente une fois le compteur.

Parce que les timers sont dépendants de l'horloge, la plus petite unité de temps mesurable sera la période de cette horloge. Par exemple, si l'on fournit un signal d'horloge à 1 MHz à un timer, la résolution du timer (ou période du timer) peut être calculée comme suit :

T = période du timer, f = fréquence de l'horloge

T = 1 / f
T = 1 / 1 MHz
T = 1 / 10^6 Hz
T = 1 * 10^(-6) s

La résolution de notre timer est d'un millionième de seconde. Voilà comment même des processeurs relativement lents peuvent découper le temps en périodes très courtes en utilisant cette méthode.
Vous pouvez également fournir une horloge externe pour les timers, mais la plupart du temps on utilise l'horloge interne du microcontrôleur. Ce qui signifie que la résolution minimale de votre timer sera celle de votre microcontrôleur (c'est-à-dire 8 ou 61 MHz pour la plupart des AVR 8 bits).

Types de timers

Si vous utilisez un Arduino standard ou une carte basée sur un AVR 8 bits, vous avez plusieurs timers à disposition. Dans ce tutoriel, je partirai du principe que vous travaillez avec un ATmega168 ou ATmega328. Cela correspond aux Arduino Uno, Duemilanove, Mini, et beaucoup de designs similaires. Vous pouvez utiliser les mêmes techniques sur d'autres AVR comme ceux de l'Arduino Mega/Mega 2560, il faudra juste penser à ajuster le brochage et vérifier dans la documentation technique les différences en détail.

L'ATmega168 et l'ATmega328 possèdent 3 timers : Timer0, Timer1, Timer2. Ils ont aussi un timer "chien de garde" (en anglais watchdog), qui peut être utilisé comme sécurité ou mécanisme de reset logiciel. Cependant, je ne vous recommande pas de le manipuler avant d'avoir des bases suffisamment solides. Voici quelques détails sur chaque timer :

    Timer0
Timer0 est un timer 8 bits, son compteur peut compter jusqu'à 255 (comme un unsigned byte sur 8 bits). Timer0 est utilisé nativement par l'Arduino pour des fonctions de chronométrage comme delay() et millis(), donc ne le bidouillez pas trop si vous ne savez pas ce que vous faites.

    Timer1
Timer1 est un timer 16 bits, avec un compteur allant jusqu'à 65536 (comme un unsigned int sur 16 bits). La librairie Servo de l'IDE Arduino utilise ce timer, faites-y attention lorsque vous l'utilisez dans vos projets.

    Timer2
Timer2 est un timer 8 bits très similaire à Timer0. Il est utilisé par la fonction tone() Arduino.

    Timer3, Timer4, Timer5
Les ATmega1280 et ATmega2560 (comme sur les Arduino Mega) ont trois timers supplémentaires. Ce sont tous des timers 16 bits, et fonctionnent de la même manière que Timer1.

Configurer et faire tourner le timer

Pour utiliser ces timers, il nous faut les paramétrer, puis les démarrer. Pour ce faire, nous utilisons les registres de l'AVR qui stockent les paramètres des timers. Chaque timer possède un certain nombre de registres ayant des fonctions particulières. Deux de ces registres stockent les valeurs de départ, et sont appelés TCCRxA et TCCRxB, où x est le numéro du timer (TCCR1A et TCCR1B, etc.). TCCR signifie Timer/Counter Control Register : Registre de Contrôle Timer/Compteur. Chaque registre contient 8 bits, et chaque bit permet de stocker un paramètre. Détails provenant de la fiche technique de l'ATmega328 :

TCCR1A_et_B.png
TCCR1A_et_B.png (14.91 Kio) Vu 1896 fois

Les paramètres les plus importants sont les trois derniers bits dans TCCR1B : CS12, CS11 et CS10. Ce sont ceux qui donnent les réglages de l'horloge du timer. Avec différentes combinaisons, on peut demander au timer de fonctionner à différentes vitesses. Voici la table correspondante dans la documentation :

ClockSelectBitDescription.png
ClockSelectBitDescription.png (18.37 Kio) Vu 1896 fois

Par défaut, ces bits sont mis à 0 (timer arrêté). Prenons un simple exemple, et admettons que nous voulions avoir Timer1 qui tourne à la vitesse de l'horloge, avec un compte par cycle d'horloge. Lorsqu'il déborde, nous exécuterons une ISR qui fera clignoter une LED branchée sur D2. Cet exemple sera écrit en code Arduino, en utilisant les routines avr-libc quand elles ne compliquent pas trop les choses. Les pros de l'AVR peuvent adapter comme bon leur semble.

Premièrement, initialisons le timer :

Code: Tout sélectionner
// Inclusion des librairies avr-libc
#include <avr/io.h>
#include <avr/interrupt.h>

#define LEDPIN 2

void setup()
{
   pinMode(LEDPIN, OUTPUT);

   cli();         // désactiver globalement les interruptions
   TCCR1A = 0;      // mettre tout le registre TCCR1A à 0
   TCCR1B = 0;      // pareil pour TCCR1A à 0

   // activer l'interruption sur débordement de Timer1
   TIMSK1 = (1 << TOIE1);
   // activer le bit CS10 pour que le timer fonctionne à la même vitesse que l'horloge
   TCCR1B |= (1 << CS10);
   // activer globalement les interruptions
   sei();
}

Note : voir le tutoriel sur les opérations bit à bit en cas de difficultés.

Vous remarquerez que nous avons utilisé un nouveau registre, TIMSK1 (Timer/Counter1 Interrupt Mask Register). Il contrôle quelles interruptions le timer peut déclencher. Activer le bit TOIE1 indique au timer de déclencher une interruption lorsque le timer déborde. Nous pouvons aussi activer d'autres bits pour déclencher d'autres interruptions. Nous reviendrons là-dessus plus tard.
Une fois le bit CS10 activé, le timer est actif et puisque nous avons autorisé une interruption sur débordement de capacité, il appellera la routine ISR(TIMER1_OVF_vect) à chaque fois que le timer signale un débordement.

Maintenant, nous pouvons définir l'ISR :

Code: Tout sélectionner
ISR(TIMER1_OVF_vect)
{
   digitalWrite(LEDPIN, !digitalRead(LEDPIN));
}

Nous sommes désormais libres de coder notre loop() et la LED clignotera indépendamment de ce qu'il se passe dans le programme principal. Pour désactiver le timer, on peut paramétrer TCCR1B = 0; à n'importe quel moment.

Cela dit, réfléchissons à comment cela va fonctionner. En utilisant ce code, à quelle vitesse va clignoter la LED ?

Nous avons paramétré Timer1 pour déclencher une interruption en cas d'overflow, partons du principe que nous utilisons un ATmega328 avec une horloge à 16 MHz. Puisque Timer1 a une résolution de 16 bits, il peut stocker une valeur maximale de (2^16 – 1) = 65535. À 16 MHz, chaque coup d'horloge aura lieu toutes les 1/(16*10^6) secondes, soit toutes les 62,5 nanosecondes. Ce qui signifie que le compte jusqu'à 65535 sera effectué en (65535 * 6,25e-8s) et notre ISR se déclenchera toutes les, oh... 0,0041 secondes. Et ainsi de suite tous les quatre millièmes de seconde. Oups. À ce rythme, nous ne verrons même pas la LED clignoter. En fait, nous avons créé un signal PWM extrêmement rapide pour la LED, avec un rapport cyclique de 50%, donc vous devriez la voir allumée mais plus faiblement que d'habitude. Une expérience comme celle-ci permet de mettre en évidence le pouvoir les microcontrôleurs : même un petit 8 bits, bon marché, peut traiter les informations largement plus vite que nous ne pouvons le détecter.

Prédiviseur et CTC

Heureusement, les ingénieurs chez Atmel ont pensé à ce problème, et ont ajouté quelques options. En fait, on peut aussi paramétrer le timer pour utiliser un prédiviseur (en anglais prescaler), ce qui permet de diviser votre signal d'horloge par des puissances de 2, en augmentant la période de votre timer. Par exemple, admettons que nous voulions faire clignoter notre LED à intervalles d'une seconde. En revenant à notre registre TCCR1B, on peut utiliser les trois bits CS pour donner une meilleure résolution à notre timer. Si nous activons CS10 et CS12 en utilisant TCCR1B |= (1 << CS10); et TCCR1B |= (1 << CS12);, on divise notre horloge par 1024. Ceci nous donne une résolution de 1/(16*10^6 / 1024) = 6,4e-5 secondes. Maintenant le timer débordera tous les (65535 * 6,4e-5s), soit toutes les 4,194s. Hmm, trop long. Que faire ?

Il se trouve que les timers AVR possèdent un autre mode d'opération. Ce mode est appelé Clear Timer on Compare Match (Redémarrer le Timer sur Comparaison), abrégé en CTC. Au lieu de compter jusqu'à débordement, le timer compare sa valeur à celle qui a été précédemment stockée dans un autre registre. Lorsque le compteur atteint cette valeur, le timer peut, au choix, activer un flag ou activer une interruption, comme dans le cas du débordement.

Pour utiliser CTC, il faut d'abord déterminer jusqu'à combien compter pour avoir notre intervalle d'une seconde. En admettant que l'on conserve le prédiviseur de 1024 comme précédemment, on calcule comme suit :

(temps cible) = (résolution du timer) * (nombre de comptes + 1)

Et on réarrange pour obtenir :

(nombre de comptes + 1) = (temps cible) / (résolution du timer)
(nombre de comptes + 1) = (1 s) / (6.4e-5 s)
(nombre de comptes + 1) = 15625
(nombre de comptes) = 15625 - 1 = 15624

Pourquoi le +1 ajouté au nombre de comptes ? En mode CTC, lorsque le timer atteint la valeur fixée, il se réinitialise à 0. Cela prend un cycle d'horloge supplémentaire, que nous devons prendre en compte dans nos calculs. Dans la plupart des cas, un simple cycle ne posera pas de problème, mais si vous voulez respecter des contraintes de temps réel ça peut suffire à faire la différence.

Maintenant nous pouvons réécrire notre setup() pour configurer le timer avec ces réglages :

Code: Tout sélectionner
void setup()
{
   pinMode(LEDPIN, OUTPUT);

   // initialisation Timer1
   cli();       // désactivation globale des interruptions
   TCCR1A = 0;    // mise de tout le registre TCCR1A à 0
   TCCR1B = 0;    // pareil pour TCCR1B

   // fixer la valeur du registre à comparer :
   OCR1A = 15624;
   // activer le mode CTC :
   TCCR1B |= (1 << WGM12);
   // Activer CS10 et CS12 pour le prédiviseur à 1024 :
   TCCR1B |= (1 << CS10);
   TCCR1B |= (1 << CS12);
   // activer l'interruption sur comparaison du timer :
   TIMSK1 |= (1 << OCIE1A);
   sei();       // activation globale des interruptions
}

Et nous devons remplacer l'ISR de débordement par une se déclenchant sur une comparaison :

Code: Tout sélectionner
ISR(TIMER1_COMPA_vect)
{
   digitalWrite(LEDPIN, !digitalRead(LEDPIN));
}

Et voilà ! Notre LED va maintenant clignoter à intervalles de précisément une seconde. Et comme toujours, nous pouvons faire ce qui nous plaît dans la loop(). Du moment que l'on ne touche pas aux réglages du timer, il n'interfèrera pas avec nos interruptions. Avec d'autres modes et réglages de prédiviseurs, il n'y a aucune limite à comment vous utilisez les timers.

Voici le code exemple complet au cas où vous voudriez l'exploiter comme point de départ de votre propre projet :

Code: Tout sélectionner
// Exemple d'interruption CTC sur timer pour Arduino
// www.engblaze.com
// Adaptation française par Snootlab

// Inclusion des librairies avr-libc
#include <avr/io.h>
#include <avr/interrupt.h>

#define LEDPIN 2

void setup()
{
   pinMode(LEDPIN, OUTPUT);

   // initialisation Timer1
   cli();       // désactivation globale des interruptions
   TCCR1A = 0;    // mise de tout le registre TCCR1A à 0
   TCCR1B = 0;    // pareil pour TCCR1B

   // fixer la valeur du registre à comparer :
   OCR1A = 15624;
   // activer le mode CTC :
   TCCR1B |= (1 << WGM12);
   // Activer CS10 et CS12 pour le prédiviseur à 1024 :
   TCCR1B |= (1 << CS10);
   TCCR1B |= (1 << CS12);
   // activer l'interruption sur comparaison du timer :
   TIMSK1 |= (1 << OCIE1A);
   // activation globale des interruptions
   sei();
}

void loop()
{
   // faire des trucs de fou pendant que ma LED clignote
}

ISR(TIMER1_COMPA_vect)
{
   digitalWrite(LEDPIN, !digitalRead(LEDPIN));
}

Pour aller plus loin

Gardez à l'esprit que vous pouvez utiliser les ISR prédéfinies pour étendre les fonctionnalités des timers. Par exemple, si vous voulez lire un capteur toutes les 10 secondes, il n'y a aucun timer qui peut aller aussi loin sans déborder. Cependant, vous pouvez utiliser l'ISR pour incrémenter une variable dans votre programme une fois par seconde, et enfin lire le capteur lorsque cette variable atteint 10. En utilisant la même configuration CTC que dans l'exemple précédent, notre ISR ressemblerait à ça :

Code: Tout sélectionner
ISR(TIMER1_COMPA_vect)
{
   secondes++;
   if (secondes == 10)
   {
      secondes = 0;
      lireMonCapteur();
   }
}

Note : pour qu'une variable soit modifiable à l'intérieur d'une ISR, elle doit être volatile. Dans notre cas, nous devrions déclarer volatile byte secondes; ou quelque chose de similaire au début de notre programme (voir tutoriel précédent)

Ce tutoriel couvre les bases des timers. Au fur et à mesure que vous comprendrez les concepts se cachant derrière tout ça, vous voudrez chercher davantage d'informations dans la documentation technique spécifique à votre microcontrôleur. Ces documents sont disponibles sur le site d'Atmel. Pour les trouver, cherchez la page spécifique à votre modèle (les AVR 8 bits se trouvent ici) ou cherchez directement sa référence. Il y a beaucoup d'informations à éplucher mais la documentation n'est pas si indigeste pour peu qu'on y passe un peu de temps.

Où trouver le nécessaire ? Documentation technique > Chapitres "Timer/Counter..." > pour l'Arduino Uno, en utilisant la documentation jointe à ce tutoriel :
  • Page 94 et suivantes la description de Timer0
  • Page 113 et suivantes la description de Timer1
  • Page 141 et suivantes la description des prédiviseurs de Timer0 et Timer1
  • Page 144 la description de Timer2

C'est tout pour cette fois ! J'espère que ce tutoriel vous aura plu, et bonne bidouille en attendant le prochain !

Source
Fichiers joints
Datasheet ATmega328P.pdf
(12.52 Mio) Téléchargé 107 fois
"If it's itchy, scratch it !" - "DIY or die"

RTFM (À lire avant de poster) - ANDb (Arduino Noob Database)
Avatar de l’utilisateur
Laetitia
 
Messages: 296
Inscription: Mar 7 Aoû 2012 15:07
Localisation: Toulouse

Retourner vers Logiciel Arduino

Qui est en ligne

Utilisateurs parcourant ce forum: Aucun utilisateur enregistré et 1 invité

cron