Linux Fu : le port série infini

Bon, le titre est un peu trompeur. Comme la plupart des choses dans la vie, ce n’est vraiment pas infini. Mais je vais vous montrer comment vous pouvez utiliser une fonctionnalité Linux très intéressante pour transformer un port série d’un microcontrôleur en un ensemble de ports virtuels. En théorie, vous pourriez créer plus de 200 ports, mais la réalité est que vous voudrez probablement vous en tenir à moins.

La fonctionnalité en question est ce qu’on appelle un pseudoterminal ou parfois un pty ou alors pts. Ces fichiers spéciaux ont été créés pour fournir des données aux programmes qui s’attendent à accepter les données d’un terminal. Les fichiers offrent deux visages. Pour le client, il ressemble à n’importe quel autre terminal. Pour le créateur, cependant, ce n’est qu’un autre fichier. Ce que vous écrivez dans ce fichier va au faux terminal et vous pouvez lire tout ce qui est envoyé depuis le programme connecté au terminal. Vous les utilisez tout le temps, probablement, sans vous en rendre compte puisque l’exécution d’un shell sous X Windows, par exemple, ne s’attache pas à un vrai terminal, après tout.

Vous pouvez, bien sûr, faire la même chose avec un périphérique USB composite, en supposant que vous en ayez un. En supposant également que vous puissiez trouver un pilote fonctionnel et le faire fonctionner. Cependant, de nombreux microcontrôleurs ont un port série – même un avec un convertisseur USB intégré – mais moins ont un matériel USB à part entière. Même ceux qui le font sont souvent en désaccord avec des pilotes étranges côté PC. Les ports série fonctionnent et fonctionnent bien même sur les microcontrôleurs les plus simples.

Le plan

Le plan est assez simple. Un programme Linux écoute un vrai port série et surveille les séquences de caractères spéciaux dans le flux de données. Ces séquences vous permettront de basculer les données afin que le flux de données aille vers un terminal particulier. Les données revenant des terminaux iront au vrai port série après avoir envoyé une séquence qui identifie sa source.

J’ai construit un exemple artificiel parce qu’il y avait certaines fonctionnalités clés que je voulais tester. Sur le microcontrôleur, un thread lit une grandeur analogique et un autre une grandeur numérique. Le système les imprime tous les deux sur des ports série virtuels séparés. Si vous appuyez sur une touche dans l’un ou l’autre des terminaux, les données associées seront mises en pause. Il y a aussi un terminal de débogage et un terminal de commande qui prend l’entrée de l’utilisateur pour modifier des choses comme la fréquence d’échantillonnage des données.

Le code, au fait, est sur GitHub. Si vous n’utilisez pas un Blackpill STM32F411 avec Mbed, vous aurez probablement du travail à faire. Cependant, gardez à l’esprit que la complexité du code doit s’adapter aux bibliothèques Mbed. Le protocole actuel est simple et vous pouvez l’implémenter n’importe où.

Le protocole

En parlant de protocole, il est à la fois léger et robuste. Chaque appareil est à la fois un émetteur et un récepteur et il y a très peu de connexion entre eux. Ainsi, lorsqu’un appareil envoie à un canal virtuel, il peut recevoir des données d’un canal virtuel complètement différent. Chaque canal virtuel a un numéro d’identification qui peut aller de 0 à 253.

L’émetteur envoie la plupart des octets de données directement sur le port. Lorsqu’il y a un désir de changer de ports virtuels, l’émetteur envoie un octet FF suivi du numéro de canal. Il y a quelques problèmes à considérer.

Tout d’abord, l’émetteur doit être conscient que s’il veut vraiment envoyer un FF, il ne peut pas. Il envoie donc FF FE à la place. Si vous n’envoyiez que des caractères FF, cela doublerait la quantité de données envoyées, mais c’est une situation relativement rare.

Deuxièmement, il y a un problème de robustesse si l’émetteur envoie un FF puis meurt. L’octet suivant pourrait être considéré comme un numéro de canal. Cependant, étant donné qu’un émetteur fraîchement redémarré doit envoyer un sélecteur de canal initial, l’octet suivant doit être un FF (le sélecteur de transmission initial). Pour lutter contre cela, le protocole accepte n’importe quel nombre d’octets FF comme préfixe légitime. Ainsi, tous les éléments suivants sélectionneront le canal 4 :

FF 04
FF FF 04
FF FF FF FF FF FF 04

Le seul problème est si la connexion série est intermittente. Il est possible d’obtenir un octet FF, de faire sortir temporairement le câble, puis d’obtenir un octet ultérieur qui déclenchera un changement de canal indésirable. Vous ne pouvez pas faire grand-chose à ce sujet, même si cela suppose que l’octet suivant est un numéro de canal légitime, car la plupart des systèmes n’utiliseront pas tous les canaux possibles. Vous pouvez ajouter une certaine robustesse en temporisant le préfixe d’échappement ou en ajoutant, par exemple, une somme de contrôle à la séquence de commutation, mais cela ne ferait que réduire les problèmes et non les éliminer. Si vous traitez des données ASCII pures, limiter les canaux au-dessus de 0x80 réduirait le problème mais, encore une fois, ne l’éliminerait pas totalement. En pratique, si vous disposez d’une connexion série fiable, il n’y a pas de problème.

Troisièmement, il y a le cas où le récepteur meurt et recommence en cours de route. Cela peut entraîner un problème où certaines données sont transmises au mauvais terminal virtuel. Il existe deux fonctionnalités pour vous aider. Premièrement, si l’émetteur envoie FF FD au récepteur, le récepteur doit retransmettre son sélecteur de canal actuel. Vous pouvez également configurer un émetteur pour qu’il envoie périodiquement le sélecteur de canal car il est inoffensif d’envoyer le même sélecteur plus d’une fois.

Une autre façon de lutter partiellement contre cela consiste à utiliser le -s allumez le serveur Linux pour empêcher toute donnée de circuler vers les terminaux virtuels jusqu’à ce qu’un soit explicitement sélectionné. Notez que cela n’a d’importance que pendant la phase de démarrage du serveur. Une fois qu’un canal est sélectionné, il reste sélectionné jusqu’à ce qu’un autre soit sélectionné.

BAISER

Vous pouvez probablement voir que toute la complexité ici consiste à faire en sorte que le serveur s’intègre à Linux et que le côté microcontrôleur s’intègre aux appels de la bibliothèque C. Si vous vouliez envoyer des données depuis, disons, un Arduino, il serait facile de tout préfixer avec « xFFxCC » où CC est le numéro de canal. Recevoir des données pourrait être presque aussi simple. Vous devez juste vous rappeler quand vous avez vu un FF et gérer les trois codes d’échappement (c’est-à-dire que FE est un vrai FF, FD est une demande de renvoi du sélecteur de canal et tout le reste est un changement de canal).

Cependant, je voulais pouvoir n’avoir que des ports série virtuels des deux côtés du câble, donc c’était beaucoup plus de travail. Cela tient en partie au fait que travailler avec les flux d’E/S Mbed est au mieux excentrique. Tout d’abord, regardons le serveur Linux.

Le serveur Linux

Le logiciel Linux ouvre un port terminal (par exemple, /dev/ttyUSB0 ou alors /dev/ttyACM0 etc.) et produit ensuite plusieurs pseudo-terminaux que la plupart des logiciels de terminaux peuvent utiliser. Pour les périphériques USB, le débit en bauds est probablement sans importance. Cependant, pour un vrai périphérique série, vous devez probablement faire correspondre les débits en bauds (non testés). Le code ne modifie actuellement pas le débit en bauds, vous devez donc le définir en externe ou modifier le code si nécessaire.

Le programme ttymux prend quelques options. La seule critique est l’option -c qui définit un port virtuel. Chaque port a un numéro d’identification de 0 à 253. Vous pouvez également demander un lien symbolique. Ainsi, par exemple, regardez cette commande :

ttymux -c 10 -c 33:virtualportA -c 50:/tmp/portB /dev/ttyACM0


Ici, nous créons trois ports. Le port #10 n’a pas de nom. Les ports 33 et 50 seront respectivement dans ./virtualportA et /tmp/portB.

Autres options:

  • -d – Suppression automatique des liens symboliques à la sortie
  • -n – Ne pas définir d’attributs sur le port série
  • -s – Ne pas envoyer de données à un port virtuel jusqu’à ce qu’il soit expressément sélectionné

Lorsque le programme s’exécute, vous verrez une liste de chaînes et leurs pseudoterminaux associés (probablement /dev/pts/X où X est un nombre). Si vous ne fournissez pas de lien symbolique, c’est ainsi que vous vous connectez au port virtuel. Si vous fournissez un lien symbolique, vous pouvez utiliser l’un ou l’autre. Notez que le numéro d’identification n’est pas le même que le numéro de point. Ainsi, le canal 10 dans l’exemple ci-dessus ne sera probablement pas /dev/pts/10. Si c’est le cas, ce n’est qu’une coïncidence.

Pour compiler, vous avez besoin de pthreads :

g++ -o ttymux ttymux.cpp -lpthread

En regardant le code, le serveur n’est pas vraiment si complexe. Il y a deux fils. L’un lit le port série et l’autre y écrit. Rien d’autre ne touche le vrai port série. Ensuite, chaque canal virtuel a son propre pty. Il n’est pas nécessaire de tamponner des caractères ou quoi que ce soit.

La partie qui crée le pty est très simple :


pty=posix_openpt(O_RDWR|O_NOCTTY|O_NONBLOCK);
if (pty==-1) return -1;
grantpt(pty);
unlockpt(pty);


Pour créer les liens symboliques, vous devez connaître le nom du pty auquel correspond l’appel getptyname. L’utilisation de ce nom de fichier pour l’ouvrir vous donnera le côté terminal du pty. J’utilise habituellement picocom qui n’a aucun problème. Mais certains programmes, comme cutecom, par exemple, en savent trop sur ce à quoi un port série est censé ressembler, vous ne pouvez donc pas ouvrir de pty avec eux.

Le microcontrôleur

Le code du microcontrôleur est un peu plus compliqué. La classe SerialMux fait tout le travail. Comme le côté Linux, il a deux threads qui contrôlent l’accès au port réel. Contrairement au côté Linux, chaque objet de port série virtuel possède son propre ensemble de tampons d’entrée et de sortie. Les threads remplissent ces tampons et – éventuellement – _read et _write faire le travail d’obtention des données dans et hors de la classe de base du flux Mbed. Ce n’est pas très efficace car les flux finissent par appeler _putch et _getch pour faire des E/S caractère par caractère, mais il y a des raisons à cela et si vous vouliez vraiment le modifier, vous pouvez remplacer les méthodes sous-jacentes.

Une chose qui m’a surpris, c’est que revenir vrai pour le isatty fonction casse totalement le système d’E/S. Je n’ai pas compris pourquoi, mais j’ai noté que la classe USBSerial standard d’usine renvoie également false pour cela et a un commentaire à ce sujet dans le code source Mbed.

Une autre chose qui était étrange est que le mutex intégré du flux agissait étrangement et je n’ai jamais compris exactement pourquoi. Il se peut qu’il protège tous les flux ou quelque chose, mais j’ai finalement recommencé à avoir mon propre mutex pour protéger chaque ensemble de tampons et un autre mutex pour protéger le port série physique.

Une fois que vous suivez la logique, la classe SerialMux n’est pas si difficile. Une grande partie du code dans main.cpp consiste à contourner les problèmes lorsque le port USB est déconnecté.

J’ai également copié et modifié une classe que j’ai déjà utilisée pour créer des systèmes de traitement de commandes. Probablement exagéré pour cela, mais je l’avais traîné et c’était facile à utiliser. Il est intéressant que le code ne sache rien du système multiplex. Cela prend juste un flux normal et fonctionne bien.

Utilisé

Une fois que vous avez chargé le programme du microcontrôleur, vous pouvez exécuter le serveur Linux avec cette ligne de commande :

ttymux -s -c 1:analogport.virtual -c 2:digitalport.virtual -c 100:debugport.virtual -c 10:cmdport.virtual

J’utilise ensuite picocom sur chaque port virtuel, bien que vous puissiez probablement utiliser n’importe quel autre programme de terminal. Vous souhaiterez définir certaines options pour le terminal de commande pour votre propre bénéfice :

picocom -c --omap crlf cmdport.virtual

Dans les terminaux analogiques et numériques, appuyez sur n’importe quelle touche sauf un espace pour mettre la boucle en pause. Un espace va reprendre. Dans la fenêtre de commande, vous pouvez utiliser la commande help pour obtenir une liste de ce que vous pouvez faire. Ne vous attendez pas à ce que le retour arrière fonctionne, bien qu’il existe des moyens de contourner cela si vous vouliez résoudre ce problème.

Directions futures

Le protocole est simple même si l’exemple ne l’est pas et il y a de nombreux ports possibles. Même de simples microcontrôleurs pourraient émuler de nombreux ports.

Côté serveur, quelqu’un demandera sûrement Windows. Il semble que vous pourriez faire quelque chose de similaire avec com0com ou l’un de ses projets connexes. D’autres idées seraient d’alimenter des sockets réseau au lieu de pseudo-terminaux ou d’élaborer un système pour dupliquer de faux ports. Le protocole et le code sont suffisamment simples pour rendre tout cela possible.

Il est vrai que les périphériques composites USB sont une meilleure réponse à ce problème si vous disposez du matériel et des pilotes pour le prendre en charge. Cependant, cela représente beaucoup plus de travail, beaucoup plus de ressources et limite la capacité du matériel bon marché à participer.

Si vous êtes intéressé par la route USB, il existe quelques exemples d’utilisation d’un Pi Zero et du code de l’espace utilisateur si vous pouvez fournir vos propres pilotes Windows. Si l’angle du réseau suscite votre intérêt, vous pourriez essayer ser2net.