Linux_应用篇(20) 串口应用编程

本小节我们来学习 Linux 下串口应用编程,串口(UART)是一种非常常见的外设,串口在嵌入式开发领域当中一般作为一种调试手段,通过串口输出调试打印信息,或者通过串口发送指令给主机端进行处理;当然除了作为基本的调试手段之外,还可以通过串口与其他设备或传感器进行通信,譬如有些 sensor 就使用了串口通信的方式与主机端进行数据交互。本章将会讨论如下主题内容。

⚫ 串口应用编程介绍

⚫ 应用编程实战

串口应用编程介绍

串口全称叫做串行接口,串行接口指的是数据一个一个的按顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通信距离远,但是速度相对会低,串口是一种很常用的工业接口。 串口(UART)在嵌入式 Linux 系统中常作为系统的标准输入、输出设备, 系统运行过程产生的打印信息通过串口输出;同理,串口也作为系统的标准输入设备, 用户通过串口与 Linux 系统进行交互。所以串口在 Linux 系统就是一个终端, 提到串口, 就不得不引出"终端(Terminal)"这个概念了。

终端 Terminal

终端就是处理主机输入、输出的一套设备,它用来显示主机运算的输出,并且接受主机要求的输入。典型的终端包括显示器键盘套件,打印机打字机套件等。其实本质上也就一句话,能接受输入、能显示输出,这就够了,不管到了什么时代,终端始终扮演着人机交互的角色,所谓 Terminal,即机器的边缘!只要能提供给计算机输入和输出功能,它就是终端,而与其所在的位置无关。

终端的分类

⚫ 本地终端: 例如对于我们的个人 PC 机来说, PC 机连接了显示器、键盘以及鼠标等设备,这样的一个显示器/键盘组合就是一个本地终端;同样对于开发板来说也是如此,开发板也可以连接一个LCD 显示器、键盘和鼠标等,同样可以构成本地终端。

⚫ 用串口连接的远程终端: 对于嵌入式 Linux 开发来说,这是最常见的终端---串口终端。譬如我们的开发板通过串口线连接到一个带有显示器和键盘的 PC 机,在 PC 机通过运行一个终端模拟程序,譬如 Windows 超级终端、 putty、 MobaXterm、 SecureCRT 等来获取并显示开发板通过串口发出的数据、同样还可以通过这些终端模拟程序将用户数据通过串口发送给开发板 Linux 系统,系统接收到数据之后便会进行相应的处理、譬如执行某个命令,这就是一种人机交互!

⚫ 基于网络的远程终端: 譬如我们可以通过 ssh、 Telnet 这些协议登录到一个远程主机。

以上列举的这些都是终端,前两类称之为物理终端;最后一个称之为伪终端。前两类都是在本地就直接关联了物理设备的,譬如显示器、鼠标键盘、串口等之类的,这种终端叫做物理终端,而第三类在本地则没有关联任何物理设备,不要把物理网卡当成终端关联的物理设备,它们与终端并不直接相关,所以这类不直接关联物理设备的终端叫做伪终端。

终端对应的设备节点

在 Linux 当中,一切皆是文件。当然,终端也不例外,每一个终端在/dev 目录下都有一个对应的设备节点。

⚫ /dev/ttyX(X 是一个数字编号,譬如 0、 1、 2、 3 等) 设备节点: ttyX(teletype 的简称)是最令人熟悉的了,在 Linux 中, /dev/ttyX 代表的都是上述提到的本地终端, 包括/dev/tty1~/dev/tty63 一共63 个本地终端, 也就是连接到本机的键盘显示器可以操作的终端。事实上, 这是 Linux 内核在初始化时所生成的 63 个本地终端。 如下所示:

⚫ /dev/pts/X(X 是一个数字编号,譬如 0、 1、 2、 3 等)设备节点:这类设备节点是伪终端对应的设备节点,也就是说,伪终端对应的设备节点都在/dev/pts 目录下、以数字编号命令。 譬如我们通过ssh 或 Telnet 这些远程登录协议登录到开发板主机,那么开发板 Linux 系统会在/dev/pts 目录下生成一个设备节点,这个设备节点便对应伪终端,如下所示:

⚫ 串口终端设备节点/dev/ttymxcX(不同CPU命名可能不同):对于 ALPHA/Mini I.MX6U 开发板来说, 有两个串口,也就是有两个串口终端,对应两个设备节点,如下所示:

需要注意的是, mxc 这个名字不是一定的,这个名字的命名与驱动有关系(与硬件平台有关) ,如果你换一个硬件平台,那么它这个串口对应的设备节点就不一定是 mxcX 了; 譬如 ZYNQ 平台,它的系统中串口对应的设备节点就是/dev/ttyPSX(X 是一个数字编号) ,所以说这个名字它不是统一的,但是名字前缀都是以"tty"开头,以表明它是一个终端。在 Linux 系统下,我们可以使用 who 命令来查看计算机系统当前连接了哪些终端(一个终端就表示有一个用户使用该计算机) ,如下所示:

可以看到,开发板系统当前有两个终端连接到它,一个就是我们的串口终端,也就是开发板的 USB 调试串口(对应/dev/ttymxc0);另一个则是伪终端,这是笔者通过 ssh 连接的。

串口应用编程

现在我们已经知道了串口在 Linux 系统中是一种终端设备, 并且在我们的开发板上,其设备节点为/dev/ttymxc0(UART1)和/dev/ttymxc2(UART3) 。其实串口的应用编程也很简单,无非就是通过 ioctl()对串口进行配置,调用 read()读取串口的数据、调用 write()向串口写入数据, 是的,就是这么简单!但是我们不这么做,因为 Linux 为上层用户做了一层封装,将这些 ioctl()操作封装成了一套标准的 API,我们就直接使用这一套标准 API 编写自己的串口应用程序即可!笔者把这一套接口称为 termios API, 这些 API 其实是 C 库函数, 可以使用 man 手册查看到它们的帮助信息;这里需要注意的是,这一套接口并不是针对串口开发的,而是针对所有的终端设备,串口是一种终端设备,计算机系统本地连接的鼠标、键盘也是终端设备,通过 ssh 远程登录连接的伪终端也是终端设备。要使用 termios API,需要在我们的应用程序中包含 termios.h 头文件。

struct termios 结构体

对于终端来说,其应用编程内容无非包括两个方面的内容:配置和读写;对于配置来说,一个很重要的数据结构便是 struct termios 结构体,该数据结构描述了终端的配置信息, 这些参数能够控制、影响终端的行为、特性,事实上,终端设备应用编程(串口应用编程) 主要就是对这个结构体进行配置。

struct termios 结构体定义如下:

cpp 复制代码
struct termios
{
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};

如上定义所示,影响终端的参数按照不同模式分为如下几类:

⚫ 输入模式;

⚫ 输出模式;

⚫ 控制模式;

⚫ 本地模式;

⚫ 线路规程;

⚫ 特殊控制字符;

⚫ 输入速率;

⚫ 输出速率。

接下来,简单地给大家介绍下如何去配置这些参数、它们分别表示什么意思。

一、输入模式: c_iflag

输入模式控制输入数据(终端驱动程序从串口或键盘接收到的字符数据)在被传递给应用程序之前的处理方式。通过设置 struct termios 结构体中 c_iflag 成员的标志对它们进行控制。所有的标志都被定义为宏,除 c_iflag 成员外, c_oflag、 c_cflag 以及 c_lflag 成员也都采用这种方式进行配置。可用于 c_iflag 成员的宏如下所示:

|--------|-----------------------------|
| IGNBRK | 忽略输入终止条件 |
| BRKINT | 当检测到输入终止条件时发送 SIGINT 信号 |
| IGNPAR | 忽略帧错误和奇偶校验错误 |
| PARMRK | 对奇偶校验错误做出标记 |
| INPCK | 对接收到的数据执行奇偶校验 |
| ISTRIP | 将所有接收到的数据裁剪为 7 比特位、也就是去除第八位 |
| INLCR | 将接收到的 NL(换行符)转换为 CR(回车符) |
| IGNCR | 忽略接收到的 CR(回车符) |
| ICRNL | 将接收到的 CR(回车符)转换为 NL(换行符) |
| IUCLC | 将接收到的大写字符映射为小写字符 |
| IXON | 启动输出软件流控 |
| IXOFF | 启动输入软件流控 |

以上所列举出的这些宏,我们可以通过 man 手册查询到它们的详细描述信息,执行命令" man 3 termios",如下图所示:

二、输出模式: c_oflag

输出模式控制输出字符的处理方式,即由应用程序发送出去的字符数据在传递到串口或屏幕之前是如何处理的。可用于 c_oflag 成员的宏如下所示:

|--------|------------------------------------|
| OPOST | 启用输出处理功能,如果不设置该标志则其他标志都被忽略 |
| OLCUC | 将输出字符中的大写字符转换成小写字符 |
| ONLCR | 将输出中的换行符(NL '\n')转换成回车符(CR '\r') |
| OCRNL | 将输出中的回车符(CR '\r')转换成换行符(NL '\n') |
| ONOCR | 在第 0 列不输出回车符(CR) |
| ONLRET | 不输出回车符 |
| OFILL | 发送填充字符以提供延时 |
| OFDEL | 如果设置该标志,则表示填充字符为 DEL 字符,否则为 NULL字符 |

三、控制模式: c_cflag

控制模式控制终端设备的硬件特性,譬如对于串口来说,该字段比较重要,可设置串口波特率、数据位、校验位、停止位等硬件特性。通过设置 struct termios 结构中 c_cflag 成员的标志对控制模式进行配置。可用于 c_cflag 成员的标志如下所示:

|----------|--------------------------|
| CBAUD | 波特率的位掩码 |
| B0 | 波特率为 0 |
| ...... | ...... |
| B1200 | 1200 波特率 |
| B1800 | 1800 波特率 |
| B2400 | 2400 波特率 |
| B4800 | 4800 波特率 |
| B9600 | 9600 波特率 |
| B19200 | 19200 波特率 |
| B38400 | 38400 波特率 |
| B57600 | 57600 波特率 |
| B115200 | 115200 波特率 |
| B230400 | 230400 波特率 |
| B460800 | 460800 波特率 |
| B500000 | 500000 波特率 |
| B576000 | 576000 波特率 |
| B921600 | 921600 波特率 |
| B1000000 | 1000000 波特率 |
| B1152000 | 1152000 波特率 |
| B1500000 | 1500000 波特率 |
| B2000000 | 2000000 波特率 |
| B2500000 | 2500000 波特率 |
| B3000000 | 3000000 波特率 |
| ...... | ...... |
| CSIZE | 数据位的位掩码 |
| CS5 | 5 个数据位 |
| CS6 | 6 个数据位 |
| CS7 | 7 个数据位 |
| CS8 | 8 个数据位 |
| CSTOPB | 2 个停止位,如果不设置该标志则默认是一个停止位 |
| CREAD | 接收使能 |
| PARENB | 使能奇偶校验 |
| PARODD | 使用奇校验、而不是偶校验 |
| HUPCL | 关闭时挂断调制解调器 |
| CLOCAL | 忽略调制解调器控制线 |
| CRTSCTS | 使能硬件流控 |

在 struct termios 结构体中,有一个 c_ispeed 成员变量和 c_ospeed 成员变量,在其它一些系统中,可能会使用这两个变量来指定串口的波特率;在 Linux 系统下, 则是使用 CBAUD 位掩码所选择的几个 bit 位来指定串口波特率。事实上, termios API 中提供了 cfgetispeed()和 cfsetispeed()函数分别用于获取和设置串口的波特率。

四、本地模式: c_lflag

本地模式用于控制终端的本地数据处理和工作模式。 通过设置 struct termios 结构体中 c_lflag 成员的标志对本地模式进行配置。可用于 c_lflag 成员的标志如下所示:

|---------|------------------------------------------------------------------------------------------------------------------------|
| ISIG | 若收到信号字符(INTR、 QUIT 等),则会产生相应的信号 |
| ICANON | 启用规范模式 |
| ECHO | 启用输入字符的本地回显功能。当我们在终端输入字符的时候,字符 会显示出来,这就是回显功能 |
| ECHOE | 若设置 ICANON,则允许退格操作 |
| ECHOK | 若设置 ICANON,则 KILL 字符会删除当前行 |
| ECHONL | 若设置 ICANON,则允许回显换行符 |
| ECHOCTL | 若设置 ECHO,则控制字符(制表符、换行符等)会显示成"^X", 其中 X 的 ASCII 码等于给相应控制字符的 ASCII 码加上 0x40。例如, 退格字符(0x08)会显示为"^H"('H'的 ASCII 码为 0x48) |
| ECHOPRT | 若设置 ICANON 和 IECHO,则删除字符(退格符等)和被删除的字符都会被显示 |
| ECHOKE | 若设置 ICANON,则允许回显在 ECHOE 和 ECHOPRT 中设定的 KILL字符 |
| NOFLSH | 在通常情况下,当接收到 INTR、 QUIT 和 SUSP 控制字符时,会清空 输入和输出队列。如果设置该标志,则所有的队列不会被清空 |
| TOSTOP | 若一个后台进程试图向它的控制终端进行写操作,则系统向该后台进 程的进程组发送 SIGTTOU 信号。该信号通常终止进程的执行 |
| IEXTEN | 启用输入处理功能 |

五、特殊控制字符: c_cc

特殊控制字符是一些字符组合,如 Ctrl+C、 Ctrl+Z 等, 当用户键入这样的组合键,终端会采取特殊处理方式。 struct termios 结构体中 c_cc 数组将各种特殊字符映射到对应的支持函数。每个字符位置(数组下标)由对应的宏定义的,如下所示

⚫ VEOF:文件结尾符 EOF,对应键为 Ctrl+D; 该字符使终端驱动程序将输入行中的全部字符传递给正在读取输入的应用程序。如果文件结尾符是该行的第一个字符,则用户程序中的 read 返回 0,表示文件结束。

⚫ VEOL: 附加行结尾符 EOL,对应键为 Carriage return(CR) ; 作用类似于行结束符。

⚫ VEOL2: 第二行结尾符 EOL2,对应键为 Line feed(LF) ;

⚫ VERASE: 删除操作符 ERASE,对应键为 Backspace(BS) ; 该字符使终端驱动程序删除输入行中的最后一个字符;

⚫ VINTR: 中断控制字符 INTR,对应键为 Ctrl+C; 该字符使终端驱动程序向与终端相连的进程发送SIGINT 信号;

⚫ VKILL: 删除行符 KILL,对应键为 Ctrl+U, 该字符使终端驱动程序删除整个输入行;

⚫ VMIN:在非规范模式下,指定最少读取的字符数 MIN;

⚫ VQUIT: 退出操作符 QUIT,对应键为 Ctrl+Z; 该字符使终端驱动程序向与终端相连的进程发送SIGQUIT 信号。

⚫ VSTART:开始字符 START,对应键为 Ctrl+Q; 重新启动被 STOP 暂停的输出。

⚫ VSTOP:停止字符 STOP,对应键为 Ctrl+S; 字符作用"截流",即阻止向终端的进一步输出。用于支持 XON/XOFF 流控。

⚫ VSUSP:挂起字符 SUSP,对应键为 Ctrl+Z; 该字符使终端驱动程序向与终端相连的进程发送SIGSUSP 信号,用于挂起当前应用程序。

⚫ VTIME:非规范模式下, 指定读取的每个字符之间的超时时间(以分秒为单位) TIME。

在以上所列举的这些宏定义中, TIME 和 MIN 值只能用于非规范模式,可用于控制非规范模式下 read()调用的一些行为特性,后面再向大家介绍。

六、总结说明

上面已经给大家介绍了 struct termios 结构体中 c_iflag 成员(输入模式)、 c_oflag 成员(输出模式)、c_cflag 成员(控制模式)以及 c_lflag 成员(本地控制)这四个参数,这些参数能够分别控制、影响终端的行为特性。这里有两个问题需要向大家说明,首先第一个是关于这些成员变量赋值的问题。对于这些变量尽量不要直接对其初始化,而要将其通过"按位与"、"按位或" 等操作添加标志或清除某个标志。 譬如,通常不会这样对变量进行初始化:

cpp 复制代码
struct termios ter;
ter.c_iflag = IGNBRK | BRKINT | PARMRK;

而是要像下面这样:

cpp 复制代码
ter.c_iflag |= (IGNBRK | BRKINT | PARMRK | ISTRIP);

说完第一个问题之后,我们来看看第二个问题。

前面我们介绍了很多的标志,但是并非所有标志对于实际的终端设备来说都是有效的,就拿串口终端来说,串口可以配置波特率、数据位、停止位等这些硬件参数,但是其它终端是不一定支持这些配置的,譬如本地终端键盘、显示器,这些设备它是没有这些硬件概念的。因为这些终端设备都使用了这一套 API 来编程,然而不同的终端设备,本身硬件上就存在很大的区别,所以会导致这些配置参数并不是对所有终端设备都是有效的。 在使用过程中也不需要去搞懂所有标志的作用, 事实上,快速掌握一项技术的核心点才是一种学习能力!

终端的三种工作模式

当 ICANON 标志被设置时表示启用终端的规范模式,什么规范模式?这里给大家简单地说明一下。终端有三种工作模式,分别为规范模式(canonical mode)、非规范模式(non-canonical mode)和原始模式(raw mode)。通过在 struct termios 结构体的 c_lflag 成员中设置 ICANNON 标志来定义终端是以规范模式(设置 ICANNON 标志)还是以非规范模式(清除 ICANNON 标志)工作,默认情况为规范模式。在规范模式下,所有的输入是基于行进行处理的。在用户输入一个行结束符(回车符、 EOF 等)之前,系统调用 read()函数是读不到用户输入的任何字符的。除了 EOF 之外的行结束符(回车符等)与普通字符一样会被 read()函数读取到缓冲区中。在规范模式中,行编辑是可行的,而且一次 read()调用最多只能读取一行数据。如果在 read()函数中被请求读取的数据字节数小于当前行可读取的字节数,则 read()函数只会读取被请求的字节数,剩下的字节下次再被读取。在非规范模式下,所有的输入是即时有效的,不需要用户另外输入行结束符,而且不可进行行编辑。在非规范模式下,对参数 MIN(c_cc[VMIN])和 TIME(c_cc[VTIME])的设置决定 read()函数的调用方式。

上一小节给大家提到过, TIME 和 MIN 的值只能用于非规范模式,两者结合起来可以控制对输入数据的读取方式。 根据 TIME 和 MIN 的取值不同,会有以下 4 种不同情况:

⚫ MIN = 0 和 TIME = 0: 在这种情况下, read()调用总是会立即返回。若有可读数据,则读取数据并返回被读取的字节数; 否则读取不到任何数据并返回 0。

⚫ MIN > 0 和 TIME = 0:在这种情况下, read()函数会被阻塞, 直到有 MIN 个字符可以读取时才返回,返回值是读取的字符数量。到达文件尾时返回 0。

⚫ MIN = 0 和 TIME > 0:在这种情况下, 只要有数据可读或者经过 TIME 个十分之一秒的时间, read()函数则立即返回,返回值为被读取的字节数。如果超时并且未读到数据,则 read()函数返回 0。

⚫ MIN > 0 和 TIME > 0:在这种情况下, 当有 MIN 个字节可读或者两个输入字符之间的时间间隔超过 TIME 个十分之一秒时, read()函数才返回。因为在输入第一个字符后系统才会启动定时器,所以,在这种情况下, read()函数至少读取一个字节后才返回。

原始模式(Raw mode)

按照严格意义来讲,原始模式是一种特殊的非规范模式。在原始模式下,所有的输入数据以字节为单位被处理。在这个模式下,终端是不可回显的, 并且禁用终端输入和输出字符的所有特殊处理。 在我们的应用程序中,可以通过调用 cfmakeraw()函数将终端设置为原始模式。cfmakeraw()函数内部其实就是对 struct termios 结构体进行了如下配置:

cpp 复制代码
termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
| INLCR | IGNCR | ICRNL | IXON);
termios_p->c_oflag &= ~OPOST;
termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
termios_p->c_cflag &= ~(CSIZE | PARENB);
termios_p->c_cflag |= CS8;

什么时候会使用原始模式?串口在 Linux 系统下是作为一种终端设备存在,终端通常会对用户的输入、输出数据进行相应的处理,如前所述!但是串口并不仅仅只扮演着人机交互的角色(数据以字符的形式传输、也就数说传输的数据其实字符对应的 ASCII 编码值);串口本就是一种数据串行传输接口, 通过串口可以与其他设备或传感器进行数据传输、通信,譬如很多 sensor 就使用了串口方式与主机端进行数据交互。 那么在这种情况下,我们就得使用原始模式,意味着通过串口传输的数据不应进行任何特殊处理、不应将其解析成 ASCII 字符。

打开串口设备

好了,前面已经向大家详细地介绍了 struct termios 结构体以及终端的三种工作模式,为我们接下来的

要讲解的内容打下了一个基础。从本小节开始,我们来看看如何编写串口应用程序。

首先第一步便是打开串口设备,使用 open()函数打开串口的设备节点文件,得到文件描述符:

cpp 复制代码
int fd;
fd = open("/dev/ttymxc2", O_RDWR | O_NOCTTY);
if (0 > fd) {
    perror("open error");
    return -1;
}

调用 open()函数时,使用了 O_NOCTTY 标志,该标志用于告知系统/dev/ttymxc2 它不会成为进程的控制终端。

获取终端当前的配置参数: tcgetattr()函数

通常,在配置终端之前, 我们会先获取到终端当前的配置参数,将其保存到一个 struct termios 结构体对象中,这样可以在之后、很方便地将终端恢复到原来的状态,这也是为了安全起见以及后续的调试方便。tcgetattr()函数可以获取到串口终端当前的配置参数, tcgetattr 函数原型如下所示(可通过命令"man 3tcgetattr"查询):

cpp 复制代码
#include <termios.h>
#include <unistd.h>
int tcgetattr(int fd, struct termios *termios_p);

首先在我们的应用程序中需要包含 termios.h 头文件和 unistd.h 头文件。

第一个参数对应串口终端设备的文件描述符 fd。

调用 tcgetattr 函数之前,我们需要定义一个 struct termios 结构体变量,将该变量的指针作为 tcgetattr()函数的第二个参数传入; tcgetattr()调用成功后,会将终端当前的配置参数保存到 termios_p 指针所指的对象中。

函数调用成功返回 0;失败将返回-1,并且会设置 errno 以告知错误原因。

使用示例如下:

cpp 复制代码
struct termios old_cfg;
if (0 > tcgetattr(fd, &old_cfg)) {
    /* 出错处理 */
    do_something();
}

对串口终端进行配置

假设我们需要采用原始模式进行串口数据通信。
1)配置串口终端为原始模式

调用<termios.h>头文件中申明的 cfmakeraw()函数可以将终端配置为原始模式:

cpp 复制代码
struct termios new_cfg;
memset(&new_cfg, 0x0, sizeof(struct termios));

//配置为原始模式
cfmakeraw(&new_cfg);

这个函数没有返回值。
2)接收使能

使能接收功能只需在 struct termios 结构体的 c_cflag 成员中添加 CREAD 标志即可,如下所示:

cpp 复制代码
new_cfg.c_cflag |= CREAD; //接收使能

3)设置串口的波特率

设置波特率有专门的函数,用户不能直接通过位掩码来操作。设置波特率的主要函数有 cfsetispeed()和cfsetospeed(),这两个函数在<termios.h>头文件中申明, 使用方法很简单,如下所示:

cpp 复制代码
cfsetispeed(&new_cfg, B115200);
cfsetospeed(&new_cfg, B115200);

B115200 是一个宏,前面已经给大家介绍了, B115200 表示波特率为 115200。

cfsetispeed()函数设置数据输入波特率,而 cfsetospeed()函数设置数据输出波特率。一般来说,用户需将终端的输入和输出波特率设置成一样的。除了之外,我们还可以直接使用 cfsetspeed()函数一次性设置输入和输出波特率, 该函数也是在<termios.h>头文件中申明, 使用方式如下:

cpp 复制代码
cfsetspeed(&new_cfg, B115200);

这几个函数在成功时返回 0,失败时返回-1。
4)设置数据位大小

与设置波特率不同,设置数据位大小并没有现成可用的函数, 我们需要自己通过位掩码来操作、设置数据位大小。 设置方法也很简单, 首先将 c_cflag 成员中 CSIZE 位掩码所选择的几个 bit 位清零,然后再设置数据位大小,如下所示:

cpp 复制代码
new_cfg.c_cflag &= ~CSIZE;
new_cfg.c_cflag |= CS8; //设置为 8 位数据位

5)设置奇偶校验位

串口的奇偶校验位配置一共涉及到 struct termios 结构体中的两个成员变量: c_cflag 和 c_iflag。首先对于 c_cflag 成员,需要添加 PARENB 标志以使能串口的奇偶校验功能,只有使能奇偶校验功能之后才会对输出数据产生校验位,而对输入数据进行校验检查; 同时对于 c_iflag 成员来说,还需要添加 INPCK 标志,这样才能对接收到的数据执行奇偶校验,代码如下所示:

cpp 复制代码
//奇校验使能
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;
//偶校验使能
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD; /* 清除 PARODD 标志,配置为偶校验 */
new_cfg.c_iflag |= INPCK;
//无校验
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;

6)设置停止位

停止位则是通过设置 c_cflag 成员的 CSTOPB 标志而实现的。若停止位为一个比特, 则清除 CSTOPB 标志;若停止位为两个,则添加 CSTOPB 标志即可。以下分别是停止位为一个和两个比特时的代码:

cpp 复制代码
// 将停止位设置为一个比特
new_cfg.c_cflag &= ~CSTOPB;
// 将停止位设置为 2 个比特
new_cfg.c_cflag |= CSTOPB;

7)设置 MIN 和 TIME 的值

如前面所介绍那样, MIN 和 TIME 的取值会影响非规范模式下 read()调用的行为特征,原始模式是一种特殊的非规范模式,所以 MIN 和 TIME 在原始模式下也是有效的。

在对接收字符和等待时间没有特别要求的情况下,可以将 MIN 和 TIME 设置为 0, 这样则在任何情况下 read()调用都会立即返回,此时对串口的 read 操作会设置为非阻塞方式,如下所示:

cpp 复制代码
new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0;

缓冲区的处理

我们在使用串口之前,需要对串口的缓冲区进行处理,因为在我们使用之前,其缓冲区中可能已经存在一些数据等待处理或者当前正在进行数据传输、接收,所以使用之前, 所以需要对此情况进行处理。 这时就可以调用<termios.h>中声明的 tcdrain()、 tcflow()、 tcflush()等函数来处理目前串口缓冲中的数据,它们的函数原型如下所示:

cpp 复制代码
#include <termios.h>
#include <unistd.h>
int tcdrain(int fd);
int tcflush(int fd, int queue_selector);
int tcflow(int fd, int action);

调用 tcdrain()函数后会使得应用程序阻塞, 直到串口输出缓冲区中的数据全部发送完毕为止!调用 tcflow()函数会暂停串口上的数据传输或接收工作,具体情况取决于参数 action,参数 action 可取值如下:

⚫ TCOOFF:暂停数据输出(输出传输);

⚫ TCOON: 重新启动暂停的输出;

⚫ TCIOFF: 发送 STOP 字符,停止终端设备向系统发送数据;

⚫ TCION: 发送一个 START 字符,启动终端设备向系统发送数据;

再来看看 tcflush()函数,调用该函数会清空输入/输出缓冲区中的数据,具体情况取决于参数queue_selector,参数 queue_selector 可取值如下:

⚫ TCIFLUSH: 对接收到而未被读取的数据进行清空处理;

⚫ TCOFLUSH: 对尚未传输成功的输出数据进行清空处理;

⚫ TCIOFLUSH: 包括前两种功能,即对尚未处理的输入/输出数据进行清空处理。

以上这三个函数,调用成功时返回 0;失败将返回-1、并且会设置 errno 以指示错误类型。通常我们会选择 tcdrain()或 tcflush()函数来对串口缓冲区进行处理。譬如直接调用 tcdrain()阻塞:

cpp 复制代码
tcdrain(fd);

或者调用 tcflush()清空缓冲区:

cpp 复制代码
tcflush(fd, TCIOFLUSH);

写入配置、使配置生效: tcsetattr()函数

前面已经完成了对 struct termios 结构体各个成员进行配置,但是配置还未生效,我们需要将配置参数写入到终端设备(串口硬件),使其生效。通过 tcsetattr()函数将配置参数写入到硬件设备,其函数原型如下所示:

cpp 复制代码
#include <termios.h>
#include <unistd.h>
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

调用该函数会将参数 termios_p 所指 struct termios 对象中的配置参数写入到终端设备中,使配置生效!而参数 optional_actions 可以指定更改何时生效,其取值如下:

⚫ TCSANOW:配置立即生效。

⚫ TCSADRAIN: 配置在所有写入 fd 的输出都传输完毕之后生效。

⚫ TCSAFLUSH: 所有已接收但未读取的输入都将在配置生效之前被丢弃。

该函数调用成功时返回 0;失败将返回-1,、并设置 errno 以指示错误类型。譬如,调用 tcsetattr()将配置参数写入设备,使其立即生效:

cpp 复制代码
tcsetattr(fd, TCSANOW, &new_cfg);

读写数据: read()、 write()

所有准备工作完成之后,接着便可以读写数据了,直接调用 read()、 write()函数即可!

串口应用编程实践

cpp 复制代码
#define _GNU_SOURCE     //在源文件开头定义_GNU_SOURCE宏
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <termios.h>

typedef struct uart_hardware_cfg {
    unsigned int baudrate;      /* 波特率 */
    unsigned char dbit;         /* 数据位 */
    char parity;                /* 奇偶校验 */
    unsigned char sbit;         /* 停止位 */
} uart_cfg_t;

static struct termios old_cfg;  //用于保存终端的配置参数
static int fd;      //串口终端对应的文件描述符

/**
 ** 串口初始化操作
 ** 参数device表示串口终端的设备节点
 **/
static int uart_init(const char *device)
{
    /* 打开串口终端 */
    fd = open(device, O_RDWR | O_NOCTTY);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
        return -1;
    }

    /* 获取串口当前的配置参数 */
    if (0 > tcgetattr(fd, &old_cfg)) {
        fprintf(stderr, "tcgetattr error: %s\n", strerror(errno));
        close(fd);
        return -1;
    }

    return 0;
}

/**
 ** 串口配置
 ** 参数cfg指向一个uart_cfg_t结构体对象
 **/
static int uart_cfg(const uart_cfg_t *cfg)
{
    struct termios new_cfg = {0};   //将new_cfg对象清零
    speed_t speed;

    /* 设置为原始模式 */
    cfmakeraw(&new_cfg);

    /* 使能接收 */
    new_cfg.c_cflag |= CREAD;

    /* 设置波特率 */
    switch (cfg->baudrate) {
    case 1200: speed = B1200;
        break;
    case 1800: speed = B1800;
        break;
    case 2400: speed = B2400;
        break;
    case 4800: speed = B4800;
        break;
    case 9600: speed = B9600;
        break;
    case 19200: speed = B19200;
        break;
    case 38400: speed = B38400;
        break;
    case 57600: speed = B57600;
        break;
    case 115200: speed = B115200;
        break;
    case 230400: speed = B230400;
        break;
    case 460800: speed = B460800;
        break;
    case 500000: speed = B500000;
        break;
    default:    //默认配置为115200
        speed = B115200;
        printf("default baud rate: 115200\n");
        break;
    }

    if (0 > cfsetspeed(&new_cfg, speed)) {
        fprintf(stderr, "cfsetspeed error: %s\n", strerror(errno));
        return -1;
    }

    /* 设置数据位大小 */
    new_cfg.c_cflag &= ~CSIZE;  //将数据位相关的比特位清零
    switch (cfg->dbit) {
    case 5:
        new_cfg.c_cflag |= CS5;
        break;
    case 6:
        new_cfg.c_cflag |= CS6;
        break;
    case 7:
        new_cfg.c_cflag |= CS7;
        break;
    case 8:
        new_cfg.c_cflag |= CS8;
        break;
    default:    //默认数据位大小为8
        new_cfg.c_cflag |= CS8;
        printf("default data bit size: 8\n");
        break;
    }

    /* 设置奇偶校验 */
    switch (cfg->parity) {
    case 'N':       //无校验
        new_cfg.c_cflag &= ~PARENB;
        new_cfg.c_iflag &= ~INPCK;
        break;
    case 'O':       //奇校验
        new_cfg.c_cflag |= (PARODD | PARENB);
        new_cfg.c_iflag |= INPCK;
        break;
    case 'E':       //偶校验
        new_cfg.c_cflag |= PARENB;
        new_cfg.c_cflag &= ~PARODD; /* 清除PARODD标志,配置为偶校验 */
        new_cfg.c_iflag |= INPCK;
        break;
    default:    //默认配置为无校验
        new_cfg.c_cflag &= ~PARENB;
        new_cfg.c_iflag &= ~INPCK;
        printf("default parity: N\n");
        break;
    }

    /* 设置停止位 */
    switch (cfg->sbit) {
    case 1:     //1个停止位
        new_cfg.c_cflag &= ~CSTOPB;
        break;
    case 2:     //2个停止位
        new_cfg.c_cflag |= CSTOPB;
        break;
    default:    //默认配置为1个停止位
        new_cfg.c_cflag &= ~CSTOPB;
        printf("default stop bit size: 1\n");
        break;
    }

    /* 将MIN和TIME设置为0 */
    new_cfg.c_cc[VTIME] = 0;
    new_cfg.c_cc[VMIN] = 0;

    /* 清空缓冲区 */
    if (0 > tcflush(fd, TCIOFLUSH)) {
        fprintf(stderr, "tcflush error: %s\n", strerror(errno));
        return -1;
    }

    /* 写入配置、使配置生效 */
    if (0 > tcsetattr(fd, TCSANOW, &new_cfg)) {
        fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));
        return -1;
    }

    /* 配置OK 退出 */
    return 0;
}

/***
--dev=/dev/ttymxc2
--brate=115200
--dbit=8
--parity=N
--sbit=1
--type=read
***/
/**
 ** 打印帮助信息
 **/
static void show_help(const char *app)
{
    printf("Usage: %s [选项]\n"
        "\n必选选项:\n"
        "  --dev=DEVICE     指定串口终端设备名称, 譬如--dev=/dev/ttymxc2\n"
        "  --type=TYPE      指定操作类型, 读串口还是写串口, 譬如--type=read(read表示读、write表示写、其它值无效)\n"
        "\n可选选项:\n"
        "  --brate=SPEED    指定串口波特率, 譬如--brate=115200\n"
        "  --dbit=SIZE      指定串口数据位个数, 譬如--dbit=8(可取值为: 5/6/7/8)\n"
        "  --parity=PARITY  指定串口奇偶校验方式, 譬如--parity=N(N表示无校验、O表示奇校验、E表示偶校验)\n"
        "  --sbit=SIZE      指定串口停止位个数, 譬如--sbit=1(可取值为: 1/2)\n"
        "  --help           查看本程序使用帮助信息\n\n", app);
}

/**
 ** 信号处理函数,当串口有数据可读时,会跳转到该函数执行
 **/
static void io_handler(int sig, siginfo_t *info, void *context)
{
    unsigned char buf[10] = {0};
    int ret;
    int n;

    if(SIGRTMIN != sig)
        return;

    /* 判断串口是否有数据可读 */
    if (POLL_IN == info->si_code) {
        ret = read(fd, buf, 8);     //一次最多读8个字节数据
        printf("[ ");
        for (n = 0; n < ret; n++)
            printf("0x%hhx ", buf[n]);
        printf("]\n");
    }
}

/**
 ** 异步I/O初始化函数
 **/
static void async_io_init(void)
{
    struct sigaction sigatn;
    int flag;

    /* 使能异步I/O */
    flag = fcntl(fd, F_GETFL);  //使能串口的异步I/O功能
    flag |= O_ASYNC;
    fcntl(fd, F_SETFL, flag);

    /* 设置异步I/O的所有者 */
    fcntl(fd, F_SETOWN, getpid());

    /* 指定实时信号SIGRTMIN作为异步I/O通知信号 */
    fcntl(fd, F_SETSIG, SIGRTMIN);

    /* 为实时信号SIGRTMIN注册信号处理函数 */
    sigatn.sa_sigaction = io_handler;   //当串口有数据可读时,会跳转到io_handler函数
    sigatn.sa_flags = SA_SIGINFO;
    sigemptyset(&sigatn.sa_mask);
    sigaction(SIGRTMIN, &sigatn, NULL);
}

int main(int argc, char *argv[])
{
    uart_cfg_t cfg = {0};
    char *device = NULL;
    int rw_flag = -1;
    unsigned char w_buf[10] = {0x11, 0x22, 0x33, 0x44,
                0x55, 0x66, 0x77, 0x88};    //通过串口发送出去的数据
    int n;

    /* 解析出参数 */
    for (n = 1; n < argc; n++) {

        if (!strncmp("--dev=", argv[n], 6))
            device = &argv[n][6];
        else if (!strncmp("--brate=", argv[n], 8))
            cfg.baudrate = atoi(&argv[n][8]);
        else if (!strncmp("--dbit=", argv[n], 7))
            cfg.dbit = atoi(&argv[n][7]);
        else if (!strncmp("--parity=", argv[n], 9))
            cfg.parity = argv[n][9];
        else if (!strncmp("--sbit=", argv[n], 7))
            cfg.sbit = atoi(&argv[n][7]);
        else if (!strncmp("--type=", argv[n], 7)) {
            if (!strcmp("read", &argv[n][7]))
                rw_flag = 0;        //读
            else if (!strcmp("write", &argv[n][7]))
                rw_flag = 1;        //写
        }
        else if (!strcmp("--help", argv[n])) {
            show_help(argv[0]); //打印帮助信息
            exit(EXIT_SUCCESS);
        }
    }

    if (NULL == device || -1 == rw_flag) {
        fprintf(stderr, "Error: the device and read|write type must be set!\n");
        show_help(argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 串口初始化 */
    if (uart_init(device))
        exit(EXIT_FAILURE);

    /* 串口配置 */
    if (uart_cfg(&cfg)) {
        tcsetattr(fd, TCSANOW, &old_cfg);   //恢复到之前的配置
        close(fd);
        exit(EXIT_FAILURE);
    }

    /* 读|写串口 */
    switch (rw_flag) {
    case 0:  //读串口数据
        async_io_init();	//我们使用异步I/O方式读取串口的数据,调用该函数去初始化串口的异步I/O
        for ( ; ; )
            sleep(1);   	//进入休眠、等待有数据可读,有数据可读之后就会跳转到io_handler()函数
        break;
    case 1:   //向串口写入数据
        for ( ; ; ) {   		//循环向串口写入数据
            write(fd, w_buf, 8); 	//一次向串口写入8个字节
            sleep(1);       		//间隔1秒钟
        }
        break;
    }

    /* 退出 */
    tcsetattr(fd, TCSANOW, &old_cfg);   //恢复到之前的配置
    close(fd);
    exit(EXIT_SUCCESS);
}
相关推荐
韩楚风1 小时前
【linux 多进程并发】linux进程状态与生命周期各阶段转换,进程状态查看分析,助力高性能优化
linux·服务器·性能优化·架构·gnu
陈苏同学1 小时前
4. 将pycharm本地项目同步到(Linux)服务器上——深度学习·科研实践·从0到1
linux·服务器·ide·人工智能·python·深度学习·pycharm
Ambition_LAO1 小时前
解决:进入 WSL(Windows Subsystem for Linux)以及将 PyCharm 2024 连接到 WSL
linux·pycharm
Pythonliu71 小时前
茴香豆 + Qwen-7B-Chat-Int8
linux·运维·服务器
你疯了抱抱我2 小时前
【RockyLinux 9.4】安装 NVIDIA 驱动,改变分辨率,避坑版本。(CentOS 系列也能用)
linux·运维·centos
追风赶月、2 小时前
【Linux】进程地址空间(初步了解)
linux
栎栎学编程2 小时前
Linux中环境变量
linux
挥剑决浮云 -2 小时前
Linux 之 安装软件、GCC编译器、Linux 操作系统基础
linux·服务器·c语言·c++·经验分享·笔记
小O_好好学3 小时前
CentOS 7文件系统
linux·运维·centos
x晕x4 小时前
Linux dlsym符号查找疑惑分析
linux·运维·服务器