Manuel de laboratoire pour contrôleurs embarqués

Utilisation du langage C et de la plateforme Arduino


précédentsommairesuivant

XIII. Mesure de temps de réaction, le retour

Parfois, il est bien de revisiter un projet passé pour y étudier une approche différente. Ici, on va retravailler l’exercice sur la mesure du temps de réactionMesure de temps de réaction. Même si celui-ci il fût nécessaire comme étape d’apprentissage, la limitation évidente de ce programme en lui-même est le fait que la carte de développement nécessite d’être raccordée à un ordinateur exécutant l’EDI Arduino, car il repose sur le Moniteur Série pour l’affichage des valeurs numériques. À la place, on propose de réaliser un chronomètre autonome, et qui fonctionnera grâce à une source d’alimentation extérieure. Il y a plusieurs méthodes pour afficher une valeur numérique, la plus directe est probablement celle utilisant un afficheur 7 segments à LED. Ces composants peuvent afficher les 10 chiffres arabes et quelques lettres latines (sans accent, et non sans quelque ambiguïté).

Note de la rédaction : afficheur 7 segments

Image non disponible
16 caractères représentés sur un digit
Image non disponible
Désignation des segments

Images CC BY-SA 3.0

Afin de simplifier le codage, notre jeu sera privé de quelques « extras », comme la détermination du meilleur score parmi les cinq tentatives, de la moyenne des temps de réaction, etc. Ceci permettra de se concentrer sur l'objectif premier qui sera d'interfacer un système d'affichage autonome pour simplement afficher le temps de réponse de chaque tentative. Le fonctionnement général du jeu sera similaire : l’affichage se mettra à clignoter rapidement pour indiquer à l’utilisateur de se tenir prêt, et après une attente de durée aléatoire entre une et dix secondes, un nouveau message donnera le top départ et le joueur devra en réponse frapper le bouton (un capteur de force résistifCapteur de force) au plus vite. Le temps de réponse sera affiché pendant quelques secondes et, après une courte pause, le jeu recommencera. Le principe général, ou le pseudocode du jeu, restent fidèles au jeu original. La modification majeure, aussi bien sur le plan matériel que logiciel, concerne le système d'affichage puisqu'un afficheur à LED 7 segments sera utilisé à la place du Moniteur Série de l'EDI Arduino.

En ce qui concerne le reste du matériel, on n'aura plus besoin des LED de signalisation, mais l'interrupteur du joueur est conservé (le capteur de force). Un temps de réaction inférieur à 100 ms étant improbable, pas plus qu'un temps supérieur à la seconde pour quelqu'un en bonne santé, à moins que la personne ne soit distraite ou sous l'emprise d'une quelconque substance chimique et récréative affectant temporairement (espérons-le) son système nerveux. Ainsi, un afficheur à trois digits devrait suffire pour rendre compte des temps entre 0 et 999 millisecondes.

Un afficheur 7 segments à trois digits nécessite normalement 21 sorties si chaque segment est piloté individuellement. C'est plus que n'en comporte l'Arduino Uno et cette solution n'est donc pas viable. Au lieu de cela, on peut multiplexer les digits dans le temps. Dans le principe, on affiche seulement le chiffre des centaines sur le digit le plus à gauche de l'afficheur pendant quelques millisecondes, puis vient le chiffre des dizaines au milieu de l'afficheur pendant quelques millisecondes, et enfin le chiffre des unités sur le digit à droite de l'afficheur là aussi pendant quelques millisecondes. On répète alors le processus en boucle, les digits s'allumant chacun à son tour. Tant que la boucle principale s'exécute en moins de 25 millisecondes environ, l’œil et le cerveau n'y voient que du feu (NDLR du fait de la persistance rétinienne) et les trois digits semblent actifs simultanément. Avec ce mécanisme de multiplexage, seules 10 sorties de la carte seront nécessaires : sept pour commander les segments et trois pour piloter chaque digit individuellement en mode multiplexé. Si les digits sont à anode commune, le schéma du montage est celui de la figure ci-dessous :

Image non disponible
Configuration d'un afficheur 7 segments à 3 digits (anode commune). Un bit 0 supplémentaire peut être utilisé au besoin pour piloter le point décimal.

Avec cette configuration à anode commune, c'est un niveau logique bas à la cathode qui va permettre d'allumer le segment, et donc un niveau logique haut qui va l'éteindre. Notez que les circuits de pilotage des LED ne sont pas représentés ici. Au lieu de cela, les cathodes des segments sont connectées directement aux sorties de l'Arduino en passant par des résistances Rs de limitation de courant. Les sorties de l'Arduino pourront absorber le courant nécessaire si le courant qui traverse chaque LED reste à un niveau peu élevé. Par exemple, pour une alimentation à 5 V et une tension de seuil de la LED (Forward voltage) à environ 2 V, une résistance Rs de 470 Ω permet de limiter le courant aux alentours de 6 ou 7 milliampères par segment, ce qui rend l'éclairage des segments suffisant pour une utilisation en intérieur. Chaque digit peut être activé grâce à un transistor PNP. Un niveau logique bas sur sa base sature le transistor, et le courant peut alors circuler à travers les segments du digit. Pour cette application, un transistor de faible puissance comme le 2N3906 suffira. Si tous les segments du digit sont allumés, c'est un courant maximum d'environ 50 milliampères qui arrive au collecteur. Ainsi, une résistance à la base du transistor de l'ordre du kiloohm fonctionnera parfaitement, avec un courant de pilotage à la base du transistor de quelques milliampères.

En pratique, il sera plus facile de dédier tout le port D au pilotage des sept segments (D.1:7, connecteurs 1 à 7 de l'Arduino). Cela signifie par contre qu'on ne pourra plus utiliser le Moniteur Série (ports D.0:1), mais ce ne sera pas un problème. On peut utiliser les ports B.0:2 (connecteurs 8 à 10 de l'Arduino) pour piloter les trois digits en mode multiplexé et B.3 (connecteur 11) pour le capteur de force. Les ports utilisés peuvent être déclarés dans des directives #define. On aura également besoin d'un tableau de valeurs codées en binaire pour le dessin des chiffres et lettres de l'afficheur :

 
Sélectionnez
// Port B.0:2 pour le multiplexage
#define DIGIT1   0x01
#define DIGIT10  0x02
#define DIGIT100 0x04
#define DIGIT111 0x07

#define FSRMASK  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 
   0b00001001,   // g 
   0b00111001    // o   
};

#define LETTER_BLANK  10
#define LETTER_E      11
#define LETTER_R      12
#define LETTER_G      13
#define LETTER_O      14
#define MSG_GO        -1
#define MSG_ERR       -2

Les dessins des chiffres et caractères sont codés dans un tableau numeral[] d'octets dont les valeurs sont initialisées en binaire. Chaque bit donne l'état du segment : le bit de poids fort donne l'état du segment A (le segment horizontal tout en haut du digit), et le bit de poids faible l'état du point décimal (non employé ici). Si on prend par exemple le dessin du chiffre « 0 », il faut activer tous les segments excepté le G (le segment horizontal du milieu) et le point décimal. Avec la logique active au niveau bas, il faut donc mettre à 0 chaque bit correspondant à un segment allumé. Le procédé est le même pour les autres chiffres. On a en plus ajouté certains caractères spéciaux comme le caractère « espace », ainsi que les lettres « E », « r », « g » et « o ». Avec les deux premières lettres, on pourra former le message « Err » pour signaler une erreur (ou si l'utilisateur « triche » par exemple), et avec les deux lettres suivantes on pourra former le message « go » pour donner le top départ à l'utilisateur à la place de la LED verte. Deux variables MSG qui seront examinées au moment propice ont également été définies.

Note de la rédaction : documentation typique d'un afficheur 7 segments à 3 digits (anode commune) adapté au multiplexage

Image non disponible

Pour allumer un segment sur un digit en particulier, il faut mettre l'anode du digit au niveau logique haut, et la cathode du segment correspondant au niveau logique bas. En mode multiplexé, on configure l'état des segments au niveau des cathodes puis on allume le digit en mettant l'anode correspondante au niveau logique haut. Pour donner l'illusion des trois digits allumés simultanément, on active tour à tour chaque digit à une fréquence suffisante pour que la persistance rétinienne joue pleinement son rôle.
Une vidéo plutôt démonstrative ci-dessous.


Mise en évidence du multiplexage


Après ces quelques définitions, on peut regarder les changements apportés à la fonction setup(). Les configurations des entrées-sorties sont modifiées, mais la définition de la graine (seed) pour la fonction random() est toujours là. Notez que comme toutes les broches du port D sont configurées en sorties, on remplit le registre de direction directement avec l'octet 0xff plutôt que de définir les bits un par un avec une succession de OU logique. Il est aussi important d'initialiser les bits pilotant les transistors des trois digits (DIGIT111) du port B au niveau logique haut. Sinon, comme les broches du port D seront au niveau logique bas par défaut, tous les segments des trois digits apparaîtraient allumés lors du démarrage de la carte. Le courant absorbé par la carte serait considérable à ce moment-là. Mettre les bits pilotant les transistors des trois digits à l'état haut permet d'assurer que l'afficheur sera désactivé avec tous les segments éteints au démarrage de la carte.

 
Sélectionnez
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 digits en mode multiplexé
   DDRB |= DIGIT111;

   // Les digits 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 &= (~FSRMASK);   // Configuration en entrée
   PORTB |= FSRMASK;     // Activation de la résistance pull-up

   // Initialiser la graine à partir du bruit sur l'entrée analogique A0 
   randomSeed(analogRead( 0 ));
}

Voici le code de la boucle principale loop(), légèrement modifié. On peut le comparer à la version originale ligne par ligne et noter les quelques modifications. Les commentaires dans le code devraient être suffisamment explicites. Les plus gros changements concernent évidemment le code d'affichage dans le Moniteur Série qui est remplacé par une nouvelle fonction DisplayValue() décrite plus bas.

 
Sélectionnez
void loop()
{
   unsigned long starttime, finishtime;
   int i, j, nettime;
   long a;
   

   for(i=0;i<5;i++)
   {
      a = random(1000, 10000);

      // Attendre deux secondes avant de débuter ce tour
      delay(2000);
   
      // Clignotement de l'afficheur pour signaler au joueur de se tenir prêt
      for(j=0;j<5;j++)
      {
         DisplayValue( 888, 100 );
         delay(100);
      }

      // Temporisation de durée aléatoire
      delay(a);

      // Message Go, pour donner le top départ
      starttime = millis();
      DisplayValue( MSG_GO, 50 );
 
      // Attente de la réponse, la frappe du capteur de force va mettre l'entrée au niveau logique bas
      while( PINB & FSRMASK );
   
      finishtime = millis();
      nettime = finishtime - starttime;

      // Si la frappe a été anticipée irrégulièrement
      if( nettime < 100 )
         nettime = MSG_ERR;

      DisplayValue( nettime, 3000 );
   }
}

La fonction DisplayValue() est assez simple à utiliser. Le premier argument de la fonction est le nombre à afficher, entre 0 et 999. Le second argument est la durée de l'affichage en millisecondes. Par exemple, DisplayValue(888, 100) signifie : « Afficher le nombre 888 pendant 100 millisecondes ». On affichera ce nombre pour signifier au joueur de se tenir prêt. Outre les nombres à afficher, d'autres valeurs particulières sont aussi autorisées comme premier argument de la fonction DisplayValue(), et celles-ci sont négatives pour les distinguer des durées en millisecondes. Les temps de réactions négatifs sont bien entendu impossibles, à moins de disposer de facultés à remonter dans le temps. Si la valeur du premier argument est MSG_ERR, le message « Err » sera affiché. Si la valeur est MSG_GO, c'est le message « go » qui s'affichera. Ainsi, l'ancienne LED verte qui se mettait à clignoter pour lancer le top départ est remplacée par un DisplayValue(MSG_GO, 50) qui affichera le message « go » pendant 50 millisecondes.

La première chose que doit réaliser la fonction est de vérifier les valeurs des arguments passés. D'abord, les codes des messages spéciaux doivent être valides, puis les durées affichées doivent être dans la plage admissible (NDLR entre 0 et 999 millisecondes). Lorsqu'un nombre correspondant à la durée en millisecondes est passé, il faut le décomposer en trois nouvelles valeurs correspondant au chiffre à afficher sur chacun des trois digits. Ces nouvelles valeurs sont stockées dans les variables h, t et u correspondant respectivement au chiffre des centaines (hundreds), des dizaines (tens) et des unités (units). Pour faire cette décomposition, on utilise la division entière et le modulo. On utilise le modulo par 10 pour récupérer le chiffre des unités à droite, puis la division entière par 10 pour décaler le nombre d'un rang vers la droite, et on recommence. Prenez un nombre quelconque à trois chiffres, et effectuez les calculs vous-mêmes sur papier pour vous approprier la logique.

 
Sélectionnez
void DisplayValue( int v, int msec )
{
   unsigned char i, h, t, u;


   if( (v <= MSG_ERR) || (v > 999) ) // Si valeur non valide, erreur
   {
      h = LETTER_E;
      t = u = LETTER_R;
   }
   else
   {
      if( v == MSG_GO )
      {
         h = LETTER_G;
         t = LETTER_O;
         u = LETTER_BLANK;
      }
      else
      {
         u = v%10; 
         v = v/10;
         t = v%10;
         h = v/10; 
      }
   }

À ce stade, le message doit être affiché pendant le temps requis en bouclant autant de fois que nécessaire. Le code fait en sorte que chaque passage dans la boucle dure environ 15 millisecondes, où les trois digits sont alors allumés chacun leur tour pendant 5 millisecondes. Une simple division par 15 suffira alors pour calculer le nombre d'itérations, mais il faut faire attention au cas où la durée d'affichage demandée accidentellement ne serait que de quelques millisecondes (et où la division retournerait une valeur nulle).

 
Sélectionnez
   // Calcul du nombre d'itérations pour la durée requise
   msec = msec/15; 
   if( msec < 1 )
      msec = 1;

On crée ensuite une boucle de la durée requise, et qui sera nécessairement un multiple de 15 millisecondes. À l'intérieur de cette boucle, on commence par éteindre tous les digits. On prépare les segments à l'aide du tableau numeral avant d'activer le digit souhaité. Notez que l'état des segments codé en binaire est saisi dans un tableau en suivant un ordre de telle sorte que l'indice du tableau évoquant l'état des segments correspond justement au chiffre à afficher. Comme quoi, une bonne anticipation permet d'économiser des efforts de codage et de la place mémoire. Chaque digit est maintenu actif pendant 5 millisecondes. En sortant de la fonction, on désactive tous les digits.

 
Sélectionnez
   // Affichage de la valeur pendant la durée spécifiée
   for( i=0; i<msec; i++ )
   {
      // On efface tout, puis activation des digits chacun leur 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;

Défi

Cet exercice requiert beaucoup de câblage et une quantité significative de code. Beaucoup de choses peuvent dysfonctionner si vous sautez dedans à pieds joints en essayant de tout faire en même temps. Par exemple, si un segment ou même un digit ne s'allume jamais, comment saurez-vous si l'origine du problème est matérielle ou logicielle ? Trouver le bug n'est pas une mince affaire dans ce cas. Il vaut mieux s'assurer d'abord que chaque bout de code fait ce qu'on attend de lui, puis se baser dessus pour faire le reste. Par exemple, vous pouvez commencer par mettre au point le code qui allume un seul digit, puis l'étendre pour l'affichage multiplexé. Considérez le code suivant :

 
Sélectionnez
// port B.0:2 pour le pilotage des digits en mode multiplexé
#define DIGIT1   0x01
#define DIGIT10  0x02
#define DIGIT100 0x04
#define DIGIT111 0x07

#define FSRMASK  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 
   0b00001001,   // g 
   0b00111001    // o   
};

#define LETTER_BLANK  10
#define LETTER_E      11
#define LETTER_R      12
#define LETTER_G      13
#define LETTER_O      14
#define MSG_ERR       -2


void loop()
{
   // Jeu d'essais
   DisplayValue(123);
   DisplayValue(456);
   DisplayValue(12);
   DisplayValue(3);
   DisplayValue(100);
   DisplayValue(-2);
   DisplayValue(50);
}

void DisplayValue( int v )
{
   unsigned char i, h, t, u; // centaines, dizaines, unités

   if( (v <= MSG_ERR) || (v > 999) ) // Erreur
   {
      h = LETTER_E;
      t = u = LETTER_R;
   }
   else
   {
      u = v%10; 
      v = v/10;
      t = v%10;
      h = v/10; 
   }

   // Afficher la valeur pendant approximativement 1s (66 x 15 millisecondes)
   for( i=0; i<66; i++ )
   {
      // On efface tout, puis activation des digits chacun leur 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;
}

Une fois que le montage est prêt, testez-le en situation ! Mettez en œuvre le programme et réalisez un schéma complet du montage. On vous demande également de décrire les éventuelles modifications à apporter pour des digits 7 segments à cathode commune à la place de ceux à anode commune.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Licence Creative Commons
Le contenu de cet article est rédigé par James M. Fiore et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2017 Developpez.com.