Nous avons appris dans les quatre premiers articles à utiliser les timers de nos microcontrôleurs ATmega328P équipant les modules Uno, Nano et Mini et même à générer notre propre PWM. Dans cet article, nous allons nous intéresser plus particulièrement au timer1 qui a la particularité d’opérer sur 16 bits contrairement aux timer0 et timer2 qui opèrent sur 8 bits. La lecture du présent article suppose d’avoir lu et compris les quatre premiers articles sur les Timers, Les Timers (I), Les Timers (II), Les Timers (III) et Les Timers (IV).
Les Timers
Les Timers (V)
L’unité de capture d’entrée
.
Par :
DIFFICULTÉ :★★★
Structure du timer1
La figure 1 montre le schéma synoptique du timer1 ; dans cette figure, la lettre n qui est le numéro du timer vaut 1. On remarque tout d’abord une grande ressemblance avec la figure 1 de l’article Les Timers (IV).
Le signal d’horloge clkTn (en jaune) arrive à la logique de contrôle, en provenance de l’extérieur (Tn) ou bien de l’horloge interne et du prescaler. La logique de contrôle incrémente ou décrémente le timer TCNTn (en vert) et celui-ci est comparé aux valeurs des registres OCRnA et OCRnB (en bleu). Lorsqu’il y a égalité, un signal est envoyé à un bloc appelé « Waveform Generation » (Génération de la forme d’onde) (en rouge) et c’est ce bloc qui met en forme la PWM en positionnant à 0 ou à 1 le registre des broches OCnA ou OCnB (en violet). Remarquons au passage que cette PWM est générée sur les broches OC1A et OC1B qui correspondent aux broches 15 et 16 du microcontrôleur ATmega328P (en bleu foncé sur la figure 2), c’est-à-dire aux sorties 9 et 10 du module Arduino Uno comme le montre le schéma de câblage du module :
http://arduino.cc/en/uploads/Main/A...
En fait, nous n’allons pas nous intéresser à la génération de la PWM dans cet article puisque nous en avons parlé en détail dans l’article Les Timers (IV). Nous allons plutôt nous intéresser aux différences que ce timer1 présente par rapport au timer0 ou au timer2.
La première est que le timer1 opère sur 16 bits, donc les registres qu’on peut voir sur la figure 1 sont des registres de 16 bits, notamment le timer lui-même TCNT1 mais aussi OCR1A et OCR1B. Comme le bus de données (DATABUS) ne fait que 8 bits, cela implique que ces registres sont constitués d’une partie basse (L) comprenant les 8 premiers bits et d’une partie haute (H) comprenant les 8 bits suivants (par exemple TCNT1L et TCNT1H). Il faut donc deux opérations de lecture ou d’écriture au CPU pour lire ou écrire ces registres.
La deuxième différence est représentée en bleu turquoise sur la figure 1 : la présence d’un registre ICR1 (rappelons que n = 1 pour ce timer) appelé Input Capture Register ou encore registre de capture d’entrée. En fait, ce registre peut capturer la valeur du timer1 lors d’un événement externe donné soit sur la broche de capture d’entrée (Input Capture Pin ICP1), soit sur les broches du comparateur analogique. La broche ICP1 est représentée en bleu turquoise sur la figure 2 : elle correspond à la broche 14 du microcontrôleur ou encore à la sortie 8 du module Uno.
Cet article va donc s’intéresser à ce registre de capture, à la façon de l’utiliser et à ce qu’on peut en faire.
L’unité de capture d’entrée
La figure 3 montre le schéma synoptique de l’unité de capture d’entrée (Input Capture Unit). Elle est extraite de la datasheet du microcontrôleur ATmega328P. Il faut se rappeler que n = 1 sur cette figure puisque seul le timer1 dispose d’une telle unité. De plus, tout ce qui est en grisé ne fait pas partie de l’unité proprement dite mais intervient dans son fonctionnement. Pour expliquer le fonctionnement de cette unité, nous nous limiterons à la capture d’un événement ayant lieu sur la broche d’entrée ICP1 entourée de jaune, mais le raisonnement est similaire pour un événement en provenance du comparateur analogique (en grisé).
Tout d’abord, il convient de définir ce qu’on appelle un événement sur cette broche ICP1 : tout changement de niveau logique sur cette broche, c’est-à-dire le passage de 0 à 5V (LOW à HIGH) ou bien le passage de 5 à 0 V (HIGH à LOW). Le passage de LOW à HIGH s’appelle un front ascendant (ou positif), le passage de HIGH à LOW s’appelle un front descendant (ou négatif). Le programmeur choisit de faire réagir l’unité de capture d’entrée soit aux fronts ascendants, soit aux fronts descendants, en positionnant un bit d’un registre comme nous le verrons plus loin.
- Figure 3
- Architecture du bloc d’unité de capture d’entrée à l’intérieur du microcontrôleur ATmega328P
C’est le détecteur de front (Edge Detector), entouré en rouge, qui confirme le changement de niveau logique sur la broche ICP1 et qui choisit s’il doit s’intéresser aux fronts ascendants ou bien descendants en fonction de la valeur du bit ICES . Pour arriver jusqu’au détecteur de front, le signal est passé par le réducteur de bruit (Noise Canceler), entouré en violet, qui permet éventuellement d’augmenter l’immunité au bruit du signal en effectuant ou non un filtrage en fonction du bit ICNC ; on y reviendra plus en détails un peu plus loin. Toujours est-il que lorsque le détecteur de front repère un événement sur la broche ICP1, il envoie un signal d’écriture (WRITE) pour que la valeur binaire du timer1 (TCNT1), entouré en vert, soit recopiée dans le registre de capture d’entrée (Input Capture Register) ICR1, entouré en bleu turquoise, et au même instant, positionne à 1 le drapeau ICF1 (Input Capture Flag). Cette opération effectue une datation de l’événement puisqu’un timer, comme son nom l’indique, est fait pour compter le temps qui s’écoule.
Si le programmeur a autorisé les interruptions, le drapeau ICF1 génère une interruption appelée Input Capture Interrupt, qui repositionnera automatiquement à zéro le flag ICF1 lorsqu’elle sera exécutée, mais le programmeur peut aussi le faire lui-même et pour cela, il doit écrire un 1 logique à l’endroit du bit ICF1 . La lecture par le CPU du registre ICR1 se fait en commençant par les 8 bits de poids faible (ICR1L) puis les 8 bits de poids fort (ICR1H). Quand les bits de poids faible sont lus, les bits de poids fort sont copiés dans un registre temporaire TEMP, entouré en marron, et quand le processeur lit les bits de poids fort, il accède à ce registre temporaire.
Revenons un peu sur le réducteur de bruit (Noise Canceler). On l’autorise à jouer son rôle en positionnant à 1 le bit ICNC1 ; dans ce cas, l’entrée du réducteur de bruit surveille quatre échantillons durant quatre cycles d’horloges, et tous les quatre doivent être égaux pour changer la sortie qui est à son tour utilisée par le détecteur de front (Edge Detector). Cela introduit un petit retard égal à quatre cycles d’horloge avant de recopier le contenu du timer1 dans le registre ICR1. Le réducteur de bruit utilise l’horloge système et n’est donc pas affecté par le prescaler (voir le rôle du prescaler ou prédiviseur dans l’article Les Timers (I)).
Pour résumer, un changement de niveau logique sur la broche ICP1, éventuellement filtré par le réducteur de bruit (Noise Canceler), est pris en compte par le détecteur de front (Edge Detector) qui envoie un signal d’écriture pour recopier la valeur du timer1 (TCNT1) dans le registre de capture d’entrée (ICR1), tout en positionnant un drapeau ICF1 servant à générer une interruption.
Utilisation de l’unité de capture d’entrée
Le principal défi dans l’utilisation de l’unité de capture d’entrée est d’avoir suffisamment de disponibilité du processeur pour ne pas manquer des événements. En effet, le temps entre deux événements est critique ; si le processeur n’a pas lu la valeur capturée dans le registre ICR1 avant qu’un nouvel événement se produise, le registre ICR1 sera écrasé par la nouvelle valeur et la valeur précédente sera perdue. Le processeur doit donc lire au plus vite chaque nouvelle valeur du registre ICR1 dans la routine d’interruption générée par le drapeau ICF1 .
Le registre ICR1 peut aussi être utilisé pour définir la valeur TOP utilisée par le timer TCNT1 pour générer de la PWM et dans ce cas, la fonction de capture n’est plus possible. Cette configuration, très spéciale, est déterminée par la valeur des bits WGM13:0 du registre TCCR1A, notamment les combinaisons 1000, 1010, 1100 et 1110. Nous ne développerons pas cette possibilité ici, mais il faut être vigilant à la valeur de ces bits si on veut utiliser la fonction de capture.
Pour bien comprendre l’intérêt de l’unité de capture d’entrée, imaginons qu’un signal périodique tel que celui de la figure 4 soit appliquée à l’entrée ICP1.
Supposons qu’on s’intéresse aux fronts ascendants, dans ce cas l’unité de capture donnera une datation pour chaque front ascendant (événements A et B de la figure par exemple). La différence entre deux datations consécutives, pour peu qu’on connaisse à quelle vitesse travaille le timer1, nous donne la période du signal, donc sa fréquence qui est l’inverse de la période.
On peut faire encore plus fort. Imaginons qu’après avoir obtenu une datation pour l’événement A (front ascendant), nous demandons à notre détecteur de front de s’intéresser aux fronts descendants ; nous obtiendrons alors une datation de l’événement C (front descendant) et la différence de datation entre l’événement A et l’événement C, toujours en connaissant la vitesse de travail du timer1, nous donne la durée où le signal est à l’état haut, ce que les anglais appelle duty cycle. Il suffit simplement, dans la routine d’interruption, de changer la valeur du bit ICES1 le plus vite possible après chaque lecture du registre ICR1. Après un changement de front, le flag ICF1 doit être remis à zéro par logiciel, c’est-à-dire en écrivant un 1 logique à son emplacement.
Le principe utilisé pour déterminer le « duty cycle » d’un signal peut être utilisé pour déterminer la durée d’enfoncement d’un bouton poussoir. On date l’enfoncement par la détection d’un premier front, puis son relâchement par la détection du front complémentaire, et on accède ainsi à la durée d’enfoncement. Le logiciel peut alors faire des choses différentes selon que le poussoir est enfoncé brièvement ou bien de façon plus prolongée, ce à quoi nous sommes habitués avec les appareils modernes.
Etude des registres de contrôle
Nous avons parlé de beaucoup de registres et également de certains bits qui jouent un rôle dans l’unité de capture d’entrée. Comme nous l’avions dit dans les précédents articles, utiliser les timers, c’est apprendre à utiliser leurs registres associés. Nous allons donc les passer en revue avant de traiter un exemple pratique. Bien entendu, nous nous limiterons aux registres 8 bits jouant un rôle dans l’utilisation de l’unité de capture d’entrée ; pour plus de détails, référez-vous à la datasheet du µC ATmega328P.
TC1 Control Register A ou encore registre de contrôle A du timer-compteur 1 (TCCR1A) est le premier de trois registres de contrôle du timer1 (repéré par la lettre A). Les bits 0 et 1 contiennent les bits WGM10 et WGM11 , deux des quatre bits servant à régler le mode de fonctionnement du timer1 (WGM pour Waveform Generation Mode ou encore mode de génération de la forme d’onde). Attention, les bits WGM12 et WGM13 ne sont pas les bits 2 et 3 de TCCR1A mais se retrouvent dans le registre de contrôle TCCR1B. Les bits 4 à 7 de TCCR1A sont les bits COM1 (Compare Output Mode for Channel ou encore mode de comparaison de sortie pour chaque canal) ; leur combinaison dépend de la combinaison des bits WGM13:0 , ce qui permet une fois de plus de sélectionner différents modes de fonctionnement du timer1.
TC1 Control Register B (TCCR1B) est le deuxième registre de contrôle (repéré par la lettre B) et est un peu plus intéressant pour l’utilisation de l’unité de capture d’entrée. Les bits 0, 1 et 2 sont les bits CS10 , CS11 et CS12 permettant de sélectionner la source de signaux d’horloge (CS signifie Clock Select ou encore sélection d’horloge). Par exemple, avec la combinaison 101 pour les bits CS12:0 , nous choisissons la sortie du prédiviseur (prescaler) qui divise le signal d’horloge par 1024, pour faire avancer le timer1. Dans les bits 3 et 4 se trouvent WGM12 et WGM13 dont nous avons parlé plus haut. Le bit 6 contient ICES1 (Input Capture Edge Select) : si le bit est égal à 0, le détecteur de front réagit aux fronts descendants, si le bit est égal à 1, le détecteur de front réagit aux fronts ascendants. Enfin, le bit 7 contient le bit ICNC1 (Input Capture Noise Canceler) qui, s’il est égal à 1, permet d’activer le réducteur de bruit (Noise Canceler) décrit plus haut.
TC1 Control Register C (TCCR1C) est le troisième registre de contrôle (repéré par la lettre C) mais ne présente pas un grand intérêt pour l’utilisation de l’unité de capture d’entrée. Les bits 6 et 7 contiennent les bits FOC1B et FOC1A , Force Output Compare for Channel A (ou B).
Timer/Counter 1 Interrupt Mask Register (TIMSK1) ou encore registre de masque d’interruption pour le timer1, est un registre de 8 bits qui permet d’autoriser ou d’inhiber les interruptions provenant de différents composants du timer1. Bien entendu, les interruptions doivent être autorisées de façon globale en positionnant le I-flag du Status Register. Par exemple, le bit 5 est le bit ICIE (Input Capture Interrupt Enable) qui, positionné à 1, permet de déclencher une interruption lorsqu’un événement a provoqué la recopie du timer1 dans le registre ICR1. Le bit 0 est le bit TOIE (Timer Overflow Interrupt Enable) qui permet de déclencher une interruption lorsqu’il y a débordement du timer1.
TC1 Interrupt Flag Register (TIFR1) est le dernier registre de contrôle de 8 bits, associé au timer1 et contient les différents drapeaux (flags) qui se positionnent à 1 lorsque des événements surviennent. Par exemple, dans le bit 5 de ce registre, on retrouve le drapeau ICF (Input Capture Flag) et dans le bit 0, on retrouve le drapeau TOV (Timer Overflow Flag). Le premier est mis à 1 lorsqu’il y a capture de la valeur du timer1 dans le registre ICR et le deuxième est mis à 1 lorsqu’il y a débordement du timer1. Rappelons ici que ces drapeaux sont automatiquement remis à 0 lorsqu’une routine d’interruption est exécutée ; pour les remettre à 0 de façon manuelle dans le programme, il faut écrire un 1 logique dans l’emplacement du bit considéré.
Datation d’un événement
La figure 5 montre que pour dater un événement, il faut savoir sur quel cycle du timer il a lieu.
En effet, si nous ne connaissons pas le cycle sur lequel a lieu l’événement, c’est comme si tous les événements avaient lieu sur le même cycle, par exemple le cycle 1 et on voit que la durée entre événements ne veut plus rien dire. Or, il est très facile de connaître le cycle du timer1 ; chaque fois que celui-ci déborde, c’est-à-dire que sa valeur passe de 65535 à 0, le flag TOV se positionne à 1. Il suffit de compter le nombre de fois qu’il se positionne à 1. En examinant la figure 5, on sait que l’événement 2 a lieu après 2 cycles complets ; pour dater l’événement, il suffit de rajouter la valeur du timer1 (en temps) à la durée (connue) des 2 cycles précédents.
Programme de mise en application des connaissances
Voici un programme très simple qui permet de mettre en application ce que nous venons de dire sur l’unité de capture d’entrée. Il fonctionne avec un module Arduino/Genuino Uno et un bouton poussoir branché entre la broche 8 du module et la masse (voir figure 6). La broche 8 doit donc être déclarée en entrée et sa résistance de pull-up doit être activée. Le programme utilise aussi le moniteur série de l’IDE pour afficher, chaque fois qu’un appui est fait sur le poussoir, le nombre de cycles complètement écoulés du timer1, la date de l’événement observée avec la fonction millis()
et la date de l’événement calculée avec les données du timer1 comme cela a été expliqué à la fin du paragraphe précédent [1]. Vous pourrez alors constater que les deux dernières valeurs sont très proches l’une de l’autre.
Commençons par décrire ce que nous voulons faire. Le programme fait travailler le timer1 en mode normal, en le faisant compter de 0 à 65535. Le prédiviseur (prescaler) est réglé pour diviser le signal d’horloge (de 16 MHz) par 1024, ce qui représente un pas de 64 µs (soit 0,064 ms). La durée d’un cycle de timer1 est donc de 4,194304 secondes (soit 4194,304 ms). Les calculs sont faits en millisecondes puisque la fonction millis() donne un résultat en millisecondes [2]. Le nombre de cycles complètement écoulés du timer1 au moment de l’événement (appui sur le poussoir) est donné par le nombre de fois que le timer1 a débordé ; la routine d’interruption augmente ce nombre d’une unité à chaque fois qu’elle est exécutée. La résistance de pull-up étant activée sur la broche 8, un appui sur le poussoir fera passer la broche 8 de l’état HIGH à l’état LOW ; l’unité de capture d’entrée est donc réglée pour réagir à un front descendant. Le réducteur de bruit n’est pas utilisé. La routine d’interruption déclenchée par l’unité de capture d’entrée, calcule et affiche les données sur le moniteur série. Les interruptions doivent être autorisées globalement et localement (overflow et capture). Petit plus pour bien contrôler ce qui se passe, la DEL du module Uno reliée à la broche 13, s’allume un cycle de timer1 sur deux : la durée d’allumage ou d’extinction est donc de 4,194 s.
Maintenant que nous savons ce que nous voulons faire, voyons comment le faire c’est-à-dire comment régler les différents registres de contrôle. La figure 7 nous permet de repérer les différents bits à régler dans les différents registres. Le timer1 doit travailler dans le mode normal en comptant de 0 à 65535 : pour cela, il faut positionner les bits WGM13:0 (en jaune) à 0, ce qui se fait dans les registres TCCR1A (bits 0 et 1) et TCCR1B (bits 3 et 4). Le prescaler doit être réglé pour diviser la fréquence horloge par 1024 : il faut positionner les bits CS12:0 (en rose) respectivement à 1, 0 et 1, ce qui se fait dans le registre TCCR1B (bit 0 = 1, bit 1 = 0 et bit 2 = 1). On veut que l’unité de capture réagisse aux fronts descendants, donc le bit ICES1 (en rouge) doit être égal à 0 (bit 6 de TCCR1B = 0). On ne veut pas utiliser le réducteur de bruit, donc le bit ICNC1 (en violet) doit être égal à 0 (bit 7 de TCCR1B = 0). Pour autoriser ou non les interruptions de façon globale, on utilise les fonctions interrupts()
ou noInterrupts()
. Pour autoriser les interruptions déclenchées par l’overflow du timer1 et par l’unité de capture, on positionne à 1 le bit TOIE (en vert, bit 0 de TIMSK1) et le bit ICIE (en bleu turquoise, bit 5 de TIMSK1). Conséquence de tout cela, TCCR1A = 0, TCCR1B = 0b00000101 et TIMSK1 = 0b00100001 (pour mieux voir le positionnement des bits, on préfère donner la valeur binaire des registres).
Voici maintenant le programme :
// Input_Capture_timer1.ino
// ------------------------
// On fait fonctionner le timer1 en mode normal, de 0 a 65535
// avec un prediviseur regle sur 1024. La durée du cycle est de 4,194 secondes
// Chaque debordement crée une action sur la LED qui s allume et s eteint
// toutes les 4,194 secondes.
// Chaque debordement augmente egalement numeroCycle d une unite.
// Une capture sur l'entree 8 (detection front descendant) provoque l affichage
// du numero de cycle, la dureeTotale entre le debut du programme et l evenement,
// et la date de l evenement calculee avec la valeur du registre ICR1.
//-------------------------------------------------------------------------------
#define ledPin 13
#define poussoirPin 8
int nombreCycle = 0;
unsigned long dureeTotale = 0;
float dateEvent = 0.0;
void setup() {
pinMode (ledPin, OUTPUT);
pinMode (poussoirPin, INPUT_PULLUP);
Serial.begin(9600);
noInterrupts();
TCCR1A = 0;
TCCR1B = 0b00000101; // prediviseur 1024, duree de pas 64 µs, ICES1 front descendant
TIMSK1 = 0b00100001; // autorisation interruption TOV et Input Capture
TCNT1 = 0;
ICR1 = 0;
interrupts();
}
ISR(TIMER1_OVF_vect)
{
digitalWrite (ledPin, !digitalRead (ledPin)); // A chaque overflow, action sur LED
nombreCycle = nombreCycle + 1;
}
ISR(TIMER1_CAPT_vect)
{
dureeTotale = millis();
dateEvent = nombreCycle * 4194.304 + ICR1 * 0.064;
Serial.println(nombreCycle);
Serial.println(dureeTotale);
Serial.println(dateEvent);
}
void loop() {
// Mettez le programme ici
}
La différence entre les résultats observés par la fonction millis() et ceux calculés avec les éléments du timer1 s’explique de plusieurs façons :
- La fonction millis() n’affiche pas les décimales
- La fonction millis() ne démarre pas tout à fait au même moment que la mise à 0 du timer1 dans la fonction setup.
- Le timer1 travaille lentement, donc avec une précision moindre (durée d’un pas = 64 µs)
Malgré cela, le programme montre comment utiliser l’unité de capture d’entrée du timer1. Amusez-vous à modifier les valeurs du prescaler pour faire travailler plus vite le timer1 et pensez à changer le calcul pour déterminer la date de l’événement (dateEvent).
Vous pouvez bien sûr vous inspirer de ce programme et de ce qui a été dit sur cette unité de capture d’entrée, pour trouver des applications plus appropriées au modélisme ferroviaire, comme déterminer la vitesse de vos trains par exemple ou bien décoder un signal DCC décrit dans l’article L’Arduino et le système de commande numérique DCC. On date la détection d’un front ascendant, puis la détection du front descendant qui suit, et on accède ainsi à la demi-période du signal DCC. Le logiciel peut alors en déduire s’il s’agit d’un bit 1 quand la demi-période est comprise entre 55 et 61 microsecondes ou d’un bit 0 quand la demi-période est comprise entre 95 et 9900 microsecondes. Enfin, n’oubliez pas que dans certains cas l’utilisation de la fonction millis()
n’est pas possible, aussi faut-il recourir à ce genre de programme pour pallier cette lacune. Bien entendu, le recours à la datasheet du µC ATmega328P est fortement conseillée voire indispensable dans certains cas.
[1] l’utilisation de la fonction millis() est possible puisqu’elle ne fait pas appel au timer1.
[2] voici le détail du calcul.
À 16 MHz, la période du signal vaut 1/(16 x 10^6) = 0,0625 µs (soit 62,5 nanosecondes).
À la sortie du prédiviseur, la période est donc de 0,0625 x 1024 = 64 µs = 0,064 ms (le pas d’avance du timer1).
Un cycle de timer1 (sur 16 bits, soit 65536 pas) dure donc 64 x 65536 = 4194304 µs = 4194,304 ms.
La date de l’événement (en millisecondes) est donc égale au nombre de cycles écoulés x 4194,304 auquel s’ajoute la valeur du timer1 (ou du registre ICR1 qui en est la copie) x 0,064.