对于Linux,我们都知道Linux下皆为文件,这当然也包括我们今天的主角——串口。因此对串口的操作都和对文件的操作一样(涉及到了open,read, write,close 等文件的基本操作)。
一、 Linux下串口编程的流程
串口编程可以简单概括为如下几个步骤:
? 1.打开串口 ? 2.串口初始化 ? 3.读串口或写串口 ? 4.关闭串口
1.打开串口
既然串口在linux中被看作文件,那么在对文件进行操作前必要先对其进行打开操作。
在 Linxu中,串口设备是通过串口终端设备文件来访问的,即通过访问/dev/tty***这些设备文件实现对串口的访问。
调用open()函数来代开串口设备,通常对于串口的打开操作一般使用如下参数。其中O_NOCTTY又是必不可少的。
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 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
这里封装了一个结构体,将所有需要用到的串口属性都放在了里面,这样,我在设计后面的函数时,就可以直接传结构体指针,再根据功能的实际要求,使用自己需要的成员即可。
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; //默认数据位为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;
}
写数据到串口—— 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)
#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指令后,与之通信的外部设备将会回复一个结果,就这样一对一的进行。
以最简单的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
|