本小节我们来学习 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 个本地终端。如下所示:




Linux 串口 SecureCRT Xmodem 使用_服务器


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


Linux 串口 SecureCRT Xmodem 使用_linux_02

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

Linux 串口 SecureCRT Xmodem 使用_服务器_03


        这里为什么是 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 串口 SecureCRT Xmodem 使用_运维_04


串口应用编程(API是C库函数,针对所有终端设备)


        现在我们已经知道了串口在 Linux 系统中是一种终端设备,并且在我们的开发板上,其设备节点为 /dev/ttymxc0( UART1 )和 /dev/ttymxc2 ( UART3 )。


        Linux 为上层用户做了一层封装,将这些 ioctl() 操作封装成了一套标准的 API 我们就直接使用这一套标准 API 编写自己的串口应用程序即可!


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


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



struct termios 结构体 (输入设备是struct input_event 结构体)

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

示例代码 26.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 启动输入软件流控

以上所列举出的这些宏,我们可以通过 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 使能硬件流控


四、本地模式: 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。


六、总结说明


struct termios 结构体中 c_iflag 成员(输入模式)、c_oflag 成员(输出模式)、 c_cflag 成员(控制模式)以及 c_lflag 成员(本地控制)这四个参数


对于这些变量尽量不要直接对其初始化,而要将其通过“按位与”、“按位或”等操作添加标志或清除


某个标志。


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


终端的三种工作模式 (通过 本地模式: c_lflag 设置)


规范模式 非规范模式 原始模式


通过在 struct termios 结构体的 c_lflag 成员中设置 ICANNON 标志来定义终端是以规范模式(设置 ICANNON 标志)还是以非规范模式(清除 ICANNON 标志)工作,默认情况为规范模式。


规范模式


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


非规范模式下


        在非规范模式下,所有的输入是即时有效的,不需要用户另外输入行结束符,而且不可进行行编辑。在非规范模式下,对参数 MIN(c_cc[VMIN]) 和 TIME(c_cc[VTIME]) 的设置决定 read() 函数的调用方式。


原始模式( Raw mode


        按照严格意义来讲,原始模式是一种特殊的非规范模式。在原始模式下,所有的输入数据以字节为单位被处理。在这个模式下,终端是不可回显的,并且禁用终端输入和输出字符的所有特殊处理。在我们的应用程序中,可以通过调用 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;


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


操作一 打开串口设备


使用 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()调用成功后,会将终端当前的配置参数保存到 termios_p 指针所指的对象中。


        

        tcgetattr()函数可以获取到串口终端当前的配置参数, tcgetattr 函数原型如下所示(可通过命令 "man 3 tcgetattr"查询):


#include <unistd.h>

int tcgetattr(int fd, struct termios *termios_p);


需要包含 termios.h 头文件和 unistd.h 头文件。

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


调用 tcgetattr 函数之前,我们需要定义一个 struct termios 结构体变量,将该变量的指针作为 tcgetattr() 函数的第二个参数传入

函数调用成功返回 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> 头文件中申明,使用方式如下:这几个函数在成功时返回 0 ,失败时返回 -1 。


cfsetspeed(&new_cfg, B115200);


4) 设置数据位大小


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



new_cfg.c_cflag &= ~CSIZE;

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



5) 设置奇偶校验位


        通过 26.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 可取值如下:


⚫ 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 :所有已接收但未读取的输入都将在配置生效之前被丢弃。



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


操作六 读写数据:read()、write()


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



串口应用编程实战


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


#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]。


在开发板上进行测试   


        ALPHA I.MX6U 开发板上一共预留出了两个串口,一个 USB 串口(对应 I.MX6U 的 UART1 )、一个 RS232/RS485 串口(对应 I.MX6U 的 UART3 )


Linux 串口 SecureCRT Xmodem 使用_运维_05


Linux 串口 SecureCRT Xmodem 使用_linux_06

 

点击发送按钮向开发板 RS232 串口发送 8 个字节数据 [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88] , 此时我们的应用程序便会读取到串口的数据,这些数据就是 PC 机串口调试助手发送过来的数据


Linux 串口 SecureCRT Xmodem 使用_串口_07

执行测试程序后,测试程序会每隔 1 秒中将 8 个字节数据[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88] 写入到 RS232 串口,此时 PC 端串口调试助手便会接收到这些数据,如下所示:

Linux 串口 SecureCRT Xmodem 使用_运维_08