串口应用编程

Posted 行稳方能走远

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了串口应用编程相关的知识,希望对你有一定的参考价值。

目录

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

串口应用编程介绍

串口全称叫做串行接口,使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通信距离远,但是速度相对会低,串口是一种很常用的工业接口。

串口(UART)在嵌入式Linux 系统中常作为系统的标准输入、输出设备,系统运行过程产生的打印信息通过串口输出;同理,串口也作为系统的标准输入设备,用户通过串口与Linux 系统进行交互。

所以串口在Linux 系统就是一个终端,提到串口,就不得不引出“终端(Terminal)”这个概念了。

终端Terminal

终端就是处理主机输入、输出的一套设备,它用来显示主机运算的输出,并且接受主机要求的输入。典型的终端包括显示器键盘套件,打印机打字机套件等。只要能提供给计算机输入和输出功能,它就是终端,而与其所在的位置无关。

终端的分类

  • 本地终端:PC 机连接了显示器、键盘以及鼠标等设备,这样的一个显示器/键盘组合就是一个本地终端;同样对于开发板来说一个LCD 显示器、键盘和鼠标等构成本地终端。
  • 用串口连接的远程终端:对于嵌入式Linux 开发来说是最常见的终端。譬如我们的开发板通过串口线连接到一个带有显示器和键盘的PC 机,在PC 机通过运行一个终端模拟程序,譬如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 等)设备节点:这类设备节点是伪终端对应的设备节点。譬如我们通过ssh 或Telnet 这些远程登录协议登录到开发板主机,那么开发板Linux 系统会在/dev/pts 目录下生成一个设备节点,如下所示:


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

这里为什么是0 和2、而不是0 和1?我们知道,I.MX6U SoC 支持8 个串口外设,分别是UART1~UART8;出厂系统只注册了2 个串口外设,分别是UART1 和UART3,所以对应这个数字就是0 和2、而不是0 和1,这里了解一下就行!

还需要注意的是,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 即可。

这些API 其实是C 库函数,可以使用man 手册查看到它们的帮助信息;这里需要注意的是,这一套接口并不是针对串口开发的,而是针对所有的终端设备,串口是一种终端设备,计算机系统本地连接的鼠标、键盘也是终端设备,通过ssh 远程登录连接的伪终端也是终端设备。

要使用这个API,需要包含termios.h 头文件。

struct termios 结构体

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

struct termios 结构体定义如下:

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
…………
B12001200 波特率
B18001800 波特率
B24002400 波特率
B48004800 波特率
B96009600 波特率
B1920019200 波特率
B3840038400 波特率
B5760057600 波特率
B115200115200 波特率
B230400230400 波特率
B460800460800 波特率
B500000500000 波特率
B576000576000 波特率
B921600921600 波特率
B10000001000000 波特率
B11520001152000 波特率
B15000001500000 波特率
B20000002000000 波特率
B25000002500000 波特率
B30000003000000 波特率
…………
CSIZE数据位的位掩码
CS55 个数据位
CS66 个数据位
CS77 个数据位
CS88 个数据位
CSTOPB2 个停止位,如果不设置该标志则默认是一个停止位
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 成员的标志如下所示:

项目Value
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 成员(本地控制)这四个参数,这些参数能够分别控制、影响终端的行为特性。

这里有两个问题需要向大家说明,首先第一个是关于这些成员变量赋值的问题。

对于这些变量尽量不要直接对其初始化,而要将其通过“按位与”、“按位或”等操作添加标志或清除某个标志。譬如,通常不会这样对变量进行初始化:

struct termios ter;

ter.c_iflag = IGNBRK | BRKINT | PARMRK;

而是要像下面这样:

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 结构体进行了如下配置:

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()函数打开串口的设备节点文件,得到文件描述符:

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 3 tcgetattr"查询):

#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 以告知错误原因。

使用示例如下:

struct termios old_cfg;

if (0 > tcgetattr(fd, &old_cfg)) 
	/* 出错处理*/
	do_something();

对串口终端进行配置

假设我们需要采用原始模式进行串口数据通信。

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

struct termios new_cfg;

memset(&new_cfg, 0x0, sizeof(struct termios));

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

这个函数没有返回值。

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

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

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

cfsetispeed(&new_cfg, B115200);
cfsetospeed(&new_cfg, B115200);

B115200 是一个宏,表示波特率为115200。

cfsetispeed()函数设置数据输入波特率,而cfsetospeed()函数设置数据输出波特率。一般来说,用户需将终端的输入和输出波特率设置成一样的。

除了之外,我们还可以直接使用cfsetspeed()函数一次性设置输入和输出波特率,该函数也是在<termios.h>头文件中申明,使用方式如下:

cfsetspeed(&new_cfg, B115200);

这几个函数在成功时返回0,失败时返回-1。

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

new_cfg.c_cflag &= ~CSIZE;
new_cfg.c_cflag |= CS8; //设置为8 位数据位

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

//奇校验使能
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 标志即可。以下分别是停止位为一个和两个比特时的代码:

// 将停止位设置为一个比特
new_cfg.c_cflag &= ~CSTOPB;

// 将停止位设置为2 个比特
new_cfg.c_cflag |= CSTOPB;

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

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

new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0;

缓冲区的处理

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

#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()阻塞:

tcdrain(fd);

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

tcflush(fd, TCIOFLUSH);

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

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

#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()将配置参数写入设备,使其立即生效:

tcsetattr(fd, TCSANOW, &new_cfg);

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

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

串口应用编程实战

编程实战,在串口终端的原始模式下,使用串口进行数据传输,包括通过串口发送数据、以及读取串口接收到的数据,并将其打印出来。

本例程源码对应的路径为:开发板光盘->11、Linux C 应用编程例程源码->26_uart->uart_test.c。

/***************************************************************
 Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
 文件名 : uart_test.c
 作者 : 邓涛
 版本 : V1.0
 描述 : 串口在原始模式下进行数据传输--应用程序示例代码
 其他 : 无
 论坛 : www.openedv.com
 日志 : 初版 V1.0 2021/7/20 邓涛创建
 ***************************************************************/

#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;
    

    以上是关于串口应用编程的主要内容,如果未能解决你的问题,请参考以下文章

Linux串口应用编程详解(Serial)

Linux应用编程之串口操作20170901

linux应用:串口模块编程

linux应用:串口模块编程

原子串口使用0D0A结尾作为结束字符

每周3课:简单的串口交互应用板间的串口交互串口通信与ASCII码 | Mixly纯干货课程...