RS-485 Half-duplex sous Linux

Publié par cpb
Sep 03 2012

RS-485 half-duplex sous LinuxLa norme RS-485 (EIA-485 de son nom officiel) définit des communications séries rappelant celles de la norme RS-232 bien connue mais disposant de beaucoup moins de signaux de contrôle. Son utilisation sous Linux est un peu plus compliquée que ce que je pensais initialement.

RS-485

Dans la panoplie des liaisons séries couramment utilisées dans le monde industriel, il existe essentiellement 3 normes. L’EIA-232 (la liaison série RS-232 classique, que l’on rencontre encore sur la plupart des PC industriels) utilisant trois fils pour transporter les données et jusqu’à six fils pour les signaux de contrôle de la communication, l’EIA-422 (ou RS-422) qui permet de dialoguer sur quatre fils en utilisant des signaux différentiels, peu sensibles aux parasites, et l’EIA-485 (RS-485) qui est une version proche de la RS-422 capable de fonctionner en mode full-duplex (émission et réception simultanées) sur quatre fils ou en mode half-duplex (émission et réception successives mais pas simultanées) sur deux fils. C’est cette dernière version qui m’intéresse ici.

J’ai eu récemment à faire à un PC industriel comportant six ports série dont deux prévus pour communiquer en RS-485 et les quatre autres en RS-232. Il était tous accessibles via des connecteurs DE-9 habituels numérotés de 1 à 6. Je devais installer une application qui recevait des données sur l’un des ports RS-485. Ceci ne fonctionnait pas, alors que les tests sur des ports RS-232 réussissaient parfaitement.

Il faut savoir que contrairement à la norme RS-232, il n’existe aucun standard de câblage du connecteur DE-9 pour la RS-485. Chaque constructeur propose sa propre disposition des signaux. Non sans mal, j’ai pu obtenir le schéma chez le fournisseur du matériel (Unicorn Computer Corp.). À titre indicatif, le voici:

Schéma DB-9 RS-485

Simple, n’est-ce pas ? Et pourtant rien ne marchait. Lorsque mon application démarrait, je voyais à l’oscilloscope varier la tension de référence des broches 1 et 3, mais je ne recevais aucune donnée, alors qu’un flux d’informations transitait entre les lignes Data, bien visible sur l’écran de l’oscilloscope. Ce qui était d’autant plus énervant, c’est qu’une autre application utilisait en émission le port RS-485 d’un autre PC du même type sans aucun souci.

Analyse du problème

Après avoir retourné les éléments du problème pendant longtemps, accusant successivement mon application,  le câblage, la configuration des ports dans le Bios, et même les drivers kernel, j’ai essayé de reprendre l’historique des programmes que j’avais déjà développés utilisant des ports RS-485. Quelques-uns me sont revenus.

  • un outil d’enregistrement et d’analyse de trames de géolocalisation sous Ms-Dos recevant des données depuis une radio sur un port série RS-485,
  • une application Linux recevant des alarmes en UDP/IP et les renvoyant vers un central en RS-485,
  • un outil sous Ms-Dos de centralisation d’alarmes avec enregistrement et impression,
  • un simulateur sous Linux qui envoyait des données de positionnement de véhicules depuis des scénarios construits par l’utilisateur,
  • cette application sous Linux, enfin, qui supervise un chantier en recevant des trames radio par port RS-485 (et qui ne marchait pas).

Pour les applications qui fonctionnaient sous Linux, rien de spécifique n’avait été nécessaire par rapport à un port série RS-232. Il suffisait d’ouvrir le port /dev/ttySxxx, fixer une vitesse de fonctionnement, une parité etc. à l’aide de la fonction tcsetattr() et d’effectuer des écritures sur le port en question. Je me suis aperçu alors que les programmes qui fonctionnaient sous Linux faisaient tous de l’émission de données. Ceux qui recevaient des informations depuis un port RS-485 avaient toujours tourné sous Ms-Dos, en utilisant une API un peu différente, basée sur une bibliothèque spécifique pour cette interface. Il devait sûrement y avoir une opération particulière à réaliser pour mettre le port en écoute ou en diffusion. Et pourtant, même avec cet indice, il m’a fallu un long moment avant de trouver les informations nécessaires.

Explication

Le port RS-485 fonctionnant en mode half-duplex, il faut disposer d’un moyen de choisir entre l’émission ou la réception de données. Électriquement ceci est géré par un état dit idle, inactif, où les deux signaux D+ et D- sont tous deux à zéro. Le protocole inclut une gestion des collisions (si les deux extrémités essayent de parler en même temps). Toutefois le basculement de la lecture à l’écriture doit être pris en charge par les couches hautes du protocole.

Sur la plupart des PC incluant des ports RS-485, se trouvent en réalité installés des convertisseurs qui emploient les signaux RS-232 (ramenés dans l’intervalle [0-5V] ou [0-3,3V]) issus de la carte mère. Et pour savoir dans quel sens les données doivent transiter, ces convertisseurs emploient le signal RTS (Request To Send) qui est activé lorsque le port doit émettre des données et inhibé sinon.

Le problème est que dès l’ouverture d’un port série, Linux active le signal RTS. Même si l’ouverture a lieu en lecture seule ! Vérifions-le à l’aide du petit programme suivant qui ouvre un port spécifié en argument en lecture seule, puis attend indéfiniment.

Ce test a lieu sur un port RS-232, afin que le signal RTS soit visible sur le connecteur DB-9 de sortie.

test-open.c:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>

int main (int argc, char * argv[])
{
    int    fd_port;
    struct termios parametres;

    if (argc < 2) {
        fprintf(stderr, "usage: %s <serial-port>n", argv[0]);
        exit(EXIT_FAILURE);
    }

    fd_port = open(argv[1], O_RDONLY | O_NONBLOCK);
    if (fd_port < 0) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }
    if (tcgetattr(fd_port, & parametres) != 0) {
        perror("tcgetattr");
        exit(EXIT_FAILURE);
    }
    cfmakeraw(& parametres);
    parametres.c_iflag  = 0;
    parametres.c_oflag  = 0;
    parametres.c_cflag |= CLOCAL;
//  parametres.c_cflag &= ~CLOCAL;
    parametres.c_cflag |= CRTSCTS;
//  parametres.c_cflag &= ~CRTSCTS;
    if (tcsetattr(fd_port, TCSANOW, & parametres) != 0) {
        perror("tcsetattr");
        exit(EXIT_FAILURE);
    }
    pause();
    return EXIT_SUCCESS;
}

On peut jouer à activer et désactiver les options CLOCAL (ignorer les signaux de contrôle du modem) et CRTSCTS (gérer les signaux RTS/CTS), rien n’y fait : dans tous les cas le signal RTS est activé dès l’ouverture du port, même en lecture seule. Pour le vérifier, nous mesurons la tension entre les broches 5 (Gnd) et 7 (Rts) du connecteur DE-9.

Signal RTS actif sur port RS-232

Nous mesurons une tension de 6.88 volts, ce qui correspond à l’activation de RTS suivant le standard RS-232 (qui tolère entre +3 et +15 V pour l’activation d’une ligne de contrôle).

Solution

Revenons à notre liaison RS-485 half-duplex. Nous devons contrôler manuellement le signal RTS, afin de pouvoir l’inhiber lorsque nous désirons écrire et l’activer lorsqu’on veut lire des données. Pour cela, nous allons faire appel à un ioctl().

Il existe deux numéros d’ioctl susceptibles de nous intéresser : TIOCMGET qui permet de lire l’état des signaux de contrôle (le M indiquant qu’il s’agit de signaux destinés aux modems) et TIOCMSET qui permet d’écrire leur nouvel état. Naturellement certains signaux seront uniquement accessibles en lecture. Les noms symboliques associés aux signaux sont les suivants.

  • TIOCM_CTS (Clear to send) nous avons l’autorisation d’écrire,
  • TIOCM_DCD (Data Carier Detect) la porteuse est présente sur le modem,
  • TIOCM_DSR (Data Set Ready) notre correspondant est disponible,
  • TIOCM_DTR (Data Terminal Ready) nous sommes prêts à recevoir,
  • TIOCM_RI (Ring Indicator) appel entrant sur le modem,
  • TIOCM_RTS (Request to Send nous voulons écrire.

Les signaux que nous pouvons configurer sont donc RTS et DTR. Le programme suivant agit sur RTS en le basculant au niveau bas après ouverture du port.

test-open-rts-bas.c

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>

#include <sys/ioctl.h>

void set_rts (int fd, int actif);

int main (int argc, char * argv[])
{
    int    fd_port;
    struct termios parametres;

    if (argc < 2) {
        fprintf(stderr, "usage: %s <serial-port>n", argv[0]);
        exit(EXIT_FAILURE);
    }

    fd_port = open(argv[1], O_RDONLY | O_NONBLOCK);
    if (fd_port < 0) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }
    if (tcgetattr(fd_port, & parametres) != 0) {
        perror("tcgetattr");
        exit(EXIT_FAILURE);
    }
    cfmakeraw(& parametres);
    parametres.c_iflag  = 0;
    parametres.c_oflag  = 0;
    parametres.c_cflag |= CLOCAL;
    parametres.c_cflag &= ~CRTSCTS;
    if (tcsetattr(fd_port, TCSANOW, & parametres) != 0) {
        perror("tcsetattr");
        exit(EXIT_FAILURE);
    }
    set_rts(fd_port, 0);
    pause();
    return EXIT_SUCCESS;
}

void set_rts (int fd, int actif)
{
    int bits;
    ioctl(fd, TIOCMGET, & bits);

    if (actif)
        bits |= TIOCM_RTS;
    else
        bits &= ~TIOCM_RTS;
    ioctl(fd, TIOCMSET, & bits);
}

Exécutons-le à nouveau sur un port RS-232 afin de surveiller le signal RTS.
Signal RTS inactif sur port RS-232
Cette fois la tension est de -6.51V, ce qui est interprété suivant le standard RS-232 comme une désactivation de RTS (les valeurs dans l’intervalle [-15V, -3V] sont considérées comme des désactivation pour les lignes de contrôle).

Conclusion

En agissant directement sur le signal RTS d’un port série grâce à un ioctl, il est possible d’indiquer si ce port veut émettre, et ainsi de sélectionner le sens de fonctionnement dans le cas de communication half-duplex comme c’est le cas sur un port RS-485.

8 Réponses

  1. David dit :

    Bonjour,

    il y a une petite inversion :
    le 1 logique en RS-232 est une tension négative (comprise entre -3V et -15V)
    et le 0 est une tension positive (comprise entre 3V et 15V).
    Et non l’inverse 😉

    • cpb dit :

      Je crois que cette inversion ne s’applique que pour les lignes de données (TX/RX) pas pour les signaux de contrôle. Je vais vérifier et rectifier le cas échéant.

      • cpb dit :

        J’ai vérifié : les signaux de contrôle sont considérés comme actifs au niveau bas (0 logique, tension positive) et inhibés au niveau haut (1 logique, tension négative). Je supprime la notion de 0 et 1 logiques dans l’article. Merci.

  2. esa dit :

    Bonjour,
    Je suis bloqué depuis un certain temps sur cette même question : comment contrôler la direction d’un port RS485 sous LINUX (pour Raspberry Pi).

    Sous DOS, j’avais réalisé le Drivers RS232 en C et je pouvais contrôler ce signal via interruption.
    Activation avant le début démission et désactivation contrôler par interruption lorsque le buffer du port est vide.

    J’ai donc un intérêt certain pour ton post!

    Question : est ce que ta solution pourrai fonctionner par interruption : càd, détecter la fin d’émission (buffer vide) et de basculer la ligne RTS?

    J’ai aussi regardé un autre post intéressant, car sur le RaspberryPi, il semble que la solution via la ligne RTS n’est pas fiable (basculement non déterministe)

    Voir réponse de « DAmesberger » qui a créer un module sur le port SPI pour contourner le problème (http://www.amescon.com/forum.aspx).

    http://www.raspberrypi.org/phpBB3/viewtopic.php?f=44&t=35205

    Une dernière solution serait d’écrire un DRIVER pour le Kernel pour le port RS232 et donc de rajouter le contrôle de la direction d’un RS485.
    Cela me semble complexe.

    Question : je suis un peu perdu dans tout cela, car je pensais que le signal RTS devrait normalement basculer comme désiré et convenir avec le Drivers RS232 de « base ».

    Quelle voie choisir?

    Mon but est de réaliser un contrôleur MODBUS.

    Merci pour ton aide.

  3. Clément dit :

    Cet article est salvateur, je le conseille plus que fortement à tous les ingés qui s’arrachent les cheveux sur du RS485 halfduplex en Linux embarqué.
    C’est d’ailleurs la seule référence sérieuse que j’ai trouvé sur le net.

    Merci 😉

    • cpb dit :

      Aujourd’hui oui.
      Ce comportement s’est généralisé dans les drivers UART à partir de Linux 4.x (2015).
      L’article date de 2012 😉

URL de trackback pour cette page