Envoyer et recevoir des données sur la voie série

Dans ce chapitre, nous allons apprendre à utiliser la voie série avec Arduino. Nous allons voir comment envoyer puis recevoir des informations avec l’ordinateur, enfin nous ferons quelques exercices pour vérifier que vous avez tout compris. :) Vous allez le découvrir bientôt, l’utilisation de la voie série avec Arduino est quasiment un jeu d’enfant, puisque tout est opaque aux yeux de l’utilisateur…

Préparer la voie série

Notre objectif, pour le moment, est de communiquer des informations de la carte Arduino vers l’ordinateur et inversement. Pour ce faire, on va d’abord devoir préparer le terrain.

Du côté de l’ordinateur

Pour pouvoir utiliser la communication de l’ordinateur, rien de plus simple. En effet, L’environnement de développement Arduino propose de base un outil pour communiquer. Pour cela, il suffit de cliquer sur le bouton Icône de la voie série (pour les versions antérieures à la version 1.0) dans la barre de menu pour démarrer l’outil. Pour la version 1.0, l’icône a changé et de place et de visuel :

Sélection moniteur série
Sélection moniteur série

Une nouvelle fenêtre s’ouvre : c’est le terminal série :

Voie série, vitesse
Voie série, vitesse

Dans cette fenêtre, vous allez pouvoir envoyer des messages sur la voie série de votre ordinateur (qui est émulée1 par l’Arduino) ; recevoir les messages que votre Arduino vous envoie ; et régler deux trois paramètres tels que la vitesse de communication avec l’Arduino et l’autoscroll qui fait défiler le texte automatiquement. On verra plus loin à quoi sert le dernier réglage.

Du côté du programme
L’objet Serial

Pour utiliser la voie série et communiquer avec notre ordinateur (par exemple), nous allons utiliser un objet (une sorte de variable mais plus évoluée) qui est intégré nativement dans l’ensemble Arduino : l’objet Serial.

Pour le moment, considérez qu’un objet est une variable évoluée qui peut exécuter plusieurs fonctions. On verra (beaucoup) plus loin ce que sont réellement des objets. On apprendra à en créer et à les utiliser lorsque l’on abordera le logiciel Processing.

Cet objet rassemble des informations (vitesse, bits de données, etc.) et des fonctions (envoi, lecture de réception,…) sur ce qu’est une voie série pour Arduino. Ainsi, pas besoin pour le programmeur de recréer tous le protocole (sinon on aurait du écrire nous même TOUT le protocole, tel que "Ecrire un bit haut pendant 1 ms, puis 1 bit bas pendant 1 ms, puis le caractère 'a' en 8 ms…), bref, on gagne un temps fou et on évite les bugs !

Le setup

Pour commencer, nous allons donc initialiser l’objet Serial. Ce code sera à copier à chaque fois que vous allez créer un programme qui utilise la voie série. Le logiciel Arduino à prévu, dans sa bibliothèque Serial, tout un tas de fonctions qui vont nous êtres très utiles, voir même indispensables afin de bien utiliser la voie série. Ces fonctions, je vous les laisse découvrir par vous même si vous le souhaitez, elles se trouvent sur cette page. Dans le but de créer une communication entre votre ordinateur et votre carte Arduino, il faut déclarer cette nouvelle communication et définir la vitesse à laquelle ces deux dispositifs vont communiquer. Et oui, si la vitesse est différente, l’Arduino ne comprendra pas ce que veut lui transmettre l’ordinateur et vice versa ! Ce réglage va donc se faire dans la fonction setup, en utilisant la fonction begin() de l’objet Serial.

Lors d’une communication informatique, une vitesse peut s’exprimer en bits par seconde (bps) ou en bauds. Ainsi, pour une vitesse de 9600 bps on enverra jusqu’à 9600 '0' ou '1' en une seule seconde. Les vitesses les plus courantes sont 9600, 19200 et 115200 bits par seconde. Le baud quant à lui correspond aux nombre de symboles par seconde. Ici, un symbole correspond à une transmission de 8 bits. Donc pour une vitesse de 9600 bps on peut aussi dire qu’on transmet à 1200 bauds (9600/8).

void setup()
{
    // on démarre la liaison
    // en la réglant à une vitesse de 9600 bits par seconde.
    Serial.begin(9600);
}
Démarrage de la liaison série

À présent, votre carte Arduino a ouvert une nouvelle communication vers l’ordinateur. Ils vont pouvoir communiquer ensemble.


  1. créée de façon fictive

Envoyer des données

Le titre est piégeur, en effet, cela peut être l’Arduino qui envoie des données ou l’ordinateur. Bon, on est pas non plus dénué d’une certaine logique puisque pour envoyé des données à partir de l’ordinateur vers la carte Arduino il suffit d’ouvrir le terminal série et de taper le texte dedans ! :P Donc, on va bien programmer et voir comment faire pour que votre carte Arduino envoie des données à l’ordinateur.

Et ces données, elles proviennent d’où ?

Eh bien de la carte Arduino… En fait, lorsque l’on utilise la voie série pour transmettre de l’information, c’est qu’on en a de l’information à envoyer, sinon cela ne sert à rien. Ces informations proviennent généralement de capteurs connectés à la carte ou de son programme (par exemple la valeur d’une variable). La carte Arduino traite les informations provenant de ces capteurs, s’il faut elle adapte ces informations, puis elle les transmet. On aura l’occasion de faire ça dans la partie dédiée aux capteurs, comme afficher la température sur son écran, l’heure, le passage d’une personne, etc.

Appréhender l’objet Serial

Dans un premier temps, nous allons utiliser l’objet Serial pour tester quelques envois de données. Puis nous nous attèlerons à un petit exercice que vous ferez seul ou presque, du moins vous aurez eu auparavant assez d’informations pour pouvoir le réaliser (ben oui, sinon c’est plus un exercice !).

Phrase ? Caractère ?

On va commencer par envoyer un caractère et une phrase. À ce propos, savez-vous quelle est la correspondance entre un caractère et une phrase ? Une phrase est constituée de caractères les uns à la suite des autres. En programmation, on parle plutôt de chaine caractères pour désigner une phrase.

  • Un caractère seul s’écrit entre guillemets simples : 'A’, 'a’, '2’, '!’, …
  • Une phrase est une suite de caractère et s’écrit entre guillemets doubles : "Salut tout le monde", "J’ai 42 ans", "Vive Clem' !"

 Pour vous garantir un succès dans le monde de l’informatique, essayez d’y penser et de respecter cette convention, écrire 'A' ce n’est pas pareil qu’écrire "A" !

println()

La fonction que l’on va utiliser pour débuter, s’agit de println(). Ces deux fonctions sont quasiment identiques, mais à quoi servent-elles ?

  • print() : cette fonction permet d’envoyer des données sur la voie série. On peut par exemple envoyer un caractère, une chaine de caractère ou d’autres données dont je ne vous ai pas encore parlé.
  • println() : c’est la même fonction que la précédente, elle permet simplement un retour à la ligne à la fin du message envoyé.

Pour utiliser ces fonctions, rien de plus simple :

Serial.print("Salut ca zeste ?!");

Bien sûr, au préalable, vous devrez avoir "déclaré/créé" votre objet Serial et définis une valeur de vitesse de communication :

void setup()
{
    // création de l'objet Serial
    // (=établissement d'une nouvelle communication série)
    Serial.begin(9600);
    // envoie de la chaine "Salut ca zeste ?!" sur la voie série
    Serial.print("Salut ca zeste ?!");
}
Envoi d’un message simple via la liaison série

Cet objet, parlons-en. Pour vous aider à représenter de façon plus concise ce qu’est l’objet Serial, je vous propose cette petite illustration de mon cru :

L'objet Serial
L’objet Serial

Comme je vous le présente, l’objet Serial est muni d’un panel de fonctions qui lui sont propres. Cet objet est capable de réaliser ces fonctions selon ce que le programme lui ordonne de faire. Donc, par exemple, quand j’écris : print() en lui passant pour paramètre la chaine de caractère : "Salut ca zeste ?!". On peut compléter le code précédent comme ceci :

void setup()
{
    Serial.begin(9600);

    // l'objet exécute une première fonction
    Serial.print("Salut ca zeste ?!");
    // puis une deuxième fonction, différente cette fois-ci
    Serial.println("Vive Clem' !");
    // et exécute à nouveau la même
    Serial.println("Cette phrase passe en dessous des deux precedentes");
}

Sur le terminal série, on verra ceci :

Salut ca zeste ?! Vive Clem' !
Cette phrase passe en dessous des deux precedentes
La fonction print() en détail

Après cette courte prise en main de l’objet Serial, je vous propose de découvrir plus en profondeur les surprises que nous réserve la fonction print().

Petite précision, je vais utiliser de préférence print().

Résumons un peu ce que nous venons d’apprendre : on sait maintenant envoyer des caractères sur la voie série et des phrases. C’est déjà bien, mais ce n’est qu’un très bref aperçu de ce que l’on peut faire avec cette fonction.

Envoyer des nombres

Avec la fonction print(), il est aussi possible d’envoyer des chiffres ou des nombres car ce sont des caractères :

void setup()
{
    Serial.begin(9600);

    Serial.println(9);            // chiffre
    Serial.println(42);           // nombre
    Serial.println(32768);        // nombre
    Serial.print(3.1415926535);   // nombre décimale
}
9
42
32768
3.14

Tiens, le nombre pi n’est pas affiché complètement ! C’est quoi le bug ? o_O

Rassurez-vous, ce n’est ni un bug, ni un oubli inopiné de ma part. ^^ En fait, pour les nombres décimaux, la fonction print() affiche par défaut seulement deux chiffres après la virgule. C’est la valeur par défaut et heureusement elle est modifiable. Il suffit de rajouter le nombre de décimales que l’on veut afficher :

void setup()
{
    Serial.begin(9600);

    Serial.println(3.1415926535, 0);
    Serial.println(3.1415926535, 2); // valeur par défaut
    Serial.println(3.1415926535, 4);
    Serial.println(3.1415926535, 10);
}
3
3.14
3.1415
3.1415926535
Envoyer la valeur d’une variable

Là encore, on utilise toujours la même fonction (qu’est-ce qu’elle polyvalente !). Ici aucune surprise. Au lieu de mettre un caractère ou un nombre, il suffit de passer la variable en paramètre pour qu’elle soit ensuite affichée à l’écran :

int variable = 512;
char lettre = 'a';

void setup()
{
    Serial.begin(9600);

    Serial.println(variable);
    Serial.print(lettre);
}
512
a

Trop facile n’est-ce pas ?

https://www.youtube.com/watch?v=Jk4jA0OHJDo
Envoyer d’autres données

Ce n’est pas fini, on va terminer notre petit tour avec les types de variables que l’on peut transmettre grâce à cette fonction print() sur la voie série. Prenons l’exemple d’un nombre choisi judicieusement : 65.

 Pourquoi ce nombre en particulier ? Et pourquoi pas 12 ou 900 ?

Eh bien, c’est relatif à la table ASCII que nous allons utiliser dans un instant.

Tout d’abord, petit cours de prononciation, ASCII se prononce comme si on disait "A ski", on a donc : "la table à ski" en prononciation phonétique.

La table ASCII, de l’américain "American Standard Code for Information Interchange", soit en bon français : "Code américain normalisé pour l’échange d’information" est, selon Wikipédia :

"la norme de codage de caractères en informatique la plus connue, la plus ancienne et la plus largement compatible"

Wikipédia

En somme, c’est un tableau de valeurs codées sur 8bits qui à chaque valeur associent un caractère. Ces caractères sont les lettres de l’alphabet en minuscule et majuscule, les chiffres, des caractères spéciaux et des symboles bizarres. Dans cette table, il y a plusieurs colonnes avec la valeur décimale, la valeur hexadécimale, la valeur binaire et la valeur octale parfois. Nous n’aurons pas besoin de tout ça, mais pour votre culture voici une table ASCII étendu (0 à 255).

Table ASCII étendu
Table ASCII étendu - (CC-BY-SA, Yuriy Arabskyy)

Revenons à notre exemple, le nombre 65. C’est en effet grâce à la table ASCII que l’on sait passer d’un nombre à un caractère, car rappelons-le, dans l’ordinateur tout est traité sous forme de nombre en base 2 (binaire). Donc lorsque l’on code :

maVariable = 'A';
// l'ordinateur stocke la valeur 65 dans sa mémoire (cf. table ASCII)

Si vous faites ensuite :

maVariable = maVariable + 1;
// la valeur stockée passe à 66 (= 65 + 1)

// à l'écran, on verra s'afficher la lettre "B"

Au début, on trouvait une seule table ASCII, qui allait de 0 à 127 (codée sur 7bits) et représentait l’alphabet, les chiffres arabes et quelques signes de ponctuation. Depuis, de nombreuses tables dites "étendues" sont apparues et vont de 0 à 255 caractères (valeurs maximales codables sur un type char qui fait 8 bits).

 Et que fait-on avec la fonction print() et cette table ?

Là est tout l’intérêt de la table, on peut envoyer des données, avec la fonction print(), de tous types ! En binaire, en hexadécimal, en octal et en décimal.

void setup()
{
    Serial.begin(9600);

    Serial.println(65, BIN); // envoie la valeur 1000001
    Serial.println(65, DEC); // envoie la valeur 65
    Serial.println(65, OCT); // envoie la valeur 101 (ce n'est pas du binaire !)
    Serial.println(65, HEX); // envoie la valeur 41
}
Différents moyens d’afficher la même information

Vous pouvez donc manipuler les données que vous envoyez à travers la voie série ! C’est là qu’est l’avantage de cette fonction.

Exercice : Envoyer l’alphabet
Objectif

Nous allons maintenant faire un petit exercice, histoire de s’entraîner à envoyer des données. Le but, tout simple, est d’envoyer l’ensemble des lettres de l’alphabet de manière la plus intelligente possible, autrement dit, sans écrire 26 fois "print();"… La fonction setup restera la même que celle vue précédemment. Un délai de 250 ms est attendu entre chaque envoi de lettre et un delay de 5 secondes est attendu entre l’envoi de deux alphabets.

Bon courage !

Correction

Bon j’espère que tout c’est bien passé et que vous n’avez pas joué au roi du copier/coller en me mettant 26 print…

void loop()
{
  char i = 0;
  char lettre = 'a'; // ou 'A' pour envoyer en majuscule

  // petit message d'accueil
  Serial.println("------  L'alphabet des Zesteurs  ------");

  // on commence les envois
  for(i=0; i<26; i++)
  {
      Serial.print(lettre); // on envoie la lettre
      lettre = lettre + 1; // on passe à la lettre suivante
      delay(250); // on attend 250ms avant de réenvoyer
   }
  Serial.println(""); // on fait un retour à la ligne

   delay(5000); // on attend 5 secondes avant de renvoyer l'alphabet
}
Exercice d’écriture de l’alphabet

Si l’exercice vous a paru trop simple, vous pouvez essayer d’envoyer l’alphabet à l’envers, ou l’alphabet minuscule ET majuscule ET les chiffres de 0 à 9… Amusez-vous bien ! ;)

Recevoir des données

Cette fois, il s’agit de l’Arduino qui reçoit les données que nous, utilisateur, allons transmettre à travers le terminal série. Je vais prendre un exemple courant : une communication téléphonique. En règle générale, on dit "Hallo" pour dire à l’interlocuteur que l’on est prêt à écouter le message. Tant que la personne qui appelle n’a pas cette confirmation, elle ne dit rien (ou dans ce cas elle fait un monologue ^^ ). Pareillement à cette conversion, l’objet Serial dispose d’une fonction pour "écouter" la voie série afin de savoir si oui ou non il y a une communication de données.

Réception de données
On m’a parlé ?

Pour vérifier si on a reçu des données, on va régulièrement interroger la carte pour lui demander si des données sont disponibles dans son buffer de réception. Un buffer est une zone mémoire permettant de stocker des données sur un court instant. Dans notre situation, cette mémoire est dédiée à la réception sur la voie série. Il en existe un aussi pour l’envoi de donnée, qui met à la queue leu leu les données à envoyer et les envoie dès que possible. En résumé, un buffer est une sorte de salle d’attente pour les données. Je disais donc, nous allons régulièrement vérifier si des données sont arrivées. Pour cela, on utilise la fonction available() (de l’anglais "disponible") de l’objet Serial. Cette fonction renvoie le nombre de caractères dans le buffer de réception de la voie série. Voici un exemple de traitement :

void loop()
{
    // lecture du nombre de caractères disponibles dans le buffer
    int donneesALire = Serial.available();
    if(donneesALire > 0) // si le buffer n'est pas vide
    {
        // Il y a des données, on les lit et on fait du traitement
    }
    // on a fini de traiter la réception ou il n'y a rien à lire
}
Lecture simple de données sur la voie série

Cette fonction de l’objet Serial, available(), renvoie la valeur -1 quand il n’y a rien à lire sur le buffer de réception.

Lire les données reçues

Une fois que l’on sait qu’il y a des données, il faut aller les lire pour éventuellement en faire quelque chose. La lecture se fera tout simplement avec la fonction… read() ! Cette fonction renverra le premier caractère arrivé non traité (comme un supermarché traite la première personne arrivée dans la file d’attente de la caisse avant de passer au suivant). On accède donc caractère par caractère aux données reçues. Ce type de fonctionnement est appelé FIFO (First In First Out, premier arrivé, premier traité). Si jamais rien n’est à lire (personne dans la file d’attente), je le disais, la fonction renverra -1 pour le signaler.

void loop()
{
    // on lit le premier caractère non traité du buffer
    char choseLue = Serial.read();

    if(choseLue == -1) // si le buffer est vide
    {
        // Rien à lire, rien lu
    }
    else // le buffer n'est pas vide
    {
        // On a lu un caractère
    }
}
Lecture simple de données sur la voie série sans available

Ce code est une façon simple de se passer de la fonction available().

https://www.youtube.com/watch?v=DpOahtA9NM0
Le serialEvent

Si vous voulez éviter de mettre le test de présence de données sur la voie série dans votre code, Arduino a rajouté une fonction qui s’exécute de manière régulière. Cette dernière se lance régulièrement avant chaque redémarrage de la loop. Ainsi, si vous n’avez pas besoin de traiter les données de la voie série à un moment précis, il vous suffit de rajouter cette fonction. Pour l’implémenter c’est très simple, il suffit de mettre du code dans une fonction nommé serialEvent() (attention à la casse) qui sera a rajouté en dehors du setup et du loop. Le reste du traitement de texte se fait normalement, avec Serial.read() par exemple. Voici un exemple de squelette possible :

const int maLed = 11; // on met une LED sur la broche 11

void setup()
{
    pinMode(maLed, OUTPUT); // la LED est une sortie
    digitalWrite(maLed, HIGH); // on éteint la LED
    Serial.begin(9600); // on démarre la voie série
}

void loop()
{
    delay(500); // fait une petite pause
    // on ne fait rien dans la loop
    digitalWrite(maLed, HIGH); // on éteint la LED

}

void serialEvent() // déclaration de la fonction d'interruption sur la voie série
{
    // lit toutes les données (vide le buffer de réception)
    while(Serial.read() != -1);

    // puis on allume la LED
    digitalWrite(maLed, LOW);
}
Utilisation de serialEvent pour tester la présence de données
Exemple de code complet

Voici maintenant un exemple de code complet qui va aller lire les caractères présents dans le buffer de réception s’il y en a et les renvoyer tels quels à l’expéditeur (mécanisme d’écho).

void setup()
{
    Serial.begin(9600);
}

void loop()
{
    // variable contenant le caractère à lire
    char carlu = 0;
    // variable contenant le nombre de caractère disponibles dans le buffer
    int cardispo = 0;

    cardispo = Serial.available();

    while(cardispo > 0) // tant qu'il y a des caractères à lire
    {
        carlu = Serial.read(); // on lit le caractère
        Serial.print(carlu); // puis on le renvoi à l’expéditeur tel quel
        cardispo = Serial.available(); // on relit le nombre de caractères dispo
    }
    // fin du programme
}
Code complet pour faire un echo avec la liaison série

Avouez que tout cela n’était pas bien difficile. Je vais donc en profiter pour prendre des vacances et vous laisser faire un exercice qui demande un peu de réflexion. :diable:

[Exercice] Attention à la casse !

Consigne

Le but de cet exercice est très simple. L’utilisateur saisit un caractère à partir de l’ordinateur et si ce caractère est minuscule, il est renvoyé en majuscule ; s’il est majuscule il est renvoyé en minuscule. Enfin, si le caractère n’est pas une lettre on se contente de le renvoyer normalement, tel qu’il est. Voilà le résultat de mon programme :

Voici le simulateur interactif avec juste une Arduino, pour ceux qui n’ont pas le matériel en réel mais veulent tout de même faire l’exercice :

!(https://www.tinkercad.com/embed/hLVCsf3NkbI)

Correction

Je suppose que grâce au superbe tutoriel qui précède vous avez déjà fini sans problème, n’est-ce pas ? :P

La fonction setup() et les variables utiles

Une fois n’est pas coutume, on va commencer par énumérer les variables utiles et le contenu de la fonction setup(). Pour ce qui est des variables globales, on n’en retrouve qu’une seule, "carlu". Cette variable de type int sert à stocker le caractère lu sur le buffer de la carte Arduino. Puis on démarre une nouvelle voie série à 9600 bps :

int carlu; // stock le caractère lu sur la voie série

void setup()
{
   Serial.begin(9600);
}
Exercice, le setup
Le programme

Le programme principal n’est pas très difficile non plus. Il va se faire en trois temps.

  • Tout d’abord, on boucle jusqu’à recevoir un caractère sur la voie série
  • Lorsqu’on a reçu un caractère, on regarde si c’est une lettre
  • Si c’est une lettre, on renvoie son acolyte majuscule ; sinon on renvoie simplement le caractère lu

Voici le programme décrivant ce comportement :

void loop()
{
   // on commence par vérifier si un caractère est disponible dans le buffer
   if(Serial.available() > 0)
   {
       carlu = Serial.read(); // lecture du premier caractère disponible

       // Est-ce que c'est un caractère minuscule ?
       if(carlu >= 'a' && carlu <= 'z')
       {
           carlu = carlu - 'a'; // on garde juste le "numéro de lettre"
           carlu = carlu + 'A'; // on passe en majuscule
       }
       // Est-ce que c'est un caractère MAJUSCULE ?
       else if(carlu >= 'A' && carlu <= 'Z')
       {
           carlu = carlu - 'A'; // on garde juste le "numéro de lettre"
           carlu = carlu + 'a'; // on passe en minuscule
       }
       // ni l'un ni l'autre on renvoie en tant que BYTE
       // ou alors on renvoie le caractère modifié
       Serial.write(carlu);
   }
}
Exercice, la loop

Je vais maintenant vous expliquer les parties importantes de ce code. Comme vu dans le cours, la ligne 4 va nous servir à attendre un caractère sur la voie série. Tant qu’on ne reçoit rien, on ne fait rien ! Sitôt que l’on reçoit un caractère, on va chercher à savoir si c’est une lettre. Pour cela, on va faire deux tests. L’un est à la ligne 8 et l’autre à la ligne 13. Ils se présentent de la même façon : SI le caractère lu à une valeur supérieure ou égale à la lettre 'a' (ou 'A') ET inférieure ou égale à la lettre 'z' ('Z'), alors on est en présence d’une lettre. Sinon, c’est autre chose, donc on se contente de passer au renvoi du caractère lu ligne 21. Une fois que l’on a détecté une lettre, on effectue quelques transformations afin de changer sa casse. Voici les explications à travers un exemple :

Description Opération (lettre) Opération (nombre) Valeur de carlu
On récupère la lettre 'e' e 101 'e'
On isole son numéro de lettre en lui enlevant la valeur de 'a' e-a 101–97 4
On ajoute ce nombre à la lettre 'A' A + (e-a) 65 + (101–97) = 69 'E'
Il ne suffit plus qu’à retourner cette lettre E 69 E

On effectuera sensiblement les mêmes opérations lors du passage de majuscule à minuscule.

A la ligne 22, j’utilise la fonction write() qui envoie le caractère en tant que variable de type byte, signifiant que l’on renvoie l’information sous la forme d’un seul octet. Sinon Arduino enverrait le caractère en tant que 'int’, ce qui donnerait des problèmes lors de l’affichage.

Voici l’exercice sous forme de simulateur interactif :

!(https://www.tinkercad.com/embed/flg9YQX2g1f)


Vous savez maintenant lire et écrire sur la voie série de l’Arduino ! Grâce à cette nouvelle corde à votre arc, vous allez pouvoir ajouter une touche d’interactivité supplémentaire à vos programmes.