如何在C语言中打开、读取和写入串口?

177

我对读写串口感到有些困惑。在Linux中,我有一个使用FTDI USB串行设备转换器驱动程序的USB设备。当我插入它时,会创建:/dev/ttyUSB1。

我原以为以C语言打开和读/写它是很简单的。我知道波特率和奇偶校验信息,但好像没有标准规定这些参数?

我是否遗漏了什么,或者有人能指点我正确的方向吗?


18
你看过串行通信编程指南了吗? - ribram
1
编辑:我会看一下ribram的链接。然而,重点在于虽然串行设备被表示为文件,但设备通常通过系统调用(如“ioctl”和“fcntl”)实现更具体的接口。 - Mr. Shickadance
9
更新了链接至POSIX操作系统串行编程指南 - svec
2
了解UNIX termios VMIN和VTIME是一个很好的资源,可以理解VTIME和VMIN如何用于处理串口上read()的阻塞特性。 - flak37
请勿使用第一条评论中提到的Frerking的“串行编程HOWTO”中的代码。它们不是为了符合POSIX标准而编写的,因此代码示例不可移植,并且可能无法可靠地工作。 - sawdust
2个回答

282

我很久以前写过这个(从1985年到1992年,之后只进行了一些微调),只需复制并粘贴所需的部分到每个项目中。

您必须在从tcgetattr获得的tty上调用cfmakeraw。 您不能将struct termios清零,配置它,然后使用tcsetattr设置tty。 如果您使用清零方法,则会遇到无法解释的间歇性故障,特别是在BSD和OS X上。 "无法解释的间歇性故障"包括在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

速度的值包括 B115200B230400B9600B19200B38400B57600B1200B2400B4800 等。 奇偶校验的值包括 0(表示无奇偶校验)、PARENB|PARODD(启用奇偶校验和使用奇校验)、PARENB(启用奇偶校验和使用偶校验)、PARENB|PARODD|CMSPAR(标记奇偶校验)和 PARENB|CMSPAR(空格奇偶校验)。
“阻塞”设置了端口上的 read() 是否等待指定数量的字符到达。 将“非阻塞”设置为意味着 read() 返回可用的任何字符,而不必等待更多字符,直到缓冲区限制。

附加说明:

CMSPAR 仅在选择标记和空格奇偶校验时需要,这种情况相对不常见。对于大多数应用程序,可以省略它。我的头文件 /usr/include/bits/termios.h 仅在预处理器符号 __USE_MISC 被定义时才启用对 CMSPAR 的定义。该定义在 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 的文件,唯一的文件名称为“usbmon”。但是这台电脑确实有很多 USB 端口。那么我该如何配置它们呢? - Bas
3
如果你使用的是 Linux 操作系统,可以使用 lsusb 命令查看所有 USB 设备。如果你的系统有自定义的 udev 规则,则它们可能会被命名为不同的名称;可以查看 /etc/udev/rules.d/ 目录。也许从那里你能找到你要寻找的端口。通过列出然后插拔端口,你肯定可以识别出它们之间的差异。 - wallyk
1
@ wallyk 我无法使用空格奇偶校验(PARENB|CMSPRAR)获得任何输出(无法写入)。但我能够与标记奇偶校验进行通信。有什么想法如何解决它? - Bas
7
有关此代码的批评,请参阅https://dev59.com/vl8e5IYBdhLWcg3wLYDt#26006680。 - sawdust
2
我向 ttyUSB0 设备发送了数据,结果却从我实际使用的 tty 设备中输出。我实际上是在使用这段代码不断地刷屏自己的终端。下面 sawdust 的回答提供了更安全的实现方式。 - Owl
显示剩余22条评论

65

为了展示符合POSIX标准的演示代码,可以参考正确设置终端模式面向POSIX操作系统的串行编程指南

下面是提供的代码示例。
该代码在x86以及ARM(甚至CRIS)处理器上应该都能正确执行。
它基本上源自于其他答案,但已更正不准确且误导性的评论。

这个演示程序以尽可能通用的方式,打开并初始化一个波特率为115200的非规范模式的串行终端。
该程序会将硬编码的文本字符串传输到另一台终端,并在输出完成后延迟执行。
然后,该程序进入无限循环,从串行终端接收和显示数据。
默认情况下,接收到的数据将以十六进制字节值形式显示。

要使程序将接收到的数据视为ASCII代码,请使用符号DISPLAY_STRING编译程序,例如:

 cc -DDISPLAY_STRING demo.c
如果接收到的数据是ASCII文本(而不是二进制数据),并且您希望将其读取为以换行符作为行终止符的行,则请参见此答案中的示例程序。
#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);
}

如果您想要一个高效的程序,可以缓冲接收到的数据,同时允许逐字节地处理输入,请参见此答案



1
很多东西都可以用 cfmakeraw 来替换,对吧? - CMCDragonkai
1
我看到的其他例子也使用O_NDELAYO_NONBLOCK打开端口。http://www.cmrr.umn.edu/~strupp/serial.html提到,如果您使用这些标志打开文件描述符,则会忽略`VTIME`。那么使用`O_NONBLOCK`文件描述符和使用`VTIME`有什么区别呢? - CMCDragonkai
1
@CMCDragonkai -- 比你所写的要复杂得多。请参见https://dev59.com/vl8e5IYBdhLWcg3wLYDt,该链接引用了此问题的已接受答案。顺便说一下,即使您以非阻塞模式打开终端,仍然可以使用**fcntl()**返回到阻塞模式。 - sawdust
1
@bakalolo -- 这只是一个简单的演示代码,用于接收和永久显示。目的是编写可移植的代码,可以编译(无错误)并可靠地工作(不像其他答案)。可以添加一个测试来确定消息结束;对于原始数据,消息包的定义取决于协议。或者可以修改此代码,仅将接收到的数据存储在循环缓冲区中供另一个线程处理,例如在此答案中所述。 - sawdust
1
@dstromberg - termios支持的“基本”终端是硬拷贝电传打字机式终端,而不是VDT(如VT220),在任何意义上都不是“基本”的。再次阅读手册页和代码。 “代码就是文档。”“Any it's cread,…”——糟糕的语法和模糊的引用根本没有意义。 - sawdust
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接