(一)简介


ESP-01S模块
一、核心处理器与功能
  • 核心处理器:ESP-01的核心处理器是ESP8266,该处理器集成了Tensilica L106超低功耗32位微型MCU,带有16位精简模式,主频支持80MHz和160MHz。
  • Wi-Fi功能:ESP8266集成了Wi-Fi MAC/BB/RF/PA/LNA,支持标准的IEEE802.11 b/g/n协议和完整的TCP/IP协议栈,可以方便地与其他设备或网络进行通信。

二、模块特点

  • 设计紧凑:ESP-01尺寸小巧,设计紧凑,适用于空间有限的应用。
  • 多种工作模式:ESP-01可以通过路由器连接到互联网(STA模式),使手机或电脑能够实现对设备的远程控制;也可以作为热点(AP模式),使其他设备能够连接到它。
  • 可编程GPIO引脚:ESP-01提供了多个可编程的GPIO引脚,可以用于控制外部设备,如传感器和执行器等。
  • 灵活的IP地址配置:用户可以通过串口输入更改控制器在局域网的IP地址。

用途
由于我们的arduino uno是没有WIFI联网功能的,所以一些好玩的项目,或者一些常用的网络通信协议tcp、mqtt等也是用不了的
那么为了拓展arduino uno的可玩性,方便开发更多项目,我们可以外接一个ESP-01S模块,让ESP-01S联网接收信息,然后将信息传入arduino uno

但是由于arduino uno本身内存和性能就比较低,运行联网的项目可能会占用大量的资源,可能导致项目运行不了
课程后续会更新ESP8266/ESP32这种高内存高性能的芯片,自带WIFI和蓝牙,做项目非常好,而且也可以用arduino IDE进行编程


(二)ESP-01S

c189c557816be6c509dd22dbdbdabc2.png
ESP-01S的通信模式为串口通信(TX/RX)
所以我们可以先用串口调试工具,来看看这个模块是怎么工作的
将串口通信工具的RX和模块的TX链接,工具的TX和模块的RX链接,GND和VCC连接


ESP-01S访问天气API获取信息
文章末尾带AT指令大全
①AT(测试指令)
返回"OK"代表模块运行正常
image.png

②AT+CWMODE=3(工作模式--AP兼Station模式)
返回"OK"代表设置成功
image.png

③AT+CWJAP="12345678","88888888"(连接WIFI,账号、密码)
连接过程会陆续返回3条信息,如果连接成功,最后返回"OK"
image.png

④AT+CIPSTART="TCP","api.xemowo.top",80(使用TCP协议,接入API服务器,端口为80)
连接成功则返回"OK",若长时间没动静,则返回"closed"关闭连接
image.png

⑤AT+CIPMODE=1(设置透传模式)
设置成功,返回"OK"
image.png

⑥AT+CIPSEND(发送数据)
此时会返回">"符号,代表可以访问API了
image.png

⑦GET http://api.xemowo.top/api/tqyb.php?city=442000(GET请求,访问API)
返回天气API的JSON信息
image.png

⑧自此,ESP-01S整套访问API的流程结束,方法就是往串口发送指令,然后返回对应的指令,还是比较简单的


(三)程序
①接线
3335dedcda97adc182f8b4c7e09653b.jpg
首先要接线,注意tx和rx的位置
arduino uno esp-01s
TX RX
RX TX
GND GND
5V VCC


②创建变量
创建一个字符串变量用于储存我们接收到的数据
创建两个整数型变量,用于存储温湿度数据
创建一个char变量,储存接收过程的数据
  1. String rcvData="";//保存数据
  2. int temperature = 0;//温度数据
  3. int humidity = 0;//湿度数据

  4. char SerialRcvBuf[500];


③发送AT指令函数
while循环,send_cmd函数可以发送一条串口数据,然后在返回的数据中含有需要的数据就退出while循环
例:while (!send_cmd("AT", "OK"));
发送指令,while (!Serial.available());一直判断是否有接收到信息
如果接收到,则进入if判断
  if (Serial.find(keyword))       //返回关键词判断,比如ok
  {
    returntrue;
  }
  else
  {
     returnfalse;
   }

Serial.find函数:如果接收的信息中,带有所需的信息,如"OK"
则返回true,否则false

   while (Serial.available()) Serial.read();      //清空串口缓存
注意,每次执行完,要情况缓存,用于下次执行
  1. //boolean 成功返回true,失败返回false
  2. boolean send_cmd(String data, char*keyword)
  3. {
  4.   boolean result = false;
  5.   if (data != "")   //对于tcp连接命令,直接等待第二次回复
  6.   {
  7.     Serial.println(data);  //发送AT指令
  8.   }
  9.   if (data == "AT")              //寻找esp8266是否正常工作
  10.   delay(1000);
  11.   else
  12.     while (!Serial.available());              // 等待wifi模块应答
  13.     delay(200);
  14.   if (Serial.find(keyword))       //返回关键词判断,比如ok
  15.   {
  16.     returntrue;
  17.   }
  18.   else
  19.   {
  20.      returnfalse;
  21.    }
  22.   while (Serial.available()) Serial.read();      //清空串口缓存
  23.   delay(500);
  24. }




④初始化函数SETUP
利用AT指令函数,我们可以在setup的时候,初始化ESP-01S
设置波特率为115200(注意,要和ESP-01S的波特率一致)
然后逐条执行AT程序,下列AT指令在上文中有讲解
然后执行完最后一条AT指令后,发送一条信息,提示初始化成功Serial.println("connect success");
  1. void setup() {

  2.   Serial.begin(115200);
  3.   while (!send_cmd("AT", "OK"));
  4.   while (!send_cmd("AT+CWMODE=3", "OK"));                                         //工作模式   
  5.   while (!send_cmd("AT+CWJAP="XEMOWO","1SUNjiajun"", "OK"));       //接入AP
  6.   while (!send_cmd("AT+CIPSTART="TCP","api.xemowo.top",80", "OK"));      //接入服务器
  7.   while (!send_cmd("AT+CIPMODE=1", "OK"));                                   //透传模式
  8.   while (!send_cmd("AT+CIPSEND", ">"));                                          //开始发送
  9.   Serial.println("connect success");                        //发送报文信息

  10. }


⑤循环函数LOOP
直接串口发送"GET http://api.xemowo.top/api/tqyb.php?city=442000"GET访问API地址
如果Serial.available()串口缓存区不为0时
先将保存数据的变量清空
然后进入while循环,直到缓存区没内容为止

读取每个字节的数据,存入到字符串变量内
SerialRcvBuf[rcvMark]= char(Serial.read());

rcvMark++;然后字节个数,自加

这里你可以直接将char转换成字符串类型,也可以不用
  1. rcvData = String(SerialRcvBuf); // 执行转换与保存
  2. Serial.println(SerialRcvBuf);

然后执行两条函数(下文解析),用于读取串口数据内的温度和湿度信息
  1. temperatureJson(SerialRcvBuf);
  2. humidityJson(SerialRcvBuf);

最后,打印温度和湿度数据
  1. Serial.println(humidity);
  2. Serial.println(temperature);


  1. Serial.println("GET http://api.xemowo.top/api/tqyb.php?city=442000");
  2.   if (Serial.available() > 0) {
  3.     memset(SerialRcvBuf, 0, sizeof(SerialRcvBuf));// 先清空buffer
  4.     byte rcvMark=0;
  5.     while(Serial.available() > 0){
  6.    
  7.     SerialRcvBuf[rcvMark]= char(Serial.read());  
  8.     delay(2);
  9.     rcvMark++;
  10.     }
  11.   //  Serial.println(SerialRcvBuf);
  12.     rcvData = String(SerialRcvBuf); // 执行转换与保存
  13.   Serial.println(SerialRcvBuf);

  14.   temperatureJson(SerialRcvBuf);
  15.   humidityJson(SerialRcvBuf);
  16.   Serial.println(humidity);
  17.   Serial.println(temperature);
  18.   }
  19.     delay(5000);
  20. }


⑥temperatureJson和humidityJson读取JSON信息内所需的数据,使用截取的方式
(两段函数大同小异,只讲一段)

首先,我们得到的数据是这样的,我们需要获取其中的temperature和humidity
很显然可以用arduinojson来解析,但是不行(下文讲解)
所以可以采用截取字符串的方法,因为json的变量名是不变的
  1. {"province":"广东","city":"中山市","adcode":"442000","weather":"晴","temperature":"18","winddirection":"北","windpower":"≤3","humidity":"59","reporttime":"2024-11-26 19:00:46","temperature_float":"18.0","humidity_float":"59.0"}

利用strstr函数,截取数据中temperature的位置
  1. const char* cityFieldStart = strstr(json, "temperature");

从temperature的位置开始算,跳过14位,相当于跳过temperature":",就只剩下18这个数据量
  1. cityFieldStart += 14; // "temperature":" 共有14个字符

寻找cityFieldStart值的下一个"分号,就是 18"的这个分号,为结束的位置
  1. const char* cityValueEnd = strchr(cityFieldStart, '"');


末尾的位置 减去 寻找值temperature的位置的位置,可以得到数据量 18 的位置
  1. size_t cityValueLength = cityValueEnd - cityFieldStart;

创建变量,用于储存数据量 18
  1. char cityValueBuffer[cityValueLength + 1];

将数据量移动到变量内
  1. strncpy(cityValueBuffer, cityFieldStart, cityValueLength);

使用atoi函数,将字符串转换成整数,并存入变量内
  1. temperature = atoi(cityValueBuffer);//atoi字符串转换为整数


  1. void temperatureJson(const char* json) {
  2.   // 查找"city":"的起始位置
  3.   const char* cityFieldStart = strstr(json, "temperature");
  4.   if (!cityFieldStart) {
  5.     Serial.println("City field not found in JSON.");
  6.     return;
  7.   }

  8.   // 跳过"temperature":"部分,指向值的起始位置(即第二个引号之后)
  9.   cityFieldStart += 14; // "temperature":" 共有14个字符

  10.   // 查找值的结束位置(即下一个引号)
  11.   const char* cityValueEnd = strchr(cityFieldStart, '"');
  12.   if (!cityValueEnd) {
  13.     Serial.println("Error extracting city value from JSON.");
  14.     return;
  15.   }

  16.   // 计算temperature值的长度(不包括起始和结束引号)
  17.   size_t cityValueLength = cityValueEnd - cityFieldStart;

  18.   // 创建一个字符数组来存储temperature值(加1用于null终止符)
  19.   char cityValueBuffer[cityValueLength + 1];

  20.   // 复制temperature值到字符数组中
  21.   strncpy(cityValueBuffer, cityFieldStart, cityValueLength);
  22.   cityValueBuffer[cityValueLength] = '\0'; // 确保字符串以null终止

  23.   // 打印temperature值
  24.   Serial.print("temperatureJson: ");
  25.   Serial.println(cityValueBuffer);
  26.   temperature = atoi(cityValueBuffer);//atoi字符串转换为整数
  27.     Serial.println(temperature);
  28. }


⑦代码现象
image.png


(四)问题
在学习ESP-01S与arduino uno通信的过程中,遇到几个问题,大家有什么解决方法也可以留言

①串口缓存区,内存不足
现象:接收到的串口的JSON数据不全,只有一半
image.png

原因:arduino uno的串口库定义的缓存区内存太小,不足以显示完整所有的数据

解决方法:
文件夹:C:\Users\XEMOWO\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.8.6\cores\arduino
image.png
找到arduino uno的内置库,找到串口通信的定义库HardwareSerial.h
将SERIAL_TX_BUFFER_SIZE和SERIAL_RX_BUFFER_SIZE都改成300,300足够显示很长的数据
image.png


②为什么不使用Arduino json库
正常来说,使用Arduino json可以解析JSON信息,但是不知道为什么我在使用的过程中,解析不了
提示:deserializeJson() failed: NoMemory
我将函数的储存改大,依旧报错
  1. StaticJsonDocument<384> doc;
解决方法:未知
(在ESP32中可以正常使用,感觉是uno的闪存跟不上)
希望有懂的朋友可以解答一下


(五)完整程序
  1. String rcvData="";//保存数据
  2. int temperature = 0;//温度数据
  3. int humidity = 0;//湿度数据

  4. char SerialRcvBuf[500];
  5. void setup() {

  6.   Serial.begin(115200);
  7.   while (!send_cmd("AT", "OK"));
  8.   while (!send_cmd("AT+CWMODE=3", "OK"));                                         //工作模式   
  9.   while (!send_cmd("AT+CWJAP="XEMOWO","1SUNjiajun"", "OK"));       //接入AP
  10.   while (!send_cmd("AT+CIPSTART="TCP","api.xemowo.top",80", "OK"));      //接入服务器
  11.   while (!send_cmd("AT+CIPMODE=1", "OK"));                                   //透传模式
  12.   while (!send_cmd("AT+CIPSEND", ">"));                                          //开始发送
  13.   Serial.println("connect success");                        //发送报文信息

  14. }

  15. void loop() {
  16.   Serial.println("GET http://api.xemowo.top/api/tqyb.php?city=442000");
  17.   if (Serial.available() > 0) {
  18.     memset(SerialRcvBuf, 0, sizeof(SerialRcvBuf));// 先清空buffer
  19.     byte rcvMark=0;
  20.     while(Serial.available() > 0){
  21.    
  22.     SerialRcvBuf[rcvMark]= char(Serial.read());  
  23.     delay(2);
  24.     rcvMark++;
  25.     }
  26.   //  Serial.println(SerialRcvBuf);
  27.     rcvData = String(SerialRcvBuf); // 执行转换与保存
  28.   Serial.println(SerialRcvBuf);

  29.   temperatureJson(SerialRcvBuf);
  30.   humidityJson(SerialRcvBuf);
  31.   Serial.println(humidity);
  32.   Serial.println(temperature);
  33.   }
  34.     delay(5000);
  35. }

  36. //boolean 成功返回true,失败返回false
  37. boolean send_cmd(String data, char *keyword)
  38. {
  39.   boolean result = false;
  40.   if (data != "")   //对于tcp连接命令,直接等待第二次回复
  41.   {
  42.     Serial.println(data);  //发送AT指令
  43.   }
  44.   if (data == "AT")              //寻找esp8266是否正常工作
  45.   delay(1000);
  46.   else
  47.     while (!Serial.available());              // 等待wifi模块应答
  48.     delay(200);
  49.   if (Serial.find(keyword))       //返回关键词判断,比如ok
  50.   {
  51.     return true;
  52.   }
  53.   else
  54.   {
  55.      return false;
  56.    }
  57.   while (Serial.available()) Serial.read();      //清空串口缓存
  58.   delay(500);
  59. }



  60. // 提取并打印JSON中temperature字段的值的函数
  61. void temperatureJson(const char* json) {
  62.   // 查找"city":"的起始位置
  63.   const char* cityFieldStart = strstr(json, "temperature");
  64.   if (!cityFieldStart) {
  65.     Serial.println("City field not found in JSON.");
  66.     return;
  67.   }

  68.   // 跳过"city":"部分,指向值的起始位置(即第二个引号之后)
  69.   cityFieldStart += 14; // "city":" 共有7个字符

  70.   // 查找值的结束位置(即下一个引号)
  71.   const char* cityValueEnd = strchr(cityFieldStart, '"');
  72.   if (!cityValueEnd) {
  73.     Serial.println("Error extracting city value from JSON.");
  74.     return;
  75.   }

  76.   // 计算city值的长度(不包括起始和结束引号)
  77.   size_t cityValueLength = cityValueEnd - cityFieldStart;

  78.   // 创建一个字符数组来存储city值(加1用于null终止符)
  79.   char cityValueBuffer[cityValueLength + 1];

  80.   // 复制city值到字符数组中
  81.   strncpy(cityValueBuffer, cityFieldStart, cityValueLength);
  82.   cityValueBuffer[cityValueLength] = '\0'; // 确保字符串以null终止

  83.   // 打印city值
  84.   Serial.print("temperatureJson: ");
  85.   Serial.println(cityValueBuffer);
  86.   temperature = atoi(cityValueBuffer);//atoi字符串转换为整数
  87.     Serial.println(temperature);
  88. }



  89. void humidityJson(const char* json) {
  90.   // 查找"city":"的起始位置
  91.   const char* cityFieldStart2 = strstr(json, "humidity");
  92.   if (!cityFieldStart2) {
  93.     Serial.println("City field not found in JSON.");
  94.     return;
  95.   }

  96.   // 跳过"city":"部分,指向值的起始位置(即第二个引号之后)
  97.   cityFieldStart2 += 11; // "city":" 共有7个字符

  98.   // 查找值的结束位置(即下一个引号)
  99.   const char* cityValueEnd2 = strchr(cityFieldStart2, '"');
  100.   if (!cityValueEnd2) {
  101.     Serial.println("Error extracting city value from JSON.");
  102.     return;
  103.   }

  104.   // 计算city值的长度(不包括起始和结束引号)
  105.   size_t cityValueLength2 = cityValueEnd2 - cityFieldStart2;

  106.   // 创建一个字符数组来存储city值(加1用于null终止符)
  107.   char cityValueBuffer2[cityValueLength2 + 1];

  108.   // 复制city值到字符数组中
  109.   strncpy(cityValueBuffer2, cityFieldStart2, cityValueLength2);
  110.   cityValueBuffer2[cityValueLength2] = '\0'; // 确保字符串以null终止

  111.   // 打印city值
  112.   Serial.print("humidity: ");
  113.   Serial.println(cityValueBuffer2);
  114.   humidity = atoi(cityValueBuffer2);//atoi字符串转换为整数
  115. }


(六)AT指令大全
指令表出处:https://blog.csdn.net/michaelchain/article/details/119627777

基本命令
AT: 测试, 模块正常应当返回OK
AT+RST: 重启模块
AT+GMR: 检查固件版本信息
ATE: 配置 AT 命令的回显.
ATE0: 关闭回显
ATE1: 打开回显
AT+UART_CUR? 查看当前的UART配置
AT+UART_DEF? 查看默认的UART配置

WIFI命令
AT+CWMODE?: 查看当前的WIFI模式(Station/SoftAP/Station+SoftAP), 用=号可以设置
AT+CWMODE=3
AT+CWLAP: 列出周围的WIFI AP, 需要先设置为station模式, AT+CWMODE=1
AT+CWJAP: 连接到WIFI AP, 命令格式 AT+CWJAP="DXQ","aa123456"
掉电重启后, 模块会自动连接之前连接的WIFI
AT+CWQAP: 断开与WIFI AP的连接
AT+CIPSTAMAC: 查看客户端模式时的MAC地址
AT+CIPSTA?: 查看客户端模式的IP地址及掩码
AT+CIPAPMAC?: Query/Set the MAC address of an ESP SoftAP.
AT+CIPAP?: Query/Set the IP address of an ESP SoftAP.

TCP/IP命令
AT+CIPSTATUS: 获取TCP/UDP/SSL连接状态和信息, 先会显示一个状态, 然后会列出各个连接
0: The ESP station is not initialized.
1: The ESP station is initialized, but not started a Wi-Fi connection yet.
2: The ESP station is connected to an AP and its IP address is obtained.
3: The ESP station has created a TCP/SSL transmission.
4: All of the TCP/UDP/SSL connections of the ESP device station are disconnected.
5: The ESP station started a Wi-Fi connection, but was not connected to an AP or disconnected from an AP.
AT+PING: ping指定的地址, 返回平均响应时间
AT+PING="192.168.110.1"
AT+CIPSTART: 建立TCP/UDP/SSL连接
AT+CIPSTART="TCP","192.168.110.1",22
AT+CIPSTART="TCPv6","test-ipv6.com",80
AT+CIPSTART="UDP","192.168.101.110",1000,1002,2 第4个参数是本地端口, 第5个是模式, UDP unicast
AT+CIPSTART="UDPv6","FF02::FC",1000,1002,0 UDP multicast based on IPv6 network
AT+CIPSTART="SSL","iot.espressif.cn",8443
AT+CIPSEND: 透传模式发送数据
AT+CIPSENDEX: Send data in the normal transmission mode in expanded ways.
AT+CIPCLOSE: 关闭TCP/UDP/SSL连接
AT+CIFSR: 获取本地IP地址和MAC地址
AT+CIPSNTPTIME: 查询SNTP时间
AT+CIPMUX?: 查询连接类型, 0单连接, 1多连接
AT+CIPMUX=1 设置为多连接
AT+CIPSERVER: 创建或删除一个 TCP/SSL 服务, 创建前需要执行AT+CIPMUX=1
AT+CIPSERVER=1,80 创建一个TCP服务
AT+CIPSERVER=1,443,"SSL",1 创建一个SSL服务
AT+CIPSERVER=0,1 删除一个服务并关闭所有连接
AT+CIPSERVERMAXCONN?: 查询允许的服务最大连接数量
AT+CIPSTO?: 查询本地TCP服务超时时间
AT+CIPSNTPCFG?: 查询 time zone and SNTP server.
AT+CIPMODE: 查询传输模式
0: 普通传输模式
1: Wi-Fi 透传模式, 只有在TCP单连接模式, 以及UDP+remote host and port do not change模式, 或者SSL单连接模式时允许