Bagaimana cara membuka, membaca, dan menulis dari port serial di C?

140

Saya agak bingung membaca dan menulis ke port serial. Saya memiliki perangkat USB di Linux yang menggunakan driver konverter perangkat serial USB FTDI. Ketika saya mencolokkannya, itu membuat: / dev / ttyUSB1.

Saya pikir itu akan mudah untuk membuka dan membaca / menulis dari itu di C. Saya tahu baud rate dan informasi paritas, tapi sepertinya tidak ada standar untuk ini?

Apakah saya melewatkan sesuatu, atau dapatkah seseorang mengarahkan saya ke arah yang benar?

gnychi
sumber
18
Pernahkah Anda melihat Serial Programming HOWTO ?
ribram
1
EDIT: Saya akan melihat tautan ribram. Namun, intinya tetap bahwa meskipun perangkat serial direpresentasikan sebagai file, perangkat sering kali memiliki antarmuka yang lebih spesifik yang diterapkan melalui panggilan sistem seperti ioctldan fcntl.
Tn. Shickadance
8
Tautan yang diperbarui ke Panduan Pemrograman Serial untuk Sistem Operasi POSIX .
svec
1
Memahami UNIX termios VMIN dan VTIME adalah sumber daya yang bagus untuk memahami VTIME dan VMIN yang digunakan untuk menangani karakteristik pemblokiran read () pada port serial.
flak37
Jangan gunakan kode dari Frerking's "Serial Programming HOWTO" seperti yang disebutkan pada komentar pertama. Mereka tidak ditulis agar sesuai dengan POSIX, jadi contoh kode tidak portabel dan mungkin tidak berfungsi dengan baik untuk Anda.
serbuk gergaji

Jawaban:

249

Saya menulis ini sejak lama ( dari tahun 1985-1992, hanya dengan beberapa perubahan sejak saat itu ), dan cukup salin dan tempel bit yang diperlukan ke dalam setiap proyek.

Anda harus memanggil cfmakerawpada ttydiperoleh dari tcgetattr. Anda tidak dapat menghilangkan nol struct termios, mengkonfigurasinya, dan kemudian mengatur ttydengan tcsetattr. Jika Anda menggunakan metode zero-out, maka Anda akan mengalami kegagalan intermiten yang tidak dapat dijelaskan, terutama pada BSD dan OS X. "Kegagalan intermiten yang tidak dapat dijelaskan" termasuk bertahan 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

Nilai untuk kecepatan yang B115200, B230400, B9600, B19200, B38400, B57600, B1200, B2400, B4800, dll nilai untuk paritas 0(artinya tidak ada paritas), PARENB|PARODD(memungkinkan paritas dan menggunakan aneh), PARENB(memungkinkan paritas dan menggunakan bahkan), PARENB|PARODD|CMSPAR(mark paritas), dan PARENB|CMSPAR( ruang paritas).

"Blocking" mengatur apakah a read()di port menunggu sampai sejumlah karakter tertentu tiba. Menetapkan tanpa pemblokiran berarti bahwa read()pengembalian berapa pun banyak karakter tersedia tanpa menunggu lebih lama, hingga batas buffer.


Tambahan:

CMSPARhanya diperlukan untuk memilih paritas mark dan spasi, yang tidak umum. Untuk sebagian besar aplikasi, ini dapat dihilangkan. File header saya /usr/include/bits/termios.hmemungkinkan definisi CMSPARhanya jika simbol preprocessor __USE_MISCdidefinisikan. Definisi itu muncul (dalam features.h) dengan

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

Komentar pengantar <features.h>mengatakan:

/* 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
sumber
1
@wallyk: Di komputer saya tidak ada file bernama ttyUSB, satu-satunya file bernama USB adalah "usbmon". Tapi pc memang memiliki banyak port USB. Jadi bagaimana cara mengkonfigurasinya?
Bas
3
@Bas: Jika itu Linux, gunakan perintah lsusbuntuk melihat semua perangkat USB. Mereka dapat diberi nama berbeda jika sistem Anda memiliki udevaturan khusus ; lihat /etc/udev/rules.d/ Mungkin dari sana Anda dapat memilih pelabuhan yang Anda cari. Tentu dengan mendaftar dan kemudian mencabut / mencolokkan port, Anda dapat mengidentifikasi perbedaannya.
wallyk
1
@ wallyk Saya tidak bisa mendapatkan keluaran (tidak dapat menulis) menggunakan paritas spasi (PARENB | CMSPRAR). Tapi saya bisa berkomunikasi dengan Mark Parity. Ada ide bagaimana mengatasinya?
Bas
6
Untuk kritik terhadap kode ini lihat stackoverflow.com/questions/25996171/…
serbuk gergaji
2
Seperti saat saya mengirim data ke perangkat ttyUSB0 dan itu keluar dari perangkat tty saya yang sebenarnya saya gunakan. Saya benar-benar melakukan spamming pada terminal saya sendiri menggunakan kode ini. Jawaban di bawah ini dari serbuk gergaji adalah implementasi yang lebih aman.
Burung Hantu
51

Untuk kode demo yang sesuai dengan standar POSIX seperti yang dijelaskan dalam Mengatur Mode Terminal dengan Benar dan Panduan Pemrograman Serial untuk Sistem Operasi POSIX , berikut ini yang ditawarkan.
Kode ini harus dijalankan dengan benar menggunakan Linux pada x86 serta prosesor ARM (atau bahkan CRIS).
Ini pada dasarnya berasal dari jawaban lain, tetapi komentar yang tidak akurat dan menyesatkan telah diperbaiki.

Program demo ini membuka dan menginisialisasi terminal serial pada 115200 baud untuk mode non-kanonik yang se-portabel mungkin.
Program mentransmisikan string teks hardcode ke terminal lain, dan penundaan saat output dijalankan.
Program kemudian memasuki loop tak terbatas untuk menerima dan menampilkan data dari terminal serial.
Secara default, data yang diterima ditampilkan sebagai nilai byte heksadesimal.

Untuk membuat program memperlakukan data yang diterima sebagai kode ASCII, kompilasi program dengan simbol DISPLAY_STRING, misalnya

 cc -DDISPLAY_STRING demo.c

Jika data yang diterima adalah teks ASCII (bukan data biner) dan Anda ingin membacanya sebagai baris yang diakhiri oleh karakter baris baru, lihat jawaban ini untuk contoh program.


#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);
}
serbuk gergaji
sumber
1
Banyak yang bisa diganti dengan just cfmakerawright?
CMCDragonkai
Contoh lain yang saya lihat juga membuka port dengan O_NDELAYatau O_NONBLOCK. The cmrr.umn.edu/~strupp/serial.html menyebutkan bahwa jika Anda membuka file descriptor dengan orang-bendera, maka VTIMEdiabaikan. Lalu apa perbedaan antara menjalankan dengan O_NONBLOCKdeskriptor file versus melakukannya dengan VTIME?
CMCDragonkai
@CMCDragonkai - Ini jauh lebih rumit dari apa yang Anda tulis. Lihat stackoverflow.com/questions/25996171/… yang mereferensikan jawaban yang diterima untuk pertanyaan ini. BTW bahkan jika Anda membuka terminal dalam mode nonblocking, Anda masih dapat kembali ke mode pemblokiran dengan fcntl ()
serbuk gergaji
Maaf untuk pertanyaan pemula tapi di mana Anda keluar dari do while loop di main atau apakah itu berulang selamanya?
bakalolo
1
@bakalolo - Ini hanya kode demo sederhana untuk diterima dan ditampilkan selamanya. Maksudnya adalah kode portabel yang akan mengkompilasi (tanpa kesalahan) dan bekerja dengan andal (tidak seperti jawaban lain). Tes untuk menentukan akhir pesan dapat ditambahkan; dengan data mentah, definisi paket pesan bergantung pada protokol. Atau kode ini dapat dimodifikasi untuk hanya menyimpan data yang diterima dalam buffer melingkar untuk diproses oleh utas lain, seperti yang dijelaskan dalam jawaban ini .
serbuk gergaji