三、Linux串口开发(详解)
具体来说,termios
所控制的终端通常包括:
-
物理终端:这些是传统的字符终端设备,比如通过串行接口连接的终端,键盘,串口通信设备等。
-
虚拟终端:这些通常是在现代操作系统中通过软件创建的终端,如 Linux 系统中的 /dev/tty 设备。这些设备提供了与物理终端类似的接口。
-
终端仿真器:这些是在图形用户界面环境下运行的程序,模仿传统终端的行为,例如 Linux 中的 GNOME 终端、Xterm 或 macOS 的 Terminal.app。
1 termios.h相关结构体和函数
1.1 struct termios
termios
(terminal input/output settings)结构体在 C 语言中用于控制终端接口的设置,包括输入、输出的字符处理和传输速率等。这个结构体定义在 <termios.h>
头文件中。这个头文件提供了控制 POSIX 终端接口所需的所有声明和宏定义。
struct termios
{
tcflag_t c_iflag; /* 输入模式flag */
tcflag_t c_oflag; /* 输出模式flag */
tcflag_t c_cflag; /* 控制模式flag */
tcflag_t c_lflag; /* 本地模式flag */
cc_t c_line; /* 行控制line discipline */
cc_t c_cc[32]; /* 控制字符 */
speed_t c_ispeed; /* 输入波特率 */
speed_t c_ospeed; /* 输出波特率 */
};
1.1.1 c_iflag
/* filename: termios-c_iflag.h */
/* c_iflag bits */
#define IGNBRK 0000001 /* 忽略BREAK键输入,如果设置了IGNBRK,BREAK键的输入将被忽略 */
#define BRKINT 0000002 /* 如果设置了BRKINT ,将产生SIGINT中断 */
#define IGNPAR 0000004 /* 忽略奇偶校验错误 */
#define PARMRK 0000010 /* 标识奇偶校验错误 */
#define INPCK 0000020 /* 允许输入奇偶校验 */
#define ISTRIP 0000040 /* 去除字符的第8个比特 */
#define INLCR 0000100 /* 将输入的NL(换行)转换成CR(回车) */
#define IGNCR 0000200 /* 忽略输入的回车 */
#define ICRNL 0000400 /* 将输入的回车转化成换行(如果IGNCR未设置的情况下) */
#define IUCLC 0001000 /* 将输入的大写字符转换成小写字符(非POSIX) */
#define IXON 0002000 /* 允许输入时对XON/XOFF流进行控制 */
#define IXANY 0004000 /* 输入任何字符重启输出 */
#define IXOFF 0010000 /* 使能开始/停止输入控制 */
#define IMAXBEL 0020000 /* 当输入队列满的时候开始响铃,Linux在使用该参数而是认为该参数总是已经设置 */
#define IUTF8 0040000 /* Input is UTF8 (not in POSIX). */
1.1.2 c_oflag
/* filename: termios-c_oflag.h */
#define OPOST 0000001 /* 处理后输出 */
#define OLCUC 0000002 /* 将输入的小写字符转换成大写字符(非POSIX) */
#define ONLCR 0000004 /* 将输入的NL(换行)转换成CR(回车)及NL(换行) */
#define OCRNL 0000010 /* 将输入的CR(回车)转换成NL(换行) */
#define ONOCR 0000020 /* 第一行不输出回车符 */
#define ONLRET 0000040 /* 不输出回车符 */
#define OFILL 0000100 /* 发送填充字符以延迟终端输出 */
#define OFDEL 0000200 /* 以ASCII码的DEL作为填充字符,如果未设置该参数,填充字符将是NUL(‘/0’)(非POSIX) */
1.1.3 c_cflag
/* filename: termios-c_cflag.h */
#define CSIZE 0000060 /* : 这是一个掩码值,用于设置字符大小的位数。这个掩码可以与下面的CS5、CS6、CS7、CS8一起使用,来指定每个字符的位数。 */
#define CS5 0000000 /* 设置字符大小为5位。 */
#define CS6 0000020 /* 设置字符大小为6位。 */
#define CS7 0000040 /* 设置字符大小为7位。 */
#define CS8 0000060 /* 设置字符大小为8位。 */
#define CSTOPB 0000100 /* 这个宏用来设置两个停止位而非一个停止位。停止位在串行通信中用于表示一个字符传输的结束。 */
#define CREAD 0000200 /* 启用接收器。这个宏用来控制端口是否能接收数据。 */
#define PARENB 0000400 /* 启用输出和输入的奇偶校验。如果设置了这个位,发送的数据会附加一个校验位,接收时也会检查校验位。 */
#define PARODD 0001000 /* 设置为奇校验。仅当PARENB也被设置时才有效,用于定义是使用奇校验还是偶校验。 */
#define HUPCL 0002000 /* 挂断控制。当所有打开该端口的进程都关闭了它的文件描述符时,如果设置了这个标志,设备连接会被挂断。 */
#define CLOCAL 0004000 /* 忽略调制解调器控制线,控制端口不受外部调制解调器控制线的影响,这样可以在不需要调制解调器的情况下使用串口。 */
1.1.4 c_lflag
/* filename: termios-c_lflag.h */
#define ISIG 0000001 /* 当输入INTR、QUIT、SUSP或DSUSP时,产生相应的信号 */
#define ICANON 0000002 /* 使用标准输入模式 (erase and kill processing). */
#define ECHO 0000010 /* 显示输入字符 */
#define ECHOE 0000020 /* 如果ICANON同时设置,ERASE将删除输入的字符,WERASE将删除输入的单词 */
#define ECHOK 0000040 /* 如果ICANON同时设置,KILL将删除当前行 */
#define ECHONL 0000100 /* 如果ICANON同时设置,即使ECHO没有设置依然显示换行符 */
#define NOFLSH 0000200 /* 中断或者退出后,不使能flush */
#define TOSTOP 0000400 /* 向后台输出发送SIGTTOU信号 */
1.2 tcgetattr ()
/**
* @brief: 该函数可以用来获取当前终端设备的特定设置和属性。
* @param __fd: 要获取属性的终端设备文件描述符。
* @param __termios_p: 指向结构体 termios 的指针,用于存储获取到的终端属性。
* @return: 如果成功,返回0;如果出错,返回-1,并设置错误码。
*/
int tcgetattr (int __fd, struct termios *__termios_p);
1.3 tcsetattr ()
/**
* @brief: 设置与终端相关的参数
* @param __fd: 要设置属性的终端设备文件描述符。
* @param __optional_actions: 修改终端参数起作用的时间。
* TCSANOW:不等数据传输完毕就立即改变属性。
* CSADRAIN:等待所有数据传输结束才改变属性。
* CSAFLUSH:等待所有数据传输结束,清空输入输出缓冲区才改变属性。
* @param __termios_p: 指向结构体 termios 的指针,用于存储设置到终端属性。
* @return: 如果成功,返回0;如果出错,返回-1,并设置错误码。
*/
int tcsetattr (int __fd, int __optional_actions, const struct termios *__termios_p)
1.4 tcflush ()
/**
* @brief: 清空终端未完成的输入/输出请求及数据。
* @param __fd: 终端设备文件描述符。
* @param __queue_selector: 控制tcflush的操作,取值为下面三个常数中的一个:
* TCIFLUSH:清除正收到的数据,且不会读取出来。
* TCOFLUSH:清除正写入的数据,且不会发送至终端。
* TCIOFLUSH:清除所有正在发生的I/O数据。
* @return: 如果成功,返回0;如果出错,返回-1,并设置错误码。
*/
int tcflush (int __fd, int __queue_selector)
1.5 cfsetospeed ()
/**
* @brief: 输出波特率
*/
int cfsetospeed (struct termios *__termios_p, speed_t __speed)
1.6 cfsetispeed ()
/**
* @brief: 输入波特率
*/
int cfsetispeed (struct termios *__termios_p, speed_t __speed)
2 fcntl.h相关函数
2.1 open ()
/**
* @brief: 在Linux下一般用来打开或者创建一个文件,我们可以根据参数来定制我们需要的文件的属性和用户权限等各种参数。
* @param __path: 指向想要打开的文件路径名,或者文件名。我们需要注意的是,这个路径名是绝对路径名。文件名则是在当前路径下的。
* @param __oflag: 表示打开文件所采用的操作,具体参数解释看后面。
* @return: 返回值是文件描述符,如果打开文件成功返回一个正整数,否则返回-1。
*/
int
open (const char *__path, int __oflag, ...);
__oflag
参数表示打开文件所采用的操作,我们需要注意的是:必须指定以下三个常量的一种,且只允许指定一个:
-
O_RDONLY:只读模式
-
O_WRONLY:只写模式
-
O_RDWR:可读可写
以下的常量是选用的,这些选项是用来和上面的必选项进行按位或起来作为flags参数。
-
O_APPEND 表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。
-
O_CREAT 表示如果指定文件不存在,则创建这个文件
-
O_EXCL 表示如果要创建的文件已存在,则出错,同时返回 -1,并且修改 errno 的值。
-
O_TRUNC 表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为0。
-
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
-
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)
以下三个常量同样是选用的,它们用于同步输入输出
-
O_DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
-
O_RSYNC read 等待所有写入同一区域的写操作完成后再进行
-
O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O
2.1.1 串口通信
fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY);
- O_NOCTTY:不会使得串口终端成为进程的控制终端。比如键盘就是进程的控制终端,按下
Ctr+C
就会结束进程。 - O_NDELAY:使I/O变成非搁置模式(non-blocking),在读取read不到数据或是write写入缓冲区已满会马上return,而不会搁置程序动作,直到有数据或写入完成。
/* filename: fcntl-linux.h */
#define O_NDELAY O_NONBLOCK
3 unistd.h
unistd.h
(UNIX standard) 是 Unix 和类 Unix 操作系统中的一个标准头文件,它提供了对 POSIX (Portable Operating System Interface) 操作系统 API 的访问。这个头文件包含了很多用于底层系统服务的函数声明,如文件操作、进程控制、管道和符号链接等。
3.1 read()
/**
* @brief: 是一个 POSIX 标准中定义的系统调用,用于向文件描述符或者套接字写入数据。
* @param fd: 是要写入数据的文件描述符或者套接字。
* @param buf: 是用于存储读取数据的缓冲区的起始地址。
* @param count: 是要读取的最大字节数。
* @return: 函数返回值是读取到的字节数,如果返回值为 0,表示已经读取到文件末尾;如果返回值为 -1,表示发生错误。
*/
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
该函数可能会读终端设备、网络、文件等情况:
-
读常规文件时,一般会在有限时间内返回,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。
-
从终端设备读,通常以行为单位,读到换行符就返回了。
-
从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数。
3.2 write()
/**
* @brief: 是一个 POSIX 标准中定义的系统调用,用于向文件描述符或者套接字写入数据。
* @param fd: 是要写入数据的文件描述符或者套接字。
* @param buf: 是要写入的数据的缓冲区的起始地址。
* @param count: 是要写入的数据的字节数。
* @return: 函数返回值是写入成功的字节数,如果发生错误则返回 -1。
*/
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
当应用程序调用write函数写入数据时,如果写入的数据大小超过了内核中socket或文件描述符的缓冲区大小,那么写操作就会被阻塞。在这种情况下,write函数将一直等待,直到缓冲区有足够的空间来写入数据。
3.3 close()
/**
* @brief: 用于关闭一个已打开的文件描述符。
*/
#include <unistd.h>
int close(int fd);