• 15 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 08/11/2019

Interfacez les entrées/sorties du robot à une carte à microcontrôleur

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Il s'agit maintenant d'entrer dans le vif du sujet. Nous allons découvrir comment accéder aux entrées et aux sorties du robot par le code.

Pour cela, nous allons encore étudier quelques fonctions du langage, puisque c'est essentiellement de cette manière que l'on y accède.

Dans ce chapitre, nous verrons comment lire et écrire les entrées et les sorties digitales et analogiques. Nous étudierons également comment piloter les servomoteurs.

C'est ce qui s'appelle un mode de pilotage "bas niveau". Le pilotage dit "haut niveau", sera abordé dans la partie 4 ! Cela permettra au robot de réaliser des tâches complexes et de s'adapter à son environnement.

Appropriez-vous les commandes de base

Lecture et écriture d’une broche numérique

Avant d’attaquer à proprement parler des interactions avec les entrées/sorties souvent notées E/S ou en anglais I/O, il nous reste à évoquer de façon formelle les fonctions que nous avons déjà rencontrées lors de la prise en main du tout premier programme “Blink”.

Une fonction est une méta-instruction qui exécute quelque chose et qui, pour ce faire et dans le cas général, a besoin de paramètres d’entrées. Quand son exécution est terminée, elle retourne un paramètre de retour qui, généralement est utilisée dans une affectation à une variable. Le cas échéant, le nombre de paramètres d’entrées peut être nul. Il peut aussi dans certains cas ne pas y avoir de paramètre de retour, essentiellement pour les fonctions qui interagissent avec les éléments matériels de la carte.

Si vous consultez le site de référence Arduino que nous avons déjà souvent rencontré et que vous regardez au menu “Functions” (Resources ->Reference->Language), vous verrez que les fonctions sont regroupées par thèmes (14 pour être précis) :

  • Digital I/O

  • Analog I/O

  • Zero, Due & MKR Family

  • Advanced I/O

  • Time

  • Math

  • Trigonometry

  • Characters

  • Random Numbers

  • Bits and Bytes

  • External Interrupts

  • Interrupts

  • Communication

  • USB

Le premier dans la liste est précisément “Digital I/O”. Comme il n’y a que 3 fonctions dans ce thème, il est possible et nécessaire de les étudier toutes :

Une broche digitale (ou numérique ou logique ou encore Tout ou Rien, soit TOR en abrégé) doit d’abord être déclarée en Entrée(Input, soit I) ou Sortie (Output, soit O).

Nous avons déjà vu (Partie 2, Chapitre 1) que la carte Arduino Uno est pourvue de 14 broches I/O numériques numérotées de 0 à 13. Chaque broche peut individuellement être déclarée en Entrée ou Sortie et cette opération est réalisée avec la fonction "pinMode". Son fonctionnement en est très simple. Elle accepte deux paramètres d’entrées :

  1. Le numéro de broche.

  2. L’état d’Entrée (Input) ou de Sortie (Output).

Les paramètres d’entrée doivent être passés dans cet ordre. Concrètement, l’instruction ci-dessous déclare la broche n°13 en sortie :

pinMode(13, OUTPUT);

Vous vous souvenez peut-être que sur la carte Uno, cette broche est reliée en dur sur la carte à une LED, si bien que la constante LED_BUILTIN vaut 13. Une fois cette déclaration faite, nous pourrons écrire sur cette Sortie et ainsi commander l’état de la LED.

L’alternative est de déclarer une broche en Entrée, auquel cas on écrira :

pinMode(7, INPUT);

qui déclare la broche 7 en Entrée parce qu’elle serait connectée à un capteur de ligne, par exemple.
Une fois la broche déclarée dans son état d’Entrée ou Sortie, on peut l’utiliser. :magicien:

Pour les sorties, il faut les commander avec la fonction"digitalWrite". Par exemple,

digitalWrite(13, HIGH); // sets the digital pin 13 on
delay(1000); // waits for a second
digitalWrite(13, LOW); // sets the digital pin 13 off
delay(1000);
  • la première ligne écrit dans la broche 13 (13 étant le premier paramètre d’entrée, c’est-à-dire le numéro de broche, l’état"HAUT" ou "HIGH"). La LED brille ;

  • puis la deuxième ligne appelle la fonction "delay" avec comme paramètre d’entrée 1000 qui représente un temps en ms. Cette fonction"delay" appartient au thème “Time” qui permet la gestion du temps ;

  • puis la troisième ligne commute la sortie à 0. La LED s’éteint ;

  • si on met cette séquence dans une boucle, la LED va clignoter.

Quand une broche a été déclarée en Entrée, il faut la lire avec la fonction "digitalRead". Considérons le code ci-dessous :

val = digitalRead(7); // read the input pin
digitalWrite(13, val); // writes it on the builtin LED

La fonction"digitalRead" ne reçoit qu’un seul paramètre d’entrée, le numéro de la broche. En revanche, elle fournit un paramètre de retour et ce paramètre de retour est ici affecté dans la variable"val". Si la broche 7 est connectée à un capteur de ligne, la variable"val" contiendra, à l’issue de l’exécution du "digitalRead", l’état du sol en face du capteur. Puis, cette valeur est ensuite écrite à la broche 13 pour visualiser sur la LED de la carte Uno l’état du capteur connecté à la broche 7.

Lecture d’une broche analogique

Vous vous souvenez peut-être que le microcontrôleur de la Uno manipule des mots de 8 bits, des octets. Pour manipuler des signaux analogiques, il est nécessaire de les convertir en leurs équivalents numériques. :magicien:

Le microcontrôleur de la Uno dispose en interne d’un convertisseur analogique numérique (CAN ou ADC pour Analog to Digital Converter) 10 bits qui permet le passage du monde analogique au monde numérique. Ce composant accepte en entrée une tension analogique, notée Vin qui varie sur une plage typiquement de 0 V à 5 V et sort un mot 10 bits noté N, tel que :

Vin=N.q+ε

Où q est appelé pas de quantification ou résolution et  ε est appelé erreur de quantification :

  • le pas de quantification est donné par la plage de tension en entrée, soit 5 V - 0 V = 5 V divisé par le nombre de combinaisons possibles du mot de sortie soit ici,  210=1024. On trouve ainsi :  q=5/10244.9mV

  • l’erreur de quantification est liée au fait que la sortie ne peut prendre qu’un nombre fini de valeurs, toutes multiples du pas de quantification. Comme l’entrée peut prendre une infinité de valeurs (tous les réels entre 0 et 5 V), la conversion entraîne par principe une erreur. On montre facilement que cette erreur de quantification est aléatoire, mais inférieure ou égale au pas de quantification. Cela signifie que l’erreur est inférieure à 5 mV, ce qui est relativement raisonnable pour nos applications. Si on souhaite une erreur plus faible, il faudra se tourner vers des modèles de cartes qui incorporent des CAN de meilleures résolutions (16 bits, par exemple).

Cette section est dédiée au fonctionnement logiciel du CAN de la carte Uno et on trouve les informations de référence dans le thème Analog I/O.

Le thème Analog I/O. Cette image provient du site Arduino, elle est couverte par une licence CC-BY-SA 3.0.
Le thème Analog I/O. Cette image provient du site Arduino, elle est couverte par une licence CC-BY-SA 3.0.

La lecture est réalisée par l’appel à la fonction"analogRead". Dans l’exemple ci-dessous,

int analogPin = 3;
int val = 0;
val = analogRead(analogPin);

on commence par définir laquelle des entrées analogiques doit être lue :

  • nous avions vu en effet, que la carte est munie de 6 broches analogiques. La variable"analogPin" est déclarée à 3 ;

  • puis la valeur de tension sur cette broche est convertie par appel de la fonction"analogRead" dont l’unique paramètre d’entrée est le numéro de broche ;

  • puis, le paramètre de retour, le mot 10 bits qui est l’équivalent numérique de la tension d’entrée, est affecté à la variable "val".

Commande d’un servomoteur

Quand on consulte la liste des 14 thèmes de fonctions évoqués plus haut, on ne trouve rien de relatif à la gestion des servomoteurs. :euh:  Il y a des fonctions “Maths” et “Trigonometry” pour exécuter des calculs mathématiques, des fonctions liées à la gestion du temps (nous avions rencontré la fonction"millis" qui retourne le temps écoulé depuis le lancement du programme, en millisecondes) ou encore la fonction "delay", mais rien sur la gestion des servomoteurs. Pourtant, ces fonctions existent parce que la plateforme Arduino, historiquement, a été développée aussi pour ce type d’applications. Difficile, donc, d’imaginer que les auteurs laissent les utilisateurs se débrouiller tout seuls pour commander les servomoteurs !
En fait, les fonctions ne résident pas dans la liste des fonctions standard mais dans des librairies qui sont aussi documentées sur la plateforme. :zorro: La subtilité à saisir est que ces librairies doivent être déclarées dans le code pour pouvoir être utilisées.

Si on consulte la documentation qui vous est maintenant familière, au sous-menu “Libraries”, ouf ! on trouve bien la librairie “Servo” (elles sont listées par ordre alphabétique) au milieu de beaucoup d’autres.

Chemin pour trouver la librarie
Chemin pour trouver la librairie "Servo" sur le site Arduino

Pour utiliser les fonctions de la librairie "Servo" :

  1. On la déclare.

  2. On l’associe à un nom local au code.

  3. On peut ensuite appeler les fonctions mais avec la subtilité qu’elles sont appelées de manière un peu différente, puisqu’elles doivent figurer dans l’instruction associée au nom de librairie correspondant.

Ce sera plus clair sur l’exemple ci-dessous :

#include <Servo.h> // on “inclut la librairie Servo”
Servo myservo; // on l’associe localement à myservo
void setup()
{
myservo.attach(9); // la fonction “attach” est appelée associée au nom local de librairie
myservo.write(90); // idem pour la fonction “write”
}
void loop() {}
  • dans cet exemple, la fonction "myservo.attach" associe un servomoteur physique à une des broches I/O digitales, la 9 en l'occurrence. Le servomoteur est donc commandé et connecté à cette broche ;

  • la fonction "myservo.write(90)" écrit la valeur 90 sur le servomoteur. Si c’est un servomoteur à rotation continue, ça correspond à une vitesse nulle. La valeur 0 met le servomoteur à vitesse maximum dans un sens, tandis que 180 met le servomoteur à vitesse maximale mais dans le sens contraire. Si c’est un servomoteur standard, la valeur fixe la position du servomoteur à laquelle il se positionnera.

Codez les déplacements du robot

Ce tout premier code permet de définir les numéros de broches associées aux deux servomoteurs, les positions de repos, puis le code écrit ces positions de repos sur les deux servomoteurs.

/* ce programme présente les toutes premières bases de notre robot :
les servomoteurs et leur branchement sur la carte
il permet de contrôler la position de repos des servomoteurs
*/
// bibliothèque de fonctions pour la gestion des servomoteurs
#include <Servo.h>
Servo roueG, roueD; // définit le nom des deux servos
int posG, posD; // position des servos
void setup() {
roueG.attach(6); // la roue gauche est reliée à la broche 6
roueD.attach(7); // la roue droite est reliée à la broche 7
posG = 90; // position de repos
posD = 90;
roueG.write(posG); // indique au servo de rejoindre sa position de repos
roueD.write(posD);
}
void loop() {
}

Le code qui suit enrichit et remplace le code précédent : on reprend la définition des broches, les positions de repos et on complète avec des nouvelles fonctions de base de mouvement. En étudiant le code qui suit, vous constaterez que les déplacements se font aux vitesses maximales des servomoteurs.

/* ce programme développe les bases de notre robot. Il enrichit
et remplace le code précédent avec les définitions de nouvelles fonctions décrites ci-après.
Y figure comme précédemment les servomoteurs et leurs
branchements sur la carte,
le code permet de contrôler une position de chaque servomoteur
Nous introduisons des mouvements de base soit
avancer, reculer, tourner à droite et tourner à gauche
*/
// bibliothèque de fonctions pour la gestion des servomoteurs
#include <Servo.h>
Servo roueG, roueD; // définit le nom des deux servos
int posG, posD; // position des servos
const int pinD = 6; // broche de la roue gauche
const int pinG = 7; // broche de la roue droite
// sous programme d'initialisation et il n'est exécuté qu'une fois
void setup() {
roueG.attach(pinG); // déclaration des broches des roues
roueD.attach(pinD);
}
//déclaration des sous routine de mouvement
void arreter(){
posG = 90; // position de repos
posD = 90;
roueG.write(posG); // indique au servo de rejoindre sa position
roueD.write(posD);
}
void avancer(){
posG = 100; // vitesse maximale pour la roue gauche
posD = 80; // vitesse maximale pour la roue droite
roueG.write(posG); // indique au servo de rejoindre sa position
roueD.write(posD);
}
void reculer(){
posG = 80; // vitesse maximale
posD = 100; // vitesse maximale
roueG.write(posG); // indique au servo de rejoindre sa position
roueD.write(posD);
}
void tournerD(){
posG = 100; // vitesse maximale
posD = 90; // position de repos
roueG.write(posG); // indique au servo de rejoindre sa position
roueD.write(posD);
}
void tournerG(){
posG = 90; // position de repos
posD = 80; // vitesse maximale
roueG.write(posG); // indique au servo de rejoindre sa position
roueD.write(posD);
}
// boucle infinie
void loop() {
}

Il y a dans le code ci-dessus des valeurs qui peuvent intriguer. Pour la fonction "avancer", par exemple, les deux moteurs sont commandés en sens inverse (90 correspond au repos, 80 fait tourner dans un sens, et 100 dans l'autre sens). Saurez-vous expliquer pourquoi les deux servomoteurs sont commandés en sens contraire ? La réponse est dans la configuration géométrique des servomoteurs sur le bâti.

À vous de jouer !

Pour définir un comportement opérationnel du robot qui exploite les fonctions de mouvement élémentaires, il n'y a plus qu'à modifier le contenu de la boucle "loop". On peut par exemple, avancer pendant quelques secondes, puis tourner à gauche, avancer à nouveau, puis tourner à droite et ainsi de suite pour définir une petite séquence de mouvements, histoire de tester que toutes ces fonctions sont bien opérationnelles. On peut utiliser la fonction "delay" pour définir la durée de chaque mouvement élémentaire.

En revanche, à ce stade, le robot ne peut pas détecter son environnement et exécute aveuglément les mouvements que vous avez programmés.

Robot "suiveur" de ligne

Quand le code devient long, il peut être difficile à lire si toutes les instructions sont placées dans un fichier unique qui apparaîtra comme un long ruban dans l'éditeur. Pour structurer de façon plus lisible le code, on utilise le concept de sketch de la plateforme Arduino. L'idée est de ranger le code complet dans plusieurs fichiers qui apparaissent en parallèle dans l'éditeur, comme illustré à la figure ci-dessous :

Copie d'écran de l'éditeur avec la structure d'un sketch
Copie d'écran de l'éditeur avec la structure d'un sketch

On voit dans l'image deux onglets qui correspondent à deux fichiers d'extension .ino. Ces deux fichiers qui apparaissent en parallèle dans l'éditeur définissent un sketch qui correspond à un répertoire ou dossier sur le disque de votre ordinateur.

Dans l'onglet "robotLigne1", figure le code ci-dessous:

/* Dans ce programme, nous avons un capteur de ligne unique branché sur PIN4
* et nous programmons le suivi d'une ligne.
* Si le capteur est sur du noir le robot tourne à droite
* si le capteur est sur du blanc le robot tourne à gauche
* nous suivons ainsi le bord de la ligne.
*/
// bibliothèque de fonctions pour la gestion des servomoteurs
#include <Servo.h>
Servo roueG, roueD; // définit le nom des deux servos
int posG, posD; // position des servos
const int pinD = 7; // broche de la roue gauche
const int pinG = 6; // broche de la roue droite
const int vmax = 10; // vitesse maximale des servos
const int repos = 90; // position de repos des servos
const int capteur = 4; // broche du capteur de ligne
// sous programme d'initialisation et il n'est exécuté qu'une fois
void setup() {
roueG.attach(pinG); // déclaration des broches des roues
roueD.attach(pinD);
arreter(); // arrêter les moteurs
pinMode(capteur, INPUT); // déclaration de la broche du capteur en entrée
}
// boucle infinie
void loop() {
if (digitalRead(capteur) == 0) // teste si le capteur est sur du blanc
tournerG(); // si oui alors on tourne à gauche
else
tournerD(); // si non alors on tourne à droite
}

Cet onglet, associé au fichier du même nom, définit en quelque sorte le programme principal. On y reconnaît la partie déclaration des constantes et variables, le void setup et la boucle void loop qu'il est aisé de comprendre. En revanche, on n'y voit pas les fonctions tournerG et tournerD. En fait, ces fonctions figurent dans le deuxième onglet, libellé "mouvements", dont le code est affiché ci-dessous :

//déclaration des sous-routines de mouvement
void mouvement(int posD, int posG){
roueG.write(posG); // indique au servo de rejoindre sa position
roueD.write(posD);
}
void arreter(){
mouvement(repos,repos);
}
void accelerer(){
for (int i=0; i<vmax; i++){ // augmentation prograssive de la vitesse
posG = repos + i;
posD = repos - i;
mouvement(posD,posG);
delay(20); // vitesse de la rampe d'accélération
}
}
void freiner(){
for (int i=vmax; i>0; i--){ // reduction prograssive de la vitesse
posG = repos + i;
posD = repos - i;
mouvement(posD,posG);
delay(20); // vitesse de la rampe de freinage
}
}
void avancer(){
posG = repos + vmax;
posD = repos - vmax;
mouvement(posD,posG);
}
void reculer(){
posG = repos - vmax;
posD = repos + vmax;
mouvement(posD,posG);
}
void tournerD(){
posG = repos + vmax;
posD = repos;
mouvement(posD,posG);
}
void tournerG(){
posG = repos;
posD = repos - vmax;
mouvement(posD,posG);
}

Vous obtenez un comportement du robot qui doit ressembler à la vidéo ci-dessous :

Robot "suiveur" d'obstacle

Le code qui suit permet d'asservir le mouvement du robot à l'obstacle qui se présente devant lui en maintenant une distance constante :

/* Ce programma mesure la distance qui sépare le capteur de distance d'un obstacle
* et il maintient une distance contante avec celui-ci.
* le capteur de distance est placé sur le connecteur analogique 0
*/
// bibliothèque de fonctions pour la gestion des servomoteurs
#include <Servo.h>
Servo roueG, roueD; // définit le nom des deux servos
int posG, posD; // position des servos
const int pinD = 6; // broche de la roue gauche
const int pinG = 7; // broche de la roue droite
const int vmax = 10; // vitesse maximale des servos
const int repos = 90; // position de repos des servos
const int capteur = A0; // broche du capteur de distance
const int limiteSup = 400; // limite de distance supérieure
const int limiteInf = 350; // limite de distance inférieure
// sous programme d'initialisation et il n'est exécuté qu'une fois
void setup() {
roueG.attach(pinG); // déclaration des broches des roues
roueD.attach(pinD);
arreter(); // s'arreter
}
// boucle infinie
void loop() {
int mesure; // distance mesurée par le capteur infra-rouge
mesure = analogRead(A0);
if (mesure > limiteSup)
reculer();
else if (mesure > limiteInf)
arreter();
else
avancer();
}

Comme vous le constatez, on lit l'entrée analogique et on affecte le résultat à la variable "mesure". Puis, on déplace le robot (fonction Avancer ou Reculer) en fonction de la valeur de la variable.

Vous obtenez un comportement du robot qui doit ressembler à la vidéo ci-dessous :

En résumé

À ce stade, vous savez programmer le robot et lui faire exécuter des mouvements élémentaires. N'hésitez pas avant de poursuivre de beaucoup manipuler, de faire toutes sortes de tests pour vous approprier les codes étudiés ci-dessus. Votre aisance à poursuivre dans les chapitres suivants dépend beaucoup de la maîtrise que vous avez acquise des éléments précédents. Quand vous vous sentirez prêt, il sera possible de passer à la partie suivante.

Exemple de certificat de réussite
Exemple de certificat de réussite