Linux串口编程教程(二)——终端IO介绍

来源:互联网 发布:陈奕迅歌词 知乎 编辑:程序博客网 时间:2024/06/05 22:52

Linux串口编程教程(二)——终端I/O介绍

前言:本篇将对终端IO的一系列常用属性以及函数进行解读,这也是为最后一篇教程进行铺垫,希望您仔细阅读。

注意:本篇文章参考了《UNIX环境高级编程》中的第18章。您也可以下载我写的一些源代码。

什么是终端

终端是一种字符型设备,它分为一下四种:

  1. 串行端口终端(Serial Port Terminal):是使用计算机串行端口连接的设备,计算机把每个串行端口都看作是一个字符设备。串行端口所对应的设备名称 为/dev/ttySn(n表示从0开始的整数)。

  2. 伪终端(Pseudo Terminal):是成对的逻辑终端设备,例如/dev/ptyp3 和/dev/ttyp3 (在设备文件系统中分别是/dev/pty/m3 和/dev/pty/s3 ), 它们与实际物理设备并不直接相关。

  3. 控制终端(Controlling Terminal):当前进程的控制终端的设备特殊文件 /dev/tty。可以使用命令”ps –ax ”来查看进程与哪个控制终端相连使用命令”tty ”可以查看它具体对应哪个实际终端设备。/dev/tty 有些类似于到实际所使用终端设备的一个联接。

  4. 控制台终端(Console):计算机显示器通常被称为控制台终端(Console),它仿真了类型为Linux的一种终端(TERM=Linux),并且有一些设备特殊文件与之相关联:tty0、tty1、tty2等。

终端I/O工作模式

终端I/O有两种工作模式:

  1. 规范输入处理:在这种模式下,输入以行为单位进行处理。每次读请求终端驱动都返回一行。若不做特殊处理,默认为规范模式。

  2. 非规范输入处理:输入字符不以行为单位进行处理。

终端设备是由一般位于内核中的终端驱动程序控制,每个终端设备有一个输入队列和一个输出队列。

大多数 UNIX 系统在一个称为终端行规程的模块中进行规范处理。它位于内核通用读、写函数和实际设备驱动程序之间的模块。

终端结构体

我们可以检测和更改的终端特性设备文件都包含在下面这个结构中:

#include <termios.h>  struct termios  {      tcflag_t    c_iflag;    /* input flag */      tcflag_t    c_oflag;    /* output flag */      tcflag_t    c_cflag;    /* control flag */      tcflag_t    c_lflag;    /* local flag */      cc_t        c_cc[NCCS]; /* control characters */  };

说明:

  1. 输入标志:控制终端设置驱动程序字符的输入。(剥除输入字节的第8位,允许输入奇偶校验等)

  2. 输出标志:控制终端设备驱动的输出(执行输出处理、将换行符映射为 CR/LF 等)。

  3. 控制标志:影响到 RS-232串行线(忽略调制解调器的状态线、每个字符的一个或两个停止位等)。

  4. 本地标志:本地标志影响驱动程序和用户之间的接口(回送的开或关、可视的擦除字符、终端产生的信号的启用以及后台输出的作业控制停止信号等)。

  5. Tcflag_t 类型对于标志位值已经足够大了,常被定义它为 unsigned int 或 unsigned long 类型。

  6. C_cc 数组包含所有我们能改变的特殊字符,Cc_t 通常定义为unsigned char 。NCCS是该数组的长度,一般介于15到20之间。

注意:各个标志的值以及特殊字符可以查阅前言提到的相关内容,后文会讲解几个常用的。

- 关于特殊字符有几点要说明:

  1. 不能更改的特殊字符是换行符和回车符(\n和\r)。

  2. 大多数特殊字符在被终端驱动程序识别并进行处理后都被丢弃,并不传送给读进程。例外的字符是换行符(NL,EOL,EOL2)和回车符(CR)。

  3. 终端定义的另一个“字符”是BREAK。BREAK实际上不是一个字符,而是异步串行数据传送时发生的一个条件。是一个0值的位序列。

终端操作

  • 获得和设置终端属性:
#include <termios.h>    int tcgetattr(int filedes, struct termios *termptr);/* 获取终端属性 */  int tcsetattr(int filedes, int opt, const struct termios *termptr); /* 设置终端属性 */   /*  *说明:   *返回值:若成功则返回0,出错则返回-1。  *fileds是终端设备描述符,若没有引用一个终端设备则出错返回-1,errno设置为ENOTTY。  *opt参数可以指定为以下的值:   *TCSANOW :更改立即生效。  *TCSADRAIN:发送所有输出后更改才发生,若更改输出参数则应该使用此选项。  *TCSAFLUSH:发送所有输出后更改才发生,更进一步,在更改发生时未读的所有输入数据都被删除。   */
  • 示例一:
    代码:
/*************************************************************************    > File Name: example1.c    > Author: AnSwEr    > Mail: 1045837697@qq.com    > Created Time: 2015年08月31日 星期一 16时25分24秒 ************************************************************************//* * tcgetattr和tcsetattr演示 */#include<stdio.h>#include<termios.h>#include<stdlib.h>#include<unistd.h>int main(void){    struct termios t;    /*先获取终端属性*/    if(tcgetattr(STDIN_FILENO,&t) < 0)    {        perror("tcgetattr failed!");        exit(EXIT_FAILURE);    }    /*与字符屏蔽标志进行与操作,得到当前的字符长度*/    switch(t.c_cflag & CSIZE)    {        case CS5:        printf("5 bits/byte\n");        break;        case CS6:        printf("6 bits/byte\n");        break;        case CS7:        printf("7 bits/byte\n");        break;        case CS8:        printf("8 bits/byte\n");        break;        default:        printf("unkown bits/byte\n");    }    t.c_cflag &= ~CSIZE; //clear out the c_cflag    t.c_cflag |= CS8; //set 8bits/byte    /*设置终端属性*/    if(tcsetattr(STDIN_FILENO,TCSANOW,&t) < 0)    {        perror("tcsetattr failed!");        exit(EXIT_FAILURE);    }    /*获取新的终端属性*/    if(tcgetattr(STDIN_FILENO,&t) < 0)    {        perror("tcgetattr failed!");        exit(EXIT_FAILURE);    }    /*检测是否设置成功*/    if((t.c_cflag & CSIZE) == CS8)        printf("set successfully!\n");    else        printf("set failed!\n");    exit(EXIT_SUCCESS);}

运行结果:
这里写图片描述

  • 命令行检查和更该终端属性
/*查看当前终端设置*/stty -a //显示终端所有选项,前面有连字符的说明禁用。/*查看名为ttya的终端设置,*/stty -a < /dev/ttya
  • 波特率函数
#include <termios.h>  speed_t cfgetispeed(const struct termios *termptr);/* 获取输入波特率 */    speed_t cfgetospeed(const struct termios *termptr);/* 获取输出波特率 */  /* 返回值:若成功则返回波特率值 */ int cfsetispeed(struct termios *termptr, speed_t speed);/* 设置输入波特率 */    int cfsetospeed(struct termios *termptr, speed_t speed);/* 设置输出波特率 */  /* 返回值:若成功则返回0,出错则返回-1;*/ 

说明:当speed为B0时,表示挂断,调制解调器的控制线不再起作用。波特率函数的演示将在下一篇中进行。

  • 行控制函数
#include <termios.h>  int tcdrain(int filedes);  //等待所有输出都被发送int tcflow(int filedes, int action);//对输入和输出控制流进行控制 /*  * 参数:  * action参数取值如下:  * TCOOFF       输出被挂起;  * TCOON        重新启动以前被挂起的输出;  * TCIOFF       系统发送一个STOP字符,将使终端设备暂停发送数据;  * TCION        系统发送一个START字符,将使终端设备恢复发送数据;  */int tcflush(int filedes, int queue);  //刷新输入或输出缓冲区/* * queue参数取值如下:  * TCIFUSH      刷清输入队列;  * TCOFUSH      刷清输出队列;  * TCIOFUSH     刷清输入、输出队列;  */int tcsendbreak(int filedes, int duration); //在一个指定的时间区内发送连续的0位流。/* * 若duration为0,则发送延续0.25至0.5s 之间,若非0,则依赖于实现。 *//*返回值:若成功则返回0,若出错则返回-1*/

具体应用见第三篇。

注意:这里之所以没有使用ioctl函数,是因为这个函数对终端设备操作的时候,其最后一个参数的数据类型随执行动作的不同而不同,于是对参数类型的检查就称为不可能。

终端标识

  • 相关函数
#include <stdio.h>  char * ctermid(char *ptr);//确定控制终端的名字/*  * 说明: * 返回值:若成功则返回指向控制终端名的指针,出错则返回指向空字符串的指针;    * ptr非null,且指向长度至少为L_ctermid字节的数组,进程的控制终端名存放在该数组中;  *大多数系统中,控制终端名字是/dev/tty */  #include <unistd.h>  int isatty(int filedes);  //判断文件描述符是否引用一个终端/* 返回值:若为终端设备则返回1(真),否则返回0(假)*/  char *ttyname(int filedes);  //获得在文件描述符上打开的终端设备的路径名/* 返回值:指向终端路径名的指针,若出错则返回NULL */ 
  • 示例二
    代码:
/*************************************************************************    > File Name: example2.c    > Author: AnSwEr    > Mail: 1045837697@qq.com    > Created Time: 2015年08月31日 星期一 18时48分47秒 ************************************************************************//* *isatty 示例 */#include<stdio.h>#include<unistd.h>int main(void){    printf("fd 0:%s\n",isatty(0)?"tty":"not a tty");    printf("fd 1:%s\n",isatty(1)?"tty":"not a tty");    printf("fd 2:%s\n",isatty(2)?"tty":"not a tty");    return 0;}

运行结果:
这里写图片描述

  • 示例三
    代码:
/*************************************************************************    > File Name: example3.c    > Author: AnSwEr    > Mail: 1045837697@qq.com    > Created Time: 20150831日 星期一 185409秒 ************************************************************************//* *ttyname函数示例 */#include<stdio.h>#include<unistd.h>int main(void){    char *name;    if(isatty(STDIN_FILENO))    {        name = ttyname(STDIN_FILENO);        if(name == NULL)            name = "undefined";    }    else        name = "not a tty";    printf("fd 0:%s\n",name);    if(isatty(STDOUT_FILENO))    {        name = ttyname(STDOUT_FILENO);        if(name == NULL)            name = "undefined";    }    else        name = "not a tty";    printf("fd 1:%s\n",name);    if(isatty(STDERR_FILENO))    {        name = ttyname(STDERR_FILENO);        if(name == NULL)            name = "undefined";    }    else        name = "not a tty";    printf("fd 2:%s\n",name);    return 0;}

运行结果:
这里写图片描述

规范模式

规范模式(我们使用的linux系统中的终端):发一个读请求,输入一行后,终端程序即返回。下列几个条件引起读返回:

  1. 当读到请求的字节数时读请求返回,下一次读从上一次停止处开始。

  2. 当读请求遇到定界符时,读请求返回。如NL,EOL,EOL2, EOF。另外,若设置ICRNL,但未设置IGNCR,则将CR字符转换为NL字符。(只有EOF字符在被终端驱动程序处理后会被删除)

  3. 如果捕获到一个信号且函数不自动重启,则读请求探返回。(读终端操作被信号中断,并且不重新读,则读请求返回)

非规范模式

非规范模式是指关闭termios结构中的c_lflag域的ICANON标志位。在非规范模式下,输入数据不组成一行,下面的一些特殊字符也不进行处 理:ERASSE, KILL, EOF,NL, EOL, EOL2, CR, REPRINT ,STATUS 和 WERASE。

利用串口收发数据时一般使用非规范模式。

  • 非规范模式的数据返回规则:已经读了指定数量的数据或是过了给定的时间后返回。该技术使用了termios结构中c_cc数组的两个变量:MIN和TIME。

    1. MIN : 指明了在读返回之前应读的最小字节数。下标为VMIN。
    2. TIME : 指明了等待数据的时间。下标为VTIME。
    3. 共有四种情况:
    1. MIN >0, TIME>0        TIME从接收到的第一个字节开始计数。如果在超时前,接收到MIN个字节,那么返回MIN个字节。如果在接收到MIN个字节前,发生了时间超时,则至少有一个字节返回,因为计时器是从接收到第一个字节开始计数的。    2. MIN >0, TIME == 0        在这种情况下,直到读到的字节数达到MIN个时才返回。可能造成read无限阻塞。    3. MIN==0, TIME >0        TIME的值从调用read开始计算,如果收到一个字节或时间超时时返回。若超时,read返回04. MIN==0, TIME==0        如果一些数据是有效的,则 read返回这些数据。如果没有数据有效,则立即返回0
  • 这里介绍一种非规范模式(原始模式):
/*************************************************************************    > File Name: raw_mode.c    > Author: AnSwEr    > Mail: 1045837697@qq.com    > Created Time: 2015年09月02日 星期三 22时41分14秒 ************************************************************************/#include<stdio.h>#include<termios.h>#include<stdlib.h>#include<unistd.h>static int RawMode(int fd) {      struct termios raw,old;    if (!isatty(STDIN_FILENO))    {        fprintf(stderr,"not a tty!\n");        exit(EXIT_FAILURE);    }    if (tcgetattr(fd,&old) == -1)    {        perror("tcgetattr failed");        exit(EXIT_FAILURE);    }    raw = old;  /* modify the original mode */    /* input modes: no break, no CR to NL, no parity check, no strip char,     * no start/stop output control. */    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);    /* output modes - disable post processing */    raw.c_oflag &= ~(OPOST);    /* control modes - set 8 bit chars */    raw.c_cflag |= (CS8);    /* local modes - choing off, canonical off, no extended functions,     * no signal chars (^Z,^C) */    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);    /* control chars - set return condition: min number of bytes and timer.     * We want read to return every single byte, without timeout. */    raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */    /* put terminal in raw mode after flushing */    if (tcsetattr(fd,TCSAFLUSH,&raw) < 0)    {        perror("tcsetattr failed!\n");        exit(EXIT_FAILURE);    }    return 0;}

终端窗口大小

linux系统提供了一个跟踪终端大小的功能,内核为每个终端或者是伪终端保存了一个winsize结构体,这个结构体中保存了当前终端大小的信息。

struct winsize {        unsigned short ws_row;        unsigned short ws_col;        unsigned short ws_xpixel;        unsigned short ws_ypixel;};

作用如下:

  1. 我们可以使用 ioctl 的 TIOCGWINSZ 得到该结构的当前值。

  2. 我们可以使用 ioctl 的 TIOCSWINSZ 将新值保存到内核维护的结构中。如果这个新值与当前内核中存放的值不一样,则SIGWINCH信号发送给当前进程组。

  3. 当值发生变化时,除了保存结构的当前值和产生一个信号之外,内核不做其它事情。

  4. 当窗口大小发生变化通知应用程序时,应用程序接收到此信号后,它可以取窗口大小的新值,重绘屏幕。

    • 示例四
      代码:
/*************************************************************************> File Name: example4.c> Author: AnSwEr> Mail: 1045837697@qq.com> Created Time: 2015年09月02日 星期三 09时29分00秒************************************************************************//** print winsize*/#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <termios.h>#ifndef TIOCGWINSZ#include <sys/ioctl.h>#endifstatic void pr_winsize(int fd){    struct winsize size;    if(ioctl(fd,TIOCGWINSZ,(char *) &size) < 0)    perror("TIOCGWINSZ error");    printf("%d rows,%d columns\n",size.ws_row,size.ws_col);}static void sig_winch(int signo){    printf("SIGWINCH received\n");    pr_winsize(STDIN_FILENO);}int main(void){    if(isatty(STDIN_FILENO)==0)    {        exit(EXIT_FAILURE);    }    if(signal(SIGWINCH,sig_winch)==SIG_ERR)        perror("signal error\n");    pr_winsize(STDIN_FILENO);//打印初始值    for(;;)    {        pause();    }    return 0;}

运行结果:
这里写图片描述

总结

本章说明了一些终端操作函数,介绍了一些标志和特殊字符。对第三篇的串口编程起到了铺垫的作用。

反馈与建议

  • 微博:@AnSwEr不是答案
  • github:AnSwErYWJ
  • 博客:AnSwEr不是答案的专栏
0 0