Làm thế nào để mở, đọc và viết từ cổng nối tiếp trong C?


139

Tôi hơi bối rối về việc đọc và viết vào một cổng nối tiếp. Tôi có một thiết bị USB trong Linux sử dụng trình điều khiển chuyển đổi thiết bị nối tiếp USB FTDI. Khi tôi cắm nó vào, nó sẽ tạo: / dev / ttyUSB1.

Tôi nghĩ thật đơn giản để mở và đọc / ghi từ nó trong C. Tôi biết tốc độ truyền và thông tin chẵn lẻ, nhưng có vẻ như không có tiêu chuẩn nào cho việc này?

Tôi đang thiếu một cái gì đó, hoặc ai đó có thể chỉ cho tôi đi đúng hướng?


18
Bạn đã xem qua chương trình nối tiếp HOWTO chưa?
ribram

1
EDIT: Tôi sẽ xem liên kết của ribram. Tuy nhiên, vấn đề vẫn là trong khi một thiết bị nối tiếp được biểu diễn dưới dạng tệp, các thiết bị thường có giao diện cụ thể hơn được thực hiện thông qua các cuộc gọi hệ thống như ioctlfcntl.
Ông Shickadance


1
Hiểu các thuật ngữ UNIX VMIN và VTIME là một nguồn tài nguyên tuyệt vời để hiểu VTIME và VMIN được sử dụng để xử lý các đặc tính chặn của read () trên một cổng nối tiếp.
flak37

Không sử dụng mã từ "Lập trình nối tiếp HOWTO" của Frerking như đã đề cập trong bình luận đầu tiên. Chúng không được viết để tuân thủ POSIX, vì vậy các ví dụ mã không thể mang theo được và có thể không hoạt động đáng tin cậy cho bạn.
mùn cưa

Câu trả lời:


246

Tôi đã viết điều này từ lâu ( từ những năm 1985-1992, chỉ với một vài điều chỉnh kể từ đó ), và chỉ cần sao chép và dán các bit cần thiết vào mỗi dự án.

Bạn phải gọi cfmakerawtrên một ttythu được từ tcgetattr. Bạn không thể loại bỏ a struct termios, cấu hình nó và sau đó đặt ttyvới tcsetattr. Nếu bạn sử dụng phương pháp zero-out, thì bạn sẽ gặp phải những thất bại không liên tục không giải thích được, đặc biệt là trên BSD và OS X. "Thất bại không liên tục không giải thích được" bao gồm treo vào 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

Các giá trị cho tốc độ là B115200, B230400, B9600, B19200, B38400, B57600, B1200, B2400, B4800, v.v. Giá trị cho chẵn lẻ được 0(nghĩa là không chẵn lẻ), PARENB|PARODD(cho phép chẵn lẻ và sử dụng lẻ), PARENB(cho phép chẵn lẻ và sử dụng ngay cả), PARENB|PARODD|CMSPAR(đánh dấu chẵn lẻ), và PARENB|CMSPAR( không gian chẵn lẻ).

"Chặn" đặt liệu a read()trên cổng có chờ số lượng ký tự được chỉ định đến hay không. Đặt không chặn có nghĩa là read()trả về tuy nhiên có nhiều ký tự có sẵn mà không phải chờ thêm, đến giới hạn bộ đệm.


Phụ lục:

CMSPARchỉ cần thiết cho việc chọn dấu và chẵn lẻ không gian, điều này không phổ biến. Đối với hầu hết các ứng dụng, nó có thể được bỏ qua. Tệp tiêu đề của tôi /usr/include/bits/termios.hcho phép định nghĩa CMSPARchỉ khi biểu tượng tiền xử lý __USE_MISCđược xác định. Định nghĩa đó xảy ra (trong features.h) với

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

Các ý kiến ​​giới thiệu của <features.h>nói:

/* 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: Trong máy tính của tôi không có tệp nào có tên ttyUSB, các tệp duy nhất có tên USB là "usbmon". Nhưng máy tính có rất nhiều cổng USB. Vậy làm cách nào để cấu hình chúng?
Bas

3
@Bas: Nếu là Linux, hãy sử dụng lệnh lsusbđể xem tất cả các thiết bị USB. Chúng có thể được đặt tên khác nếu hệ thống của bạn có các udevquy tắc tùy chỉnh ; xem /etc/udev/rules.d/ Có thể từ đó bạn có thể chọn ra cổng bạn đang tìm kiếm. Chắc chắn bằng cách liệt kê và sau đó bỏ / cắm cổng, bạn có thể xác định sự khác biệt.
wallyk

1
@ wallyk Tôi không thể nhận được bất kỳ đầu ra nào (không thể viết) bằng cách sử dụng không gian chẵn lẻ (PARENB | CMSPRAR). Nhưng tôi có thể giao tiếp với nhãn hiệu Parity. Bất kỳ ý tưởng làm thế nào để giải quyết nó?
Bas

5
Để biết phê bình về mã này, hãy xem stackoverflow.com/questions/25996171/ mài
mùn cưa

2
Như trong tôi đã gửi dữ liệu đến một thiết bị ttyUSB0 và nó đã ra khỏi thiết bị tty của tôi mà tôi đang thực sự sử dụng. Tôi thực sự đã spam thiết bị đầu cuối của riêng tôi bằng cách sử dụng mã này. Câu trả lời dưới đây từ mùn cưa là cách thực hiện an toàn hơn.

50

Đối với mã demo phù hợp với tiêu chuẩn POSIX như được mô tả trong Cài đặt chế độ đầu cuối đúngHướng dẫn lập trình nối tiếp cho hệ điều hành POSIX , những điều sau đây được cung cấp.
Về cơ bản, nó bắt nguồn từ câu trả lời khác, nhưng những bình luận không chính xác và sai lệch đã được sửa chữa.

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

Để làm cho chương trình coi dữ liệu nhận được dưới dạng mã ASCII, hãy biên dịch chương trình với ký hiệu DISPLAY_STRING, ví dụ:

 cc -DDISPLAY_STRING demo.c

Nếu dữ liệu nhận được là văn bản ASCII (chứ không phải dữ liệu nhị phân) và bạn muốn đọc nó dưới dạng các dòng kết thúc bởi ký tự dòng mới, thì hãy xem câu trả lời này cho một chương trình mẫu.


1
Rất nhiều trong số đó có thể được thay thế bằng vừa cfmakerawphải?
CMCDragonkai

Các ví dụ khác tôi đã thấy cũng mở cổng với O_NDELAYhoặc O_NONBLOCK. Các cmrr.umn.edu/~strupp/serial.html đề cập rằng nếu bạn mở tập tin mô tả với những lá cờ, thì VTIMEđược bỏ qua. Sau đó, sự khác biệt giữa chạy với O_NONBLOCKmô tả tập tin so với làm với nó là VTIMEgì?
CMCDragonkai

@CMCDragonkai - Nó phức tạp hơn nhiều so với những gì bạn viết. Xem stackoverflow.com/questions/25996171/ mà tham khảo câu trả lời được chấp nhận cho câu hỏi này. BTW ngay cả khi bạn mở thiết bị đầu cuối ở chế độ không chặn, bạn vẫn có thể trở lại chế độ chặn với fcntl ()
mùn cưa

Xin lỗi cho câu hỏi của người mới nhưng bạn đang thoát khỏi vòng lặp do while trong vòng lặp chính hay nó lặp lại mãi mãi?
bakalolo

1
@bakalolo - Đó chỉ là mã demo đơn giản để nhận và hiển thị mãi mãi. Mục đích là mã di động sẽ biên dịch (lỗi w / o) và hoạt động đáng tin cậy (không giống như câu trả lời khác). Một bài kiểm tra để xác định kết thúc tin nhắn có thể được thêm vào; với dữ liệu thô định nghĩa của gói tin phụ thuộc vào giao thức. Hoặc mã này có thể được sửa đổi để chỉ lưu trữ dữ liệu nhận được trong bộ đệm tròn cho một luồng khác để xử lý, như được mô tả trong câu trả lời này .
mùn cưa
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.