¿Cómo abrir, leer y escribir desde el puerto serie en C?

10 minutos de lectura

avatar de usuario
gnychis

Estoy un poco confundido acerca de leer y escribir en un puerto serie. Tengo un dispositivo USB en Linux que utiliza el controlador convertidor de dispositivo serie USB FTDI. Cuando lo conecto, crea: /dev/ttyUSB1.

Pensé que sería sencillo abrirlo y leerlo/escribirlo en C. Conozco la velocidad en baudios y la información de paridad, pero parece que no hay un estándar para esto.

¿Me estoy perdiendo algo o alguien puede indicarme la dirección correcta?

  • ¿Has echado un vistazo a la Cómo programar en serie?

    – ribram

    04/08/2011 a las 19:30

  • EDITAR: miraría el enlace de ribram. Sin embargo, el punto sigue siendo que, si bien un dispositivo serial se representa como un archivo, los dispositivos a menudo tienen interfaces más específicas implementadas a través de llamadas al sistema como ioctl y fcntl.

    – Sr. Shickadance

    04/08/2011 a las 19:31


  • Enlace actualizado a Guía de programación en serie para sistemas operativos POSIX.

    – svec

    28 de diciembre de 2013 a las 1:29


  • Comprender UNIX termios VMIN y VTIME es un gran recurso para comprender VTIME y VMIN que se utilizan para manejar las características de bloqueo de una lectura () en un puerto serie.

    – flak37

    11/08/2014 a las 20:43


  • No use el código del “CÓMO de programación en serie” de Frerking como se menciona en el primer comentario. No están escritos para ser compatibles con POSIX, por lo que los ejemplos de código no son portátiles y es posible que no funcionen de manera confiable para usted.

    – aserrín

    22 de enero de 2019 a las 0:37


avatar de usuario
Wallyk

Escribí esto hace mucho tiempo (de los años 1985 a 1992, con solo algunos ajustes desde entonces), y solo copié y pegué los bits necesarios en cada proyecto.

debes llamar cfmakeraw en un tty obtenido de tcgetattr. No puede poner a cero un struct termiosconfigúrelo y, a continuación, configure el tty con tcsetattr. Si usa el método de puesta a cero, experimentará fallas intermitentes inexplicables, especialmente en los BSD y OS X. Las “fallas intermitentes inexplicables” incluyen colgar 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

Los valores de la velocidad son B115200, B230400, B9600, B19200, B38400, B57600, B1200, B2400, B4800etc. Los valores para la paridad son 0 (es decir, sin paridad), PARENB|PARODD (habilitar paridad y usar impar), PARENB (habilitar paridad y usar incluso), PARENB|PARODD|CMSPAR (marque la paridad), y PARENB|CMSPAR (paridad espacial).

“Bloqueo” establece si un read() en el puerto espera que llegue el número especificado de caracteres. Entorno sin bloqueo significa que un read() devuelve la cantidad de caracteres disponibles sin esperar más, hasta el límite del búfer.


Apéndice:

CMSPAR solo se necesita para elegir la paridad de marca y espacio, lo cual es poco común. Para la mayoría de las aplicaciones, se puede omitir. Mi archivo de cabecera /usr/include/bits/termios.h permite la definición de CMSPAR solo si el símbolo del preprocesador __USE_MISC se define. Esa definición ocurre (en features.h) con

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

Los comentarios introductorios de <features.h> dice:

/* 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.
...
 */

  • @wallyk: En mi computadora no hay archivos llamados ttyUSB, los únicos archivos llamados USB son “usbmon”. Pero la PC tiene muchos puertos USB. Entonces, ¿cómo los configuro?

    – bajo

    23 de abril de 2014 a las 6:18

  • @Bas: si es Linux, use el comando lsusb para ver todos los dispositivos USB. Podrían tener un nombre diferente si su sistema tiene udev normas; ver /etc/udev/rules.d/ Tal vez desde allí pueda elegir el puerto que está buscando. Ciertamente, al enumerar y luego desconectar/conectar el puerto, puede identificar la diferencia.

    – Wallyk

    23 de abril de 2014 a las 13:48

  • @ wallyk No puedo obtener ningún resultado (no puedo escribir) usando la paridad espacial (PARENB | CMSPRAR). Pero puedo comunicarme con Mark Parity. ¿Alguna idea de cómo resolverlo?

    – bajo

    5 de mayo de 2014 a las 5:11

  • Para ver una crítica de este código, consulte stackoverflow.com/questions/25996171/…

    – aserrín

    17 de febrero de 2015 a las 9:00

  • Como envié datos a un dispositivo ttyUSB0 y salió de mi dispositivo tty que en realidad estaba usando. Literalmente estaba enviando spam a mi propia terminal usando este código. La respuesta a continuación de aserrín es una implementación más segura.

    – Búho

    1 oct 2018 a las 13:42


avatar de usuario
serrín

Para el código de demostración que cumple con el estándar POSIX como se describe en Configuración adecuada de los modos de terminal
y Guía de programación en serie para sistemas operativos POSIXse ofrece lo siguiente.
Este código debería ejecutarse correctamente utilizando Linux en x86, así como procesadores ARM (o incluso CRIS).
Se deriva esencialmente de la otra respuesta, pero se han corregido los comentarios inexactos y engañosos.

Este programa de demostración abre e inicializa un terminal serial a 115200 baudios para el modo no canónico que es lo más portátil posible.
El programa transmite una cadena de texto codificada a la otra terminal y se demora mientras se realiza la salida.
Luego, el programa ingresa en un bucle infinito para recibir y mostrar datos del terminal serial.
De forma predeterminada, los datos recibidos se muestran como valores de bytes hexadecimales.

Para hacer que el programa trate los datos recibidos como códigos ASCII, compile el programa con el símbolo DISPLAY_STRING, por ejemplo

 cc -DDISPLAY_STRING demo.c

Si los datos recibidos son texto ASCII (en lugar de datos binarios) y desea leerlos como líneas terminadas por el carácter de nueva línea, consulte esta respuesta para obtener un programa de muestra.


#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);
}

Para ver un ejemplo de un programa eficiente que proporciona almacenamiento en búfer de los datos recibidos pero que permite el manejo byte a byte de la entrada, vea esta respuesta.


  • Mucho de eso podría ser reemplazado con solo cfmakeraw ¿derecho?

    – CMC Dragonkai

    20 de enero de 2017 a las 12:28

  • Otros ejemplos que he visto también abren el puerto con O_NDELAY o O_NONBLOCK. Él cmrr.umn.edu/~strupp/serial.html menciona que si abre el descriptor de archivo con esas banderas, entonces el VTIME se ignora Entonces, ¿cuál es la diferencia entre correr con O_NONBLOCK descriptor de archivo versus hacerlo con VTIME?

    – CMC Dragonkai

    20 de enero de 2017 a las 12:55

  • @CMCDragonkai: es mucho más complicado de lo que escribiste. Consulte stackoverflow.com/questions/25996171/… que hace referencia a la respuesta aceptada a esta pregunta. Por cierto, incluso si abre el terminal en modo sin bloqueo, aún puede volver al modo de bloqueo con un fcntl()

    – aserrín

    20 de enero de 2017 a las 23:57

  • Perdón por la pregunta de novato, pero ¿dónde está saliendo del bucle do while en principal o se repite para siempre?

    – bakalolo

    9 de febrero de 2017 a las 2:16

  • @bakalolo: es solo un código de demostración simple para recibir y mostrar para siempre. La intención es un código portátil que se compilará (sin errores) y funcionará de manera confiable (a diferencia de la otra respuesta). Se podría agregar una prueba para determinar el final del mensaje; con datos sin procesar, la definición de un paquete de mensajes depende del protocolo. O este código podría modificarse para simplemente almacenar los datos recibidos en un búfer circular para que otro subproceso los procese, como se describe en esta respuesta.

    – aserrín

    9 de febrero de 2017 a las 2:37

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad