Linux串口开发
一直以来在Linux下用到串口的时候都是用Python的pyserial库操作,现在发现直接使用Linux的系统调用操作串口还真是挺复杂的。
得益于Linux一切皆文件的思想,串口的读写可以直接使用read、write系统调用操作/dev目录下的串口设备节点,串口开发复杂的地方在于串口属性的配置,十分繁琐。
串口设备属性配置
一般来说,使用串口设备需要配置的属性有:波特率、数据位、停止位、校验位。Linux中,这些属性使用struct termios结构进行存储,该结构定义在termios.h头文件中,查看系统的man手册可以看到该结构的详细介绍。该结构至少包含以下属性:
1 | struct termios |
使用tcgetattr()、tcsetattr()函数可以读取或设置串口设备的属性,函数原型见man手册。
1 | struct termios portOption; |
波特率的设置
可以使用cfsetispeed()、cfsetospeed()分别设置串口的输入、输出波特率,使用cfgetispeed()、cfgetospeed()分别获取串口的输入、输出波特率。波特率的数据类型为speed_t,是一个枚举类型,其取值范围可以查看man手册
1 | cfsetispeed(&portOption,B115200); //设置为115200Bps |
注意,该设置仅为修改struct termios结构的值,要让设置生效还需要使用tcsetattr()函数。
数据位设置
数据位长度可以设置为5、6、7、8,分别对应宏CS5、CS6、CS7、CS8,根据需要将对应的宏与struct termios结构中的c_cflag字段按位或即可,这种设置属性的方法很符合Unix风格。
1 | portOption.c_cflag |= CS7; // 7位数据位 |
停止位设置
termios.h中定义了一个宏CSTOPB来表示两位停止位,如果需要设置2位停止位,同数据位设置一样与c_cflag按位或即可。如果要设置1位停止位,则对CSTOPB取反再与c_cflag按位与。
1 | portOption.c_cflag |= CSTOPB; // 2位停止位 |
校验位设置
涉及校验的宏定义有INPCK、PARENB、PARODD:
- 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 | portOption.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/ |
也可以偷懒使用termios.h中提供的函数cfmakeraw()
1 | cfmakeraw(&portOption); |
阻塞与非阻塞
串口设备的读写阻塞与非阻塞不仅仅与设备节点被open的时候设置的参数有关,还与struct termios结构中c_cc[VMIN]和c_cc[VTIME]有关。
参考wiringPi库和pyserial的实现,我发现大家再打开串口设备的时候都是将其设置为非阻塞,然后在设置完设备属性后再使用fcntl()将其设置为阻塞。
1 | int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NONBLOCK | O_NDELAY); |
设置完文件描述符的属性,还需要根据c_cc[VMIN]和c_cc[VTIME]才能确定读写的时候是否为阻塞模式,这两项的组合如下:
c_cc[VMIN]==0; c_cc[VTIME]==0;
非阻塞,
read函数将立即返回实际读取的字节数,没有读取到则返回0c_cc[VMIN]>0; c_cc[VTIME]==0;
阻塞,串口缓冲区中至少有
c_cc[VMIN]个字节可供读取时,read才会返回,read的返回值为c_cc[VMIN]与read的len参数中的较小者。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;即可;阻塞的设置相对比较复杂:
如果需要保证每次
read的字节数,可以设置c_cc[VMIN]>0; c_cc[VTIME]==0;,但是需要注意,c_cc[]的数据类型cc_t实际上为unsigned char,其取值范围为0~255如果需要为
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 | fd_set set; |
关于select()函数的使用方法可以参考man手册