XV. Compteur d’événements…▲
XV-A. …ou la revanche du fils du retour de la mesure du temps de réaction▲
Il semblerait que notre microcontrôleur ait souvent besoin de faire plusieurs choses en même temps, comme scruter ses entrées tandis qu'il doit opérer des changements sur ses sorties. Un simple contrôleur ne peut faire ça au sens littéral, mais il peut opérer si rapidement qu'il semble effectuer plusieurs tâches simultanément. Par contre, dans certaines situations, cela peut être compliqué à mettre en œuvre si l’on code avec une technique d'attente active (polling ou busy-waiting).
Par exemple, considérez l'opération de multiplexage de plusieurs afficheurs 7-segments. Alors qu’une simple LED ne nécessite que la modification ponctuelle d’un bit, les affichages multiplexés nécessitent, quant à eux, une succession permanente de modifications. Dans un exercice précédent sur la mesure du temps de réactionMesure de temps de réaction, le retour, l'affichage nécessitait 15 millisecondes par cycle, soit 5 millisecondes pour chacun des trois afficheurs. Si l'on voulait seulement afficher une valeur pendant quelques secondes, il fallait simplement boucler sur cette partie du code. Bien que ce principe a fonctionné pour cette application, tel n'est pas toujours le cas.
Considérez maintenant le cas d'un compteur d'événements. Il s'agit d'un dispositif qui compte des choses et qui affiche la valeur courante du compteur. On pourrait envisager ce genre de dispositif sur une ligne d'assemblage pour garder une trace du nombre d'articles qui sont détectés, grâce à un capteur mécanique, photoélectrique, magnétique ou autre détecteur qui se déclencherait lors du passage de l'article. On voudrait en plus que ce dispositif affiche en permanence la valeur courante du compteur, tout en scrutant constamment l'état du détecteur. Comment faire les deux à la fois ? Dans le contexte de notre système de mesure de temps de réaction, on pourrait ajouter du code à l'intérieur de la boucle qui gère l'affichage pour relever l'état du détecteur, mais si la procédure de détection dure moins longtemps que la procédure d’affichage, soit 15 millisecondes, on pourrait louper une détection si celle-ci avait lieu pendant la procédure d’affichage. Fort heureusement, il y a une solution élégante pour gérer ce genre de problème.
XV-B. Nous interrompons le déroulement normal du programme…▲
La solution passe par l'utilisation d'une interruption. Imaginez un système par lequel un bout de code appelé « gestionnaire d'interruption » (interrupt handler) ou « routine de service d'interruption » (ISR pour Interrupt Service Routine) est activé pour s'exécuter sous certaines conditions. L'exécution du code courant est suspendu jusqu'à ce que la routine d'interruption ISR soit terminée. L'interruption peut être activée par un événement soit logiciel, soit matériel.
Un exemple typique pourrait être le cas d'une interruption déclenchée par un changement d'état d'un signal sur une entrée. La plupart des microcontrôleurs ont différents niveaux d'interruption qui peuvent être classés par ordre de priorité, c'est-à-dire que certains événements peuvent être prioritaires sur d'autres, et qu'il est possible qu'une routine d'interruption ISR soit elle-même interrompue par une interruption de priorité plus élevée. En général, les routines d'interruption ISR sont courtes, exécutant rapidement des bouts de code qui ne vont pas rentrer en conflit avec le programme principal.
Dans le cas d'un compteur d'événements, notre détecteur pourrait être connecté à une entrée qui déclenche une interruption. La routine d'interruption ISR n'aurait rien d'autres à faire qu'à incrémenter une variable globale qui ferait office de compteur d'articles. Le programme principal devra scruter périodiquement cette variable globale et répondre en conséquence (ici, mettre à jour l'affichage). Tout se passe comme si un mécanisme s'exécutant en tâche de fond scrutait en permanence la broche d'entrée. Ce mécanisme nous exonère d’avoir à disséminer, au cœur du programme principal, des fragments de code destinés à scruter l’état de la broche.
Habituellement, aucune des interruptions disponibles n'est active, à l'exception du signal de remise à zéro (reset). Elles doivent être activées individuellement avant d'être utilisées. Le système de développement attribue un nom pour chaque routine d'interruption, donc tout ce qu'il nous reste à faire est d'activer le ou les bits appropriés dans le ou les registres de contrôle des interruptions (interrupt control register) et de compléter le code de la routine d'interruption ISR.
Pour le compteur d'événements, on va réutiliser une grande partie du code et du matériel de cet exercice précédent sur la mesure du temps de réactionMesure de temps de réaction, le retour, plus particulièrement les afficheurs 7-segments et le code gérant le multiplexage. Le capteur de forceCapteur de force qui faisait office de bouton pour le joueur sera notre déclencheur d’événement. On exploitera « l'interruption sur changement d'état » des broches de l'ATmega 328P (PCINT pour Pin Change INTerrupt). Correctement configurée, chacune des broches en entrée peut déclencher une interruption quand le signal bascule de l'état haut à l'état bas, ou inversement de l'état bas à l'état haut. Ces broches sont groupées en blocs (banks) correspondant aux ports de l'ATmega 328P, par exemple le bloc 0 (bank 0) correspond au port B (NDLR voir les interruptions sur Arduino pour plus de détails).
Chaque bloc a son vecteur d'interruption associé. Pour le bloc bank 0, ce sera PCINT0_vect (Pin Change INTerrupt Bank 0 Vector). Techniquement, un vecteur d'interruption est une adresse vers une fonction d'interruption prédéfinie. Chacune des broches d'un port en particulier est activée par son propre bit d'interruption. Pour le bloc bank 0, ces bits sont nommés PCINT0 à PCINT7. Donc, si les bits 0 et 1 du port sont activés pour interruptions (via PCINT0 et PCINT1), un changement d'état sur n'importe laquelle des deux broches générera une interruption du bloc bank 0 et la routine d'interruption ISR du vecteur PCINT0_vect s'exécutera. Une fois la routine ISR terminée, l'exécution normale du code principal reprend là ou elle a été interrompue.
Avant de jeter un œil au matériel, regardons du côté du code. Comme mentionné plus tôt, on peut modifier le code existant du système de mesure de temps de réaction, nouvelle formule. On n'a plus besoin du message spécial « go », et on peut donc supprimer certaines lignes du tableau formant les lettres sur l’afficheur. Les autres modifications du code resteront minimes si on conserve le même câblage. On utilisera le terme générique « capteur » (sensor) à la place de FSR (Force Sensor Resistive)Capteur de force, et on doit encore déterminer comment le comptage sur événement sera implémenté. Remarquez que documenter son code en décrivant notamment le câblage nous évite de réinventer la roue. Tout bon programmeur, technicien ou ingénieur doit le faire !
/* Compteur d’événements avec un ensemble de 3 afficheurs 7-segments anode commune.
Segments sur D1:7, afficheurs sur B0:2 (chiffre des centaines sur B.2)
Pilotage des afficheurs par transistors PNP
Capteur sur B.3. Valeurs>999 ==> "Err"
Segments et afficheurs activés à l’état bas */
// Port B.0:2 pour le multiplexage
#define DIGIT1 0x01
#define DIGIT10 0x02
#define DIGIT100 0x04
#define DIGIT111 0x07
#define SENSORMASK 0x08
unsigned
char
numeral[]={
//ABCDEFG,dp
0b00000011, // 0
0b10011111, // 1
0b00100101, // 2
0b00001101, // 3
0b10011001,
0b01001001,
0b01000001,
0b00011111,
0b00000001,
0b00011001, // 9
0b11111111, // espace
0b01100001, // E
0b01110011 // r
}
;
#define LETTER_BLANK 10
#define LETTER_E 11
#define LETTER_R 12
À ce stade, on doit déclarer la variable globale mentionnée plus tôt et modifier la fonction setup().
unsigned
int
g_count =
0
;
void
setup
(
)
{
// Configuration des connecteurs 0 à 7 (port D.0:7) en sortie pour le pilotage des segments
DDRD =
0xff
;
// Configuration des connecteurs 8 à 10 (port B.0:2) en sortie pour le pilotage des afficheurs en mode multiplexé
DDRB |=
DIGIT111;
// Les afficheurs sont actifs par défaut au niveau logique bas, donc on les désactive au démarrage
PORTB |=
DIGIT111;
// Connecteur 11, port B.3 comme entrée pour le capteur de force
DDRB &=
(~
SENSORMASK); // Configuration en entrée
PORTB |=
SENSORMASK; // Activation de la résistance pull-up
// activation pin change interrupt bank 0
bitSet
(
PCICR, PCIE0);
// activation pin change interrupt sur port B.3
bitSet
(
PCMSK0, PCINT3);
}
Les deux dernières lignes du code ci-dessus sont particulièrement importantes. La première permet d'autoriser les interruptions sur changement d'état du bloc bank 0 (bit PCIE0 pour Pin Change Interrupt Enable bank 0) dans le registre de contrôle des interruptions (PCICR pour Pin Change Interrupt Control Register). La seconde spécifie alors la ou les broches concernées (PCINT3 pour Pin Change INTerrupt pin 3) dans le registre PCMSK0 (PCMSK0 pour Pin Change MaSK for bank 0)
Une fois l'interruption activée, il reste à écrire la routine d'interruption ISR. Comme dit précédemment, cette routine a déjà un nom (PCINT0_vect), et tout ce qu'elle doit faire, c'est d'incrémenter la variable globale qui fait office de compteur.
/* gestionnaire d’interruption sur bloc 0 (bank 0).
Trigger sur tout changement d’état de toute broche du bloc 0 (bank 0, c-à-d port B). État Haut->Bas, ou Bas→Haut) */
ISR
(
PCINT0_vect)
{
// incrémentation uniquement sur front descendant pour éviter un double comptage
if
(
!(
PINB &
SENSORMASK) )
g_count++
;
}
La boucle loop() doit être entièrement réécrite. À l’intérieur, on y placera le contenu de l'ancienne fonction DisplayValue() qui contrôlait l'affichage multiplexé. Contrairement à la fonction originale, il n’y a plus besoin de maintenir un affichage pendant un laps de temps déterminé. Au lieu de cela, elle effectue en permanence un simple balayage des trois afficheurs sur une durée totale de 15 millisecondes par cycle. La boucle loop() reprend alors au début, et le processus se répète indéfiniment.
Au début de la boucle, le contenu de la variable globale g_count, le compteur, est copié dans une variable locale traitée séparément pour déterminer les indices du tableau comme auparavant. Remarquez une petite amélioration, à savoir ce bout de code supplémentaire qui affiche des « blancs » au niveau des zéros non significatifs à gauche.
Il reste à répondre à une question : pourquoi se donner la peine de faire une copie locale de la variable globale ? La raison est qu'une interruption peut surgir à tout moment. Ce qui signifie que des traitements pourraient n’être effectués que partiellement sur les variables h, t et u alors qu'une interruption modifie la valeur de la variable globale. Si cela se produit, les indices calculés peuvent prendre des valeurs fausses, car le code a fait le traitement sur une valeur qui a été modifiée une fois, voire plus, entre temps. Si on fait une copie locale, cela ne se produira pas. Seule la variable globale changera et l’affichage sera rafraîchi proprement au prochain passage dans la boucle loop().
void
loop
(
)
{
unsigned
int
v;
unsigned
char
h, t, u; // centaines, dizaines, unités
// copie dans une variable locale
v =
g_count;
if
(
v >
999
) // erreur
{
h =
LETTER_E;
t =
u =
LETTER_R;
}
else
{
u =
v%
10
;
v =
v/
10
;
t =
v%
10
;
h =
v/
10
;
// zéros non significatifs à gauche remplacés par des blancs
if
(
!
h )
{
h =
LETTER_BLANK;
if
(
!
t )
t =
LETTER_BLANK;
}
}
// On efface tout, puis activation des afficheurs chacun son tour
PORTB |=
DIGIT111;
PORTD =
numeral[h];
PORTB &=
~
DIGIT100;
delay
(
5
);
PORTB |=
DIGIT111;
PORTD =
numeral[t];
PORTB &=
~
DIGIT10;
delay
(
5
);
PORTB |=
DIGIT111;
PORTD =
numeral[u];
PORTB &=
~
DIGIT1;
delay
(
5
);
// On efface tout
PORTB |=
DIGIT111;
}
À ce stade, le code est fonctionnel. Si le matériel dont on disposait précédemment n'est pas encore en place, recâblez-le. Saisissez le code ci-dessus, compilez-le et transférez-le dans la carte. Chaque pression sur le bouton du joueur devrait incrémenter la valeur affichée d'une unité. Remarque : si aucun dispositif antirebond n'a été prévu, il se peut que chaque pression augmente la valeur du compteur de deux unités ou plus, de façon aléatoire.
XV-C. Le choix du commutateur▲
L'astuce consiste maintenant à implémenter le commutateur approprié pour compter les articles ou événements en question. Le commutateur doit établir ou rompre un contact, ou produire une transition d'état : état haut-état bas-état haut. Imaginez qu'on doive compter des articles convoyés par tapis roulant. L'implémentation dépendra des articles eux-mêmes. Ont-ils tous la même taille et la même orientation ? Est-ce qu'ils sont lourds ? Sont-ils répartis de manière aléatoire et à intervalle régulier ? Dans certains cas, un simple contacteur mécanique peut suffire : l'article qui vient en contact avec un mécanisme à poussoir, le déplace en poussant lors de son passage et produit un signal. Un ressort de rappel ramène le mécanisme en position initiale une fois l'article éloigné, le contacteur est ainsi prêt à détecter l'article suivant. Mais cette détection par contact peut ne pas être adaptée selon la taille, le poids ou la forme de l'objet détecté ou si les objets sont disposés trop irrégulièrement sur le tapis.
Une autre approche consiste à utiliser un capteur photoélectrique. Ici, un faisceau de lumière visible ou infrarouge traverse le tapis roulant (NDLR Barrage optique). À l'opposé de l'émetteur est placé un détecteur optique. Quand un article coupe le faisceau lors de son passage, un signal est généré. Ce principe fonctionnera avec des formes atypiques d'objet, quel que soit son placement sur la bande du tapis et même si l'objet est très léger puisqu'il n'y a aucun contact physique entre l'objet et le capteur. Il y aura toutefois quelques difficultés de détection avec des matériaux transparents et autres morceaux de fromages suisse découpés finement (sérieusement, un objet de forme atypique présentant des trous pourrait être détecté plusieurs fois ! NDLR Et plusieurs objets se masquant partiellement pourraient ne donner lieu qu’à une seule détection. De même, un objet mince pourrait passer sous le faisceau. La fiabilité de ce système de détection peut être facilement mise en défaut, surtout si l’on veut comptabiliser le passage d’objets différents.)
La fabrication des contacteurs mécaniques est complexe, mais leur principe de fonctionnement est assez simple. Pour la solution photoélectrique, deux possibilités se présentent à vous. La première est d'utiliser une photorésistance (cellules à bas coût au sulfure de cadmium ou CdS) et une source de lumière assez puissante. Ce genre de cellules a sa résistivité qui augmente sous faible éclairage, et qui diminue sous un fort éclairage. La photorésistance peut être connectée directement à un connecteur d'entrée avec la résistance de tirage au plus (pull-up) activée. La difficulté majeure ici provient de la lumière ambiante. L'autre possibilité passe par une paire émetteur-détecteur infrarouge (LED infrarouge et phototransistor infrarouge). Le circuit électrique est un peu plus complexe, mais ce capteur est beaucoup moins sensible à la luminosité ambiante.
L'alignement de l'émetteur et du récepteur photoélectrique, ainsi que la distance qui les sépare doit être précis pour déclencher un signal à bon escient. Pour faciliter les tests en laboratoire, il est préférable de valider le concept avec l'émetteur et le récepteur distant de seulement quelques centimètres, et en simulant le passage des articles avec une carte de couleur sombre entre les deux.
Défi
Retirez l'ancien capteur FSR et insérez votre nouveau détecteur d'articles à la place. Ajoutez un nouveau bouton qui remettra le compteur d'articles à zéro (bien entendu, sans utiliser le bouton reset de la carte Arduino). Incluez une description et le schéma de câblage de votre nouveau détecteur.