dimanche 21 avril 2019

ARDUINO : un protocole série




ARDUINO : un protocole série


Le but de cet article est de décrire un protocole de transmission de données sur ligne série. Ce protocole est fourni sous forme de librairie pour ARDUINO.

Il peut être utilisé entre deux systèmes du type ARDUINO ou un ARDUINO et un PC (un code écrit en PYTHON est également fourni).

Je n'ai pas essayé de faire tourner ce code sur ESP8266 ou ESP32, mais normalement rien ne devrait s'y opposer, bien que ce ne soit pas la vocation de ces modules.


1. Les bases

Le but d'un protocole est de fournir un service de transfert de données fiable et efficace entre deux systèmes.

On peut citer parmi les plus connus :

  • HDLC : High Level Data Link Control
  • SLIP : Serial Line Internet Protocol
  • PPP : Point To Point Protocol
  • KERMIT : transfert de fichiers
  • XMODEM, YMODEM, ZMODEM : transfert de fichiers

Un protocole transmet des entités appelées trames constituées le plus souvent de données encadrées par un marqueur ou fanions de début et de fin :

Dans cet exemple tiré de HDLC, dont est dérivé PPP d'ailleurs, nous voyons les deux fanions de début et de fin, mais également d'autres éléments :

  • adresse : le destinataire
  • commande : le N° de la commande à exécuter
  • FCS : un CRC de vérification de la validité des données

Les marqueurs de début et de fin permettent de s'assurer que la réception des données se fera intégralement. Tant que le marqueur de début n'est pas reçu, les caractères reçus sont ignorés. En effet à qui cela servirait-il de conserver des données si le début a été raté ?

Le CRC est un mot sur 16 bits calculé à partir des données et transmis à la suite de celles-ci. Il permettra au destinataire des données de vérifier leur intégrité.

Il ne faut pas confondre Checksum et CRC.

Un Checksum est une simple somme de tous les octets de données. Si deux octets sont permutés dans la trame, un Checksum ne permet pas de s'en rendre compte.

Un CRC est beaucoup plus élaboré et offre un niveau de sécurité nettement supérieur.

Un autre point important : un protocole sérieux implémente en général un acquittement des données transmises. Cela permet à l'expéditeur de savoir que sa requête est bien arrivée à destination et a été vérifiée.

Il ne faut pas confondre acquittement protocolaire et acquittement applicatif :

L'acquittement protocolaire permet au destinataire, en réponse à une requête, de dire qu'il a bien reçu celle-ci et qu'elle est conforme au protocole.

L'acquittement protocolaire est en général un simple octet : ACK.

L'acquittement applicatif permet au destinataire, en réponse à une requête, de dire qu'il a bien reconnu celle-ci et l'a bien exécutée.

Un acquittement applicatif est une trame à part entière et peut contenir :

  • un simple octet indiquant l'exécution de la requête :
    • oui / non
    • requête refusée
    • erreur interne
    • requête inconnue
  • un simple octet suivi de données

L'acquittement applicatif est facultatif, l'acquittement protocolaire ne l'est pas.

2. Les protocoles dits "légers"

Classiquement, dans le monde ARDUINO, un seul marqueur de fin est utilisé, qui est souvent '\r' ou "Carriage Return", la touche RETURN du clavier. Mais on rencontre aussi '\0' '\xff' ou '#' ou un quelconque caractère exotique.

En l'absence de marqueur de début, rien ne permet d'affirmer que la trame a été reçue dans son intégralité. Ce n'est pas un moyen sûr de transmettre des données.

L'absence de CRC est également un problème, car on n'est jamais certain qu'un parasite ne viendra pas perturber la transmission.

Pour ce qui est de l'acquittement des requêtes les amateurs se contentent en général de peu : on ignore purement et simplement ce point.

Lorsque l'on cumule absence de marqueur de début, absence de CRC et absence d'acquittement on obtient un protocole sans aucune fiabilité, qui montrera très vite ses faiblesses.

Bien entendu, pour transmettre des données destinées à un afficheur, il n'est pas nécessaire ni vital que le message soit transmis de manière sûre. Le seul risque est d'afficher une phrase incomplète ou comportant une faute d'orthographe.

Mais quand il s'agit d'envoyer des requêtes commandant l'activation de moteurs, de pompes, de gâches de porte, de vérins ou d'autres organes mécaniques, l'erreur de transmission peut poser un vrai problème et même coûter très cher.

3. Les besoins

Dans un projet ARDUINO les besoins sont réduits, la taille des données est faible, mais on ne doit pas négliger pour autant la fiabilité des données transmises.

Nous allons éviter les protocoles de transmission de fichiers, inadaptés. SLIP serait un choix possible mais il lui manque un petit CRC pour être plus sûr.Enfin PPP et HDLC sont vraiment très lourds et possèdent une notion de connexion dont nous n'avons pas besoin.

Je propose donc un protocole éprouvé provenant d'anciens projets développés dans le monde industriel sur des plateformes légères : ARM7, MSP430, STM32, etc.

Il a les caractéristiques suivantes

  • un marqueur de début et de fin standard : STX / ETX
  • acquittement ou non acquittement standard : ACK / NAK
  • un CRC sur 16 bits
  • mode transparent (DLE)
  • une numérotation des trames
  • un timer inter-caractères
  • un timer trame
  • des répétitions
 

3.1. Les caractères de contrôle

Ici les choix se sont portés sur des caractères de contrôle standard :

  • STX (0x02) : Start of TeXt
  • ETX (0x03) : End of TeXt
  • ACK (0x06) : acquittement
  • NAK (0x15) : non acquittement
  • DLE (0x10) : échappement

Un caractère STX est transmis en début de chaque trame. Un caractère ETX est transmis en fin de chaque trame. Ceci permet de s'assurer que la trame reçue est complète.

3.2. CRC et acquittement

Chaque trame reçue est acquittée par un caractère ACK par le destinataire si le CRC recalculé par lui est correct. Dans le cas contraire, NAK est envoyé. L'émetteur répétera donc la requête. Le nombre de répétitions est fixé à 3.

Une trame reçue est acquittée par le destinataire en précisant quel est le numéro (SEQ) de la trame concernée. L'expéditeur peut donc s'assurer que la trame acquittée est bien celle qu'il a émise et pas une autre, transmise précédemment par exemple.

3.4. Le mode transparent

Qu'est-ce que le mode transparent ?

Si les données transmises sont binaires, il est parfaitement possible d'y rencontrer des caractères de contrôle STX (0x02), ETX (0x03), ACK (0x06) ou NACK (0x15).

Le mode transparent permet d'échapper ces caractères à l'aide d'un caractère spécial DLE (0x10). A chaque fois que le destinataire reçoit DLE, il sait donc que le caractère suivant est une donnée, et non pas un caractère de contrôle.

En d'autres termes si le récepteur reçoit ETX, il s'agit du caractère de EndOfText. Si le récepteur reçoit DLE+ETX, il s'agit d'une donnée : 0x02.

3.5. Les timers

Le timer inter-caractères est utilisé pour interrompre la réception des données dans le cas où le temps séparant deux caractères dépasse une certaine valeur. Cette valeur est fixée à 500 millisecondes.

Ce timer est important car quand la réception est interrompue, si le destinataire attend indéfiniment la suite de la trame qui n'arrive pas, il va se retrouver dans une situation critique car l'expéditeur, ne voyant pas d'acquittement, va répéter la requête. Le timer de répétition d'une requête est de 3 secondes.

Ce timer permet donc de jeter les caractères déjà reçus et de remettre l'automate de réception dans un état IDLE (en attente).

3.6. Les données

Les données transmises peuvent être composées d'un seul octet de commande ou d'un octet de commande suivi des données associées.

Il est bien entendu possible de réserver deux octets pour la commande. Rien n'est imposé.

Il est possible de transmettre également des mots et des phrases en clair. A la charge du destinataire d'interpréter la grammaire.

En général, j'organise les requêtes et les réponses sous forme de structures C, beaucoup plus faciles à interpréter.

4. L'implémentation

La librairie est disponible ici :

https://bitbucket.org/henri_bachetti/sdltp-serial-protocol/

Cette page comporte un chapitre API qui décrit les fonctions.

Les librairies suivantes sont également utilisées :

https://bitbucket.org/henri_bachetti/mpp-console-logger/
https://bitbucket.org/henri_bachetti/kernel_timers
https://github.com/PaulStoffregen/SoftwareSerial 

Cette page vous donne toutes les informations nécessaires :

https://riton-duino.blogspot.com/p/migration-sous-bitbucket.html

Deux exemples d'utilisation sont fournis :

4.1. Émetteur récepteur

Deux sketches permettent de dialoguer entre deux cartes.

transmitter.ino

receiver.ino

Ces deux sketches échangent des données sous la forme d'une structure.

Bien évidemment on peut aussi envoyer ou recevoir des types simples (int, long, float, etc.) ou une chaîne de caractères. Ce n'est qu'une question de définition de ce que l'on veut envoyer et recevoir.

4.2. serial-test

Cet exemple est organisé comme suit :

  • serial-test.ino : le programme principal
  • host.h :  les définitions des structures des requêtes
  • host.cpp : l'implémentation des requêtes
  • server.h : les définitions de base du moteur
  • server.cpp : le moteur d'interprétation des requêtes

Un petit protocole applicatif est implémenté. Les commandes sont codées sur un octet et les réponses également :

  • 'V' : requête de demande de version
  • '!' :  echo
  • 'R' : reboot
  • 'Z' : poweroff (fictif)
  • 'U' : mise à l'heure
  • 'T' : récupération de l'heure
  • 'S' : delay
  • 'L' : LED ON ou OFF 

Chaque requête est acquittée par une réponse applicative contenant un octet identifiant la réponse :

  • 'v' : réponse à la demande de version
  • '!' :  réponse à echo
  • 'r' : réponse à reboot
  • 'z' : réponse à poweroff
  • 'u' : réponse à la mise à l'heure
  • 't' : réponse à la récupération de l'heure
  • 's' : delay
  • 'l' : LED ON ou OFF 

La réponse (acquittement applicatif) contient ensuite un octet de statut :

  • 'A' : requête acceptée
  • 'R' : requête refusée
  • 'B' : occupé
  • 'E' : erreur
  • 'U' : requête inconnue

La réponse contient ensuite les données, s'il y en a.

Le sketch exemple occupe de la place :

Le croquis utilise 15684 octets (48%) de l'espace de stockage de programmes.
Les variables globales utilisent 1106 octets (54%) de mémoire dynamique.

Il tient donc largement dans la mémoire d'une UNO ou NANO.
Il communique par l'intermédiaire d'un convertisseur USB / Série connecté sur les broches 10 et 11 (SoftwareSerial).

Il sera plus à l'aise sur une MEGA si l'application est de taille importante :

Le croquis utilise 18578 octets (7%) de l'espace de stockage de programmes.
Les variables globales utilisent 1652 octets (20%) de mémoire dynamique,

Le programme de test a été testé sur les plateformes suivantes :

  • ARDUINO UNO : software serial
  • ARDUINO MEGA : software serial
  • ARDUINO MEGA : Serial2 Serial3

Un logiciel écrit en PYTHON est également présent dans la repository.

Il permet de tester le protocole et l'application exemple.

python test.py CommonTest.testVersion

Demande la version de l'application distante

python test.py CommonTest.testSetTime

Met à l'heure du PC l'application distante.

python test.py CommonTest.testGetTime

Récupère l'heure de l'application distante.

python test.py CommonTest.testAckTimeout

Teste le timeout d'acquittement

python test.py CommonTest.testCharTimeout

Teste le timeout inter-caractère

python test.py CommonTest.testNak

Test le non acquittement.

python test.py DemoTest.testLedOn

Allume la LED de l'ARDUINO.

python test.py DemoTest.testLedOff

Eteint la LED de l'ARDUINO.

python test.py

Exécute tous les tests.

PYTHON unittest est un bon outil de test très largement utilisé qu'il est assez intéressant de connaître.



Cordialement
Henri

Aucun commentaire:

Enregistrer un commentaire