飞道的博客

Linux串口应用编程

709人阅读  评论(0)

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

串口应用编程介绍

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

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

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

终端Terminal

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

终端的分类

  • 本地终端:PC 机连接了显示器、键盘以及鼠标等设备,这样的一个显示器/键盘组合就是一个本地终端;同样对于开发板来说一个LCD 显示器、键盘和鼠标等构成本地终端。
  • 用串口连接的远程终端:开发板通过串口线连接到一个带有显示器和键盘的PC 机,在PC 机通过运行一个终端模拟程序,譬如SecureCRT 等来获取并显示开发板通过串口发出的数据、同样还可以通过这些终端模拟程序将用户数据通过串口发送给开发板Linux 系统。
  • 基于网络的远程终端:譬如我们可以通过ssh、Telnet 这些协议登录到一个远程主机。

前面两个属于物理终端,远程终端又叫做伪终端。

终端对应的设备节点

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

⚫ /dev/ttyX(X 是一个数字编号,譬如0、1、2、3 等)设备节点:在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 个串口外设,出厂系统只注册了2 个串口外设,分别是UART1 和UART3,所以对应这个数字就是0 和2。

mxc 这个名字不是一定的,命名与驱动有关系(与硬件平台有关),但是名字前缀都是以“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

输入模式控制输入数据(终端驱动程序从串口或键盘接收到的字符数据)在被传递给应用程序之前的处理方式。所有的标志都被定义为宏,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(波特率、数据位、校验位、停止位)

控制模式控制终端设备的硬件特性,譬如对于串口来说,该字段比较重要,可设置串口波特率、数据位、校验位、停止位等硬件特性。可用于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_ispeed 成员变量和c_ospeed 成员变量来指定串口的波特率;
在Linux 系统下,则是使用CBAUD 位掩码所选择的几个bit 位来指定串口波特率。
事实上,termios API 中提供了cfgetispeed()和cfsetispeed()函数分别用于获取和设置串口的波特率。

本地模式: 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 ter;

ter.c_iflag = IGNBRK | BRKINT | PARMRK;

而是要像下面这样:

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

并非所有标志对于实际的终端设备来说都是有效的,例如串口可以配置波特率、数据位、停止位等参数,但其它终端是不一定支持这些配置的,譬如本地终端键盘、显示器,只不过这些终端设备都使用了这一套API 来编程,

终端的三种工作模式(原始模式)

当ICANON 标志被设置时表示启用终端的规范模式,默认情况为规范模式。

规范模式下,所有的输入是基于行进行处理的。在用户输入一个行结束符(回车符、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 字符。

打开串口设备

接下来编写串口应用程序。第一步便是打开串口设备,使用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>中声明的函数来处理目前串口缓冲中的数据,函数原型:

#include <termios.h>
#include <unistd.h>

//这三个函数,调用成功时返回0;失败将返回-1、并且会设置errno 以指示错误类型。
int tcdrain(int fd);
int tcflush(int fd, int queue_selector);
int tcflow(int fd, int action);

1、调用tcdrain()函数后会使得应用程序阻塞,直到串口输出缓冲区中的数据全部发送完毕为止!

2、调用tcflow()函数会暂停串口上的数据传输或接收工作,具体情况取决于参数action取值如下:

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

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

⚫ TCIFLUSH:对接收到而未被读取的数据进行清空处理;
⚫ TCOFLUSH:对尚未传输成功的输出数据进行清空处理;
⚫ TCIOFLUSH:包括前两种功能,即对尚未处理的输入/输出数据进行清空处理。

通常我们会选择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;
    }

    /* 设置奇偶校验 */
    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]。

编译示例代码:

在开发板上进行测试

将上小节编译得到的可执行文件拷贝到开发板Linux 系统/home/root 目录下,如下所示:

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


注意,板子上的485 和232 接口是共用了I.MX6U 的UART3,这两个接口无法同时使用,可通过配置底板上的JP1 端子来使能RS232 或RS485 接口,使用跳线帽将每一列上面的两个针脚连接起来,此时RS232接口被使能、而RS485 接口不能使用;如果使用跳线帽将下面两个针脚连接起来,如图26.3.2 中所示,则此时RS485 接口被使能、RS232 接口不能使用。

本次测试笔者使用RS232 串口,注意不能使用USB 串口进行测试,它是系统的控制台终端。由于Mini开发板只有一个USB 串口,没有RS232 或RS485 接口,所以不太好测试,当然并不是说没有办法进行测试;虽然Mini 板上没有232 或485 接口,但是串口用到的I/O 都已经通过扩展口引出了,你使用一个USB转TTL 模块也是可以测试的。

将板上的RS232 接口通过<USB 转RS232>串口线连接到PC 机。

接下来进行测试,首先执行如下命令查看测试程序的帮助信息:

./testApp --help


可选选项表示是可选的,如果没有指定则会使用默认值!
先进行读测试:

./testApp --dev=/dev/ttymxc2 --type=read


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

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

打开XCOM 之后,对其进行配置、并打开串口,如下所示:

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

测试完读串口后,我们再来测试向串口写数据,按Ctrl+C 结束测试程序,再次执行测试程序,本次测试写串口,如下所示:

./testApp --dev=/dev/ttymxc2 --type=write


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


转载:https://blog.csdn.net/zhuguanlin121/article/details/123291365
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场