Un timer est un registre à l’intérieur du microcontrôleur qui s’incrémente (ou se décrémente) chaque fois qu’il reçoit une impulsion d’un signal d’horloge. Ce signal d’horloge peut être propre au microcontrôleur ou bien extérieur à celui-ci. Un timer est donc un compteur capable de compter le temps qui s’écoule, d’où son nom anglais de timer counter. Dans ce qui suit, le timer est toujours employé dans un mode où il s’incrémente, mais dans d’autres modes, il peut se décrémenter. On peut télécharger la documentation du constructeur sur ce lien : https://ww1.microchip.com/downloads....
Les Timers (I)
Les bases
. Par :
. URL : https://www.locoduino.org/spip.php?article84Si le registre du timer comporte 8 bits, il est alors capable de compter de 0 à 255 (en hexadécimal, de 00 à FF). Lorsqu’il arrive à 255 (FF), un coup d’horloge supplémentaire devrait le faire passer à 256 (soit 100 en hexadécimal), ce qui n’est pas possible puisque le registre n’a que 8 bits. Le registre passe donc à 0 ; on dit qu’il subit un débordement (Overflow en anglais) [1], mais ce débordement entraîne la mise à 1 d’un bit bien particulier dans un registre de contrôle associé au timer. Ce bit est appelé un flag (drapeau en anglais) et indique que le timer vient de compter jusqu’à 256, ce qui permet d’attirer l’attention du programmeur, un peu comme ces boîtes aux lettres américaines qui possèdent un petit drapeau qui se dresse chaque fois que le facteur a déposé du courrier à l’intérieur.
L’intérêt d’un timer est qu’il compte sans cesse et que pendant ce temps, le programme peut réaliser autre chose, ce qui n’est pas possible si on utilise la fonction delay() qui est bloquante et qui ne permet pas de faire autre chose pendant ce temps d’attente. Le temps que le timer met pour compter 256 coups dépend bien sûr de la fréquence de l’horloge ; à 16 MHz (fréquence du microcontrôleur utilisé dans les modules Arduino), c’est très rapide, mais il est possible de diviser cette fréquence d’horloge grâce à des circuits internes au microcontrôleur appelés prédiviseur (prescaler en anglais). On peut alors diviser la fréquence de base (16 MHz) par 8, 32, 64, 128, 256 ou 1024 ; pour cela, il faut utiliser intelligemment d’autres registres de contrôle associés au timer. Par exemple, si on règle de prédiviseur pour diviser la fréquence par 1024, le timer comptera donc à une fréquence de 15625 Hz.
Comme pour tout registre, on peut lire la valeur d’un timer ou bien écrire une valeur particulière dans le timer. Mais ce qui est surtout important, ce sont les registres de contrôle associés au timer car ce sont eux qui permettent de modifier le comportement du timer et de contrôler ce qu’il fait. Il faut donc bien les connaître pour bien savoir les utiliser et la lecture de la documentation liée au microcontrôleur est souvent indispensable.
Timers du microcontrôleur de l’Arduino
Le module Arduino Uno est construit autour du microcontrôleur AVR ATmega328P d’Atmel qui possède 3 timers :
- Le timer0, sur 8 bits, utilisé par les fonctions delay(), millis() et micros(). Il commande également des PWM (Pulse Width Modulation ou Modulation par Largeur d’Impulsion) sur les broches 5 et 6.
- Le timer1, sur 16 bits, qui compte de 0 à 65535 (0 à FFFF en hexadécimal) et qui est utilisé par la bibliothèque Servo ou bien pour de la PWM sur les broches 9 et 10.
- Le timer2, sur 8 bits, qui est utilisé par la fonction Tone() ou bien pour de la PWM sur les broches 3 et 11.
Le langage d’Arduino fait donc appel aux timers du microcontrôleur mais ceci reste transparent pour le programmeur. D’ailleurs, dans la majorité des applications, le langage d’Arduino est souvent très suffisant et permet de développer des applications sans avoir besoin de faire appel à l’architecture du microcontrôleur. Néanmoins, dans quelques cas particuliers, il peut être intéressant de savoir programmer les timers ; dans cet article, nous allons évoquer l’utilisation des timers comme compteur de temps, même si ceux-ci sont capables de faire bien d’autres choses.
Les registres de contrôle
Le tableau suivant donne les différents registres de contrôle associés à chaque timer ; nous verrons le rôle de chaque registre tout en nous limitant à ce qui est vraiment à connaître pour réaliser un premier exemple.
Timer 0 | Timer 1 | Timer 2 | Rôle |
---|---|---|---|
TCNT0 | TCNT1L | TCNT2 | Timer (bit 0 à 7) |
- | TCNT1H | - | Timer (bit 8 à 15) |
TCCR0A | TCCR1A | TCCR2A | Registre de contrôle |
TCCR0B | TCCR1B | TCCR2B | Registre de contrôle |
- | TCCR1C | - | Registre de contrôle |
OCR0A | OCR1AL | OCR2A | Output Compare (bit 0 à 7) |
- | OCR1AH | - | Output Compare (bit 8 à 15) |
OCR0B | OCR1BL | OCR2B | Output Compare (bit 0 à 7) |
- | OCR1BH | - | Output Compare (bit 8 à 15) |
- | ICR1L | - | Input Capture (bit 0 à 7) |
- | ICR1H | - | Input Capture (bit 8 à 15) |
TIMSK0 | TIMSK1 | TIMSK2 | Interrupt Mask |
TIFR0 | TIFR1 | TIFR2 | Interrupt Flag |
ASSR Asynchronous Status Register
GTCCR General Timer/Counter Control Register
Le timer1 est un timer de 16 bits et est donc constitué de deux registres de 8 bits, l’un donnant les bits 0 à 7, l’autre donnant les bits 8 à 15.
Pour se repérer dans ce tableau, il faut savoir que TCNT signifie Timer/Counter (Register), TCCR Timer/Counter Control Register, OCR Output Compare Register, ICR Input Capture Register, TIMSK Timer/Counter Interrupt Mask Register et TIFR Timer/Counter Interrupt Flag Register. OCR et ICR ont des rôles particuliers dont nous ne parlerons pas dans ce premier article. TIMSK et TIFR servent pour que le timer puisse générer des interruptions [2] comme nous le verrons dans un autre article.
Exemple d’utilisation simple
Nous allons maintenant nous intéresser au timer 2 dans son rôle le plus simple, compter le temps, et aux registres de contrôle qui y sont associés. Ce que nous allons dire reste bien entendu valable pour les timers 0 ou 1. Pour mieux comprendre, nous prendrons comme exemple le clignotement d’une DEL à 1 Hz, ce qui signifie que toutes les 500 ms (demi-période), il faut inverser la DEL, c’est-à-dire l’allumer si elle est éteinte et l’éteindre si elle est allumée.
Ralentir ou accélérer le timer
Nous avons vu qu’il était facile de ralentir un timer : il suffit d’utiliser le prédiviseur pour diviser la fréquence du signal d’horloge. Pour faire cela, il suffit de positionner certains bits du registre TCCR2B. À partir de là, notre compteur comptera de 0 à 255 (cela lui prendra un certain temps) puis passera en débordement, c’est-à-dire qu’il repartira de 0 après avoir positionné le flag TOV2 (Timer/Counter 2 Overflow Flag) à 1. Il s’agit là du bit 0 du registre TIFR2. Chaque fois que le bit 0 du registre TIFR2 passe à 1, cela signifie que notre timer a compté un certain laps de temps (connu) ; il suffit alors de repositionner le flag TOV2 à 0 et d’incrémenter un compteur (une variable) pour continuer ce processus. Lorsque ce compteur arrive à une certaine valeur, c’est que le temps à attendre s’est écoulé.
Pour accélérer un timer, il faut le faire déborder avant qu’il ait compté 256 coups : pour cela, il suffit de partir d’une valeur différente de 0 pour effectuer le comptage puisqu’il est possible d’écrire dans le registre du timer.
On voit maintenant comment il faut opérer pour faire clignoter notre DEL. Le timer part de 0 et s’incrémente jusqu’à 255, puis recommence de 0 vers 255 en ayant positionné le flag TOV2 à 1. Il suffit de surveiller ce flag ; chaque fois qu’il est à 1, on repositionne le flag à 0 et on incrémente un compteur. Lorsque ce compteur arrive à une certaine valeur, on a atteint 500 ms et il faut agir sur la DEL. Pour que le timer ait ce comportement, il doit être utilisé en mode normal ; dans ce mode, le timer compte en incrémentant, et le flag est positionné à 1 chaque fois que le timer repasse par 0.
Pour que le timer soit en mode normal, il faut que les trois bits appelés WGM20 , WGM21 et WGM22 soient à 0, ce qui nécessite d’aller écrire dans 2 registres (WGM2 pour Waveform Generation Mode du timer 2). En effet, WGM20 et WGM21 sont les bits 0 et 1 du registre TCCR2A, et WGM22 est le bit 3 du registre TCCR2B. Les bits 0 à 2 de ce même registre sont appelés CS20 , CS21 et CS22 et servent à régler le prédiviseur suivant le facteur de division souhaité (CS2 pour Clock Select du timer 2). Tout cela sera plus clair sur un exemple.
Calcul théorique du comptage
Le timer 2 sera utilisé en le faisant compter à la fréquence de 62500 Hz (fréquence d’horloge divisée par 256). Un cycle d’horloge dure donc 16 µs (l’inverse de la fréquence). Pour avoir 500 ms (500000 µS), il faut compter 500 000 µs / 16 µs = 31250 fois. Cette valeur est décomposable en 125 * 250. Le timer doit compter 250 fois pour déborder ; il suffit de le faire partir de la valeur 6. Chaque fois qu’il déborde, une variable compteur est incrémentée ; quand cette variable atteint 125, on a bien 500 ms qui se sont écoulées et on agit sur la DEL.
Initialisation des registres de contrôle
Pour que le timer soit en mode normal, il faut que WGM20 , WGM21 et WGM22 soient à 0. On positionne ces bits à 0 dans deux registres TCCR2A et TCCR2B par une instruction adéquate (bitClear
dans le langage Arduino). Pour que le prédiviseur divise par 256, il faut que le bit CS22 soit égal à 1, le bit CS21 soit égal à 1 et le bit CS20 soit égal à 0 ; il suffit d’écrire la valeur binaire 0b00000110 dans le registre TCCR2B. Heureusement pour nous, le compilateur d’Arduino connaît les noms des registres et les noms des bits.
La dernière chose à savoir, c’est que pour réinitialiser le flag TOV2 par logiciel, il faut écrire à 1 le bit 0 du registre TIFR2 ; l’instruction est bitSet
(et non bitClear
comme on aurait pu le croire).
Programme
Voici le programme, écrit avec le langage Arduino :
/*
* Clignotement d'une DEL (LED en anglais) à 1 Hz par timer 2.
*/
const byte Led = 13; // Pour utiliser la LED du module
#define LedToggle digitalWrite (Led, !digitalRead(Led))
void setup ()
{
pinMode (Led, OUTPUT);
bitClear (TCCR2A, WGM20); // WGM20 = 0
bitClear (TCCR2A, WGM21); // WGM21 = 0
TCCR2B = 0b00000110; // Clock / 256 soit 16 micro-s et WGM22 = 0
TIFR2 = 0b00000001; // TOV2
TCNT2 = 256 - 250; // Chargement du timer à 6
}
byte varCompteur = 0; // La variable compteur
void loop () {
if (bitRead (TIFR2, 0) == 1) { // Flag TOV2 mis à 1 ?
TCNT2 = 256 - 250; // Rechargement du timer à 6
bitSet (TIFR2, TOV2); // Remise à zéro du flag TOV2 (voir texte)
if (varCompteur++ > 125) { // Incrémentation et a atteint 125 ?
varCompteur = 0; // On recommence un nouveau cycle
LedToggle; // Inversion de la LED
}
}
}
Conclusion
Nous avons vu qu’un timer est un registre compteur qui s’incrémente (ou se décrémente) à chaque impulsion d’une horloge qui peut être celle du microcontrôleur divisée par un certain nombre pour la ralentir. Lorsque le timer déborde, un flag est positionné à 1 dans un registre de contrôle. En surveillant ce flag, on peut compter le temps qui s’écoule jusqu’à obtenir une durée déterminée. On peut lire la valeur du timer et on peut écrire une valeur dans le timer. Plusieurs registres de contrôle sont associés au timer et permettent de modifier son comportement et de suivre son débordement. La connaissance de ces registres de contrôle et du rôle joué par chacun des bits les composant est indispensable au programmeur qui veut utiliser les timers. Pour cela, une lecture assidue de la documentation du constructeur est indispensable.
L’intérêt d’un timer pour compter le temps qui s’écoule est que le programme peut réaliser autre chose pendant ce temps. Dans l’exemple donné, il suffit de surveiller le flag TOV2 pour connaître l’écoulement du temps ; ceci est possible parce que le programme ne fait rien d’autre que de surveiller. Nous verrons dans un prochain article que le débordement du timer peut engendrer une interruption ; le programme peut alors faire autre chose et être interrompu pour traiter l’événement. Nous verrons aussi que les timers peuvent faire d’autres choses grâce aux autres registres de contrôle comme OCR ou ICR. Mais tout ceci est une autre histoire.
[1] Voir à ce propos l’article « Types, constantes et variables ».
[2] Voir l’article « Les interruptions (1) »