Linux应用层例程4 串口应用编程

2023-05-16

        本小节我们来学习 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 个本地终端。如下所示:
/dev/pts/X X 是一个数字编号,譬如 0 1 2 3 等)设备节点:这类设备节点是伪终端对应的设备节点,也就是说,伪终端对应的设备节点都在/dev/pts 目录下、以数字编号命令。譬如我们通过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 连接的。 

串口应用编程(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 <termios.h>
#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 可取值如下:
⚫ 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 :所有已接收但未读取的输入都将在配置生效之前被丢弃。
调用 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

 

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

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

 

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux应用层例程4 串口应用编程 的相关文章

随机推荐

  • 集美大学 - 2840 - 实验8 - 编程题

    实验8 1 9 指针 输出学生成绩 本题要求编写程序 xff0c 根据输入学生的成绩 xff0c 统计并输出学生的平均成绩 最高成绩和最低成绩 建议使用动态内存分配来实现 输入格式 xff1a 输入第一行首先给出一个正整数N xff0c 表
  • vscode中文乱码问题及几种常见的解决方案

    问题及原因 问题原因 xff1a 代码文件的字符编码格式为UTF 8 xff0c 但是terminal的字符编码格式为GBK 解决思路 xff1a 统一代码文件和terminal的字符编码格式 解决办法 说明 xff1a 以下的解决方案是针
  • 集美大学 - 2840 - 实验11-2 - 函数题

    实验11 2 1 链表 建立学生信息链表 本题要求实现一个将输入的学生成绩组织成单向链表的简单函数 函数接口定义 xff1a span class token keyword void span span class token funct
  • python正则表达式

    python正则表达式 match函数 re match尝试从字符串的起始位置匹配一个模式 xff0c 如果不是起始位置匹配成功的话 xff0c match 就返回none 函数语法 xff1a re span class token pu
  • 轻松解决VS配置OpenCV环境及导出OpenCV的VS项目模板

    一 OpenCV配置 1 下载OpenCV 点击进入下载OpenCV的官网界面 这里以Windows为例 xff0c 其他同理 xff08 可直接下载最新 xff09 2 提取OpenCV 在这里浅说一句 xff0c 为了方便环境配置文件管
  • 最大子段和问题

    以下给出具体代码 xff1a span class token macro property span class token directive hash span span class token directive keyword i
  • 如何简单又好看地美化你的Ubuntu界面

    起因 最近使用Ubuntu界面实属是审美疲劳了 xff0c 使用老版本的一大问题就是 界面太难看了 秉持新手学习最好是用老一点的稳定版本的观念 Ubuntu旧版本使用或使用过的人非常非常多 xff0c 学习的过程中你一旦出现什么问题互联网上
  • 【实战】物联网安防监控项目【2】———boa服务器的移植

    一 boa服务器的移植 1 源码下载 1 1 boa简介 xff1a 其可执行代码只有大约60KB左右 xff0c Boa是一个单任务的HTTP服务器 xff0c Boa只能依次完成用户的请求 xff0c 而不会fork出新的进程来处理并发
  • 【实战】物联网安防监控项目【4】———从网页上控制A9的LED灯

    前言 学习了一个新知识 xff0c 当然要记录一下啦 这两天学习了boa服务器 cgic标准库和html标签语言 xff0c 又双叕解锁一个嵌入式的新玩法 cgic库是沟通C语言和html网页编程语言的一座桥梁 xff0c 通过在linux
  • 【实战】物联网安防监控项目【5】———把模拟数据传输到web网页、web显示mjpeg-streamer视频图像

    1 模拟数据传输到web 为了把硬件传感器上的数据上传到web网页 xff0c 我们需要在跑linux服务器的开发板上写一个应用程序 xff0c 并创建出几个线程来收集传感器检测到的数据 xff0c 通过进程 线程间通信 boa与cgic库
  • HttpGet Digest授权认证

    工具类 xff1a compile com burgstaller okhttp digest 1 13 import android span class hljs preprocessor content span span class
  • ubuntu下git push失败error: 无法推送一些引用到 ‘xxx ‘解决方法

    如果你在Ubuntu下使用git push上传你的代码到gitee xff0c 突然出现一行报错 xff1a To git 64 gitee com imysy twenty two thread pool test git rejecte
  • 【Linux驱动开发】并发控制机制:原子操作、自旋锁、信号量、互斥锁详解

    并发控制机制 首先我们来了解一下 操作系统的并发性 这个概念 xff1a 操作系统的并发性 concurrence xff1a 指的是两个或者两个以上事件在同一时间间隔内发生 xff0c 即这个设备一会执行这个事件一会执行那个事件 xff0
  • STM32F051K8U6按键中断实例

    引言 最近要开始做毕设了 xff0c 准备用STM32做一个平衡小车 xff0c 好久没做过STM32的裸机项目了 xff0c 做几个项目练练手 xff0c 复习一下 本例程使用STM32CubeMX配套hal库来实现按键中断和串口中断 芯
  • STM32库函数笔记分享

    之前刚开始自学的部分STM32笔记放出 xff0c 希望对新入门STM32和想要复习库函数的小伙伴们起到帮助 建立工程 1 寄存器操作方式 需要不断地查手册来了解每一位是干什么用的 优点 xff1a 代码简介 xff1b 缺点 xff1a
  • rc.exe not found.(完美解决,亲测有效)

    完美解决rc exe not found 报错出错原因解决方法完美解决 报错 这两天安装了vs2015和IVF2016 xff0c 安装完之后在运行程序的时候一直会出现rc exe not found xff0c 重新生成解决方案后还是一样
  • 单片机与上位机通过串口通信--笔记

    定义 先说什么是串口 xff1f xff08 1 xff09 他是一种通信接口 xff0c 单片机 IO 口上的复用功能 xff0c 上位机 xff08 电脑 xff09 和下位机 xff08 开发板 xff09 之间的数据传输 xff08
  • Qt 的Cmake方式如何创建资源文件

    传统的qmake创建的工程有pro qrc xff0c 但是如果使用cmake方式创建的工程就没有这两个东西 xff0c 我们公司就是在linux下使用cmake创建的Qt工程 xff0c 没有pro也看不到qrc xff0c 想在ui界面
  • 理解ROS Topic 通信频率背后的机制

    Topic是ROS的三种通信方式中最为基本 也是常用的一种 本文对于ROS的Topic通信背后的数据吞吐机制做一个较为详细 深入的介绍 Publisher ROS中发布一个topic的函数是这样的 ros span class token
  • Linux应用层例程4 串口应用编程

    本小节我们来学习 Linux 下串口应用编程 xff0c 串口 xff08 UART xff09 是一种非常常见的外设 xff0c 串口在嵌入式开发领域当中一般作为一种调试手段 xff0c 通过串口输出调试打印信息 xff0c 或者通过串口