五、RETURN VALUE 返回值
1.cfgetispeed() 返回 termios 结构中存储的输入波特率。
2.cfgetospeed() 返回 termios 结构中存储的输出波特率。
3.其他函数返回:
(1)0: 成功
(2) -1: 失败,
并且为 errno 置值来指示错误。 注意 tcsetattr() 返回成功,如果任何所要求的修改可以实现的话。因此,当进行多重修改时,应当在这个函数之后再次调用 tcgetattr()来检测是否所有修改都成功实现。
1.2 linux串口编程
Linux 操作系统从一开始就对串行口提供了很好的支持,下面就 Linux 下的串行口通讯编程进行简单的介绍。
(一) 串口简介
串行口是计算机一种常用的接口,具有连接线少,通讯简单,得到广泛的使用。常用的串口是 RS-232-C 接口(又称 EIA RS-232-C)它是在 1970 年由美国电子工业协会(EIA)联合贝尔系统、
调制解调器厂家及计算机终端生产厂家共同制定的用于串行通讯的标准。它的全名是"数据终端设备(DTE)和数据通讯设备(DCE)之间串行二进制数据交换接口技术标准"该标准规定采用一个
25 个脚的 DB25 连接器,对连接器的每个引脚的信号内容加以规定,还对各种信号的电平加以规定。传输距离在码元畸变小于 4%
的情况下,传输电缆长度应为 50 英尺。
(二)计算机串口的引脚说明
(三)串口操作
头文件
串口操作需要的头文件
#include <stdio.h> /*标准输入输出定义*/ #include <stdlib.h> /*标准函数库定义*/ #include <unistd.h> /*Unix 标准函数定义*/ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> /*文件控制定义*/ #include <termios.h> /*POSIX 终端控制定义*/ #include <errno.h> /*错误号定义*/
打开串口
在 Linux 下串口文件是位于 /dev 下的
串口一 为 /dev/ttyS0
串口二 为 /dev/ttyS1
打开串口是通过使用标准的文件打开函数操作:
int fd; /*以读写方式打开串口*/ fd = open( "/dev/ttyS0", O_RDWR); if (-1 == fd){ /* 不能打开串口一*/ perror(" 提示错误!"); }
设置串口
最基本的设置串口包括波特率设置,效验位和停止位设置。 串口的设置主要是设置 struct termios 结构体的各成员值。
struct termio { unsigned short c_iflag; /* 输入模式标志 */ unsigned short c_oflag; /* 输出模式标志 */ unsigned short c_cflag; /* 控制模式标志*/ unsigned short c_lflag; /* local mode flags */ unsigned char c_line; /* line discipline */ unsigned char c_cc[NCC]; /* control characters * };
设置这个结构体很复杂,我这里就只说说常见的一些设置:
波特率设置
下面是修改波特率的代码:
struct termios Opt; tcgetattr(fd, &Opt); cfsetispeed(&Opt,B19200); /*设置为19200Bps*/ cfsetospeed(&Opt,B19200); tcsetattr(fd,TCANOW,&Opt);
设置波特率的例子函数
/** *@brief 设置串口通信速率 *@param fd 类型 int 打开串口的文件句柄 *@param speed 类型 int 串口速度 *@return void */ int speed_arr[] = { B38400, B19200, B9600, B4800, B2400, B1200, B300, B38400, B19200, B9600, B4800, B2400, B1200, B300, }; int name_arr[] = {38400, 19200, 9600, 4800, 2400, 1200, 300, 38400, 19200, 9600, 4800, 2400, 1200, 300, }; void set_speed(int fd, int speed){ int i; int status; struct termios Opt; tcgetattr(fd, &Opt); for ( i= 0; i < sizeof(speed_arr) / sizeof(int); i++) { if (speed == name_arr[i]) { tcflush(fd, TCIOFLUSH); cfsetispeed(&Opt, speed_arr[i]); cfsetospeed(&Opt, speed_arr[i]); status = tcsetattr(fd1, TCSANOW, &Opt); if (status != 0) { perror("tcsetattr fd1"); return; } tcflush(fd,TCIOFLUSH); } } }
效验位和停止位的设置
设置效验的函数
/** *@brief 设置串口数据位,停止位和效验位 *@param fd 类型 int 打开的串口文件句柄 *@param databits 类型 int 数据位 取值 为 7 或者8 *@param stopbits 类型 int 停止位 取值为 1 或者2 *@param parity 类型 int 效验类型 取值为N,E,O,,S */ int set_Parity(int fd,int databits,int stopbits,int parity) { struct termios options; if ( tcgetattr( fd,&options) != 0) { perror("SetupSerial 1"); return(FALSE); } options.c_cflag &= ~CSIZE; switch (databits) /*设置数据位数*/ { case 7: options.c_cflag |= CS7; break; case 8: options.c_cflag |= CS8; break; default: fprintf(stderr,"Unsupported data size\n"); return (FALSE); } switch (parity) { case 'n': case 'N': options.c_cflag &= ~PARENB; /* Clear parity enable */ options.c_iflag &= ~INPCK; /* Enable parity checking */ break; case 'o': case 'O': options.c_cflag |= (PARODD | PARENB); /* 设置为奇效验*/ options.c_iflag |= INPCK; /* Disnable parity checking */ break; case 'e': case 'E': options.c_cflag |= PARENB; /* Enable parity */ options.c_cflag &= ~PARODD; /* 转换为偶效验*/ options.c_iflag |= INPCK; /* Disnable parity checking */ break; case 'S': case 's': /*as no parity*/ options.c_cflag &= ~PARENB; options.c_cflag &= ~CSTOPB;break; default: fprintf(stderr,"Unsupported parity\n"); return (FALSE); } /* 设置停止位*/ switch (stopbits) { case 1: options.c_cflag &= ~CSTOPB; break; case 2: options.c_cflag |= CSTOPB; break; default: fprintf(stderr,"Unsupported stop bits\n"); return (FALSE); } /* Set input parity option */ if (parity != 'n') options.c_iflag |= INPCK; tcflush(fd,TCIFLUSH); options.c_cc[VTIME] = 150; /* 设置超时15 seconds*/ options.c_cc[VMIN] = 0; /* Update the options and do it NOW */ if (tcsetattr(fd,TCSANOW,&options) != 0) { perror("SetupSerial 3"); return (FALSE); } return (TRUE); }
需要注意的是:
如果不是开发终端之类的,只是串口传输数据,而不需要串口来处理,那么使用原始模式( Raw Mode)方式来通讯,设置方式如下:
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/ options.c_oflag &= ~OPOST; /*Output*/
读写串口
设置好串口之后,读写串口就很容易了,把串口当作文件读写就是。
发送数据
char buffer[1024]; int Length; int nByte; nByte = write(fd, buffer ,Length)
读取串口数据
使用文件操作read函数读取,如果设置为原始模式(Raw Mode)传输数据,那么read函数返回的字符数是实际串口收到的字符数。
可以使用操作文件的函数来实现异步读取,如fcntl,或者select等来操作。
char buff[1024]; int Len; int readByte = read(fd,buff,Len);
关闭串口
关闭串口就是关闭文件。
close(fd);
例子
下面是一个简单的读取串口数据的例子,使用了上面定义的一些函数和头文件
/**********************************************************************代码说明:使用串口二测试的,发送的数据是字符, 但是没有发送字符串结束符号,所以接收到后,后面加上了结束符号。我测试使用的是单片机发送数据到第二个串口,测试通过。 **********************************************************************/ #define FALSE -1 #define TRUE 0 /*********************************************************************/ int OpenDev(char *Dev) { int fd = open( Dev, O_RDWR ); //| O_NOCTTY | O_NDELAY if (-1 == fd) { perror("Can't Open Serial Port"); return -1; } else return fd; } int main(int argc, char **argv){ int fd; int nread; char buff[512]; char *dev = "/dev/ttyS1"; //串口二 fd = OpenDev(dev); set_speed(fd,19200); if (set_Parity(fd,8,1,'N') == FALSE) { printf("Set Parity Error\n"); exit (0); } while (1) //循环读取数据 { while((nread = read(fd, buff, 512))>0) { printf("\nLen %d\n",nread); buff[nread+1] = '\0'; printf( "\n%s", buff); } } close(fd); return 0; }
a. 串口配置完整函数
下面给出了串口配置的完整的函数。通常,为了函数的通用性,通常将常用的选项都在函数中列出,这样可以大大方便以后用户的调试使用。该设置函数如下所示:
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; /*设置停止位*/ 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; case 460800: cfsetispeed(&newtio, B460800); cfsetospeed(&newtio, B460800); 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[VTIME] = 0; newtio.c_cc[VMIN] = 0; /*处理未接收字符*/ tcflush(fd,TCIFLUSH); /*激活新配置*/ if((tcsetattr(fd,TCSANOW,&newtio))!=0) { perror("com set error"); return -1; } printf("set done!\n"); return 0; }
b. 打开函数完整函数
下面给出了一个完整的打开串口的函数,同样写考虑到了各种不同的情况。程序如下所示:
/*打开串口函数*/ int open_port(int fd,int comport) { char *dev[]={"/dev/ttyS0","/dev/ttyS1","/dev/ttyS2"}; long vdisable; if (comport==1)//串口 1 { fd = open( "/dev/ttyS0", O_RDWR|O_NOCTTY|O_NDELAY); if (-1 == fd){ perror("Can't Open Serial Port"); return(-1); } } else if(comport==2)//串口 2 { fd = open( "/dev/ttyS1", O_RDWR|O_NOCTTY|O_NDELAY); if (-1 == fd){ perror("Can't Open Serial Port"); return(-1); } } else if (comport==3)//串口 3 { fd = open( "/dev/ttyS2", O_RDWR|O_NOCTTY|O_NDELAY); if (-1 == fd){ perror("Can't Open Serial Port"); return(-1); } } /*恢复串口为阻塞状态*/ if(fcntl(fd, F_SETFL, 0)<0) printf("fcntl failed!\n"); else printf("fcntl=%d\n",fcntl(fd, F_SETFL,0)); /*测试是否为终端设备*/ if(isatty(STDIN_FILENO)==0) printf("standard input is not a terminal device\n"); else printf("isatty success!\n"); printf("fd-open=%d\n",fd); return fd; } /*从串口中读取数据*/ int read_datas(int fd, char *rcv_buf,int rcv_wait) { int retval; fd_set rfds; struct timeval tv; int ret,pos; tv.tv_sec = rcv_wait; // wait 2.5s tv.tv_usec = 0; pos = 0; // point to rceeive buf while (1) { FD_ZERO(&rfds); FD_SET(fd, &rfds); retval = select(fd+1 , &rfds, NULL, NULL, &tv); if (retval == -1) { perror("select()"); break; } else if (retval) {// pan duan shi fou hai you shu ju ret = read(fd, rcv_buf+pos, 2048); pos += ret; if (rcv_buf[pos-2] == '\r' && rcv_buf[pos-1] == '\n') { FD_ZERO(&rfds); FD_SET(fd, &rfds); retval = select(fd+1 , &rfds, NULL, NULL, &tv); if (!retval) break;// no datas, break } } else { printf("No data\n"); break; } } return 1; } /*向串口传数据*/ int send_data(int fd, char *send_buf) { ssize_t ret; ret = write(fd,send_buf,strlen(send_buf)); if (ret == -1) { printf ("write device %s error\n", DEVICE_TTYS); return -1; } return 1; }
2. 串口收发实验
本实验用过把串口的发送、接收引脚短接,实现自发自收:使用write函数发出字符,使用read函数读取字符。
2.1 接线
2.1.1 IMX6ULL
2.1.2 STM32MP157
2.2 编程
#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) { //获得驱动程序的默认参数放到oldtio中,备份下来 perror("SetupSerial 1"); return -1; } //设置新的参数 bzero( &newtio, sizeof( newtio ) );//清0,下面设置新的参数 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)//tcsetattr函数设置行规程 { 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);//打开设备结点,O_NOCTTY /* The O_NOCTTY flag tells UNIX that this program doesn't want to be the "controlling terminal" for that port. If you don't specify this then any input (such as keyboard abort signals and so forth) will affect your process. Programs like getty(1M/8) use this feature when starting the login process, but normally a user program does not want this behavior. */ if (-1 == fd){ return(-1); } if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/ /* 1.fcntl(fd,F_SETEL,FNDELAY); 读数据时不等待,没有数据就返回0; 2.fcntl(fd,F_SETFL,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);//这就是我要重点关注的,设置比特率位115200,数据位为8,没有校验位,停止位的个数是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; }
2.3 上机实验
2.3.1 IMX6ULL
先设置工具链:
export ARCH=arm export CROSS_COMPILE=arm-buildroot-linux-gnueabihf- export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
编译、执行程序:
1. Ubuntu上
arm-buildroot-linux-gnueabihf-gcc -o serial_send_recv serial_send_recv.c
2. 板子上
/mnt/serial_send_recv /dev/ttymxc5
2.3.2 STM32MP157
先设置工具链:
export ARCH=arm export CROSS_COMPILE=arm-buildroot-linux-gnueabihf- export PATH=$PATH:/home/book/100ask_stm32mp157_pro-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
编译、执行程序:
1. Ubuntu上 arm-buildroot-linux-gnueabihf-gcc -o serial_send_recv serial_send_recv.c 2. 板子上 /mnt/serial_send_recv /dev/ttySTM3