Comme cela a été souligné au chapitre C2‑I , si une carte Arduino ne permet pas de mettre en œuvre des entrées‑sorties standards, elle peut néanmoins échanger des données textuelles avec le poste de travail auquel elle est reliée via une liaison série W. De manière générale, une liaison série est constituée d'un médium – c'est‑à‑dire un câble ou autre support de transmission – associé à un protocole de communication (cf. chap. R1‑IV R) permettant, entre deux systèmes numériques :
- d'établir la communication (avec, par exemple, un bit de start), c'est‑à‑dire préparer chaque système à la transmission de données ;
- de transmettre des données les unes après les autres sur le médium – ce que sous‑tend le terme « série » ;
- de clore la communication (avec, par exemple, un bit de stop), éventuellement avec des éléments de vérification de bonne transmission (typiquement, par contrôle de parité).
Parce qu'elles ne requièrent qu'un nombre minimal de conducteurs (3 fils) dans le support de transmission, les liaisons séries ont depuis longtemps supplanté les liaisons parallèles. Ainsi, un protocole série ancien comme l'UART (universal asynchronous receiver transmitter) W reste très employé en informatique embarquée. Mais même s'il peut sembler simple par rapport à d'autres (Ethernet, etc.), sa mise en œuvre complète reste assez complexe Il ne suffit pas de quelques heures d'étude pour en comprendre tous les ressorts.
Heureusement, en programmation des microcontrôleurs, cette complexité est masquée par l'emploi de fonctions de haut niveau comme, par exemple, Serial.print dans le framework Arduino. Néanmoins, pour une bonne compréhension de ces fonctions, il est souhaitable de connaître quelques concepts et mécanismes sous‑jacents : buffer d'entrée‑sortie, circuit de sérialisation/désérialisation, etc. Mais attention ! Certains de ces aspects font appel des notions de codage qui n'ont été que peu ou pas étudiées jusqu'à présent dans ce module : objets, chaînes de caractères, pointeurs, interruptions… certaines notions n'étant même pas au programme du module de formation au langage C.
Ce chapitre n'a donc pas vocation à exposer en détail tous les aspects d'une liaison série (pour des compléments, on pourra se reporter à ce cours ). L'objectif est ici d'aborder les principes essentiels de communication par liaison série, afin de pouvoir ensuite coder, avec quand même une bonne maîtrise, des opérations de lecture et d'écriture de données dans le moniteur série émulé par le logiciel Arduino IDE ou dans l'environnement de simulation Tinkercad. Seront donc étudiés dans l'ordre :
- des rudiments sur les aspects matériels d'une liaison série de type UART ;
- les éléments sur lesquels reposent aspects logiciels d'une telle liaison série dans le framework Arduino, notamment l'objet
Serialet ses buffers, ainsi que le moniteur série ; - les quelques fonctions d'administration d'une telle liaison série ;
- les principales fonctions associées aux opérations de sortie, c'est‑à‑dire d'écriture dans le moniteur série ;
- les principales fonctions associées aux opérations d'entrée, c'est‑à‑dire de lecture dans le moniteur série ;
sachant que ce sont ces deux dernières sections du chapitre qui apportent les éléments de langage les plus opérationnels dans le bagage d'un codeur.
Aspects matériels – généralités
Cas des cartes Arduino
Sauf cas particuliers, toute carte Arduino possède au moins (cf. chap. C1‑III ) :
- un circuit USART W (universal synchronous & asynchronous receiver transmitter) intégré à son microcontrôleur principal ;
- un port USB avec un circuit intégré dédié à l'interface USB‑USART.
Ces dispositifs permettent une mise en œuvre facile des communications par liaison série standard avec un autre système, notamment un poste de travail – cf. figure ci‑dessous.
À titre d'exemple emblématique, une carte Arduino Uno R3 est équipée d'un port USB type B et embarque, en plus de son microcontrôleur principal, un deuxième microcontrôleur (un Atmel ATmega16U2), cadencé par un circuit oscillant externe à quartz 16 MHz. Ce dispositif est entièrement dédié à l'interface USB‑USART (cf. la figure ci‑contre).
Attention ! Sur les cartes clones de type « Uno » produites par des fabricants tiers (principalement chinois), à la place d'un microcontrôleur dédié, c'est plutôt un ASIC W (application‑specific integrated circuit) avec un circuit oscillant interne qui réalise l'interface USB‑USART. Typiquement, il peut s'agir :
Cette différence technologique peut poser des problèmes de connexion de la carte avec le poste de travail, notamment si le pilote du circuit n'est pas installé sur le système d'exploitation. Si tel est le cas, on pourra se reporter à cette remarque du chap. C1‑III .
Pour une liaison série, le médium de transmission employé est typiquement un câble USB (universal serial bus) raccordé au port USB de la carte. Cette liaison sert notamment au téléversement du programme utilisateur mais aussi, durant son exécution, à afficher du texte et lire des valeurs saisies par l'utilisateur sur le poste de travail. On parle de moniteur série, comme l'illustre la figure ci‑dessous.
Mais plus généralement, une liaison série peut aussi être employée pour échanger des données entre une carte à microcontrôleur et un serveur de données, une machine (station de mesure, dispositif contrôlé à distance…), un instrument de musique, etc.
Médium direct – broches TX & RX
Sur les cartes Arduino, on peut aussi employer un médium direct (cf. la carte Uno représentée partiellement en figure ci‑contre) constitué :
- d'une paire de fils torsadés reliée aux broches 0 et 1 du port numérique, lesquelles sont respectivement dédiées à l'émission (celle désignée TX) et à la réception (celle désignée RX) ;
- et d'un fil de masse relié à la broche GND, ce dernier fournissant le potentiel de référence pour déterminer les niveaux de tension bas et haut (respectivement 0 V et 5 V) sur les broches TX et RX.
On rappelle que c'est la raison pour laquelle il est recommandé de ne pas utiliser comme des entrées‑sorties usuelles les broches 0 et 1 du port numérique – même si cela reste possible moyennant certaines précautions – cf. chap. C2‑XIII .
Par ailleurs, la carte comporte deux leds désignées également TX et RX mais ces dernières ne sont pas reliées électriquement aux broches 0 et 1 du port numérique du microcontrôleur principal de la carte (cf. par exemple le schéma électronique d'une carte Uno R3 A). Elles ne reproduisent donc pas les niveaux de tension sur ces broches. En fait, ces led sont reliées à des broches du microcontrôleur dédié à l'interface USB‑USART, leur rôle étant de signaler l'activité de la liaison entre la carte et le poste de travail.
Remarque. La led TX clignote lorsque l'on génère (via le programme utilisateur) un signal variable sur la broche 1 du port numérique. Mais il s'agit alors simplement d'un artefact : le microcontrôleur de l'interface USB‑USART interprète à tort ce signal variable comme une activité de la liaison entre la carte et le poste de travail.
Ports série multiples
Sur certains modèles de cartes à microcontrôleur, il peut exister plusieurs autres paires de broches permettant la mise en œuvre d'autres liaisons série en plus de celle utilisée couramment.
Sur les cartes Arduino Mega et Arduino Due, On dispose en tout de 4 liaisons série avec les associations suivantes :
| Nº broches | Fonctions | Objet |
|---|---|---|
| 1 & 0 | TX & RX | Serial |
| 18 & 19 | TX1 & RX1 | Serial1 |
| 16 & 17 | TX2 & RX2 | Serial2 |
| 14 & 15 | TX3 & RX3 | Serial3 |
Schéma‑bloc de l'USART interne au microcontrôleur Atmel ATmega328p
Le schéma‑bloc ci‑dessous détaille l'implémentation matérielle de l'USART intégré au microcontrôleur Atmel ATmega328p, lequel équipe notamment les cartes Arduino Uno et Nano (cf. chap. C1‑III ).
Il est constitué de 3 principaux circuits :
- un circuit d'horloge (clock generator) qui, en divisant la fréquence du signal issu du circuit oscillant externe, génère le signal de cadencement de la liaison série à la vitesse spécifiée – ce qu'on appelle en anglais le baud rate ;
- un circuit d'émission (transmitter) dédié à la sortie des données, qui est lui‑même composé :
- d'un registre d'émission pour stocker un octet à émettre ;
- et d'un sous‑circuit de sérialisation qui, à partir des bits de l'octet contenu dans le registre d'émission, génère à la fréquence du circuit d'horloge le signal logique à écrire sur la broche TX ;
- un circuit de réception (receiver) dédié à l'entrée des données, qui est lui‑même composé :
- d'un sous‑circuit de désérialisation qui, à partir du signal logique reçu sur la broche RX, reconstitue un octet reçu ;
- cet octet étant stocké dans le registre de réception ;
- d'un groupe de 3 registres, d'un octet chacun, pour stocker les bits d'état et de commande de l'USART, à des fins de contrôle logiciel.
Cas des cartes à modules ESP8266 et ESP32
À la différence de la plupart des cartes Arduino, les cartes à modules ESP8266 et ESP32 (cf. chap. C1‑III ) ne sont pas dotées d'un microcontrôleur et d'un quartz externe dédiés pour la conversion USB‑USART.
Comme les cartes clones Arduino, elles sont équipées à la place d'un ASIC W (application‑specific integrated circuit) qui intègre un circuit oscillant interne. Cela peut éventuellement poser les mêmes problèmes de connexion de la carte au poste de travail si les pilotes de ces circuits ne sont pas installés sur le poste de travail (chap. C1‑III ).
En règle générale, on retrouve les mêmes fonctionnalités de liaisons séries que sur des cartes Arduino.
À titre d'exemple, la carte SBC‑NodeMCU à module ESP8266 de l'assembleur Joy‑It permet deux liaisons séries UART (cf. la figure ci‑contre dite pinout et le chap. C2‑VIII pour plus de détails) :
- UART0 sur les broches GPIO 1 & 3 (TXD0 & RXD0) ;
- UART2 sur les broches GPIO 15 & 13, étiquetée D8 & D7 sur la carte (TXD2 & RXD2) ;
sachant que la liaison UART0 est celle qui est prise en charge par défaut via le circuit d'interface USB‑USART de typeCP2102 (cf. supra ).
La carte SBC‑NodeMCU illustrant l'exemple supra possède aussi une liaison UART1 qui est réservée au téléversement du programme utilisateur et du firmware de la carte. Seule la broche TXD1 est déployée et porte le numéro GPIO 2, étiqueté D4 sur la carte .
Aspects logiciels – généralités
L'objet Serial
La liaison série entre une carte Arduino ou compatible et le moniteur série de l'application Arduino IDE s'exécutant sur un poste de travail est une liaison asynchrone W spécifiée par le protocole UART W (universal asynchronous receiver transmitter).
Du point de vue logiciel, cette liaison est implémentée par défaut sur la base d'un objet déclaré, désigné par l'identificateur Serial A.
Instanciation de l'objet Serial
Attention ! Les notions de base de la programmation orientée objet (classe, objet, constructeur) ont été ébauchées au chapitre C2‑VI , justement pour pouvoir comprendre les grandes lignes d'éléments de code figurant les fichiers d'en‑tête et d'implémentation du framework Arduino. Toutefois, ce ne sera pas facile car, pour des questions de pédagogie, certains notions comme par exemple celle d'héritage W n'ont pas été abordées.
- L'objet
Serialest déclaré dans le fichier d'implémentationHardwareSerial0.cppcomme une instance de la classeHardwareSerial, laquelle est déclarée le fichier d'en‑têteHardwareSerial.h, inclus par défaut dans tout programme d'extension.inopar une directive codée dans le fichierArduino.hG (ligne nº 233). - La classe
HardwareSerialest elle‑même une fille de la classeStreamqui est déclarée dans fichier d'en‑têteStream.h, lequel est inclus par une directive codée dans le fichier d'en‑têteHardwareSerial.hG (ligne nº 29). - La classe
Streamest elle‑même une fille de la classePrintqui est déclarée dans le fichier d'en‑têtePrint.h, lequel est inclus par une directive codée dans le fichier d'en‑têteStream.hG(ligne nº 26).
Donc, en résumé :
L'objet Serial n'a pas besoin d'être instancié par le codeur dans le code source des programmes pour cartes Arduino. Il est déjà instancié dans un fichier d'en‑tête du framework Arduino et est donc inclus par défaut dans le programme utilisateur.
Parce qu'il n'est pas une classe mais une instance de classe, l'objet Serial devrait, conformément aux bonnes pratiques du codage en C++ (cf. chap. C2‑X W), être nommé avec une lettre minuscule initiale, c'est‑à‑dire serial.
Le choix de la majuscule initiale par les concepteurs du framework Arduino se justifie par la nécessité de faire la distinction de cet objet par rapport aux données déclarées par utilisateur, aux yeux d'un public cible de codeurs débutants ou amateurs.
Buffers de l'objet Serial
On rappelle que d'une manière générale, tout objet est une variable structurée, définie avec divers champs, c'est‑à‑dire des composantes de stockage de données (en quelque sorte, des « sous‑variables »).
L'objet Serial possède deux champs essentiels issus de la classe HardwareSerial, déclarés dans le fichier d'en‑tête HardwareSerial.h G :
- le buffer de réception :
unsigned char _rx_buffer[SERIAL_RX_BUFFER_SIZE];
unsigned char _tx_buffer[SERIAL_TX_BUFFER_SIZE];
sachant qu'un buffer est un espace mémoire tampon permettant de stocker des données de façon temporaire.
Dans les instructions ci‑dessus, les deux buffers sont déclarés :
- comme des tableaux (notion abordée au chap. C5‑III ),
- dont les éléments sont de type
unsigned char, donc encodés en binaire naturel sur 1 octet (8 bits) chacun, - le nombre d'éléments étant respectivement spécifié par les pseudo‑constantes
SERIAL_RX_BUFFER_SIZEetSERIAL_TX_BUFFER_SIZE, définies dans le fichier d'en‑têteHardwareSerial.havec comme valeur par défaut :
Remarque. Dans le framework Arduino, les identificateurs de ces deux pseudo‑constantes sont utilisables pour former des expressions dans les programmes sources.
De plus, les buffers d'émission et de réception de l'objet Serial sont l'un et l'autre implémentés de façon circulaire avec, pour chacun, un indice de queue (_buffer_tail) et un indice de tête (_buffer_head) W.
Comme illustré sur la figure ci‑contre, chaque buffer est opéré avec une gestion des priorités comme celle d'une file d'attente FIFO (first in first out W) :
- L'indice de queue cible l'octet arrivé en premier dans le buffer (le plus ancien), qui doit être traité en premier (parmi tous ceux qu'il contient).
- L'indice de tête cible l'élément placé juste après l'octet arrivé en dernier dans le buffer (le plus récent), qui sera traité en dernier (parmi tous ceux qu'il contient).
Sachant sa circularité, le buffer est donc :
- vide lorsque l'indice de queue est égal à l'indice de tête (cf. le cas de figure ci‑contre) ;
- partiellement plein lorsque l'indice de tête est différent d'au moins deux unités (supérieur ou inférieur) à l'indice de queue (cf. les deux cas de figure ci‑dessous) ;
- plein lorsque l'indice de tête est égal moins une unité à l'indice de queue (cf. le cas de figure ci‑contre).
Avec cette structure de données, la capacité effective du buffer est inférieure d'une unité au nombre d'éléments du tableau.
Exemple. Dans le cas d'une carte Arduino Uno R3, puisque le nombre d'éléments des tableaux _rx_buffer et _tx_buffer vaut 64, la capacité effective des buffers est de 63 octets.
En résumé, pour une mise en œuvre asynchrone de la liaison série :
L'objet Serial dispose, respectivement pour l'émission et la réception de données d'un buffer circulaire d'une capacité effective qui dépend du type de carte à microcontrôleur employée, typiquement 63 octets pour une Arduino Uno.
Ces deux buffers constituent l'un et l'autre un espace mémoire tampon géré comme une file d'attente FIFO (first in first out) :
- avant sérialisation en signal logique de chaque octet émis vers le système auquel la carte est raccordée ;
- après désérialisation du signal logique de chaque octet reçu depuis le système auquel la carte est raccordée.
Ports série multiples
Pour les cartes Arduino disposant de plusieurs ports série (Mega, Due…, cf. supra ), le fichier HardwareSerial0.cpp déclare des instances supplémentaires de la classe HardwareSerial, qui sont identifiées respectivement Serial1, Serial2 et Serial3.
Ces objets ont exactement les mêmes propriétés (mêmes champs et mêmes méthodes) que l'objet Serial.
Méthodes associées à l'objet Serial
Dans la logique de la programmation orientée objet, un objet se manipule à l'aide de méthodes, c'est‑à‑dire des fonctions dédiées qui sont définies dans la déclaration de la classe à laquelle l'objet appartient ou des classes dont il descend (c'est la notion d'héritage).
L'objet Serial compte 20 méthodes publiques, recensées sur la page de référence suivante A. On peut les classer en trois catégories comme dans le tableau ci‑dessous.
| Administration | begin end |
|---|---|
| Écriture | availableForWrite flush print println write |
| Lecture | available find findUntil parseFloat parseInt peek read readBytes readByteUntil readString serialEvent setTimeout |
Toutes ces méthodes ne peuvent être appelées que via l'opérateur de sélection (cf. chap. C2‑IV ), codé par le symbole . et appliqué à l'objet Serial (ou un autre objet de la même classe).
Pour mettre fin à la liaison série entre une carte Arduino et un équipement (poste de travail, etc.), on code une instruction appelant la méthode end via la syntaxe :
Serial.end();
Cas des cartes à modules ESP8266 et ESP32
Dans le cas des cartes à modules ESP8266 et ESP32 (cf. supra ), les fichiers de bibliothèque qui implémentent les liaisons série UART (HardwareSerial.h, etc.) sont similaires à ceux pour les cartes Arduino, mais pas identiques. En effet :
Le moniteur série
Sur le poste de travail, l'application Arduino IDE V2 émule un moniteur série dans le cadre multi‑fonctions en dessous de la fenêtre d'édition du code (cf. chap. C1‑III . Par des instructions codées dans le programme utilisateur téléversé sur la carte,il permet à l'utilisateur de visualiser les affichages (sorties) et de saisir des valeurs (entrées) via la liaison série.
L'onglet du moniteur série peut être ouvert dans le cadre multi‑fonctions (sous la fenêtre d'édition de code) soit par une commande éponyme du menu Outils, soit en cliquant sur le bouton de raccourci en haut à droite de l'éditeur.
Dans cet onglet, on trouve :
- en haut, la barre de saisie pour les entrées de la carte, qui sont envoyées après validation par la touche ENTRÉE ↵ du clavier sur le poste de travail ;
- au centre, la zone d'affichage pour les sorties de la carte ;
- en haut à gauche, des boutons pour contrôler quelques fonctionnalités de la zone d'affichage des sorties – le défilement automatique (autoscroll), l'horodatage (timestamp) et l'effaçage ;
- juste en dessous, deux menus déroulants pour respectivement sélectionner :
La barre de saisie et la zone d'affichage opèrent l'une comme l'autre au format UTF‑8 qui est, rappelons-le, compatible avec le jeu de caractères ASCII restreint (cf. chap. C3‑IX ).
Vitesse de transmission
On parle de liaison asynchrone entre deux machines lorsqu'elles ne partagent pas d'horloge commune (ce qui permet d'économiser un fil sur le médium de transmission). Il est alors indispensable que chacune des deux machines ajuste à la même fréquence un signal d'horloge interne pour cadencer la transmission. Le choix de cette fréquence commune conditionne directement la vitesse de transmission des données sur la liaison.
Dans une transmission en série, l'unité de vitesse est le baud W (symbole Bd). Cette unité est définie comme étant égale à 1 symbole par seconde, où le symbole est l'unité de taille des données transmises. Dans la plupart des cas, et notamment avec les cartes Arduino ou compatible, le symbole est simplement le bit. On a donc la formule :
Concrètement on spécifie la même vitesse de transmission de la liaison série entre un poste de travail et une carte Arduino ou compatible qui y est raccordée :
Les valeurs standards de la vitesse de transmission vont de 300 à 2 000 000 Bd. Pour faire un bon choix, il faut avoir en tête les considérations suivantes :
- La valeur 9600 Bd est restée durant longtemps la vitesse par défaut. C'est un choix « historique » qui aujourd'hui est déconseillé car il peut être pénalisant pour la réactivité du programme utilisateur.
- La valeur 115200 Bd est aujourd'hui celle qui est employée préférentiellement ; elle tient lieu de nouvelle valeur par défaut. Elle est environ 10 fois supérieure à la précédente.
Le traceur série
En alternative au moniteur série, l'application Arduino IDE permet d'afficher sous forme de diagramme temporel les valeurs numériques de sortie d'une carte à microcontrôleur. C'est le traceur série (serial plotter).
On accède à ce mode d'affichage soit via une commande éponyme du menu Tools, soit en cliquant sur le bouton de raccourci en haut à droite de l'éditeur. Il peut être opérationnel même lorsque le moniteur série est également activé.
Pour chaque variable du programme exécuté dans la carte, dont les valeurs sont envoyées via la liaison série par une instruction répétitive (c'est‑à‑dire codée dans la fonction loop) de la forme :
Serial.print(identificateur); (cf. infra )
le traceur trace sur un diagramme cartésien une courbe dont les points ont pour abscisse l'instant de réception et pour ordonnée la valeur de la variable transmise à cet instant.
On voit donc apparaître sur le diagramme autant de courbes de que variables suivies, chacune étant tracée avec une couleur distincte, automatiquement choisie.
Considérons le programme académique ci‑dessous qui, sur une carte Arduino Uno, effectue une lecture analogique de la broche A0 laissée flottante (c'est‑à‑dire reliée à aucun dispositif). Un appel de la fonction delay permet de limiter la fréquence des lectures.
void setup()
{
Serial.begin(115200);
Serial.println();
Serial.flush();
delay(100);
}
void loop()
{
int randomAnalog = analogRead(A0);
Serial.println(randomAnalog);
delay(100);
}
La capture d'écran ci‑dessous montre l'affichage du traceur série lors de l'exécution :
Dans l'IDE VS Code
Dans l'IDE VS code avec installées les extensions Arduino Community Edition et Serial Monitor de Microsoft (cf. chap. C1‑III ), le moniteur série est accessible dans un onglet dédié du cadre inférieur sous le cadre d'édition de code – cf. la capture d'écran ci‑dessous. Il s'ouvre simplement par un clic de souris.
Ce moniteur série fournit plus de fonctionnalités et de possibilités de paramétrage que celui de l'application Arduino IDE. En particulier, il permet de gérer en parallèle plusieurs liaisons séries entre une carte Arduino ou compatible et le poste de travail. C'est pourquoi l'ouverture du moniteur série ne provoque pas d'activation immédiate de ce dernier. Pour cela, il faut effectuer deux actions :
- via un menu déroulant, sélectionner le port USB auquel la carte est raccordée ;
- cliquer sur le bouton
▷ Start Monitoring
Les messages envoyés par la carte peuvent alors apparaître dans la zone d'affichage. De plus, la barre de saisie s'active pour permettre à l'utilisateur d'envoyer des messages vers la carte.
Comme sur l'application Arduino IDE (et avec davantage de fonctionnalités), on dispose de plusieurs boutons de contrôle, respectivement pour :
- l'effacement du contenu de la zone d'affichage ;
- l'activation/désactivation de l'horodatage des messages (timestamp), du défilement automatique du texte (autoscroll), de la barre de saisie (terminal mode) et des messages internes à l'application (sent message echoing).
Par ailleurs, le bouton ⚙ permet d'accéder aux réglages fins du protocole de communication de la liaison série, que l'on ne détaillera pas ici (cf. chap. R3‑VII R).
L'extensions Arduino Community Edition, tout comme l'extension Serial Monitor de Microsoft, n'implémentent pas de traceur série.
On peut néanmoins pallier cette lacune en installant une extension dédiée, comme par exemple Teleplot.
Dans l'environnement en ligne Tinkercad
L'environnement de simulation Tinkercad permet de simuler le moniteur série comme si une vraie carte était reliée à l'ordinateur par liaison série.
Dans une fenêtre de circuit dont le volet code est ouvert, la simulation du moniteur série est activée par un clic en bas du volet.
Il apparaît alors un zone d'affichage pour visualiser les sorties et en dessous une barre de saisie pour taper au clavier des entrées, à valider en cliquant sur le bouton de droite « envoyer ».
Il n'est pas nécessaire de paramétrer la vitesse de transmission : elle s'ajuste automatiquement à celle du code source (il ne s'agit pas d'une véritable liaison série mais d'une simulation).
En plus du bouton « envoyer », les deux autres boutons à droite dans la barre de saisie ont respectivement pour fonction :
- d'effacer le contenu de la zone d'affichage,
- d'activer le mode traceur série.
On l'a déjà vu à plusieurs reprises, l'environnement Tinkercad ne simule pas bien certains aspects du fonctionnement de l'application Arduino IDE.
En particulier, la fenêtre du moniteur série n'interprète pas les octets au format UTF‑8 (cf. chap. C3‑IX ), mais en ASCII (cf. chap. C3‑VIII ). En conséquence, les caractères accentués ne sont pas correctement affichés : on est confronté à un problème classique de transcodage comme celui abordé dans l'exercice nº 4 de la feuille C3 ).
Administration d'une liaison série
Initialisation
Même si les objets comme Serial sont déjà déclarés par défaut, toute liaison série doit impérativement être initialisée avant d'être opérationnelle.
Dans le cas de la liaison série usuelle avec le poste de travail, l'initialisation est effectuée par une instruction d'appel de la méthode begin de la forme :
Serial.begin(vitesse); A
dont l'argument vitesse est une expression à valeur dans le type unsigned long qui spécifie la vitesse de transmission en baud.
En fait, la méthode begin possède une syntaxe générale d'appel plus détaillée. Elle admet un deuxième argument nommé config qui spécifie par une pseudo‑constantes de la forme SERIAL_xLy les options possibles du protocole UART, où :
- x est un nombre allant de
5à8qui code nombre de bits de données du mot transmis (code8par défaut) ; - L est une lettre qui code le contrôle de parité W mis en œuvre, avec
Npour no parity (code par défaut),Epour even parity (parité paire) ou0pour odd parity (parité impaire) ; - y est un nombre à un chiffre valant
1ou2pour coder nombre de bits de stop marquant la fin d'émission (code1par défaut).
La pseudo‑constante par défaut est donc SERIAL_8N1 (transmission par mots de 8 bits sans contrôle de parité et avec un seul bit de stop).
Pour se défaire de quelques idées reçues, certains aspects de l'exécution de l'appel de la méthode begin nécessitent des explications.
- Elle procède simplement au paramétrage du protocole UART.
- Elle n'engendre aucun signal logique sur les broches TX et RX associées à la liaison série.
- Elle opère même en l'absence de medium de transmission (câble USB ou autre) raccordé à la carte.
- Elle peut, sur certaines cartes (notamment celles à module ESP8266 et ESP32) être parasitée par le téléversement ou la réinitialisation du programme, avec :
- l'affichage d'une chaîne de caractères inattendus dits garbage (déchets) ou gibberish (charabia), typiquement :
⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮
print ou println ;
void setup()
{
delay(500);
Serial.begin(115200);
Serial.println();
…
Terminaison
Pour coder la terminaison de la liaison série, il suffit d'appeler la méthode end, sans argument, dans une instruction de la forme :
Serial.end(); A
Ensuite, pour réactiver la liaison série, il suffit d'appeler à nouveau la méthode begin.
La terminaison de la liaison série (si elle n'est plus d'utilité) permet notamment d'employer autrement les broches nº 0 et 1 du port numérique auxquelles la liaison est associée, sans risque de voir les niveaux logiques sur ces broches perturbés par une liaison maintenue sans motif.
La première instruction dans le code de définition de la méthode end G est un appel de la méthode flush afin de laisser le temps d'achever l'envoi des octets encore présents dans le buffer d'émission (cf. infra ). Il n'est donc pas nécessaire de coder cet appel avant celui de la méthode end dans le programme utilisateur.
Test
Le test if (Serial)… A est implémenté par surcharge de l'opérateur bool() pour déterminer si la liaison série USB est « ouverte ».
Attention ! Un tel test n'est valable que pour les très rares cartes Arduino ayant un port USB natif – c'est‑à‑dire directement relié au microcontrôleur principal de la carte – donc pas pour les modèles usuels de cartes (Uno, Mega, Nano, etc.).
En revanche, ce test peut être employé avec une carte Arduino Due. L'objet associé à son port natif étant identifié par SerialUSB, l'expression SerialUSB prend la valeur 1 (true) :
- si la carte est bien raccordée à un terminal via un câble USB,
- et si la liaison série est opérationnelle, par exemple si le numéro de port est bien sélectionné avec le logiciel Arduino IDE et si le moniteur série est activé.
Sinon, l'expression SerialUSB prend la valeur 0 (false). L'intérêt du test de donc pouvoir faire ce diagnostic.
Opérations de sortie par liaison série
Principe général d'une opération de sortie
Dès lors qu'une liaison série est initialisée par un programme sur une carte Arduino, effectuer une opération de sortie – c'est‑à‑dire l'émission de données depuis la carte – se déroule en 3 étapes : 1) écriture d'octets dans le buffer par appel d'une méthode d'écriture, 2) sérialisation et émission sur la broche de sortie (TX), 3) réception et traitement sur le système raccordé à la carte.
Plus en détails :
- Les données sont d'abord écrites octet par octet dans le buffer d'émission de l'objet
Serialassocié à la liaison, sachant que chaque octet écrit occupe un élément du buffer. - Tant que le buffer n'est pas vide, ces octets sont ensuite l'un après l'autre sérialisés, c'est‑à‑dire décomposés bit par bit pour générer signal logique par l'USART, cadencé par la vitesse de transmission réglée ;
- Enfin, la réception (mémorisation) et le traitement (affichage ou autre) des données émises sont pris en charge par le système avec lequel la carte communique, donc au rythme de ce système, et indépendamment de l'exécution du programme sur la carte.
- l'occupation d'un élément du buffer d'émission s'effectue simplement par incrémentation unitaire de l'indice de tête du buffer ;
- la libération d'un élément du buffer d'émission s'effectue par incrémentation unitaire de l'indice de queue du buffer ;
Si l'indice de tête « rattrape » l'indice de queue (moins une unité), le buffer est plein.
Quant à l'USART, il opère en arrière‑plan de l'exécution du programme sur le microcontrôleur :
- Sur déclenchement d'une interruption, l'octet de queue du buffer d'émission est d'abord copié dans le registre d'émission, ce qui libère un élément du buffer d'émission.
- L'USART sérialise les bits de l'octet stocké dans le registre d'émission et génère un signal logique conforme au protocole UART (c'est‑à‑dire avec les bits de start et de stop ajoutés à chaque octet) sur la broche TX associée à la liaison série.
- Lorsque la génération du signal logique est achevée, l'USART déclenche une interruption pour signifier que son registre d'émission est prêt pour la sérialisation d'un nouvel octet.
Ce processus est mis en œuvre même en l'absence de medium de transmission branché à la carte (câble USB ou autre).
Lorsque le buffer d'émission est vide, tout octet à émettre est directement copié dans le registre d'émission de l'USART, ce qui augmente significativement la vitesse maximale de transmission possible.
Modes d'écriture
Pour coder une émission de données par liaison série, le codeur dispose principalement de deux modes d'écriture :
- le mode par octets, via la méthode
writequi est considérée comme étant de bas niveau ; - le mode par caractères, via les méthodes
printetprintlnqui sont considérée comme étant de haut niveau au sens où elles font appel à la méthode de bas niveauwrite. (On rappelle qu'au format UTF‑8, un caractère est potentiellement encodé sur plusieurs octets.)
Les méthodes (ou fonctions) d'écriture sont héritées de la classe Print, et sont définies dans le fichier Print.cpp.
Les méthodes d'écriture font elles‑mêmes appel à des méthodes de très bas niveau définies dans le fichier HardwareSerial.cpp, en particulier :
- la méthode
writeG (synonyme) pour écrire un nouvel octet en tête du buffer d'émission (cet octet étant le dernier entré parmi de tous ceux déjà présents dans le buffer, il sera donc le dernier à sortir) ; - la méthode
_tx_udr_empty_irqG pour copier l'octet en queue de buffer d'émission dans le registre d'émission (cet octet étant alors le premier entré parmi de tous ceux présents dans le buffer, il est donc le premier à sortir).
Même si ce sont des méthodes publiques, elles n'ont pas vocation à être appelée dans un programme codé sur une carte Arduino.
La méthode write de très bas niveau définie dans le fichier HardwareSerial.cpp est à ne pas confondre avec celle ayant le même identificateur mais définie dans le fichier Print.cpp.
Écriture par octets
La méthode write définie dans le fichier Print.cpp G est une méthode d'écriture de bas niveau qui procède par octets. Un appel de la forme :
Serial.write(expression) A
écrit octet par octet la valeur de l'expression en tête du buffer d'émission de l'objet Serial.
- La méthode
writeretourne le nombre d'octets écrits, encodé dans le typesize_t. Mais cette valeur est rarement exploitée ; usuellement, l'appel est codé comme une simple instruction. - Dans le moniteur série, les octets reçus sont interprétés au format UTF‑8 et affichés comme des caractères, ce qui peut prêter à confusion sur le mode d'écriture de la méthode
write, qui procède bien par octets.
L'expression passée en argument de la méthode write peut être de type :
- entier, et en particulier codée par une valeur de caractère saisie entre guillemets simples
''; - chaîne de caractère, et en particulier codée par suite de caractères simples ou étendus encadrée par des guillemets doubles
"".
- Si l'argument expression est de type entier :
- L'instruction
Serial.write(65);écrit l'octet de valeur numérique65(ou0x41) dans le buffer d'émission. Et cet octet apparaît dans la zone d'affichage du moniteur série :
A
car65est le code UTF‑8 de la lettre « A » (cf. chap. C3‑VIII ). - L'instruction
Serial.write(65 + 256);donne exactement le même résultat parce qu'un octet encode seulement 256 valeurs entières, donc le rebouclage opère modulo 256. - L'instruction
Serial.write('A');donne aussi le même résultat parce que la valeur de caractère'A'est évaluée par le compilateur comme son code UTF‑8, à savoir l'entier65. - L'instruction
Serial.write(128);écrit l'octet de valeur numérique128(ou0x80) dans le buffer d'émission. Toutefois, cet octet apparaît dans la zone d'affichage du moniteur série comme le symbole générique :
⍰
car en UTF‑8, le code128n'est pas un code de caractère affichable. - L'instruction
Serial.write('é');écrit l'octet de valeur numérique169(ou0xA9) dans le buffer d'émission car c'est la valeur de l'octet de poids faible du code UTF‑8 du caractère « é ». Et cet octet apparaît dans la zone d'affichage du moniteur série comme le symbole générique :
⍰
car en UTF‑8, le code1169n'est pas un code de caractère affichable. - Si l'argument expression est de type chaîne de caractères :
- L'instruction
Serial.write("C++");écrit les trois octets de codes UTF‑8 respectifs67('C'),43('+') et43('+') dans le buffer d'émission. Après réception dans le moniteur série, ils sont interprétés comme tels et apparaissent dans la zone d'affichage pour former le mot :
C++ - L'instruction
Serial.write("é");écrit le code UTF‑80xC3A9, soit les deux octets de valeur décimale169(0xA9) et195(0xC3) dans le buffer d'émission. Après réception dans le moniteur série, ils sont interprétés comme tels et apparaissent dans la zone d'affichage comme le caractère affichable attendu :
é
unsigned char, avec rebouclage cyclique éventuel (cf. chap. C3‑VI . S'il s'agit d'une valeur de caractère saisie entre guillemets simples seul l'octet de poids faible de son code UTF‑8 est retenu. Dans tous les cas, un seul octet est écrit en tête du buffer d'émission.
Dans tous les exemples ci‑dessous, on fait l'hypothèse que le programme s'exécute sur une vraie carte Arduino reliée à un poste de travail où le moniteur série est activé. La liaison série est supposée initialisée correctement. Typiquement, le squelette de programme de test donné ci‑dessous suffit :
void setup()
{
delay(500);
Serial.begin(115200);
Serial.println();
// type your statement here
}
void loop() {}
Les instructions à tester sont à coder en ligne nº 6. Ainsi :
Comme supra , on fait l'hypothèse que le programme s'exécute sur une vraie carte Arduino reliée à un poste de travail où le moniteur série est activé, la liaison série étant initialisée. Et on utilise le même squelette de code.
Serial.write('é'); (cf. supra ).
Serial.write("é"); on obtient l'affichage de la chaîne de caractères é dans la fenêtre de simulation du moniteur série. Ces deux glyphes correspondent respectivement aux valeurs de caractères des codes ASCII étendus 0xC3 et 0xA9 de la page de code CP1252 (cf. chap. C3‑VIII ), sachant que 0xC3A9 est le code UTF‑8 du caractère « é » (cf. l'exercice C3‑4 ).
Limitation du nombre d'octets émis
Dans le cas où l'expression est une chaîne de caractères, on peut limiter le nombre n d'octets effectivement écrits en tête du buffer d'émission de l'objet Serial par un argument optionnel de type size_t dans l'appel de la méthode.
Il suffit pour cela d'employer la syntaxe d'appel :
Serial.write(expression [, n])
L'instruction Serial.write("Bonjour", 3); affiche la chaîne tronquée Bon (3 premiers caractères) sur le moniteur série.
Écriture par caractères
Principe de l'écriture par caractères
Les méthodes print et println sont des fonctions similaires à write, mais elles opèrent à plus haut niveau : elles acceptent également comme argument principal une expression à valeurs numériques entières ou décimales, et non plus seulement un octet ou une chaîne d'octets.
Le principe de traitement est le suivant :
- Une fois que l'expression est évaluée, la valeur numérique obtenue est éventuellement convertie dans une autre base de numération, ou encore tronquée, si un argument optionnel de format est codé dans l'appel de la méthode.
- Cette valeur numérique n'est pas interprétée dans son format usuel d'encodage mais comme une chaîne de caractères numériques codés en ASCII. Cette interprétation est mise en œuvre par la méthode privée
printNumberdéfinie dans le fichier d'implémentationPrint.cppG. - Les octets constituant les codes ASCII de chaque chiffre ou symbole de la chaîne de caractères sont ensuite écrits l'un après l'autre dans le buffer d'émission par appel de la méthode de bas niveau
write(dernière instruction de la méthodeprintNumber).
print. Ensuite, le procédé suit le même cours qu'avec la méthode write : les octets contenus dans le buffer d'émission sont copiés l'un après l'autre dans le registre d'émission pour y être sérialisés. S'ils sont émis par l'USART à destination du moniteur série, ils seront interprétés – et donc affichés – comme des caractères au format UTF‑8.
Pour bien comprendre la différence avec la méthode write, comparons l'affichage obtenu pour le même argument qu'au 1er exemple supra (pour mémoire, l'instruction Serial.write(65); affiche A sur le moniteur série car 65 est le code UTF‑8 du caractère « A »).
Reprenons les hypothèses et le squelette de programme de test précédent .
- Lors du traitement de l'instruction
Serial.print(65);l'argument65est interprété non pas comme le code UTF‑8 d'un caractère mais comme la chaîne de caractères numérique formant le nombre entier 65, c'est‑à‑dire les caractères chiffres « 6 » et « 5 » pris successivement dans cet ordre. Et chacun de ces caractères est alors copié dans le buffer d'émission par appel de la méthode de bas niveauwrite. - En revanche, l'instruction
Serial.print('A');produit le même effet queSerial.write('A');à savoir l'émission du code UTF‑8 du caractère « A » passé en argument, qui est ensuite bien affichéAsur le moniteur série.
65
et non pas le caractère « A » comme avec l'instruction
Serial.write(65);. print est conçue comme une méthode de haut niveau capable de prendre en charge l'affichage de données de types les plus variés soient‑ils. Syntaxes d'appel
Les méthodes print et println obéissent à une syntaxe d'appel plus variée que write. Elle dépend de l'expression passée en premier argument, qui peut être de type :
- entier,
- décimal,
- caractère ou chaîne de caractère,
sachant que dans tous les cas, l'expression peut en particulier être codée par une constante littérale avec la syntaxe appropriée (cf. les chapitres précédents et spécifiquement le chapitre C5‑VI pour les chaînes de caractères).
-
Si l'expression est à valeur entière, signée ou non, alors la syntaxe d'appel est :
Serial.print(expression [, base])A
où l'argument optionnel base spécifie la base de numération de la valeur formatée à écrire en tête du buffer d'émission de l'objetSerial.Ce formatage est appliqué quel que soit le préfixe (
0x,0b…) éventuellement spécifié dans l'expression passée comme premier argument.L'argument optionnel base peut être codé :
- par n'importe quelle expression prenant la valeur
2,8,10ou16; - où, pour une meilleure lisibilité, par l'une des pseudo‑constantes
BIN,OCT,DECouHEXdéfinies dans le fichier d'en‑têtePrint.h;
sachant que c'est par défaut la base 10 de formatage qui est adoptée.
Pour tester la méthode
printappliquées à des valeurs entières, comme précédemment, on fait l'hypothèse que le programme s'exécute sur une carte Arduino reliée à un poste de travail où le moniteur série est activé, la liaison série étant initialisée (cf. supra pour le squelette du programme de test qu'on peut utiliser).- L'instruction
Serial.print(6, BIN);affiche la chaîne de caractères110. - convertit la valeur
6en base 2, aboutissant au nombre binaire110, - convertit ce nombre binaire en la chaîne de trois caractères «
1», «1» et «0», - convertit ces caractères en leurs codes ASCII
0x31,0x31et0x30, - écrit ces octets de code dans le buffer d'émission.
- L'instruction
Serial.print(0b1111, HEX);affiche la chaîne de caractèresF. - convertit la valeur binaire
1111en base 16, aboutissant au nombre hexadécimal à un seul digitF(c'est‑à‑dire15en base 10) ; - convertit ce nombre en la chaîne de caractères équivalente à
"F"; - convertit ce caractère en son code ASCII
0x46, - écrit cet octet de code dans le buffer d'émission.
Plus précisément, cette instruction :Copiés l'un après l'autre dans le registre d'émission, puis sérialisés par l'USART, ces octets sont interprétés par le moniteur série comme des codes UTF‑8 (identiques aux codes ASCII restreints). Les caractère qui s'affichent sont donc les digits binaires de la constante littérale6passée en argument.Plus précisément, cette instruction :Comme pour l'exemple précédent, cet octet est ensuite interprété comme un code UTF‑8 et apparaît donc commeFdans la zone d'affichage du moniteur série. - par n'importe quelle expression prenant la valeur
-
Si expression est à valeur décimale (c'est‑à‑dire codée dans un type flottant) alors la syntaxe d'appel est :
Serial.print(expression [, digit])A
où l'argument optionnel digit spécifie le nombre de décimales près auquel arrondir la valeur formatée écrite en tête du buffer d'émission de l'objetSerial.L'argument optionnel digit peut être codé par n'importe quelle expression à valeur entière positive ou nulle.
En principe, toutes les décimales spécifiées supplémentaires à celles encodées dans le type flottant de l'expression passée en premier argument sont formatées
0.Pour tester la méthode
printappliquée à des valeurs décimales, comme précédemment, on fait l'hypothèse que le programme s'exécute sur une carte Arduino reliée à un poste de travail où le moniteur série est activé, la liaison série étant initialisée (cf. supra pour le squelette du programme de test qu'on peut utiliser).- L'instruction
Serial.print(12.345678, 3);affiche la chaîne de caractères12.346. - arrondit à
12.346la valeur formatée (la 3e décimale est passée à la valeur entière supérieure parce que la décimale suivante6est supérieure où égale à 5) ; - convertit cette valeur numérique en la chaîne de caractères équivalente à
"12.346"; - convertit chaque caractère de cette chaîne en son code ASCII ;
- écrit ces octets de code dans le buffer d'émission.
- L'instruction
Serial.print(PI, 25)affiche :
3.1415927410125732421875000
sur le moniteur série (pour mémoire, le nombre π vaut 3,14159265 à 10−8 près). - L'instruction
Serial.print(123456789.012345, 2)affiche :
123456792.00
sur le moniteur série.
Plus précisément, cette instruction :Interprétés comme des codes UTF‑8, ils formeront la chaîne de caractères décimaux souhaitée dans la zone d'affichage du moniteur série.Le fait de spécifier 25 décimales n'apporte pas de précision supplémentaire au nombre affiché, qui reste tributaire de la résolution du typedoubledans lequel la pseudo‑constantePI(définie pourtant avec 31 décimales dans le fichierArduino.hG) est implicitement convertie, sachant que sur une carte Uno, le typedoubleest implémenté avec la même précision que le typefloat(cf. chap. C3‑V ).Là encore, le nombre affiché diffère de celui spécifié à cause de sa conversion implicite dans le typedoublequi n'est implémenté qu'en simple précision. - L'instruction
-
Si l'expression prend une valeur de caractère ou de chaîne de caractères, alors la syntaxe d'appel est simplement :
Serial.print(expression)A
avec des résultats identiques à ceux obtenus par appel de la méthodewrite(cf. supra ).Pour tester la méthode
printappliquées à des caractères ou des chaînes de caractères, comme précédemment, on fait l'hypothèse que le programme s'exécute sur une carte Arduino reliée à un poste de travail où le moniteur série est activé, la liaison série étant initialisée (cf. supra pour le squelette du programme de test qu'on peut utiliser).Dans la zone d'affichage du moniteur série :
- L'instruction
Serial.print('e')donneemaisSerial.print('é')donne⍰. On est confronté au même problème qu'avec la méthodewrite(cf. supra ), tout simplement parce que les caractères encodés en multi‑octets dans le format UTF‑8 ne sont pas pris en charge dans les expressions de valeurs de caractères – cf. chap. C3‑IX . - En revanche, l'instruction
Serial.print("Hé")restitue bien la chaîne de caractèreHé, encodée par défaut en UFT‑8.
- L'instruction
Spécificité de la méthode println
La méthode println A obéit à la même syntaxe que print.
Sa spécificité est qu'elle écrit en plus un saut de ligne après le dernier octet encodant l'expression passée en argument principal.
Émis par la carte Arduino – et donc indépendamment du système d'exploitation du poste de travail où s'exécute le moniteur série – ce saut de ligne est toujours constitué de deux caractères spéciaux :
- un caractère retour chariot
'\r'(carriage return), code ASCII ou UTF‑80x0D; - un caractère nouvelle ligne
'\n'(new line ou line feed), code ASCII ou UTF‑80x0A.
Les sauts de ligne émis à destination du moniteur série sont toujours effectifs dans la zone d'affichage du moniteur série quel que soit le paramétrage choisi dans le menu déroulant situé bas de la fenêtre – cf. supra . En effet, ce dernier ne s'applique à qu'à la barre de saisie et non pas à la zone d'affichage.
Gestion du buffer d'émission
On vient de voir que les méthodes write ou print(ln) se résument in fine à écrire des octets en tête de buffer d'émission.
Quant au transfert de ces octets dans le registre d'émission puis leur sérialisation en signal logique sur la broche TX, cela est effectué en arrière-plan de l'exécution du programme.
Mais, tant qu'ils ne sont pas émis, les octets restent stockés dans le buffer d'émission. Donc si trop de d'opérations de sortie sont codées, le buffer se remplit plus vite qu'il ne se vide et on s'achemine vers une saturation au sens où le buffer est plein.
En cas de saturation du buffer d'émission, tout nouvel appel d'une méthode de sortie ne provoque pas d'écrasement d'octets.
En revanche, l'exécution de ce nouvel appel comprend une attente de libération d'éléments dans le buffer d'émission jusqu'à ce que tous les octets de l'expression passée en argument y soient écrits.
La préservation de l'intégrité des données se paye donc par une potentielle baisse de réactivité du programme. Pour prévenir ce phénomène, le module de bibliothèque HardwareSerial fournit :
-
flushA, une méthode pour attendre que le buffer soit vide ; -
availableForWriteA, une méthode pour connaître le nombre d'éléments disponibles dans le buffer.
La méthode flush
La méthode flush est une fonction sans argument et qui ne retourne aucune valeur. Elle suspend l'exécution du programme jusqu'à ce que tous les octets stockés dans le buffer d'émission aient été envoyés, avant de passer à l'exécution de l'instruction suivante.
Ainsi, le codage d'une instruction :
Serial.flush();
garantit, immédiatement après, la disponibilité maximale du buffer d'émission.
En contre‑partie, l'appel de la méthode flush demande un temps d'exécution d'autant plus long que le buffer est rempli. L'idéal est de la programmer durant une phase d'exécution qui n'est pas soumises à des exigences de vitesse trop sévères.
On peut coder également coder l'appel de flush dans une instruction conditionnelle, en testant la valeur du nombre d'éléments disponibles en écriture à l'aide de la méthode availableForWrite (cf. ci‑après).
La méthode availableForWrite
La méthode availableForWrite est une fonction sans argument qui retourne dans le type int le nombre d'éléments vacants dans le buffer d'émission .
Sachant le taille du buffer de réception associé à l'objet Serial (cf. supra ), cette méthode permet aussi de déterminer par complément le nombre d'éléments en attente d'envoi. En effet, il est déterminé par la valeur de l'expression :
SERIAL_TX_BUFFER_SIZE - 1 - Serial.availableForWrite()
Grâce à la connaissance du nombre d'éléments vacants dans le buffer d'émission, en cas d'impératifs de rapidité, on peut alors conditionner l'appel d'une méthode de sortie, sachant par ailleurs le nombre d'octets à émettre que cette sortie suppose.
Pour envoyer un message non urgent dont on connaît la taille, on peut coder :
if (Serial.availableForWrite() >= 17) { // message not urgent
Serial.println("Wait, please...");
}
Remarque. La chaîne de caractères à envoyer compte seulement 15 octets, mais on a codé le test avec la valeur de comparaison 17.
En effet, avant de programmer une sortie série par la méthode println, il faut aussi tenir compte des deux caractères spéciaux de saut de ligne (et pas seulement les caractères de la chaîne ou de la valeur numérique à émettre) pour vérifier s'il y a suffisamment d'éléments vacants dans le buffer d'émission.
Gestion de la zone d'affichage du moniteur série
Le moniteur série de l'application Arduino IDE n'a pas toutes les fonctionnalités d'un véritable terminal série.
Il est juste destiné à la mise au point des programmes (debugging).
À l'heure actuelle (cf. la date de version en haut de cette page web), dans les bibliothèques Arduino, il n'existe pas de méthode pour effacer le contenu de la zone d'affichage du moniteur série.
Il est également impossible d'opérer un retour en arrière dans une ligne pour en modifier l'affichage par écrasement.
En effet, lorsqu'ils sont émis via un appel de write ou de toute autre méthode d'écriture, les caractères de contrôle :
- backspace (code UTF‑8 ou ASCII
0x08), - ou delete (code UTF‑8 ou ASCII
0xF7),
ne sont pas exécutés par le moniteur série. Ils sont simplement représentés dans la zone d'affichage par le symbole générique □ comme tous les caractères de contrôle.
Le seul pis‑aller consiste à cliquer sur le bouton « Effacer la sortie » en bas à droite de la fenêtre du moniteur série pour effacer « manuellement » le contenu de la zone d'affichage. Mais une telle action ne peut pas être codée dans le programme embarqué sur une carte Arduino.
Opérations d'entrée par liaison série
Principe général d'une opération d'entrée
Dès lors qu'une liaison série est initialisée par un programme sur une carte Arduino, une opération de lecture via cette liaison série, c'est‑à‑dire une réception de données sur la carte, se déroule en 3 étapes : 1) emission par le système raccordé à la carte, 2) réception sur la broche d'entrée RX et désérialisation dans le buffer de réception de la carte, 3) lecture proprement dite d'octets par appel d'une méthode dans le programme utilisateur.
Plus en détails :
- L'émission des données est effectuée par le système auquel la carte est raccordée via la liaison série, sous la forme d'un signal logique conforme au protocole UART sur la broche RX associée à la liaison série.
- Détecté par l'USART en arrière-plan de l'exécution du programme, ce signal logique est désérialisé octet par octet. Chaque octet est stocké en tête du buffer de réception, et occupe donc un élément de plus dans ce buffer.
- C'est seulement alors que peut opérer un appel d'une méthode de lecture codé dans le programme utilisateur. Une telle méthode retourne une valeur formée à partir des octets stockés dans le buffer de réception et, éventuellement (cf. infra ), cela libère un élément du buffer.
Plus précisément, tout octet issu de la désérialisation d'un signal logique reçu sur la broche RX de la carte est d'abord stocké dans le registre de réception. Chaque nouvel octet déclenche une interruption du programme, qui appelle la méthode _rx_complete_irq définie dans le fichier HardWareSerial_private.h.
Cette méthode _rx_complete_irq vérifie que le buffer de réception de l'objet Serial n'est pas plein :
- Si tel est le cas, l'octet du registre de réception est alors écrit en tête de buffer, et donc occupe cet élément – l'occupation étant implémentée par l'incrémentation unitaire de l'indice de tête du buffer.
- Sinon, l'octet est perdu (il sera écrasé par le prochain octet désérialisé issu d'un nouveau signal logique reçu alors que le buffer de réception dispose enfin d'éléments vacants).
Le processus d'écriture d'octets dans le buffer de réception se répète tant que l'USART détecte un nouveau signal logique sur la broche RX. Comme pour celui d'émission, le buffer de réception n'est plein que quand l'indice de tête n'est plus qu'à une unité de l'indice de queue (cf. supra ).
Méthodes de lecture
Le framework Arduino met à disposition du codeur une dizaine de méthodes de lecture associées à l'objet Serial. On peut les classer en deux catégories :
- les méthodes de bas niveau
available,peeketread, définies dans le fichierhardwareSerial.cpp; - les méthodes de haut niveau, notamment
readString,parseIntouparseFloat, définies dans le fichierStream.cpp.
Seules les méthodes citées ci‑dessus seront détaillées ci‑après. Pour les autres, on se reportera au lien suivant A.
Temporisation des méthodes de haut niveau
Contrairement à celles de bas niveau, dont la valeur de retour est « immédiate », les méthodes de lecture de haut niveau sont basées sur des variables et des méthodes protégées (c'est‑à‑dire non appelables par le codeur dans son programme utilisateur).
En particulier, elles emploient la méthode protégée de lecture unitaire timedRead qui opère de façon temporisée avec réitération de la lecture jusqu'à expiration d'un délai d'abandon (timeout).
La durée de ce délai est fixée par un champ de la classe Stream : la variable de type unsigned long nommée _timeOut, qui exprime en millisecondes une durée maximale d'attente à ne pas dépasser W.
Cette variable _timeOut prend la valeur 1000 par défaut, ce qui correspond donc à une durée d'une seconde). Protégée, cette variable n'est consultable et modifiable dans le programme utilisateur que via des méthodes spécifiques (cf. infra ).
Gestion des fins de ligne dans la barre de saisie
Lorsque la barre de saisie du moniteur série est active (état signalé par le curseur clignotant dans la barre), l'appui sur la touche ENTRÉE ↵ du clavier est traité de différentes manières selon l'option choisie dans le menu déroulant en bas de la fenêtre (cf. la capture d'écran ci‑contre).
Cette action ajoute dans le buffer de réception, en plus des caractères saisis, zéro, un ou deux des caractères de contrôle ci‑dessous :
- le caractère retour chariot
'\r'(carriage return CR), dont le code ASCII ou UTF‑8 est0x0D; - le caractère nouvelle ligne
'\n'(new line NL ou line feed LF), dont le code ASCII ou UTF‑8 est0x0A.
Le choix de cette option dépend des traitements que l'on souhaite effectuer dans le programme utilisateur.
Lecture unitaire d'octets
Les méthodes read et peek sont deux méthodes de lecture de bas niveau qui ne lisent qu'un un seul octet par appel : celui situé en queue du buffer de réception de l'objet Serial.
La méthode read
L'expression d'appel :
Serial.read()
A
retourne immédiatement une valeur entière de type int qui peut être :
-
-1si le buffer de réception de l'objetSerialest vide ; - sinon égale à l'octet lu en queue du buffer réception, ce qui libère cet élément du buffer.
La libération d'un élément du buffer de réception consiste simplement en l'incrémentation unitaire de son indice de queue.
Des appels successifs de la méthode read permettent de lire l'un après l'autre tous les octets stockés dans le buffer de réception.
Pour tester la méthode read, comme précédemment, on fait l'hypothèse que le programme s'exécute sur une carte Arduino reliée à un poste de travail où le moniteur série est activé, la liaison série étant initialisée. Un squelette de programme pour tester les exemples d'instructions d'appel des méthodes de lecture données ci‑dessous est le suivant :
void setup()
{
delay(500);
Serial.begin(115200);
Serial.println();
Serial.flush();
}
void loop()
{
if (Serial.available()) {
byte readByte = /* type your statement here */;
Serial.print(readByte);
}
Serial.println();
}
De plus, on suppose qu'avant la saisie, le buffer de réception est vide et que l'option pas de fin de ligne est choisie dans les options de saisie du moniteur série.
- Après saisie et envoi de la lettre
A, l'évaluation de l'expression d'appelSerial.read()retourne la valeur entière65qui est le code UTF‑8 du caractère « A ». - Après saisie et envoi du nombre
65, l'évaluation de l'expression d'appelSerial.read()retourne la valeur entière54qui est le code UTF‑8 du caractère « 6 ». - Après saisie et envoi de la lettre
é, l'évaluation de l'expression d'appelSerial.read()retourne la valeur entière195(c'est‑à‑dire0xC3)qui est l'octet de poids fort du code UTF‑8 du caractère « é ».
53 qui est le code UTF‑8 du caractère « 5 ». Un nouvelle appel Serial.read() retourne cette valeur (ce que fait le programme de test ci‑dessus, puisque les appels de la méthode read sont codés dans la fonction loop qui s'exécute en boucle). 169 (c'est‑à‑dire 0xA9) qui est l'octet de poids faible du code UTF‑8 du caractère « é ». Un nouvel appel de Serial.read() retourne cette valeur (ce que fait le programme de test ci‑dessus, puisque les appels de la méthode read sont codés dans la fonction loop qui s'exécute en boucle). La méthode peek
L'expression d'appel :
Serial.peek()
A
retourne la même valeur (de type int) que la méthode read, mais sans libérer l'élément correspondant dans le buffer de réception.
Elle permet donc de connaître la valeur de l'octet en queue du buffer de réception tout en le laissant « intact ».
Pour tester la méthode peek, on fait les mêmes hypothèses que pour les exemples donnés supra d'application de la méthode read. On peut utiliser le même squelette de test, en ajoutant l'instruction suivante à la fin de la fonction loop (on va voir pourquoi) :
delay(1000);
Après saisie et envoi de la lettre A, l'évaluation de l'expression d'appel Serial.peek() retourne la valeur entière 65 qui est le code UTF‑8 du caractère « A ».
Et suite à cet appel, le buffer de réception est inchangé. Ainsi, un nouvel appel de Serial.peek() retourne la même valeur. Or justement, le programme de test effectue ces appels en boucle. C'est pourquoi on a codé une pause d'une seconde à la fin de la fonction loop pour limiter les affichages successifs dans le moniteur série.
La méthode peek ne permet donc pas de scanner tout le contenu du buffer de réception, seulement son octet de queue (des appels successifs de cette méthode rendent toujours la même valeur tant qu'une méthode de lecture comme read n'a pas été appelée).
Lecture de chaînes de caractères
Sans argument, l'expression d'appel :
Serial.readString()
A
procède par lecture unitaire temporisée de tous les octets du buffer de réception de l'objet Serial.
Elle retourne une valeur de chaîne de caractère de classe String qui peut être :
- la chaîne vide (réduite au caractère de fin de chaîne NUL) si le nombre d'octets lus est nul depuis le début de l'évaluation de l'appel jusqu'à expiration du délai d'abandon
_timeOut; - sinon la chaîne formée de tous les octets lus dans l'ordre, quelle que soit la valeur de ces octets.
Quel que soit le résultat, l'appel de la méthode readString laisse le buffer de réception complètement vide.
La chaîne de caractères retournée par la méthode readString comporte à la suite des octets lus un octet supplémentaire de valeur nulle, qui constitue pour toutes les valeurs de la classe String le caractère de fin de chaîne dit NUL (de code ASCII 0x0).
Pour tester la méthode readString, comme précédemment, on fait l'hypothèse que le programme s'exécute sur une carte Arduino reliée à un poste de travail où le moniteur série est activé, la liaison série étant initialisée. De plus, on suppose qu'avant la saisie, le buffer de réception est vide. Et on peut utiliser le même squelette de programme de test que supra .
- Après saisie et envoi des caractères
Hé !, l'évaluation de l'expression d'appelSerial.readString()retourne : - si l'option « pas de fin de ligne » est choisie, la chaîne de caractères constituée des 6 octets hexadécimaux :
48 C3 A9 20 21 0
qui concatène les codes UTF‑8 respectifs des caractères'H'(0x48),'é'(0xC3A9),' '(0x20),'!'(0x21) et du caractère NUL (0x0). - si l'option « Les deux, NL et CR » est choisie, la chaîne de caractères constituée des 8 octets hexadécimaux :
48 C3 A9 20 21 0D 0A 0
où l'on retrouve les codes des caractères de contrôle CR (0x0D) et NL (0x0A) juste avant le caractère NUL de fin de chaîne. - Si l'on saisit et envoie d'abord le caractère
C, puis les caractères++, l'évaluation de l'expressionSerial.readString()retourne : - si l'option « pas de fin de ligne » est choisie, la chaîne de caractères constituée des 4 octets hexadécimaux :
43 2B 2B 0
qui concatène les codes UTF‑8 respectifs des caractères'C'(0x43),'+'(0x2B),'+'(0x2B) et NUL (0x0). - si l'option « Les deux, NL et CR » est choisie, la chaîne de caractères constituée des 8 octets hexadécimaux :
43 0D 0A 2B 2B 0D 0A 0
où l'on retrouve à deux reprises les codes des caractères de contrôle CR (0x0D) et NL (0x0A), puisqu'on a procédé en deux envois. - Si l'on saisit et envoie les caractères
\nformant la séquence d'échappement usuelle pour un saut de ligne, et que l'on a choisi l'option « pas de fin de ligne », l'évaluation de l'expressionSerial.readString()retourne la chaîne de caractères constituée des 3 octets hexadécimaux :
5C 6E 0
qui concatène les codes UTF‑8 respectifs des caractères'\'(0x5C),'n'(0x6E) et NUL (0x0).
On comprend à la lumière de ce dernier exemple qu'il n'est pas possible dans la barre de saisie de coder des séquences d'échappement comme dans une valeur de chaîne de caractères entre guillemets doubles "" dans un code source (cf. chap. C3‑VIII .
En effet, tout caractère dans la barre de saisie – y compris l'antislash '\' – est interprété individuellement par son code UTF‑8.
Lecture de nombres formatés
Pour lire une valeur numérique, la méthode readString n'est pas adaptée. En effet, dans la chaîne d'octets stockées dans le buffer de réception, il faut non seulement délimiter ceux qui codent la valeur, mais aussi distinguer les éléments de sa syntaxe de codage : les chiffres, mais aussi éventuellement un signe, un point décimal…
Le fichier Stream.cpp du framework Arduino définit donc deux méthodes spécifiques de haut niveau pour accomplir cette action :
Ces méthodes mettent en œuvre de façon sous-jacente une analyse lexicographique (d'où le mot anglais parse qui signifie « analyser ») des éléments contenus dans le buffer de réception pour y lire une valeur numérique codée conformément aux syntaxes de saisie usuelles des constantes littérales entières et décimales en langage C.
La méthode parseInt
La méthode parseInt A s'utilise usuellement par une expression d'appel de la forme réduite (c'est‑à‑dir,e sans argument) suivante :
Serial.parseInt()
Elle retourne une valeur entière dans le type signé long, qui peut être :
-
0en cas d'échec d'identification d'une valeur entière dans le buffer de réception, après expiration du délai d'abandon_timeOut; - sinon, la première valeur entière identifiée et dans ce cas, tous les octets lus sont libérés dans le buffer de réception.
La méthode parseInt admet deux arguments optionnels pour adapter aux besoins du programme l'algorithme d'analyse de la saisie de l'utilisateur. Ils se codent conformément à la syntaxe d'appel complète suivante :
Serial.parseInt([mode de lecture, caractère ignoré])
- Le mode de lecture (argument identifié
lookaheaddans le fichierStream.cpp) permet de spécifier des catégories de caractères dont toutes les occurrences sont à ignorer dans le buffer de réception. Cet argument optionnel peut être codé par l'une des trois constantes énumérées ci‑dessous : -
SKIP_ALL– les caractères autres que les chiffres (codes ASCII0x30à0x39) et le signe « - » (code ASCII0x2D) sont ignorés ; c'est le mode par défaut ; -
SKIP_NONE– aucun caractère n'est ignoré ; -
SKIP_WHITESPACE– les caractères d'espacement espace (0x20), tabulation horizontale (0x09), nouvelle ligne (0x0C) et retour chariot (0x0D) sont ignorés. - L'argument optionnel caractère ignoré permet d'indiquer un caractère spécifique dont toutes les occurrences sont à ignorer dans le buffer de réception. Sa valeur peut être spécifiée par son glyphe entre guillemets simples
''ou par son code ASCII.
À l'heure actuelle (cf. la date de version indiquée en haut de cette la page web), les arguments optionnels des méthodes parseInt et parseFloat ne sont pas codables dans l'environnement de simulation Tinkercad.
Pour tester la méthode parseInt, comme précédemment, on fait l'hypothèse que le programme s'exécute sur une carte Arduino reliée à un poste de travail où le moniteur série est activé, la liaison série étant initialisée. De plus, on suppose qu'avant la saisie, le buffer de réception est vide et que l'option pas de fin de ligne est choisie dans les options de saisie du moniteur série. Et on peut utiliser presque le même squelette de programme de test que supra , en remplaçant la ligne n° 12 par :
int readInt = /* type your statement here */;
- Après saisie et envoi des caractères
-123, l'évaluation de l'expression d'appelSerial.parseInt()retourne la valeur entière-123et vide le buffer de réception. - Après saisie et envoi des caractères
4 5(avec un espace initial), l'évaluation de l'expression d'appel : -
Serial.parseInt()retourne la valeur entière4et laisse dans le buffer de réception les octets codants les caractères' 'et'5'; en effet : - l'espace initial a été ignoré, conformément au mode de lecture par défaut
SKIP_ALL; - l'espace saisi après
'4'n'étant pas un chiffre, il est analysé comme un séparateur ; comme la chaîne des symboles déjà lus forme un nombre entier, le processus de lecture s'achève ; -
Serial.parseInt(SKIP_NONE)retourne la valeur entière0après expiration du délai d'abandon car l'espace initial n'est pas un caractère autorisé dans une valeur entière et bloque le processus de lecture. - Après saisie et envoi des caractères
12,345.6, l'évaluation de l'expression d'appel : -
Serial.parseInt()retourne la valeur entière12et laisse dans le buffer de réception la chaîne de caractères",345.6"; en effet : - la virgule saisie après
'2'n'étant pas un chiffre, elle est analysée comme un séparateur ; - comme la chaîne des symboles déjà lus forme un nombre entier, le processus de lecture s'achève ;
-
Serial.parseInt(SKIP_NONE, ',')retourne la valeur entière12345et laisse dans le buffer de réception la chaîne de caractères".6"; en effet : - la virgule saisie après
'2'est ignorée, conformément au deuxième argument optionnel spécifié dans l'appel ; - le point saisi après
'5'n'étant pas un chiffre, il est analysé comme un séparateur ; comme la chaîne des symboles déjà lus forme un nombre entier, le processus de lecture s'achève.
La méthode parseFloat
La méthode parseFloat A obéit à la même syntaxe d'appel que celle de parseInt exposée supra :
Serial.parseFloat()
mais :
- elle opère pour lire des chaînes de caractères conformes à la syntaxe des constantes littérales décimales où le symbole
.joue le rôle de séparateur décimal ; - elle n'opère pas la lecture des constantes littérales formatées avec des exposants ;
- sa valeur de retour est encodée dans le type
float, avec une éventuelle erreur d'encodage à partir du 7e chiffre significatif.
Comme parseInt (cf. supra ), la méthode parseFloat admet deux arguments optionnels – mode de lecture et caractère ignoré – pour adapter aux besoins du programme l'algorithme d'analyse de la saisie de l'utilisateur.
Pour tester la méthode parseFloat, comme précédemment, on fait l'hypothèse que le programme s'exécute sur une carte Arduino reliée à un poste de travail où le moniteur série est activé, la liaison série étant initialisée. De plus, on suppose qu'avant la saisie, le buffer de réception est vide et que l'option pas de fin de ligne est choisie dans les options de saisie du moniteur série. Et on peut utiliser le même squelette de programme de test que supra , en remplaçant la ligne n° 12 par :
float readFloat = /* type your statement here */;
- Après saisie et envoi des caractères
0.25, l'évaluation de l'expression d'appelSerial.parseFloat()retourne la valeur décimale0.2500000023…et vide le buffer de réception. - Après saisie et envoi des caractères
12,345.6, l'évaluation de l'expression d'appelSerial.parseFloat(SKIP_NONE, ',')retourne la valeur décimale12345.60058…et vide le buffer de réception.
Gestion du buffer de réception
Si le buffer de réception est vide :
- un appel de la méthode
readretourne la valeur-1et un appel de la méthodereadStringretourne une valeur de chaîne vide ; dans les deux cas, la valeur de retour permet de diagnostiquer sans ambiguïté la vacuité du buffer ; - en revanche, un appel d'une méthode comme
parseIntretourne la valeur0qui peut prêter à confusion ; cette valeur aurait aussi pu avoir été saisie.
Pour permettre un diagnostic fiable de l'état du buffer de réception, on dispose de la méthode available définie dans le fichier Stream.cpp.
L'évaluation de l'expression d'appel :
Serial.available() A
retourne la valeur entière dans le type int égale au nombre d'octets stockés – donc, en attente de lecture – dans le buffer de réception associé à l'objet Serial.
Donc, si un appel de la méthode available retourne la valeur 0, cela signifie que le buffer de réception est vide (autrement dit, il n'y a aucun octet à lire).
La méthode available permet de coder très facilement un algorithme pour vider le buffer de réception, comme ci‑dessous.
while (Serial.available() > 0) {
Serial.read(); // all bytes in _rx_buffer are lost
}
Dans cet algorithme de vidage, à la ligne nº 11, l'expression Serial.read() n'est pas composée comme r-value d'une affectation : la méthode s'exécute mais la valeur qu'elle retourne n'est pas exploitée (elle est donc perdue).
Par ailleurs, rappelons que l'on peut déterminer la contenance maximale du buffer de réception via l'expression :
SERIAL_RX_BUFFER_SIZE - 1
Paramétrage de la temporisation
On a vu supra que la durée du délai d'abandon des méthodes de lecture de haut niveau est fixée par la valeur de la variable _timeout, exprimée en millisecondes, sachant que
- il s'agit d'un champ de la classe
Streamdont hérite l'objetSerial; - ce champ est protégé, donc son identificateur ne peut être employé directement dans un programme Arduino.
Pour consulter ou modifier la valeur du champ _timeout, le fichier Stream.cpp de la bibliothèque Arduino définit respectivement les méthodes getTimeout et setTimeout.
- L'évaluation de l'expression :
Serial.getTimeout()A
retourne dans le typeunsigned longla valeur du champ_timeout. - Une instruction d'appel de la forme :
Serial.setTimeout(durée);A
affecte au champ_timeoutla valeur prise par l'expression durée passée en argument, de typeunsigned long(la méthodesetTimeoutétant elle‑même de typevoid).
La valeur par défaut du champ _timeout est de 1000 ms, soit une seconde. Cette valeur est très grande au regard des valeurs typiques de la durée d'exécution de la fonction loop d'un programme Arduino – durée qui se mesure usuellement en microsecondes ou millisecondes.
Avec cette valeur par défaut, il en résulte que coder l'appel d'une méthode de lecture sans condition dans la fonction loop risque de compromettre la réactivité du programme si le buffer de réception n'est pas rempli en permanence par un flux d'octets.
Pour prévenir ce problème, il est judicieux :
- soit de diminuer à sa valeur minimale – 1 ms – le champ
_timeout; en effet, comme la lecture des octets dans le buffer de réception ne requiert que quelques microsecondes et que la capacité par défaut du buffer est très limitée (63 octets), une milliseconde suffit largement, quelle que soit la méthode de lecture employée ; - soit de coder l'appel de la méthode de lecture sous une condition rare et durant laquelle la réactivité du programme n'est pas essentielle.