版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2021 凌云物网智科实验室·郭工

  • Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292

wechat_pub

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 串口测试

硬件连接图如下

image-uart_hardware_connection

实物图如下

img-uart_profile_display

在 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

img-uart_write

使用串口调试助手发送数据,使用应用程序接受数据,先用应用程序阻塞监听

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

img-uart_read

发送后可以看到接受到的数据

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 ]