版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。
Copyright (C) 2021 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
2.9 Linux 下TTY 串口编程
2.9.1 TTY 简介
TTY 的由来,很久以前,计算机体积很大,所以用 teletype 这样一个工具用来远程连接到计算机,而 TTY 这个名字沿用至今,它作为一个子系统既支持串口,也支持键盘,显示器,还支持更复杂的功能
2.9.1.1 终端 Terminal 和控制台 Console
终端就是处理主机输入和输出的一套设备,用来显示主机的运算输出,以及接受主机要求的输入,只要能够提供给计算机输入和输出功能的就是终端,与所在的位置无关,可以是真实设备也可能是虚拟设备 终端的分类包括,本地终端,用串口连接的终端以及基于网络的远程终端 1)本地终端,对于个人 pc 机,连接了显示器,键盘,鼠标等设备就可以称作一个本地终端 2)用串口连接的终端,也就是将开发板连接到一个带显示器和键盘的 pc 机,然后 pc 机通过运行一个终端模拟程序,从而实现数据收发 3)基于网络的远程终端,则是利用 ssh 协议远程登录到一个主机 控制台,主要突出控制之意,也是一种 Terminal,但是它有更大的权限,可以查看内核打印的信息,因此我们可以从多个 Terminal 中指定一个作为 Console
2.9.1.2 TTY 设备节点
在根文件系统的 /dev 路径下可以看到很多和 tty 相关的设备节点,这些节点的总结如下 注:表格中的 X 代表数字编号
设备节点 |
含义 |
---|---|
/dev/ttyX |
/dev/tty0 代表前台程序的终端,/dev/tty 代表自己所使用的终端,剩余的从 /dev/tty1 开始的 /dev/ttyX 分别代表一个虚拟终端 |
/dev/pts/X |
这类设备节点是伪终端对应的设备节点,伪终端对应的设备节点都在 /dev/pts 目录下,以数字编号命名,通过 ssh 或者 telnet 这些远程登录协议登录到开发板,那么开发板就会在 /dev/pts 目录下生成一个设备节点 |
/dev/ttymxcX |
imx6ull 的串口终端,以此命名 |
/dev/console |
通过内核的配置可以指定 console 是哪一个 tty 设备 |
2.9.2 TTY 应用编程
串口的应用编程其实就是通过 ioctl 对串口进行配置,然后调用 read 读取串口的数据,再使用 write 向串口写入数据,但是 linux 为上层用户做了一层封装,也就是将 ictrl 操作封装成了一套标准 api,我们直接使用这一套标准 api 编写自己的串口应用程序
2.9.2.1 termios 结构体
应用编程内容包含两个方面,读写和配置,而对于配置而言最重要的结果体就是这个 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 输入模式控制输入数据在被传递给应用程序之前的处理方式
宏 |
含义 |
---|---|
IGNBRK |
忽略输入终止条件 |
BRKINT |
当检测到输入终止条件时发送 SIGINT 信号 |
IGNPAR |
忽略帧错误和奇偶校验错误 |
PARMRK |
对奇偶校验错误做出标记 |
INPCK |
对接收到的数据执行奇偶校验 |
ISTRIP |
将所有接收到的数据裁剪为 7 比特位,也就是去除第八位 |
c_oflag 输出模式控制字符的处理方式,也就是由应用程序发出去的字符在传递到串口之前是如何处理的
宏 |
含义 |
---|---|
OPOST |
启用输出处理功能,如果不设置该标志则其他标志都被忽略 |
OLCUC |
将输出字符中的大写字符转换成小写字符 |
ONOCR |
在第 0 列不输出回车符 |
ONLRET |
不输出回车符 |
OFILL |
发送填充字符以提供延时 |
c_cflag 控制模式控制终端设备的硬件特性,例如对于串口而言该字段可以设置串口波特率,数据位,校验位,停止位等硬件特性,在一些系统中也可以使用 c_ispeed 和 c_ospeed 这两个成员来指定串口的波特率
宏 |
含义 |
---|---|
B4800 |
4800 波特率 |
B9600 |
9600 波特率 |
B19200 |
19200 波特率 |
B38400 |
38400 波特率 |
B57600 |
57600 波特率 |
B115200 |
115200 波特率 |
CS5 |
5 个数据位 |
CS6 |
6 个数据位 |
CS7 |
7 个数据位 |
CS8 |
8 个数据位 |
CSTOPB |
2 个停止位,如果不设置该标志则默认是一个停止位 |
CREAD |
接收使能 |
PARENB |
使能奇偶校验 |
PARODD |
使用奇校验、而不是偶校验 |
c_lflag 本地模式用于控制终端的本地数据处理和工作模式
宏 |
含义 |
---|---|
ISIG |
若收到信号字符,则会产生相应的信号 |
ICANON |
启用规范模式 |
ECHO |
启用输入字符的本地回显功能,当我们在终端输入字符的时候,字符会显示出来,这就是回显功能 |
ECHOE |
若设置 ICANON,则允许退格操作 |
ECHOK |
若设置 ICANON,则 KILL 字符会删除当前行 |
ECHONL |
若设置 ICANON,则允许回显换行符 |
ECHOPRT |
若设置 ICANON 和 IECHO,则删除字符和被删除的字符都会被显示 |
ECHOKE |
若设置 ICANON,则允许回显在 ECHOE 和 ECHOPRT 中设定的 KILL字符 |
TOSTOP |
若一个后台进程试图向它的控制终端进行写操作,则系统向该后台进程的进程组发送 SIGTTOU 信号 |
IEXTEN |
启用输入处理功能 |
c_cc 特殊控制字符是一些字符组合,例如 ctrl + c 或者 ctrl + z,当用户键入这样的组合键终端采取特殊处理的方式
宏 |
含义 |
---|---|
VTIME |
非规范模式下, 指定读取的每个字符之间的超时时间(以分秒为单位) TIME |
VMIN |
在非规范模式下,指定最少读取的字符数 MIN |
2.9.2.2 终端的三种工作模式
终端的三种工作模式,分别是规范模式 canonical mode,非规范模式 non-canonical mode 和原始模式 raw mode,通过设置 c_lflag 设置 ICANNON 标志来定义终端是以规范模式还是非规范模式工作,默认为规范模式 规范模式 所有输入是基于行处理的,在用户输入一个行结束符之前,系统调用 read 函数是无法读到用户输入的任何字符的,除了 eof 之外的行结束符与普通字符一样会被 read 函数读取到缓冲区中 在规范模式下,行编辑是可行的,而且一次调用 read 最多只能读取一行数据 非规范模式 所有输入及时有效,不需要用户另外输入行结束符 在非规范模式下,对参数 MIN(c_cc[VMIN])和 TIME(c_cc[VTIME])的设置决定 read 函数的调用方式,MIN 和 TIME 的取值不同,会有以下四种不同的情况
MIN |
TIME |
说明 |
---|---|---|
= 0 |
= 0 |
read 调用总是会立即返回,若有可读数据,则读数据并返回被读取的字节数,否则读取不到数据返回 0 |
> 0 |
= 0 |
read 函数会被阻塞,直到有 MIN 个字符可以读取时,read 才返回,返回值为读取的字节数 |
= 0 |
> 0 |
只要有数据可读或者经过 TIME 个十分之一秒的时间,read立即返回,返回为读取的字节数 |
> 0 |
> 0 |
当有 MIN 个字节可读或者两个输入字符之间的时间间隔超过 TIME 个十分之一秒,read 才返回,因为在输入第一个字符后系统才会启动定时器,所以,read 至少读取一个字节后才返回 |
原始模式 |
是一种特殊的非规范模式,所有的输入数据以字节为单位被处理,即有一个字节输入时,触发输入有效,但是终端不可回显,并且禁用终端输入和输出字符的所有特殊处理
2.9.2.3 串口应用代码流程
定义串口参数结构体,一个用于保存原有终端数据的 termios 结构体以及 uart 设备的 fd
struct uart_parameter {
unsigned int baudrate; // 波特率
unsigned char dbit; // 数据位
char parity; // 奇偶校验
unsigned char sbit; // 停止位
};
static struct termios oldtio; // 用于保存终端的配置参数
static int fd_uart; // 串口终端对应的文件描述符
串口初始化,也就是 open 串口设备节点,然后获取当前的配置,获取当前配置主要是为了程序结束后能够恢复终端的原有配置,以防对终端之后的使用造成影响
static int uart_init(const char *device)
{
fd_uart = open(device, O_RDWR | O_NOCTTY);
if (0 > fd_uart)
{
printf("fail to open uart file\n");
return -1;
}
/* 获取串口当前的配置参数 */
if (0 > tcgetattr(fd_uart, &oldtio))
{
printf("fail to get old attribution of terminal\n");
close(fd_uart);
return -2;
}
return 0;
}
然后,配置串口属性,则通过程序使用时解析传入参数,从而配置串口
static int uart_configuration(const struct uart_parameter *para)
{
struct termios newtio;
speed_t speed;
/* 设置为原始模式
* 配置为原始模式相当于已经对 newtio 做了如下配置
* IGNBRK 忽略输入终止条件,BRKINT 检测到终止条件发送 SIGINT 信号,PARMRK 对奇偶校验做出标记
* ISTRIP 裁剪数据位为 7 bit,去掉第八位,INLCR 换行符转换为回车符,IGNCR 忽略回车符
* ICRNL 将回车符转换为换行符,IXON 启动输出流控
* OPOST 启用输出处理功能
* ECHO 使能回显,ICANON 规范模式,ISIG 收到信号产生相应的信号,IEXTEN 输入处理
* CSIZE 数据位掩码,PARENB 使能校验,CS8 8 个数据位
* 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;
*/
memset(&newtio, 0x0, sizeof(struct termios));
cfmakeraw(&newtio);
/* CREAD 使能接受 */
newtio.c_cflag |= CREAD;
/* 设置波特率 */
switch (para->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:
speed = B115200;
printf("default baud rate is 115200\n");
break;
}
/* cfsetspeed 函数,设置波特率 */
if (0 > cfsetspeed(&newtio, speed))
{
printf("fail to set baud rate of uart\n");
return -1;
}
/* 设置数据位大小
* CSIZE 是数据位的位掩码,与上掩码的反,就是将数据位相关的比特位清零
* CSX (X=5,6,7,8) 表示数据位位数
*/
newtio.c_cflag &= ~CSIZE;
switch (para->dbit)
{
case 5:
newtio.c_cflag |= CS5;
break;
case 6:
newtio.c_cflag |= CS6;
break;
case 7:
newtio.c_cflag |= CS7;
break;
case 8:
newtio.c_cflag |= CS8;
break;
default:
newtio.c_cflag |= CS8;
printf("default data bit size is 8\n");
break;
}
/* 设置奇偶校验
* PARENB 用于使能校验
* INPCK 用于对接受的数据执行校验
* PARODD 指的是奇校验
*/
switch (para->parity)
{
case 'N': //无校验
newtio.c_cflag &= ~PARENB;
newtio.c_iflag &= ~INPCK;
break;
case 'O': //奇校验
newtio.c_cflag |= (PARODD | PARENB);
newtio.c_iflag |= INPCK;
break;
case 'E': //偶校验
newtio.c_cflag |= PARENB;
newtio.c_cflag &= ~PARODD;
newtio.c_iflag |= INPCK;
break;
default: //默认配置为无校验
newtio.c_cflag &= ~PARENB;
newtio.c_iflag &= ~INPCK;
printf("default parity is N (no check)\n");
break;
}
/* 设置停止位
* CSTOPB 表示设置两个停止位
*/
switch (para->sbit)
{
case 1: //1个停止位
newtio.c_cflag &= ~CSTOPB;
break;
case 2: //2个停止位
newtio.c_cflag |= CSTOPB;
break;
default: //默认配置为1个停止位
newtio.c_cflag &= ~CSTOPB;
printf("default stop bit size is 1\n");
break;
}
/* 将 MIN 和 TIME 设置为 0,通过对 MIN 和 TIME 的设置有四种 read 模式
* read 调用总是会立即返回,若有可读数据,则读数据并返回被读取的字节数,否则读取不到数据返回 0
*/
newtio.c_cc[VTIME] = 0;
newtio.c_cc[VMIN] = 0;
/* 清空输入输出缓冲区 */
if (0 > tcflush(fd_uart, TCIOFLUSH))
{
printf("fail to flush the buffer\n");
return -3;
}
/* 写入配置,使配置生效 */
if (0 > tcsetattr(fd_uart, TCSANOW, &newtio))
{
printf("fail to set new attribution of terminal\n");
return -4;
}
return 0;
}
其次,该应用程序是使用异步 i/o 来实现串口读取数据,因此需要编写异步 i/o 初始化函数,以及当串口有数据可读时,会跳到的信号处理函数
/* 异步 i/o 初始化函数 */
static void async_io_init(void)
{
struct sigaction sigatn;
int flag;
/* 使能异步 i/o,获取当前进程状态,并开启当前进程异步通知功能 */
flag = fcntl(fd_uart, F_GETFL);
flag |= O_ASYNC;
fcntl(fd_uart, F_SETFL, flag);
/* 设置异步 i/o 的所有者,将本应用程序进程号告诉内核 */
fcntl(fd_uart, F_SETOWN, getpid());
/* 指定实时信号 SIGRTMIN 作为异步 i/o 通知信号 */
fcntl(fd_uart, F_SETSIG, SIGRTMIN);
/* 为实时信号 SIGRTMIN 注册信号处理函数
* 当串口有数据可读时,会跳转到 io_handler 函数
*/
sigatn.sa_sigaction = io_handler;
sigatn.sa_flags = SA_SIGINFO;
/* 初始化信号集合为空 */
sigemptyset(&sigatn.sa_mask);
/* sigaction 的功能是为信号指定相关的处理程序,但是它在执行信号处理程序时
* 会把当前信号加入到进程的信号屏蔽字中,从而防止在进行信号处理期间信号丢失
*/
sigaction(SIGRTMIN, &sigatn, NULL);
}
/* 信号处理函数,当串口有数据可读时,会跳转到该函数执行 */
static void io_handler(int sig, siginfo_t *info, void *context)
{
unsigned char buf[10];
int ret;
int n;
memset(buf, 0x0, sizeof(buf));
if(SIGRTMIN != sig)
{
return;
}
/* 判断串口是否有数据可读 */
if (POLL_IN == info->si_code)
{
ret = read(fd_uart, buf, 8);
printf("[ ");
for (n = 0; n < ret; n++)
{
printf("0x%hhx ", buf[n]);
}
printf("]\n");
}
}
最后,编写主函数用于测试
int main(int argc, char *argv[])
{
struct uart_parameter uart_para;
char device[64];
int rw_flag = -1;
unsigned char write_buf[10] = {0x11, 0x22, 0x33, 0x44,0x55, 0x66, 0x77, 0x88};
int n;
int opt;
memset(&uart_para, 0x0, sizeof(struct uart_parameter));
memset(device, 0x0, sizeof(device));
struct option long_options[] = {
{"device", required_argument, NULL, 'D'},
{"type", required_argument, NULL, 'T'},
{"brate", no_argument, NULL, 'b'},
{"dbit", no_argument, NULL, 'd'},
{"parity", no_argument, NULL, 'p'},
{"sbit", no_argument, NULL, 's'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
memset(&uart_para, 0x0, sizeof(struct uart_parameter));
while((opt = getopt_long(argc, argv, "D:T:b:d:p:s:h", long_options, NULL)) != -1)
{
switch(opt)
{
case'D':
strcpy(device, optarg);
break;
case'T':
if (!strcmp("read", optarg))
{
rw_flag = READ_FLAG;
}
else if (!strcmp("write", optarg))
{
rw_flag = WRITE_FLAG;
}
break;
case'b':
uart_para.baudrate = atoi(optarg);
break;
case'd':
uart_para.dbit = atoi(optarg);
break;
case'p':
uart_para.parity = *optarg;
break;
case's':
uart_para.sbit = atoi(optarg);
break;
case'h':
print_help(argv[0]);
return 0;
default:
break;
}
}
if (NULL == device || -1 == rw_flag)
{
print_help(argv[0]);
return -1;
}
/* 串口初始化 */
if (uart_init(device))
{
printf("fail to execute uart_init\n");
return -2;
}
/* 串口配置 */
if (uart_configuration(&uart_para))
{
/* 恢复之前的配置 */
tcsetattr(fd_uart, TCSANOW, &oldtio);
return -3;
}
/* 通过读写标志判断读写,然后进行读写 */
switch (rw_flag)
{
case 0: // 读串口数据
async_io_init(); // 我们使用异步 i/o 方式读取串口的数据,调用该函数去初始化串口的异步 i/o
for ( ; ; ) // 进入休眠,等待有数据可读,有数据可读之后就会跳转到 io_handler() 函数
{
sleep(1);
}
break;
case 1: // 向串口写入数据
for ( ; ; )
{
write(fd_uart, write_buf, 8);
sleep(1);
}
break;
}
tcsetattr(fd_uart, TCSANOW, &oldtio);
close(fd_uart);
return 0;
}
2.9.2.4 串口测试
硬件连接图如下
实物图如下
在 PC 上打开串口调试助手,测试数据收发,收发的数据最好是 8 个字节
使用应用程序发送数据到 PC,用串口调试助手收到数据
root@igkboard: ./uart_app -D /dev/ttymxc1 -T write
default baud rate is 115200
default data bit size is 8
default parity is N (no check)
default stop bit size is 1
使用串口调试助手发送数据,使用应用程序接受数据,先用应用程序阻塞监听
root@igkboard:~/app/07uart# ./uart -D /dev/ttymxc1 -T read
default baud rate is 115200
default data bit size is 8
default parity is N (no check)
default stop bit size is 1
发送后可以看到接受到的数据
root@igkboard:~/app/07uart# ./uart -D /dev/ttymxc1 -T read
default baud rate is 115200
default data bit size is 8
default parity is N (no check)
default stop bit size is 1
[ 0x88 0x77 0x66 0x55 0x44 0x33 0x22 0x11 ]