Comment ouvrir, lire et écrire à partir du port série en C?

139

Je suis un peu confus sur la lecture et l'écriture sur un port série. J'ai un périphérique USB sous Linux qui utilise le pilote de convertisseur de périphérique série USB FTDI. Lorsque je le branche, il crée: / dev / ttyUSB1.

Je pensais qu'il serait simple d'ouvrir et de lire / écrire à partir de celui-ci en C. Je connais le débit en bauds et les informations de parité, mais il semble qu'il n'y ait pas de norme pour cela?

Est-ce que je rate quelque chose ou quelqu'un peut-il m'indiquer la bonne direction?

gnychis
la source
18
Avez-vous jeté un œil au Serial Programming HOWTO ?
ribram
1
EDIT: Je regarderais le lien de ribram. Cependant, le fait demeure que si un périphérique série est représenté sous forme de fichier, les périphériques ont souvent des interfaces plus spécifiques implémentées via des appels système tels que ioctlet fcntl.
M. Shickadance
1
Comprendre les termios UNIX VMIN et VTIME est une excellente ressource pour comprendre VTIME et VMIN qui sont utilisés pour gérer les caractéristiques de blocage d'un read () sur un port série.
flak37
N'utilisez pas le code du "Serial Programming HOWTO" de Frerking comme mentionné dans le premier commentaire. Ils ne sont pas écrits pour être compatibles POSIX, de sorte que les exemples de code ne sont pas portables et peuvent ne pas fonctionner de manière fiable pour vous.
sciure

Réponses:

247

J'ai écrit ceci il y a longtemps ( des années 1985-1992, avec juste quelques modifications depuis ), et il suffit de copier et coller les éléments nécessaires dans chaque projet.

Vous devez faire appel cfmakerawà un ttyobtenu de tcgetattr. Vous ne pouvez pas mettre à zéro un struct termios, le configurer, puis définir le ttyavec tcsetattr. Si vous utilisez la méthode zéro-out, vous rencontrerez des pannes intermittentes inexpliquées, en particulier sur les BSD et OS X. Les «pannes intermittentes inexpliquées» incluent la suspension read(3).

#include <errno.h>
#include <fcntl.h> 
#include <string.h>
#include <termios.h>
#include <unistd.h>

int
set_interface_attribs (int fd, int speed, int parity)
{
        struct termios tty;
        if (tcgetattr (fd, &tty) != 0)
        {
                error_message ("error %d from tcgetattr", errno);
                return -1;
        }

        cfsetospeed (&tty, speed);
        cfsetispeed (&tty, speed);

        tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;     // 8-bit chars
        // disable IGNBRK for mismatched speed tests; otherwise receive break
        // as \000 chars
        tty.c_iflag &= ~IGNBRK;         // disable break processing
        tty.c_lflag = 0;                // no signaling chars, no echo,
                                        // no canonical processing
        tty.c_oflag = 0;                // no remapping, no delays
        tty.c_cc[VMIN]  = 0;            // read doesn't block
        tty.c_cc[VTIME] = 5;            // 0.5 seconds read timeout

        tty.c_iflag &= ~(IXON | IXOFF | IXANY); // shut off xon/xoff ctrl

        tty.c_cflag |= (CLOCAL | CREAD);// ignore modem controls,
                                        // enable reading
        tty.c_cflag &= ~(PARENB | PARODD);      // shut off parity
        tty.c_cflag |= parity;
        tty.c_cflag &= ~CSTOPB;
        tty.c_cflag &= ~CRTSCTS;

        if (tcsetattr (fd, TCSANOW, &tty) != 0)
        {
                error_message ("error %d from tcsetattr", errno);
                return -1;
        }
        return 0;
}

void
set_blocking (int fd, int should_block)
{
        struct termios tty;
        memset (&tty, 0, sizeof tty);
        if (tcgetattr (fd, &tty) != 0)
        {
                error_message ("error %d from tggetattr", errno);
                return;
        }

        tty.c_cc[VMIN]  = should_block ? 1 : 0;
        tty.c_cc[VTIME] = 5;            // 0.5 seconds read timeout

        if (tcsetattr (fd, TCSANOW, &tty) != 0)
                error_message ("error %d setting term attributes", errno);
}


...
char *portname = "/dev/ttyUSB1"
 ...
int fd = open (portname, O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0)
{
        error_message ("error %d opening %s: %s", errno, portname, strerror (errno));
        return;
}

set_interface_attribs (fd, B115200, 0);  // set speed to 115,200 bps, 8n1 (no parity)
set_blocking (fd, 0);                // set no blocking

write (fd, "hello!\n", 7);           // send 7 character greeting

usleep ((7 + 25) * 100);             // sleep enough to transmit the 7 plus
                                     // receive 25:  approx 100 uS per char transmit
char buf [100];
int n = read (fd, buf, sizeof buf);  // read up to 100 characters if ready to read

Les valeurs de vitesse sont B115200, B230400, B9600, B19200, B38400, B57600, B1200, B2400, B4800, etc. Les valeurs de parité sont 0( ce qui signifie pas de parité), PARENB|PARODD(activer la parité et l' utilisation impair), PARENB(activer la parité et l' utilisation même), PARENB|PARODD|CMSPAR(parité de marque) et PARENB|CMSPAR( parité spatiale).

"Blocking" définit si un read()sur le port attend que le nombre spécifié de caractères arrive. La définition de l' absence de blocage signifie que a read()renvoie le nombre de caractères disponibles sans attendre davantage, jusqu'à la limite de la mémoire tampon.


Addenda:

CMSPARn'est nécessaire que pour choisir la parité entre les marques et les espaces, ce qui est rare. Pour la plupart des applications, il peut être omis. Mon fichier d'en-tête /usr/include/bits/termios.hpermet de définir CMSPARuniquement si le symbole du préprocesseur __USE_MISCest défini. Cette définition se produit (dans features.h) avec

#if defined _BSD_SOURCE || defined _SVID_SOURCE
 #define __USE_MISC     1
#endif

Les commentaires introductifs de <features.h>dit:

/* These are defined by the user (or the compiler)
   to specify the desired environment:

...
   _BSD_SOURCE          ISO C, POSIX, and 4.3BSD things.
   _SVID_SOURCE         ISO C, POSIX, and SVID things.
...
 */
marcher
la source
1
@wallyk: Dans mon ordinateur, il n'y a pas de fichiers nommés ttyUSB, les seuls fichiers nommés USB sont "usbmon". Mais le PC a beaucoup de ports USB. Alors, comment puis-je les configurer?
Bas
3
@Bas: S'il s'agit de Linux, utilisez la commande lsusbpour voir tous les périphériques USB. Ils peuvent être nommés différemment si votre système dispose de udevrègles personnalisées ; voir /etc/udev/rules.d/ Peut - être qu'à partir de là, vous pourrez choisir le port que vous recherchez. Certes, en listant puis en débranchant le port, vous pouvez identifier la différence.
wallyk
1
@ wallyk Je ne peux pas obtenir de sortie (impossible d'écrire) en utilisant la parité spatiale (PARENB | CMSPRAR). Mais je suis capable de communiquer avec Mark Parity. Des idées comment le résoudre?
Bas
5
Pour une critique de ce code, voir stackoverflow.com/questions/25996171/…
sciure
2
Comme dans j'ai envoyé des données à un appareil ttyUSB0 et elles sont sorties de mon appareil tty que j'utilisais réellement. J'étais littéralement en train de spammer mon propre terminal en utilisant ce code. La réponse ci-dessous à partir de la sciure de bois est une mise en œuvre plus sûre.
Owl
50

Pour le code de démonstration conforme à la norme POSIX, comme décrit dans la section Configuration correcte des modes de terminal et Guide de programmation série pour les systèmes d'exploitation POSIX , les éléments suivants sont proposés.
Ce code doit s'exécuter correctement en utilisant Linux sur x86 ainsi que des processeurs ARM (ou même CRIS).
Il est essentiellement dérivé de l'autre réponse, mais les commentaires inexacts et trompeurs ont été corrigés.

Ce programme de démonstration ouvre et initialise un terminal série à 115 200 bauds pour un mode non canonique aussi portable que possible.
Le programme transmet une chaîne de texte codée en dur à l'autre terminal et retarde l'exécution de la sortie.
Le programme entre alors dans une boucle infinie pour recevoir et afficher les données du terminal série.
Par défaut, les données reçues sont affichées sous forme de valeurs d'octets hexadécimaux.

Pour que le programme traite les données reçues comme des codes ASCII, compilez le programme avec le symbole DISPLAY_STRING, par exemple

 cc -DDISPLAY_STRING demo.c

Si les données reçues sont du texte ASCII (plutôt que des données binaires) et que vous souhaitez les lire sous forme de lignes terminées par le caractère de nouvelle ligne, consultez cette réponse pour un exemple de programme.


#define TERMINAL    "/dev/ttyUSB0"

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

int set_interface_attribs(int fd, int speed)
{
    struct termios tty;

    if (tcgetattr(fd, &tty) < 0) {
        printf("Error from tcgetattr: %s\n", strerror(errno));
        return -1;
    }

    cfsetospeed(&tty, (speed_t)speed);
    cfsetispeed(&tty, (speed_t)speed);

    tty.c_cflag |= (CLOCAL | CREAD);    /* ignore modem controls */
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8;         /* 8-bit characters */
    tty.c_cflag &= ~PARENB;     /* no parity bit */
    tty.c_cflag &= ~CSTOPB;     /* only need 1 stop bit */
    tty.c_cflag &= ~CRTSCTS;    /* no hardware flowcontrol */

    /* setup for non-canonical mode */
    tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
    tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tty.c_oflag &= ~OPOST;

    /* fetch bytes as they become available */
    tty.c_cc[VMIN] = 1;
    tty.c_cc[VTIME] = 1;

    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
        printf("Error from tcsetattr: %s\n", strerror(errno));
        return -1;
    }
    return 0;
}

void set_mincount(int fd, int mcount)
{
    struct termios tty;

    if (tcgetattr(fd, &tty) < 0) {
        printf("Error tcgetattr: %s\n", strerror(errno));
        return;
    }

    tty.c_cc[VMIN] = mcount ? 1 : 0;
    tty.c_cc[VTIME] = 5;        /* half second timer */

    if (tcsetattr(fd, TCSANOW, &tty) < 0)
        printf("Error tcsetattr: %s\n", strerror(errno));
}


int main()
{
    char *portname = TERMINAL;
    int fd;
    int wlen;
    char *xstr = "Hello!\n";
    int xlen = strlen(xstr);

    fd = open(portname, O_RDWR | O_NOCTTY | O_SYNC);
    if (fd < 0) {
        printf("Error opening %s: %s\n", portname, strerror(errno));
        return -1;
    }
    /*baudrate 115200, 8 bits, no parity, 1 stop bit */
    set_interface_attribs(fd, B115200);
    //set_mincount(fd, 0);                /* set to pure timed read */

    /* simple output */
    wlen = write(fd, xstr, xlen);
    if (wlen != xlen) {
        printf("Error from write: %d, %d\n", wlen, errno);
    }
    tcdrain(fd);    /* delay for output */


    /* simple noncanonical input */
    do {
        unsigned char buf[80];
        int rdlen;

        rdlen = read(fd, buf, sizeof(buf) - 1);
        if (rdlen > 0) {
#ifdef DISPLAY_STRING
            buf[rdlen] = 0;
            printf("Read %d: \"%s\"\n", rdlen, buf);
#else /* display hex */
            unsigned char   *p;
            printf("Read %d:", rdlen);
            for (p = buf; rdlen-- > 0; p++)
                printf(" 0x%x", *p);
            printf("\n");
#endif
        } else if (rdlen < 0) {
            printf("Error from read: %d: %s\n", rdlen, strerror(errno));
        } else {  /* rdlen == 0 */
            printf("Timeout from read\n");
        }               
        /* repeat read to get full message */
    } while (1);
}
sciure
la source
1
Beaucoup de cela pourrait être remplacé par juste cfmakeraw?
CMCDragonkai
D'autres exemples que j'ai vus ouvrent également le port avec O_NDELAYou O_NONBLOCK. Le cmrr.umn.edu/~strupp/serial.html mentionne que si vous ouvrez le descripteur de fichier avec ces indicateurs, alors le VTIMEest ignoré. Alors quelle est la différence entre exécuter avec O_NONBLOCKun descripteur de fichier et le faire avec VTIME?
CMCDragonkai
@CMCDragonkai - C'est beaucoup plus compliqué que ce que vous avez écrit. Voir stackoverflow.com/questions/25996171/… qui fait référence à la réponse acceptée à cette question. BTW même si vous ouvrez le terminal en mode non bloquant, vous pouvez toujours revenir en mode blocage avec un fcntl ()
sciure
Désolé pour la question du débutant, mais où sortez-vous de la boucle do while dans main ou est-ce qu'elle boucle pour toujours?
bakalolo
1
@bakalolo - C'est juste un simple code de démonstration à recevoir et à afficher pour toujours. L'intention est un code portable qui se compilera (sans erreurs) et fonctionnera de manière fiable (contrairement à l'autre réponse). Un test pour déterminer la fin du message pourrait être ajouté; avec des données brutes, la définition d'un paquet de messages dépend du protocole. Ou ce code pourrait être modifié pour simplement stocker les données reçues dans un tampon circulaire pour un autre thread à traiter, comme décrit dans cette réponse .
sciure de bois