Linux 串口编程学习记录(termios.h)

目录

0、Linux 串口编程简述

1、open() / close() 函数

open() 函数参数说明:

close() 函数参数说明

2、termios 结构体

a、c_cflag 控制模式标志位可选参数(unsigned short,16位)

b、c_iflag 输入模式标志位可选参数(unsigned short,16位)

c、c_oflag 输入模式标志位可选参数(unsigned short,16位)

d、c_lflag 本地模式标志位可选参数(unsigned short,16位)

e、c_cc[NCC] 数组:特殊控制字符

3、tcsetattr() / tcgetatt()函数

4、tcflush() 函数

5、write()/read() 函数

6、ioctl() 函数

7、其他函数tcdrain() / tcflow()


0、Linux 串口编程简述

Linux 上 C++ 的串口编程主要 API 都定义在了在头文件 termios.h 中。Linux 串口与单片机串口本质上是一样的,在初始化阶段都是要设置波特率、停止位、奇偶校验位等属性的。

C++ Linux串口编程的一般流程为:

  1. 使用 open() 函数(POSIX标准函数,需包含头文件 fcntl.h)打开串口;
  2. 设置结构体 termios 中的对应参数(串口的所有属性都在这个结构体里设置,后面会细说);
  3. 调用 tcflush() 函数选择性地清空 I/O 缓冲区中的数据;
  4. 调用 tcsetattr() 函数,并传入 termios 结构体(参数之一)来初始化串口;
  5. 调用 write()/read() 函数(需包含头文件 unistd.h)进行串口的读写;
  6. 调用 close() 函数(需包含头文件 unistd.h)关闭串口。

下面对以上提到的函数和结构体逐一介绍:

1、open() / close() 函数

open() / close() 函数分别定义在头文件 fcntl.h 和 unistd.h 中,是 Linux 中的底层系统调用函数,且都是遵循 POSIX(Portable Operating System Interface of UNIX,可移植操作系统接口) 标准的函数。

Linux 中的所有 open 类的函数比如 fopen()/fclose() 都是在 open() / close() 的基础上封装得到的,因此不管是打开一个串口、一个 socket 还是一个文件,本质上都是通过 open 函数实现的,因为在 Linux 中一切皆文件嘛,以下介绍是对Linux文件通用的,当然也适用于串口操作。可以先看一下 open() / close() 函数的定义:

#incude <fcntl.h>
// 若打开成功则返回新文件描述符 fd(一个整型数),若出错为-1
// 有 两参 和 三参 版本
int open(const char* file, int oflags)
int open(const char* file, int oflags, mode_t mode)

#incude <unistd.h>
// 若关闭成功则返回 0, 否则返回 -1,关闭一个已关闭的描述符会出错
int close(int fd) 

open() 函数参数说明:

文件描述符 fd 是一个整型数,系统是怎么通过这个整型数来操作串口的呢?可以看下这个解释。总的来说,这个返回的整型句柄 fd 其实相当于一个结构体数组的索引,系统可以通过这个索引值对应的结构体来操作相应的串口。Linux 中 fd=0 代表标准输入,fd=1 代表标准输出, fd=2 代表标准错误。

oflag 用来设置一些标志位进而控制文件(或者说串口等)的打开模式

1、O_RDONLY(只读) / O_WRONLY(只写) / O_RDWR(可读可写):这三种参数在使用时只能三选一使用
# 另外还有其他模式可以设置:
2、O_APPEND   : 每次写操作都写入文件的末尾,不影响已存在的数据。
3、O_CREAT    : 如果指定文件不存在,则创建这个文件。使用此选项时,需要同时说明第三个参数mode,用其说明该新文件的存取许可权限。
4、O_EXCL     : 如果要创建的文件已存在,则返回 -1,并且修改errno的值。
5、O_TRUNC    : 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容。
6、O_NOCTTY   : 该标志用于告知系统它不会成为进程的控制终端。如果不指定该标志,任何输入都会影响程序(比如键盘中止信号等)。
7、O_NONBLOCK : 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O 设置为非阻塞模式(nonblocking mode)。
8、O_NDELAY   : 告诉内核这个程序不关注 DCD 信号线的状态。如果不指定该标志,当 DCD 信号线是空电压值的时候,程序将会进入睡眠。
9、O_SYNC     : 同步方式写入文件
10、O_DSYNC   : 提供同步的I/O数据完整性
11、O_ASYNC   : 当I/O操作可行,产生信号通知进程
12、O_DIRECT  : 无缓冲输入输出

注:O_NDELAY 标志位中出现了 DCD(Data Carrier Detect,载波检测),这里解释一下其作用是 Received Line Signal Detector,即 接收线信号检出。当本地的 DCE(Data Communications Equipment,数据通信设备)收到由通信链路另一端的 DCE 送来的载波信号时,使 DCD 变为有效(高电平),表示已检测出远端的载波信号,要求数据终端设备(DTE)准备接收。DCD 信号通常来自串口连结线的另一端。这条信号线上的space电压表示另一端的电脑或者设备现在已经连接。但是,DCD 信号线却不是总可以得到的,有些设备上有这条信号线,而有的则没有。串口通信中一般有这条线。

使用示例如下:

fd = open("/dev/ttyUSB0", O_RDWR|O_NOCTTY|O_NONBLOCK);

mode 参数在第二个参数中有 O_CREAT 时才起作用,也就是说只有新建文件时才填写,用8进制的数(与umask有关)代表新建文件的权限。具体的使用方式我目前没涉及到,暂时先不记录了。

Linux中的 umask 函数主要用于在创建新文件或目录时 屏蔽掉新文件或目录不应有的访问允许权限,直接在 Linux 终端中输入 umask 即可查看本地 umask 权限值。在给文件赋权限时,我们使用 open 函数给的权限其实并不是文件真正的权限,还要与文件本地的掩码按位与之后的结果才是其真正的权限。通过 umask 可以获取本地的掩码或者修改本地的掩码。

贴个链接可以参考下:Linux之open()、close()函数_昒曦的博客-CSDN博客_close 头文件 

close() 函数参数说明

close 函数只有一个参数,即文件描述符,传入已打开文件的文件描述符即可关闭该文件。若关闭成功则返回 0,否则返回 -1 。

2、termios 结构体

termios 结构体中定义了设置串口有关的参数,tcsetattr() 函数用来根据 termios 结构体的设置来激活配置,其定义如下:

typedef unsigned char	cc_t;
typedef unsigned int	speed_t;
typedef unsigned int	tcflag_t;
#define NCCS 32

// termios 结构体 在头文件 termios-struct.h 里
struct termios{
    tcflag_t c_iflag;    // 输入模式标志。控制终端驱动程序从串口或键盘接收到的字符数据在被传递给应用程序之前的处理方式。
    tcflag_t c_oflag;    // 输出模式标志。控制由应用程序发送出去的字符数据在传递到串口或屏幕之前是如何处理的。
    tcflag_t c_cflag;    // 控制模式标志。对串口来说可以设置波特率,数据位、校验位、停止位的宽度、校验位等
    tcflag_t c_lflag;    // 本地模式用于控制终端的本地数据处理和工作模式。
    cc_t     c_line;     // 线路规程
    cc_t     c_cc[NCCS];  // 特殊控制字符。特殊控制字符是一些字符组合,如 Ctrl+C、 Ctrl+Z 等, 当用户键入这样的组合键,终端会采取特殊处理方式。
    speed_t  c_ispeed;   // 终端的输入速度
    speed_t  c_ospreed;  // 终端的输出速度
#define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
#define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
};

// tcsetattr() 函数定义 定义在头文件 termios.h 中
tcsetattr(int fd, int optional_actions, const struct termios *termios_p)

a、c_cflag 控制模式标志位可选参数(unsigned short,16位)

c_cflag 参数可以用来设置波特率数据位校验位停止位的宽度、校验位等。在设置 c_cflag 参数时要用位操作来设置或清除相应的位,在源码中的有关宏定义如下:

// 波特率设置 以下定义在头文件 bits/termios.h 中
#define  B0	      0000000  // 挂起
#define  B50	  0000001
#define  B75	  0000002
#define  B110	  0000003
#define  B134	  0000004
#define  B150	  0000005
#define  B200	  0000006
#define  B300	  0000007
#define  B600	  0000010
#define  B1200	  0000011
#define  B1800	  0000012
#define  B2400	  0000013
#define  B4800	  0000014
#define  B9600	  0000015
#define  B19200	  0000016
#define  B38400	  0000017
// 波特率设置 以下定义在头文件 termios-baud.h 中
// 在 Linux 系统下, 使用 CBAUD 位掩码所选择的位来指定串口波特率
#define  CBAUD    0001017  // 波特率的位掩码 (not in POSIX)
#define  B57600   0010001
#define  B115200  0010002
#define  B230400  0010003
#define  B460800  0010004
#define  B500000  0010005
#define  B576000  0010006
#define  B921600  0010007
#define  B1000000 0010010
#define  B1152000 0010011
#define  B1500000 0010012
#define  B2000000 0010013
#define  B2500000 0010014
#define  B3000000 0010015
#define  B3500000 0010016
#define  B4000000 0010017
#define __MAX_BAUD B4000000
// 以下均定义在头文件 termios-c_cflag.h 中
// 停止位,设置该值则为2个停止位,不设置则为1个停止位
#define CSTOPB	0000100
// 数据位
#define   CS5	0000000  // 5 位数据位
#define   CS6	0000020  // 6 位数据位
#define   CS7	0000040  // 7 位数据位
#define   CS8	0000060  // 8 位数据位
// 设置该值则为2个停止位,不设置则为1个停止位
#define CSTOPB	0000100
// 使能校验位
#define PARENB	0000400
// 设置该值则使用奇校验,否则使用偶校验
#define PARODD	0001000
// 接收使能,这个位和下面那个位都是要在开始的时候使能的
#define CREAD	0000200
// 本地连接(不改变端口所有者),这个位和上面那个位都是要在开始的时候使能的
#define CLOCAL	0004000
// termios.c_cflag |=CLOCAL |CREAD;

b、c_iflag 输入模式标志位可选参数(unsigned short,16位)

// 以下定义在头文件 termios-c_iflag.h 中
#define IGNBRK 0000001 // 忽略输入终止条件
#define BRKINT 0000002 // 当检测到输入终止条件时发送SIGINT信号
#define IGNPAR 0000004 // 忽略奇偶校验错误
#define PARMRK 0000010 // 奇偶校验错误掩码
#define INPCK  0000020 // 奇偶校验使能
#define ISTRIP 0000040 // 裁剪掉第8比特,将所有接收到的数据裁剪为 7 比特位
#define INLCR  0000100 // 将接收到的 NL(换行)映射到 CR(回车)
#define IGNCR  0000200 // 忽略接收到的 CR
#define ICRNL  0000400 // 将接收到的 CR 映射到 NL
#define IUCLC  0001000 // 将接收到的大写字符映射到小写字符 (not in POSIX)
#define IXON   0002000 // 使能输出软件控制流
#define IXANY  0004000 // 输入任意字符可以重新启动输出
#define IXOFF  0010000 // 使能输入软件控制流
#define IMAXBEL 0020000 // 当输入队列满时响铃(ring bell)(not in POSIX)
#define IUTF8  0040000 // 输入是 UTF8 (not in POSIX)

c、c_oflag 输入模式标志位可选参数(unsigned short,16位)

// 以下定义在头文件 termios-c_oflag.h 中
#define OPOST  0000001 // 启动输出处理功能,如果不设置该位,将忽略下面介绍的所有设置
#define OLCUC  0000002 // 将输出中的大写字符转换成小写字符(not in POSIX)
#define ONLCR  0000004 // 将输出中的换行符(NL '\n')转换成回车符(CR '\r')
#define OCRNL  0000010 // 将输出中的回车符转换成换行符
#define ONOCR  0000020 // 如果当前列号为0,则不输出回车字符
#define ONLRET 0000040 // 不输出回车符
#define OFILL  0000100 // 发送填充字符以提供延时
#define OFDEL  0000200 // 如果设置该标志,则表示填充字符为 DEL 字符,否则为 NULL

#define NLDLY  0000400 // 换行符(newline)延时掩码
#define CRDLY  0003000 // 回车符(carriage-return)延时掩码
#define TABDLY 0014000 // 制表符(horizontal-tab)延时掩码
#define BSDLY  0020000 // 退格符(backspace)延时掩码
#define FFLDY  0100000 // 换页符(form-feed)延时掩码
#define VTDLY  0040000 // 垂直制表符(vertical-tab)延时掩码

d、c_lflag 本地模式标志位可选参数(unsigned short,16位)

// 以下定义在头文件 termios-c_lflag.h 中
#define ISIG   0000001  // 若收到信号字符(INTR,QUIT等)则会产生相应的信号
#define ICANON 0000002  // 启动规范(canonical)模式
#define ECHO   0000010  // 启动输入字符的本地回显功能
#define ECHOE  0000020  // 若设置ICANON,则允许退格操作
#define ECHOK   0000040 // 若设置ICANON,则KILL字符会删除当前行
#define ECHONL  0000100 // 若设置ICANON,则允许回显换行符
#define NOFLASH 0000200 // 在通常情况下,当接收到INTR,QUIT,SUSP控制字符时,会清空输入和输出队列。如果设置该标志,则所有的队列不会被清空
#define TOSTOP  0000400 // 若一个后台进程试图向它的控制终端进行写操作,则系统向该后台进程的进程组发送SIGTTOU信号。该信号通常终止进程的执行
#define ECHOCTL 0001000 // 若设置ECHO,则控制字符 Y(Y可以是回车符、制表符等)会显示成 "^X" 的样子,其中 "X" 的ASCII码等于给相应的控制字符的 ASCII 码加上 0x40。
                        // 例如,退格字符(0x08)会显示为“^H”('H'的 ASCII 码为 0x48)。这与我们在终端中按下 Ctrl+C 后会显示一个 "^C" 类似。
#define ECHOPRT 0002000 // 若设置ICANON和IECHO, 则删除字符和被删除的字符都会被显示
#define ECHOKE  0004000 // 若设置ICANON, 则允许回显在ECHOE和ECHOPRT中设定的KILL字符
#define IEXTEN  0100000 // 启动输入处理功能

注:规范模式非规范模式

ICANON 被用来设置规范模式。规范模式下,输入终端知道读到了行结束标志(回车符文件结束符 EOF(end of file))时输入缓冲区的数据才会被送到 read() 函数完成读取,也就是这种情况下的输入时基于行的操作。这与我们平时使用 cin 的时候的终端处理流程一样。

非规范模式下,所有的输入是即时有效的,不需要用户另外输入行结束符,而且不可进行行编辑。

下面的内容可以看到 c_cc 特殊控制字符中的 VTIME 与 VMIN 在非规范模式下的使用方式。

e、c_cc[NCC] 数组:特殊控制字符

c_cc 数组用于保存终端驱动程序中的特殊字符,如输入结束符等,可以理解为它是用来定义当输入为特殊字符(如ctrl+...的快捷键输入时)时程序应该做出哪些特殊的操作

#define VINTR  0 // (Ctrl-C) 中断控制字符。
                 // 该字符使终端驱动程序向与终端相连的进程以送SIGINT信号  
#define VQUIT  1 // (Ctrl-Z) 退出操作符。
                 // 该字符使终端驱动程序向与终端相连的进程发送SIGQUIT信号
#define VERASE 2 // (Backspace) 删除操作符。
                 // 该字符使终端驱动程序删除输入行中的最后一个字符,但不删除上一个EOF或行首。
                 // 当设置 ICANON 时可被识别,不再作为输入传递。  
#define VKILL  3 // (Ctrl-U) 删除行符。
                 // 删除输入行,即删除自上一个 EOF(文件结束符) 或行首以来的输入。
                 // 当设置 ICANON 时可被识别,不再作为输入传递。
#define VEOF   4 // (Ctrl-D) 文件结束符。
                 // 这个字符使得缓冲中的内容被送到等待输入的用户程序中,而不必等到 EOL。如果它
                 // 是一行的第一个字符,那么用户程序的 read() 将返回 0, 表示文件结束,即读到了EOF。 
                 // 当设置 ICANON 时可被识别,不再作为输入传递。
#define VTIME  5 // 在非规范模式下,指定读取的每个字符之间的超时时间,以十分之一秒为计时单位。
#define VMIN   6 // 在非规范模式下,指定最少读取的字符数。  
#define VSWTC  7 // 开关字符。(not in POSIX) 
#define VSTART 8 // (Ctrl-Q) 开启字符。重新开始被 STOP 字符中止的输出。
#define VSTOP  9 // (Ctrl-S) 停止字符。停止输出,直到键入 START 字符。 
#define VSUSP 10 // (Ctrl-Z) 挂起字符。向与终端相连的进程发送 SIGTSTP 信号,用于挂起当前的应用程序
#define VEOL  11 // 附加的行尾字符 EOL。对应键为 Carriage return(CR), 作用类似于行结束符。
                 // 当设置 ICANON 时可被识别。
#define VREPRINT 12 // (Ctrl-R) 重新输出未读的字符。
                    // 当设置 ICANON 和 IEXTEN 时可被识别,不再作为输入传递。  
#define VDISCARD 13 // (Ctrl-O) 开关:开始/结束丢弃未完成的输出。
                    // 当设置IEXTEN 时可被识别,不再作为输入传递。 
#define VWERASE  14 // (Ctrl-W) 删除词。
                    // 当设置 ICANON 和 IEXTEN 时可被识别,不再作为输入传递。  
#define VLNEXT   15 // (Ctrl-V) 字面上的下一个。引用下一个输入字符,取消它的任何特殊含义。
                    // 当设置IEXTEN 时可被识别,不再作为输入传递。  
#define VEOL2    16 // 第二行结束字符。,对应键为 Line feed(LF)。
                    // 当设置ICANON 时可被识别。  

VTIME和VMIN常规情况下,设置为0。但是有些应用场景我们需要使用非规范模式,也就是将二者结合起来共同控制对串口的读取行为,参数组合说明如下:

  • VMIN = 0 和 VTIME = 0 :在这种情况下,read 调用总是立刻返回。如果有等待处理的字符,read 就会立刻返回;如果没有字符等待处理,read 调用返回0,并且不读取任何字符;
  • VMIN = 0 和 VTIME > 0 :在这种情况下,只要有字符可以处理或者是经过 VTIME 个十分之一秒的时间间隔,read 调用就返回。如果因为超时而未读到任何字符,read 返回0,否则 read 返回读取的字符数目。
  • VMIN > 0 和 VTIME = 0 :在这种情况下,read 调用将一直等待,直到有 MIN 个字符可以读取时才返回,返回值是读取的字符数量。到达文件尾时返回0。
  • VMIN > 0 和 VTIME > 0 :在这种情况下, 当有 MIN 个字节可读或者两个输入字符之间的时间间隔超过 TIME 个十分之一秒时, read()函数才返回。因为在输入第一个字符后系统才会启动定时器,所以,在这种情况下, read()函数至少读取一个字节后才返回。

一个典型的串口初始化例子如下(这是我从最近看的一个云台设备的控制源码中截取的部分):

......
struct termios mytio;
// 波特率 38400, 8 位数据位, 使能接收并忽略调制解调器控制线
mytio.c_cflag = B38400 | CS8 | CLOCAL | CREAD; 
mytio.c_iflag     = IGNPAR;  // 忽略奇偶校验错误
mytio.c_oflag     = 0;  // 不启用输出控制
mytio.c_cc[VTIME] = 0;  // 规范模式
mytio.c_cc[VMIN]  = 0;  // 规范模式

tcflush(fd, TCIFLUSH);  // 清空输入缓冲区
tcsetattr(fd, TCSANOW, &mytio);  // 初始化串口
......

3、tcsetattr() / tcgetatt()函数

tcsetattr() 函数可以设置串口的结构属性,tcgetatt() 可以获取串口的结构属性。

tcsetattr() 函数用来根据 termios 结构体的设置来激活配置。声明如下:

// tcsetattr() 函数定义
tcsetattr(int fd, int optional_actions, const struct termios *termios_p)

tcsetattr() 函数的第一个参数是是文件描述符,第三个参数 termios 是结构体指针。第二个参数 optional_actions 指定了什么时候改变会起作用,可选值如下:

// 源码中都是宏定义的形式
// 配置的修改立即生效
#define	TCSANOW		0  
// 改变在所有写入 fd 的输出都被传输后生效。这个函数应当用于修改影响输出的参数时使用。(当前输出完成时将值改变) 
#define	TCSADRAIN	1 
// 改变在所有写入 fd 引用的对象的输出都被传输后生效,所有已接受但未读入的输入都在改变发生前丢弃(同TCSADRAIN,但会舍弃当前所有值)。 
#define	TCSAFLUSH	2  

可以通过  tcgetattr() 函数获取 termios  结构体属性

// 函数声明,获取成功返回 0,否则返回 -1
int tcgetattr(int fd, struct termios *termios_p)
/*
使用方法举例
*/
struct termios getitem;
if(tcgetattr(fd, &getitem) == -1)
{
    cout << "failed" << endl;
}

4、tcflush() 函数

不过在调用 tcsetattr() 之前最好调用 tcflush() 函数选择性地清空 I/O 缓冲区(输入、输出或者两者都清空)中的数据,其声明如下:

int tcflush(int fd, int queue_selector)

参数 queue_selector 可以选择的值如下:

// 定义在头文件 bits/termios.h 中
#define TCIFLUSH 0   // 清空输入缓冲区
#define TCOFLUSH 1   // 清空输出缓冲区
#define TCIOFLUSH 2  // 清空输入和输出缓冲区
// 使用举例,清空输入缓冲区
tcflush(fd, TCIFLUSH)

5、write()/read() 函数

调用 write()/read() 函数(需包含头文件 unistd.h)事项串口数据的读写,在头文件 unistd.h 中,其原型如下

// 返回值是读/写的数据的数量,如果发生错误则返回 -1,如果读/写到 EOF 则返回 0。
ssize_t read(int fd, void* buf, size_t nbytes)
ssize_t write(int fd, const void* buf, size_t nbytes)

很容易理解,第一个参数 fd 是要操作的文件/设备的文件描述符,第二个参数 *buf 是要读写的数据的地址,第三个参数 nbytes 是要读写的数据字节数。如果在 read() 函数中被请求读取的数据字节数小于当前行可读取的字节数,则 read() 函数只会读取被请求的字节数,剩下的字节下次再被读取。

6、ioctl() 函数

可以调用 ioctl() 函数对设备的 IO 通道进行管理, ioctl() 函数的声明如下:

// 包含头文件 unistd.h ,第三个参数总是一个指针,但指针的类型依赖于request参数。
int ioctl(int fd, unsigned long int request, void *arg )

该函数的说明较为复杂,这里不展开讲解,有兴趣可以查看相关资料,或者文末的链接。

一个使用 ioctl() 获取缓冲区的字节数的例子:

// 获取接收缓存区中的字节数并赋值给第三个参数,此时第三个参数是整型指针
int bytes_available;
ioctl(fd, FIONREAD, &bytes_available)

7、其他函数tcdrain() / tcflow()

tcdrain() 函数的作用是阻塞程序,直到发送缓冲区的数据发送完毕,函数定义如下:

// 调用成功时返回 0;失败将返回-1,并设置 errno
int tcflush(int fd);

tcflow() 函数的作用是暂停或重新开启 fd 的传输,具体与 action 的设置有关,函数定义如下:

// 调用成功时返回 0;失败将返回-1,并设置 errno
int tcflow(int fd, int action);

action 可选的四个参数如下:

#define TCOOFF 0 // 暂停数据输出(输出传输)
#define TCOON  1 // 重新启动暂停的输出
#define TCIOFF 2 // 发送 STOP 字符,停止终端设备向系统发送数据
#define TCION  3 // 发送一个 START 字符,启动终端设备向系统发送数据;

参考:

linux应用编程12-串口终端_邻居家的小南瓜的博客-CSDN博客_linux串口应用

Linux ~ termios 串口编程 - Burden - 博客园

Linux串口应用编程详解(Serial)_Mculover666的博客-CSDN博客_linux 串口编程

linux termios结构_querdaizhi的博客-CSDN博客_linux termios

tcgetattr: 

tcgetattr函数的说明_petershina的博客-CSDN博客_tcgetattr函数

ioctl:

Linux内核驱动 --ioctl函数解析_一只特立独行的程序猿的博客-CSDN博客_linux驱动ioctl

ioctl( ) 函数 - 拦云 - 博客园 (cnblogs.com)

猜你喜欢

转载自blog.csdn.net/Flag_ing/article/details/125644852
今日推荐