Meilleures pratiques et modèles de protocole de communication

19

Chaque fois que je conçois un protocole série à utiliser entre deux Arduinos, j'ai l'impression de réinventer une roue. Je me demande s'il existe des pratiques exemplaires ou des modèles que les gens suivent. Cette question concerne moins le code réel, mais davantage le format des messages.

Par exemple, si je voulais dire à un arduino de flasher, c'est la première LED 3 fois que je pourrais envoyer:

^L1,F3\n
  • '^': démarre une nouvelle commande
  • 'L': définit la commande, (L: ciblez cette commande sur une LED)
  • «1»: ciblez la première LED
  • ',': Séparateur de ligne de commande, nouvelle valeur à suivre dans ce message
  • 'F': sous-commande Flash
  • '3': 3 fois (clignoter la LED trois fois)
  • '\ n': terminer la commande

Pensées? Comment abordez-vous habituellement l'écriture d'un nouveau protocole série? Et si je voulais envoyer une requête de arduino 1 à arduino 2 et recevoir une réponse?

Jeremy Gillick
la source

Réponses:

13

Il existe de nombreuses façons d'écrire un protocole série en fonction des fonctionnalités que vous souhaitez et du niveau de vérification des erreurs dont vous avez besoin.

Certaines des choses courantes que vous voyez dans les protocoles point à point sont:

Fin du message

Les protocoles ASCII les plus simples ont juste une séquence de caractères de fin de message, souvent \rou \ncomme c'est ce qui est imprimé lorsque la touche Entrée est enfoncée. Les protocoles binaires peuvent utiliser 0x03ou un autre octet commun.

Début du message

Le problème avec la fin du message est que vous ne savez pas quels autres octets ont déjà été reçus lorsque vous envoyez votre message. Ces octets seraient alors préfixés au message et provoqueraient une mauvaise interprétation. Par exemple, si l'Arduino vient de se réveiller, il peut y avoir des ordures dans le tampon série. Pour contourner cela, vous avez un début de séquence de messages. Dans votre exemple ^, dans les protocoles binaires, souvent0x02

Vérification des erreurs

Si le message peut être corrompu, nous avons besoin d'une vérification d'erreur. Cela pourrait être une somme de contrôle ou une erreur CRC ou autre chose.

Caractères d'échappement

Il se peut que la somme de contrôle s'ajoute à un caractère de contrôle, tel que l'octet «début de message» ou «fin de message», ou que le message contienne une valeur égale à un caractère de contrôle. La solution est d'introduire un caractère d'échappement. Le caractère d'échappement est placé avant un caractère de contrôle modifié afin que le caractère de contrôle réel ne soit pas présent. Par exemple, si un caractère de début est 0x02, en utilisant le caractère d'échappement 0x10, nous pouvons envoyer la valeur 0x02 dans le message comme paire d'octets 0x10 0x12 (caractère de contrôle XOR octet)

Numéro de paquet

Si un message est corrompu, nous pourrions demander un renvoi avec un message nack ou retry, mais si plusieurs messages ont été envoyés, seul le dernier message peut être renvoyé. Au lieu de cela, le paquet peut recevoir un numéro qui se renouvelle après un certain nombre de messages. Par exemple, si ce nombre est 16, le périphérique émetteur peut stocker les 16 derniers messages envoyés et s'il y en a un endommagé, le périphérique récepteur peut demander un renvoi en utilisant le numéro de paquet.

Longueur

Souvent, dans les protocoles binaires, vous voyez un octet de longueur qui indique au périphérique récepteur le nombre de caractères dans le message. Cela ajoute un autre niveau de vérification d'erreur comme si le nombre correct d'octets n'était pas reçu, il y avait alors une erreur.

Spécifique à l'Arduino

Lors de l'élaboration d'un protocole pour Arduino, la première considération est la fiabilité du canal de communication. Si vous envoyez sur la plupart des supports sans fil, XBee, WiFi, etc., la vérification des erreurs et les tentatives sont déjà intégrées et donc inutile de les mettre dans votre protocole. Si vous envoyez un RS422 sur quelques kilomètres, cela sera nécessaire. Les choses que j'inclurais sont le début du message et la fin des caractères du message, comme vous l'avez fait. Mon implémentation typique ressemble à ceci:

>messageType,data1,data2,…,dataN\n

La délimitation des parties de données par une virgule permet une analyse facile et le message est envoyé en ASCII. Les protocoles ASCII sont excellents car vous pouvez taper des messages dans le moniteur série.

Si vous voulez un protocole binaire, peut-être pour raccourcir la taille des messages, vous devrez implémenter l'échappement si un octet de données peut être le même qu'un octet de contrôle. Les caractères de contrôle binaire sont meilleurs pour les systèmes où le spectre complet de vérification d'erreur et de nouvelles tentatives est souhaité. La charge utile peut toujours être ASCII si vous le souhaitez.

geometrikal
la source
N'est-il pas possible que les ordures avant le début réel du code de message puissent contenir un début de code de contrôle de message? Comment gérez-vous cela?
CMCDragonkai
@CMCDragonkai Oui, c'est une possibilité, en particulier pour les codes de contrôle à un octet. Toutefois, si vous rencontrez un code de contrôle de démarrage à mi-parcours de l'analyse d'un message, le message est ignoré et l'analyse reprend.
geometrikal
9

Je n'ai aucune expertise formelle sur les protocoles série, mais je les ai utilisés pas mal de fois, et plus ou moins installés sur ce schéma:

(En-tête de paquet) (octet ID) (données) (somme de contrôle fletcher16) (pied de page de paquet)

Je fais d'habitude l'en-tête 2 octets et le pied de page 1 octet. Mon analyseur va tout vider quand il voit un nouvel en-tête de paquet et tenter d'analyser le message s'il voit un pied de page. Si la somme de contrôle échoue, elle n'abandonnera pas le message, mais continuera d'ajouter jusqu'à ce que le caractère de pied de page soit trouvé et qu'une somme de contrôle réussisse. De cette façon, le pied de page n'a besoin que d'un octet, car les collisions ne perturbent pas le message.

L'ID est arbitraire, parfois avec la longueur de la section de données étant le quartet inférieur (4 bits). Un deuxième bit de longueur pourrait être utilisé, mais je ne me dérange pas normalement car la longueur n'a pas besoin d'être connue pour être analysée correctement, donc voir la bonne longueur passer pour un ID donné est juste une confirmation supplémentaire que le message était correct.

La somme de contrôle fletcher16 est une somme de contrôle de 2 octets avec presque la même qualité que CRC mais est beaucoup plus facile à implémenter. quelques détails ici . Le code peut être aussi simple que cela:

for(int i=0; i < bufSize; i++ ){
   sum1 = (sum1 + buffer[i]) % 255;
   sum2 = (sum2 + sum1) % 255;
}
uint16_t checksum = (((uint16_t)sum1)<<8) | sum2;

J'ai également utilisé un système d'appel et de réponse pour les messages critiques, où le PC enverra un message toutes les 500 ms environ jusqu'à ce qu'il obtienne un message OK avec une somme de contrôle de tout le message d'origine en tant que données (y compris la somme de contrôle d'origine).

Ce schéma n'est bien sûr pas bien adapté pour être tapé dans un terminal comme le serait votre exemple. Votre protocole semble assez bon pour être limité à ASCII et je suis sûr qu'il est plus facile pour un projet rapide que vous souhaitez pouvoir lire et envoyer directement des messages. Pour les grands projets, il est agréable d'avoir la densité d'un protocole binaire et la sécurité d'une somme de contrôle.

BrettAM
la source
Puisque "[votre] analyseur va tout vider quand il voit un nouvel en-tête de paquet", je me demande si cela ne crée pas de problèmes si par hasard l'en-tête est rencontré à l'intérieur des données?
humanityANDpeace
@humanityANDpeace La raison de sa suppression est que lorsqu'un paquet est coupé, il ne sera jamais analysé correctement, alors quand décidez-vous de ses déchets et continuez? La solution la plus simple, et d'après mon expérience, est de supprimer un mauvais paquet dès que l'en-tête suivant arrive. J'ai utilisé un en-tête 16 bits sans problème, mais vous pouvez le prolonger si la certitude est plus importante que bande passante.
BrettAM
Donc, ce à quoi vous vous référez en tant qu'en-tête est en quelque sorte une combinaison de Magic 16 bits. ie 010101001 10101010, non? Je suis d'accord qu'il n'y a que 1/256 * 256 changement à frapper, mais cela désactive également l'utilisation de ce 16 bits dans vos données, sinon il est mal interprété comme en-tête et vous jetez le message, non?
humanityANDpeace
@humanityANDpeace Je sais que c'est un an plus tard, mais vous devez introduire une séquence d'échappement. Avant d'envoyer, le serveur vérifie la charge utile pour tout octet spécial, puis les échappe avec un autre octet spécial. Côté client, vous devez reconstituer la charge utile d'origine. Cela signifie que vous ne pouvez pas envoyer de paquets de longueur fixe et complique la mise en œuvre. Il existe de nombreuses normes de protocole série parmi lesquelles choisir. Voici une très bonne lecture sur le sujet .
RubberDuck
1

Si vous êtes dans les normes, vous pouvez jeter un œil au codage ASN.1 / BER TLV. ASN.1 est un langage utilisé pour décrire les structures de données, conçu spécifiquement pour la communication. BER est une méthode TLV de codage des données structurées en utilisant ASN.1. Le problème est que le codage ASN.1 peut être difficile au mieux. La création d'un compilateur ASN.1 à part entière est un projet en soi (et particulièrement délicat à cela, pensez-y des mois ).


Il est probablement préférable de ne conserver que la structure TLV. TLV se compose essentiellement de trois éléments: une balise, une longueur et un champ de valeur. La balise définit le type des données (chaîne de texte, chaîne d'octets, entier, etc.) et la longueur la longueur de la valeur .

Dans BER, le T indique également si la valeur est un ensemble de structures TLV lui-même (un nœud construit) ou directement une valeur (un nœud primitif). De cette façon, vous pouvez créer un arbre en binaire, un peu comme XML (mais sans la surcharge XML).

Exemple:

TT LL VV
02 01 FF

est un entier (balise 02) d'une longueur de la valeur de 1 (longueur 01) et de la valeur -1 (valeur FF). Dans ASN.1 / BER, les entiers sont de grands nombres endiens, mais vous pouvez bien sûr utiliser votre propre format.

TT LL (TT LL VV, TT LL VV VV)
30 07  02 01 FF  02 02 00 FF

est une séquence (une liste) de longueur 7 contenant deux entiers, un avec la valeur -1 et l'autre avec la valeur 255. Les deux codages entiers constituent ensemble la valeur de la séquence.

Vous pouvez également simplement jeter cela dans un décodeur en ligne, n'est-ce pas sympa?


Vous pouvez également utiliser une longueur indéfinie dans BER qui vous permettra de diffuser des données. Dans ce cas, vous devez cependant analyser correctement votre arbre. Je considérerais que c'est un sujet avancé, vous devez connaître d'abord l'analyse en largeur et en profondeur, pour commencer.


L'utilisation d'un schéma TLV vous permet de penser à n'importe quel type de structure de données et de l'encoder. ASN.1 va beaucoup plus loin que cela, vous donnant des identifiants uniques (OID), des choix (un peu comme les unions C), des inclusions d'autres structures ASN.1, etc., etc., mais cela peut être exagéré pour votre projet. Les structures les plus connues définies par ASN.1 sont probablement les certificats utilisés par votre navigateur.

Maarten Bodewes
la source
0

Sinon, vous avez couvert les bases. Vos commandes peuvent être créées et lues par des humains et des machines, ce qui est un gros plus. Vous pouvez ajouter une somme de contrôle pour détecter une commande mal formée ou endommagée pendant le transport, en particulier si votre chaîne comprend un long câble ou une liaison radio.

Si vous avez besoin d'une puissance industrielle (votre appareil ne doit pas causer ou blesser quelqu'un ou mourir; vous avez besoin de débits de données élevés, de la récupération des pannes, de la détection de paquets manquants, etc.), consultez certains des protocoles et pratiques de conception standard.

JRobert
la source