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

139

Estoy un poco confundido acerca de leer y escribir en un puerto serie. Tengo un dispositivo USB en Linux que utiliza el controlador del 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 de transmisión y la información de paridad, pero parece que no hay un estándar para esto.

¿Me estoy perdiendo algo o alguien puede señalarme en la dirección correcta?

gnychis
fuente
18
¿Has echado un vistazo al COMO de Programación en Serie ?
ribram
1
EDITAR: miraría el enlace de ribram. Sin embargo, el punto sigue siendo que si bien un dispositivo en serie se representa como un archivo, los dispositivos a menudo tienen interfaces más específicas implementadas a través de llamadas al sistema como ioctly fcntl.
Sr. Shickadance
1
Comprender los términos UNIX VMIN y VTIME es un gran recurso para comprender VTIME y VMIN que se utilizan para manejar las características de bloqueo de un read () en un puerto serie.
flak37
No utilice 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 pueden no funcionar de manera confiable para usted.
aserrín

Respuestas:

247

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

Debe llamar cfmakerawa un ttyobtenido de tcgetattr. No puede struct termiosponer a cero a , configurarlo y luego configurarlo ttycon tcsetattr. Si utiliza el método de cero, experimentará fallas intermitentes inexplicables, especialmente en los BSD y OS X. Las "fallas intermitentes inexplicables" incluyen el bloqueo 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, B4800, etc. Los valores de paridad están 0(lo que significa sin paridad), PARENB|PARODD(habilitar la paridad y el uso impar), PARENB(habilitar paridad y usar incluso), PARENB|PARODD|CMSPAR(paridad marca), y PARENB|CMSPAR( paridad espacial).

"Bloquear" establece si un read()en el puerto espera a que llegue el número especificado de caracteres. Si no se configura el bloqueo , se read()devuelve una cantidad de caracteres disponibles sin esperar más, hasta el límite del búfer.


Apéndice:

CMSPARsolo es necesario 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 encabezado /usr/include/bits/termios.hpermite la definición de CMSPARsolo si el símbolo del preprocesador __USE_MISCestá definido. 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
fuente
1
@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?
Bas
3
@Bas: si es Linux, use el comando lsusbpara ver todos los dispositivos USB. Podrían nombrarse de manera diferente si su sistema tiene udevreglas personalizadas ; vea /etc/udev/rules.d/ Quizás desde allí puede elegir el puerto que está buscando. Ciertamente, al enumerar y luego desenchufar / enchufar el puerto puede identificar la diferencia.
wallyk
1
@ 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?
Bas
55
Para una crítica de este código, consulte stackoverflow.com/questions/25996171/…
serrín
2
Como en el caso, envié datos a un dispositivo ttyUSB0 y salió de mi dispositivo tty que realmente estaba usando. Literalmente estaba enviando spam a mi propio terminal usando este código. La siguiente respuesta de serrín es una implementación más segura.
Owl
50

Para el código de demostración que se ajusta al estándar POSIX como se describe en Configuración adecuada de los modos de terminal y la Guía de programación en serie para sistemas operativos POSIX , se ofrece lo siguiente.
Se deriva esencialmente de la otra respuesta, pero se han corregido los comentarios inexactos y engañosos.

#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 = "/dev/ttyUSB0";
    int fd;
    int wlen;

    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, "Hello!\n", 7);
    if (wlen != 7) {
        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 que el programa trate los datos recibidos como códigos ASCII, compile el programa con el símbolo DISPLAY_STRING, p. Ej.

 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 un programa de muestra.

serrín
fuente
1
¿Mucho de eso podría ser reemplazado por el cfmakerawcorrecto?
CMCDragonkai
Otros ejemplos que he visto también abren el puerto con O_NDELAYo O_NONBLOCK. El cmrr.umn.edu/~strupp/serial.html menciona que si abre el descriptor de archivo con esos indicadores, entonces VTIMEse ignora. Entonces, ¿cuál es la diferencia entre correr con O_NONBLOCKun descriptor de archivo o hacerlo con VTIME?
CMCDragonkai
@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 ()
serrín el
Perdón por la pregunta de novato, pero ¿de dónde sales del bucle do while en main o se repite para siempre?
bakalolo
1
@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 en bruto, la definición de un paquete de mensajes depende del protocolo. O este código podría modificarse para almacenar los datos recibidos en un búfer circular para que otro hilo lo procese, como se describe en esta respuesta .
aserrín