一直以来在Linux下用到串口的时候都是用Python的pyserial库操作,现在发现直接使用Linux的系统调用操作串口还真是挺复杂的。

得益于Linux一切皆文件的思想,串口的读写可以直接使用readwrite系统调用操作/dev目录下的串口设备节点,串口开发复杂的地方在于串口属性的配置,十分繁琐。

串口设备属性配置

一般来说,使用串口设备需要配置的属性有:波特率、数据位、停止位、校验位。Linux中,这些属性使用struct termios结构进行存储,该结构定义在termios.h头文件中,查看系统的man手册可以看到该结构的详细介绍。该结构至少包含以下属性:

1
2
3
4
5
6
7
8
struct termios
{
tcflag_t c_iflag; /* input modes */
tcflag_t c_oflag; /* output modes */
tcflag_t c_cflag; /* control modes */
tcflag_t c_lflag; /* local modes */
cc_t c_cc[NCCS]; /* special characters */
};

使用tcgetattr()tcsetattr()函数可以读取或设置串口设备的属性,函数原型见man手册

1
2
3
4
5
struct termios portOption;

// fd: 串口设备文件描述符,使用 open 函数创建
tcgetattr(fd, &portOption); // 读取串口设备属性,存入portOption中
tcsetattr(fd, TCSANOW, &portOption); // 设置设备属性, “TCSANOW”参数表示使属性立即生效

波特率的设置

可以使用cfsetispeed()cfsetospeed()分别设置串口的输入、输出波特率,使用cfgetispeed()cfgetospeed()分别获取串口的输入、输出波特率。波特率的数据类型为speed_t,是一个枚举类型,其取值范围可以查看man手册

1
2
cfsetispeed(&portOption,B115200);   //设置为115200Bps
cfsetospeed(&portOption,B115200);

注意,该设置仅为修改struct termios结构的值,要让设置生效还需要使用tcsetattr()函数。

数据位设置

数据位长度可以设置为5、6、7、8,分别对应宏CS5、CS6、CS7、CS8,根据需要将对应的宏与struct termios结构中的c_cflag字段按位或即可,这种设置属性的方法很符合Unix风格。

1
2
portOption.c_cflag |= CS7;  // 7位数据位
portOption.c_cflag |= CS8; // 8位数据位

停止位设置

termios.h中定义了一个宏CSTOPB来表示两位停止位,如果需要设置2位停止位,同数据位设置一样与c_cflag按位或即可。如果要设置1位停止位,则对CSTOPB取反再与c_cflag按位与。

1
2
portOption.c_cflag |= CSTOPB;   // 2位停止位
portOption.c_cflag &= ~CSTOPB; // 1位停止位

校验位设置

涉及校验的宏定义有INPCKPARENBPARODD

  • INPCK:开启输入校验
  • PARENB:开启输入输出时的校验码生成
  • PARODD:设置奇校验
    使用这三个宏,即可设置校验方式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /* 无校验 */
    portOption.c_cflag &= ~PARENB;
    portOption.c_cflag &= ~INPCK;

    /* 奇校验 */
    portOption.c_cflag |= PARENB;
    portOption.c_cflag |= PARODD;
    portOption.c_cflag |= INPCK;

    /* 偶校验 */
    portOption.c_cflag |= PARENB;
    portOption.c_cflag &= ~PARODD;
    portOption.c_cflag |= INPCK;

注意:如果不是开发串口终端,而仅仅使用串口传输数据,则数据需要使用RAW Mode进行传输:

1
2
portOption.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);  /*Input*/
portOption.c_oflag &= ~OPOST; /*Output*/

也可以偷懒使用termios.h中提供的函数cfmakeraw()

1
cfmakeraw(&portOption);

阻塞与非阻塞

串口设备的读写阻塞与非阻塞不仅仅与设备节点被open的时候设置的参数有关,还与struct termios结构中c_cc[VMIN]c_cc[VTIME]有关。

参考wiringPi库和pyserial的实现,我发现大家再打开串口设备的时候都是将其设置为非阻塞,然后在设置完设备属性后再使用fcntl()将其设置为阻塞。

1
2
3
4
5
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NONBLOCK | O_NDELAY);
...
/* 设置属性... */
...
fcntl(fd, F_SETFL, 0); // 设置为阻塞模式

设置完文件描述符的属性,还需要根据c_cc[VMIN]c_cc[VTIME]才能确定读写的时候是否为阻塞模式,这两项的组合如下:

  • c_cc[VMIN]==0; c_cc[VTIME]==0;

    非阻塞,read函数将立即返回实际读取的字节数,没有读取到则返回0

  • c_cc[VMIN]>0; c_cc[VTIME]==0;

    阻塞,串口缓冲区中至少有c_cc[VMIN]个字节可供读取时,read才会返回,read的返回值为c_cc[VMIN]readlen参数中的较小者。

  • c_cc[VMIN]==0; c_cc[VTIME]>0;

    这种情况下,当调用read时,计时器开始计时,c_cc[VTIME]的单位为十分之一秒,如果计时超过c_cc[VTIME]设置的时间,read将会返回0,或者缓冲区中至少有一个字节可供读取,read正常返回,否则将会阻塞。

  • c_cc[VMIN]>0; c_cc[VTIME]>0;

    该情况中从调用read并且缓冲区中至少有一个字节可用时,计时器开始计时,并且每次调用read且缓冲区中有数据可供读取时,计时器会重新计时,直到计时器超时或者read已经读到len个字节,read会返回实际读取的字节数。注意在该情况下,如果缓冲区中没有可供读取的数据,那么计时器不会启动,read将被一直阻塞。

分析

使用c_cc[VMIN]c_cc[VTIME]可以灵活的按需设置串口读取的阻塞与非阻塞状态。非阻塞read直接设置c_cc[VMIN]==0; c_cc[VTIME]==0;即可;阻塞的设置相对比较复杂:

  1. 如果需要保证每次read的字节数,可以设置c_cc[VMIN]>0; c_cc[VTIME]==0;,但是需要注意,c_cc[]的数据类型cc_t实际上为unsigned char,其取值范围为0~255

  2. 如果需要为read设置超时时间,需要注意后面两种情况的超时是不同的。c_cc[VMIN]==0; c_cc[VTIME]>0;只能确保串口缓冲区中有数据可读,但是无法保证实际read到的字节数量;c_cc[VMIN]>0; c_cc[VTIME]>0;能够确保read到的字节数量,但是read每读取一个字节会重新设置计时器,即设置的超时时间并不是read超时返回的时间,并且需要注意,当缓冲区无数据可读时,计时器并不会启动,read将被一直阻塞。

从某种意义上来说,串口超时的设置都不是“真正的”超时,并非从read函数被调用到超时返回的真实时间。pyserial库中的read函数是可以设置其超时返回时间的,参考其源码发现可以使用select()来实现“真正的”超时。

为了同时保证read读取的字节数和超时返回的时间,换一种思路就是使用c_cc[VMIN]>0; c_cc[VTIME]==0;来保证read读取的字节数,使用select()函数来设置超时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fd_set set;
struct timeval timeout;

timeout.tv_sec = 5; // 设置超时时间为5s
FD_ZERO(&set);
FD_SET(fd, &set);

portOption.c_cc[VMIN] = 255;
portOption.c_cc[VTIME] = 0;
tcsetattr(fd, TCSANOW, &portOption);

switch(select(fd+1, &set, NULL, NULL, &timeout))
{
case 0:
/* timeout */
case -1:
/* select() error */
default:
read(fd, buffer, len);
}

关于select()函数的使用方法可以参考man手册