前言
本篇文章将讲解如何在Linux下使用串口。
一、Linux下的TTY体系介绍
在Linux系统中,tty(Teletypewriter)是指一种终端设备,它提供了用户与操作系统之间的交互界面。在较早的计算机系统中,tty是以打字机作为输入输出设备的终端系统,而现代的Linux系统中,tty则对应着虚拟终端。
Linux下的tty体系是由多个虚拟终端组成的,每个虚拟终端都对应着一个tty设备文件。在Linux系统中,tty设备文件位于/dev目录下,以tty开头,后面跟随一个数字,如tty1、tty2等。
对于用户来说,tty设备提供了一个字符设备节点,用于输入和输出字符数据。用户可以通过tty设备读取输入的字符,并将输出字符发送到tty设备。这些字符可以是用户输入的命令、系统的输出信息等。
用户可以在Linux系统中使用特殊的快捷键(如Ctrl+Alt+F1至Ctrl+Alt+F6)在各个终端之间切换,并在各个tty设备上独立地登录和执行命令。TTY设备为用户提供了与操作系统进行交互的接口,用户可以在不同的虚拟终端上同时工作,执行不同的任务。
在终端下通过ls命令可以查看到非常多的tty设备节点,这些节点都分别对应了不同的功能。
这里引用百问网的一张图片:
二、行规层
行规层(Line Discipline)是指在Unix-like操作系统中提供的一种机制,用于处理终端设备或网络连接中的数据流。它位于终端设备驱动程序和用户进程之间,负责对输入和输出的字符数据进行处理和转换。
行规层在传统的串行终端设备上起着重要的作用。它可以执行以下功能:
输入处理:行规层可以处理终端设备上的输入字符流。它可以执行字符的缓冲、编辑和回显等操作。例如,当用户在终端上输入字符时,行规层可以提供行缓冲,使得用户可以逐行输入,并在按下回车键后将整行字符发送给用户进程。
输出处理:行规层可以对输出字符进行处理。它可以执行转义序列的解释和字符的替换等操作。例如,当用户进程向终端设备发送输出字符流时,行规层可以将特定字符序列转换为控制终端的命令,以调整光标位置、修改显示属性等。
规范模式和原始模式:行规层支持规范模式和原始模式的切换。在规范模式下,行规层提供行缓冲、按行输入和回显等功能。这是默认的终端模式,适用于大多数交互式应用。在原始模式下,行规层关闭缓冲和编辑功能,字符逐个传递给用户进程,适用于特定的应用需求,如串口通信等。
控制字符处理:行规层负责处理特殊控制字符的输入和输出。例如,CTRL+C用于中断正在运行的程序,CTRL+D用于表示输入的结束等。行规层将检测和处理这些特殊字符,并执行相应的操作。
行规层的具体实现在不同的操作系统中会有所不同。在Linux系统中,行规层是通过终端设备驱动程序和TTY子系统来实现的。Linux提供了多个行规层的实现,如原始(line discipline)、终端(line discipline)、网络(line discipline)等。用户可以根据需要选择不同的行规层来完成特定的终端和网络通信任务。
总结来说,行规层是位于终端设备驱动程序和用户进程之间的一层软件机制,用于处理终端设备和网络连接中的数据流。它提供了输入处理、输出处理、规范模式和原始模式的切换以及控制字符处理等功能。通过行规层,用户可以更加灵活地操控和控制终端设备和网络连接。
三、Linux串口编程步骤
在ARM Linux系统上进行串口编程可以按照以下步骤进行:
1.打开串口设备:
首先,需要打开要使用的串口设备文件,一般在Linux系统中,串口设备文件位于/dev/ttySx或/dev/ttyUSBx,其中x是串口号或USB串口号。可以使用系统调用open()函数以读写方式打开串口设备文件。
2.配置串口参数:
在打开串口设备后,需要配置串口的通信参数,如波特率、数据位、停止位、校验位等。可以使用termios数据结构和相关的函数来进行设置。可以使用函数tcgetattr()获取当前串口参数,然后修改相关参数,最后通过tcsetattr()函数将修改后的参数应用到串口设备。
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_cc[NCCS]; /* control characters */ cc_t c_line; /* line discipline (== c_cc[19]) */ speed_t c_ispeed; /* input speed */ speed_t c_ospeed; /* output speed */ };
各个参数介绍:
c_iflag(输入模式标志位):这个字段包含一系列用于控制输入模式的标志位。例如,可以使用IGNBRK来忽略BREAK键输入,使用INPCK来启用奇偶校验等。
c_oflag(输出模式标志位):这个字段包含一系列用于控制输出模式的标志位。例如,可以使用OPOST来启用输出处理(执行输出转义字符的功能),使用ONLCR来将输出中的换行符转换为回车换行等。
c_cflag(控制模式标志位):这个字段包含一系列用于控制串口硬件特性的标志位。例如,可以使用CBAUD来设置波特率,使用CS8来设置数据位为8位等。
c_lflag(本地模式标志位):这个字段包含一系列用于控制本地模式(终端行为)的标志位。例如,可以使用ICANON来启用规范模式,使用ECHO来启用回显等。
c_cc(控制字符数组):这个数组存储了一些特殊控制字符的值,如终端驱动程序使用的信号字符,输入和输出的起始字符等。
c_line(线路规程):这个字段存储了用于处理数据的线路规程(line discipline)的标识符。一般情况下,可以将其设置为0。
c_ispeed(输入波特率):这个字段指定输入的波特率,表示从串口接收数据的速度。
c_ospeed(输出波特率):这个字段指定输出的波特率,表示向串口发送数据的速度。
3.读取和写入数据:
一旦串口打开且参数配置完成,就可以进行数据的读取和写入。可以使用read()函数从串口设备读取数据,使用write()函数将数据写入串口设备。
4.清理并关闭串口:
在使用完串口后,需要进行清理和关闭。可以使用tcsetattr()函数将串口参数恢复到默认值,并使用close()函数关闭串口设备文件。
四、代码编写
代码的话也是使用的是百问网提供的代码:
#include <stdio.h> #include <string.h> #include <sys/types.h> #include <errno.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <termios.h> #include <stdlib.h> /* set_opt(fd,115200,8,'N',1) */ int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop) { struct termios newtio,oldtio; if ( tcgetattr( fd,&oldtio) != 0) { perror("SetupSerial 1"); return -1; } bzero( &newtio, sizeof( newtio ) ); newtio.c_cflag |= CLOCAL | CREAD; newtio.c_cflag &= ~CSIZE; newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/ newtio.c_oflag &= ~OPOST; /*Output*/ switch( nBits ) { case 7: newtio.c_cflag |= CS7; break; case 8: newtio.c_cflag |= CS8; break; } switch( nEvent ) { case 'O': newtio.c_cflag |= PARENB; newtio.c_cflag |= PARODD; newtio.c_iflag |= (INPCK | ISTRIP); break; case 'E': newtio.c_iflag |= (INPCK | ISTRIP); newtio.c_cflag |= PARENB; newtio.c_cflag &= ~PARODD; break; case 'N': newtio.c_cflag &= ~PARENB; break; } switch( nSpeed ) { case 2400: cfsetispeed(&newtio, B2400); cfsetospeed(&newtio, B2400); break; case 4800: cfsetispeed(&newtio, B4800); cfsetospeed(&newtio, B4800); break; case 9600: cfsetispeed(&newtio, B9600); cfsetospeed(&newtio, B9600); break; case 115200: cfsetispeed(&newtio, B115200); cfsetospeed(&newtio, B115200); break; default: cfsetispeed(&newtio, B9600); cfsetospeed(&newtio, B9600); break; } if( nStop == 1 ) newtio.c_cflag &= ~CSTOPB; else if ( nStop == 2 ) newtio.c_cflag |= CSTOPB; newtio.c_cc[VMIN] = 1; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */ newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间: * 比如VMIN设为10表示至少读到10个数据才返回, * 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒) * 假设VTIME=1,表示: * 10秒内一个数据都没有的话就返回 * 如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回 */ tcflush(fd,TCIFLUSH); if((tcsetattr(fd,TCSANOW,&newtio))!=0) { perror("com set error"); return -1; } //printf("set done!\n"); return 0; } int open_port(char *com) { int fd; //fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY); fd = open(com, O_RDWR|O_NOCTTY); if (-1 == fd){ return(-1); } if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/ { printf("fcntl failed!\n"); return -1; } return fd; } /* * ./serial_send_recv <dev> */ int main(int argc, char **argv) { int fd; int iRet; char c; /* 1. open */ /* 2. setup * 115200,8N1 * RAW mode * return data immediately */ /* 3. write and read */ if (argc != 2) { printf("Usage: \n"); printf("%s </dev/ttySAC1 or other>\n", argv[0]); return -1; } fd = open_port(argv[1]); if (fd < 0) { printf("open %s err!\n", argv[1]); return -1; } iRet = set_opt(fd, 115200, 8, 'N', 1); if (iRet) { printf("set port err!\n"); return -1; } printf("Enter a char: "); while (1) { scanf("%c", &c); iRet = write(fd, &c, 1); iRet = read(fd, &c, 1); if (iRet == 1) printf("get: %02x %c\n", c, c); else printf("can not get data\n"); } return 0; }
代码测试:
这里输入a后不会马上返回数据只有按下回车后才会打印出数据。
这是因为有行规层的限制,输入a后行规层并不会唤醒scanf,只有当输入回车后行规层才会将scanf唤醒。
总结
本篇文章就讲解到这里。