本小节我们来学习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个本地终端。如下所示:
图 27.1.1 本地终端设备节点
- /dev/pts/X(X是一个数字编号,譬如0、1、2、3等)设备节点:这类设备节点是伪终端对应的设备节点,也就是说,伪终端对应的设备节点都在/dev/pts目录下、以数字编号命令。譬如我们通过ssh或Telnet这些远程登录协议登录到开发板主机,那么开发板Linux系统会在/dev/pts目录下生成一个设备节点,这个设备节点便对应伪终端,如下所示:
图 27.1.2 伪终端设备节点
- 串口终端设备节点/dev/ttymxcX:对于ALPHA/Mini I.MX6U开发板来说,有两个串口,也就是有两个串口终端,对应两个设备节点,如下所示:
图 27.1.3 串口终端对应的设备节点
这里为什么是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命令来查看计算机系统当前连接了哪些终端(一个终端就表示有一个用户使用该计算机),如下所示:
图 27.1.4 查看系统连接了哪些终端
可以看到,开发板系统当前有两个终端连接到它,一个就是我们的串口终端,也就是开发板的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结构体定义如下:
示例代码 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
输入模式控制输入数据(终端驱动程序从串口或键盘接收到的字符数据)在被传递给应用程序之前的处理方式。通过设置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 |
启动输入软件流控 |
表 27.1.1 用于c_iflag成员的标志
以上所列举出的这些宏,我们可以通过man手册查询到它们的详细描述信息,执行命令" man 3 termios ",如下图所示:
图 27.1.5 通过man手册查询
二、输出模式:c_oflag
输出模式控制输出字符的处理方式,即由应用程序发送出去的字符数据在传递到串口或屏幕之前是如何处理的。可用于c_oflag成员的宏如下所示:
OPOST |
启用输出处理功能,如果不设置该标志则其他标志都被忽略 |
OLCUC |
将输出字符中的大写字符转换成小写字符 |
ONLCR |
将输出中的换行符(NL '\n')转换成回车符(CR '\r') |
OCRNL |
将输出中的回车符(CR '\r')转换成换行符(NL '\n') |
ONOCR |
在第0列不输出回车符(CR) |
ONLRET |
不输出回车符 |
OFILL |
发送填充字符以提供延时 |
OFDEL |
如果设置该标志,则表示填充字符为DEL字符,否则为NULL字符 |
表 27.1.2 用于c_oflag成员的标志
三、控制模式: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 |
使能硬件流控 |
表 27.1.3 用于c_cflag成员的标志
在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 |
启用输入处理功能 |
表 27.1.4 用于c_lflag成员的标志
五、特殊控制字符: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是一个宏,前面已经给大家介绍了,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)设置奇偶校验位
通过27.1.3小节的内容可知,串口的奇偶校验位配置一共涉及到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);
前面已经完成了对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()函数即可!
串口应用编程实战通过上一节的介绍,详细大家已经知道了如何对串口进行应用编程,其实总的来说还是非常简单地,本小节我们进行编程实战,在串口终端的原始模式下,使用串口进行数据传输,包括通过串口发送数据、以及读取串口接收到的数据,并将其打印出来。
示例代码笔者已经写好了,如下所示:
本例程源码对应的路径为:开发板光盘->11、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: //默认配置为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);
}
代码稍稍有点长,不过与串口相关的代码并不是很多,代码中所涉及到的所有内容在前面的章节中都给大家详细介绍过。
首先来看下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]。
好了,这个示例代码就给大家介绍完了,非常简单,没什么难点,相信各位聪明的读者绝对能看懂!如果看不懂?那可能就……呵呵了!
接下来我们要编译示例代码:
图 27.2.1 编译示例代码
在开发板上进行测试将上小节编译得到的可执行文件拷贝到开发板Linux系统/home/root目录下,如下所示:
图 27.3.1 将串口测试程序拷贝到开发板
ALPHA I.MX6U开发板上一共预留出了两个串口,一个USB串口(对应I.MX6U的UART1)、一个RS232/RS485串口(对应I.MX6U的UART3),如图 27.3.2和图 27.3.3所示。
图 27.3.2 RS232/RS485串口
图 27.3.3 USB串口
注意,板子上的485和232接口是共用了I.MX6U的UART3,这两个接口无法同时使用,可通过配置底板上的JP1端子来使能RS232或RS485接口,使用跳线帽将每一列上面的两个针脚连接起来,此时RS232接口被使能、而RS485接口不能使用;如果使用跳线帽将下面两个针脚连接起来,如图 27.3.2中所示,则此时RS485接口被使能、RS232接口不能使用。
本次测试笔者使用RS232串口,注意不能使用USB串口进行测试,它是系统的控制台终端。由于Mini开发板只有一个USB串口,没有RS232或RS485接口,所以不太好测试,当然并不是说没有办法进行测试;虽然Mini板上没有232或485接口,但是串口用到的I/O都已经通过扩展口引出了,你使用一个USB转TTL模块也是可以测试的。
将板上的RS232接口通过<USB转RS232>串口线连接到PC机。
接下来进行测试,首先执行如下命令查看测试程序的帮助信息:
./testApp --help
图 27.3.4 查看测试程序的帮助信息
可选选项表示是可选的,如果没有指定则会使用默认值!
先进行读测试:
./testApp --dev=/dev/ttymxc2 --type=read
图 27.3.5 读测试
执行测试程序时,笔者没有指定波特率、数据位个数、停止位个数以及奇偶校验等,程序将使用默认的配置,波特率115200、数据位个数为8、停止位个数为1、无校验!
程序执行之后,在Windows下打开串口调试助手上位机软件,譬如正点原子的XCOM串口调试助手:
图 27.3.6 串口调试助手
打开XCOM之后,对其进行配置、并打开串口,如下所示:
图 27.3.7 配置上位机并打开串口
点击发送按钮向开发板RS232串口发送8个字节数据[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88],此时我们的应用程序便会读取到串口的数据,这些数据就是PC机串口调试助手发送过来的数据,如下所示:
图 27.3.8 应用程序读取到串口的数据
测试完读串口后,我们再来测试向串口写数据,按Ctrl C结束测试程序,再次执行测试程序,本次测试写串口,如下所示:
./testApp --dev=/dev/ttymxc2 --type=write
图 27.3.9 测试写串口
执行测试程序后,测试程序会每隔1秒中将8个字节数据[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]写入到RS232串口,此时PC端串口调试助手便会接收到这些数据,如下所示:
图 27.3.10 串口调试助手接收到开发板RS232串口发送过来的数据
本章内容到此结束!
,