文章目录

  • 一、 Linux下串口编程的流程
    • 1.打开串口
    • 2. 串口初始化
      • 2.1 常用函数总览
      • 2.2 初始化
    • 3. 串口的读写
    • 4. 串口关闭
  • 二、代码——串口编程实现自发自收
  • 三、可变参数控制串口属性的函数封装
    • 3.1 头文件——serial_port.h
    • 3.2 函数定义——serial_port.c
      • 打开串口——serial_open
      • 关闭串口——serial_close
      • 串口初始化——serial_init
      • 写数据到串口—— serial_send
      • 从串口读取数据—— serial_recv
    • 3.3 主程序(已更新为收发AT指令)
  • 四、补充一下串口实现AT指令的收发

对于Linux,我们都知道Linux下皆为文件,这当然也包括我们今天的主角——串口。因此对串口的操作都和对文件的操作一样(涉及到了open,read, write,close 等文件的基本操作)。

一、 Linux下串口编程的流程

串口编程可以简单概括为如下几个步骤:

​ 1.打开串口
​ 2.串口初始化
​ 3.读串口或写串口
​ 4.关闭串口

1.打开串口

既然串口在linux中被看作文件,那么在对文件进行操作前必要先对其进行打开操作。

在 Linxu中,串口设备是通过串口终端设备文件来访问的,即通过访问/dev/tty***这些设备文件实现对串口的访问。

调用open()函数来代开串口设备,通常对于串口的打开操作一般使用如下参数。其中O_NOCTTY又是必不可少的。

  • O_RDWR:以可读可写的方式打开文件。

  • O_NOCTTY:表示打开的是一个终端设备,程序不会成为该端口的控制终端。如果不使用此标志,任务一个输入(eg:键盘中止信号等)都将影响进程。

  • O_NDELAY:表示不关心DCD信号线所处的状态(端口的另一端是否激活或者停止)。

 open("/dev/ttyUSB5", O_RDWR | O_NOCTTY | O_NDELAY) ; //打开串口设备

2. 串口初始化

在初始化串口之前,我们不得不掌握一些必要的知识。内容比较多,我就不在这里整理了。下面是一位好心人整理的有关串口属性的一些相关知识,不是很了解的可以look look

https://blog.csdn.net/qq_37932504/article/details/121125906

2.1 常用函数总览
#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 输出波特率*/
};

注:对于这些变量尽量不要直接对其初始化,而要将其通过“按位与”、“按位或” 等操作添加标志或清除某个标志。

注:不同的终端设备,本身硬件上就存在很大的区别,所以配置参数不是对所有终端设备都是有效的。

2.2 初始化

当然这里只是简单的初始化过程,没有什么可变性,只是一种固定的串口配置,这样只是便于理解罢了

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);  //设置串口属性

3. 串口的读写

​ 串口的读写就比较简单了,像上面我们说的一样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);
}

4. 串口关闭

串口关闭就比较简单了,但是不要忘记了一件重要的事情哦~

恢复原有的串口属性~~

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()函数的使用方法。

三、可变参数控制串口属性的函数封装

但是要知道的是,我们操作串口的时候,对于串口属性的设置参数不是一尘不变的,所以为了提高代码的可重用性,我们可以使用可变参数来设置串口的属性~~

3.1 头文件——serial_port.h

#ifndef  _SERIALPORT_H_
#define  _SERIALPORT_H_#define SERIALNAME_LEN 128typedef 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

这里封装了一个结构体,将所有需要用到的串口属性都放在了里面,这样,我在设计后面的函数时,就可以直接传结构体指针,再根据功能的实际要求,使用自己需要的成员即可。

3.2 函数定义——serial_port.c

打开串口——serial_open
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;
}
关闭串口——serial_close
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;
}
串口初始化——serial_init
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;  //默认数据位为8break;}/* -------设置校验方式------- */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;
}
写数据到串口—— serial_send

​ 接下来的两个函数的定义可以有,但是没必要,因为我这里并没有多加什么东西。如果写的内容要进行封装打包,或者解析的话,就需要加上这两个定义了。

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;
}
从串口读取数据—— serial_recv
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;}/**********************************      这里可以自由发挥*********************************/
}

3.3 主程序(已更新为收发AT指令)

#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)
#endifvoid 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");
#endiffflush(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_DEBUGserial_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]);}
#endiffflush(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指令后,与之通信的外部设备将会回复一个结果,就这样一对一的进行。

以最简单的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

C语言串口编程收发数据 并实现AT指令的收发 可变参数控制串口属性 树莓派4G模块相关推荐

  1. UART0串口编程(五):串口编程(UART0)之UC/OS(一)UC/OS下的串口发送任务编程

    UART0串口编程(五) 串口编程(UART0)之UC/OS(一) 一.在UC/OS中设计串口程序所要考虑的问题 1.     串口通信数据以帧为单位进行处理,如果需要接收大量数据,则帧缓冲区规模必然 ...

  2. 大数据学习day16-Set接口、Map、可变参数

    大数据学习day16 Set接口.Map.可变参数 今日内容 Set接口中hashSet LinkedHashSet Map集合特点 Map集合的遍历方式 方法可变参数 斗地主案例排序 总结 第一章 ...

  3. QT入门第十四天 串口通信协议+收发数据+波特率+数据位+停止位+奇偶校验+串口识别射频RFID的卡号

    QT入门第十四天 串口通信[QT入门第十四天 串口通信协议+收发数据+波特率+数据位+停止位+奇偶校验+串口识别射频RFID的卡号 第一章 常见的硬件通信接口协议 [1]硬件通信接口协议 [2]使用串 ...

  4. QT5实现串口收发数据(上位机与下位机通信)

    最近帮老师做一个应用程序,通过上位机与下位机进行串口通信,最后实现实时绘图,通过几天努力,成功实现蓝牙串口通信. 参考博客1 注意:代码中一些与串口无关代码,可以忽略掉 一.QT5串口基础知识 1. ...

  5. C语言程序周期接收虚拟串口发送的数据

    背景 我之前的一篇博客讲解了怎么使用虚拟串口和串口调试助手:虚拟串口模拟器和串口调试助手使用教程,这次我们在此基础上继续来使用虚拟串口周期发送和接收功能. 我们知道,在Windows的操作系统上,将串 ...

  6. C——Linux下的串口编程

    原 C--Linux下的串口编程 2017年06月06日 19:30:50 C_Aya 阅读数:11537 <span class="tags-box artic-tag-box&qu ...

  7. Qt 多线程串口编程

    一.问题 以前串口编程使用第三方的CnComm.h编程,CnComm作者博客链接,使用起来还蛮好的,不过既然用qt了就想着用qt自带的QSerialPort,移植性更好一些,结果折腾了好几天,主要遇到 ...

  8. 单片机 串口编程之串口通信仿真实验

    单片机 串口编程之串口通信仿真实验 一.简述        记--简单的使能串口,串口收发数据的例子.(使用Proteus仿真+虚拟串口调试)        代码,仿真文件打包:链接: https:/ ...

  9. 【linux】串口编程(一)——配置串口

    目前遇到的串口编程都是用于通信,很少作为终端显示.以前没有对串口编程做深入研究,本次以libmodbus源码中对串口的设置为例,详解总结串口编程时配置的属性(struct termios) 以libm ...

最新文章

  1. C# where用法
  2. [仙剑四]仙剑四十大经典语句评析
  3. 开发高性能并发应用不是一件容易的事情。这类应用的例子包括高性能Web服务器、游戏服务器和搜索引擎爬虫...
  4. Appium定位元素的几种方法总结
  5. [luoguP4306][JSOI2010]连通数
  6. 通信基站(dfs回溯,思维)
  7. 芝麻HTTP:redis-py的安装
  8. winform窗体控件多,加载慢、卡顿的解决办法
  9. LOOP WITH CONTROL 用法
  10. 实践与反思_在行动中反思的实践
  11. java quartz 2.2.3_java – Spring 3 Quartz 2错误
  12. 用汇编的眼光看C++(之特殊函数)
  13. pstack: Input/output error failed to read target解决
  14. 计算机程序设计语言分为机器语言,汇编语言和高级语言三种,简述计算机程序设计语言(机器语言、汇编语言、高级语言)的优缺点。...
  15. 最新版校园招聘进大厂系列----------(4)京东篇 -----未完待续
  16. 进化算法之粒子群算法介绍附代码——PSO
  17. 鸿蒙系统是怎样一种系统,鸿蒙系统pc版怎么安装 鸿蒙系统pc版安装教程
  18. Problem F: Matrix Problem (III) : Array Practice Time Limit: 1 Sec Memory Limit: 4 MB Submit: 8787
  19. 收废品小程序的推广策略与实践
  20. 若依前后端分离框架去掉首页 登录后跳转至动态路由的第一个路由

热门文章

  1. linux系统查看串口占用,Linux 系统串口信息查看
  2. 国庆旅游3天,Python 把我的疲倦治愈了
  3. android OTG (USB读写,U盘读写)最全使用相关总结
  4. P1287 盒子与球
  5. 原理 一篇文章通透理解序列号实现原理
  6. 深度学习评估指标之目标检测——(yolov5 可视化训练结果以及result.txt解析)
  7. 小青蛙oracle跟踪,《小青蛙》三实践三反思观课报告
  8. 易语言 文本_替换_正则
  9. 基于GPT硬盘模式重装win10操作系统
  10. 什么时候使用left join?