经过以下学习,我们掌握:
- AT指令与wifi模块的测试方法:通过CH340直接测试,研究各种AT指令下wifi模块的响应信息形式。
- 编程,使用串口中断接收wifi模块对AT指令的响应信息以及透传数据,通过判断提高指令执行的准确度。
- wifi模块的调试方法:白盒测试。
- 串口接收缓冲区字符串的获取方法有两种:①直接存在字符数组中,用strstr()函数查找子串(有效指令);②定义临时字符,保证子串(有效指令)从字符数组第0位开始存储。第二种更好,因为你要考虑有效指令的存放可能因为字符数组的长度限制而只被部分捕获。
- 了解wifi模块分别以客户端(client)以及服务器(server)模式进行工作的特点:
- 服务器和客户端联入同一个局域网,因此都知道互相的IP地址,也是数据交互的前提;
- 以客户端模式工作就是多对1的关系(8266和PC都是多,路由器是1),以服务器模式工作就是1对多的关系(8266是1,其余是多)。总之,服务器就是1,客户端就是多。
- 服务器有端口号,服务器与每个客户端进行数据交互有通道号。
- 服务器与客户端建立连接时的连接请求,一般都是由客户端发起的(通过服务器端口,向服务器IP地址发送请求),然后服务器这边就有个响应。服务器与客户端在连接期间都是保持一个握手状态。
- 服务器与客户端断开连接的请求,可以由服务器发起,也可以由客户端发起。
1.ESP8266wifi模块
1、ESP8266介绍:可以在乐鑫科技官网查询。
- ESP8266模块是面向物联网应用的高性价比、高度集成的 Wi-Fi MCU,也就是说它就是一个单片机。
- 集成了 32 位 Tensilica 处理器、标准数字外设接口、天线开关、射频 balun、功率放大器、低噪放大器、过滤器和电源管理模块等,仅需很少的外围电路,可将所占 PCB 空间降低。
- ESP8266EX 内置超低功耗 Tensilica L106 32 位 RISC 处理器,CPU 时钟速度最高可达 160 MHz,支持实时操作系统 (RTOS) 和 Wi-Fi 协议栈,可将高达 80% 的处理能力留给应用编程和开发。
- 可以将wifi模块设计到51或32的PCB板上,这样就制作成了一个可以上网的开发板。
2、AT指令:蓝牙,ESP-01s,Zigbee, NB-Iot等通信模块都是基于AT指令的设计。所有的指令没必要去记。
- 终端设备(比如PC、MCU)通过串口向终端适配器(比如说蓝牙、wifi模块)发送AT指令:AT指令集是从终端设备(Terminal Equipment,TE)或数据终端设备(Data Terminal Equipment,DTE)向终端适配器(Terminal Adapter,TA)或数据电路终端设备(Data Circuit Terminal Equipment,DCE)发送的。
-
AT指令的功能就是驱动终端适配器干活,比如联网、数据交互等。
3、AT指令和普通数据的区别:
- AT指令对所传输的数据包大小有定义:即对于AT指令的发送,除AT两个字符外,最多可以接收1056个字符的长度(包括最后的空字符)。
- 每个AT命令行中只能包含一条AT指令;对于由终端设备主动向PC端报告的URC指示或者response响应,也要求一行最多有一个,不允许上报的一行中有多条指示或者响应。
- AT指令是控制类数据,都要以回车作为结尾:响应或上报以回车换行为结尾。AT指令发送时,一定需要在串口助手中勾选“发送新行”,没有此选项的话,需要手动在指令末位键盘敲一个回车键和指令一起发送给wifi模块。辨析:比如说在AT指令模式下,我第一次发送指令时忘记发送新行了,下一次发送带新行的指令后,wifi模块会响应“ERROR”,因为wifi模块在读取AT指令时是以\r\n作为结尾的,第一次没有新行的指令相当于还在缓冲区当中,也就是没有结束符,第二次带新行的指令发送过来,才读到最后的\r\n,相当于发送了两遍指令,所以wifi模块不认识这个指令,会响应“EEROR”。
4、ESP-01S初始配置和验证:ESP-01s出厂波特率正常是115200,也有可能是9600,需要连在CH340驱动上,再与电脑进行连接,进行验证。此时PC就是终端设备,ESP-01S就是终端适配器。
- 接线:交叉接线,经过测试,ESP-01S的VCC接在CH340驱动的5V针脚上,串口才能正常工作。另外,模块会存在发热现象。
- 打开ESP8266调试助手:使用安信可串口助手,实际上ESP8266的技术支持很多都是安信可做的。
- 确定波特率:经过验证是115200的波特率,因为以9600的波特率发送AT指令后,模块没有响应。
- 发送AT+RST:重启wifi模块。购买来的模块的响应信息有两种情况,如下,但是以看见ready为准(多次发送可能就看不到ready了)。
- 发送AT:测试模块功能是否正常,正常的话会响应OK。
- 发送AT+UART=9600,8,1,0,0:配置成9600的波特率,同时说明数据位是8位、停止位是1位、校验位是0位。成功wifi模块会响应“OK”。
- 点击关闭串口,切换成9600波特率:目的是以后用单片机来玩ESP-01S时,波特率是对的上的(实际上51单片机也能配置成115200,但前面我们一直在讲9600)。
2.AT指令入网设置
0、互联网的4个协议层:从上到下依次是应用层、传输层、网络层、链路层。
网络层 | 应用层 |
1发送和接受数据包时,要注明发送者和接收者的IP地址; 2IP协议版本:IPV4:四个8位二进制数(十进制下就是0~255)例如 192.168.0.123; 3连上路由器的无线终端设备会被分配独立(但每次连接不固定)的IP地址; 4查找我的电脑在WLAN中的IP地址方法:ipconfig; 5默认网关(gateway): 连上“中国移动…服务器”的路由器可以使它的局域网连接互联网,路由器(Router)的IP地址是网关,它由上述服务器分配,一个局域网下只有一个网关; 6子网掩码(subnet):255.255.255.0 子网是巨型网络的分支,用于区分IPV4中的子网地址和设备序号,二者组成了设备的IP 7实例:用连上WiFi的电脑在互联网上打开某网站的过程:已知网站IP地址 数据包传输路线:电脑(浏览器)→路由器→中国移动服务器→互联网→某网站 | 1HTTP:Hyper Text Transfer Protocol,超文本传输协议; 2HTTP请求和响应演示:用连上WiFi的电脑在互联网上打开某网站的过程: 电脑浏览器←→互联网←→网站服务器(HTTP请求数据包→、HTTP响应数据包←); 3HTTP请求数据包: ①组成:请求行 & 请求头; ②请求行:请求方法 + 网站首页 + 协议版本,例如:GET/HTTP/1.1; ③请求头: HOST:网址 ……………… 4网站IP地址的确定: 请求数据包中的网址先被送至DNS服务器进行域名解析,之后会返回网站服务器的IP地址 5HTTP响应数据包: ①组成:响应状态行 & 响应头; ②响应状态行:协议版本 + 状态码 + 状态码含义,例如:HTTP/1.1 200 OK; ③响应头: 时间信息 Content-Type:响应体内容类型;字符集 = 类型 (例如:Content-Type: text/html; charset = UTF-8) 6注:请求行、请求头以及响应行、响应头之间用 \r\n 区分开(表示隔一行), \r\n\r\n 则表示空行,头与体之间用空行分隔。 |
传输层 | |
1TCP:Transmission Control Protocol,传输控制协议; 应用:电子邮件、文件; 2UDP:User Datagram Protocol,用户数据报协议; 应用:语音、视频、网游; |
1、设置wifi模块的工作模式:这属于网络的链路层,共有三种模式可选。指令是:AT+CWMODE=(1/2/3)
- 1是设备模式,或称无线设备模式(Wirless Station):这种模式下就像手机联wifi进行上网一样让ESP8266联入家里的路由器,可与连入同一个局域网的其他设备通讯。
- 2是路由模式,或称接入点模式(Acess Point):这种模式下就像手机开热点一样将ESP8266当做一个小路由器。
- 3是双模式;
- 设置成功,wifi模块会响应“OK”
- 设置失败,wifi模块会响应“ERROR”,失败的原因可能是拼写出错。
- 没有响应的话,说明你没有发送新行。
- 设置双模式成功后,能在电脑上看到这个wifi模块的wifi名字是ESP_28DCAB
2、以设备模式联入路由器:告知wifi名字和密码。
- 在家里,开路由器,我的指令是:AT+CWJAP="CMCC-XyVF","XyVFVsrz"
- 在学校,开手机热点,我的指令是:AT+CWJAP="HUAWEI P20","abcdefgh"
- 连接成功,wifi模块会响应“WIFI CONNECTED” 、 “WIFI GOT IP” 、“OK”(有时候可能不会显示“OK”,不用担心,实际上是有的)
- 连接失败,有两种情况,①wifi模块会响应“ERROR”。失败的原因可能是双引号和逗号没写成半角(英文模式),或者指令后面多了个空格;②wifi模块会响应“+CWJAP:3”、“FAIL”,原因是wifi名字拼写错误导致这个wifi检索不到,或者wifi密码拼写错误。
- 没有响应的话,说明你没有发送新行。
- wifi模块一上电就会去自动匹配上次连接的路由器,一旦匹配上会自动连接wifi,并返回“WIFI CONNECTED”、“WIFI GOT IP”(没有“OK”);没有匹配上的话会显示“WIFI DISCONNECT”,并且wifi模块上的led指示灯会闪烁,只有连上了才会熄灭。
- 再次用上述AT指令连接wifi时,wifi模块先会响应“WIFI DISCONNECTED”,然后再进行连接,响应“WIFI CONNECTED”、“GOT IP”、“OK”(有时候可能不会显示“OK”,不用担心,实际上是有的)
- 我发现,进入透传模式再返回AT指令模式后,再次连网,第一次wifi模块会响应“CLOSED”、“OK”,之后再连网,wifi模块会响应“OK”。
3、查询ESP-01S的IP地址:指令是:AT+CIFSR
- 成功后,wifi模块会响应多行信息,包括路由模式的网关、路由模式的mac地址、设备模式的IP地址、设备模式的mac地址,最后响应“OK”。
- 设备模式的IP地址:图中,在连入家中wifi的前提下,我的ESP-01S的IP地址是192.168.1.8;
- 路由模式的网关:APIP后面的网关是当ESP-01S在路由模式下的网关,当前的值是192.168.4.1(网关的最后一位一般都是1)
- 路由模式的mac地址:APMAC后面的12个(6组,一组2个)16进制数是ESP-01S在路由模式下的mac地址(物理地址),相当于人的身份证,每台可以联网的设备都有mac地址,它是出厂就烧录好的,IP地址不是永久的,但mac地址是固定的。关于mac地址的具体解释参阅此链接。
- 设备模式的mac地址:STAMAC后面的也是mac地址,我们可以发现只有连入wifi后,我们才能看到这个设备的mac地址。并且一台设备可能有多个MAC地址,比如说这个ESP8266,它的MAC地址就有两个。
- 注意:家中路由器的网关不怎么变化,如果你是用手机当做热点的话,今天和明天的手机网关可能会变化,所以用的时候需要实时去查。
3.作为客户端连接TCP服务器
1、数据传输路线:我们接下来在PC上用网络调试助手构建一个基于TCP协议的服务器。接下来也是发送AT指令,让ESP8266当做客户端工作。这个时候ESP8266和外部的通讯方式分为两类。
- 走串口:wifi模块和PC走串口协议,PC通过USB转TTL的CH340驱动,将指令传送给wifi模块,然后wifi模块响应一些配置信息给PC。
- 走wifi:wifi模块和PC也可以走网络传输层中的TCP协议,这时候PC和wifi模块是连接在同一个局域网下的,wifi模块作为客户端,PC作为服务器。数据从wifi模块传递到路由器,再由路由器中转,将这个信息传递给PC上的TCP服务器;数据也可以从TCP服务器经过路由器中转,传递到wifi模块。
2、网络调试助手设置:像这个服务器我们以后也能自己写,基于Linux的或者基于Windows的,通过Scoket编程,用java或者C语言编程。或者说用手机上的网络助手app也是可以的。
- 协议类型选择TCP服务器
- 本地主机IP用自己电脑的IP:在家里连接路由器,查到是192.168.1.7
- 本机端口号:使用8880,尽量不要用8080。
3、wifi模块连接TCP服务器:指令是:AT+CIPSTART="TCP","192.168.1.7",8880 (这里的IPV4地址是PC的IP地址,在PC的命令提示符中通过config指令查询)
- 连接成功,wifi模块会响应“CONNECT”、“OK”。另外还能在网络调试助手中的连接对象中看见wifi模块的IP地址。
- 连接失败,wifi模块会响应“ERROR”、“CLOSED”。失败的原因可能是:没有让本机和ESP8266连在同一个局域网下,或者8266没有提前连上局域网,或者电脑需要关闭防火墙(百度看看),或者网络调试助手出问题了,没有成功建立TCP服务器(换一个)。
- 成功连接后,如果再次发送上述AT指令,wifi模块会响应“ALREADY CONNECTED”、“ERROR”。
- 成功连接后,如果在网络调试助手上手动将TCP服务器断开,那么wifi模块会响应“CLOSED”,这时候如果想再次连接,需要手动重新建立TCP服务器,并再往wifi模块发送连接该服务器的AT指令。
4、向服务器发送固定字节数据:这种模式比较蠢,每次发数据都要先设置数据的长度。
- 不要勾选网络调试助手中接收区设置中的16进制显示,否则发送来的数据是16进制的。
- 设置要发送数据的字节长度,进入数据待发状态:指令是AT+CIPSEND=n (n表示数据的字节数)
- 设置成功,wifi模块会响应“OK”,同时在安可信串口调试助手中能看见">"这个符号,代表等待数据发送。
- 如果失败,且wifi模块响应“IPMODE=1”、“ERROR”,那么其原因是你之前配置过透传模式(见下面第3.5小节)。
- 发送的数据尽量不要有新行,也就是我们不发送换行和回车符号"\r\n",因为它们占2个字节。
- 发送成功后,wifi模块会从透传模式自动回到AT指令模式。
- 如果发送的字节数超出了n字节数,wifi模块先会响应“busy s...”,再响应“Recv n bytes”、“SEND OK”,同时只会截取前面的字节发送给TCP服务器。
- 如果你发送的数据长度不足n字节,wifi模块不会有任何响应,同时网络调试助手上看不到任何发送来的数据,这个数据就好像一直存在“缓冲区”中,只有你达到或者超过了设定的n个字节数,才会显示。
5、透传模式下数据交互:我们不希望每次发数据之前都设置数据长度,同时我们也不希望受到是否发送新行的限制。在透传模式下,随便你怎么发,随便你怎么收。
- 设置透传模式:指令是:AT+CIPMODE=1 。
- 设置成功,wifi模块会响应“OK”。
- 设置失败,wifi模块会响应“ERROR”。
- 进入数据待发状态:指令是AT+CIPSEND 。
- 设置成功,wifi模块会响应“OK”。
- 设置失败,wifi模块会响应“ERROR”。
- 在透传模式下如果还继续让wifi模块给TCP服务器发送带有新行的AT指令,wifi模块自身不会有任何响应,这些数据会出现在网络调试助手上,只不过成为带新行的普通数据。
- 退出透传模式,回到AT指令模式:wifi模块发送数据时,发送"+++"(不加新行),发送一次没反应,就发送两次。
4.单片机帮你做这一切,实现wifi透传控制LED(重在调试)
1、单片机串口需要做哪些事情:终端设备从PC变成单片机(MCU),让单片机通过串口给模块发送一系列AT指令,指挥wifi模块以客户端模式工作。
重启wifi模块- 配置wifi模块的工作模式(如果之前设置过的话,这一步wifi模块会自动完成)
- 连接上局域网络(之前设置过网络连接的话,这一步wifi模块会自动完成)
- 连接上TCP服务器
- 设置透传模式
- 进入数据待发状态,正式进入透传模式
发送数据(本demo不演示发送透传指令,也就是不向服务器发送任何请求,只用来接收服务器数据)关闭透传模式(本demo不退出透传模式)
注:波特率是提前用电脑通过AT指令给wifi模块配置好的,用9600。
2、白盒测试:【项目工程文件夹】
- 白盒测试的原因:在我们的串口中断程序中,有一个从缓冲寄存器SBUF中读取字符到字符数组的这么一个过程,所以wifi模块响应回来的字符串被单片机截胡了,这导致我们没法在串口助手上看见它们,这也称为黑盒测试。在AT指令模式下,单片机通过串口给wifi模块发送指令后,wifi模块会响应信息;在透传模式下,wifi模块会接收到由TCP服务器透传来的数据,然后wifi模块会将数据响应给单片机。所以需要使用白盒测试来提前得知wifi模块的响应信息。
- 白盒测试的接线方法:不将wifi模块的TX接到单片机的RX,而将其接到电脑的RX,那么电脑就能通过安信可串口调试助手来观察“51给模块发送的指令之后,模块到底有没有去执行这些指令”,这有点像让ESP8266脚踏两只船。
- 测试原理:将由wifi模块发送的所有数据(AT指令模式下的响应信息 & 透传模式下接收到的信息)在串口助手上显示出来。在第2节习题4(PC发送字符串指令给单片机)的代码基础上,我们让单片机每隔5s给wifi模块发送上述AT指令。当串口助手上对每条指令都响应“OK” 或者 PC上的网络调试助手上能收到数据 或者 串口能查看到网络调试助手发送来的数据 时,都能说明单片机已经成功驱动wifi模块进入透传模式
- 代码心得:
- 用字符数粗保存AT指令这些字符串时,需要注意字符串当中有双引号时则需要用转义字符保留原来的含义;还需要注意AT指令需要新行,也就是字符串末尾加上\r\n
- 对于比较长AT指令的字符串,它们太占空间,程序会编译失败,所以需要在定义的字符数组前面加上“code”,这是个宏定义,目的是将程序存放在比较占空间的字符串的存储位置(关键字code放在以后介绍)。
- 白盒测试前需要验证:只让51单片机与PC进行串口通讯(先不着急把上图连线接好),在串口助手中,验证程序中写的AT指令是OK的,也就是说,检查我们写的字符串中,逗号、双引号是否有错,是否漏写\r\n。
- 测试代码:
#include "reg52.h" #include "intrins.h" #include <string.h> #define len 12 sfr AUXR = 0x8E; sbit ledD5 = P3^7; char cmd[len]; char esp01s_modeSetting[] = "AT+CWMODE=3\r\n"; //配置wifi模块的工作模式 code char esp01s_connectLAN[] = "AT+CWJAP=\"CMCC-XyVF\",\"XyVFVsrz\"\r\n"; //连接上局域网络 code char esp01s_connectTCPServer[] = "AT+CIPSTART=\"TCP\",\"192.168.1.7\",8880\r\n"; //连接上TCP服务器 // code char esp01s_connectLAN[] = "AT+CWJAP=\"HUAWEI P20\",\"abcdefgh\"\r\n"; //连接上局域网络 // code char esp01s_connectTCPServer[] = "AT+CIPSTART=\"TCP\",\"192.168.199.201\",8880\r\n";//连接上TCP服务器 char esp01s_serialNet_mode[] = "AT+CIPMODE=1\r\n"; //设置透传模式 char esp01s_dataSend_waiting[] = "AT+CIPSEND\r\n"; //进入数据待发状态,正式进入透传模式 /* API1. 测试,用于每隔5秒给串口缓冲寄存器发送代表AT指令的字符串 */ void Delay5000ms(); /* API2. 初始化串口 */ void UartInit(void); /* API3. 通过串口给PC发送一个字符 */ void sendByte(char data_msg); /* API4. 通过串口给PC发送一个字符串 */ void sendString(char *str); void main(void) { UartInit(); ledD5 = 1; while(1){ Delay5000ms(); sendString(esp01s_modeSetting); Delay5000ms(); sendString(esp01s_connectLAN); Delay5000ms(); sendString(esp01s_connectTCPServer); Delay5000ms(); sendString(esp01s_serialNet_mode); Delay5000ms(); sendString(esp01s_dataSend_waiting); } } void Uart_Routine() interrupt 4 { static int i = 0; //静态全局区的变量,数组下标 /* 中断处理程序中,对于接收中断的响应 */ if(RI == 1){ RI = 0; cmd[i] = SBUF; i++; if(i == len){ i = 0; } if(strstr(cmd,"en")!=NULL){ ledD5 = 0; i = 0; memset(cmd,'\0',len); }else if(strstr(cmd,"se")!=NULL){ ledD5 = 1; i = 0; memset(cmd,'\0',len); } } /* 中断处理程序中,对于发送中断的响应 */ if(TI == 1){ // 暂时不做任何事情 } } void Delay5000ms() //@11.0592MHz { unsigned char i, j, k; _nop_(); i = 36; j = 5; k = 211; do { do { while (--k); } while (--j); } while (--i); } void UartInit(void) //9600bps@11.0592MHz { AUXR = 0x01; SCON = 0x50; //8位UART,允许串口接收 TMOD &= 0xDF; TMOD |= 0x20; //定时器8位重载工作模式 TH1 = 0xFD; TL1 = 0xFD; //9600波特率初值 TR1 = 1; EA = 1; ES = 1; //开启串口中断 } void sendByte(char data_msg) { SBUF = data_msg; // Delay10ms(); while(TI == 0); TI = 0; } void sendString(char *str) { char *p = str; while(*p != '\0'){ sendByte(*p); p++; } }
- 测试内容:
- 观察串口调试助手上wifi模块对AT指令的反应结果,以及从网络调试助手上透传来的数据:
- 拔掉wifi模块插在CH340模块RX上的TX引线,将这个TX引脚插到51单片机上(不重启51单片机),试一下能否通过wifi透传点亮LED。
- 我测试了一下是可以的。
- 这里留了个伏笔,为什么说不要重启51单片机呢?因为刚才的操作已经保证我们的wifi模块进入透传模式了,你把引脚换一下就能成功。你重新上电之后,网络调试助手有时候能看见透传数据,但更多时候看不到任何透传数据,也就是连接TCP服务器失败了,你没法通过wifi透传对LED进行测试。
3、连接服务器失败的原因:关于指令响应时间的讨论
- 问题剖析:在上述白盒测试中,我们重复给单片机上电,wifi模块大概率会出现连接服务器失败的情况(表现为wifi模块能够联网,但是网络调试助手上没有数据),从而后面的AT指令也自然变成了无效操作。
- 猜测:wifi模块以设备模式联入路由器的过程可能有点久(因为会有一个自动匹配联网—>断网—>再联网的过程),最终造成了还没有联上网之前,单片机就又发送下一条AT指令,导致联入TCP服务器的指令失效。
- 测试:我把wifi模块回到本节1.4的接线方法 ,经过串口调试助手测试,在AT指令还在执行期间,如果继续向wifi模块发送数据(不论是普通数据还是AT指令),wifi模块都会响应busy p...,也就是说这些数据被吞掉了、无视掉了,最后wifi模块没有连上TCP服务器。
- 总结:只留5s时间给wifi模块联网有点稍短,导致连接TCP服务器的AT指令发送后被丢失了,同时每次重新给51单片机上电后,连接wifi的时间是不一样,不能简单用5s时间笼统概括。另外上述白盒测试代码有点残疾,一上电之后只能停留在网络调试助手页面干巴巴地等,没有指示灯告诉我成没成功,到底连不连得上心里也没底,很被动。
4、单片机处理wifi模块响应信息:
- 目的:针对上述连接TCP服务器失败的问题,我们需要考虑给5s时间联网会不会太短,考虑给5s时间联入服务器、设置工作模式、进入透传模式会不会太长。等于说我们需要优化AT指令执行的效率和准确度。
- 总体方法:
- ①跟上节课串口中断点亮LED一样,wifi模块对AT指令的响应信息也是在串口中断程序中完成读取的,此外wifi模块对于从TCP服务器透传来的数据也是在串口中断程序中完成捕获的,这两路数据共用一个接收数据缓冲寄存器SBUF。因此,我们让单片机针对上述各类信息进行关键字眼的检索,一旦检索到关键字眼说明单片机发送或者接收到了一条有效指令(或是失败指令)。
- ②需要定义wifi模块响应AT指令成功的标志位,这些标志位是全局变量,在串口中断程序中读取到了关键字眼,就对这些全局变量进行修改。标志位最终能帮助单片机判断是否发送下一条AT指令,方法是在main函数中用while空循环体进行等待,直到标志位改变为1,才继续往下执行代码,发送下一条AT指令。
- ③肯定存在联网失败的情况(在入网指令书写正确的情况下这是小概率事件)。如果联网失败,我们总不能让程序一直等待吧?答:在串口中断程序中,一旦检测到联网失败的响应信息,就发送一条联网的AT指令(或者重启wifi模块的AT指令),注:可以发送重启wifi模块的AT指令的原因是wifi模块重新上电后会自动匹配上次的wifi,wifi模块又会自动响应联网的信息。
- 确定关键字眼与标志位:在串口中断程序中不方便随手处理的有效指令需要标志位,方便随手处理的有效指令就不需要标志位。当我们读到能标志响应成功的信息时(“WIFI GOT IP”、“OK”),我们就令对应的响应成功标志位为1;当我们读到联网失败的响应信息时(“FAIL”),我们就随手重新发送一条联网的AT指令。
- AT指令响应成功的关键字眼:通过CH340直接测试,观察各种AT指令下wifi模块的响应信息形式(之前已经测试过了)。针对《4.1小节单片机需要做哪些事情》中的那5个AT指令,确定了以下关键字眼和标志位。
配置wifi模块的工作模式 连接上局域网络 连接上TCP服务器 设置透传模式 进入数据待发状态 成功:“OK” 成功:“WIFI GOT IP” & “OK”
注:一上电,自动匹配上wifi时不会响应“OK”
失败:“FAIL”成功:“OK” 成功:“OK” 成功:“OK” 成功标志位: AT_OK_Flag 成功标志位:AT_ConnectLAN_Flag 成功标志位:AT_OK_Flag 成功标志位:AT_OK_Flag 成功标志位:AT_OK_Flag - 透传有效指令的关键字眼:我用首字母'L'代表LED,我观察到在所有的AT指令的响应信息中,字符'L'出现的次数比较少,因此本demo中,我们规定指令"L-1"亮灯,指令"L-0"灭灯。即关键字眼是
灯控指令 亮灯:“L-1” 灭灯:“L-0”
- AT指令响应成功的关键字眼:通过CH340直接测试,观察各种AT指令下wifi模块的响应信息形式(之前已经测试过了)。针对《4.1小节单片机需要做哪些事情》中的那5个AT指令,确定了以下关键字眼和标志位。
- 临时字符变量的引入:对关键字眼读取的前提是让数据缓冲寄存器SBUF读取到的字符串存放在我们定义的字符数组中,我们不希望用上节课那样查找子串的方式来捕捉这些关键字眼,因为有效指令的存放可能因为字符数组的长度限制而只被部分捕获。但是存储的顺序不一定从第0位开始存放(以关键字眼“WIFI GOT IP”为例),如下图,怎么办呢?
- 答:方法是定义一个临时字符变量(temp)。用于检测是否读到字符'W',当检测到字符'W'后就让字符串从头开始存放,这样就能保证有效指令字符串全部保存在字符数组中,同时方便了我们编程。
- 通过硬件来窥探wifi模块配置情况:
- 让wifi模块在成功联入局域网后,D5亮(main函数中对标志位 AT_ConnectLAN_Flag 判断);
- 让wifi模块成功联入服务器后,D6亮(main函数中对标志位 AT_OK_Flag 判断);
- 如果入网失败我们闪烁D5(串口中断程序捕获到关键字眼“FAIL”后随手处理,但实际上中断程序中最好不要有任何耗时操作)。
习题1(优化wifi透传控制LED):【项目工程文件夹】
- 代码心得:
- 本代码的串口中断程序中多出来了一个临时字符变量temp,对temp的判断语句需要放在读取SBUF内容之前,同时要注意串口缓冲寄存器SBUF中的内容被temp读取之后,内容就不存在SBUF中了,所以得用temp来给我们的字符数组赋值。
- 字符数组下标i的偏移语句必须写在判断字符数组是否存满的判断语句前面,不能写在后面,因为我们要保证每次存满之后都从下标0位置开始存放,如果写在后面那么每次都会从下标1位置开始存放。从而可以发现我们预设的字符数组长度为12,实际上只能存放11个字符。
- 思路:
宏定义: 1. 定义符号常量len,用它代表用于接收SBUF中缓冲字符串的全局数组的长度: #define len 12 全局变量: 1. sfr指令直接找到AUXR寄存器: sfr AUXR = 0X8E; //因为AUXR没有在reg52.h中声明 2. sbit指令找到P3这个I/O口组的第7位P3^7,也就是D5这个LED: sbit ledD5 = P3^7; 3. sbit指令找到P3这个I/O口组的第6位P3^6,也就是D6这个LED: sbit ledD6 = P3^6; 4. “设置wifi模块工作模式的AT指令”: char esp01s_modeSetting[] = "AT+CWMODE=3\r\n"; 5. “设置wifi模块连接网络的AT指令”: code char esp01s_connectLAN[] = "AT+CWJAP=\"HUAWEI P20\",\"abcdefgh\"\r\n"; 6. “设置wifi模块连接TCP服务器的AT指令”: code char esp01s_connectTCPServer[] = "AT+CIPSTART=\"TCP\",\"192.168.45.201\",8880\r\n"; //5和6都要在前面加个关键词code,因为太长了 7. “设置wifi模块透传模式的AT指令”: char esp01s_serialNet_mode[] = "AT+CIPMODE=1\r\n"; 8. “设置wifi模块数据传输的AT指令”: char esp01s_dataSend_waiting[] = "AT+CIPSEND\r\n"; 9. 定义wifi模块响应AT指令"OK"的标志位: char AT_OK_Flag = 0; //AT_OK_Flag的传递路线为:SBUF ——> 串口中断(中断4)——> API2: wifiModule_Config(); 10. 定义wifi模块响应AT指令"WIFI GOT IP"的标志位: char AT_ConnectLAN_Flag = 0; //AT_ConnectLAN_Flag的传递路线为:SBUF ——> 串口中断(中断4)——> API2: wifiModule_Config(); 11. 定义一个用于接收串口中缓冲区字符串的全局数组serial_buffer: char serial_buffer[len]; //serial_buffer的传递路线为:SBUF ——> 串口中断(中断4)
1. 一上电先让指示灯D5和D6灭: ledD5 = ledD6 = 1; 2. 调用API1. 初始化串口: UartInit(); 3. 调用API5. 软件延时1s,给wifi模块上电后ready的准备预留时间: Delay1000ms(); 4. 调用API2. 配置wifi模块,进入透传模式: wifiModule_Config(); 5. while死循环,每隔一秒通过串口给PC发送一个字符串,当做心跳包 5.1 调用API5,软件延时1s: Delay1000ms(); 5.2 调用API4,给输出数据缓冲寄存器SBUF发送一条字符串: sendString("hello shuaige\r\n");
中断: 中断4: 封装串口中断的中断服务程序, void Uart_Routine() interrupt 4 4.1 定义一个静态全局区的静态变量,用来表示数组serial_buffer的下标: static int i = 0; 4.2 定义一个临时字符变量temp,用于检测关键字眼,保证我们的字符串是从字符数粗的第0位开始存放的。 char temp; 4.3 中断处理程序中,对于接收中断的响应,判据是RI == 1 4.3.1 在接受到1字节数据后,程序复位RI: RI = 0; 4.3.2 串口缓冲区接收到的字符先存放在临时变量temp中: temp = SBUF; 4.3.3 从数据缓冲寄存器SBUF中读到字符后,根据我们提前设计好的关键字眼,关心四件事: "WIFI GOT IP"的'W'、 "OK"的'O'、 "L-1"的'L',"FAIL"的'F', 判据是temp=='W' || temp=='O' || temp=='L' || temp=='F' 4.3.3.1 如果是,那么需要从头开始存放: i = 0; 4.3.3.2 否则,那么什么也不做,继续往下执行 4.3.4 将temp的值保存在数组serial_buffer的第i个元素中: serial_buffer[i] = temp; 4.3.5 偏移数组下标: i++; 4.3.6 判断字符数组serial_buffer是否存满了,判据是 i == len //内在逻辑:由于serial_buffer长度的限制,当字符串超过len时,我们需要覆盖掉原先的字符 4.3.6.1 如果是,那么需要从头开始存放: i = 0; 4.3.6.2 否则,那么什么也不做,继续往下执行 4.3.7 通过字符数组的第0位和第5位捕捉关键字眼,判断wifi模块是否响应"WIFI GOT IP",判据是 serial_buffer[0]=='W' && serial_buffer[5]=='G' 4.3.7.1 如果是, 令标志位为1: AT_ConnectLAN_Flag = 1; 有效指令后清空字符数组: memset(serial_buffer,'\0',len); 4.3.7.2 否则,那么什么也不做,继续往下执行 4.3.8 通过字符数组的第0位和第1位捕捉关键字眼,判断wifi模块是否响应"OK",判据是 serial_buffer[0]=='O' && serial_buffer[1]=='K' 4.3.8.1 如果是, 令标志位为1: AT_OK_Flag = 1; 有效指令后清空字符数组: memset(serial_buffer,'\0',len); 4.3.8.2 否则,那么什么也不做,继续往下执行 4.3.9 通过字符数组的第0位和第1位捕捉关键字眼,判断wifi模块是否响应"FAIL",判据是 serial_buffer[0]=='F' && serial_buffer[1]=='L' 4.3.9.1 如果是, 用i当做循环变量,使用for循环语句闪烁D5五次,亮一秒,灭一秒 再次调用API4,通过串口发送对应入网的AT指令: sendString(esp01s_connectLAN); 有效指令后清空字符数组: memset(serial_buffer,'\0',len); 4.3.9.2 否则,那么什么也不做,继续往下执行 4.3.10 通过字符数组的第0位和第2位捕捉关键字眼,判断wifi模块是否收到透传数据"L-1",判据是 serial_buffer[0]=='L' && serial_buffer[2]=='1' 4.3.10.1 如果是, 点亮D5: ledD5 = 0; 有效指令后清空字符数组: memset(serial_buffer,'\0',len); 4.3.10.2 否则,如果wifi模块收到透传数据"L-0" 熄灭D5: ledD5 = 1; 有效指令后清空字符数组: memset(serial_buffer,'\0',len); 4.4 中断处理程序中,对于发送中断的响应,判据是TI == 1 暂时不做任何事情
/* 一级函数:f1、f2、f4、f5 */ f1. 封装初始化串口的API: void UartInit(void); f1.1 禁用ALE信号: AUXR = 0X01; f1.2 让串口以方式1工作(8位UART,可变波特率),并允许串口接收: SCON = 0x50; f1.3 让定时器1以8位重载工作模式工作: TMOD &= 0xDF; TMOD |= 0x20; f1.4 根据波特率为9600,波特率不翻倍,设置定时器1的初值: TH1 = 0xFD; TL1 = 0xFD; f1.5 定时器开始数数: TR1 = 1; f1.6 开启串口中断: EA = 1; ES = 1; f2. 封装配置wifi模块以客户端模式工作的API: void wifiModule_Config(); f2.1 配置wifi模块工作模式为双模式(设备模式+路由模式): 调用API4,通过串口发送对应AT指令: sendString(esp01s_modeSetting); 空循环体等待,直到wifi模块响应"OK": while(!AT_OK_Flag); 为了不影响下一个条指令响应,复位标志位: AT_OK_Flag = 0; f2.2 配置wifi模块,进行入网设置: 调用API4,通过串口发送对应AT指令: sendString(esp01s_connectLAN); 空循环体等待,直到wifi模块响应"WIFI GOT IP": while(!AT_ConnectLAN_Flag); 为了不影响下一个条指令响应,复位标志位: AT_ConnectLAN_Flag = 0; 空循环体等待,直到wifi模块响应"OK": while(!AT_OK_Flag); 为了不影响下一个条指令响应,复位标志位: AT_OK_Flag = 0; f2.3 配置wifi模块,连接TCP服务器: 调用API4,通过串口发送对应AT指令: sendString(esp01s_connectTCPServer); 空循环体等待,直到wifi模块响应"OK": while(!AT_OK_Flag); 为了不影响下一个条指令响应,复位标志位: AT_OK_Flag = 0; f2.4 配置wifi模块,设置成透传模式: 调用API4,通过串口发送对应AT指令: sendString(esp01s_serialNet_mode); 空循环体等待,直到wifi模块响应"OK": while(!AT_OK_Flag); 为了不影响下一个条指令响应,复位标志位: AT_OK_Flag = 0; f2.5 配置wifi模块,进入数据待发状态,正式进入透传模式: 调用API4,通过串口发送对应AT指令: sendString(esp01s_dataSend_waiting); 空循环体等待,直到wifi模块响应"OK": while(!AT_OK_Flag); //不复位,留给后面判断 f2.6 测试代码,经COM3这个端口发给PC上的串口助手看: sendString("wifi Module setting success!\r\n"); f2.7 测试代码,如果AT_OK_Flag==1,就点亮D6,代表成功: ledD6 = 0; f4. 封装给PC发送字符串的API: void sendString(char *str); //形参是字符串的地址 f4.1 定义一个字符指针变量p用来保存字符串首地址: char *p = str; f4.2 while循环,控制循环的变量是*p,当*p != '\0' 时,进入循环,进行单个字符的发送 f4.2.1 通过指针间接访问字符串字符,再调用API3. 发送单个字符: sendByte(*p); f4.2.2 修改循环变量p的值,让指针p偏移: p++; f5. 封装软件延时1s的API,用于每隔1秒发送心跳包,以及串口初始化后的短暂休眠: void Delay1000ms();
/* 二级函数:f3 */ f3. 封装定时给PC发送一个字符的API: void sendByte(char data_msg); //形参是字符值 f3.1 往SBUF寄存器中写入字符data_msg: SBUF = data_msg; f3.2 根据串口发送中断触发位TI,利用空循环体暂停程序: while(TI == 0); f3.3 程序复位TI: TI = 0;
- 代码:
#include "reg52.h" #include "intrins.h" #include <string.h> #define len 12 sfr AUXR = 0x8E; sbit ledD5 = P3^7; sbit ledD6 = P3^6; char esp01s_modeSetting[] = "AT+CWMODE=3\r\n"; //配置wifi模块的工作模式 // code char esp01s_connectLAN[] = "AT+CWJAP=\"CMCC-XyVF\",\"XyVFVsrz\"\r\n"; //连接上局域网络 // code char esp01s_connectTCPServer[] = "AT+CIPSTART=\"TCP\",\"192.168.1.7\",8880\r\n"; //连接上TCP服务器 code char esp01s_connectLAN[] = "AT+CWJAP=\"HUAWEI P20\",\"abcdefgh\"\r\n"; //连接上局域网络 code char esp01s_connectTCPServer[] = "AT+CIPSTART=\"TCP\",\"192.168.45.201\",8880\r\n";//连接上TCP服务器 char esp01s_serialNet_mode[] = "AT+CIPMODE=1\r\n"; //设置透传模式 char esp01s_dataSend_waiting[] = "AT+CIPSEND\r\n"; //进入数据待发状态,正式进入透传模式 // char esp01s_reset[] = "AT+RST\r\n"; //wifi连接失败重启wifi模块 char AT_OK_Flag = 0; //关键字眼OK的标志位,用1代表AT指令响应成功,0代表AT指令响应失败 char AT_ConnectLAN_Flag = 0; //关键字眼WIFI GOT IP的标志位,用1代表AT指令响应成功,0代表AT指令响应失败 char serial_buffer[len]; /* API1. 初始化串口 */ void UartInit(void); /* API2. 配置wifi模块以客户端模式工作,进入透传模式 */ void wifiModule_Client_Config(); /* API3. 通过串口给PC发送一个字符 */ void sendByte(char data_msg); /* API4. 通过串口给PC发送一个字符串 */ void sendString(char *str); /* API5. 用于每隔1秒给wifi模块发送心跳包,以及串口初始化后的短暂休眠 */ void Delay1000ms(); void main(void) { ledD5 = ledD6 = 1; UartInit(); Delay1000ms(); //给wifi模块上电后ready的准备时间 wifiModule_Client_Config(); while(1){ Delay1000ms(); sendString("hello shuaige\r\n"); //心跳包 } } void Uart_Routine() interrupt 4 { static int i = 0; //静态全局区的变量,数组下标 char temp; /* 中断处理程序中,对于接收中断的响应 */ if(RI == 1){ RI = 0;//清除接收中断标志位 temp = SBUF; if(temp=='W' || temp=='O' || temp=='L' || temp=='F'){ //从数据缓冲寄存器SBUF中读到字符后,关心四件事 i = 0; } serial_buffer[i] = temp; i++; if(i == len){ i = 0; } //wifi模块响应值的判断 if(serial_buffer[0]=='W' && serial_buffer[5]=='G'){ AT_ConnectLAN_Flag = 1; memset(serial_buffer, '\0', len); } if(serial_buffer[0]=='O' && serial_buffer[1]=='K'){ AT_OK_Flag = 1; memset(serial_buffer, '\0', len); } if(serial_buffer[0]=='F' && serial_buffer[1]=='A'){ for(i=0; i<5; i++){ //测试:入网失败后闪灯,并发送联网指令 ledD5 = 0; Delay1000ms(); ledD5 = 1; Delay1000ms(); } // sendString(esp01s_reset); sendString(esp01s_connectLAN); memset(serial_buffer, '\0', len); } //灯控指令 if(serial_buffer[0]=='L' && serial_buffer[2]=='1'){ ledD5 = 0; memset(serial_buffer, '\0', len); }else if(serial_buffer[0]=='L' && serial_buffer[2]=='0'){ ledD5 = 1; memset(serial_buffer, '\0', len); } } /* 中断处理程序中,对于发送中断的响应 */ if(TI == 1){ // 暂时不做任何事情 } } void UartInit(void) //9600bps@11.0592MHz { AUXR = 0x01; SCON = 0x50; //8位UART,允许串口接收 TMOD &= 0xDF; TMOD |= 0x20; //定时器8位重载工作模式 TH1 = 0xFD; TL1 = 0xFD; //9600波特率初值 TR1 = 1; EA = 1; ES = 1; //开启串口中断 } void wifiModule_Client_Config() { /* 经过白盒测试以及优化后,以下AT指令只需要发送一遍所以不需要反复配置,暂停程序直到AT指令响应成功 */ sendString(esp01s_modeSetting); while(!AT_OK_Flag); //等待,直到wifi模块响应OK AT_OK_Flag = 0; sendString(esp01s_connectLAN); while(!AT_ConnectLAN_Flag); //等待,直到wifi模块响应WIFI GOT IP ledD5 = 0; //测试,点亮D5,代表入网成功 while(!AT_OK_Flag); //等待,直到wifi模块响应OK AT_OK_Flag = 0; sendString(esp01s_connectTCPServer); while(!AT_OK_Flag); //等待,直到wifi模块响应OK AT_OK_Flag = 0; sendString(esp01s_serialNet_mode); while(!AT_OK_Flag); //等待,直到wifi模块响应OK AT_OK_Flag = 0; sendString(esp01s_dataSend_waiting); while(!AT_OK_Flag); //等待,直到wifi模块响应OK sendString("wifi Module setting success!\r\n"); //测试,经COM3这个端口发给PC上的串口助手看 if(AT_OK_Flag == 1){ ledD6 = 0; //测试,点亮D6,代表连接服务器并打开透传模式 } } void sendByte(char data_msg) { SBUF = data_msg; while(TI == 0); TI = 0; } void sendString(char *str) { char *p = str; while(*p != '\0'){ sendByte(*p); p++; } } void Delay1000ms() //@11.0592MHz { unsigned char i, j, k; _nop_(); i = 8; j = 1; k = 243; do { do { while (--k); } while (--j); } while (--i); }
5、白盒调试跟踪代码:优化代码写完之后,再按照白盒测试的套路进行调试,跟踪代码执行过程。
我们这个代码烧进去之后,由于51单片机的RX引脚没有接在wifi模块的TX引脚上,所以51单片机从接收缓冲寄存器SBUF中接收不到任何数据。所以白盒测试中的串口助手(COM6端口:CH340和wifi模块的串口)一定会卡着。所以需要我们在另一个串口助手(COM3端口:51单片机和PC的串口)中手动模拟wifi模块本来应该响应给51单片机的数据。最后测试成功后,就可以认为代码是可靠的了,可以烧录进单片机中。
注:白盒测试下不能够用透传来测试wifi控制LED,也得用串口手动模拟。
5.ESP-01S当做服务器
1、数据传输路线:前面4小节我们都是让wifi以客户端模式进行工作。我们接下来在PC上用网络调试助手构建一个基于TCP协议的客户端。也是给wifi模块发送AT指令,让ESP8266在路由模式下以TCP服务器模式工作。然后让PC上的TCP客户端联入wifi模块构建的局域网。这个时候ESP8266和外部的通讯方式也分为两类。
- 走串口:wifi模块和PC走串口协议,PC通过USB转TTL的CH340驱动,将AT指令传送给wifi模块,然后wifi模块响应一些配置信息给PC。
- 走wifi:wifi模块和PC也可以走网络传输层中的TCP协议,这时候PC和wifi模块是连接在同一个局域网下的,wifi模块作为服务器,PC作为客户端。数据可以从TCP客户端传递到wifi模块。数据也可以从wifi模块透传到TCP客户端。
2、串口测试时的AT指令及响应(测试截图略,总的响应信息见5.3中的图):
- wifi模块工作模式设置:AT+CWMODE=3(也可以写成AT+CWMODE=2)
wifi模块联网:AT+CWJAP="HUAWEI P20","abcdefgh"(这种模式下是路由模式不需要联网)- 查阅wifi模块以AP模式工作时的网关:AT+CIFSR
- 目的是用于确认服务器的IP地址(就是wifi模块的网关,有的网络调试助手上会写成远程主机地址,一个意思),将它写在网络调试助手的对应位置。
- 也需要在命令提示符中,明确当前PC的IPV4地址,输入网络调试助手中。
- 配置wifi模块成使能多链接:AT+CIPMUX=1
- 配置成功,wifi模块会响应“OK”
- 配置失败,wifi模块会响应“ERROR”
- 如果再次发送该AT指令,wifi模块会响应“OK”
- 如果在wifi模块已经与TCP客户端建立连接的前提下再次发送该AT指令,wifi模块会响应“link is builded”、“ERROR”
- 让wifi模块建立TCP服务器(端口号默认是333):AT+CIPSERVER=1
- 建立成功,wifi模块会响应“OK”
- 建立失败,wifi模块会响应“ERROR”
- 建立成功后,重复发送上述AT指令,wifi模块会响应“no change”、“OK”
- 在PC上的网络调试助手上配置好之后,网络调试助手上点击连接后,如果连接成功,wifi模块会响应“0,CONNECT”(逗号后无空格,这里0代表连接的通道,一个IP地址对应一个通道,第1个联入的IP地址的通道是0);
- 网络调试助手上点击连接后,如果连不上,可能的原因是你的PC还没有联入wifi模块构建的局域网(我的8266构建的局域网的名字是ESP_28DCAB),或者网络调试助手出问题了,没有成功建立TCP客户端(换一个)。
- TCP客户端和wifi模块成功建立连接后,如果在网络调试助手上点击断开后,也就是TCP客户端断开连接时,wifi模块会响应“0,CLOSED”(逗号后无空格)。
- 让wifi模块进入数据待发状态:AT+CIPSEND=0,n (0是通道号、n是待发数据字节数,逗号后无空格)
- 当你数据手动发送成功后,wifi模块会响应“Recv n bytes”、“SEND OK”
- 如果在没有与TCP客户端建立连接时就立马发送上述AT指令,wifi模块会响应“link is not valid”、“ERROR”
- 让wifi模块主动断开TCP连接(用的很少):AT+CIPCLOSE=0 (0是通道号)
- 在TCP客户端和wifi模块成功建立连接的前提下,发送上述指令并断开后,wifi模块会响应“0,CLOSED”、“OK” (0是通道号,逗号后无空格)。同时网络调试助手上会显示未连接的状态。
- 重复发送断开的AT指令,也就是TCP服务器和客户端已经断开的状态下继续发送该AT指令,wifi模块会响应“UNLINK”、“ERROR”。
- 注意上述指令只是让wifi模块断开TCP连接,并不是取消构建TCP服务器,下次连接时,仍然还是由TCP客户端发起的。
3、与客户端的数据交互:
- 接收客户端发来的数据:让wifi模块配置到上述第5步(建立TCP服务器)就能接收TCP客户端发送来的数据了。
- 但实际上这还不是透传模式。
- 接收的数据具有固定的格式:“+IPD,0,n:xxx”(逗号,冒号后均无空格),其中+IPD是固定字符串,0是连接的通道,n是数据的字节数,xxx才是从客户端透传来的数据
- 往客户端透传数据:需要让wifi模块配置到上述第6步(进入数据待发状态),才正式进入透传模式。
- 透传时尽量不要发送新行,因为新行占两个字节。
- 发送完毕后会自动退出透传模式。
- 如果发送的字节数超出了n字节数,wifi模块先会响应“busy s...”,再响应“Recv n bytes”、“SEND OK”,同时只会截取前面的字节发送。
- 如果你发送的数据长度不足n字节,wifi模块不会有任何响应,同时网络调试助手上看不到任何发送来的数据,这个数据就好像一直存在“缓冲区”中,只有你达到或者超过了设定的n个字节数,才会显示。
4、单片机配置服务器模式:这里的步骤和4.1~4.5一致。
- 确认单片机要发送的AT指令:终端设备是单片机(MCU),让单片机通过串口给模块发送一系列AT指令,指挥wifi模块以服务器模式工作。
重启wifi模块- 配置wifi模块的工作模式(如果之前设置过的话,这一步wifi模块会自动完成)
- 设置成使能多链接模式
- 建立TCP服务器
- 进入数据待发状态,正式进入透传模式(发送完毕后会自动退回到AT指令模式)
波特率是提前用电脑通过AT指令给wifi模块配置好的,用9600。
- 单片机处理wifi模块响应信息:
AT指令响应成功的关键字眼及标志位 配置wifi模块为路由模式 使能多链接 建立TCP服务器 与TCP客户端建立连接 进入数据待发状态(5字节、通道0) 成功:“OK” 成功:“OK” 成功:“OK” 成功:“0,CONNECT” 成功:“OK” 成功标志位: AT_OK_Flag 成功标志位: AT_OK_Flag 成功标志位: AT_OK_Flag 成功标志位: Client_Connect_Flag 不检测 透传有效指令的关键字眼 灯控指令 亮灯:“:op”
灭灯:“:cl”- 对于进入数据待发状态的AT指令来说,不要去检测“OK”,因为我们要在main函数的while(1)循环中把这个指令用于给TCP客户端发送心跳包(通过串口也能看见发送的心跳包),如果说以后一旦检测不到OK了(比如说TCP客户端主动与TCP服务器断开连接),那么程序就会卡死在while(1)循环中,那么就连串口上也无法监测到心跳包,会让我们误以为单片机死掉了了。
- 我在用白盒测试跟踪代码运行情况时,灯控指令设置成了“:L-1”和“:L-0”,出现了大问题:①第一个OK需要发送两次才能被接收到,②后面发送0,CONNECT也很难被接收到,③即使与TCP客户端建立起了连接,灯控指令也不能正常工作。经过我不断的调试(心累,其实从问题描述中也能看出),终于发现了问题所在:灯控指令不能设置成“:L-1”和“:L-0”。所以我选用了“:op”与“:cl”。
- 通过硬件来窥探wifi模块配置情况:
- 让wifi模块在成功建立TCP服务器后,D5亮(main函数中对标志位 AT_OK_Flag 判断);
- 让wifi模块与客户端建立连接后,D6亮(main函数中对标志位 Client_Connect_Flag 判断);
- 白盒测试跟踪代码:代码见习题2。注意白盒测试下不能够测试wifi控制LED,也得用串口手动模拟。
习题2(wifi模块作为服务器):在习题1的代码上做适当修改【项目工程文件夹】
- 思路:
宏定义: 1. 定义符号常量len,用它代表用于接收SBUF中缓冲字符串的全局数组的长度: #define len 12 全局变量: 1. sfr指令直接找到AUXR寄存器: sfr AUXR = 0X8E; //因为AUXR没有在reg52.h中声明 2. sbit指令找到P3这个I/O口组的第7位P3^7,也就是D5这个LED: sbit ledD5 = P3^7; 3. sbit指令找到P3这个I/O口组的第6位P3^6,也就是D6这个LED: sbit ledD6 = P3^6; 4. “设置wifi模块工作模式的AT指令”: char esp01s_modeSetting[] = "AT+CWMODE=2\r\n"; 5. “设置wifi模块使能多链接的AT指令”: char esp01s_multiLink[] = "AT+CIPMUX=1\r\n"; 6. “设置wifi模块建立TCP服务器的AT指令”: char esp01s_setTCPServer[] = "AT+CIPSERVER=1\r\n"; 7. “设置wifi模块数据传输的AT指令”: char esp01s_dataSend_waiting[] = "AT+CIPSEND=0,5\r\n"; 8. 定义wifi模块响应AT指令"OK"的标志位: char AT_OK_Flag = 0; //AT_OK_Flag的传递路线为:SBUF ——> 串口中断(中断4)——> API2: wifiModule_Server_Config(); 9. 定义wifi模块响应AT指令"0,CONNECT"的标志位: char Client_Connect_Flag = 0; //Client_Connect_Flag的传递路线为:SBUF ——> 串口中断(中断4)——> API2: wifiModule_Server_Config(); 10. 定义一个用于接收串口中缓冲区字符串的全局数组serial_buffer: char serial_buffer[len]; //serial_buffer的传递路线为:SBUF ——> 串口中断(中断4)——> 临时字符变量temp
1. 一上电先让指示灯D5和D6灭: ledD5 = ledD6 = 1; 2. 调用API1. 初始化串口: UartInit(); 3. 调用API5. 软件延时1s,给wifi模块上电后ready的准备预留时间: Delay1000ms(); 4. 调用API2. 配置wifi模块以TCP服务器模式工作: wifiModule_Server_Config(); 5. while死循环,每隔4秒以两种方式(串口和透传)给PC发送一个字符串,当做心跳包 5.1 调用API4,通过串口发送数据传输的AT指令: sendString(esp01s_0Channel_dataSend); 5.2 调用API5,软件延时2s: Delay1000ms(); Delay1000ms(); 5.3 调用API4,给输出数据缓冲寄存器SBUF发送一条字符串: sendString("hello"); 5.2 调用API5,软件延时2s: Delay1000ms(); Delay1000ms();
中断: 中断4: 封装串口中断的中断服务程序, void Uart_Routine() interrupt 4 4.1 定义一个静态全局区的静态变量,用来表示数组serial_buffer的下标: static int i = 0; 4.2 定义一个临时字符变量temp,用于检测关键字眼,保证我们的字符串是从字符数粗的第0位开始存放的。 char temp; 4.3 中断处理程序中,对于接收中断的响应,判据是RI == 1 4.3.1 在接受到1字节数据后,程序复位RI: RI = 0; 4.3.2 串口缓冲区接收到的字符先存放在临时变量temp中: temp = SBUF; 4.3.3 从数据缓冲寄存器SBUF中读到字符后,根据我们提前设计好的关键字眼,关心四件事: "OK"的'O'、 "0,CONNECT"的'0'、 "+IPD,0,n:op"的':', 判据是temp=='O' || temp=='0' || temp==':' 4.3.3.1 如果是,那么需要从头开始存放: i = 0; 4.3.3.2 否则,那么什么也不做,继续往下执行 4.3.4 将temp的值保存在数组serial_buffer的第i个元素中: serial_buffer[i] = temp; 4.3.5 偏移数组下标: i++; 4.3.6 判断字符数组serial_buffer是否存满了,判据是 i == len //内在逻辑:由于serial_buffer长度的限制,当字符串超过len时,我们需要覆盖掉原先的字符 4.3.6.1 如果是,那么需要从头开始存放: i = 0; 4.3.6.2 否则,那么什么也不做,继续往下执行 4.3.7 通过字符数组的第0位和第1位捕捉关键字眼,判断wifi模块是否响应"OK",判据是 serial_buffer[0]=='O' && serial_buffer[1]=='K' 4.3.7.1 如果是, 令标志位为1: AT_OK_Flag = 1; 有效指令后清空字符数组: memset(serial_buffer,'\0',len); 4.3.7.2 否则,那么什么也不做,继续往下执行 4.3.8 通过字符数组的第0位和第2位捕捉关键字眼,判断wifi模块是否响应"0,CONNECT",判据是 serial_buffer[0]=='0' && serial_buffer[2]=='C' 4.3.8.1 如果是, 令标志位为1: Client_Connect_Flag = 1; 有效指令后清空字符数组: memset(serial_buffer,'\0',len); 4.3.8.2 否则,那么什么也不做,继续往下执行 4.3.9 通过字符数组的第0位和第2位捕捉关键字眼,判断wifi模块是否收到透传数据"+IPD,0,n:open",判据是 serial_buffer[0]==':' && serial_buffer[1]=='o' && serial_buffer[2]=='p' 4.3.9.1 如果是, 点亮D5: ledD5 = 0; 有效指令后清空字符数组: memset(serial_buffer,'\0',len); 4.3.9.2 否则,如果wifi模块收到透传数据"+IPD,0,n:close" 熄灭D5: ledD5 = 1; 有效指令后清空字符数组: memset(serial_buffer,'\0',len); 4.4 中断处理程序中,对于发送中断的响应,判据是TI == 1 暂时不做任何事情
/* 一级函数:f1、f2、f4、f5 */ f1. 封装初始化串口的API: void UartInit(void); f1.1 禁用ALE信号: AUXR = 0X01; f1.2 让串口以方式1工作(8位UART,可变波特率),并让REN使能允许串口接收: SCON = 0x50; f1.3 让定时器1以8位重载工作模式工作: TMOD &= 0xDF; TMOD |= 0x20; f1.4 根据波特率为9600,波特率不翻倍,设置定时器1的初值: TH1 = 0xFD; TL1 = 0xFD; f1.5 定时器开始数数: TR1 = 1; f1.6 开启串口中断: EA = 1; ES = 1; f2. 封装配置wifi模块以客户端模式工作的API: void wifiModule_Server_Config(); f2.1 配置wifi模块工作模式为路由模式: 调用API4,通过串口发送对应AT指令: sendString(esp01s_modeSetting); 空循环体等待,直到wifi模块响应"OK": while(!AT_OK_Flag); 为了不影响下一个条指令响应,复位标志位: AT_OK_Flag = 0; f2.2 配置wifi模块,使能多链接模式: 调用API4,通过串口发送对应AT指令: sendString(esp01s_multiLink); 空循环体等待,直到wifi模块响应"OK": while(!AT_OK_Flag); 为了不影响下一个条指令响应,复位标志位: AT_OK_Flag = 0; f2.3 配置wifi模块,建立TCP服务器: 调用API4,通过串口发送对应AT指令: sendString(esp01s_setTCPServer); 空循环体等待,直到wifi模块响应"OK": while(!AT_OK_Flag); 为了不影响下一个条指令响应,复位标志位: AT_OK_Flag = 0; 测试代码:点亮D5,代表成功建立TCP服务器: ledD5 = 0; f2.4 空循环体等待,直到wifi模块响应"0,CONNECT": while(!Client_Connect_Flag); //不复位,留给后面判断 测试代码,如果Client_Connect_Flag==1,就点亮D6,代表成功与客户端建立链接: ledD6 = 0; f4. 封装给PC发送字符串的API: void sendString(char *str); //形参是字符串的地址 f4.1 定义一个字符指针变量p用来保存字符串首地址: char *p = str; f4.2 while循环,控制循环的变量是*p,当*p != '\0' 时,进入循环,进行单个字符的发送 f4.2.1 通过指针间接访问字符串字符,再调用API3. 发送单个字符: sendByte(*p); f4.2.2 修改循环变量p的值,让指针p偏移: p++; f5. 封装软件延时1s的API,用于每隔1秒发送心跳包,以及串口初始化后的短暂休眠: void Delay1000ms();
/* 二级函数:f3 */ f3. 封装定时给PC发送一个字符的API: void sendByte(char data_msg); //形参是字符值 f3.1 往SBUF寄存器中写入字符data_msg: SBUF = data_msg; f3.2 根据串口发送中断触发位TI,利用空循环体暂停程序: while(!TI); f3.3 程序复位TI: TI = 0;
- 代码:
#include "reg52.h" #include "intrins.h" #include <string.h> #define len 12 sfr AUXR = 0x8E; sbit ledD5 = P3^7; sbit ledD6 = P3^6; char serial_buffer[len]; char esp01s_modeSetting[] = "AT+CWMODE=2\r\n"; //配置wifi模块的工作模式 char esp01s_multiLink[] = "AT+CIPMUX=1\r\n"; //使能多链接 char esp01s_setTCPServer[] = "AT+CIPSERVER=1\r\n"; //建立TCP服务器 char esp01s_0Channel_dataSend[] = "AT+CIPSEND=0,5\r\n"; //发送5个字节数据在连接0通道上 char AT_OK_Flag = 0; //关键字眼OK的标志位,用1代表AT指令响应成功,0代表AT指令响应失败 char Client_Connect_Flag = 0; //关键字眼0,CONNECT的标志位,用1代表客户端接入成功,0代表客户端接入失败 /* API1. 初始化串口 */ void UartInit(void); /* API2. 配置wifi模块以服务器模式工作 */ void wifiModule_Server_Config(); /* API3. 通过串口给PC发送一个字符 */ void sendByte(char data_msg); /* API4. 通过串口给PC发送一个字符串 */ void sendString(char *str); /* API5. 用于每隔1秒给wifi模块发送心跳包,以及串口初始化后的短暂休眠 */ void Delay1000ms(); void main() { ledD5 = ledD6 = 1;//灭状态灯 UartInit(); Delay1000ms();//给espwifi模块上电时间 wifiModule_Server_Config(); while(1){ sendString(esp01s_0Channel_dataSend); Delay1000ms(); //虽然说发送也是需要时间,但是不要用检测“OK”来暂停程序 Delay1000ms(); sendString("Hello"); //服务器给客户端的心跳包(串口助手上也能看见) Delay1000ms(); Delay1000ms(); } } void Uart_Routine() interrupt 4 { static int i = 0;//静态变量,被初始化一次 char temp; /* 中断处理程序中,对于接收中断的响应 */ if(RI)//中断处理函数中,对于接收中断的响应 { RI = 0;//清除接收中断标志位 temp = SBUF; if(temp == 'O' || temp == '0' || temp == ':'){ i = 0; } serial_buffer[i] = temp; i++; if(i == len) i = 0; //wifi模块响应值的判断 if(serial_buffer[0] == 'O' && serial_buffer[1] == 'K'){ AT_OK_Flag = 1; memset(serial_buffer, '\0', len); } if(serial_buffer[0] == '0' && serial_buffer[2] == 'C'){ Client_Connect_Flag = 1; memset(serial_buffer, '\0', len); } //灯控指令 if(serial_buffer[0] == ':' && serial_buffer[1] == 'o' && serial_buffer[2]=='p'){ ledD5 = 0;//点亮D5 memset(serial_buffer, '\0', len); }else if(serial_buffer[0] == ':' && serial_buffer[1] == 'c' && serial_buffer[2]=='l'){ ledD5 = 1;//熄灭D5 memset(serial_buffer, '\0', len); } } /* 中断处理程序中,对于发送中断的响应 */ if(TI == 1){ // 暂时不做任何事情 } } void UartInit(void) //9600bps@11.0592MHz { AUXR = 0x01; SCON = 0x50; //8位UART,允许串口接收 TMOD &= 0xDF; TMOD |= 0x20; //定时器8位重载工作模式 TH1 = 0xFD; TL1 = 0xFD; //9600波特率初值 TR1 = 1; //启动定时器 EA = 1; ES = 1; //开启串口中断 } void wifiModule_Server_Config() { sendString(esp01s_modeSetting); while(!AT_OK_Flag); AT_OK_Flag = 0; sendString(esp01s_multiLink); while(!AT_OK_Flag); AT_OK_Flag = 0; sendString(esp01s_setTCPServer); while(!AT_OK_Flag); AT_OK_Flag = 0; ledD5 = 0; //点亮D5,代表成功建立TCP服务器 while(!Client_Connect_Flag); if(Client_Connect_Flag){ ledD6 = 0; //点亮D6,代表有客户端接入 } } void sendByte(char data_msg) { SBUF = data_msg; while(!TI); TI = 0; } void sendString(char *str) { char *p = str; while(*p != '\0'){ sendByte(*p); p++; } } void Delay1000ms() //@11.0592MHz { unsigned char i, j, k; _nop_(); i = 8; j = 1; k = 243; do { do { while (--k); } while (--j); } while (--i); }