dimanche 7 août 2011

Premier essai avec les detecteur infrarouge

Introduction
Après avoir monté et testé les détecteurs infrarouge (voir article précédent), j'ai décidé de mettre en place mon premier programme de commande pour Arduino Car.


Principes du programme
Le principe est simple, avancer tout droit jusqu'à rencontrer un obstacle.
Lorsqu'un obstacle est rencontré, le véhicule entame une marche arrière en tournant. Suivant que cela soit le détecteur droit ou gauche ou les deux, le changement de direction en marche arrière est différent.
Finalement, le programme cesse toute activité après 60 secondes (histoire que la voiture ne se mette pas à dévaler toute seule tout le quartier :-) 

Une machine à état fini
Pour facilité la prise en mains, j'ai réduit le fonctionnement d'ArduinoCar en différents états (asSetup, asInitialized, asMove, asChangeDir, asEnd).
La machine peut passer d'un état à l'autre en fonction de conditions visibles sur le graphique.

Différents états de l'ArduinoCar

A chaque étape correspond une ou plusieurs actions exécutées par ArduinoCar.
Dès que l'exécution des actions (correspondant à un état) sont terminés le programme principale termine l'exécution de loop() et rend la main à la couche hardware/logiciel Arduino.
Lorsque qu'Arduino ré-exécutera une nouvelle fois la fonction loop(), l'état de la "machine à état fini" sera très utile pour savoir quelles actions loop() doit entreprendre/exécuter.

Voici une liste des différents états accompagnés d'une description et des actions effectuée par l'état.

asSetup
ArduinoCar entame son initialisation, phase durant laquelle le logiciel configure les pins et initialise les différentes variables/structures.
Cet état est assigné à la première ligne de la fonction Setup().
Si plus tard j'utilise des interruptions, cela permettra au code d'interruption de savoir qu'ArduinoCar n'est pas encore initialisée.

asInitialized
Cet état est assigné à la dernière ligne de la fonction setup().
Cela permet de savoir que tout le processus d'initialisation est terminé... et ArduinoCar peut commencer à fonctionner.
asInitialized n'est qu'un état transitoire destiné à la phase d'initialisation matérielle.
Dès que cette valeur est détecté dans la fonction loop(), ArduinoCar passe automatiquement de l'état asInitialized --> asMove

asMove
Etat quasi permanent d'ArduinoCar.
Dans cet état, ArduinoCar avance en ligne droite et effectue une détection d'obstacle à chaque exécution de loop().
Pour le moment, la détection d'obstacle consiste à tester la proximité des deux premiers détecteurs Infra-Rouge à l'aide de la fonction hasProximity().

Dès qu'un obstacle est détecté, ArduinoCar passe immédiatement dans l'état asChangeDir.

asChangeDir
Etat qui n'intervient que lorsque ArduinoCar à détecté un obstacle.
Le but de cet état est de mettre en place une stratégie d'évitemment qui pour le moment se résume à:
  • Arrêter la marche avant.
  • Entamer une marche arrière.
  • Braquer en marche arrière en fonction du/des détecteurs IR activés.
  • Faire une marche arrière pendant une seconde.
Le but étant de faire 1/4 de tour pour partir dans la direction opposée du détecteur IR activé.
Si les deux détecteurs sont activé, la voiture part vers la gauche.

Lorsqu'ArduinoCar a terminé sa phase d'évitement, le code termine son éxécution en réactivant l'état asMove.

asEnd
Permet d'arrêté ArduinoCar après un temps de fonctionnement défini (60 secondes).
Cet état spécial est activé dans la loop() lorsque le temps d'exécution maximim est atteind.
Lorsque cet état est activé, ArduinoCar arrête les moteurs et replace la direction dans le sens "tout droit".

A part cela, l'état asEnd ne fait rien d'autre que de systématiquement terminer l'exécution de loop().

Premiers résultats
Voici une vidéo du premier résultat obtenu.
De prime abord, je suis assez content de moi... mais... visiblement, cette voiture à un problème!
Elle passe son temps à reculer... comme s'il y avait toujours un obstacle devant elle! Cela me fait penser à un chien atteint de la maladie du carré et donc qui se comporte en dépit de tout sens logique :-)

C'est en fait un bug de conversion-comparaison dans la détection de proximité (voir plus loin... c'est très instructif)



Bug de conversion-comparaison

Comme précisé, ArduinoCar passait son temps a reculer... comme si elle détectait constamment un obstacle!
Sur base de ce constat, j'ai jeter un oeil sur la fonction qui détecte la proximité d'un objet hasProximity().
Le code est assez simple à la base:

boolean hasProximity( byte Proximity ){
  return (Proximity < 255);
}

Cela semble enfantin, pourtant il y a un gros bug!
En effet, la variable Proximity est de type Byte... et par conséquent le compilateur fait le nécessaire pour convertir un Byte (non signé) en un entier (signé) avant de faire la comparaison < 255.
Et comme il y a une différence d'encodage binaire, la valeur Proximity prise telle quelle comme un nombre entier signé apparaît comme négative (si je ne me trompe pas).
En conséquence, hasProximity() est toujours vrai... même s'il n'y a pas d'obstacle.

Pour résoudre le problème, il suffit de faire une conversion explicite vers le type entier signé int :-)
Cela produit le code suivant:

boolean hasProximity( byte Proximity ){
  return ((int)Proximity < (int)255);
}

Il est peut être un peu excessif de forcer explicitement la conversion de 255.
Heureusement que j'ai eu l'occasion de rencontrer des articles mentionnant les problèmes de conversion implicite.

Premières conclusions
Mais aussi premières conclusions.

Conclusion 1: l'arrêt
Ne pas oublier de couper la motorisation quand on passe dans l'état asEnd.
Si cela semble évident, je n'y avais pas pensé dans mon empressement à tester mon premier jet de code.
Comme ArduinoCar est passer de l'état asMove vers asEnd après 60 secondes, j'ai dut me mettre à galoper dans le quartier pour la rattraper... en effet, je n'avais pas pensé à positionner la marche sur arrêt en passant à l'état asEnd!
Ce bug là est déjà corrigé :-)

Conclusion 2: Led et bouton
Il serait bien d'utiliser une led pour indiquer la fin de l'exécution du programme (état asEnd).
Eteind = en cours de fonctionnement
Clignotante = fin de programme.
De même, utiliser un bouton pour démarrer/arrêter la séquence serait plus approprié que d'utiliser le bouton Reset (pour recommencer)


Conclusion 3: Apprendre à freiner et a ralentir
Sur sol plat, la marche à 100 % est très efficace.
Le temps qu'ArduinoCar détecte l'obstacle et passe la marche arrière, le véhicule se fracasse sur l'obstacle du seul fait de son inertie.
Le problème est identique si l'obstacle arrive de biais.

Il y a plusieurs options pour corriger ce problème:
  1. Freiner à la détection de l'obstacle
  2. Diminuer la vitesse de Marche.
    Inférieur à 100% de régime, soit configurable avec un bouton.
Conclusion 4: Obstacle de biais en reculant
Après un changement de direction, il faut avancer un peu avant de faire une nouvelles détection d'obstacle.
En effet, si ArduinoCar recule sur une bordure de trottoir de biais (ou une rigole), les détecteurs sont dirigés vers le sol.
Il en resulte donc une nouvelle détection d'obstacle et une nouvelle tentative de recul et changement de direction.
Pour contourner ce problème facilement, ArduinoCar devrait avancer un peu avant de quitter l'état asChangeDir.

D'autres stratégies pourraient également se monter efficaces.
L'utilisation d'un IMU (inertial measurement unit) pourraient également faciliter la détection d'une telle situation (car modification d'angle du véhicule durant le retour).

Conclusion 5: Redresser la direction
Le redressement de la direction est assuré par un moyen mécanique (ressort).
Ce n'est pas toujours très efficace lorsque le véhicule se déplace.
Il faudrait aider la direction à se redresser en envoyant une impulsion de changement de direction dans l'autre sens.
Il est également possible d'envisager l'usage d'un capteur a effet Hall (et d'un aimant placé sur l'arbre de direction) pour savoir si la direction est bien revenue au point mort.

Conclusion 6: Se méfier de la détection Infrarouge
A plein régime, le véhicule tressaute un peu dans tous les sens.
Cela crée des faux positif de détection d'obstacle (suivant le sens dans lequel le véhicule est bousculer)... et elle se met à faire un changement de direction spontanément.
Pour résoudre ce problème, il faudrait faire un déparasitage logiciel (comme pour les boutons).
Dans ce cas-ci, plutôt que de bloquer l'exécution 10 ms, il serait plus approprié de s'assurer qu'il y ait deux détections consécutives.
A noter qu'après avoir tourné, il faut annuler la dernière détection en date (puisque l'on a changé de direction).

Code Source
Sources: ArduinoCar_StateMachine_v1.zip
Cette archive contient un répertoire avec les deux fichiers sources que j'utilise (en autre ArduinoStates.h qui contient la définition des états sous forme d'un enum).
Il suffit de dézipper le répertoire et ses fichiers dans votre répertoire sketchbook.

Informations utiles sur le code source
La fonction irProximity( int pinIr )
Cette fonction   fait une capture de la proximité à l'aide d'un senseur infrarouge Sharp 2Y0A21 branché sur la pin pinIr.
Cette valeur est transformée en indice de proximité (voir les valeurs de retour).
Pour info, la tension de reférence ARef d'Arduino est fixé à 3.3v pour augmenter la précision de la lecture.

Paramètres:
pinIr - pin analogique sur laquelle le senseur est branché (ex: pinIrGauche, pinIrGauche)

Résultat:Retourne un byte qui représente un indice de proximité.
  • 255: hors du champs de détection
  • 24: 24 cm ou moins
  • 15: 15 cm ou moins
La fonction hasProximity( byte Proximity )
Fonction très simple qui prend l'indice de proximité en paramètre (valeur retournée par la fonction irProximity.

Résultat:Retourne un booléen à  true s'il y a quelque-chose à proximité.

La fonction DoMove()
Cette fonction, appelée continuellement lorsque la voiture est dans l'état asMove à pour principale utilité:
  1. De donner l'ordre de déplacement d'ArduinoCar
  2. De détecter les objets à proximité (appel à irProximity et hasProximity)
  3. De faire le nécessaire pour changer l'état vers asChangeDir si un obstacle est détecté.
Voici le détail de la fonction DoMove():
enum ArduinoState doMove(){
  // Detection de proximité
  irGaucheProximity = irProximity( pinIrGauche );
  irDroiteProximity = irProximity( pinIrDroite );
 
  // Changement de direction ?
  if( hasProximity( irGaucheProximity ) || hasProximity( irDroiteProximity ) )
    return asChangeDir;
 
  marche( AVANT, 100 );
   
  return asMove;
}


Variables volatiles
Dans le point précédent, la fonction DoMove capture les indices de proximités sur les deux détecteurs infrarouges.
Les deux valeurs sont stockées dans les variables globales irGaucheProximity et irDroiteProximity qui sont déclarées comme suit:

volatile byte irGaucheProximity;
volatile byte irDroiteProximity;


Le mot clé volatile informe le compilateur qu'il ne faut pas optimiser le code relatif à l'utilisation des variables en questions.
En effet, sans volatile, le compilateur pourrait décider de changer l'ordre des instructions pour amélioré la rapidité d'exécution.
C'est ainsi que par exemple, le compilateur pourrait changer l'ordre d'exécution d'une boucle for pour qu'elle "décompte" au lieu de "compter". Si en générale cela à peu d'implication sur l'exécution du programme, cela peut avoir des conséquences importantes dans d'autres cas.

Dans le cas d'utilisation de variables tels que irGaucheProximity et irDroiteProximity, l'optimisation du code par le compilateur pourrait réserver des surprises.

Finalement, l'usage d'une variable globale pour le stockage de irGaucheProximity et irDroiteProximity peu paraître à première vue comme "une lacune".
Cependant, à terme, j'ai l'intention de faire une moyenne des deux ou trois dernières lectures de proximité pour éliminer les faux positifs (voir plus haut dans l'article).
Dans ce cas, l'utilisation d'un tableau en variable globale sera approprié... la raison pour laquelle les variables sont déjà déclarées comme globales.

Aucun commentaire:

Enregistrer un commentaire