Jak otwierać, czytać i pisać z portu szeregowego w C?


139

Jestem trochę zdezorientowany czytaniem i pisaniem do portu szeregowego. Mam urządzenie USB w systemie Linux, które korzysta ze sterownika konwertera urządzeń szeregowych FTDI USB. Kiedy go podłączam, tworzy: / dev / ttyUSB1.

Pomyślałem, że łatwo będzie go otworzyć i odczytać / zapisać w C. Znam szybkość transmisji i informacje o parzystości, ale wygląda na to, że nie ma na to standardu?

Czy coś mi brakuje, czy może ktoś wskazać mi właściwy kierunek?


18
Czy przyjrzałeś się Serial Programming HOWTO ?
ribram

1
EDYCJA: spojrzałbym na link ribrama. Jednak pozostaje kwestia, że ​​chociaż urządzenie szeregowe jest reprezentowane jako plik, urządzenia często mają bardziej specyficzne interfejsy zaimplementowane za pośrednictwem wywołań systemowych, takich jak ioctli fcntl.
Mr. Shickadance


1
Zrozumienie termios UNIX VMIN i VTIME to świetne źródło wiedzy do zrozumienia VTIME i VMIN, które są używane do obsługi blokowania charakterystyk read () na porcie szeregowym.
flak37

Nie używaj kodu z „Serial Programming HOWTO” Frerkinga, jak wspomniano w pierwszym komentarzu. Nie są napisane tak, aby były zgodne z POSIX, więc przykłady kodu nie są przenośne i mogą nie działać niezawodnie.
trociny

Odpowiedzi:


247

Napisałem to dawno temu (w latach 1985-1992, z kilkoma poprawkami od tego czasu ) i po prostu skopiuj i wklej potrzebne bity do każdego projektu.

Musisz zadzwonić cfmakerawna ttyotrzymany od tcgetattr. Nie można wyzerować struct termios, skonfigurować, a następnie ustawić za ttypomocą tcsetattr. Jeśli użyjesz metody zerowania, wystąpią niewyjaśnione, sporadyczne awarie, szczególnie na BSD i OS X. „Niewyjaśnione sporadyczne awarie” obejmują zawieszanie się 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

Wartości prędkości są B115200, B230400, B9600, B19200, B38400, B57600, B1200, B2400, B4800, itd. Wartości są parytetu 0(czyli bez parzystości), PARENB|PARODD(włączyć parytetu i używać nieparzyste), PARENB(włączyć i używać nawet parytetu), PARENB|PARODD|CMSPAR(znak parzystości) i PARENB|CMSPAR( parzystość przestrzeni).

„Blokowanie” określa, czy read()na porcie oczekuje na nadejście określonej liczby znaków. Ustawienie braku blokowania oznacza, że ​​a read()zwraca dowolną liczbę dostępnych znaków bez czekania na więcej, aż do limitu bufora.


Uzupełnienie:

CMSPARjest potrzebny tylko do wybrania parzystości znaku i spacji, co jest rzadkością. W większości zastosowań można go pominąć. Mój plik nagłówkowy /usr/include/bits/termios.humożliwia definicję CMSPARtylko wtedy, gdy __USE_MISCzdefiniowano symbol preprocesora . Ta definicja występuje (w features.h) z

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

Uwagi wstępne <features.h>mówi:

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

1
@wallyk: Na moim komputerze nie ma plików o nazwie ttyUSB, jedyne pliki o nazwie USB to „usbmon”. Ale komputer ma wiele portów USB. Jak więc je skonfigurować?
Bas

3
@Bas: Jeśli jest to Linux, użyj polecenia, lsusbaby wyświetlić wszystkie urządzenia USB. Można by je nazwać inaczej, jeśli twój system ma własne udevreguły; zobacz /etc/udev/rules.d/ Może stamtąd możesz wybrać port, którego szukasz. Z pewnością wyświetlając, a następnie odłączając / odłączając port, można zidentyfikować różnicę.
wallyk

1
@ wallyk Nie mogę uzyskać żadnych danych wyjściowych (nie mogę pisać) przy użyciu parzystości przestrzeni (PARENB | CMSPRAR). Ale jestem w stanie komunikować się z marką Parity. Jakieś pomysły, jak to rozwiązać?
Bas

5
Krytyka tego kodu znajduje się na stackoverflow.com/questions/25996171/…
trociny

2
Tak jak w przypadku wysłania danych do urządzenia ttyUSB0 i wyszło z mojego urządzenia tty, którego faktycznie używałem. Dosłownie spamowałem swój własny terminal przy użyciu tego kodu. Poniższa odpowiedź z trocin to bezpieczniejsza implementacja.
Owl

50

Dla kodu demonstracyjnego, który jest zgodny ze standardem POSIX, jak opisano w Prawidłowe ustawianie trybów terminala i Poradniku programowania szeregowego dla systemów operacyjnych POSIX , oferowane są następujące informacje.
Zasadniczo pochodzi od drugiej odpowiedzi, ale niedokładne i wprowadzające w błąd komentarze zostały poprawione.

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

Aby program traktował odebrane dane jako kody ASCII, należy skompilować program z symbolem DISPLAY_STRING, np.

 cc -DDISPLAY_STRING demo.c

Jeśli odebrane dane to tekst ASCII (a nie dane binarne) i chcesz je odczytać jako linie zakończone znakiem nowej linii, zobacz tę odpowiedź dla przykładowego programu.


1
Wiele z tego można by zastąpić cfmakerawpoprawnymi?
CMCDragonkai

Inne przykłady, które widziałem, również otwierają port za pomocą O_NDELAYlub O_NONBLOCK. W cmrr.umn.edu/~strupp/serial.html wspomniano, że jeśli otworzysz deskryptor pliku z tymi flagami, to VTIMEzostanie zignorowany. Jaka jest zatem różnica między uruchamianiem z O_NONBLOCKdeskryptorem pliku a wykonywaniem tego z VTIME?
CMCDragonkai

@CMCDragonkai - To dużo bardziej skomplikowane niż to, co napisałeś. Zobacz stackoverflow.com/questions/25996171/…, który odwołuje się do zaakceptowanej odpowiedzi na to pytanie. BTW, nawet jeśli otworzysz terminal w trybie nieblokującym, nadal możesz powrócić do trybu blokowania za pomocą fcntl ()
trociny

Przepraszam za pytanie dla początkujących, ale gdzie wychodzisz z pętli do while w main, czy jest to pętla na zawsze?
bakalolo

1
@bakalolo - To tylko prosty kod demonstracyjny do otrzymywania i wyświetlania na zawsze. Celem jest przenośny kod, który będzie się kompilował (bez błędów) i będzie działał niezawodnie (w przeciwieństwie do drugiej odpowiedzi). Można dodać test określający koniec wiadomości; w przypadku surowych danych definicja pakietu wiadomości zależy od protokołu. Lub ten kod można zmodyfikować, aby po prostu przechowywać odebrane dane w buforze cyklicznym, aby inny wątek mógł je przetworzyć, tak jak opisano w tej odpowiedzi .
trociny
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.