赞
踩
串口编程可以简单概括为如下几个步骤:
1.打开串口
2.串口初始化
3.读串口或写串口
4.关闭串口
既然串口在linux中被看作文件,那么在对文件进行操作前必要先对其进行打开操作。
在 Linxu中,串口设备是通过串口终端设备文件来访问的,即通过访问/dev/tty***这些设备文件实现对串口的访问。
调用open()函数来代开串口设备,通常对于串口的打开操作一般使用如下参数。其中O_NOCTTY又是必不可少的。
O_RDWR:以可读可写的方式打开文件。
O_NOCTTY:表示打开的是一个终端设备,程序不会成为该端口的控制终端。如果不使用此标志,任务一个输入(eg:键盘中止信号等)都将影响进程。
O_NDELAY:表示不关心DCD信号线所处的状态(端口的另一端是否激活或者停止)。
open("/dev/ttyUSB5", O_RDWR | O_NOCTTY | O_NDELAY) ; //打开串口设备
在初始化串口之前,我们不得不掌握一些必要的知识。内容比较多,我就不在这里整理了。下面是一位好心人整理的有关串口属性的一些相关知识,不是很了解的可以look look
https://blog.csdn.net/qq_37932504/article/details/121125906
#include <termios.h> #include <unistd.h> int tcgetattr(int fd, struct termios *termios_p); //用于获取与终端相关的参数 int tcsetattr(int fd, int optional_actions, struct termios *termios_p); //用于设置终端参数 int tcdrain(int fd); //等待直到所有写入 fd 引用的对象的输出都被传输 int tcflush(int fd, int queue_selector); //刷清(扔掉)输入缓存 int tcflow(int fd, int action); //挂起传输或接受 int cfmakeraw(struct termios *termios_p);// 制作新的终端控制属性 speed_t cfgetispeed(struct termios *termios_p); //得到输入波特率 speed_t cfgetospeed(struct termios *termios_p); //得到输出波特率 int cfsetispeed(struct termios *termios_p, speed_t speed); //设置输入波特率 int cfsetospeed(struct termios *termios_p, speed_t speed) //设置输出波特率 int tcsendbreak(int fd, int duration);
这里,我们可以看到有一个结构体struct 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_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters 特殊控制字符是一些字符组合,如 Ctrl+C、 Ctrl+Z 等, 当用户键入这样的组合键,终端会采取特殊处理方式。*/
speed_t c_ispeed; /* input speed 输入波特率*/
speed_t c_ospeed; /* output speed 输出波特率*/
};
注:对于这些变量尽量不要直接对其初始化,而要将其通过“按位与”、“按位或” 等操作添加标志或清除某个标志。
注:不同的终端设备,本身硬件上就存在很大的区别,所以配置参数不是对所有终端设备都是有效的。
当然这里只是简单的初始化过程,没有什么可变性,只是一种固定的串口配置,这样只是便于理解罢了
tcgetattr(fd, &oldtermios); //获取原有的串口属性,以便后面可以恢复 tcgetattr(fd, &newtermios); //获取原有的串口属性,以此为基修改串口属性 newtermios.c_cflag|=(CLOCAL|CREAD ); // CREAD 开启串行数据接收,CLOCAL并打开本地连接模式 /* For example: * * c_cflag: 0 0 0 0 1 0 0 0 * CLOCAL: | 0 0 0 1 0 0 0 0 * -------------------- * 0 0 0 1 1 0 0 0 * * */ newtermios.c_cflag &=~CSIZE; // 先清零数据位 /* For example: * * CSIZE = 0 1 1 1 0 0 0 0 ---> ~CSIZE = 1 0 0 0 1 1 1 1 * * c_cflag: 0 0 1 0 1 1 0 0 * ~CSIZE: & 1 0 0 0 1 1 1 1 * ----------------------- * 0 0 0 0 1 1 0 0 * * 这样与数据位无关的部分就保留了下来,单单把数据位全部清零了 * * */ newtermios.c_cflag |= CS8; //设置8bits数据位 newtermios.c_cflag &= ~PARENB; //无校验位 /* 设置9600波特率 */ cfsetispeed(&newtermios, B9600); cfsetospeed(&newtermios, B9600); newtermios.c_cflag &= ~CSTOPB; // 设置1位停止位 newtermios.c_cc[VTIME] = 0; // 非规范模式读取时的超时时间 newtermios.c_cc[VMIN] = 0; // 非规范模式读取时的最小字符数 tcflush(fd ,TCIFLUSH);/* tcflush清空终端未完成的输入/输出请求及数据;TCIFLUSH表示清空正收到的数据,且不读取出来 */ tcsetattr(fd, TCSANOW, &newtermios); //设置串口属性
串口的读写就比较简单了,像上面我们说的一样Linux下皆为文件。因此对串口调用read, write就行了。因为无论是读还是写,我们都是对同一串口进行操作的,所以在这里就不分程序操作了,而是使用select多路复用来实现自发自收的功能。
while(1) { FD_ZERO(&fdset); FD_SET(fd, &fdset); //文件描述符 FD_SET(STDIN_FILENO, &fdset); //标准输入 rv = select(fd+1, &fdset, NULL, NULL, NULL); if(rv < 0) { printf("select() failed: %s\n", strerror(errno)); goto cleanup; } if(rv == 0) { printf("select() time out!\n"); goto cleanup; } /* ----------写串口 -----------*/ if(FD_ISSET(STDIN_FILENO, &fdset)) { memset(wr_buf, 0, sizeof(wr_buf)); fgets(wr_buf, sizeof(wr_buf), stdin); rv = write(fd, wr_buf, strlen(wr_buf)); if(rv < 0) { printf("Write() error:%s\n",strerror(errno)); goto cleanup; } } /* -----------读串口----------- */ if(FD_ISSET(fd, &fdset)) { memset(rd_buf, 0, sizeof(rd_buf)); rv = read(fd, rd_buf, sizeof(rd_buf)); if(rv <= 0) { printf("Read() error:%s\n",strerror(errno)); goto cleanup; } printf("Read %d bytes data from serial port: %s\n", rv, rd_buf); } sleep(5); }
串口关闭就比较简单了,但是不要忘记了一件重要的事情哦~
恢复原有的串口属性~~
tcsetattr(fd, TCSANOW, &newtermios); //恢复默认的串口属性
close(fd);
将上面的代码结合如下:
#include <stdio.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <fcntl.h> #include <termios.h> #include <unistd.h> #include <sys/select.h> #include <sys/stat.h> #include <sys/types.h> int main(int argc, char **argv) { int fd, rv; char wr_buf[128]; char rd_buf[128]; fd_set fdset; struct termios oldtermios; struct termios newtermios; fd = open("/dev/ttyUSB5", O_RDWR | O_NOCTTY | O_NDELAY) ; //打开串口设备 if(fd < 0) { printf("open failure: %s\n", strerror(errno)); goto cleanup; } printf("Open sucess!\n") ; memset(&newtermios, 0, sizeof(newtermios)) ; rv = tcgetattr(fd, &oldtermios); //获取原有串口属性 rv = tcgetattr(fd, &newtermios); //获取原有串口属性,并在此更改 if(rv != 0) { printf("tcgetattr() failure:%s\n", strerror(errno)) ; goto cleanup; } newtermios.c_cflag|=(CLOCAL|CREAD ); // CREAD 开启串行数据接收,CLOCAL并打开本地连接模式 newtermios.c_cflag &=~CSIZE; // 先清零数据位 newtermios.c_cflag |= CS8; //设置8bits数据位 newtermios.c_cflag &= ~PARENB; //无校验位 /* 设置9600波特率 */ cfsetispeed(&newtermios, B9600); cfsetospeed(&newtermios, B9600); newtermios.c_cflag &= ~CSTOPB; // 设置1位停止位 newtermios.c_cc[VTIME] = 0; // 非规范模式读取时的超时时间 newtermios.c_cc[VMIN] = 0; // 非规范模式读取时的最小字符数 tcflush(fd ,TCIFLUSH);/* tcflush清空终端未完成的输入/输出请求及数据;TCIFLUSH表示清空正收到的数据,且不读取出来 */ if((tcsetattr(fd, TCSANOW,&newtermios))!=0) { printf("tcsetattr failed:%s\n", strerror(errno)); goto cleanup ; } while(1) { FD_ZERO(&fdset); FD_SET(fd, &fdset); FD_SET(STDIN_FILENO, &fdset); rv = select(fd+1, &fdset, NULL, NULL, NULL); if(rv < 0) { printf("select() failed: %s\n", strerror(errno)); goto cleanup; } if(rv == 0) { printf("select() time out!\n"); goto cleanup; } /* ------写串口 ------*/ if(FD_ISSET(STDIN_FILENO, &fdset)) { memset(wr_buf, 0, sizeof(wr_buf)); fgets(wr_buf, sizeof(wr_buf), stdin); rv = write(fd, wr_buf, strlen(wr_buf)); if(rv < 0) { printf("Write() error:%s\n",strerror(errno)); goto cleanup; } } /* ------读串口------ */ if(FD_ISSET(fd, &fdset)) { memset(rd_buf, 0, sizeof(rd_buf)); rv = read(fd, rd_buf, sizeof(rd_buf)); if(rv <= 0) { printf("Read() error:%s\n",strerror(errno)); goto cleanup; } printf("Read %d bytes data from serial port: %s\n", rv, rd_buf); } } cleanup: tcsetattr(fd, TCSANOW,&oldtermios); //恢复默认属性 close(fd); return 0; }
如下图所示,是代码的运行结果:这里我们可以看到,收到的确实就是发送的消息,但是返回值却是比我们肉眼可见的字符长度多了“1”,这其实是因为,我们在获取标准输入的字符串的时候,在最后还接受了一个“\n”的换行符。这里可以去了解一下fgets()函数的使用方法。
但是要知道的是,我们操作串口的时候,对于串口属性的设置参数不是一尘不变的,所以为了提高代码的可重用性,我们可以使用可变参数来设置串口的属性~~
#ifndef _SERIALPORT_H_ #define _SERIALPORT_H_ #define SERIALNAME_LEN 128 typedef struct attr_s{ int flow_ctrl; //流控制 int baud_rate; //波特率 int data_bits; //数据位 char parity; //奇偶校验位 int stop_bits; //停止位 }attr_t; extern int serial_open(char *fname); //打开串口 extern int serial_close(int fd, struct termios *termios_p); //关闭串口 extern int serial_init(int fd, struct termios *oldtermios, attr_t *attr); //串口初始化 extern int serial_send(int fd, char *msg, int msg_len); //写数据到串口 extern int serial_recv(int fd, char *recv_msg, int size); //接收串口数据 #endif
这里封装了一个结构体,将所有需要用到的串口属性都放在了里面,这样,我在设计后面的函数时,就可以直接传结构体指针,再根据功能的实际要求,使用自己需要的成员即可。
int serial_open(char *fname) { int fd, rv; if(NULL == fname) { printf("%s,Invalid parameter\n",__func__); return -1; } if((fd = open(fname,O_RDWR|O_NOCTTY|O_NDELAY)) < 0) { printf("Open %s failed: %s\n",fname, strerror(errno)); return -1; } /* 判断串口的状态是否处于阻塞态*/ if((rv = fcntl(fd, F_SETFL, 0)) < 0) { printf("fcntl failed!\n"); return -2; } else { printf("fcntl=%d\n",rv); } if(0 == isatty(fd)) //是否为终端设备 { printf("%s:[%d] is not a Terminal equipment.\n", fname, fd); return -3; } printf("Open %s successfully!\n", fname); return fd; }
int serial_close (int fd, struct termios *termios_p) { /* 清空串口通信的缓冲区 */ if(tcflush(fd,TCIOFLUSH)) { printf("%s, tcflush() fail: %s\n", __func__, strerror(errno)); return -1; } /* 将串口设置为原有属性, 立即生效 */ if(tcsetattr(fd,TCSANOW,termios_p)) { printf("%s, set old options fail: %s\n",__func__,strerror(errno)); return -2; } close(fd); printf("close OK.............."); return 0; }
int serial_init(int fd, struct termios *oldtermios, struct attr_s *attr) { char baudrate[32] = {0}; struct termios newtermios; memset(&newtermios,0,sizeof(struct termios)); memset(oldtermios,0,sizeof(struct termios)); if(!attr) { printf("%s invalid parameter.\n", __func__); return -1; } /* 获取默认串口属性 */ if(tcgetattr(fd, oldtermios)) { printf("%s, get termios to oldtermios failure:%s\n",__func__,strerror(errno)); return -2; } /* 先获取默认属性,后在此基础上修改 */ if(tcgetattr(fd, &newtermios)) { printf("%s, get termios to newtermios failure:%s\n",__func__,strerror(errno)); return -3; } /* 修改控制模式,保证程序不会占用串口 */ newtermios.c_cflag |= CLOCAL; /* 启动接收器,能够从串口中读取输入数据 */ newtermios.c_cflag |= CREAD; /* * ICANON: 标准模式 * ECHO: 回显所输入的字符 * ECHOE: 如果同时设置了ICANON标志,ERASE字符删除前一个所输入的字符,WERASE删除前一个输入的单词 * ISIG: 当接收到INTR/QUIT/SUSP/DSUSP字符,生成一个相应的信号 * * 在原始模式下,串口输入数据是不经过处理的,在串口接口接收的数据被完整保留。 newtermios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); * */ /* * BRKINT: BREAK将会丢弃输入和输出队列中的数据(flush),并且如果终端为前台进程组的控制终端,则BREAK将会产生一个SIGINT信号发送到这个前台进程组 * ICRNL: 将输入中的CR转换为NL * INPCK: 允许奇偶校验 * ISTRIP: 剥离第8个bits * IXON: 允许输出端的XON/XOF流控 * newtermios.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); * */ /* --------设置数据流控制------- */ switch(attr->flow_ctrl) { case 0: //不使用流控制 newtermios.c_cflag &=~CRTSCTS; break; case 1: //使用硬件流控制 newtermios.c_cflag |= CRTSCTS; break; case 2: //使用软件流控制 newtermios.c_cflag |= IXON| IXOFF|IXANY; break; default: break; } /* 设置波特率,否则默认设置其为B115200 */ if(attr->baud_rate) { sprintf(baudrate,"B%d",attr->baud_rate); cfsetispeed(&newtermios, (int)baudrate); //设置输入输出波特率 cfsetospeed(&newtermios, (int)baudrate); } else { cfsetispeed(&newtermios, B115200); cfsetospeed(&newtermios, B115200); } /* ------设置数据位-------*/ newtermios.c_cflag &= ~CSIZE; //先把数据位清零,然后再设置新的数据位 switch(attr->data_bits) { case '5': newtermios.c_cflag |= CS5; break; case '6': newtermios.c_cflag |= CS6; break; case '7': newtermios.c_cflag |= CS7; break; case '8': newtermios.c_cflag |= CS8; break; default: newtermios.c_cflag |= CS8; //默认数据位为8 break; } /* -------设置校验方式------- */ switch(attr->parity) { /* 无校验 */ case 'n': case 'N': newtermios.c_cflag &= ~PARENB; newtermios.c_iflag &= ~INPCK; break; /* 偶校验 */ case 'e': case 'E': newtermios.c_cflag |= PARENB; newtermios.c_cflag &= ~PARODD; newtermios.c_iflag |= INPCK; break; /* 奇校验 */ case 'o': case 'O': newtermios.c_cflag |= (PARODD | PARENB); newtermios.c_iflag |= INPCK; /* 设置为空格 */ case 's': case 'S': newtermios.c_cflag &= ~PARENB; newtermios.c_cflag &= ~CSTOPB; /* 默认无校验 */ default: newtermios.c_cflag &= ~PARENB; newtermios.c_iflag &= ~INPCK; break; } /* -------设置停止位-------- */ switch(attr->stop_bits) { case '1': newtermios.c_cflag &= ~CSTOPB; break; case '2': newtermios.c_cflag |= CSTOPB; break; default: newtermios.c_cflag &= ~CSTOPB; break; } /* OPOST: 表示处理后输出,按照原始数据输出 */ newtermios.c_oflag &= ~(OPOST); newtermios.c_cc[VTIME] = 0; //最长等待时间 newtermios.c_cc[VMIN] = 0; //最小接收字符 //attr->mSend_Len = 128; //若命令长度大于mSend_Len,则每次最多发送为mSend_Len /* 刷新串口缓冲区 / 如果发生数据溢出,接收数据,但是不再读取*/ if(tcflush(fd,TCIFLUSH)) { printf("%s, clear the cache failure:%s\n", __func__, strerror(errno)); return -4; } /* 设置串口属性,立刻生效 */ if(tcsetattr(fd,TCSANOW,&newtermios) != 0) { printf("%s, tcsetattr failure: %s\n", __func__, strerror(errno)); return -5; } printf("Serial port Init Successfully!\n"); return 0; }
接下来的两个函数的定义可以有,但是没必要,因为我这里并没有多加什么东西。如果写的内容要进行封装打包,或者解析的话,就需要加上这两个定义了。
int serial_send (int fd, char *msg, int msg_len) { int rv = 0; rv = write(fd, msg, msg_len); if(rv == msg_len) { return rv; } else { tcflush(fd, TCOFLUSH); return -1; } return rv; }
int serial_recv(int fd, char *recv_msg, int size) { int rv; rv = read(fd, recv_msg, size); if(rv) { return rv; } else { return -1; } /******************************** * * 这里可以自由发挥 * ********************************/ }
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <signal.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <getopt.h> #include <errno.h> #include <string.h> #include <termios.h> #include "serial_port.h" #define SERIAL_DEBUG #ifdef SERIAL_DEBUG #define serial_print(format, args...) printf(format,##args) #else #define serial_print(format, args...) do{} while(0) #endif void sig_stop(int signum); void usage(); void adjust_msg(char *buf); int run_stop = 0; int main(int argc, char **argv) { int fd, rv, ch, i; char *fname = "/dev/ttyUSB6"; //如果未指定,使用该设备节点 char buf[64] = {0}; char send_msg[128]; char recv_msg[128]; fd_set fdset; attr_t attr; struct termios oldtio; struct option opts[] = { {"help" , no_argument , NULL, 'h'}, {"flowctrl", required_argument, NULL, 'f'}, {"baudrate", required_argument, NULL, 'b'}, {"databits", required_argument, NULL, 'd'}, {"parity" , required_argument, NULL, 'p'}, {"stopbits", required_argument, NULL, 's'}, {"name" , required_argument, NULL, 'n'}, {NULL , 0 , NULL, 0 } }; if(argc < 2) { serial_print("WARN: without arguments!"); usage(); return -1; } while((ch = getopt_long(argc,argv,"hf:b:d:p:s:n:",opts,NULL)) != -1) { switch(ch) { case 'h': usage(); return 0; case 'f': attr.flow_ctrl = atoi(optarg); break; case 'b': attr.baud_rate = atoi(optarg); break; case 'd': attr.data_bits = atoi(optarg); break; case 'p': attr.parity = *optarg; break; case 's': attr.stop_bits = atoi(optarg); break; case 'n': fname = optarg; break; } } if((fd = serial_open(fname)) < 0) { serial_print("Open %s failure: %s\n", fname, strerror(errno)); return -1; } if(serial_init(fd, &oldtio, &attr) < 0) { return -2; } signal(SIGINT, sig_stop); signal(SIGTERM, sig_stop); while(!run_stop) { FD_ZERO(&fdset); //清空所有文件描述符 FD_SET(STDIN_FILENO,&fdset); //添加标准输入到fdset中 FD_SET(fd,&fdset); //添加文件描述符fd到fdset中 /* 使用select多路复用监听标准输入和串口fd */ rv = select(fd + 1, &fdset, NULL, NULL, NULL); if(rv < 0) { serial_print("Select failure......\n"); break; } if(rv == 0) { serial_print("Time Out.\n"); goto cleanup; } //有事件发生 if(FD_ISSET(STDIN_FILENO,&fdset)) { memset(send_msg, 0, sizeof(send_msg)); /* 从标准输入读取命令 */ fgets(send_msg, sizeof(send_msg), stdin); /* 处理要发送的数据,因为我们从fgets函数获取的字符串末尾是"\n",而发送AT指令需要的是"\r"*/ adjust_msg(send_msg); // serial_print("Serial port will send: %s\n", send_msg); if((rv = serial_send(fd, send_msg, strlen(send_msg))) < 0) { serial_print("Write failed.\n"); goto cleanup; } #ifndef SERIAL_DEBUG /* 逐一打印一下发送的的数据都是什么 */ for(i = 0; i < rv; i++) { serial_print("Byte: %c\t ASCII: 0x%x\n", send_msg[i], (int)send_msg[i]); } serial_print("INFO:------Write success!\n\n"); #endif fflush(stdin); } if(FD_ISSET(fd,&fdset)) { memset(recv_msg, 0, sizeof(recv_msg)); rv = serial_recv(fd, recv_msg, sizeof(recv_msg)); if(rv <= 0) { serial_print("Read failed: %s\n",strerror(errno)); break; } printf("%s", recv_msg); #ifndef SERIAL_DEBUG serial_print("Receive %d bytes data: %s",rv, recv_msg); /* 逐一打印一下收到的数据一个一个都是什么 */ for(i = 0; i < rv; i++) { serial_print("Byte: %c\t ASCII: 0x%x\n", recv_msg[i], (int)recv_msg[i]); } #endif fflush(stdout); } sleep(3); } cleanup: serial_close(fd, &oldtio); return 0; } void adjust_msg(char *buf) { int i = strlen(buf); strcpy(&buf[i-1],"\r"); } void sig_stop(int signum) { serial_print("catch the signal: %d\n", signum); run_stop = 1; } void usage() { serial_print("-h(--help ): aply the usage of this file\n"); serial_print("-f(--flowctrl): arguments: 0(no use) or 1(hard) or 2(soft)\n"); serial_print("-b(--baudrate): arguments with speed number\n"); serial_print("-d(--databits): arguments: 5 or 6 or 7 or 8 bits\n"); serial_print("-p(--parity ): arguments: n/N(null) e/E(even) o/O(odd) s/S(space)\n"); serial_print("-s(--stopbits): arguments: 1 or 2 stopbits\n"); }
先简单讲一下AT指令收发的实际数据是什么,然后在最后小小的验证一波~~
使用AT指令与串口进行通信,是一种“礼尚往来”的通信方式,即当控制端输入一个AT指令后,与之通信的外部设备将会回复一个结果,就这样一对一的进行。
以最简单的AT指令为例,当串口连接好以后,使用
busybox microcom -s 115200 ttyUSB2
每输入一次AT设备都会回复一个OK,就可以利用不同的指令,结合设备的返回码来与设备通信。
其实,当我敲下AT 回车后,发送给设备的指令实际是
AT<CR>
也就是 “AT\r”
“\r” 是指回到行首,但不会换到下一行,而当我们收到OK时,实际上是收到了
<CR><LF><OK><CR><LF>
也就是 “\r\nOK\r\n”
" /r/n " 合起来才是Windows下的Enter,即回到行首并新建一行。从上面的图中可以看到,OK的确换到了新的一行,当我们在敲AT时,又是在新的一行。
这里就是验证的结果了~
ATE1模式下,发送的数据会接收一遍,再接收应答数据
“\r”和“\n”的ASCII值分别为十六进制的 d 和 a
参考链接:https://blog.csdn.net/weixin_45121946/article/details/107130238
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。