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

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>
#endif

static 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;
}

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

总结

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

反馈与建议

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值