目录
一、uart
串口全称叫做串行接口,串行接口指的是数据一个一个的按顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通信距离远,但是速度相对会低,串口是一种很常用的工业接口。具体细节可以看裸机开发
二、终端 Terminal
终端就是处理主机输入、输出的一套设备,它用来显示主机运算的输出,并且接受主机要求的输入。典型的终端包括显示器键盘套件,打印机打字机套件等。其实本质上也就一句话,能接受输入、能显示输出,这就够了,不管到了什么时代,终端始终扮演着人机交互的角色,所谓 Terminal,即机器的边缘!只要能提供给计算机输入和输出功能,它就是终端,而与其所在的位置无关
1、终端的分类
PC 机连接了显示器、键盘以及鼠标等设备,这样的一个显示器/键盘组合就是一个本地终端;同样对于开发板来说也是如此,开发板也可以连接一个LCD 显示器、键盘和鼠标等,同样可以构成本地终端;
开发板通过串口线连接到一个带有显示器和键盘的 PC 机,在 PC 机通过运行一个终端模拟程序;
基于网络的远程终端: 譬如我们可以通过 ssh、 Telnet 这些协议登录到一个远程主机
前两类称之为物理终端;最后一个称之为伪终端。前两类都是在本地就直接关联了物理设备的,譬如显示器、鼠标键盘、串口等之类的,这种终端叫做物理终端,而第三类在本地则没有关联任何物理设备,注意,不要把物理网卡当成终端关联的物理设备,它们与终端并不直接相关,所以这类不直接关联物理设备的终端叫做伪终端
2、终端对应的设备节点
每一个终端在/dev 目录下都有一个对应的设备节点,/dev/ttyX(X 是一个数字编号,譬如 0、 1、 2、 3 等) 设备节点: ttyX(teletype 的简称),在 Linux 中, /dev/ttyX 代表的都是上述提到的本地终端,Linux 内核在初始化时所生成的 63 个本地终端。
/dev/pts/X(X 是一个数字编号,譬如 0、 1、 2、 3 等)设备节点:这类设备节点是伪终端对应的设备节点,也就是说,伪终端对应的设备节点都在/dev/pts 目录下、以数字编号命令,用ssh 或 Telnet 这些远程登录协议登录到开发板主机,那么开发板 Linux 系统会在/dev/pts 目录下生
成一个设备节点,这个设备节点便对应伪终端
需要注意的是,这个名字它不是统一的,但是名字前缀都是以“tty”开头,以表明它是一个终端
这里使用的开发板,有两个串口,也就是有两个串口终端,对应两个设备节点,如下
I.MX6U SoC 支持 8 个串口外设,分别是 UART1~UART8;出厂系统只注册了 2 个串口外设,分别是 UART1 和 UART3,所以对应这个数字就是 0 和 2
三、串口的应用编程
Linux 为上层用户做了一层封装,将这些 ioctl()操作封装成了一套标准的 API,我们就直接使用这一套标准 API 编写自己的串口应用程序即可!
一套接口称为 termios API, 这些 API 其实是 C 库函数, 可以使用 man 手册查看到它们的帮助信息;这里需要注意的是,这一套接口并不是针对串口开发的,而是针对所有的终端设备,串口是一种终端设备,计算机系统本地连接的鼠标、键盘也是终端设备,通过 ssh 远程登录连接的伪终端也是终端设备
1、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_ispeed 成员变量和 c_ospeed 成员变量,在其它一些系统中,可能会使用这两个变量来指定串口的波特率, termios API 中提供了 cfgetispeed()和 cfsetispeed()函数分别用于获取和设置串口的波特率,可以直接使用 cfsetspeed()函数一次性设置输入和输出波特率
①输入模式: c_iflag
输入模式控制输入数据(终端驱动程序从串口或键盘接收到的字符数据)在被传递给应用程序之前的处理方式。所有的标志都被定义为宏,除 c_iflag 成员外, c_oflag、 c_cflag 以及 c_lflag 成员也都采用这种方式进行配置。可以通过 man 手册查询到它们的详细描述信息,执行命令" man 3 termios“
②输出模式: c_oflag
输出模式控制输出字符的处理方式,即由应用程序发送出去的字符数据在传递到串口或屏幕之前是如何处理的
③控制模式: c_cflag
控制模式控制终端设备的硬件特性,譬如对于串口来说,该字段比较重要,可设置串口波特率、数据位、校验位、停止位等硬件特性。
④本地模式: c_lflag
本地模式用于控制终端的本地数据处理和工作模式。
⑤特殊控制字符: c_cc
特殊控制字符是一些字符组合,如 Ctrl+C、 Ctrl+Z 等, 当用户键入这样的组合键,终端会采取特殊处理方式。 struct termios 结构体中 c_cc 数组将各种特殊字符映射到对应的支持函数,每个字符位置(数组下标)对应的宏定义
注意 ,对于成员变量赋值,尽量不要直接对其初始化,而要将其通过“按位与”、“按位或” 等操作添加标志或清除某个标志
api
2、终端的三种工作模式
终端有三种工作模式,分别为规范模式(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 的取值不同,会有以下 4 种不同情况
③原始模式
按照严格意义来讲,原始模式是一种特殊的非规范模式。在原始模式下,所有的输入数据以字节为单位被处理。在这个模式下,终端是不可回显的, 并且禁用终端输入和输出字符的所有特殊处理。 在我们的应用程序中,可以通过调用 cfmakeraw()函数将终端设置为原始模式
什么时候会使用原始模式?串口数据以字符的形式传输、也就是传输的数据其实字符对应的 ASCII 编码值),通过串口可以与其他设备或传感器进行数据传输、通信,那么在这种情况下,我们就得使用原始模式,意味着通过串口传输的数据不应进行任何特殊处理、不应将其解析成 ASCII 字符。
代码编写
![](https://i-blog.csdnimg.cn/blog_migrate/5c6b7f9c988f3cffbbfa41bd0c8602a7.png)
第一行,这是一个预处理指令,它告诉编译器包含GNU特定的功能。在本例中,这个宏定义告诉编译器使用GNU特定的结构体termios2(定义于termios.h),该结构体扩展了termios结构体并增加了一些额外的属性
这是一个用于保存串口配置信息的结构体类型,它包括了波特率、数据位、奇偶校验和停止位等属性。
这是两个静态变量,用于保存当前的串口配置参数(old_cfg)和串口设备文件的文件描述符fd
这个函数用于初始化串口,它打开串口设备文件并获取当前的串口配置参数。首先,它使用open函数打开串口设备文件,O_NOCTTY 表示不把该设备作为进程的控制终端,如果失败则输出错误信息并返回-1。接着,它使用tcgetattr函数获取串口的当前配置参数,并将其保存在old_cfg变量中。如果获取失败,则输出错误信息并关闭串口设备文件,然后返回-1。最后,如果初始化成功,则返回0。 strerror(errno)用于打印错误原因类似perror
这段代码用于串口配置,uart_cfg 函数的作用是设置串口的参数。首先定义了一个结构体类型的变量 new_cfg,并将它清零。接着使用 cfmakeraw 函数将 new_cfg 配置为原始模式,并使能接收CERAD,然后根据传入的波特率参数用cfsetispeed()设置速率,根据数据位参数设置数据位CSIZE(数据位的位掩码)大小,根据奇偶校验参数设置奇偶校验类型,根据停止位参数设置停止位大小。最后将非规范模式下的 VTIME 和 VMIN 设置为 0,表示在非规范模式下立即返回。tcflush函数清空终端,TCIFLUSH: 对接收到而未被读取的数据进行清空处理,tcsetattr()函数
写入配置、使配置生效,TCSANOW:配置立即生效。如果函数执行过程中出现错误,就使用 fprintf 函数输出错误信息,并返回 -1,如果函数执行成功,就返回 0。
在输入传参错误时候打印这个信息
判断串口是否有数据可读。如果串口有数据可读,就使用read函数读取数据并打印输出,以16进制格式显示每个字节的值。判断是否接收到了实时信号SIGRTMIN,如果不是,则直接返回。如果收到了实时信号SIGRTMIN,则进入if语句块,判断串口是否有数据可读,si_code表示文件描述符 si_fd 发生了什么事件,读就绪态、写就绪态或者是异常事件等,POLL_IN表示可读取数据事件。如果串口有数据可读,则使用read函数从串口中读取数据,并将读取的数据存储到buf数组中,一次最多读取8个字节的数据,使用printf函数将读取的数据按16进制格式输出。
这段函数实现了串口异步 I/O 的初始化,包括了打开异步 I/O 标志、设置异步 I/O 的所有者、指定异步 I/O 通知信号以及注册异步 I/O 事件处理函数。fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,通过fcntl函数使能串口的异步I/O功能,首先通过F_GETFL获取文件描述符的状态标志,再通过按位或操作添加O_ASYNC标志,告诉操作系统打开的文件支持异步I/O操作。F_SETOWN
指定文件的拥有者(owner),文件拥有者是一个进程,当文件上发生某些事件时,内核会向该进程发送信号。在上下文中,F_SETOWN
指定了串口文件的所有者,也就是在串口上发生数据可读事件时会被通知的进程。getpid()
是一个系统调用,它会返回调用进程的进程 ID(PID),将串口文件的所有权设置为当前进程,以便当前进程能够接收到有关串口上数据可读事件的通知信号。通过F_SETSIG
设置SIGRTMIN
作为异步I/O通知信号,将信号处理函数设置为io_handler,指定使用sa_sigaction信号处理器标志,清空信号屏蔽字,防止在处理该信号时被其他信号中断,最后在注册SIGRTMIN信号处理函数。
代码如下
#define _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_gardware_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)
{
if ((fd = open(device, O_RDWR | O_NOCTTY)) < 0) /* 打开串口终端 */
{
fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
return -1;
}
if (tcgetattr(fd, &old_cfg) < 0) /* 获取串口当前的配置参数 */
{
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;
default:
speed = B115200;
printf("default baud rate: 115200\n");
break;
}
if (cfsetispeed(&new_cfg, speed) < 0)
{
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:
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 (tcflush(fd, TCIFLUSH) < 0)
{
fprintf(stderr, "tcflush error: %s\n", strerror(errno));
return -1;
}
/*写入配置、使配置生效*/
if (tcsetattr(fd, TCSANOW, &new_cfg) < 0)
{
fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));
return -1;
}
return 0;
}
/* 打印帮助信息*/
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 ,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,n;
unsigned char w_buf[10] = {0x11, 0x22, 0x33, 0x44,
0x55, 0x66, 0x77, 0x88}; //通过串口发送出去的数据
/* 解析出参数 */
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);
}
验证需要用另外的串口发送或接收数据