Manuel de laboratoire pour contrôleurs embarqués

Utilisation du langage C et de la plateforme Arduino


précédentsommairesuivant

XII. Mesure de temps de réaction

Dans cet exercice, nous utiliserons les entrées et sorties numériques, les fonctions de gestion du temps et la génération de nombres aléatoires pour créer un jeu simple. Le but sera de tester le temps de réaction du joueur (techniquement, nous mesurons le temps de réponse qui est la somme du temps de réaction et du temps du mouvement du joueur). Au lieu de simplement regarder le code, cet exercice nous permettra également de prendre du recul, de regarder la conception du système et les problèmes d’une interface utilisateur simple.

Le jeu donnera au joueur cinq essais pour atteindre son meilleur temps de réaction. Avant chaque tentative, un premier voyant rouge sera activé pour signaler au joueur de se préparer (c.-à-d. « prêt »). Ensuite, un deuxième voyant vert sera activé pour dire au joueur d’y aller (c.-à-d. « partez »). Le temps entre l’allumage des voyants rouge et vert sera toujours compris entre une et dix secondes mais sera aléatoire entre les tentatives. Une fois que le voyant vert s'allume, le joueur doit frapper sur un interrupteur. Le temps qui s’est écoulé entre le feu vert allumé et l'activation de l’interrupteur est le temps de réaction du joueur. Le meilleur temps sera affiché à la fin des cinq essais. Le jeu doit également se prémunir contre la tricherie, c'est-à-dire qu’il doit interdire l’anticipation du feu vert.

Avant de considérer les aspects matériels et logiciels, nous devons avoir une idée plus précise de ce que nous mesurons. D’une manière générale, quel est le temps de réponse du corps humain ? Il s'avère que cela est en partie fonction du stimulus (les signaux audibles sont traités plus rapidement par le cerveau humain que les signaux visuels). Les temps de réponse typiques sont d'environ 160 millisecondes pour un stimulus auditif et 190 millisecondes pour un stimulus visuel, bien que les athlètes professionnels tels les sprinteurs olympiques puissent parfois atteindre une valeur de seulement 2/3 de ceux-ci. De toute évidence, le matériel et le logiciel doivent être très rapides si nous voulons obtenir un niveau de précision acceptable.

L'interface utilisateur se compose de quatre éléments : un voyant rouge, un voyant vert, l’interrupteur du joueur et un moyen pour afficher du texte. Pour simplifier et faciliter le prototypage, nous utiliserons le Moniteur Série (Serial Monitor) pour l’affichage du texte. Si cela devait devenir un produit fini, le moniteur pourrait être remplacé par un écran LCD dédié. En ce qui concerne les lumières, les LED semblent être un choix évident peut-être en raison de leur caractère familier et de leur taille adaptée à ce jeu individuel (si plusieurs personnes faisaient un essai chacune, nous aurions besoin d'une source de lumière beaucoup plus grande ou plus lumineuse qu'une LED ordinaire). Un autre élément à considérer est la rapidité d’allumage des voyants. Les LED s'allument en une fraction de milliseconde tandis que les lampes à incandescence peuvent ne pas atteindre une luminosité totale pendant des dizaines ou même des centaines de millisecondes. Comme nous nous attendons à des temps de réponse aux alentours de 200 millisecondes, une lampe à incandescence peut ne pas être assez rapide. C'est-à-dire que le délai nécessaire pour atteindre une luminosité totale pourrait fausser les résultats, ralentissant le temps de réponse apparent (mesuré) du joueur. Donc, il semble que de simples LED soient un bon choix ici.

L'élément final est l’interrupteur du joueur. Il pourrait frapper assez fort, car il voudra déplacer sa main aussi vite que possible pour minimiser le temps de mouvement. Par conséquent, l'interrupteur doit être assez robuste. Il devrait également avoir une taille décente pour le joueur, c'est-à-dire assez large pour que le joueur ne soit pas obligé de faire un mouvement très précis. Alors qu'un simple bouton poussoir momentané pourrait sembler un choix décent, un capteur de force résistif pourrait s'avérer meilleur. Il ne contient pas de pièces mobiles et offre une grande cible (NDLR voir le capteur de force du chapitre précédent). Il répond également presque instantanément et ne souffre pas de rebond. L'interconnexion est également simple : nous pouvons simplement le connecter entre une broche d'entrée et la masse, en activant la résistance de pull-up interne de l'ATmega328P. Sans pression sur le capteur sa résistance est très élevée et lorsqu'il est sous pression, la valeur baisse à environ 100 Ohms. Comme il sera en série avec le pull-up connecté à l'alimentation, sans aucune pression dessus la règle du diviseur de tension indique que la tension à ses bornes sera vue comme un état logique haut. Lorsqu’une pression sera exercée sur le capteur, la tension à ses bornes sera vue comme un état logique bas. Le seul inconvénient de celui-ci est l’absence de ressentis physiques lorsqu’on appuie dessus. De nombreux interrupteurs font un « clic » lorsqu'ils sont enfoncés et cela fournit une information auditive au joueur confirmant un appui franc. Nous pouvons apporter une information aux joueurs en allumant une LED, voire les deux, au moment où l’appui est détecté.

Nous avons maintenant une idée des potentiels besoins matériels. Qu'en est-il du logiciel ? De toute évidence, les techniques de gestion des entrées et sorties numériques étudiées précédemment peuvent être utilisées pour lire et contrôler le capteur de force et les LED. La fonction delay() peut être utilisée pour gérer la temporisation entre l'allumage des LED pour lancer le départ. Deux nouveaux éléments sont cependant nécessaires : une façon de générer des valeurs aléatoires (dans notre cas, entre 1000 millisecondes et 10 000 millisecondes) et une méthode de mesure du temps entre deux événements. Considérons d'abord le nombre aléatoire.

D’une manière générale, les circuits numériques ne sont pas adaptés pour générer de véritables valeurs aléatoires, car ils sont déterministes. Concrètement, nous essayons de « fabriquer » tout le potentiel du hasard, car nous avons besoin d'une opération hautement imprévisible. Les valeurs véritablement aléatoires ne sont en aucun cas corrélées entre elles. Autrement dit, il n'y a rien sur une suite de valeurs aléatoires qui vous permettra de déterminer la valeur suivante. Il s'avère cependant que nous pouvons créer des séquences pseudo-aléatoires sans trop d'effort. Une séquence pseudo-aléatoire semble être aléatoire, mais si vous exécutez la séquence assez longtemps, elle se répétera. Celles-ci peuvent ne pas être suffisamment aléatoires pour des recherches scientifiques, mais elles sont suffisantes pour quelque chose comme notre jeu. Le processus de tirage de nombres pseudo-aléatoires sort du cadre de cet exercice, mais on peut dire qu’il implique un traitement assez rudimentaire, tel qu'un décalage de bits, sur une valeur de départ appelée une graine (seed). Le résultat de ce calcul est ensuite utilisé comme donnée d’entrée pour calculer la valeur suivante et ainsi de suite.

La bibliothèque Arduino comprend une fonction random(), qui renverra une valeur entière pseudo-aléatoire de type entier long. La fonction peut également être appelée avec des limites supérieure et inférieure pour contraindre les valeurs renvoyées comme random(30, 200). Cette suite pseudo-aléatoire est initialisée par la fonction randomSeed() qui prend un entier long comme argument. Un cas de figure parfois utile est que si l’initialisation de départ est la même pour chaque exécution du programme, la suite pseudo-aléatoire résultante se répétera exactement de la même manière. Cela permet le débogage d'une fonctionnalité supposément aléatoire. Mais cela soulève un problème : comment pouvons-nous obtenir une graine aléatoire pour initialiser une suite pseudo-aléatoire ? Bien qu'il soit possible de demander à l'utilisateur une valeur de départ, cela ouvre la porte à une tricherie éventuelle. Une meilleure façon est d'utiliser une tension issue d’un bruit. Par sa nature même, le bruit est aléatoire. Si on regarde une broche d'entrée analogique qui n'est connectée à rien, on verra une faible tension de bruit. Nous pouvons numériser cette valeur vraiment aléatoire avec la fonction analogRead() et l'utiliser pour la graine. Cette tension ne changera pas sur une très large plage, mais elle changera en quantité suffisante pour que notre séquence pseudo-aléatoire semble réellement aléatoire.

Le programme suivant montre comment fonctionne le système de nombres aléatoires. Entrez le code, compilez-le et transférez-le, puis exécutez-le. Tout d'abord, il affichera la tension de bruit, puis des valeurs aléatoires entre 50 et 1000. Après avoir vu un nombre suffisant de valeurs, appuyez sur le bouton reset de la carte pour redémarrer le programme. Faites-le plusieurs fois. Ces redémarrages devraient produire différentes séquences. De temps en temps, vous pourriez avoir la même tension de bruit. Si vous voyez cela, notez que la séquence de nombres pseudo-aléatoires sera répétée exactement de la même manière.

 
Sélectionnez
void setup()
{
    long i;
    Serial.begin(9600);

    i=analogRead(0); // ou tout autre entrée analogique libre
    randomSeed(i);
    Serial.print("Noise voltage: ");
    Serial.println(i);
}
void loop()
{
    long a;
    a = random(50,1000);
    Serial.println(a);
    delay(2000);
}

Cette technique devrait fonctionner parfaitement pour notre application. Tout ce que nous devons faire c’est borner la fonction aléatoire entre 1000 et 10 000 afin de pouvoir utiliser directement les valeurs en tant qu’argument, en millisecondes, pour la fonction delay() qui servira pour l'allumage aléatoire de la LED verte « partez ».

La deuxième partie dont nous avons besoin consiste à mesurer le délai entre l'éclairage de la LED « partez » et l’appui sur le capteur de force résistif. Si vous le faisiez à partir de zéro, vous devriez initialiser l'un des timers de l'ATmega328P et vérifier la valeur qu’il contient au moment voulu. Les événements de synchronisation sont un besoin tellement commun dans le domaine de l’embarqué que, lors de la routine d'initialisation Arduino, un timer est créé pour vous. À l'instant où vous allumez ou réinitialisez la carte, ce timer interne commence à incrémenter un registre qui a été initialisé à zéro. Le registre est incrémenté à chaque milliseconde. Ce timer continue de fonctionner indépendamment du code principal qui est en cours d'exécution (à moins que le code principal ne reprogramme le timer, bien sûr). Par conséquent, la valeur du registre indique combien de millisecondes se sont écoulées depuis la mise sous tension ou la réinitialisation de la carte. Le contenu de ce registre peut être lu via la fonction millis(). C'est-à-dire que millis() renvoie le nombre de millisecondes qui se sont écoulées depuis la dernière réinitialisation ou mise sous tension. Après environ 50 jours, ce registre débordera et repassera à 0 puis l’incrémentation reprendra à nouveau. Comme il est très peu probable qu’un individu joue à ce jeu pendant aussi longtemps, ce débordement ne devrait pas être problématique. L'extrait de code suivant fera ce dont nous avons besoin :

 
Sélectionnez
starttime = millis();
// Allumer la LED  "Partez !"
/* Scruter l'état de l'entrée  est connecté le capteur de force, puis se mettre
en attente jusqu'à la réponse du joueur qui appuie sur le capteur */
finishtime = millis();
responsetime = finishtime - starttime;

Donc, nous pouvons maintenant trouver un pseudocode pour la boucle du jeu :

 
Sélectionnez
1. Initialiser le meilleur temps de réponse en tant que valeur la plus élevée (long int)
2. Demander à l'utilisateur de lancer le jeu en appuyant sur le capteur de force
3. Attendre la réponse du joueur par l’appui sur le capteur de force
4. Démarrer une boucle de cinq essais, pour chaque essai :
    a. Générer un nombre aléatoire entre 1000 et 10 000
    b. Attendre une seconde ou deux pour permettre à l'utilisateur de se préparer
    c. Allumer une LED rouge une ou plusieurs fois rapidement pour indiquer le début de cet essai
    d. Attendre pendant un délai aléatoire défini en 4.a
    e. Enregistrer le temps millis() de départ
    f. Faire un Flash sur la LED verte « partez ! »
    g. Attendre la réponse du joueur en scrutant le bit FSR (image du capteur de force)
    h. Enregistrer le temps millis() de fin et calculer le temps de réponse (4.h - 4.e)  
    j. Faire un Flash à la fois sur la LED rouge et la LED verte pour un retour visuel
    k. Si le temps de réponse est inférieur à 100 ms, déclarer une tricherie et réinitialiser le temps de réponse à une valeur de pénalité élevée
    l. Afficher le temps de réponse
    m. Comparer les temps de réponse jusqu'à présent, ne garder que la valeur la plus petite (c'est-à-dire la meilleure) (boucle complète)
5. Afficher la meilleure valeur
6. Attendre un moment avant de commencer la prochaine partie

Certaines de ces étapes pourraient nécessiter des éclaircissements ou des ajustements supplémentaires. À l'étape 4.k, une « tricherie » est détectée si le temps de réponse est inférieur à 100 millisecondes. Ceci est probablement exagéré compte tenu des statistiques vues précédemment et pourrait être ajusté à une valeur plus élevée. La valeur de pénalité ici pourrait être identique à la valeur d'initialisation, peut-être 10 000 millisecondes ou plus. Après tout, si cela nécessite plus de 10 secondes pour appuyer sur le capteur de force, on peut supposer que le joueur est en état d’ébriété, étourdi ou en train d’écrire un SMS.

Un autre élément important est le code pour les étapes 3 et 4.g. Concrètement, il doit « attendre » que le joueur frappe sur le capteur de force résistif. Si celui-ci est câblé sur une broche d'entrée utilisant une résistance de pull-up interne, en appuyant dessus, l'entrée va passer d’un état haut à un état bas. (Rappelez-vous, en appuyant sur le capteur, sa résistance devient faible et comme le capteur est en série avec la résistance de pull-up, la règle du diviseur de tension montre que la tension aux bornes du capteur diminue et atteint une valeur égale à un état logique bas). Par conséquent, le code peut « attendre » sur cette broche et en sortir quand la broche est à l’état bas. Nous pouvons câbler le capteur de force sur la broche 8 de l'Arduino qui est le bit 0 du port B. En outre, nous pourrions éviter de coder ceci en dur en utilisant un #define :

 
Sélectionnez
#define FSRMASK 0x01

Donc, le code de test ressemble à ceci :

 
Sélectionnez
while( PINB & FSRMASK ); // PINB, et non PORTB !!

Rappelez-vous, pour une entrée, nous regardons PINx et pour une sortie, nous utilisons PORTx. Cette boucle continue à fonctionner aussi longtemps que le bit d'entrée FSR est à l'état logique haut. Dès que le joueur presse le capteur avec une force raisonnable, la tension sur la broche diminue et la boucle se termine.

Nous pouvons rationaliser un peu plus notre code sur les étapes 4.c, 4.f et 4.h qui consistent à faire clignoter une LED. Nous pouvons connecter les LED verte et rouge sur les broches 6 et 7 de l'Arduino (ports D.6 et D.7) et utiliser un #define avec une fonction qui allume ou éteint une LED rapidement :

 
Sélectionnez
// LED verte, port D.6
#define GREENLED 0x40
// LED rouge, port D.7
#define REDLED 0x80
// Délai on/off en millisecondes
#define LEDTIME 30
void FlashLED( int mask )
{
    PORTD |= mask;    // Allumer la ou les LED
    delay(LEDTIME);
    PORTD &= (~mask); // Éteindre la ou les LED
    delay(LEDTIME);
}

Cette fonction serait appelée pour un seul flash rapide :

 
Sélectionnez
FlashLED(GREENLED);

Elle pourrait également être placée dans une boucle pour effectuer une série de flashs rapides et vous pourriez modifier un bit ou les valeurs du masque pour faire flasher les deux LED simultanément.

Le dernier point concerne l'étape 6, « Attendre un moment avant de commencer une nouvelle partie ». Pourquoi inclure cela ? La raison n'est pas évidente. Rappelez-vous que le microcontrôleur traitera tout le programme en un temps très court, peut-être que quelques millisecondes se passeront entre le moment où le joueur frappe le capteur, les valeurs finales sont affichées, et le moment où la fonction loop() se termine, pour être rappelée à nouveau par la fonction invisible main(). Lors de la réintroduction, dans la fonction loop(), le programme indique au joueur d'appuyer sur le capteur de force pour démarrer la prochaine partie, puis il attend dans une boucle que le bit FSR passe à l’état bas. Le problème est que l’appui sur le capteur va démarrer une nouvelle partie mais le joueur n’aura peut-être pas le temps de retirer son doigt que l’essai sera enregistré puisque le bit FSR sera vu comme un état bas, tellement rapidement, que la boucle d’attente se terminera tout aussi vite ! Pour éviter cela, nous pouvons entrer un petit délai, car en principe personne ne continuera à appuyer sur le capteur de force une fois le temps de réponse enregistré, ou nous pourrions utiliser un autre capteur indépendant pour débuter la partie. Un seul capteur semble plus adapté.

Selon les extraits de code précédents, l’initialisation pourrait ressembler à ceci :

 
Sélectionnez
// LED verte sur D.6, rouge sur D.7, FSR sur B.0, délai flash en millisecondes
#define GREENLED 0x40
#define REDLED 0x80
#define FSRMASK 0x01
#define LEDTIME 30
void setup()
{
    Serial.begin(9600);

    // Configuration des connecteurs 6 et 7 en sortie pour les LED
    DDRD |= (GREENLED | REDLED);
    // Configuration connecteur 8 en entrée pour le capteur de force FSR
    DDRB &= (~FSRMASK); // Initialiser le connecteur en entrée
    PORTB |= FSRMASK;   // Activer la résistance de pull-up interne
    // Initialiser la graine d'après le signal bruité de l'entrée analogique A0
    randomSeed(analogRead( 0 ));
}

Défi

Sur la base du pseudocode présenté précédemment, créez la fonction loop() pour le jeu et testez-la plusieurs fois. Entrez votre code complété avec un schéma approprié montrant toutes les interconnexions au microcontrôleur. Les LED doivent être alimentées par un circuit de commande approprié pour minimiser le courant de sortie du contrôleur. Une petite modification pour un bonus supplémentaire serait que le programme détermine et affiche le temps de réponse total pour chaque partie de cinq essais ainsi que le temps de réponse moyen. Il devrait également indiquer le nombre de « tricheries » s'il y a lieu. Notez que les fraudes entraîneront une augmentation considérable du temps de réponse total et de la moyenne.


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.