วิธีการเปิดอ่านและเขียนจากพอร์ตอนุกรมใน C?


139

ฉันสับสนเล็กน้อยเกี่ยวกับการอ่านและการเขียนไปยังพอร์ตอนุกรม ฉันมีอุปกรณ์ USB ใน Linux ที่ใช้ไดรเวอร์อุปกรณ์แปลงอนุกรม FTDI USB เมื่อฉันเสียบมันจะสร้าง: / dev / ttyUSB1

ฉันคิดว่ามันจะง่ายต่อการเปิดและอ่าน / เขียนจากมันใน C. ฉันรู้อัตราการรับส่งข้อมูลและความเท่าเทียมกัน แต่ดูเหมือนว่ามันไม่มีมาตรฐานสำหรับเรื่องนี้?

ฉันทำบางสิ่งบางอย่างหายไปหรือบางคนชี้ไปในทิศทางที่ถูกต้อง


18
คุณได้ดูที่Serial Programming HOWTO แล้วหรือยัง?
ribram

1
แก้ไข: ฉันจะดูลิงก์ของ ribram แต่ยังคงจุดว่าในขณะที่อุปกรณ์แบบอนุกรมจะแสดงเป็นแฟ้มอุปกรณ์ที่มักจะมีอินเตอร์เฟซเฉพาะเจาะจงมากขึ้นดำเนินการผ่านทางสายระบบเหมือนและioctl fcntl
Mr. Shickadance

8
การเชื่อมโยงการปรับปรุงเพื่ออนุกรมการเขียนโปรแกรมคำแนะนำสำหรับ POSIX ระบบปฏิบัติการ
svec

1
การทำความเข้าใจกับเทอร์มินัล UNIX VMIN และ VTIMEเป็นทรัพยากรที่ดีในการทำความเข้าใจ VTIME และ VMIN ซึ่งใช้ในการจัดการลักษณะการบล็อกของ read () บนพอร์ตอนุกรม
flak37

อย่าใช้รหัสจาก "การเขียนโปรแกรมแบบอนุกรม HOWTO" ของ Frerking ตามที่กล่าวไว้ในความคิดเห็นแรก สิ่งเหล่านี้ไม่ได้ถูกเขียนขึ้นเพื่อให้สอดคล้องกับ POSIX ดังนั้นตัวอย่างของรหัสจึงไม่สามารถพกพาได้และอาจไม่สามารถทำงานได้อย่างน่าเชื่อถือสำหรับคุณ
ขี้เลื่อย

คำตอบ:


247

ฉันเขียนสิ่งนี้มานานแล้ว ( ตั้งแต่ปี 1985-1992 โดยมีการปรับแต่งเล็กน้อยหลังจากนั้น ) และเพียงแค่คัดลอกและวางบิตที่จำเป็นในแต่ละโครงการ

คุณต้องเรียกใช้cfmakerawบนที่ได้รับจากtty tcgetattrคุณไม่สามารถเป็นศูนย์ออกstruct termiosกำหนดค่าและจากนั้นตั้งค่าด้วยtty tcsetattrหากคุณใช้วิธีการ zero-out คุณจะพบกับความล้มเหลวเป็นระยะ ๆ โดยเฉพาะ BSD และ OS X โดยที่ "Unmplent intermittent failure" รวมอยู่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

ค่าสำหรับความเร็วที่มีB115200, B230400, B9600, B19200, B38400, B57600, B1200, B2400, B4800ฯลฯ ค่าเท่าเทียมกันที่มี0(หมายถึงความเท่าเทียมกันไม่มี) PARENB|PARODD(เปิดใช้งานความเท่าเทียมกันและใช้เลขคี่) PARENB(เปิดใช้งานความเท่าเทียมกันและการใช้งานมากยิ่ง) PARENB|PARODD|CMSPAR(เครื่องหมายความเท่าเทียมกัน) และPARENB|CMSPAR( ความเท่าเทียมกันของพื้นที่)

"การปิดกั้น" ตั้งค่าว่าจะให้read()รอที่จำนวนของอักขระที่จะมาถึงหรือไม่ การตั้งค่าไม่มีการบล็อกหมายความว่าการread()ส่งคืนอย่างไรก็ตามอักขระจำนวนมากพร้อมใช้งานโดยไม่ต้องรอเพิ่มมากขึ้นจนถึงขีด จำกัด บัฟเฟอร์


ภาคผนวก:

CMSPARจำเป็นสำหรับการเลือกเครื่องหมายและความเท่าเทียมกันของพื้นที่ซึ่งเป็นเรื่องแปลก สำหรับแอปพลิเคชันส่วนใหญ่จะสามารถตัดออกได้ ไฟล์ส่วนหัวของฉัน/usr/include/bits/termios.hเปิดใช้งานการกำหนดCMSPARเฉพาะถ้า__USE_MISCมีการกำหนดสัญลักษณ์พรีโปรเซสเซอร์ คำจำกัดความนั้นเกิดขึ้น (ในfeatures.h) กับ

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

ความคิดเห็นเบื้องต้นของ<features.h>พูดว่า:

/* 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: ในคอมพิวเตอร์ของฉันไม่มีไฟล์ชื่อ ttyUSB เฉพาะไฟล์ที่ชื่อว่า USB คือ "usbmon" แต่พีซีนั้นมีพอร์ต USB จำนวนมาก ดังนั้นฉันจะกำหนดค่าได้อย่างไร
Bas

3
@Bas: หากเป็น Linux ให้ใช้คำสั่งlsusbเพื่อดูอุปกรณ์ USB ทั้งหมด ชื่อเหล่านี้อาจแตกต่างกันหากระบบของคุณมีudevกฎที่กำหนดเอง เห็น/etc/udev/rules.d/ จากที่นั่นคุณสามารถเลือกพอร์ตที่คุณต้องการได้ แน่นอนโดยรายการแล้วยกเลิกการ / เสียบพอร์ตคุณสามารถระบุความแตกต่าง
wallyk

1
@ wallyk ฉันไม่สามารถรับเอาต์พุตใด ๆ (ไม่สามารถเขียน) โดยใช้ space parity (PARENB | CMSPRAR) แต่ฉันสามารถสื่อสารกับมาร์คพาริตี้ได้ ความคิดใด ๆ วิธีการแก้ไขหรือไม่
Bas

5
สำหรับคำวิจารณ์ของรหัสนี้โปรดดูstackoverflow.com/questions/25996171/ …
ขี้เลื่อย

2
ในขณะที่ฉันส่งข้อมูลไปยังอุปกรณ์ ttyUSB0 และมันออกมาจากอุปกรณ์ tty ของฉันที่ฉันใช้จริง แท้จริงฉันสแปมอาคารผู้โดยสารของฉันโดยใช้รหัสนี้ คำตอบด้านล่างจากขี้เลื่อยเป็นการใช้งานที่ปลอดภัยยิ่งขึ้น
Owl

50

สำหรับโค้ดตัวอย่างที่สอดคล้องกับมาตรฐาน POSIX ดังที่อธิบายไว้ในการตั้งค่าโหมดเทอร์มินัลอย่างเหมาะสม และคู่มือการเขียนโปรแกรมแบบอนุกรมสำหรับระบบปฏิบัติการ POSIXมีให้บริการดังต่อไปนี้
เป็นหลักมาจากคำตอบอื่น ๆ แต่ความคิดเห็นที่ไม่ถูกต้องและทำให้เข้าใจผิดได้รับการแก้ไข

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

ในการทำให้โปรแกรมรักษาข้อมูลที่ได้รับเป็นรหัส ASCII ให้รวบรวมโปรแกรมด้วยสัญลักษณ์ DISPLAY_STRING เช่น

 cc -DDISPLAY_STRING demo.c

หากข้อมูลที่ได้รับเป็นข้อความ ASCII (แทนที่จะเป็นข้อมูลไบนารี) และคุณต้องการอ่านมันเป็นบรรทัดที่ลงท้ายด้วยอักขระบรรทัดใหม่ให้ดูคำตอบนี้สำหรับโปรแกรมตัวอย่าง


1
มีจำนวนมากที่สามารถถูกแทนที่ด้วยcfmakerawใช่ไหม?
CMCDragonkai

ตัวอย่างอื่น ๆ ที่ผมเคยเห็นยังเปิดพอร์ตด้วยหรือO_NDELAY cmrr.umn.edu/~strupp/serial.htmlกล่าวว่าถ้าคุณเปิดอธิบายไฟล์ที่มีธงเหล่านั้นแล้วจะถูกละเว้น แล้วอะไรคือความแตกต่างระหว่างการรันด้วยfile descriptor กับการทำมัน? O_NONBLOCKVTIMEO_NONBLOCKVTIME
CMCDragonkai

@CMCDragonkai - มันซับซ้อนกว่าสิ่งที่คุณเขียน ดูstackoverflow.com/questions/25996171/ซึ่งอ้างถึงคำตอบที่ยอมรับสำหรับคำถามนี้ BTW แม้ว่าคุณจะเปิดเครื่องในโหมด nonblocking คุณยังสามารถเปลี่ยนกลับไปเป็นโหมดการปิดกั้นด้วยfcntl ()
ขี้เลื่อย

ขออภัยสำหรับคำถามที่สมัครใหม่ แต่คุณออกจากที่ทำในขณะที่วนรอบในหลักหรือมันวนซ้ำตลอดไป?
bakalolo

1
@bakalolo - มันเป็นเพียงรหัสตัวอย่างที่ง่ายต่อการรับและแสดงผลตลอดไป เจตนาคือรหัสพกพาที่จะรวบรวม (ข้อผิดพลาดโดยไม่มี) และทำงานได้อย่างน่าเชื่อถือ (ไม่เหมือนคำตอบอื่น ๆ ) การทดสอบเพื่อกำหนดจุดสิ้นสุดข้อความสามารถเพิ่มได้ ด้วยข้อมูลดิบคำจำกัดความของแพ็กเก็ตข้อความขึ้นอยู่กับโปรโตคอล หรือรหัสนี้สามารถแก้ไขเพื่อเก็บข้อมูลที่ได้รับในบัฟเฟอร์แบบวงกลมเพื่อให้เธรดอื่นดำเนินการเช่นที่อธิบายไว้ในคำตอบนี้
ขี้เลื่อย
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.