【嵌入式linux】Linux串口通信

2019-07-12 20:09发布

一、串口简介         rs232是三芯通信,即DB9的第2引脚RXD(接收)、第3引脚TXD(发送数据)、第5引脚DG(信号地),rs232是三芯通信,485是两芯通讯的,RS-232串口线 通常 是 DB9--DB9 的 串口通信线,9芯RS-485数据线 是 双绞线或者屏蔽双绞线,232传输距离较近,485传输距离比较远,485是单工(向)通讯,232是双工(向)的。         串口编程都没有区别,都是按照RS232编程的,计算机没有485接口,需要用一个232转485的转换器就可以了。 ------------------------------------------------------- 二、串口操作        在linux下编写终端程序时,有规范模式,非规范模式(原始模式特殊的非规范模式)之分;如不用于终端,而是用在像串口这种情况下,一般设置其为原始模式1、操作步骤         1、打开串口         2、配置串口:对串口的 波特率、数据位、停止位、校验码、等进行设置。         3、读写串口         4、关闭串口 2、读写原理          串口的读写像正常读取其他文件一样使用read()、write()   进行读写即可。但是我在用read()函数从串口接收指定的数量的字符时,发现往往接收到的实际字符数,都与指定的不同。         深层原因:         一般而言,串口的读写模式有直接模式和缓存模式之分。         直接模式:串口的读写都是单字节的,也就是说read/write每次只能操作一个字节;         缓存模式:顾名思义,就是先讲收/发的字节缓存到串口内存中然后再操作,绝大部份串口芯片都支持缓存模式,缓存模式一般同时支持中断聚合和超时机制,也就是说在有数据时,当缓存满或者超时时间到时,都会触发读/写中断操作。         例如,本人在Ubuntu14.04下用一个usb转232来充当串口测试读写时发现一次可以read的字节数总不是我期望的字节数,也进一步证明了当我们读取串口的时候是读取的缓存,而每次读取的时候缓存收到的字节数是不固定的。         总之,通讯过程中无法保证一次发送的数据肯定是一次接收的,所以必须写代码来一次一次的接收,直到接收满足预定的为止,在此过程中可使用select/poll来避免超时接收 ------------------------------------------------------- 三、串口编程         1.关于阻塞和非阻塞模式         简单的串口编程,一般设置成阻塞模式,便可以了。但是在大多数应用场合,把串口设置成阻塞模式是很不实用的,如read()时,如果没有数据发来,这程序一直会阻塞在这里(除非用多线程)。         因此一般把其设置为非阻塞模式。一般是需要用串口读取指定长度的数据,但是read函数实际读取的数据长度,往往会与指定的不同,所以必须自己编写一个读写N字节数据的函数:很快想到用个循环,但是循环中必须有:即使一直没有收到指定长度的数据但在一定时间后也必须跳出循环”的机制,否则就与阻塞模式的没有区别了(也就是让函数一直等,等到指定长度数据接收为止)。         而非阻塞I/O使我们的操作要么成功,要么立即返回错误,不被阻塞。         对于一个给定的描述符两种方法对其指定非阻塞I/O:         (1)调用open获得描述符,并指定O_NONBLOCK标志         (2)对已经打开的文件描述符,调用fcntl,打开O_NONBLOCK文件状态标志。 [C/C++ code]         int flags,s为描述符         flags = fcntl( s, F_GETFL, 0 ) )         fcntl( s, F_SETFL, flags | O_NONBLOCK )   -------------------------------------------------------         2.关键代码解析         最基本的串口设置包括波特率、数据位、校验位和停止位设置,且串口设置主要使用termios.h头文件中定义的termios结构,如下: struct termios {         tcflag_t  c_iflag;   //输入模式标志         tcflag_t  c_oflag;  //输出模式标志         tcflag_t  c_cflag;  //控制模式标志         tcflag_t  c_lflag;   //本地模式标志         cc_t   c_line;        //line discipline         cc_t   c_cc[NCC]; //control characters }   1)打开串口 int uart_open(int fd, char* port) { // 1.以读写方式打开串口 fd = open(port, O_RDWR|O_NOCTTY|O_NDELAY); if (-1 == fd) goto ERR_LABLE_OPEN; // 2.恢复串口的阻塞状态 if(fcntl(fd, F_SETFL, 0) < 0) goto ERR_LABLE_OPEN; //非阻塞:FNDELAY // 3.测试是否为终端设备 if(0 == isatty(STDIN_FILENO)) goto ERR_LABLE_OPEN; return fd; ERR_LABLE_OPEN: perror("uart_open"); return -1; }   说明:         O_RDWR   :指定串口为读写模式。         O_NOCTTY:告诉Unix这个程序不想成为“控制终端”控制的程序,不说明这个标志的话,任何输入都会影响你的程序。         O_NDELAY :告诉Unix这个程序不关心DCD信号线状态,即其他端口是否运行,不说明这个标志的话,该程序就会在DCD信号线为低电平时停止   2)设置串口 int uart_set(int fd,int speed,int databits,int stopbits,int parity,int flow_ctrl) { int i=0; int speed_val[] = {115200, 38400, 19200, 9600, 4800, 2400, 1200, 300}; int speed_arr[] = {B115200, B38400, B19200, B9600, B4800, B2400, B1200, B300}; struct termios options; // 1.获取串口属性。 /*tcgetattr(fd,&options)得到与fd指向对象的相关参数,并存于options中, 该函数还可测试串口配置是否正确,是否可用等。返回值 成功 0 失败-1.*/ if ( 0!=tcgetattr( fd,&options) ) goto ERR_LABLE_SET; // 2.设置串口I/O波特率 for (i= 0; i < sizeof(speed_arr) / sizeof(int); i++) { if (speed == speed_val[i]) { cfsetispeed(&options, speed_arr[i]); cfsetospeed(&options, speed_arr[i]); } } // 3.修改控制模式,保证终端程序不会占用串口 options.c_cflag |= CLOCAL; // 4.修改控制模式,保证能从串口读取输入数据 options.c_cflag |= CREAD; // 5.设置数据流控制(适用于收发波特率不同时) switch(flow_ctrl) { case 0 ://不用任何流控制 options.c_cflag &= ~CRTSCTS; break; case 1 ://使用硬件流控制 options.c_cflag |= CRTSCTS; break; case 2 ://使用软件流控制 options.c_cflag |= IXON | IXOFF | IXANY; break; default: fprintf(stderr,"Unsupported ctrl_flow type! "); goto ERR_LABLE_SET; } // 6.设置有效数据位数 options.c_cflag &= ~CSIZE;//屏蔽其它数据位 switch (databits) { case 5 : options.c_cflag |= CS5; break; case 6 : options.c_cflag |= CS6; break; case 7 : options.c_cflag |= CS7; break; case 8: options.c_cflag |= CS8; break; default: fprintf(stderr,"Unsupported data size "); goto ERR_LABLE_SET; } // 7.设置校验位 switch (parity) { case 'n': case 'N': //无奇偶校验位 options.c_cflag &= ~PARENB; //清除校验使能 options.c_iflag &= ~INPCK; //不使能奇偶校验检测 break; case 'o': case 'O': //设置为奇校验 options.c_cflag |= PARENB; options.c_cflag |= PARODD; options.c_iflag |= INPCK; break; case 'e': case 'E': //设置为偶校验 options.c_cflag |= PARENB; options.c_cflag &= ~PARODD; options.c_iflag |= INPCK; break; case 's': case 'S': //设置为空格符 options.c_cflag &= ~PARENB; options.c_cflag &= ~CSTOPB; break; default: fprintf(stderr,"Unsupported parity "); goto ERR_LABLE_SET; } // 8.设置停止位 switch (stopbits) { case 1: options.c_cflag &= ~CSTOPB; break; case 2: options.c_cflag |= CSTOPB; break; default: fprintf(stderr,"Unsupported stop bits "); goto ERR_LABLE_SET; } // 9.修改输出模式,原始数据通讯(raw) options.c_oflag &= ~OPOST; options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 10.防止串口把回车和换行当成同一个字符 options.c_iflag &= ~(INLCR | ICRNL | IGNCR); options.c_oflag &= ~(ONLCR | OCRNL); // 12.如果在传输XON和XOFF字符时传不过去,需把软件流控制屏蔽 options.c_iflag &= ~(IXON | IXOFF | IXANY); // 13.设置等待时间和最小接收字符 options.c_cc[VTIME] = 0; /* 读取一个字符等待1*(1/10)s */ options.c_cc[VMIN] = 1; /* 满足读取功能的最低字元接收个数*/ // 14.如果发生数据溢出,则刷新收到的数据但是不读 tcflush(fd,TCIFLUSH); // 15.激活配置 (将修改后的termios数据设置到串口配置中) if (tcsetattr(fd,TCSANOW,&options) != 0) goto ERR_LABLE_SET; return 0; ERR_LABLE_SET: perror("uart_set"); return -1; } 说明:         a.在第四步中我们看到一些比较特殊的设置,下面简述一下他们的作用。 c_cc数组的VSTART和VSTOP元素被设定成DC1和DC3,代表ASCII标准的XON和XOFF字符,如果在传输这两个字符的时候就传不过去,需要把软件流控制屏蔽,即:        options.c_iflag &= ~(IXON | IXOFF | IXANY);         b.有时候,在用write发送数据时没有键入回车,信息就发送不出去,这主要是因为我们在输入输出时是按照规范模式接收到回车或换行才发送,而更多情况下我们是不必键入回车或换行的。此时应转换到行方式输入,不经处理直接发送,设置如下:        options.c_oflag &= ~OPOST;        options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);         c.还存在这样的情况:发送字符0X0d的时候,往往接收端得到的字符是0X0a,原因是因为在串口设置中c_iflag和c_oflag中存在从NL-CR和CR-NL的映射,即串口能把回车和换行当成同一个字符,可以进行如下设置屏蔽之:        options.c_iflag &= ~(INLCR | ICRNL | IGNCR);        options.c_oflag &= ~(ONLCR | OCRNL);         d、linux 下串口编程VTIME和VMIN的设置 1、只有设置为阻塞时这两个参数才有效,且仅针对于读操作。       VTIME 定义要求等待的时间量(取值不能大于cc_t))。       VMIN定义了要求等待的最小字节数, 这个字节数可以是0(立即返回)。       options.c_cc[VTIME]  = X;   //设置从获取到Y个字节后开始计时的超时返回时间       options.c_cc[VMIN]    = Y;   //设置要求等待的最小字节数 在原始模式下对read()函数的影响:       1、X  = 0; Y != 0    read()只在读取了Y个字节的数据或者收到一个终止信号时才返回;       2、X != 0; Y  = 0    即使没有数据可以读取,read()等待X时间量(X*0.1s)后返回;       3、X != 0; Y != 0    收到至少Y个数据或者超时时间量够X*0.1S后read()返回;       4、X  =0 ;  Y = 0    即使读取不到任何数据,函数read也会立即返回(相当于非阻塞)。 2、示例:         通过串口连ID卡读卡器,要求读串口至少收6个字节数据立即(VTIME=0)返回,可以将串口的上述两个设置项设置如下:       options.c_cc[VTIME] = 0;       options.c_cc[VMIN]   = 6;   3)读写串口 直接用read()函数和write()函数把串口当做文件来读写即可。        int nwByte = 0; int nrByte  = 0;         nwByte = write(fd, buffer, length);         nrByte  = read(fd, buffer, len);         读取数据方式如下,原始数据模式下每个read函数将返回实际串口收到的字符数,如果串口中没有字符可用,回叫将会阻塞直到以下几种情况:a.有字符进入; b.一个间隔计时器失效; c.错误发送。         在打开串口成功后,使用fcntl(fd, F_SETFL, FNDELAY)语句,可以使read函数立即返回而不阻塞。FNDELAY选项使read函数在串口无字符时立即返回且为0。     注意:         设置为原始模式传输数据的话,read函数返回的字符数是实际串口收到的字符数。Linux下直接用read读串口可能会造成堵塞,或者数据读出错误,此时可使用fcntl或者select等函数实现异步读取。用select先查询com口,再用read去读就可以避免上述错误。   4)关闭串口         串口作为文件来处理,所以一般的关闭文件函数即可: close(fd); 5)其他参数 c_iflag:输出模式标志,控制终端输入方式,具体参数如表1所示。 表1 c_iflag参数 键 值 说 明 IGNBRK 忽略BREAK键输入 BRKINT 如果设置了IGNBRK,BREAK键输入将被忽略 IGNPAR 忽略奇偶校验错误 PARMRK 标识奇偶校验错误 INPCK 允许输入奇偶校验 ISTRIP 去除字符的第8个比特 INLCR 将输入的NL(换行)转换成CR(回车) IGNCR 忽略输入的回车 ICRNL 将输入的回车转化成换行(如果IGNCR未设置的情况下) IUCLC 将输入的大写字符转换成小写字符(非POSIX) IXON 允许输出时对XON/XOFF流进行控制 IXANY 输入任何字符将重启停止的输出 IXOFF 允许输入时对XON/XOFF流进行控制 IMAXBEL 当输入队列满的时候开始响铃 c_oflag:输出模式标志,控制终端输出方式,具体参数如表2所示。 表2 c_oflag参数 键 值 说 明 OPOST 处理后输出 OLCUC 将输入的小写字符转换成大写字符(非POSIX) ONLCR 将输入的NL(换行)转换成CR(回车)及NL(换行) OCRNL 将输入的CR(回车)转换成NL(换行) ONOCR 第一行不输出回车符 ONLRET 不输出回车符 OFILL 发送填充字符以延迟终端输出 OFDEL 以ASCII码的DEL作为填充字符,如果未设置该参数,填充字符为NUL NLDLY 换行输出延时,可以取NL0(不延迟)或NL1(延迟0.1s) CRDLY 回车延迟,取值范围为:CR0、CR1、CR2和 CR3 TABDLY 水平制表符输出延迟,取值范围为:TAB0、TAB1、TAB2和TAB3 BSDLY 空格输出延迟,可以取BS0或BS1 VTDLY 垂直制表符输出延迟,可以取VT0或VT1 FFDLY 换页延迟,可以取FF0或FF1 c_cflag:控制模式标志,指定终端硬件控制信息,具体参数如表3所示。 表3 c_cflag参数 键 值 说 明 CBAUD 波特率(4+1位)(非POSIX) CBAUDEX 附加波特率(1位)(非POSIX) CSIZE 字符长度,取值范围为CS5、CS6、CS7或CS8 CSTOPB 设置两个停止位 CREAD 使用接收器 PARENB 使用奇偶校验 PARODD 对输入使用奇偶校验,对输出使用偶校验 HUPCL 关闭设备时挂起 CLOCAL 忽略调制解调器线路状态 CRTSCTS 使用RTS/CTS流控制 c_lflag:本地模式标志,控制终端编辑功能,具体参数如表4所示。 表4 c_lflag参数 键 值 说 明 ISIG 当输入INTR、QUIT、SUSP或DSUSP时,产生相应的信号 ICANON 使用标准输入模式 XCASE 在ICANON和XCASE同时设置的情况下,终端只使用大写。 ECHO 显示输入字符 ECHOE 如果ICANON同时设置,ERASE将删除输入的字符 ECHOK 如果ICANON同时设置,KILL将删除当前行 ECHONL 如果ICANON同时设置,即使ECHO没有设置依然显示换行符 ECHOPRT 如果ECHO和ICANON同时设置,将删除打印出的字符(非POSIX) TOSTOP 向后台输出发送SIGTTOU信号 c_cc[NCCS]:控制字符,用于保存终端驱动程序中的特殊字符,如输入结束符等。c_cc中定义了如表5所示的控制字符。 表5 c_cc支持的控制字符 宏 说 明 宏 说 明 VINTR Interrupt字符 VEOL 附加的End-of-file字符 VQUIT Quit字符 VTIME 非规范模式读取时的超时时间 VERASE Erase字符 VSTOP Stop字符 VKILL Kill字符 VSTART Start字符 VEOF End-of-file字符 VSUSP Suspend字符 VMIN 非规范模式读取时的最小字符数     四、相关函数 1、tcflush tcflush函数刷清(扔掉)输入缓存(终端驱动法度已接管到,但用户法度尚未读)或输出缓存(用户法度已经写,但尚未发送).     int tcflush(int filedes,int quene)
    quene数该当是下列三个常数之一:
    *TCIFLUSH  刷清输入队列
    *TCOFLUSH  刷清输出队列
    *TCIOFLUSH 刷清输入、输出队列 例如:tcflush(fd,TCIFLUSH);     在打开串口后,串口其实已经可以开始读取数据了 ,这段时间用户如果没有读取,将保存在缓冲区里,如果用户不想要开始的一段数据,或者发现缓冲区数据有误,可以使用这个函数清空缓冲     tcflush(fd, TCIFLUSH);     sleep(2);     ret = uart_recv(fd, RecvBuf, 10, 0*1000); 这样,在sleep之前发的数据都被清空了。 2、多多交流,共同进步,欢迎提出问题~~~