[imx6ull]Linux下TTY-串口编程


一、Linux TTY

1.TTY介绍

TTY 是Teletype或Teletypewriter的缩写,原来是指电传打字机,在以前计算机体积很大,所以用teletype这个设备来连接到计算机,后来这种设备键盘显示器取代,但是他们都作为计算机的终端设备所存在,所以TTY沿用至今,用来泛指计算机的终端设备,它作为一个子系统既支持串口,也支持键盘,显示器,还支持更复杂的功能。

在这里插入图片描述

老式电传打字机

2.控制台和终端

控制台(Console)和终端(Terminal)在我们如今的平时使用中以经很少回去区分他们,但是在计算机的早期时代它们却不同,我们知道早期计算机价格昂贵,座椅我们会使用专门的设备也就是电传打字机(TTY)通常通过串口连接计算机,然后来使用计算机,这样一台只有键盘和显示器通过串口连接到计算机的设备就是终端,而早期直接连接在计算机上,而并不是用电传打字机通过串口连接计算机的那套的键盘和显示器就是控制器。控制器是计算机本身的设备,一个计算机只有一个控制台,在计算机启动时内核与后台服务等消息,都可以显示到控制台上,而不会显示到终端上,如下:

在这里插入图片描述

由于计算机的硬件价格越来越便宜,导致如今通常都是一个人用一台计算机,用不上了早期的那种终端设备了,所以渐渐的控制台和终端从硬件的概念演化成了软件的概念,所以现在把直接显示系统消息的终端称为控制台,而其他的则称为终端,但是在我们使用的Linux系统中已经很少区分控制台与终端了。

注意:控制台也是一种终端,不过它相对权限更大,它可以查看内核打印的信息,我们可以从多个终端中指定一个作为控制台

终端就是处理主机输入和输出的一套设备,用来显示主机的运算输出,以及接受主机要求的输入,只要能够提供给计算机输入和输出功能的就是终端,与所在的位置无关,可以是真实设备也可能是虚拟设备
终端的分类包括,本地终端,用串口连接的终端以及基于网络的远程终端

  1. 本地终端,对于个人 pc 机,连接了显示器,键盘,鼠标等设备就可以称作一个本地终端
  2. 用串口连接的终端,也就是将开发板连接到一个带显示器和键盘的 pc 机,然后 pc 机通过运行一个终端模拟程序,从而实现数据收发
  3. 基于网络的远程终端,则是利用 ssh 协议远程登录到一个主机

3.TTY 设备节点

在根文件系统的 /dev 路径下可以看到很多和 tty 相关的设备节点,如下所示:

设备节点含义
/dev/ttyNdev/tty0 代表前台程序的终端,/dev/tty 代表自己所使用的终端,剩余的从 /dev/tty1 开始的 /dev/ttyX 分别代表一个虚拟终端
/dev/pts/N这类设备节点是伪终端对应的设备节点,伪终端对应的设备节点都在 /dev/pts 目录下,以数字编号命名,通过 ssh 或者 telnet 这些远程登录协议登录到开发板,那么开发板就会在 /dev/pts 目录下生成一个设备节点
/dev/ttymxcNimx6ull 的串口终端,以此命名
/dev/console通过内核的配置可以指定 console 是哪一个 tty 设备

注意:N代表编号如0、1、2等等

二、TTY应用编程

串口的应用编程其实就是通过ioctl对串口进行配置,然后调用read读取串口的数据,再使用write向串口写入数据,但是linux为上层用户做了一层封装,也就是将ioctl操作操作封装成了一套标准API,我们直接使用这一套标准API编写自己的串口应用程序。

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 这两个成员来指定串口的波特率

含义
B48004800 波特率
B96009600 波特率
B1920019200 波特率
B3840038400 波特率
B5760057600 波特率
B115200115200 波特率
CS55 个数据位
CS66 个数据位
CS77 个数据位
CS88 个数据位
CSTOPB2 个停止位,如果不设置该标志则默认是一个停止位
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.终端的三种工作模式

终端的三种工作模式,分别是规范模式 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 的取值不同,会有以下四种不同的情况:
MINTIME说明
=0=0read 调用总是会立即返回,若有可读数据,则读数据并返回被读取的字节数,否则读取不到数据返回 0
>0=0read 函数会被阻塞,直到有 MIN 个字符可以读取时,read 才返回,返回值为读取的字节数
=0>0只要有数据可读或者经过 TIME 个十分之一秒的时间,read立即返回,返回为读取的字节数
>0>0当有 MIN 个字节可读或者两个输入字符之间的时间间隔超过 TIME 个十分之一秒,read 才返回,因为在输入第一个字符后系统才会启动定时器,所以,read 至少读取一个字节后才返回
  • 原始模式:
    是一种特殊的非规范模式,所有的输入数据以字节为单位被处理,即有一个字节输入时,触发输入有效,但是终端不可回显,并且禁用终端输入和输出字符的所有特殊处理

3.应用代码

uart.c

/*********************************************************************************
 *      Copyright:  (C) 2023 Deng Yonghao<dengyonghao2001@163.com>
 *                  All rights reserved.
 *
 *       Filename:  uart.c
 *    Description:  This file uart TTY  
 *                 
 *        Version:  1.0.0(2023年03月29日)
 *         Author:  Deng Yonghao <dengyonghao2001@163.com>
 *      ChangeLog:  1, Release initial version on "2023年03月29日 15时21分00秒"
 *                 
 ********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <getopt.h>
#include <libgen.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <signal.h>

#define	READ_FLAG 0			/*读标志*/
#define	WRITE_FLAG 1		/*写标志*/

struct uart_parameter {
    unsigned int    baudrate;       // 波特率 
    unsigned char   dbit;           // 数据位 
    char            parity;         // 奇偶校验 
    unsigned char   sbit;           // 停止位 
};

static struct termios   oldtio;     // 用于保存终端的配置参数
static int              fd_uart;  	// 串口终端对应的文件描述符

static int uart_init(const char *device);//串口初始化
static int uart_configuration(const struct uart_parameter *para);//串口配置
static void async_io_init(void);//异步i/o初始化函数
static void io_handler(int sig, siginfo_t *info, void *context);//信号处理函数,当串口有数据可读时,会跳转到该函数执行
static void program_usage(char *progname);//提示信息

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;
	char					*progname=NULL;

    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));
	progname = (char *)basename(argv[0]);

    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':
                program_usage(progname);
                return 0;

            default:
                break;
        }
    }

    if (NULL == device || -1 == rw_flag) 
    {
        program_usage(progname);
        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;
}

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 初始化函数 */
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");
    }
}

static void program_usage(char *progname)
{
	printf("Usage: %s [OPTION]...\n", progname);

	printf("-D device\n");
    printf("-T type\n");
    printf("-b brate\n");
    printf("-d dbit\n");
    printf("-p parity\n");
    printf("-s sbit\n");
    printf("-h help\n");

    return;

}

4.串口测试

硬件连接如下:

在这里插入图片描述

使用tftp命令把我们交叉编译好的可执行文件下载到我们的开发板上,并赋予可执行权限,然后运行此程序,可以看见我们的参数提示:

在这里插入图片描述

打开串口调试工具后通过参数-D选择对应的串口终端与写功能来发送数据:

在这里插入图片描述

然后可以在串口调试工具中接收到我们所发送的十六进制信息:

在这里插入图片描述

然后我们执行程序来接收数据,使用程序阻塞监听然后发送十六进制的数据后如下:

在这里插入图片描述
在这里插入图片描述
成功的我们通过TTY串口编程发送和接收到了数据,成功完成了串口编程。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值