baudrate(baudrate)

串口应用程序编程在这一节,我们来学习Linux下的串口应用编程。串口(UART)是一种非常常见的外设。在嵌入式开发领域,串口一般被用作一种调试手段。它通过串口输

串口应用程序编程

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

本章将讨论以下主题。

串口应用编程介绍应用编程实战

串口应用程序编程简介

串口全称是串行接口,指的是数据一个接一个的顺序传输。通信线路很简单。使用两条线可以实现双向通信,一条线用于发送,另一条线用于接收。串口通信距离长,但是速度比较低。串口是一种非常常见的工业接口。

串口的基础知识,常用原理,常用数据格式等我就不介绍了。,以免过于啰嗦。在嵌入式Linux系统中,串口(UART)常被用作标准的输入输出设备,系统运行时产生的打印信息通过串口输出。同样,串口也作为系统的标准输入设备,用户可以通过它与Linux系统进行交互。

所以串口是Linux系统中的一个终端。说到串口,就不得不引入“终端”的概念。

终端终端

终端是一组处理主机输入和输出的设备。它用于显示主机操作的输出和接受主机所需的输入。典型的终端包括显示键盘套件、打印机打字机套件等。其实本质上只是一个字。接受输入,显示输出就够了。无论什么时代,终端始终扮演着人机交互的角色。所谓终端就是机器的边缘!

只要能给计算机提供输入输出功能,不管它在什么位置,都是终端。

终端分类

本地终端:例如对于我们的个人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个本地终端。如下所示:正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.1.1本地终端设备节点

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

图27.1.2伪终端设备的节点

串口终端设备节点/dev/ttymxcX:对于ALPHA/Mini I.MX6U开发板来说,有两个串口,也就是有两个串口终端,对应两个设备节点,如下所示:正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.1.3对应串行终端的设备节点

为什么这里是0和2而不是0和1?我们知道,I.MX6U SoC支持八种串行外设,分别是UART1 ~ UART8出厂时只注册了两个串行外设UART1和UART3,所以对应的数字是0和2,而不是0和1。就在这里找吧!

还需要注意的是,mxc这个名字并不确定。这个名字的命名和驱动有关(和硬件平台有关)。如果换一个硬件平台,其串口对应的设备节点不一定是mxcX比如ZYNQ平台,其系统中串口对应的设备节点是/dev/ttyPSX(X是数字),所以名称不统一,但名称前缀以“tty”开头,表示是终端。

在Linux系统下,我们可以使用who命令来检查当前有哪些终端连接到计算机系统(终端表示用户使用计算机),如下所示:

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.1.4检查哪些终端连接到系统

大家可以看到,开发板系统目前连接了两个终端,一个是我们的串口,是开发板的USB调试串口(对应/dev/tty mxc 0);另一种是伪终端,作者通过ssh连接。

串口应用程序编程

现在我们知道串口是Linux系统中的一个终端设备,它的设备节点是我们开发板上的/dev/ttymxc0(UART1)和/dev/ttymxc2(UART3)。

其实串口的应用编程也很简单。无非是通过ioctl()配置串口,调用read()读取串口的数据,调用write()将数据写入串口。是的,就是这么简单!但是我们不这么做,因为Linux给上层用户做了一层封装,把这些ioctl()操作封装成一套标准API,所以我们可以直接用这套标准API来编写自己的串口应用!

我称之为windows sockets termios API。这些API实际上是C库函数,你可以使用man手册查看它们的帮助信息。这里需要注意的是,这个windows sockets不是为串口开发的,而是为所有终端设备开发的。串口是一种终端设备,计算机系统本地连接的鼠标键盘也是终端设备,通过ssh远程登录连接的伪终端也是终端设备。

要使用termios API,我们需要在应用程序中包含termios.h头文件。

Struttermios结构

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

结构术语结构定义如下:

示例代码 27.1.1 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

输入模式控制输入数据(终端驱动程序从串口或键盘接收的字符数据)在传递给应用程序之前的处理方式。通过设置c_iflag成员的标志来控制structterminios结构中的c _ iflag成员。的所有标志都被定义为宏,c_oflag、c_cflag和c_lflag的成员也是这样配置的,c_iflag的成员除外。

c_iflag成员可用的宏如下:

IGNBRK

忽略输入终止条件。

布尔金特

检测到输入终止条件时,发送SIGINT信号。

伊宁帕

忽略帧错误和奇偶错误。

帕马克

标记奇偶校验错误。

in pack

对接收到的数据执行奇偶校验。

伊斯特里普

所有接收到的数据被分割成7位,即第8位被删除。

INLCR

将收到的NL(换行)转换为CR(回车)

IGNCR

忽略收到的CR(回车)

ICRNL

将收到的CR(回车)转换为NL(换行)

IUCLC

将接收到的大写字符映射到小写字符

IXON

开始输出软件流量控制。

伊克索夫

启动输入软件流量控制

表27 . 1 . 1 c _ iflag成员使用的标志

有了上面列出的这些宏,我们可以通过man手册找到它们的详细描述信息并执行命令& # 34;男子3 termios & # 34,如下图所示:

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.1.5通过手册查询

二。输出模式:c_oflag

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

低位进攻能力

启用输出处理功能。如果未设置此标志,其他标志将被忽略。

奥尔丘克

将输出字符中的大写字符转换为小写字符。

ONLCR

输出中的换行符(NL & # 39\ n & # 39)变成回车(CR & # 39\ r & # 39)

OCRNL

输出中的回车(CR & # 39\ r & # 39)分成换行符(NL & # 39\ n & # 39)

奥诺科尔

第0列不输出回车(CR)。

翁雷特

不输出回车。

奥菲尔

发送填充字符以提供延迟。

OFDEL

如果设置了该标志,则表示填充字符是DEL字符,否则为空字符。

表27 . 1 . 2 c _ of lag成员的标志

三。控制模式:c_cflag

该模式控制终端设备的硬件特性。例如,对于串行端口,该字段很重要。您可以设置串行端口的硬件特性,如波特率、数据位、校验位和停止位。控制模式通过设置struct termios结构中c_cflag成员的标志来配置。可用于c_cflag成员的符号如下:

CBAUD

波特率的位屏蔽

B0

波特率为0

……

……

B1200

200波特率

B1800

1800波特率

B2400

200波特率

B4800

400波特率

B9600

900波特率

B19200

9200波特率

B38400

3400波特率

B57600

5600波特率

B115200

15200波特率

B230400

20400波特率

B460800

60800波特率

500000英镑

500000波特率

B576000

56000波特率

B921600

91600波特率

B1000000

1000000波特率

B1152000

152000波特率

B1500000

100000波特率

B2000000

2000000波特率

B2500000

200000波特率

B3000000

3000000波特率

……

……

大小

数据位的位掩码

CS5

5个数据位

CS6

6个数据位

CS7

7个数据位

CS8

8个数据位

否要求设置两个停止位

2个停止位。如果未设置该标志,默认情况下它将是一个停止位。

使用接收器

接收使能

不进行奇偶性检测

启用奇偶校验。

帕洛德

用奇奇偶校验代替偶奇偶校验。

HUPCL

当调制解调器关闭时,将其挂断。

克洛卡尔

忽略调制解调器控制线

CRTSCTS

启用硬件流控制

表27 . 1 . 3 c _ cflag成员使用的符号

在structterminios结构中,有一个c_ispeed的成员变量和一个c_ospeed的成员变量。在其他系统中,这两个变量可以用来指定串口的波特率。在Linux系统下,CBAUD位掩码选择的几个位用来指定串口波特率。实际上,termios API中提供了函数cfgetispeed()和cfsitispeed()来分别获取和设置串口的波特率。

四。本地模式:c_lflag

本地模式用于控制终端的本地数据处理和工作模式。本地模式通过设置structterminios结构中c_lflag成员的标志来配置。c_lflag成员可用的标志如下:

电流信号

如果一个信号字符(INTR,退出等。)时,将产生相应的信号。

ICANON

启用规范模式

回声

启用输入字符的本地回显功能。当我们在终端中输入字符时,字符就会显示出来,这就是echo功能。

魔兽之

如果设置了ICANON,则允许退格操作。

埃科克

如果设置了ICANON,KILL字符将删除当前行。

ECHONL

如果设置了ICANON,则允许换行。

ECHOCTL

如果设置了ECHO,则控制字符(制表符、换行符等。)会显示为“X”,其中X的ASCII码等于对应控制字符的ASCII码加0x40。例如,退格字符(0x08)将显示为“h”(& # 39;H & # 39ASCII码是0x48)

回声报

如果设置了ICANON和IECHO,则删除字符(退格等。)和删除的字符。

埃科克

如果设置了ICANON,则允许回显ECHOE和ECHOPRT中设置的KILL字符。

NOFLSH

通常,当接收到INTR、退出和SUSP控制字符时,将清除空输入和输出队列。如果设置了此标志,将不会清除所有队列空

停止

如果一个后台进程试图写它的控制终端,系统发送SIGTTOU信号给后台进程的进程组。这个信号通常会终止进程的执行。

IEXTEN

启用输入处理功能

表27 . 1 . 4 c _ lflag成员的标志

动词 (verb的缩写)特殊控制字符:c_cc

特殊控制字符是一些字符组合,如Ctrl+C、Ctrl+Z等。当用户键入这样的组合键时,终端将进行特殊处理。structterminios结构中的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成员(局部控制)四个参数。这些参数可以分别控制和影响终端的行为特征。

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

尽可能不要直接初始化这些变量,而应该通过按位AND、按位or等操作进行标记或清除。例如,变量通常不是这样初始化的:

struct termios ter;ter.c_iflag = IGNBRK | BRKINT | PARMRK;

但是像这样:

ter.c_iflag |= (IGNBRK | BRKINT | PARMRK | ISTRIP);

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

我们引入了很多标志,但并不是所有的标志都对实际的终端设备有效。以串口终端为例,串口可以配置波特率、数据位、停止位等硬件参数。,但其他终端不一定支持这些配置,比如本地终端键盘和显示器,就没有这些硬件概念。

因为这些终端设备都是用这套API来编程的,但是,不同的终端设备在各自的硬件上有很大的差异,所以这些配置参数并不是对所有的终端设备都有效。你不需要了解所有标志在使用过程中的作用。其实快速掌握一项技术的核心点是一种学习能力!

终端的三种工作模式

当ICANON标志被置位时,意味着终端的标准模式被启用。什么标准模式?下面给大家简单解释一下。

终端有三种工作模式,即规范模式、非规范模式和raw模式。ICANNON标志在struct termios结构的c_lflag成员中设置,用于定义终端是工作在规范模式(设置ICANNON标志)还是非标准模式(清除ICANNON标志)。默认为规范模式。

在规范模式下,所有输入都基于行进行处理。在用户输入行结束符(回车、EOF等)之前。),系统调用read()函数,无法读取用户输入的任何字符。行终止符(回车等。)之外的EOF都会像普通字符一样被read()函数读入缓冲区。在规范模式下,行编辑是可行的,一个read()调用最多只能读取一行数据。如果read()函数中请求读取的数据字节数小于当前行可以读取的字节数,read()函数将只读取请求的字节数,剩余的字节将在下次读取。

在非标准模式下,所有输入立即有效,因此用户不需要输入行终止符,并且不允许行编辑。在非标准模式下,参数MIN(c_cc[VMIN])和TIME(c_cc[VTIME])的设置决定了如何调用read()函数。

如前所述,TIME和MIN的值只能在非标准模式下使用,两者的组合可以控制输入数据的读取模式。根据TIME和MIN的不同取值,会有以下四种不同的情况:

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()函数至少读取一个字节后才返回。

原始模式(原始模式)

严格来说,原图案是一种特殊的非标准图案。在原始模式下,所有输入数据都以字节为单位进行处理。在这种模式下,终端不能回显,终端输入输出字符的所有特殊处理都被禁用。在我们的应用程序中,我们可以通过调用cfmakeraw()函数将终端设置为原始模式。

cfmakeraw()函数实际上按如下方式配置structterminios结构:

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码值);串口是一种数据串行传输接口,通过它可以与其他设备或传感器进行数据传输和通信。例如,许多传感器使用串口与主机进行交互。那么在这种情况下,我们必须使用原始模式,这意味着通过串口传输的数据不应受到任何特殊处理,也不应被解析为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标志用于通知system /dev/ttymxc2它不会成为进程的控制终端。

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

通常在配置终端之前,我们会先获取终端当前的配置参数,并保存到一个struct termios结构对象中,以便后期可以方便地将终端恢复到原来的状态,这也是为了后续调试的安全和方便。

函数的作用是:获取串口终端的当前配置参数。tcgetattr函数的原型如下所示(您可以使用命令& # 34;man 3 tcgetattr & # 34查询):

#include <termios.h>#include <unistd.h>int tcgetattr(int fd, struct termios *termios_p);

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

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

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

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

使用示例如下:

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

配置串行终端

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

1)将串行终端配置为原始模式。

引起

struct termios new_cfg;memset(&new_cfg, 0x0, sizeof(struct termios));//配置为原始模式cfmakeraw(&new_cfg);

这个函数没有返回值。

2)接收使能

要启用接收函数,只需在structterminios结构的c_cflag成员中添加CREAD标志,如下所示:

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

3)设置串口的波特率。

有一个设置波特率的特殊功能,用户不能通过位掩码直接操作。设置波特率的主要函数是cfsetSpeed()和cfsetSpeed(),它们在

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

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

函数的作用是:设置数据输入波特率,函数的作用是:设置数据输出波特率。一般来说,用户需要将终端的输入和输出波特率设置为相同。

此外,我们还可以直接使用cfsetspeed()函数一次性设置输入和输出波特率,这在

cfsetspeed(&new_cfg, B115200);

这些函数成功时返回0,失败时返回-1。

4)设置数据位大小。

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

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

5)设置奇偶校验位

由27.1.3小节的内容可知,串口的校验位配置涉及structterminios结构中的两个成员变量: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标志来实现。如果停止位是1位,则CSTOPB标志被清除;如果有两个停止位,只需添加CSTOPB标志。以下是停止位分别为一位和两位时的代码:

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

7)设置分钟和时间的值

如前所述,MIN和TIME的值会影响非标准模式下read()调用的行为特征。原始模式是一种特殊的非标准模式,因此MIN和TIME在原始模式下也有效。

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

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

缓冲区的处理

在我们使用串口之前,我们需要对串口的缓冲区进行处理,因为在我们使用它之前,可能会有一些数据在缓冲区中等待处理或者当前正在进行数据发送和接收,所以我们需要在使用它之前对这种情况进行处理。这时,你可以打电话

#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,其值如下:

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

让我们来看看tcflush()函数。调用此函数将清除空输入/输出缓冲区中的数据。具体情况取决于参数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);

调用该函数会将参数terminios _ p指示的struct termios对象中的配置参数写入终端设备,使配置生效!

参数optional_actions可以指定更改何时生效,其值如下:

TCSANOW:配置立即生效。TCSADRAIN:配置在所有写入fd的输出都传输完毕之后生效。TCSAFLUSH:所有已接收但未读取的输入都将在配置生效之前被丢弃。

函数调用成功时返回0;失败将返回-1,并设置errno来指示错误类型。

例如,调用tcsetattr()将配置参数写入设备,并使其立即生效:

tcsetattr(fd, TCSANOW, &new_cfg);

读写数据:Read(),write()

所有准备工作完成后,就可以读写数据了。直接调用read()和write()函数就行了!

串行应用程序设计实践

通过上一节的介绍,我们已经知道了如何对串口进行详细的编程。其实总的来说还是很简单的。在本节中,我们进行实际的编程。在串口终端的原始模式中,我们使用串口进行数据传输,包括通过串口发送数据,读取串口接收到的数据并打印出来。

示例代码的作者已经编写了它,如下所示:

这个例程的源代码对应的路径是:开发板CD->: 1。Linux C应用程序编程例程源代码->:27 _ UART->;uart_test.c .

示例代码 27.2.1 串口数据传输示例代码/***************************************************************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: //默认配置为115200speed = 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: //默认数据位大小为8new_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/Ofor ( ; ; )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);}

代码有点长,但是和串口相关的代码不多。本规范涉及的所有内容在前面的章节中已经详细介绍过了。

首先,看main()函数。进入main()函数后,有一个for()循环,解析用户传递的参数。在设计该应用程序时,允许用户传入相应的参数,如串行终端的设备节点、串行波特率、数据位数、停止位数、奇偶校验等。具体用法,可以看看show_help()函数。

接下来,调用uart_init()函数,这是一个用于初始化串行端口的自定义函数。实际上它做了两件事:打开串口终端设备,获取串口终端的当前配置参数,保存到old_cfg变量中。

然后调用uart_cfg()函数,也是用户自定义函数。它用于配置串行端口,包括将串行端口配置为原始模式、启用串行端口接收、设置串行端口波特率、数据位数、停止位数、奇偶校验以及设置MIN和TIME值。最后清空空缓冲区,将配置参数写入串口设备使其生效。具体代码自己看。

最后,根据用户参数传输中-type选项指定的类型来读写串口。如果- type=read,这个测试是针对串口读的,如果- type=write,这个测试是针对串口写的。

为了读取串行数据,程序使用异步I/O来读取数据。首先,它调用async_io_init()函数来初始化异步I/O并注册信号处理函数。当检测到数据可读时,它将跳转到信号处理函数io_handler()执行。在此功能中,串行端口数据将被读取并打印出来。这里应该注意,这个例程一次最多可以读取8个字节的数据。如果可读数据大于8个字节,冗余数据将在下一个read()调用中被读出。

对于写操作,我们直接调用write()函数,每秒向串口写入8个字节的数据[0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88]。

好了,这个样本代码介绍给你。很简单,没有什么困难。相信各位聪明的读者一定能看懂!如果你不能理解?那可能是.....呵呵!

接下来,我们将编译示例代码:

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.2.1编译示例代码

在开发板上测试

将上一节编译的可执行文件复制到开发板的Linux system/home/根目录下,如下图所示:

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.1将串行端口测试程序复制到开发板

ALPHA I.MX6U的开发板上预留了两个串口,一个USB串口(对应I.MX6U的UART1)和一个RS232/RS485串口(对应I.MX6U的UART3),如图27.3.2和图27.3.3所示。

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.2 RS232/RS485串口

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.3 USB串行端口

注意,板上的485和232接口是UART3共用I.MX6U这两个接口不能同时使用。您可以在背板上配置JP1端子,以启用RS232或RS485接口,并使用跳线帽连接每列上的两个引脚。此时,RS232接口启用,但RS485接口不能使用。如果跳线帽用于连接以下两个引脚,如图27.3.2所示,则RS485接口启用,RS232接口不能使用。

在这个测试中,作者使用了RS232串口。注意,USB串口不能用于测试。它是系统的控制台终端。由于Mini开发板只有一个USB串口,没有RS232或RS485接口,不容易测试。当然不代表没有办法测试。虽然Mini板上没有232或485接口,但串口使用的I/O已经通过扩展口引出。您也可以使用USB转TTL模块来测试它。

通过以下方式连接板上的RS232接口:串口连接到PC。

接下来,测试。首先,执行以下命令来检查测试程序的帮助信息:

./testApp --help正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.4检查测试程序的帮助信息

可选就是可选的意思。如果未指定,将使用默认值!

首先阅读测试:

./testApp --dev=/dev/ttymxc2 --type=read正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.5阅读测试

在执行测试程序时,我没有指定波特率、数据位数、停止位数、奇偶校验等。程序将使用默认配置,波特率为115200,数据位数为8,停止位数为1,无校验!

程序执行后,在Windows下打开串口调试助手的上位机软件,如正点atom的XCOM串口调试助手:

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.6串行调试助手

打开XCOM后,配置它并打开串口,如下所示:

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.7配置上位机并打开串口

点击发送按钮,将8字节数据[0x11、0x22、0x33、0x44、0x55、0x66、0x77、0x88]发送到开发板的RS232串行端口。此时,我们的应用程序将读取串口数据,这是PC的串口调试助手发送的数据,如下所示:

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.8应用程序从串口读取数据

读完串口之后,让我们再次测试向串口写数据。按Ctrl+C结束测试程序,并再次执行测试程序。在这个测试中编写串口,如下所示:

./testApp --dev=/dev/ttymxc2 --type=write正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.9测试和写入串口

测试程序执行后,每1秒钟测试程序会向RS232串口写入8个字节的数据[0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88],然后PC机端的串口调试助手就会接收到这些数据,如下图所示:

正点原子I.MX6U嵌入式Linux C应用编程 第二十七章 串口应用编程

图27.3.10串口调试助手接收开发板RS232串口发送的数据。

本章到此结束!

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。

作者:美站资讯,如若转载,请注明出处:https://www.meizw.com/n/147926.html

发表回复

登录后才能评论