(一)简介


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变量,储存接收过程的数据
String rcvData="";//保存数据
  • int temperature = 0;//温度数据
  • int humidity = 0;//湿度数据

  • 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();      //清空串口缓存
    注意,每次执行完,要情况缓存,用于下次执行
    //boolean 成功返回true,失败返回false
  • boolean send_cmd(String data, char*keyword)
  • {
  •   boolean result = false;
  •   if (data != "")   //对于tcp连接命令,直接等待第二次回复
  •   {
  •     Serial.println(data);  //发送AT指令
  •   }
  •   if (data == "AT")              //寻找esp8266是否正常工作
  •   delay(1000);
  •   else
  •     while (!Serial.available());              // 等待wifi模块应答
  •     delay(200);
  •   if (Serial.find(keyword))       //返回关键词判断,比如ok
  •   {
  •     returntrue;
  •   }
  •   else
  •   {
  •      returnfalse;
  •    }
  •   while (Serial.available()) Serial.read();      //清空串口缓存
  •   delay(500);
  • }


  • 复制代码


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

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

  • }
  • 复制代码


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

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

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

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

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

    最后,打印温度和湿度数据
    Serial.println(humidity);
  • Serial.println(temperature);
  • 复制代码


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

  •   temperatureJson(SerialRcvBuf);
  •   humidityJson(SerialRcvBuf);
  •   Serial.println(humidity);
  •   Serial.println(temperature);
  •   }
  •     delay(5000);
  • }
  • 复制代码


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

    首先,我们得到的数据是这样的,我们需要获取其中的temperature和humidity
    很显然可以用arduinojson来解析,但是不行(下文讲解)
    所以可以采用截取字符串的方法,因为json的变量名是不变的
    {"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的位置
    const char* cityFieldStart = strstr(json, "temperature");
    复制代码

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

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


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

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

    将数据量移动到变量内
    strncpy(cityValueBuffer, cityFieldStart, cityValueLength);
    复制代码

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


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

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

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

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

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

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

  •   // 打印temperature值
  •   Serial.print("temperatureJson: ");
  •   Serial.println(cityValueBuffer);
  •   temperature = atoi(cityValueBuffer);//atoi字符串转换为整数
  •     Serial.println(temperature);
  • }
  • 复制代码


    ⑦代码现象
    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
    我将函数的储存改大,依旧报错
    StaticJsonDocument<384> doc;
    复制代码
    解决方法:未知
    (在ESP32中可以正常使用,感觉是uno的闪存跟不上)
    希望有懂的朋友可以解答一下


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

  • char SerialRcvBuf[500];
  • void setup() {

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

  • }

  • void loop() {
  •   Serial.println("GET http://api.xemowo.top/api/tqyb.php?city=442000");
  •   if (Serial.available() > 0) {
  •     memset(SerialRcvBuf, 0, sizeof(SerialRcvBuf));// 先清空buffer
  •     byte rcvMark=0;
  •     while(Serial.available() > 0){
  •    
  •     SerialRcvBuf[rcvMark]= char(Serial.read());  
  •     delay(2);
  •     rcvMark++;
  •     }
  •   //  Serial.println(SerialRcvBuf);
  •     rcvData = String(SerialRcvBuf); // 执行转换与保存
  •   Serial.println(SerialRcvBuf);

  •   temperatureJson(SerialRcvBuf);
  •   humidityJson(SerialRcvBuf);
  •   Serial.println(humidity);
  •   Serial.println(temperature);
  •   }
  •     delay(5000);
  • }

  • //boolean 成功返回true,失败返回false
  • boolean send_cmd(String data, char *keyword)
  • {
  •   boolean result = false;
  •   if (data != "")   //对于tcp连接命令,直接等待第二次回复
  •   {
  •     Serial.println(data);  //发送AT指令
  •   }
  •   if (data == "AT")              //寻找esp8266是否正常工作
  •   delay(1000);
  •   else
  •     while (!Serial.available());              // 等待wifi模块应答
  •     delay(200);
  •   if (Serial.find(keyword))       //返回关键词判断,比如ok
  •   {
  •     return true;
  •   }
  •   else
  •   {
  •      return false;
  •    }
  •   while (Serial.available()) Serial.read();      //清空串口缓存
  •   delay(500);
  • }



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

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

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

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

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

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

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



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

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

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

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

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

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

  •   // 打印city值
  •   Serial.print("humidity: ");
  •   Serial.println(cityValueBuffer2);
  •   humidity = atoi(cityValueBuffer2);//atoi字符串转换为整数
  • }
  • 复制代码


    (六)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单连接模式时允许