Il y a quelques semaines, j’ai publié un billet démontrant l’importance d’une solution en temps réel pour le support du standard RS-485 sur BeagleBone. Bien qu’il soit possible d’éliminer d’emblée la solution existante de l’UART d’OMAP (le type d’UART présent et supporté sur le SoC des BeagleBone) tel que mentionné dans l’article, j’ai quand même décidé d’y jeter un coup d’oeil.
Je me suis intéressé à la façon dont les auteurs ont décidé d’implémenter le support RS-485, nécessairement du côté pilote, ce support remontant tout juste à la mi-août. Puis, j’ai trouvé un petit bogue de performance qui concerne les fins de transmissions qui représentent une étape cruciale dans l’implémentation du protocole RS-485. J’ai donc soumis un correctif au noyau de Linux; il a été accepté la semaine passée.
Ce billet assez technique devrait donc intéresser les ingénieurs avides du noyau Linux et du standard RS-485.
Analyse du code
En lisant rapidement la nouvelle version de omap-serial.c
après l’ajout du support RS-485, j’ai remarqué que plusieurs propriétés concernant RS-485 peuvent maintenant être ajoutées aux noeuds des UART d’OMAP. Parmi celles-ci, rs485-rts-delay
a retenu mon attention : en passant en diagonale dans le code, j’ai vu des appels à mdelay
qui utilisent les valeurs de cette propriété (un délai avant de transmettre et un après une transmission).
J’ai d’abord pensé qu’il s’agissait là d’une stratégie pour s’assurer d’une transmission complètement terminée : attendre un délai fixé d’avance une fois une interruption de tampon circulaire (FIFO) d’envoi vide reçue. Mais en scrutant plus attentivement le code, j’ai trouvé qu’il s’agit d’un temps d’attente supplémentaire, à ajouter avant et après une transmission, alors que la fin de transmission est connue d’une autre façon. On peut donc fixer les deux valeurs de rs485-rts-delay
à 0 pour atteindre un délai de redressement le plus court possible. En fait, les valeurs de cette propriété sont directement données aux membres delay_rts_before_send
et delay_rts_before_send
d’une instance de struct serial_rs485
, une structure définie par le sous-système sériel en soi.
Le signal de direction des données est assuré par une ligne GPIO réservée par le pilote.
En somme, voici le mécanisme que j’ai décelé lorsque le support RS-485 est activé :
- Lorsque le sous-système sériel de Linux demande au pilote de commencer une transmission, celui-ci active les interruptions de FIFO d’envoi (ou interruptions THR, pour Transmitter Holding Register). Il active également le signal GPIO de direction des données pour prendre le bus RS-485.
- L’UART émet une interruption THR tant et aussi longtemps que sa FIFO d’envoi contient 32 caractères ou moins.
- Lorsque le pilote traite une interruption THR, il ajoute jusqu’à 16 nouveaux caractères, montant potentiellement le niveau de cette FIFO à 48.
- Si le pilote, lors d’une interruption THR, n’a plus rien à fournir à l’UART (aucun nouveau caractère fourni par l’utilisateur), il vérifie si la transmission est terminée (un bit de statut est dédié à cette condition), et :
- si la transmission n’est pas terminée, il laisse l’interruption THR activée;
- si la transmission est terminée, il désactive l’interruption THR et désactive le signal GPIO de direction des données afin de relâcher le bus RS-485.
En se rappelant que l’UART émet une interruption THR aussitôt que sa FIFO d’envoi contient 32 caractères, on constate que le pilote se trouve à scruter le bit indiquant la fin d’une transmission, et ce pendant la durée de transmission de 33 caractères (puisqu’aussitôt que la FIFO d’envoi atteint 32 caractères, c’est qu’il y en a un autre en cours de transmission). En effet, en ne désactivant pas l’interruption, elle est relancée aussitôt puisque la FIFO d’envoi est en train de se vider. À 9600 bauds (avec 1 stop bit et sans parité), on parle d’un blocage d’environ 34 ms pendant lesquelles le microprocesseur ARM est en constant traitement d’interruptions à chaque fin de transmission, évènement qui arrive souvent dans les applications typiques de RS-485, tel que les systèmes d’automation industrielle.
Vérifions ces suppositions physiquement.
Préparation d’un premier test
Dans mon fichier DTS, j’ai spécifié aucun délai additionnel avant et après une transmission. J’ai aussi demandé l’activation de RS-485 dès le chargement du pilote. Cette activation peut aussi se faire par la suite du côté utilisateur avec un appel à ioctl
. Comme un premier UART (nommé UART0) est utilisé pour la console, je teste avec l’UART1 (la puce AM335x comprend 6 UART identiques) :
uart1: serial@48022000 {
pinctrl-names = "default";
pinctrl-0 = <&uart1_pins>;
/* Aucun délai supplémentaire avant/après transmission */
rs485-rts-delay = ;
/* Broche 17 de GPIO1 pour direction des données */
rts-gpio = <&gpio1 17 GPIO_ACTIVE_HIGH>;
/* Broche 16 de GPIO1 pour tester */
test-gpio = <&gpio1 16 GPIO_ACTIVE_HIGH>;
/* Activer RS-485 */
linux,rs485-enabled-at-boot-time;
status = "okay";
};
Le symbole uart1_pins
fait référence à cette configuration de multiplexage des broches :
uart1_pins: pinmux_uart1_pins {
pinctrl-single,pins = < 0x180 (PIN_INPUT_PULLUP | MUX_MODE0) /* uart1_rxd */ 0x184 (PIN_OUTPUT_PULLDOWN | MUX_MODE0) /* uart1_txd */ 0x044 (PIN_OUTPUT_PULLDOWN | MUX_MODE7) /* gpio1_17 */ 0x040 (PIN_OUTPUT_PULLDOWN | MUX_MODE7) /* gpio1_16 */ >;
};
La broche 17 du contrôleur GPIO1 est disponible sur le port d’extension P9, juste à côté de la sortie Tx de l’UART1, sur le BeagleBone blanc que j’utilise pour tester (broches 23 et 24 du port d’extension). Afin de vérifier la fréquence des interruptions de FIFO d’envoi, j’ai également ajouté au tout une autre sortie GPIO (broche 16 de GPIO1, donc broche 15 du port d’extension) qui émettra une très courte pulsation à chaque appel à transmit_chars
(qui est appelée lors d’une interruption THR).
J’utilise l’analyseur logique Saleae Logic pour vérifier avec précision la sortie de l’UART et le délai de redressement RS-485. Voici mon installation :
La sonde orange est branchée sur la sortie Tx de l’UART tandis que la sonde rouge est connectée au signal de direction des données RS-485. La sonde bleue analyse les pulsations d’interruption THR.
Maintenant, du côté utilisateur sur le BeagleBone :
# stty -F /dev/ttyO1 115200 cs8
# while true; do
echo -n '0123456789abcdef0123456789abcdefWXYZ0123456789abcdef0123456789abcdef' > /dev/ttyO1
done
Le temps requis d’une itération à l’autre pour ouvrir, paramétrer et fermer le port /dev/ttyO1
est suffisant pour bien délimiter chaque petite transmission. La partie WXZY
de la transmission sépare ici le début de la transmission des 32 derniers caractères.
Premier test
Avec le logiciel de Saleae, je fais une lecture de 5 millions d’échantillons à 12 MHz :
On remarque donc le comportement mentionné plus haut. Au tout début d’une transmission, trois interruptions ont lieu très rapidement :
- La première ajoute 16 caractères à la FIFO d’envoi vide de l’UART. Celui-ci continue d’émettre une interruption THR parce que sa FIFO d’envoi contient moins de 33 caractères. De plus, il commence immédiatement la transmission du premier caractère, faisant passer la FIFO d’envoi à 15 caractères.
- La deuxième ajoute encore 16 caractères, ce qui monte le niveau de la FIFO d’envoi à 31 caractères. Encore une fois, la FIFO d’envoi contient 32 caractères ou moins, donc l’UART continue l’émission d’interruption THR.
- Enfin, la troisième ajoute 16 autres caractères et l’UART n’a plus faim pour l’instant : le compte est à 47 caractères.
La FIFO d’envoi doit alors se vider de 15 caractères avant l’émission de la prochaine interruption, ce qu’on observe avec les caractères 1
à f
. Aussitôt que le caractère f
est transféré de la FIFO d’envoi au registre à décalage de transmission, la FIFO passe à 32 caractères, d’où la pulsation constatée avant la transmission de f
.
Ce cycle se répète et à un certain moment, seulement 4 caractères (à cause de l’ajout des 4 caractères WXYZ
) peuvent être ajoutés parce que le tampon du côté pilote n’a plus rien à fournir. Ces caractères sont les cdef
qui seront transmis en dernier. Lors de la prochaine interruption, avant la transmission de Z
, le tampon du pilote est vide et celui-ci entre dans la boucle de scrutage mentionnée plus haut pendant la durée de transmission de 33 caractères. Les gros rectangles blancs à la fin représentent en réalité un torrent d’interruptions, plus facilement visibles lorsqu’on se rapproche :
Puis, le dernier bit est transmis. Cette condition étant remplie, le pilote désactive enfin les interruptions THR et le signal GPIO de direction des données RS-485.
Comme vous pouvez le voir sur la capture d’écran ci-haut, le délai de redressement RS-485 est plutôt intéressant (beaucoup plus court que la durée d’un caractère). En scrutant constamment l’état de fin de transmission grâce au relancement de la fonction gérant l’interruption, le pilote arrive à obtenir un très petit délai entre la fin effective d’une transmission et la mise à jour du signal GPIO de direction des données.
Amélioration de omap-serial.c
Le comportement du pilote d’UART montré ci-haut peut être amélioré. L’UART d’OMAP est compatible 16C750, un type d’UART bien connu, mais il ajoute un registre de contrôle supplémentaire, SCR (Supplementary Control Register), dont le bit TMEMPTYCTLIT est particulièrement intéressant si activé : THR interrupts is generated when TX FIFO and TX shift register are empty. C’est exactement l’évènement qui nous intéresse.
Évidemment, on ne veut pas être interrompu seulement à la fin d’une transmission lorsque notre tampon du côté pilote n’est pas vide : la transmission doit être continue, sans avoir de « trou ». Il suffit donc d’activer TMEMPTYCTLIT lorsque nous recevons une interruption THR (la FIFO d’envoi a atteint le seuil) et que le pilote n’a plus rien à fournir à l’UART, ce qui signifie qu’une fin de transmission est imminente. Dans ce cas, il faut également éviter de désactiver les interruptions THR. La prochaine interruption THR devrait donc nous signaler que la transmission est complète, auquel cas nous pouvons désactiver les interruptions THR en toute quiétude.
Résultat? Constatez la différence :
Si, pendant la transmission des 32 derniers caractères, une nouvelle transmission est demandée au pilote, TMEMPTYCTLIT est désactivé et cela provoque instantanément une interruption THR puisque le niveau de la FIFO d’envoi est sous les 33 caractères. Ce comportement est désirable afin d’éviter un trou entre deux transmissions consécutives indépendantes.
Bien que l’UART d’OMAP semble supporter le standard RS-485 avec un délai de redressement respectable ici, il faut garder en tête que ce n’est pas une solution temps réel pour autant. Le temps de redressement dépend toujours de la disponibilité du pilote et n’est donc aucunement garanti. Si plusieurs autres interruptions plus prioritaires sont en cours de traitement, celle du pilote de l’UART d’OMAP subira une latence plus importante et le délai de redressement RS-485 sera nécessairement plus grand.
Bonjour
suite a la lecture de votre article j’ai des questions a vous poser
car je dois configuré en Pic 18F en langage C
Merci de votre aide
Bonjour Arnaud. Je ne sais pas si Philippe a bien reçu votre message, mais je vais m’en assurer. Surveillez votre boîte de réception. 🙂