Introduction
L'échange d'information entre différents blocs d'un système
constitue une brique fondamentale du monde hyper connecté dans
lequel nous vivons. Les normes de communication se multiplient et se
comptent par dizaines. Voici quelques exemples loin d'être
exhaustifs :
- Ethernet
Une norme de réseau local (LAN) qui spécifie
les aspects physiques et de liaison de données pour la
transmission de données sur des câbles Ethernet,
- Wi-Fi (IEEE 802.11) :
Une famille de normes de communication sans
fil qui permettent la connexion d'appareils à un réseau local sans
avoir besoin de câbles physiques,
- Bluetooth (IEEE 802.15.1) :
Une norme de communication sans fil à courte
portée utilisée pour connecter des appareils électroniques, tels
que des smartphones, des écouteurs sans fil et des périphériques
informatiques, dans un réseau personnel (PAN),
- USB (Universal Serial Bus) :
Une norme de communication filaire utilisée
pour connecter des périphériques électroniques, tels que des
claviers, des souris, des imprimantes, des caméras et des
périphériques de stockage, à un ordinateur ou à d'autres appareils
hôtes,
- GSM (Global System for Mobile
Communications) :
Une norme de communication mobile utilisée
pour les réseaux de téléphonie mobile. Elle spécifie les normes
pour la transmission de la voix et des données sur les réseaux
cellulaires,
- LTE (Long-Term Evolution) :
Une norme de communication sans fil utilisée
pour les réseaux de téléphonie mobile de quatrième génération
(4G), offrant des vitesses de données plus élevées et une
meilleure qualité de service que les normes précédentes,
- TCP/IP (Transmission Control
Protocol/Internet Protocol) :
Alors que TCP et IP sont techniquement deux
protocoles distincts, ils sont souvent mentionnés ensemble car ils
forment la base du fonctionnement d'Internet et des réseaux
informatiques modernes. TCP gère la transmission des données de
manière fiable et ordonnée, tandis qu'IP s'occupe du routage des
paquets de données sur le réseau. Ensemble, ils permettent la
communication et l'échange de données entre les appareils
connectés à Internet,
- NFC (Near Field Communication) :
Une norme de communication sans fil à courte
portée utilisée pour l'échange de données entre des dispositifs
situés à proximité les uns des autres, souvent utilisée pour les
paiements sans contact et le partage d'informations entre
appareils mobiles,
- I2C (Inter-Integrated Circuit) :
Un protocole de communication série utilisé
pour la communication entre circuits intégrés sur une carte de
circuit imprimé. Il est souvent utilisé pour connecter des
composants tels que des capteurs, des afficheurs et des mémoires à
un microcontrôleur,
- CAN (Controller Area Network) :
Un protocole de communication série largement
utilisé dans l'industrie automobile pour permettre la
communication entre les différents systèmes électroniques des
véhicules, tels que les moteurs, les freins, les airbags, etc,
- LoRa (Long Range) :
Une technologie de communication sans fil à
longue portée conçue pour les réseaux bas débit, souvent utilisée
dans les applications IoT pour la surveillance à distance, le
suivi d'actifs, et d'autres cas d'utilisation nécessitant une
large couverture géographique,
- SPI (Serial Peripheral Interface) :
Un protocole de communication série synchrone
utilisé pour la communication entre des périphériques
électroniques sur une carte de circuit imprimé. Il est couramment
utilisé pour connecter des microcontrôleurs à des capteurs, des
écrans, des modules de mémoire, et d'autres périphériques,
Toutes ces normes de communication ont un ancêtre
commun: Le standard de communication série
développé dans les années 60 et standardisé sous la nomenclature
RS232C
Parallèle vs Série
Lors d'une séquence de communication, on échange du texte et des
nombres. Ces deux types d'information sont codés en binaire
empaqueté par groupe de 8 bits appelé octets.
- En code ASCII ISO-8859-1, le caractère 'é' est codé sur un octet
par 0xE9 = 111010001
- En code Unicode UTF-8, le caractère 'é' est codé sur deux octets
par 0xC3 0xA9 = 11000011 10101001
- En code ASCII ISO-8859-6, le caractère arabe 'ك' est codé sur un
octet par 0xE3 = 111000011
- En code Unicode UTF-8, le caractère 'ك' est codé sur deux octets
par 0xD9 0x83 = 11011001 10000011
- Le nombre entier positif 23456 est codé sur deux octets par
01011011 10100000
- Le nombre entier négatif -23456 est codé sur deux octets par
10100100 01100000
- Le nombre réel 25.34 est représenté en virgule flottante
IEEE-754 sur 4 octets: 01000001 11001010 10111000 01010001
En résumé, quelque soit le type
d'information à échanger, il suffit de savoir transmettre et
recevoir des octets
Transmission parallèle
On utilise un cable d'au moins 8 fils. l'émetteur place chaque fil
à une tension équivalente au bit correspondant. Par exemple 0 Volts
pour le 0 logique et 5 Volts pour le 1 logique. Les huit bits sont
envoyés en même temps, tout se passe comme si on a une autoroute à 8
voies et 8 voitures passent en même temps
Transmission Série
Les 8 bits sont envoyé l'un après l'autre sur le même fil.
l'émetteur place le fil successivement soit à 0V soit à 5V selon le
bit à transmettre. Tout se passe comme si on a une autoroute à une
seule voie et 8 voitures qui passent l'une après
Pour un tas de raisons plus au moins évidentes, la quasi totalité
des standard de communication filaires ont adoptés le mode de
communication série. Parmi ces raisons, on peut citer :
- Réduction des coûts et de complexité
- Augmentation de la portée
- Meilleurs performance, car le couplage capacitif entre les fils
d'un même cable parallèle engendre un taux d'erreur qui augmente
avec la fréquence et la longueur du câble et qui de vient très
vite prohibitif
Câble de communication série
Pour une communication bidirectionnelle (liaison full duplex), deux
fils sont utilisés, un dans chaque sens, avec un fil de masse
supplémentaire servant de référence électrique.
- Tx: Transmit Data
- Rx: Receive Data
- Gnd : Ground = référence électrique
Vu son apparence, ce cable est souvent appelé câble croisé
Vitesse de communication
Comme les bits sont transmis l'un après l'autre, la vitesse de
communication représente le nombre de bits transmis par seconde. On
utilise souvent le terme "Baud Rate" avec l'unité "baud" pour
désigner la vitesse de communication. Par exemple 9600 baud signifie
9600 bits/s.
Le mode de communication est dit Asynchrone,
cela signifie que l'émetteur ne partage par son horloge avec
récepteur sur un fil dédié comme c'est le cas des systèmes de
communication synchrones. Pour ne pas perdre d'information, il faut
que le récepteur collecte les bits exactement au même rythme qu'ils
ont été transmis par l'émetteur. D'où la règle fondamentale : L'émetteur et le
récepteur doivent être configuré
à la même vitesse de communication
Pour des raisons historiques, les vitesses de communication
communément utilisées sont 300, 600, 1200, 2400, 4800, 9600, 14400,
19200, 28800, 38400, 57600, ou 115200 baud
Synchronisation
Dans le flux de bits transmis par l'émetteur, il est important que
le récepteur puissent repérer le premier bits de chaque octet. Pour
cette raison, le récepteur va insérer deux bits de synchronisation
supplémentaires et chaque octet sera transmis de la façon suivante :
- Au repos la ligne Tx est toujours au niveau 1
- L'émetteur transmet un "Start bit" (S) toujours = 0
- L'émetteur transmet le 8 bits de données en commençant par le
LSB
- L'émetteur transmet un "Stop bit" (P) toujours = 1
- La ligne Tx restera à 1 jusqu'au début de l'octet suivant
Comme on peut le constater sur le signal, la transmission d'un
octet commence toujours par une transition descendante. En détectant
cette transition, le récepteur aligne son horloge sur celle de
l'émetteur ce qui lui permet de lire correctement les bits de
données.
L'UART
L'UART (Universal Asynchronous Receiver/Transmitter) est le moteur du port série. C'est un module intégré
dans le microcontrôleur qui se charge de gérer les communications série.
- Elle s'occupe de la sérialisation des données :
- En émission, elle reçoit les données en parallèle sur le bus du
microcontrôleur et les transmet en série un bit après l'autre sur un seul fil vers un système distant,
- En réception, elle réalise l'opération inverse, elle reçoit les données bits par bit et les transfert en
parallèle sur le bus du microcontrôleur
- Elle gère la vitesse de communication connue sous le nom de baud rate. Elle
fixe la fréquence de l'horloge qui cadence la transmission des bits sur la ligne TxD et la réception des
bits sur la ligne RxD
- Elle s'occupe de la synchronisation en émission et en réception à l'aide du Start bit et du Stop bit
- Elle s'occupe de la détection d'erreurs à l'aide du bit de parité
- Elle s'occupe du control de flus à l'aide des lignes DTR, DSR, RTS, CTS ...
Buffer de réception
Le buffer de réception du port série est une zone de mémoire temporaire (prise dans la RAM) qui stocke les
données entrantes
provenant de la liaison série (UART) jusqu'à ce qu'elles soient lues par le programme.
Le buffer de réception a une taille fixe, qui est généralement de 64 octets sur le Arduino Uno.
Le buffer de réception permet d'éviter la perte de données lorsqu'elles sont reçues plus rapidement que le
programme ne peut les traiter. Les données sont stockées dans le buffer jusqu'à ce que le programme
puisse les lire.
Les données sont lues du buffer de manière séquentielle. Chaque octet lu est retiré du
buffer pour faire place aux données suivantes.
Quand le buffer de réception est plein, les nouvelles données reçues seront perdue.
Buffer de transmission
Le buffer de transmission du port série est une zone de mémoire qui stocke temporairement les données
envoyées par le programme jusqu'à ce qu'elles soient effectivement transmises par l'UART
Le buffer de transmission a une taille fixe, généralement de 64 octets sur le Arduino Uno.
Une fois que les données sont placées dans le buffer de transmission, l'UART les envoie à un
rythme déterminé par la vitesse de transmission (baud rate).
Port Série Arduino ≠ RS232C
Attention, le port série du Arduino n'est pas tout à fait
compatible avec le standard de communication série RS232C
- Niveaux électriques des Signaux Tx, Rx
- Arduino utilise les niveaux TTL : 0 -> 0 Volts, 1 -> 5
Volts
- RS232C utilise des niveaux inversés de forte valeur : 0 ≈>
+12 Volts, 1 ≈> -12 Volts
- Control de flux hardware
- Arduino ne dispose que de deux broches de données TxD et RxD.
Le control de flux doit se faire en software.
- RS232C utilise un connecteur DB9 standard sur lequel, en plus
des broches TxD et RxD, on trouve les broches de control de flux
DTR, DSR, RTS, CTS, RI, DCD. Ces broches permettent aux deux
machines qui communiquent de gérer les échanges et éviter les
pertes de données
- Control de parité
- Arduino utilise toujours une donnée de 8 bits. Il n'utilise
pas le mécanisme de control de parité pour détecter les erreurs
- RS232C offre la possibilité d'utiliser une donnée de 7 bits
plus un bit de parité pour détecter les erreurs de transmission
- Bit de Stop
- Arduino utilise toujours un seul bit de Stop
- RS232C offre la possibilité d'utiliser plusieurs bits de Stop
La librairie Serial
La librairie Serial est installée d'office avec l'environnement Arduino-IDE
- Serial.begin(speed);
- Initialise le port série et définit la vitesse de
communication
- Serial.begin(9600);
- Serial.write()
- Serial.write(nombre); Transmet
seulement l'octet bas du nombre (troncature)
- Serial.write(66); //transmet
l'octet 66
- Serial.write(856); // Transmet
l'octet 88 (= 01011000 = octet bas de 856=1101011000)
- Serial.write(chaîne); Transmet
le code UTF-8 de chaque caractère de la chaîne
Le code UTF-8 d'un caractère peut être
constitué de 1, 2 ou 3 octets
- Serial.write("A"); //transmet
l'octet 65 = 0x41 = code du caractère A
- Serial.write("déçu"); //transmet
0x64 0xC3 0xA9 0cC3 0xA7 0x75
- Serial.write(tableau, n); Transmet
n octets du tableau
byte B[] = {0x11, 0x22, 0x33, 0x44, 0x55};
Serial.write(B,3); //transmit
3 octets du tableau B
- Serial.print()
- Serial.print(chaîne); Transmet
le code UTF-8 de chaque caractère de la chaîne
Le code UTF-8 d'un caractère peut être
constitué de 1, 2 ou 3 octets
- Serial.print("A"); //transmet
l'octet 65 = 0x41 = code du caractère A
- Serial.print("déçu"); //transmet
0x64 0xC3 0xA9 0cC3 0xA7 0x75
- Serial.print(entier, base); //
transmet la chaîne représentant le nombre `entier` dans la bas
`base`
- Serial.print(2048); //Transmet
la chaîne "2048" -> 4 octets: '2' , '0' , '4' , '8' ->
octets 50, 48, 52, 56
- Serial.print(23456,HEX);
//Transmet la chaîne "5BA0" qui est la représentation de 23456
en hexadécimal -> 4 octets 53, 66, 65 et 48 = codes ascii
des caractères '5', 'B', 'A' et '0'
- Serial.print(240,BIN); //Transmet
la chaîne "11110000". Un nombre négatif est représenté sur 32
bits quelque soit son type
- Serial.print(reel, n); //
transmet la chaîne représentant le nombre réel 'reel' avec n décimales
- Serial.print(235.4567); //Transmet
la chaîne "235.46" (2 décimales, arrondit correct)
- Serial.print(235.45673,4); //Transmet la chaîne "235.4567" (4
décimales)
- Serial.println();
- Identique à Serial.print() mais transmet en plus un retour à la ligne (CR
LF)=(\r \n)=(13 10)
- Serial.println(); //Retour à la ligne
- Serial.println("Bonjour"); // Transmet "Bonjour" suivie d'un retour à la
ligne
- Serial.setTimeout(tms);
- Définit la durée de timeout en lecture. Par défaut le timeout est fixé à 1000 ms = 1s
- Le Timeout évite le blocage lors des opérations de lecture
- Serial.available();
- Retourne le nombre d'octets disponible dans le buffer de réception
- b = Serial.read();
- Lit un octet à partir du buffer de réception. L'octet est retiré du buffer,
- Non bloquante, retourne (immédiatement) -1 (255) si le buffer de réception est vide,
- n = Serial.readBytes(B, length);
- Lit un ensemble d'octets à partir du buffer de réception
- B: nom du tableau qui reçoit les données. Type char[] ou byte[]
- length: nombre d'octets à lire (int)
- Retourne n = nombre d'octets effectivement lus. Peut être inférieur à
length en cas de timeout
- n = Serial.readBytesUntil(terminator, B, length);
- Lit un ensemble d'octets et les place dans le tableau B
- La réception s'arrête quand on reçoit l'octet terminator ou après la
réception de length octets ou après un timeout
- Retourne le nombre n d'octets effectivement lus. Peut être inférieur à
length en cas de timeout
- S = Serial.readString();
- Lit les caractères qui arrivent et les place dans le String S
- S'arrête après un Timeout
- Si le flux entrant contient des caractères spéciaux comme un retour à la ligne, il est ajouté à la
chaîne S
- S = Serial.readStringUntil(C);
- Lit les caractères qui arrivent et les place dans la chaîne S,
- S'arrête à la réception du caractère C ou après un Timeout,
- Le caractère C est retiré du buffer de réception mais il n'est pas inclus dans la chaîne
retournée
- Les caractères situés après le terminateur C restent dans le buffer de réception et pourront être lus
plus tard
- Serial.readStringUntil('#'); // arrête la lecture à la réception du
caractère '#'
Si on désire recevoir du texte qui a été transmis ligne par ligne. Le retour à la ligne est
normalement
codé par deux caractères de contrôle CR (retour chariot: code ascii 13) et LF (saut de ligne: code ascii
10). En langage C (mais pas seulement), ces deux caractères sont représentés par '\r' et '\n'. Le meilleur
choix de
terminateur pour Serial.readStringUntil() est '\n' car il arrive toujours en dernier. Le caractère
'\r' qui précède, sera inclus dans la chaîne retournée, on peut le supprimer à l'aide la méthode
.trim() de l'objet String. Normalement, .trim() sert à supprimer les espaces au
début et à la fin d'une chaîne mais elle permet aussi de supprimer les caractères de contrôle '\r', '\n' et
'\t' (tabulation: code ascii 09)
String ligne = Serial.readStringUntil('\n');
ligne.trim(); // supprime \r
Serial.end();
- Désactive l'UART. Les broches 0 et 1 peuvent de nouveau être utilisées comme des E/S normales
N = Serial.parseInt();
- Lit correctement le premier entier (int ou long) reçu en mode texte
- Les premiers caractères qui ne sont pas des chiffres sont retirés du buffer et ignorés
- La lecture du nombre s'arrête au premier caractère qui n'est pas un chiffre y compris CR ou LF. Ce
caractère et les suivants ne sont pas retirés du buffer
- La lecture peut aussi s'arrêter au bout du timeout
- Si par exemple le buffer contient xx yz 234abc xyz
- Les caractères xx yz sont retirés du buffer et ignorés
- Le nombre 234 est affecté à N
- Les caractères abc xyz restent dans le buffer de réception
serialEvent(){ ... }
- Si on ajoute cette fonction au programme, à chaque repassage dans la fonction loop(), si le buffer de
réception contient quelque chose, cette fonction sera exécutée
Vider le buffer de réception
- Il arrive souvent d'avoir besoin de vider le buffer de réception avant de commencer la scrutation du
flux entrant
- while(Serial.available()){Serial.read(); delay(2);}
Choix du timeout
Lorsqu'on utilise Serial.readString(), la lecture de la chaîne s'arrête après un
timeout d'inactivité sur la ligne de réception. Par défaut, ce timeout est fixé à une seconde
(1000 ms).
L'ajustement du timeout doit tenir compte de la nature de l'application. Un timeout trop court
peut provoquer
une lecture partielle de la chaîne, tandis qu'un timeout trop long peut ralentir la réactivité du
programme :
- Cas 1 : Transmission continue sans intermittence :
Si la chaîne attendue est transmise sans interruption par l'émetteur distant (par exemple, un autre
microcontrôleur ou un module série), alors le timeout peut être ajusté en fonction de la vitesse de
transmission. Par exemple, à 9600 bauds, chaque caractère (10 bits avec start/stop) prend environ 1,04 ms à
transmettre. Dans ce cas, un timeout de 2 ms peut suffire (Si ne reçoit rien durant 2ms, c'est que la chaîne
est terminée) : Serial.setTimeout(2);
- Cas 2 : Transmission manuelle caractère par caractère
Si la chaîne est saisie par un opérateur humain via un clavier (transmission caractère par caractère), il
faut prévoir un timeout plus long pour laisser à l'utilisateur le temps de taper l'ensemble du message.
Un timeout de 5 secondes (Serial.setTimeout(5000);) semble raisonnable pour couvrir la plupart des cas
d'utilisation, y compris les saisies lentes ou hésitantes.
Conflit sur le port série
Si on a un module branché sur le port série du Arduino (broches 0 et 1), on peut avoir un problème lors du
téléversement d'un
programme. Comme on peut le constater sur la figure, le port série de l'ATmega est connecté en parallèle sur
celui du module d'ou le conflit
Dans ce cas il faut débrancher le module pendant le téléversement
Pour la même raison, lors de la pase d'exécution, il ne faut pas ouvrir le moniteur série car il va entrer en
conflit avec le module
Exemples
Exemple 1 : Communication basique
Utilisez le moniteur série ou le virtual terminal sur Proteus ISIS pour tester le programme suivant :
void setup() {
Serial.begin(9600);
Serial.println("Hello, world!");
Serial.println("Envoyez moi des messages");
}
void loop() {
if (Serial.available() > 0) {
String msg = Serial.readString();
Serial.print("Message reçu : ");
Serial.println(msg);
}
}
Exemple 2 : Contrôle d'une LED via le port série
Ce programme permet de contrôler une LED en recevant des commandes via le port série. La commande "ON" allume
la LED, la commande "OFF" éteint la LED. Le coté éloigné qui envoie les commandes doit les valider par un
"retour à la ligne"
const int ledPin = 13; // Pin de la LED intégrée
void setup() {
Serial.begin(9600);
pinMode(ledPin, OUTPUT);
Serial.println("J'attend les commandes 'ON' ou 'OFF'");
}
void loop() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
command.trim(); // Supprimer le '\r'
if (command.equalsIgnoreCase("ON")) {
digitalWrite(ledPin, HIGH);
Serial.println("LED allumée.");
} else if (command.equalsIgnoreCase("OFF")) {
digitalWrite(ledPin, LOW);
Serial.println("LED éteinte.");
} else {
Serial.println("Commande non reconnue.");
}
}
}
Exemple 3 : Chaîne de commande avec paramètre entier
Ce programme illustre une façon parmi d'autres de lire une commande constituée d'un mot-clé suivi
d'un paramètre numérique.
la commande et le paramètre doivent être séparés d'au moins un espace
Par exemple: LEFT 125
void setup() {
Serial.begin(9600);
Serial.setTimeout(5000); // en cas de saisie hésitante de la commande
}
void loop() {
if (Serial.available()) {
String cmd = Serial.readStringUntil(' '); // Lecture de la commande jusqu'à l'espace
int param = Serial.parseInt(); // Lecture du paramètre entier
while (Serial.available()) Serial.read(); // Vidage du buffer pour éviter les résidus de fin de ligne
// Affichage de la commande et du paramètre reçus
Serial.println("Commande reçue : -->" + cmd + "<--");
Serial.println("Paramètre reçu : -->" + String(param) + "<--");
}
}
Quelques indications sur le code :
- Sur le dispositif émetteur (ex. : un terminal série), la commande est généralement validée par un
retour à la ligne , souvent encodé par les caractères de contrôle \r \n
On recevra donc quelque chose de la forme: "LEFT 125\r\n"
- Serial.readStringUntil(' ') lit les caractères jusqu'au premier espace. On obtient ici
"LEFT"
- Serial.parseInt() lit ensuite les chiffres qui suivent et les convertit en entier (125
dans l'exemple)
- Cette fonction parseInt() ne consomme pas les caractères de fin de ligne (\r et \n), qui
restent dans le
buffer. Si on ne les enlève pas, ils seront interprétés comme une future commande vide ou provoqueront des
lectures incorrectes, d'où la nécessité de vider le buffer de réception.