Dans l’article précédent « Les Timers (I) », nous avons étudié quelques généralités sur les timers et nous avons insisté sur le fait qu’il est primordial de bien connaître les registres de contrôle associés aux timers et de bien comprendre leur utilisation. Nous avons réalisé un premier programme faisant clignoter la DEL du module Arduino Uno grâce au timer 2, en surveillant son débordement un certain nombre de fois pour agir sur la DEL.
La surveillance du débordement du timer revient à surveiller le flag TOV2 , ce qui prend du temps et interfère avec le déroulement du programme principal. Afin de libérer le programme de cette tâche de surveillance, nous allons faire appel aujourd’hui aux interruptions commandées par les timers. Une fois de plus, nous utiliserons le timer 2, mais ce que nous allons faire peut aussi se concevoir pour les autres timers du microcontrôleur.
Qu’est-ce qu’une interruption ?
C’est ce qui nous arrive à tous dans notre vie de tous les jours. Imaginez que vous soyez au téléphone et que l’on sonne à votre porte. Vous demandez à votre interlocuteur de patienter, vous posez votre téléphone puis vous allez ouvrir. C’est le facteur qui vous apporte un colis. Une fois celui-ci réceptionné et la porte refermée, vous retournez prendre votre téléphone pour continuer votre conversation. L’interruption (sonnerie) vous a fait vous détourner de votre programme principal (conversation téléphonique) pour aller réaliser un sous-programme (ouvrir la porte) puis vous avez repris votre programme principal là où vous l’aviez laissé (si vous n’avez pas perdu le fil de votre conversation !).
Un microcontrôleur fonctionne de la même façon : un événement interne (timer) ou bien externe (broche) demande une interruption au microcontrôleur (voir aussi « Les interruptions (1) »). Celui-ci va s’interrompre dans son programme principal, puis va exécuter un sous-programme (on parle de routine d’interruption). Une fois que cette routine est exécutée, le microcontrôleur reprend son programme principal, là où il s’était arrêté, et continue son exécution. Pour cela, il a fallu que le microcontrôleur sauve différents registres sur une pile et les rétablisse en fin de routine d’interruption. Il faut aussi que le programmeur ait autorisé les interruptions ; si le programme doit réaliser une tâche selon une chronologie très rigoureuse, il peut être nécessaire de ne pas les autoriser pour ne pas déranger le microcontrôleur et c’est donc au programmeur de décider d’autoriser ou non les interruptions. La fonction sei()
permet d’autoriser les interruptions alors que la fonction cli()
permet de les ignorer ; on peut aussi utiliser les fonctions interrupts()
et noInterrupts()
du langage Arduino.
L’interruption du timer 2
Toutes les demandes d’interruption arrivent sur une porte OU et sont éventuellement bloquées par le signal GIE (Global Interrupt Enable) activé par la fonction sei()
. La figure suivante nous permet de comprendre comment le timer 2 peut faire une demande d’interruption.
- Architecture de pilotage d’un timer
- Ce circuit se trouve à l’intérieur du microcontrôleur qui équipe le module Arduino Uno.
La figure se lit de la droite vers la gauche : le signal d’horloge de 16 MHz arrive au prédiviseur qui divise la fréquence en fonction des bits 0 à 2 du registre TCCR2B (voir la première partie de l’article). Le timer TCNT compte ainsi moins rapidement. Lorsqu’il déborde, une bascule est positionnée à 1 : c’est le flag TOV2 que l’on retrouve dans le bit 0 du registre TIFR2. Pour autoriser une interruption par le timer, il faut que le flag TOIE2 (Timer/Counter2 Overflow Interrupt Enable) qui est le bit 0 du registre TIMSK2 (Timer/Counter2 Interrupt Mask Register), soit à 1 ; c’est ce qu’on appelle une autorisation d’interruption locale. Pour que notre timer déclenche une interruption, il faut qu’il y soit autorisé ( TOIE2 à 1) ET qu’il déborde ( TOV2 à 1), ces deux conditions arrivant sur une porte ET en amont de la porte OU. Mais comme on l’a dit plus haut, il faut aussi que les interruptions soient autorisées de façon générale : c’est le rôle du flag GIE , le bit 7 du registre SREG (Status Register) qui alimente une porte ET en aval de la porte OU.
Si toutes les conditions que nous venons de voir sont réunies, l’interruption arrive au microcontrôleur qui va donc interrompre son programme principal pour aller exécuter sa routine d’interruption (ISR pour Interrupt Routine Service). Le système Arduino a prévu une routine au nom réservé qu’il suffit de compléter : celle-ci commence par ISR(TIMER2_OVF_vect)
signifiant qu’il s’agit d’une routine d’interruption déclenchée par le vecteur d’overflow du timer 2. Cette routine remet automatiquement à 0 le flag TOV2 , ce qu’il fallait faire soi-même lorsqu’on surveillait le flag sans faire appel aux interruptions (programme de la première partie de l’article).
Programme par interruption
Nous allons donc modifier le programme vu dans la première partie pour que l’inversion de la DEL se fasse par une interruption provenant du timer 2. Les autres conditions restent identiques : initialisation, réglage du prédiviseur (voir le précédent programme). La boucle principale peut accueillir le programme de l’utilisateur ; à titre d’exemple, nous avons mis un programme très classique faisant clignoter une autre DEL (branchée sur la broche 5 d’Arduino) en utilisant la fonction delay()
. Bien que cette fonction soit bloquante, cela n’empêche pas la DEL reliée à la broche 13 de clignoter.
// Clignotement d'une LED (mot anglais pour DEL) à 1 Hz par une
// interruption en provenance du timer 2,
// pendant que le programme principal fait
// clignoter une autre LED (période 10s)
const byte Led = 13; // Pour utiliser la LED du module
const byte Led2 = 5; // Led clignotante du programme principal
#define LedToggle digitalWrite (Led, !digitalRead(Led))
#define Led2Toggle digitalWrite (Led2, !digitalRead(Led2))
void setup () {
pinMode (Led, OUTPUT);
pinMode (Led2, OUTPUT);
cli(); // Désactive l'interruption globale
bitClear (TCCR2A, WGM20); // WGM20 = 0
bitClear (TCCR2A, WGM21); // WGM21 = 0
TCCR2B = 0b00000110; // Clock / 256 soit 16 micro-s et WGM22 = 0
TIMSK2 = 0b00000001; // Interruption locale autorisée par TOIE2
sei(); // Active l'interruption globale
}
byte varCompteur = 0; // La variable compteur
// Routine d'interruption
ISR(TIMER2_OVF_vect) {
TCNT2 = 256 - 250; // 250 x 16 µS = 4 ms
if (varCompteur++ > 125) { // 125 * 4 ms = 500 ms (demi-période)
varCompteur = 0;
LedToggle;
}
}
void loop () {
// Mettre ici le programme. Exemple :
Led2Toggle;
delay (5000); // Demi-période de la deuxième LED
}
Si vous avez bien compris le premier programme, donné dans l’article Les Timers (I), il n’y a aucune difficulté à comprendre celui-ci.
Conclusion
Utiliser les interruptions générées par les timers permet de laisser le programme principal s’occuper d’une autre tâche. Il faut bien sûr autoriser les interruptions d’une façon générale en positionnant à 1 le flag GIE (bit 7 du registre SREG) par la fonction sei()
par exemple. Il faut également autoriser les interruptions en provenance du timer (flag TOIE du registre TMSK). Lorsque le timer déborde, le timer génère l’interruption et le programme exécute la routine d’interruption liée à ce débordement. Le flag TOV est alors automatiquement remis à 0 et il suffit alors de réinitialiser le timer avec la valeur adéquate pour son comptage. Le compilateur de l’environnement de développement d’Arduino connaît le nom des routines d’interruptions, ainsi que le nom des registres et des différents flags, ce qui simplifie bien le travail du programmeur. Dans un prochain article, nous verrons comment utiliser d’autres registres liés aux timers.