物联网ESP32快速指南



物联网简要概述

在软件方面,您需要在您的机器上安装Arduino IDE。请访问 https://www.arduino.cc/en/software

在硬件方面,您需要以下组件:

  • ESP32开发板 - 必需

ESP32
  • Micro USB数据线 - 用于为ESP32供电和编程

Micro USB
  • MPU6050模块 - 可选(仅在MPU6050相关的章节中需要)

MPU6050
  • 光敏电阻(LDR)和一个阻值相当的普通电阻或任何其他模拟传感器 - 可选(仅在ADC章节中需要)

LDR
  • OLED显示屏 - 可选(仅在OLED接口相关的章节中需要)

OLED
  • 跳线 - 可选(将ESP32与MPU6050、LDR和/或OLED显示屏连接时需要)

Jumper wires

关于GitHub使用的说明

如概述部分所述,每个章节都提供了一个GitHub链接,其中包含代码演练。其中许多代码取自Arduino中ESP32开发板附带的示例。因此,您无需额外努力即可在本地机器上运行它们。安装ESP32开发板到Arduino后(我们为此专门设置了一个章节),您可以在Arduino IDE中找到它们(文件 -> 示例)。在使用示例代码的地方,都会提到示例代码的确切路径。

所有不在示例中的代码都可以在以下代码库中找到:https://github.com/yash-sanghvi/ESP32。现在,如果您希望下载并在本地机器上运行这些代码,您需要执行以下操作:

  • 点击显示“代码”的绿色按钮。

GitHub
  • 如果您不熟悉Git,您可以简单地下载zip文件并将其解压缩到您选择的文件夹中。子文件夹包含所需的Arduino(.ino)文件,您可以随后在Arduino IDE中打开这些文件,并编译并烧录到ESP32中。

  • 如果您熟悉Git并在您的机器上安装了Git,您可以复制HTTPS地址(https://github.com/yash-sanghvi/ESP32.git),导航到您希望克隆此代码库的文件夹,打开您的Git命令行并输入git clone https://github.com/yash-sanghvi/ESP32.git

GitHub

如果您不熟悉Git,您可能想知道为什么我们应该费力克隆代码库,而下载和解压zip文件会产生相同的效果。答案是下载zip文件是一次性过程。如果将来此代码库发生某些更改,本地机器上下载的版本无法直接反映这些更改。您需要再次下载zip文件。如果您克隆了代码库,则只需调用git pull即可获取所有将来的更改。使用克隆还可以做更多的事情。如果代码库中有多个分支,则只需git checkout branch-name即可切换分支。如果您下载zip文件,则需要为每个分支下载单独的zip文件。总而言之,克隆通常更方便。但是,对于此特定用例,由于我们预计将来此代码库不会发生重大更改,并且您只需要主分支,如果您不熟悉Git,您可以继续下载zip文件。

您是否注意到最近很多日常用品都变得“智能化”了?有智能电视、智能空调、智能冰箱等等。这些设备的“智能化”指的是什么?虽然每个设备的答案略有不同,但智能化的一个共同要素是“连接性”。您的电视连接到您的WiFi,因此您可以串流以前只能在手机上观看的节目。您的空调连接到互联网。您可以从另一个城市通过手机发送命令,家里的空调就会打开/关闭。您的手表连接到您的手机(通过BLE),您可以使用手表本身接听电话。您通常处理的所有事物都连接在一起,就像在一个网络中一样。这是一个物联网。

IoT

以上段落应该让您对物联网有了一些了解。根据维基百科,物联网的定义如下:

物联网 (IoT) 描述的是物理对象的网络——“事物”——这些对象嵌入了传感器、软件和其他技术,用于通过互联网连接和交换数据与其他设备和系统。

上述定义非常准确。嵌入传感器的物体,包含软件,通过互联网与其他设备/系统共享数据。此定义还清楚地突出了任何物联网设备的三个主要功能模块中的两个:

  • 感知

  • 处理和存储

  • 传输

感知

物联网设备感知什么?它们可以感知任何值得感知的东西。如果您的物联网设备安装在垃圾场,它可能会检查垃圾的填充水平。如果您的物联网设备安装在工厂,它可能会感知电力消耗。如果您的物联网设备安装在机器上,它可能会感知机器的振动特征以确定机器是打开、关闭还是切割。如果您的设备安装在车辆上,它可能会感知车辆的移动和位置。

您的物联网设备将感知任何可以帮助您节省成本、增加利润或警告您即将发生灾难的事情。传统的火灾报警器非常接近物联网设备。它感知烟雾,对其进行处理以确定烟雾浓度是否高于安全水平。它只是没有将此信息传输到任何地方。但是,如果您将建筑物中的所有火灾报警器连接到互联网,并在安全室中设置一个仪表板,显示哪个房间发生了火灾,那么您的火灾报警器将非常像一个物联网设备。

处理和存储

物联网设备上进行哪些处理/存储?此答案很大程度上取决于您的用例。有些物联网设备不进行板载处理,只是将原始传感器数据传输到服务器。有些物联网设备在板载进行实时视频处理以识别物体/人员。这取决于您的数据量、可用RAM、所需的最终输出和可用的传输带宽。如果您的设备每毫秒获取一次机器的振动特征,那么您在一秒钟内就会有1000个读数。在这种情况下,将如此大量的数据发送到服务器可能没有意义(特别是如果您使用的是低带宽网络,例如NB-IoT)。在这种情况下,您可能希望在设备上执行FFT,只需将振动的频率和幅度发送到服务器。如果您的设备每5分钟感知一次大气中的温度和湿度,您可能只需要一个公式将原始读数转换为温度和湿度并将其发送出去。或者您可以只发送原始读数,让服务器进行转换。在这种情况下,您可以发送每个读数。

几乎所有物联网设备都有一些板载内存,用于在网络错误的情况下存储丢失的数据包。有些设备有配置文件,也需要板载存储。有些设备将其内存中的最后X小时数据保留以供将来访问。进行大量板载处理的物联网设备肯定需要存储空间才能在处理开始前收集足够的数据。例如,如果您的设备每10,000个读数后对振动数据执行FFT,则需要存储传入的读数,直到数量达到10,000。

传输

物联网设备如何传输数据?嗯,有几种解决方案可用。其中一些是:

选择合适的传输解决方案本身就是一个重大决定,很大程度上取决于您可用的电源、带宽要求、通信距离、成本和可接受的延迟。您的智能手表可以使用BLE与您的手机通信,您的智能电视可以使用WiFi,而安装在车辆上的设备可以使用蜂窝网络。为农业应用(例如土壤湿度测量)而制造的物联网设备,尤其是在偏远地区,可以使用LoRa与另一个设备通信,而该设备反过来可能具有WiFi或以太网连接。最终目标几乎总是将数据放在服务器上,和/或在仪表板/应用程序上向用户显示数据。

总结

如果您是物联网新手,本章将为您很好地概述物联网的重点所在。如果您对此感到兴奋,请继续阅读下一章,我们将讨论ESP32,这是一款系统级芯片 (SoC) 微控制器,本教程将围绕它展开。我们将讨论ESP32为什么在物联网领域如此受欢迎,以及它在传感、处理、存储和传输领域的各项功能。我们下一章见。

ESP32简介

ESP32是一款系统级芯片 (SoC) 微控制器,最近获得了巨大的普及。ESP32的流行是因为物联网的发展,还是物联网的发展是因为ESP32的推出,这是一个值得商榷的问题。如果您认识10位参与过任何物联网设备固件开发的人,那么其中7-8位很可能在某个时候使用过ESP32。那么,这究竟是怎么回事呢?为什么ESP32能如此迅速地普及呢?让我们来了解一下。

ESP32

在我们深入探讨ESP32流行的实际原因之前,让我们先来看看它的一些重要规格。以下列出的规格属于ESP32 WROOM 32版本。

  • 集成晶体:40 MHz

  • 模块接口:UART、SPI、I2C、PWM、ADC、DAC、GPIO、脉冲计数器、电容式触摸传感器

  • 集成SPI闪存:4 MB

  • ROM:448 KB(用于引导和核心功能)

  • SRAM:520 KB

  • 集成连接协议:WiFi、蓝牙、BLE

  • 片上传感器:霍尔传感器

  • 工作温度范围:-40至85摄氏度

  • 工作电压:3.3V

  • 工作电流:80 mA(平均)

有了上述规格,很容易就能解释ESP32流行的原因。考虑一下物联网设备对其微控制器 (μC) 的需求。如果您阅读了上一章,您就会意识到任何物联网设备的主要操作模块都是传感、处理、存储和传输。因此,首先,μC应该能够与各种传感器接口。它应该支持传感器接口所需的所有常用通信协议:UART、I2C、SPI。它应该具有ADC和脉冲计数功能。ESP32满足所有这些要求。最重要的是,它还可以与电容式触摸传感器接口。因此,大多数常用传感器可以与ESP32无缝对接。

其次,μC应该能够对传入的传感器数据进行基本处理,有时需要高速处理,并具有足够的内存来存储数据。ESP32的最大工作频率为40 MHz,足够高。它有两个内核,允许并行处理,这是一个额外的优势。最后,它的520 KB SRAM对于处理大量板载数据来说足够大。许多流行的流程和转换,如FFT、峰值检测、RMS计算等,都可以在ESP32上进行。在存储方面,ESP32比传统的微控制器更进一步,并在闪存中提供了一个文件系统。在4 MB的板载闪存中,默认情况下,1.5 MB被预留为SPIFFS(SPI闪存文件系统)。可以把它想象成一个位于芯片内部的迷你SD卡。您不仅可以存储数据,还可以存储文本文件、图像、HTML和CSS文件等等。人们已经使用ESP32创建的WiFi服务器显示了精美的网页,方法是将HTML文件存储在SPIFFS中。

最后,对于数据传输,ESP32集成了WiFi和蓝牙协议栈,这被证明是一个改变游戏规则的功能。无需连接单独的模块(如GSM模块或LTE模块)即可测试云通信。只需拥有ESP32开发板和运行中的WiFi,即可开始使用。ESP32允许您在接入点和站点模式下使用WiFi。虽然它支持TCP/IP、HTTP、MQTT和其他传统的通信协议,但它也支持HTTPS。是的,您没听错。它有一个加密核心或加密加速器,这是一个专门的硬件,其工作是加速加密过程。因此,您不仅可以与您的Web服务器通信,还可以安全地进行通信。BLE支持对于许多应用也很关键。当然,您可以将LTE、GSM或LoRa模块与ESP32接口。因此,在“数据传输”方面,ESP32也超出了预期。

拥有如此多的功能,ESP32的价格一定很贵,对吧?这是最好的部分。ESP32开发模块的价格在500卢比左右。不仅如此,芯片尺寸也很小(25毫米x 18毫米,包括天线区域),允许将其用于需要非常小尺寸的设备。

最后,ESP32可以使用Arduino IDE进行编程,这使得学习曲线变得平缓得多。是不是很棒?您是否渴望开始使用ESP32?那么,让我们在下一章开始在Arduino IDE中安装ESP32开发板。我们下一章见。

在Arduino IDE中安装ESP32开发板

ESP32的一个非常大的优势,也是它快速普及和广受欢迎的原因,是在Arduino IDE中编程ESP32的功能。

现在,我需要指出的是,Arduino并不是唯一一个帮助您编译ESP32代码并将其烧录到微控制器的IDE。还有ESP-IDF,它是ESP32的官方开发框架,在配置选项方面提供了更大的灵活性。但是,它远不如Arduino IDE直观和用户友好,如果您刚开始使用ESP32,Arduino IDE是上手的理想选择。此外,由于庞大的开发者社区,为ESP32在Arduino中构建了大量的支持库,几乎ESP32的任何功能都可以通过Arduino IDE实现。ESP-IDF更适合那些需要将ESP32发挥到极致的高级和经验丰富的程序员。如果您是其中之一,您需要查找ESP-IDF入门指南。其他人可以继续。

安装步骤

现在,要在Arduino IDE中安装ESP32开发板,您需要按照以下步骤操作:

  • 确保您的机器上已安装Arduino IDE(最好是最新版本)

  • 打开Arduino,然后转到文件 -> 首选项

  • 在“附加开发板管理器网址”中,输入

https://dl.espressif.com/dl/package_esp32_index.json

如果您在首选项中已有JSON文件的网址(如果您已在IDE中安装了ESP8266、stm32duino或任何其他附加开发板,则很可能如此),您可以使用逗号将上述路径附加到现有路径。下面显示了ESP8266和ESP32开发板的示例:

http://arduino.esp8266.com/stable/package_esp8266com_index.json, https://dl.espressif.com/dl/package_esp32_index.json
Additional Boards Manager
  • 转到工具 -> 开发板 -> 开发板管理器。将打开一个弹出窗口。搜索ESP32并安装由Espressif Systems提供的esp32开发板。下图显示了已安装的开发板,因为我在准备本教程之前已经安装了该开发板。

  • ESP32 Installation

    验证安装

    安装ESP32开发板后,您可以通过转到工具 -> 开发板来验证安装。您可以在ESP32 Arduino部分看到许多开发板。选择您选择的开发板。如果您不确定哪个开发板最能代表您拥有的开发板,您可以选择ESP32 Dev Module

    ESP32 Board Selection

    接下来,使用USB线将您的开发板连接到您的机器。您应该在工具 -> 端口下看到一个额外的COM端口。选择该额外端口。如果您看到多个端口,您可以断开USB连接并查看哪个端口消失了。该端口对应于ESP32。

    确定端口后,从文件 -> 示例中选择任何一个示例草图。我们将从文件 -> 示例 -> 首选项 -> StartCounter中选择StartCounter示例。

    ESP32 Example

    打开该草图,编译它,然后通过单击上传按钮(编译按钮旁边的右箭头按钮)将其烧录到ESP32中。

    ESP32 Sketch Upload

    然后使用工具 -> 串口监视器打开串口监视器,或者只需按键盘上的Ctrl + Shift + M。您应该看到计数器值在每次ESP32重启后递增。

    ESP32 Serial Monitor

    恭喜您!您已设置好使用ESP32的环境。

    为双核和多线程操作设置RTOS

    ESP32的一个关键特性使其比其前身ESP8266更受欢迎,那就是芯片上存在两个内核。这意味着我们可以让两个进程在两个不同的内核上并行执行。当然,您可以争辩说,也可以使用FreeRTOS/任何其他等效的RTOS在一个线程上实现并行操作。但是,在一个内核上并行运行两个进程与在不同的内核上并行运行两个进程之间存在差异。在一个内核上,通常一个线程必须等待另一个线程暂停才能开始执行。在两个内核上,并行执行确实是并行的,因为它们实际上占据了不同的处理器。

    听起来很激动人心?让我们从一个真实的例子开始,演示如何创建两个任务并将它们分配给ESP32中的特定内核。

    代码演练

    GitHub链接:https://github.com/

    要在Arduino IDE中使用FreeRTOS,不需要额外的导入。它是内置的。我们需要做的是定义两个我们希望在两个内核上运行的函数。它们首先被定义。一个函数计算斐波那契数列的前25项,并打印其中每第5项。它在一个循环中这样做。第二个函数计算从1到100的数字之和。它也在一个循环中这样做。换句话说,在计算从1到100的和一次之后,它会在打印它正在执行的内核的ID之后再次这样做。我们没有打印所有数字,而只是打印两个序列中的每第5个数字,因为两个内核都会尝试访问同一个串口监视器。因此,如果我们打印每个数字,它们将频繁地同时尝试访问串口监视器。

    void print_fibonacci() {
       int n1 = 0;
       int n2 = 1;
       int term = 0;
       char print_buf[300];
       sprintf(print_buf, "Term %d: %d\n", term, n1);
       Serial.print(print_buf);
       term = term + 1;
       sprintf(print_buf, "Term %d: %d\n", term, n1);
       Serial.print(print_buf);
       for (;;) {
          term = term + 1;
          int n3 = n1 + n2;
          if(term%5 == 0){
          sprintf(print_buf, "Term %d: %d\n", term, n3);
          Serial.println(print_buf);
       }
       n1 = n2;
       n2 = n3;
    
       if (term >= 25) break;
       }
    }
    void sum_numbers() {
       int n1 = 1;
       int sum = 1;
       char print_buf[300];
       for (;;) {
          if(n1 %5 == 0){
             sprintf(print_buf, "                                                            Term %d: %d\n", n1, sum);
             Serial.println(print_buf);
          }
          n1 = n1 + 1;
          sum = sum+n1;
          if (n1 >= 100) break;
       }
    }
    void codeForTask1( void * parameter ) {
       for (;;) {
          Serial.print("Code is running on Core: ");Serial.println( xPortGetCoreID());
          print_fibonacci();
       }
    }
    void codeForTask2( void * parameter ) {
       for (;;) {
          Serial.print("                                                            Code is running on Core: ");Serial.println( xPortGetCoreID());
          sum_numbers();
       }
    }
    

    您可以看到上面我们已将任务2的打印语句向右移动。这将帮助我们区分任务1和任务2的打印。

    接下来,我们定义任务句柄。任务句柄用于在代码的其他部分引用该特定任务。由于我们有两个任务,我们将定义两个任务句柄。

    TaskHandle_t Task1, Task2;
    

    现在函数已准备就绪,我们可以转到setup部分。在setup()中,我们只需将两个任务固定到各自的内核。首先,让我向您展示代码片段。

    void setup() {
       Serial.begin(115200);
       /*Syntax for assigning task to a core:
       xTaskCreatePinnedToCore(
                        coreTask,   // Function to implement the task
                        "coreTask", // Name of the task 
                        10000,      // Stack size in words 
                        NULL,       // Task input parameter 
                        0,          // Priority of the task 
                        NULL,       // Task handle. 
                        taskCore);  // Core where the task should run 
       */
       xTaskCreatePinnedToCore(    codeForTask1,    "FibonacciTask",    5000,      NULL,    2,    &Task1,    0);
       //delay(500);  // needed to start-up task1
       xTaskCreatePinnedToCore(    codeForTask2,    "SumTask",    5000,    NULL,    2,    &Task2,    1);
    }
    

    现在让我们深入研究xTaskCreatePinnedToCore函数。如您所见,它总共接受7个参数。它们的描述如下。

    • 第一个参数codeForTask1是任务将执行的函数

    • 第二个参数 **"FibonacciTask"** 是该任务的标签或名称。

    • 第三个参数 **1000** 是分配给此任务的栈大小(以字节为单位)。

    • 第四个参数 **NULL** 是任务输入参数。基本上,如果您希望向任务输入任何参数,则将其放在此处。

    • 第五个参数 **1** 定义任务的优先级。值越高,任务的优先级越高。

    • 第六个参数 **&Task1** 是任务句柄。

    • 最后一个参数 **0** 是任务将运行在其上的内核代码。如果值为 0,则任务将在内核 0 上运行;如果值为 1,则任务将在内核 1 上运行。

    最后,循环可以留空,因为此处在两个内核上运行的两个任务更重要。

    void loop() {}
    

    您可以在串口监视器上看到输出。请注意,代码中没有任何延迟。因此,两个递增序列都表明计算是并行进行的。串口监视器上打印的内核 ID 也证实了这一点。

    Serial Monitor Output

    请注意,Arduino 草图默认情况下在内核 1 上运行。可以使用 **Serial.print( xPortGetCoreID());** 验证这一点。因此,如果您在 **loop()** 中添加一些代码,它将作为另一个线程在内核 1 上运行。在这种情况下,内核 0 将运行单个任务,而内核 1 将运行两个任务。

    ESP32与MPU6050接口

    加速度计和陀螺仪广泛用于工业物联网,用于测量各种机器的运行状况和运行参数。MPU6050 是一款流行的六轴加速度计+陀螺仪。它是一种 MEMS(微机电系统)传感器,这意味着它非常紧凑(从下图可以看出),并且在很宽的频率范围内也非常精确。

    MPU6050

    在本教程中,我们将了解如何将 ESP32 与 MPU6050 接口连接。在此过程中,您将学习 I2C(集成电路间通信)协议的使用方法,这将使您能够将 ESP32 与使用 I2C 协议进行通信的多个传感器和外围设备连接起来。本教程需要您的 ESP32、MPU6050 和几根跳线。

    将 MPU6050 与 ESP32 连接

    如下图所示,您需要将 MPU6050 的 SDA 线连接到 ESP32 的 21 引脚,SCL 线连接到 22 引脚,GND 连接到 GND,VCC 连接到 3V3 引脚。MPU6050 的其他引脚无需连接。

    MPU6050 Connection with ESP32

    代码演练

    GitHub 链接 − https://github.com/

    ESP32 和 Arduino 通常将 I2C 协议称为“Wire”。因此,所需的库导入是 Wire.h。

    #include<Wire.h>
    

    接下来,我们定义常量和全局变量。

    const int MPU_ADDR = 0x68; // I2C address of the MPU-6050
    int16_t AcX, AcY, AcZ, Tmp, GyX, GyY, GyZ;
    

    每个 I2C 设备都有一个固定的地址,其他设备使用该地址来识别它并与其通信。对于 MPU6050,该地址为 0x68。我们稍后将在初始化与 MPU6050 的 I2C 通信时使用它。接下来我们转到 setup 代码。

    void setup() {
       Serial.begin(115200);
       Wire.begin(21, 22, 100000); // sda, scl, clock speed
       Wire.beginTransmission(MPU_ADDR);
       Wire.write(0x6B);  // PWR_MGMT_1 register
       Wire.write(0);     // set to zero (wakes up the MPU−6050)
       Wire.endTransmission(true);
       Serial.println("Setup complete");
    }
    

    第一行很简单。我们正在以 115200 波特率启动与串口监视器的通信。接下来,我们开始 I2C 通信。为此,我们向 **Wire.begin()** 函数提供 3 个参数。

    这些是 SDA 和 SCL 引脚以及时钟速度。现在,I2C 通信需要两条线:数据线 (SDA) 和时钟线 (SCL)。在 ESP32 上,引脚 21 和 22 通常保留用于 I2C,其中 21 为 SDA,22 为 SCL。为了与 MPU6050 通信,我们有两个速度选项:100kbit/s 和 400kbit/s。我们在这里选择了 100kHz。如果您的用例需要,您也可以选择更高的速度选项。

    接下来,我们使用 **Wire.beginTransmission()** 命令指示 ESP32 我们想要与地址等于 MPU_ADDR 的芯片通信。此时,您可能已经猜到一个 ESP32 芯片可以与多个 I2C 外设通信。实际上,共有 128 个唯一的地址(地址字段为 7 位),因此 ESP32 可以使用 I2C 与 128 个不同的外设通信,前提是它们都具有不同的地址。

    在接下来的几行中,我们将 MPU6050 的 PWR_MGMT_1 寄存器设置为 0。这用于唤醒 MPU6050。PWR_MGMT_1 寄存器的地址 0x6B 是 MPU6050 内存中的地址。

    它与 MPU6050 的 I2C 地址无关。MPU 唤醒后,我们结束这段特定的 I2C 传输,我们的设置就完成了,我们使用 print 语句在串口监视器上指示这一点。现在让我们进入循环。您会注意到我们将布尔值 **true** 作为参数传递给 **Wire.endTransmission**。这告诉 ESP32 发送停止命令并释放 I2C 线路。如果我们将 true 替换为 false,则 ESP32 将发送重新启动而不是停止,保持连接活动。

    void loop() {
       Wire.beginTransmission(MPU_ADDR);
       Wire.write(0x3B);  // starting with register 0x3B (ACCEL_XOUT_H)
       Wire.endTransmission(true);
       Wire.beginTransmission(MPU_ADDR);
       Wire.requestFrom(MPU_ADDR, 14, true); // request a total of 14 registers
       AcX = Wire.read() −− 8 | Wire.read(); // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)
       AcY = Wire.read() −− 8 | Wire.read(); // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
       AcZ = Wire.read() −− 8 | Wire.read(); // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
       Tmp = Wire.read() −− 8 | Wire.read(); // 0x41 (TEMP_OUT_H) &  0x42 (TEMP_OUT_L)
       GyX = Wire.read() −− 8 | Wire.read(); // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)
       GyY = Wire.read() −− 8 | Wire.read(); // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
       GyZ = Wire.read() −− 8 | Wire.read(); // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
    
    
       Serial.print(AcX); Serial.print(" , ");
       Serial.print(AcY); Serial.print(" , ");
       Serial.print(AcZ); Serial.print(" , ");
       Serial.print(GyX); Serial.print(" , ");
       Serial.print(GyY); Serial.print(" , ");
       Serial.print(GyZ); Serial.print("\n");
    }
    

    在循环中,如果您扫描上面的代码片段,您会看到我们总共执行了两次传输。在第一次传输中,我们指示 MPU6050 我们想要从中开始读取数据的地址,或者更确切地说,将 MPU6050 的内部指针设置为此特定地址。在第二次传输中,我们告诉 MPU 我们请求从前面发送的地址开始的 14 个字节。然后我们逐个读取字节。您可能会注意到我们在读取结束时没有 **Wire.endTransmission(true)** 命令。这是因为 **Wire.requestFrom(MPU,14,true)** 的第三个参数指示 ESP32 在读取所需字节数后发送停止命令。如果我们传递 false 而不是 true,ESP32 将发送重新启动命令而不是停止命令。

    现在,您可能想知道如何确定哪个寄存器对应于哪个读数。答案是 MPU6050 寄存器映射。顾名思义,它提供了有关可以从哪个寄存器获得哪个值的信息。根据此映射,我们意识到我们理解 0x3B 和 0x3C 对应于 16 位 X 方向加速度值的较高和较低字节。接下来的两个寄存器 (0x3D 和 0x3E) 包含 16 位 Y 方向加速度值的较高和较低字节,依此类推。在加速度计和陀螺仪读数之间,有两个字节包含温度读数,我们读取并忽略它们,因为我们不需要它们。

    因此,通过这种方式,您可以成功地从 ESP32 上的 MPU6050 获取数据。恭喜!!继续下一个教程,学习如何从 ESP32 上的模拟传感器获取数据。

    参考文献

    将 ESP32 与模拟传感器接口连接

    您需要与 ESP32 接口连接的另一类重要传感器是模拟传感器。模拟传感器有很多类型,LDR(光敏电阻)、电流和电压传感器是流行的例子。现在,如果您熟悉任何 Arduino 板(如 Arduino Uno)上 analogRead 的工作原理,那么本章对您来说将是小菜一碟,因为 ESP32 使用相同的函数。您只需要注意一些细微之处,本章将介绍这些细微之处。

    关于模数转换 (ADC) 过程的简要说明

    每个支持 ADC 的微控制器都将具有定义的分辨率和参考电压。参考电压通常是电源电压。提供给 ADC 引脚的模拟电压应小于或等于参考电压。分辨率表示将用于表示数字值的位数。因此,如果分辨率为 8 位,则该值将由 8 位表示,并且可能的最大值为 255。此最大值对应于参考电压的值。其他电压的值通常是通过缩放得出的。

    因此,如果参考电压为 5V 且使用 8 位 ADC,则 5V 对应于 255 的读数,1V 对应于 (255/5*1) = 51 的读数,2V 对应于 (255/5*2) = 102 的读数,依此类推。如果我们有一个 12 位 ADC,则 5V 将对应于 4095 的读数,1V 将对应于 (4095/5*1) = 819 的读数,依此类推。

    反向计算可以类似地执行。如果您在参考电压为 3.3V 的 12 位 ADC 上得到 1000 的值,则它大约对应于 (1000/4095*3.3) = 0.8V 的值。如果您在参考电压为 5V 的 10 位 ADC 上得到 825 的读数,则它大约对应于 (825/1023*5) = 4.03V 的值。

    通过以上解释,很明显,用于 ADC 的参考电压和位数都决定了可以检测到的最小可能的电压变化。如果参考电压为 5V 且分辨率为 12 位,则您有 4095 个值来表示 0-5V 的电压范围。因此,可以检测到的最小变化为 5V/4095 = 1.2mV。类似地,对于 5V 和 8 位参考电压,您只有 255 个值来表示 0-5V 的范围。因此,可以检测到的最小变化为 5V/255 = 19.6mV,大约是 12 位分辨率检测到的最小变化的 16 倍。

    将 ADC 传感器与 ESP32 连接

    考虑到传感器的普及性和可用性,我们将使用 LDR 进行演示。我们将基本上将 LDR 与常规电阻串联连接,并将连接两个电阻的点的电压馈送到 ESP32 的 ADC 引脚。哪个引脚?好吧,有很多。ESP32 拥有 18 个 ADC 引脚(通道 1 中有 8 个,通道 2 中有 10 个)。但是,通道 2 引脚不能与 WiFi 一起使用。并且某些电路板上的通道 1 的某些引脚未暴露。因此,我通常坚持使用以下 6 个 ADC 引脚——32、33、34、35、36、39。在下图中,将 90K 电阻的 LDR 连接到 150K 电阻。LDR 的自由端连接到 ESP32 的 3.3V 引脚,电阻的自由端连接到 GND。LDR 和电阻的公共端馈送到 ESP32 的 ADC 引脚 36 (VN)。

    LDR Connection with ESP32

    代码演练

    GitHub 链接 − https://github.com/

    此处的代码很简单。不需要包含库。我们只需将 LDR 引脚定义为常量,在 setup() 中初始化串口,并设置 ADC 的分辨率。在这里,我们设置了 10 位的分辨率(这意味着最大值为 1023)。默认情况下,分辨率为 12 位,对于 ESP32,最小可能分辨率为 9 位。

    const int LDR_PIN = 36;
    
    void setup() {
       // put your setup code here, to run once:
       Serial.begin(115200);
       analogReadResolution(10); //default is 12. Can be set between 9-12.
    }
    

    在循环中,我们只需读取 LDR 引脚的值并将其打印到串口监视器。此外,我们将其转换为电压并打印相应的电压。

    void loop() {
       // put your main code here, to run repeatedly:
       // LDR Resistance: 90k ohms
       // Resistance in series: 150k ohms
       // Pinouts:
       // Vcc −> 3.3 (CONNECTED TO LDR FREE END)
       // Gnd −> Gnd (CONNECTED TO RESISTOR FREE END)
       // Analog Read −> Vp (36) − Intermediate between LDR and resistance. 
       int LDR_Reading = analogRead(LDR_PIN);
       float LDR_Voltage = ((float)LDR_Reading*3.3/1023);
       Serial.print("Reading: ");Serial.print(LDR_Reading); Serial.print("\t");Serial.print("Voltage: ");Serial.println(LDR_Voltage);
    }
    

    我们使用 1023 作为除数,因为我们将 ADC 分辨率设置为 10 位。如果您将 ADC 值更改为 N,则需要将除数更改为 (2^N −1)。现在将您的手放在 LDR 上

    我们使用 1023 作为除数,因为我们将 ADC 分辨率设置为 10 位。如果您将 ADC 值更改为 N,则需要将除数更改为 (2^N −1)。现在将您的手放在 LDR 上,看看对电压的影响,然后用火炬照射 LDR,看看串口监视器上电压的剧烈变化。就是这样。您已成功从 ESP32 上的模拟传感器捕获数据。

    参考文献

    ESP32 中的偏好设置

    非易失性存储器是嵌入式系统的重要需求。我们经常希望芯片即使在电源循环之间也能记住一些东西,例如设置变量、WiFi凭据等。如果每次设备进行电源重置后都必须执行设置或配置,那将非常不方便。ESP32有两种常用的非易失性存储方法:Preferences和SPIFFS。Preferences通常用于存储键值对,而SPIFFS(SPI Flash File System),顾名思义,用于存储文件和文档。本章,让我们关注Preferences。

    Preferences存储在主闪存的一个区域中,类型为data,子类型为nvsnvs代表非易失性存储器。默认情况下,为Preferences保留20 KB的空间,因此不要尝试在Preferences中存储大量庞大的数据。对于大量数据,请使用SPIFFS(SPIFFS默认保留1.5 MB的空间)。Preferences可以存储哪些类型的键值对?让我们通过示例代码来理解。

    代码演练

    我们将使用提供的示例代码。前往文件 -> 示例 -> Preferences -> StartCounter。它也可以在GitHub上找到。

    此代码记录ESP32重置的次数。因此,每次唤醒时,它都会从Preferences中获取现有计数,将其加1,并将更新后的计数保存回Preferences。然后它会重置ESP32。您可以通过ESP32上的打印语句看到计数的值在重置之间不会丢失,它确实是持久保存的。

    此代码有非常详细的注释,因此在很大程度上是不言自明的。尽管如此,让我们逐步浏览代码。

    我们首先包含Preferences库。

    #include <Preferences.h>

    接下来,我们创建一个Preferences类的对象。

    Preferences preferences;

    现在让我们逐行查看setup。我们首先初始化Serial。

    void setup() {
       Serial.begin(115200);
       Serial.println();
    

    接下来,我们使用命名空间打开Preferences。现在,将Preferences存储想象成一个银行储物柜室。有很多储物柜,你可以一次打开一个。命名空间就像储物柜的名称。在每个储物柜中,都有你可以访问的键值对。如果你的命名空间对应的储物柜不存在,则会创建它,然后你可以向该储物柜添加键值对。为什么会有不同的储物柜?为了避免名称冲突。假设你有一个使用Preferences存储凭据的WiFi库,还有一个也使用Preferences存储凭据的蓝牙库。假设这两个库是由不同的开发者开发的。如果两者都使用相同的键名credentials怎么办?这显然会造成很大的混乱。但是,如果两者都在不同的储物柜中存储其键,则根本不会出现任何混乱。

    // Open Preferences with my-app namespace. Each application module, library, etc
    // has to use a namespace name to prevent key name collisions. We will open storage in
    // RW-mode (second parameter has to be false).
    // Note: Namespace name is limited to 15 chars.
    preferences.begin("my−app", false);
    

    preferences.begin()的第二个参数false表示我们想要读取和写入这个储物柜。如果是true,我们只能读取储物柜,而不能写入。此外,注释中提到的命名空间长度不应超过15个字符。

    接下来,代码有一些注释掉的语句,您可以根据需要使用它们。一个可以让你清除储物柜,另一个可以帮助你从储物柜中删除特定的键值对(键为“counter”)。

    // Remove all preferences under the opened namespace
    //preferences.clear();
    
    // Or remove the counter key only
    //preferences.remove("counter");
    

    下一步,我们获取与键“counter”关联的值。现在,第一次运行此程序时,可能不存在这样的键。因此,我们还将0作为默认值传递给preferences.getUInt()函数。这告诉ESP32,如果键“counter”不存在,则创建一个新的键值对,键为“counter”,值为0。还要注意,我们使用getUInt是因为值是无符号整型。需要根据值的类型调用其他函数,例如getFloatgetString等。完整的选项列表可以在这里找到。

    unsigned int counter = preferences.getUInt("counter", 0);
    

    接下来,我们将此计数加1,并在串口监视器上打印出来。

    // Increase counter by 1
    counter++;
    
    // Print the counter to Serial Monitor
    Serial.printf("Current counter value: %u\n", counter);
    

    然后我们将此更新后的值存储回非易失性存储器。我们基本上是在更新键“counter”的值。下次ESP32读取键“counter”的值时,它将获得增量后的值。

    // Store the counter to the Preferences
    preferences.putUInt("counter", counter);
    

    最后,我们关闭Preferences储物柜并在10秒后重启ESP32。

    // Close the Preferences
    preferences.end();
    
    // Wait 10 seconds
    Serial.println("Restarting in 10 seconds...");
    delay(10000);
       
    // Restart ESP
    ESP.restart();
    }
    

    因为我们在进入循环之前重启了ESP32,所以循环从未执行。因此,它保持为空。

    void loop() {}
    
    Serial Monitor Output

    此示例很好地演示了ESP32 Preferences存储确实是持久保存的。当你在串口监视器上检查打印的语句时,你可以看到计数在连续重置之间递增。如果使用局部变量,则不会发生这种情况。只有通过Preferences使用非易失性存储才有可能。

    参考文献

    ESP32中的SPIFFS

    在上一章中,我们研究了Preferences作为一种在非易失性存储器中存储数据的方法,并了解了如何使用它们来存储键值对。本章,我们研究SPIFFS(SPI Flash File Storage),它用于以文件形式存储更大的数据。可以将SPIFFS想象成ESP32芯片本身上的一个非常小的SD卡。默认情况下,大约1.5 MB的板载闪存分配给SPIFFS。你可以通过工具 -> 分区方案来查看。

    ESP32 Partition Scheme

    您可以看到还有其他几种分区选项可用。但是,我们现在先不讨论这些。对于大多数应用程序,更改分区方案都是不需要的。本教程中的所有章节都可以与默认分区方案良好地配合使用。

    现在让我们看看使用示例创建、修改、读取和删除SPIFFS文件的过程。

    代码演练

    我们将再次使用提供的示例代码。前往文件 -> 示例 -> SPIFFS -> SPIFFS_Test。此代码非常适合理解SPIFFS可能执行的所有文件操作。它也可以在GitHub上找到。

    我们首先包含两个库:FS.h和SPIFFS.h。FS代表文件系统。

    #include "FS.h"
    #include "SPIFFS.h"
    

    接下来,你看到一个宏定义,FORMAT_SPIFFS_IF_FAILED。有一个相关的注释建议你只需要在第一次运行测试时格式化SPIFFS。这意味着你可以在第一次运行后将此宏的值设置为false。格式化SPIFFS需要时间,不需要每次运行代码时都执行。因此,人们通常的做法是为格式化SPIFFS编写单独的代码,在烧录主代码之前先烧录它。主代码不包含格式化命令。但是,在这个例子中,为了完整性,这个宏被保留为true。

    /* You only need to format SPIFFS the first time you run a
       test or else use the SPIFFS plugin to create a partition
       https://github.com/me−no−dev/arduino−esp32fs−plugin */
    #define FORMAT_SPIFFS_IF_FAILED true
    

    接下来,您可以看到为不同的文件系统操作定义了许多函数。它们是:

    • listDir - 列出所有目录

    • readFile - 读取特定文件

    • writeFile - 写入文件(这将覆盖文件中已存在的内容)

    • appendFile - 向文件追加内容(当你想添加现有内容而不是覆盖它时使用)

    • renameFile - 更改文件名

    • deleteFile - 删除文件

    void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
       Serial.printf("Listing directory: %s\r\n", dirname);
    
       File root = fs.open(dirname);
       if(!root){
          Serial.println("− failed to open directory");
          return;
       }
       if(!root.isDirectory()){
          Serial.println(" − not a directory");
          return;
       }
    
       File file = root.openNextFile();
       while(file){
          if(file.isDirectory()){
             Serial.print("  DIR : ");
             Serial.println(file.name());
             if(levels){
                listDir(fs, file.name(), levels -1);
             }
          } else {
             Serial.print("  FILE: ");
             Serial.print(file.name());
             Serial.print("\tSIZE: ");
             Serial.println(file.size());
          }
          file = root.openNextFile();
       }
    }
    
    void readFile(fs::FS &fs, const char * path){
       Serial.printf("Reading file: %s\r\n", path);
    
       File file = fs.open(path);
       if(!file || file.isDirectory()){
           Serial.println("− failed to open file for reading");
           return;
       }
    
       Serial.println("− read from file:");
       while(file.available()){
          Serial.write(file.read());
       }
    }
    
    void writeFile(fs::FS &fs, const char * path, const char * message){
       Serial.printf("Writing file: %s\r\n", path);
    
       File file = fs.open(path, FILE_WRITE);
       if(!file){
          Serial.println("− failed to open file for writing");
          return;
       }
       if(file.print(message)){
          Serial.println("− file written");
       }else {
          Serial.println("− frite failed");
       }
    }
    
    void appendFile(fs::FS &fs, const char * path, const char * message){
       Serial.printf("Appending to file: %s\r\n", path);
    
       File file = fs.open(path, FILE_APPEND);
       if(!file){
          Serial.println("− failed to open file for appending");
          return;
       }
       if(file.print(message)){
          Serial.println("− message appended");
       } else {
          Serial.println("− append failed");
       }
    }
    
    void renameFile(fs::FS &fs, const char * path1, const char * path2){
       Serial.printf("Renaming file %s to %s\r\n", path1, path2);
       if (fs.rename(path1, path2)) {
          Serial.println("− file renamed");
       } else {
          Serial.println("− rename failed");
       }
    }
    
    void deleteFile(fs::FS &fs, const char * path){
       Serial.printf("Deleting file: %s\r\n", path);
       if(fs.remove(path)){
          Serial.println("− file deleted");
       } else {
          Serial.println("− delete failed");
       }
    }
    

    请注意,以上所有函数都没有请求文件名。它们请求的是完整的路径。因为这是一个文件系统。你可能有目录、子目录以及这些子目录中的文件。因此,ESP32需要知道你要操作的文件的完整路径。

    接下来是一个不完全是文件操作函数的函数-testFileIO。这更像是一个时间基准测试函数。它执行以下操作:

    • 将大约1 MB(2048 * 512字节)的数据写入你提供的文件路径并测量写入时间

    • 读取相同的文件并测量读取时间

    void testFileIO(fs::FS &fs, const char * path){
       Serial.printf("Testing file I/O with %s\r\n", path);
    
       static uint8_t buf[512];
       size_t len = 0;
       File file = fs.open(path, FILE_WRITE);
       if(!file){
          Serial.println("− failed to open file for writing");
          return;
       }
    
       size_t i;
       Serial.print("− writing" );
       uint32_t start = millis();
       for(i=0; i<2048; i++){
          if ((i & 0x001F) == 0x001F){
             Serial.print(".");
          }
          file.write(buf, 512);
       }
       Serial.println("");
       uint32_t end = millis() − start;
       Serial.printf(" − %u bytes written in %u ms\r\n", 2048 * 512, end);
       file.close();
    
       file = fs.open(path);
       start = millis();
       end = start;
       i = 0;
       if(file && !file.isDirectory()){
          len = file.size();
             size_t flen = len;
             start = millis();
             Serial.print("− reading" );
             while(len){
                size_t toRead = len;
                if(toRead > 512){
                    toRead = 512;
                }
                file.read(buf, toRead);
                if ((i++ & 0x001F) == 0x001F){
                  Serial.print(".");
                }
                len −= toRead;
             }
          Serial.println("");
          end = millis() - start;
          Serial.printf("- %u bytes read in %u ms\r\n", flen, end);
          file.close();
       } else {
          Serial.println("- failed to open file for reading");
       }
    }
    

    请注意,buf数组从未初始化为任何值。我们很可能会将垃圾字节写入文件。这没关系,因为该函数的目的是测量写入时间和读取时间。

    定义函数后,我们继续进行设置,其中显示了每个函数的调用。

    void setup(){
       Serial.begin(115200);
       if(!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)){
          Serial.println("SPIFFS Mount Failed");
          return;
       }
       listDir(SPIFFS, "/", 0);
       writeFile(SPIFFS, "/hello.txt", "Hello ");
       appendFile(SPIFFS, "/hello.txt", "World!\r\n");
       readFile(SPIFFS, "/hello.txt");
       renameFile(SPIFFS, "/hello.txt", "/foo.txt");
       readFile(SPIFFS, "/foo.txt");
       deleteFile(SPIFFS, "/foo.txt");
       testFileIO(SPIFFS, "/test.txt");
       deleteFile(SPIFFS, "/test.txt");
       Serial.println( "Test complete" );
    }
    

    设置主要执行以下操作:

    • 它首先使用SPIFFS.begin()初始化SPIFFS。这里使用了开头定义的宏。为true时,它格式化SPIFFS(耗时);为false时,它在不进行格式化的情况下初始化SPIFFS。

    • 然后它列出根级别上的所有目录。请注意,我们已将级别指定为0。因此,我们没有列出目录中的子目录。你可以通过递增levels参数来增加嵌套。

    • 然后它将“Hello”写入根目录中的hello.txt文件。(如果文件不存在,则会创建它)

    • 然后它读取hello.txt。

    • 然后它将hello.txt重命名为foo.txt。

    • 然后它读取foo.txt以查看重命名是否成功。你应该看到打印出“Hello”,因为这就是存储在文件中的内容。

    • 然后它删除foo.txt。

    • 然后它在新的文件test.txt上执行testFileIO例程。

    • 例程执行完毕后,它删除test.txt。

    就是这样。此示例代码很好地列出了并测试了您可能想要与SPIFFS一起使用的所有函数。您可以继续修改此代码,并尝试不同的函数。

    由于我们不想在这里执行任何重复活动,因此循环为空。

    void loop(){
    }
    

    串口监视器中显示的输出可能如下面的图像所示:

    ESP32 SPIFFS Sketch Output

    注意 - 如果在运行草图时出现“SPIFFS Mount Failed”,请将FORMAT_SPIFFS_IF_FAILED的值设置为false,然后重试。

    参考文献

    ESP32与OLED显示屏接口

    OLED与ESP32的组合非常流行,以至于有些ESP32开发板集成了OLED。但是,我们将假设您将使用单独的OLED模块与您的ESP32开发板一起使用。如果您有OLED模块,它可能看起来像下面的图像。

    OLED Module

    将OLED显示模块连接到ESP32

    与我们在上一章中讨论的MPU6050模块一样,OLED模块通常也使用I2C进行通信。因此,连接将类似于MPU6050模块。您需要将SDA线连接到ESP32上的21引脚,SCL线连接到22引脚,GND连接到GND,VCC连接到3V3引脚。

    OLED Module with ESP32

    OLED显示器的库

    有许多库可用于将OLED显示器与ESP32连接。您可以随意使用任何您感到舒适的库。在本例中,我们将使用ThingPulse和Fabrice Weinberg的“ESP8266和ESP32 OLED驱动程序,用于SSD1306显示器”。您可以从工具 -> 管理库中安装此库。它也可以在GitHub上找到。

    OLED Module Library

    代码演练

    由于我们刚刚安装的库,代码变得非常简单。我们将运行一个计数器代码,它将计算自上次重置以来的秒数,并在OLED模块上打印出来。代码可以在GitHub上找到。

    我们首先包含SSD1306库。

    #include "SSD1306Wire.h"
    

    接下来,我们定义OLED引脚及其I2C地址。请注意,有些OLED模块包含额外的复位引脚。一个很好的例子是ESP32 TTGO开发板,它带有内置的OLED显示屏。对于该开发板,引脚16是复位引脚。如果您将外部OLED模块连接到ESP32,则很可能不会使用复位引脚。0x3c的I2C地址通常对所有OLED模块都适用。

    //OLED related variables
    #define OLED_ADDR   0x3c
    #define OLED_SDA    21//4     //TTGO board without SD Card has OLED SDA connected to pin 4 of ESP32
    #define OLED_SCL    22//15    //TTGO board without SD Card has OLED SCL connected to pin 15 of ESP32
    #define OLED_RST    16        //Optional, TTGO board contains OLED_RST connected to pin 16 of ESP32
    

    接下来,我们创建OLED显示对象和计数器变量。

    SSD1306Wire  display(OLED_ADDR, OLED_SDA, OLED_SCL); 
    int counter = 0;
    

    之后,我们定义两个函数。一个用于初始化OLED显示屏(如果您的OLED模块不包含复位引脚,则此函数是冗余的),另一个用于在OLED显示屏上打印文本消息。showOLEDMessage()函数将OLED显示区域划分为3行,并要求提供3个字符串,每行一个。

    void initOLED() {
       pinMode(OLED_RST, OUTPUT);
       //Give a low to high pulse to the OLED display to reset it
       //This is optional and not required for OLED modules not containing a reset pin
       digitalWrite(OLED_RST, LOW);
       delay(20);
       digitalWrite(OLED_RST, HIGH);
    }
    void showOLEDMessage(String line1, String line2, String line3) {
       display.init();                        // clears screen
       display.setFont(ArialMT_Plain_16);
       display.drawString(0, 0, line1);       //  adds to buffer
       display.drawString(0, 20, line2);
       display.drawString(0, 40, line3);
       display.display();                     // displays content in buffer
    }
    

    最后,在setup函数中,我们只需初始化OLED显示屏;在loop函数中,我们只需使用显示屏的前两行来显示计数器。

    void setup() {
       // put your setup code here, to run once:
       initOLED();
    }
    void loop() {
       // put your main code here, to run repeatedly
       showOLEDMessage("Num seconds is: ", String(counter), "");
       delay(1000);
       counter = counter+1;
    }
    

    就是这样。恭喜您在OLED显示屏上显示了您的第一个文本语句。

    ESP32上的WiFi

    WiFi堆栈的可用性是ESP32与其他微控制器之间主要区别之一。本章将简要概述ESP32上可用的各种WiFi模式。后续章节将介绍使用HTTP、HTTPS和MQTT通过WiFi传输数据。ESP32上可以配置WiFi的3种主要模式:

    • 站模式(Station Mode)− 这类似于WiFi客户端模式。ESP32连接到可用的WiFi网络,该网络又连接到您的互联网。这与将您的手机连接到可用的WiFi网络完全相同。

    • 接入点模式(Access Point Mode)− 这相当于打开手机上的热点,以便其他设备可以连接到它。类似地,ESP32在其周围创建一个WiFi网络,其他设备可以连接到它。但是,ESP32本身没有互联网访问权限。因此,使用此模式,您通常只能显示硬编码到ESP32内存中的几个网页。此模式通常用于安装期间执行设备设置。例如,您将ESP32带到一个未知的客户端站点,事先不知道其WiFi凭据。您将对ESP32进行编程以接入点模式开始运行。一旦您的手机连接到ESP32创建的WiFi网络,就会打开一个页面(Captive Portal),并提示您输入WiFi凭据。输入这些凭据后,ESP32将切换到站模式,并尝试使用提供的凭据连接到可用的WiFi网络。

    • 组合AP-STA模式(Combined AP-STA mode)− 正如您可能猜到的那样,在此模式下,ESP32连接到现有的WiFi网络,同时它也创建了自己的网络,其他设备可以连接到它。

    大多数情况下,您将以站模式使用ESP32。在接下来的3章中,我们也将以站模式使用ESP32。但是,您也应该了解AP模式,并鼓励您自己探索AP模式的示例。

    使用HTTP通过WiFi传输数据

    HTTP(超文本传输协议)是最常见的通信形式之一,使用ESP32,我们可以使用HTTP请求与任何Web服务器交互。让我们在本节了解一下。

    关于HTTP请求的简要介绍

    HTTP请求发生在客户端和服务器之间。顾名思义,服务器根据请求向客户端“提供”信息。Web服务器通常提供网页。例如,当您在Internet浏览器中键入https://www.linkedin.com/login时,您的PC或笔记本电脑充当客户端,并向托管linkedin.com的服务器请求与/login地址对应的页面。您将获得一个HTML页面作为返回,然后由您的浏览器显示。

    HTTP遵循请求-响应模型,这意味着通信始终由客户端发起。服务器不能毫无征兆地与任何客户端通信,也不能与任何客户端启动通信。通信始终必须由客户端以请求的形式发起,服务器只能响应该请求。服务器的响应包含状态代码(记得404吗?这是一个状态代码),以及(如果适用)请求的内容。所有状态代码的列表可以在这里找到这里

    那么,服务器如何识别HTTP请求呢?通过请求的结构。HTTP请求遵循固定的结构,该结构由3部分组成:

    • 请求行后跟回车换行符(CRLF = \r\n)

    • 零个或多个标题行后跟CRLF和一个空行,再次后跟CRLF

    • 可选正文

    典型的HTTP请求如下所示:

    POST / HTTP/1.1         //Request line, containing request method (POST in this case)
    Host: www.example.com   //Headers
                            //Empty line between headers
    key1=value1&key2=value2   //Body	
    

    服务器响应如下所示:

    HTTP/1.1 200 OK                     //Response line; 200 is the status code
    Date: Mon, 23 May 2005 22:38:34 GMT //Headers
    Content-Type: text/html; charset=UTF-8
    Content-Length: 155
    Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
    Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
    ETag: "3f80f−1b6−3e1cb03b"
    Accept-Ranges: bytes
    Connection: close
                                        //Empty line between headers and body
    <html>						
      <head>
        <title>An Example Page</title>
      </head>
      <body>
        <p>Hello World, this is a very simple HTML document.</p>
      </body>
    </html>
    

    事实上,TutorialsPoint上有一个关于HTTP请求结构的非常好的教程。它还介绍了各种请求方法(GET、POST、PUT等)。在本节中,我们将关注GET和POST方法。

    GET请求包含所有参数,这些参数以请求URL本身中的键值对形式存在。例如,如果使用GET而不是POST发送上面的相同示例请求,它将如下所示:

    GET /test/demo_form.php?key1=value1&key2=value2 HTTP/1.1   //Request line
    Host: www.example.com                                     //Headers	
                                                              //No need for a body
    

    正如您现在所猜到的那样,POST请求包含在正文中的参数,而不是URL。GET和POST之间还有其他一些区别,您可以在这里阅读。但关键是,您将使用POST与服务器共享敏感信息,例如密码。

    代码演练

    在本节中,我们将从头开始编写我们的HTTP请求。有像httpClient这样的库专门用于处理ESP32 HTTP请求,这些库负责构建HTTP请求,但我们将自己构建请求。这给了我们更大的灵活性。在本教程中,我们将限制在ESP32客户端模式。ESP32也可以使用HTTP服务器模式,但这留给您去探索。

    我们将使用httpbin.org作为我们的服务器。它基本上是为测试您的HTTP请求而构建的。您可以使用此服务器测试GET、POST和各种其他方法。参见此处

    代码可以在GitHub上找到。

    我们首先包含WiFi库。

    #include <WiFi.h>
    

    接下来,我们将定义一些常量。对于HTTP,使用的端口是80。这是标准的。同样,我们对HTTPS使用443,对FTP使用21,对DNS使用53,等等。这些是保留的端口号。

    const char* ssid = "YOUR_SSID";
    const char* password = "YOUR_PASSWORD";
    
    const char* server = "httpbin.org";
    const int port = 80;
    

    最后,我们创建我们的WiFiClient对象。

    WiFiClient client
    

    在setup函数中,我们只需使用提供的凭据以站模式连接到WiFi。

    void setup() {
       Serial.begin(115200);
       WiFi.mode(WIFI_STA);          //The WiFi is in station mode. The other is the softAP mode
       WiFi.begin(ssid, password);
       while (WiFi.status() != WL_CONNECTED) {
          delay(500);
          Serial.print(".");
       }
       Serial.println("");  Serial.print("WiFi connected to: "); Serial.println(ssid);  Serial.println("IP address: ");  Serial.println(WiFi.localIP());
       delay(2000);
    }
    

    loop函数在这里变得很重要。HTTP请求就是在那里执行的。我们首先读取ESP32的芯片ID。我们将将其作为参数与我们的名称一起发送到服务器。我们将使用这些参数构造HTTP请求的正文。

    void loop() {
       int  conn;
       int chip_id = ESP.getEfuseMac();;
       Serial.printf("  Flash Chip id = %08X\t", chip_id);
       Serial.println();
       Serial.println();
       String body = "ChipId=" + String(chip_id) + "&SentBy=" + "your_name";
       int body_len = body.length();
    

    请注意SentBy字段之前的&&用作HTTP请求中不同键值对之间的分隔符。接下来,我们连接到服务器。

    Serial.println(".....");
    Serial.println(); Serial.print("For sending parameters, connecting to ");      Serial.println(server);
    conn = client.connect(server, port);
    

    POST请求

    如果我们的连接成功,client.connect()将返回1。我们在发出请求之前检查这一点。

    if (conn == 1)  {
       Serial.println(); Serial.print("Sending Parameters...");
       //Request
       client.println("POST /post HTTP/1.1");
       //Headers
       client.print("Host: "); client.println(server);
       client.println("Content-Type: application/x−www−form−urlencoded");
       client.print("Content-Length: "); client.println(body_len);
       client.println("Connection: Close");
       client.println();
       //Body
       client.println(body);
       client.println();
    
       //Wait for server response
       while (client.available() == 0);
    
       //Print Server Response
       while (client.available()) {
          char c = client.read();
          Serial.write(c);
       }
    } else {
       client.stop();
       Serial.println("Connection Failed");
    }
    

    正如您所看到的,我们使用client.print()client.println()发送我们的请求行。请求、标头和正文通过注释清楚地指示。在请求行中,POST /post HTTP/1.1等效于POST http://httpbin.org/post HTTP/1.1。由于我们已经在client.connect(server,port)中提到了服务器,因此可以理解/post指的是服务器/post URL。

    特别是对于POST请求,Content-Length标头非常重要。如果没有它,许多服务器会假设内容长度为0,这意味着没有正文。Content-Type已保留为application/x−www−form−urlencoded,因为我们的正文表示表单数据。在典型的表单提交中,您将拥有Name、Address等键以及相应的值。您可以拥有其他几种内容类型。有关完整列表,请参见此处

    Connection: Close标头告诉服务器在请求处理完毕后关闭连接。如果您希望在请求处理完毕后保持连接,则可以改用Connection: Keep-Alive

    这些只是我们可以包含的一些标头。HTTP标头的完整列表可以在此处找到。

    现在,httpbin.org/post URL通常只会回显我们的正文。示例响应如下:

    HTTP/1.1 200 OK
    Date: Sat, 21 Nov 2020 16:25:47 GMT
    Content−Type: application/json
    Content−Length: 402
    Connection: close
    Server: gunicorn/19.9.0
    Access−Control−Allow−Origin: *
    Access−Control−Allow−Credentials: true
    {
       "args": {}, 
       "data": "", 
       "files": {}, 
       "form": {
          "ChipId": "1780326616", 
          "SentBy": "Yash"
       }, 
       "headers": {
          "Content−Length": "34", 
          "Content−Type": "application/x−www−form−urlencoded", 
          "Host": "httpbin.org", 
          "X-Amzn−Trace−Id": "Root=1−5fb93f8b−574bfb57002c108a1d7958bb"
       }, 
       "json": null, 
       "origin": "183.87.63.113", 
       "url": "http://httpbin.org/post"
    }
    
    Post Request Response

    正如您所看到的,“form”字段中回显了POST正文的内容。您应该在串行监视器上看到类似以上内容的输出。还要注意URL字段。它清楚地表明请求行中的/post地址被解释为http://httpbin.org/post。

    最后,我们将等待5秒钟,然后结束循环,从而再次发出请求。

      delay(5000);
    }
    

    GET请求

    此时,您可能想知道,将此POST请求转换为GET请求需要进行哪些更改。实际上这非常简单。首先,您将调用/get地址而不是/post地址。然后,您将在问号(?)后将正文内容附加到URL。最后,您将方法替换为GET。此外,不再需要Content-Length和Content−Type标头,因为您的正文为空。因此,您的请求块将如下所示:

    if (conn == 1) {
       String path = String("/get") + String("?") +body;
       Serial.println(); Serial.print("Sending Parameters...");
       //Request
       client.println("GET "+path+" HTTP/1.1");
       //Headers
       client.print("Host: "); client.println(server);
       client.println("Connection: Close");
       client.println();
       //No Body
    
       //Wait for server response
       while (client.available() == 0);
    
       //Print Server Response
       while (client.available()) {
          char c = client.read();
          Serial.write(c);
       }
    } else {
       client.stop();
       Serial.println("Connection Failed");
    }
    

    相应的响应将如下所示:

    HTTP/1.1 200 OK
    Date: Tue, 17 Nov 2020 18:05:34 GMT
    Content-Type: application/json
    Content-Length: 497
    Connection: close
    Server: gunicorn/19.9.0
    Access-Control−Allow−Origin: *
    Access-Control-Allow-Credentials: true
    
    {
       "args": {
          "ChipID": "3F:A0:A1:77:0D:84", 
          "SentBy": "Yash"
       }, 
       "headers": {
          "Accept": "*/*", 
          "Accept-Encoding": "deflate, gzip", 
          "Host": "httpbin.org", 
          "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", 
          "X−Amzn−Trace−Id": "Root=1−5fb410ee−3630963b0b7980c959c34038"
       }, 
       "origin": "206.189.180.4", 
       "url": "https://httpbin.org/get?ChipID=3F:A0:A1:77:0D:84&SentBy=Yash"
    }
    
    GET request response

    正如您所看到的,发送到服务器的参数现在返回在args字段中,因为它们作为参数发送到URL本身。

    恭喜!!您已成功使用ESP32发送HTTP请求。

    参考文献

    使用HTTPS通过WiFi传输数据

    在上一章中,我们探讨了使用 ESP32 通过 HTTP 传输数据。本章,我们将学习通过 HTTPS 传输数据。HTTPS 中的“S”代表“安全”。基本上,您传输的任何数据都会使用传输层安全协议 (TLS) 进行加密。这意味着,如果有人窃听您的通信,他们将无法理解您传输的内容。他们看到的将是一些乱码。本章不涵盖 HTTPS 的工作原理。但只需简单的 Google 搜索即可找到多个有用的资源供您入门。本章,我们将学习如何在 ESP32 上实现 HTTPS。

    将任何 HTTP 请求转换为 ESP32 上的 HTTPS

    一般来说,如果您有发送 HTTP 请求到服务器的代码,您可以按照以下简单的步骤将其转换为 HTTPS:

    • 将库从 WiFiClient 更改为 WiFiClientSecure(您需要包含 WiFiClientSecure.h)

    • 将端口从 80 更改为 443

    可选的第四步:为服务器添加 CA 证书。此步骤是可选的,因为它不会影响通信的安全。它只是确保您正在与正确的服务器通信。如果您不提供 CA 证书,您的通信仍然是安全的。

    代码演练

    您看到的以下代码与用于 HTTP 通信的代码非常相似。强烈建议您重新阅读上一章。在本演练中,我们将仅重点介绍与 HTTP 代码不同的部分。

    代码可在 GitHub 上找到

    我们首先包含 WiFi 库。我们还需要在此处包含 WiFiClientSecure 库。

    #include <WiFi.h>
    #include <WiFiClientSecure.h>
    

    接下来,我们将定义常量。请注意,端口现在是 443 而不是 80。

    const char* ssid = "YOUR_SSID";
    const char* password = "YOUR_PASSWORD";
    
    const char* server = "httpbin.org";
    const int port = 443;
    

    接下来,我们将创建 WiFiClientSecure 对象,而不是 WiFiClient 对象。

    WiFiClientSecure client;
    

    接下来,我们为我们的服务器 (httpbin.org) 定义 CA 证书。现在,您可能想知道如何获取我们服务器的 CA 证书。此处 提供了使用 Google Chrome 获取任何服务器 CA 证书的详细步骤。在同一篇文章中,还提供了一份关于 CA 证书有效性的说明,建议使用证书颁发机构的证书,而不是服务器的证书,尤其是在您只编程设备一次并将其用于现场多年应用的场景中。证书颁发机构的证书具有更长的有效期(15年以上),而服务器证书的有效期较短(1-2年)。因此,我们使用 Starfield Class 2 证书颁发机构的证书(有效期至 2034 年),而不是 httpbin.org 的证书(有效期至 2021 年 2 月)。

    CA Certificate
    const char* ca_cert = \ 
    "-----BEGIN CERTIFICATE-----\n" \
    "MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl\n"\
    "MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp\n"\
    "U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw\n"\
    "NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE\n"\
    "ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp\n"\
    "ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3\n"\
    "DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf\n"\
    "8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN\n"\
    "+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0\n"\
    "X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa\n"\
    "K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA\n"\
    "1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G\n"\
    "A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR\n"\
    "zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0\n"\
    "YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD\n"\
    "bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w\n"\
    "DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3\n"\
    "L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D\n"\
    "eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl\n"\
    "xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp\n"\
    "VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY\n"\
    "WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=\n"\
    "-----END CERTIFICATE-----\n";
    

    在设置中,我们像以前一样使用提供的凭据以站模式连接到 WiFi。在这里,我们还有额外的步骤来为我们的 WiFiSecureClient 设置 CA 证书。通过这样做,我们告诉客户端只有在它的 CA 证书与提供的证书匹配时才与服务器通信。

    void setup() {
       Serial.begin(115200);
       WiFi.mode(WIFI_STA);                    //The WiFi is in station mode. The    other is the softAP mode
       WiFi.begin(ssid, password);
       while (WiFi.status() != WL_CONNECTED) {
          delay(500);
          Serial.print(".");
       }
       Serial.println("");  Serial.print("WiFi connected to: "); Serial.println(ssid);  Serial.println("IP address: ");  Serial.println(WiFi.localIP());
       client.setCACert(ca_cert);            //Only communicate with the server if the CA certificates match
       delay(2000);
    }
    

    循环与 HTTP 示例中使用的循环完全相同。

    void loop() {
       int  conn;
       int chip_id = ESP.getEfuseMac();;
       Serial.printf("  Flash Chip id = %08X\t", chip_id);
       Serial.println();
       Serial.println();
       String body = "ChipId=" + String(chip_id) + "&SentBy=" + "your_name";
       int body_len = body.length();
    
       Serial.println(".....");
       Serial.println(); Serial.print("For sending parameters, connecting to "); Serial.println(server);
       conn = client.connect(server, port);
    
       if (conn == 1) {
          Serial.println(); Serial.print("Sending Parameters...");
          //Request
          client.println("POST /post HTTP/1.1");
          //Headers
          client.print("Host: "); client.println(server);
          client.println("Content-Type: application/x−www−form−urlencoded");
          client.print("Content-Length: "); client.println(body_len);
          client.println("Connection: Close");
          client.println();
          //Body
          client.println(body);
          client.println();
    
          //Wait for server response
          while (client.available() == 0);
    
          //Print Server Response
          while (client.available()) {
             char c = client.read();
             Serial.write(c);
          }
       } else {
          client.stop();
          Serial.println("Connection Failed");
       }
       delay(5000);
    }
    

    服务器应返回的响应也与 HTTP 示例类似。唯一的区别是收到的响应也将是安全的。但我们不必担心解密加密的消息。ESP32 会为我们完成。

    HTTPS Server response

    注意服务器响应中的 URL 字段。它包含 https 而不是 http,这确认我们的传输是安全的。事实上,如果您稍微修改 CA 证书,例如删除一个字符,然后尝试运行草图,您将看到连接失败。

    HTTPS Server Failed Connection

    但是,如果您从设置中删除了`client.setCACert()`行,即使使用有问题的 CA 证书,连接也将再次安全地建立。这证明设置 CA 证书不会影响我们通信的安全。它只是帮助我们验证我们正在与正确的服务器通信。如果我们设置证书,那么 ESP32 将不会与服务器通信,除非提供的 CA 证书与服务器的 CA 证书匹配。如果我们不设置证书,ESP32 仍然可以安全地与服务器通信。

    恭喜!!您已成功使用 ESP32 发送 HTTPS 请求。

    注意 - ESP32 上执行 HTTPS 消息加密的硬件加速器最多支持 16384 字节(16 KB)的数据。因此,如果您的消息大小超过 16 KB,您可能需要将其分解成块。

    参考文献

    使用MQTT通过WiFi传输数据

    MQTT(消息队列遥测传输)在物联网设备领域获得了广泛的关注。它是一种通常在 TCP/IP 上运行的协议。MQTT 使用代理-客户端模型,而不是我们看到的 HTTP 的服务器-客户端模型。维基百科将 MQTT 代理和客户端定义为:

    MQTT 代理是一个服务器,它接收来自客户端的所有消息,然后将消息路由到相应的目标客户端。MQTT 客户端是任何运行 MQTT 库并通过网络连接到 MQTT 代理的设备(从微控制器到成熟的服务器)。

    将代理想象成像 Medium 这样的服务。主题将是 Medium 的出版物,客户端将是 Medium 的用户。用户(客户端)可以发布到出版物,订阅该出版物(主题)的另一个用户(客户端)将被告知有新的帖子可供阅读。到目前为止,您应该已经了解了 HTTP 和 MQTT 之间的一个主要区别。在 HTTP 中,您的消息直接发送到目标服务器,您甚至会以状态码的形式获得确认。在 MQTT 中,您只是将消息发送到代理,希望您的目标服务器会从中获取。如果您资源受限,MQTT 的几个特性将成为一大优势。它们列在下面:

    • 使用 MQTT,报头开销非常短,吞吐量很高。这有助于节省时间和电池电量。

    • MQTT 以字节数组的形式发送信息,而不是文本格式。这使消息更轻量级。

    • 因为 MQTT 不依赖于服务器的响应,所以客户端是独立的,一旦传输消息,就可以进入休眠状态(节省电池电量)。

    这些只是导致 MQTT 受欢迎的一些方面。您可以此处 获取 MQTT 和 HTTP 之间更详细的比较。

    代码演练

    一般来说,测试 MQTT 需要您注册一个免费/付费代理帐户。AWS IoT 和 Azure IoT 是非常流行的提供 MQTT 代理服务的平台,但它们需要冗长的注册和配置过程。幸运的是,有一个来自 HiveMQ 的免费代理服务,可用于在无需注册或配置的情况下测试 MQTT。它非常适合那些刚接触 MQTT 并只想动手操作的人,还可以让您更专注于 ESP32 的固件。因此,我们将使用本章中的代理。当然,因为它是一项免费服务,所以会有局限性。您不能共享敏感信息,因为所有消息都是公开的,任何人都可以订阅您的主题。当然,出于测试目的,这些限制无关紧要。

    代码可在 GitHub 上找到

    我们将使用 PubSubClient 库。您可以从工具 -> 管理库中安装它。

    PubSubClient Library Install

    安装库后,我们将包含 WiFi 和 PubSubClient 库。

    #include <WiFi.h>
    #include <PubSubClient.h>
    

    接下来,我们将定义一些常量。请记住替换 WiFi 凭据。mqttServer 和 mqttPort 是 http://www.mqtt−dashboard.com/ 规定的。mqtt_client_name、mqtt_pub_topic 和 mqtt_sub_topic 可以是您选择的任何字符串。只需确保更改它们的值。如果多个用户从本教程复制相同的代码,您将在测试时收到来自未知客户端的大量消息。

    我们还定义了 WiFiClient 和 mqttClient 对象。MQTTClient 对象需要网络客户端作为参数。如果您使用以太网,则会提供以太网客户端作为参数。由于我们使用的是 WiFi,因此我们提供了 WiFi 客户端作为参数。

    const char* ssid = "YOUR_SSID";
    const char* password = "YOUR_PASSWORD";
    
    //The broker and port are provided by http://www.mqtt−dashboard.com/
    char *mqttServer = "broker.hivemq.com";
    int mqttPort = 1883;
    
    //Replace these 3 with the strings of your choice
    const char* mqtt_client_name = "ESPYS2111";
    const char* mqtt_pub_topic = "/ys/testpub"; //The topic to which our client will publish
    const char* mqtt_sub_topic = "/ys/testsub"; //The topic to which our client will subscribe
    
    WiFiClient client;
    PubSubClient mqttClient(client);
    

    接下来,我们定义回调函数。回调函数是一个中断函数。每当从已订阅的主题接收到新消息时,都会触发此函数。它有三个参数:接收消息的主题、作为字节数组的消息以及消息的长度。您可以对该消息执行任何操作(将其存储在 SPIFFS 中,将其发送到另一个主题,等等)。在这里,我们只是打印主题和消息。

    void callback(char* topic, byte* payload, unsigned int length) {
       Serial.print("Message received from: "); Serial.println(topic);
       for (int i = 0; i < length; i++) {
          Serial.print((char)payload[i]);
       }
       Serial.println();
       Serial.println();
    }
    

    在设置中,我们像在其他每个草图中一样连接到 WiFi。最后两行与 MQTT 有关。我们设置 MQTT 的服务器和端口以及回调函数。

    void setup() {
       // put your setup code here, to run once:
       Serial.begin(115200);
       WiFi.mode(WIFI_STA);                    //The WiFi is in station mode
       WiFi.begin(ssid, password);
       while (WiFi.status() != WL_CONNECTED) {
          delay(500);
          Serial.print(".");
       }
       Serial.println("");  Serial.print("WiFi connected to: "); Serial.println(ssid);  Serial.println("IP address: ");     Serial.println(WiFi.localIP());
       delay(2000);
       mqttClient.setServer(mqttServer, mqttPort);
       mqttClient.setCallback(callback);
    }
    

    在循环中,我们执行以下操作

    • 如果客户端未连接到代理,我们将使用我们的客户端名称连接它。

    • 连接后,我们还将客户端订阅到 mqtt_sub_topic。

    • 然后,我们将消息发布到 mqtt_pub_topic

    • 然后我们运行`mqttClient.loop()`。此 loop() 函数应定期调用。它维护客户端与代理的连接,并帮助客户端处理传入的消息。如果您没有此`mqttClient.loop()`行,您将能够发布到 mqtt_pub_topic,但不会从 mqtt_sub_topic 获取消息,因为只有在此行被调用时才会处理传入的消息。

    • 最后,我们等待 5 秒,然后再开始此循环。

    void loop() {
       // put your main code here, to run repeatedly:
       if (!mqttClient.connected()){
          while (!mqttClient.connected()){
             if(mqttClient.connect(mqtt_client_name)){
                Serial.println("MQTT Connected!");
                mqttClient.subscribe(mqtt_sub_topic);
             }
             else{
                Serial.print(".");
             }
          }
       }
       mqttClient.publish(mqtt_pub_topic, "TestMsg");
       Serial.println("Message published");
       mqttClient.loop();
       delay(5000);
    }
    

    测试代码

    为了测试上述代码,您需要访问 www.hivemq.com

    进入该网页后,请按照以下步骤操作:

    • 单击连接

    Connect Websocket Client
    • 单击“添加新的主题订阅”,然后输入 ESP32 将发布到的主题名称(在本例中为 /ys/testpub)

    Subscribe to a Topic
    • 刷新 ESP32 后,您将每 5 秒开始接收该主题上的消息。

    View Messages
    • 接下来,要测试 ESP32 上的消息接收,请输入 ESP32 订阅的主题名称(在本例中为 ys/testsub),然后在消息框中键入消息并单击发布。您应该在串口监视器上看到该消息。
    ESP32 Subscribe Test

    恭喜!!您已经测试了使用 ESP32 发布和订阅 MQTT。

    参考文献

    通过经典蓝牙传输数据

    本章介绍了使用 ESP32 通过蓝牙传输数据。Arduino 为 ESP32 提供了一个专用的 BluetoothSerial 库,它使通过蓝牙传输数据就像向串口监视器传输数据一样简单。我们将学习如何创建 ESP32 周围的蓝牙字段,将智能手机连接到该字段,并与 ESP32 通信。

    代码演练

    我们将使用本章的示例代码。您可以在文件 -> 示例 -> BluetoothSerial -> SerialToSerialBT 中找到它。它也可以在 GitHub 上找到。

    我们首先包含 BluetoothSerial 库。

    #include <BluetoothSerial.h>
    

    如果你之前没有使用过 ESP32,那么接下来的几行代码可能有些无关紧要。它们检查蓝牙配置是否已启用,如果未启用则会显示警告。ESP32 默认情况下启用蓝牙配置,因此,如果你仅使用 Arduino IDE 来操作 ESP32,可以直接注释掉这些行。错误消息中提到的 `make menuconfig` 实际上是通过 ESP-IDF 访问的,而不是通过 Arduino IDE。所以,不用担心这些代码。

    #if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
    #error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
    #endif
    

    接下来,我们定义 BluetoothSerial 对象。

    BluetoothSerial SerialBT;
    

    在 `setup` 函数中,我们将使用 `**SerialBT.begin()**` 函数启动 ESP32 的蓝牙功能。此函数需要一个参数,即你的蓝牙设备名称(本例中为 ESP32)。这是你在手机上扫描蓝牙网络时显示的名称。

    void setup() {
       Serial.begin(115200);
       SerialBT.begin("ESP32test"); //Bluetooth device name
       Serial.println("The device started, now you can pair it with bluetooth!");
    }
    

    现在,循环部分非常简单。如果串口 (例如,你在串口监视器上输入的文本) 有任何传入文本,则将其通过 SerialBT 发送出去。如果 SerialBT 有任何传入文本,则将其通过串口发送,或者换句话说,将其打印在串口监视器上。

    void loop() {
       if (Serial.available()) {
          SerialBT.write(Serial.read());
       }
       if (SerialBT.available()) {
          Serial.write(SerialBT.read());
       }
       delay(20);
    }
    

    代码测试

    要测试此代码,建议你在智能手机上下载一个串口蓝牙终端应用程序(可以使用下面显示的应用程序或任何等效的应用程序)。它可以帮助你与 ESP32 配对,显示从 ESP32 收到的消息,并帮助你向 ESP32 发送消息。

    Bluetooth Serial Terminal

    要在 Android 设备上安装它,请点击 这里。iOS 的等效应用程序可以是 BluTerm

    你可以查看下面使用串口蓝牙终端应用程序执行的测试截图。我已经将 ESP32 的蓝牙名称更改为“ESP32test345”,因为我的手机已经与另一个蓝牙名称为“ESP32test”的 ESP32 配对了。配对完成后,可以在串口蓝牙终端应用程序中添加该设备,然后你可以像在消息应用程序中与其他用户通信一样与你的设备通信。

    Pairing and Communication

    配对和通信

    Arduino Serial Terminal

    Arduino 串口终端的对应视图

    恭喜你!你已经使用蓝牙与你的 ESP32 通信了。继续探索 BluetoothSerial 库附带的其他示例。

    注意 - 你可能会想同时在 ESP32 上使用 WiFi 和蓝牙。这并不推荐。虽然 ESP32 为 WiFi 和蓝牙提供了独立的堆栈,但它们共享一个公共无线电天线。因此,当两个堆栈都试图访问天线时,模块的行为变得不可预测。建议一次只允许一个堆栈访问天线。

    从 NTP 服务器获取当前时间

    在物联网设备中,时间戳成为设备和服务器之间交换的数据包的一个重要属性。因此,始终在设备上拥有正确的时间非常必要。一种方法是使用与 ESP32 相连的 RTC(实时时钟)。你甚至可以使用 ESP32 的内部 RTC。一旦给定参考时间,它就可以正确输出未来的时间戳。但是你如何获取参考时间呢?一种方法是在编程 ESP32 时硬编码当前时间。但这并不是一个好方法。其次,RTC 容易漂移,定期向其提供参考时间戳是一个好主意。在本章中,我们将学习如何从 NTP 服务器获取当前时间,将其馈送到 ESP32 的内部 RTC 一次,并打印未来的时间戳。

    关于 NTP 的简要介绍

    NTP 代表网络时间协议 (Network Time Protocol)。它是一种用于计算机系统之间时钟同步的协议。简单来说,某个地方有一个服务器准确地维护着时间。每当客户端向 NTP 服务器请求当前时间时,它都会返回精确到 100 毫秒以内的当前时间。你可以这里了解更多关于 NTP 的信息。对于 ESP32,有一个内置的 `time` 库可以处理与 NTP 服务器的所有通信。让我们在下面的代码演练中探索该库的使用。

    代码演练

    我们将使用一个内置示例进行此演练。它可以在 文件 -> 示例 -> ESP32 -> 时间 -> SimpleTime 中找到。它也可以在 GitHub 上找到。

    我们首先包含 WiFi 和 time 库。

    #include <WiFi.h>
    #include "time.h"
    

    接下来,我们定义一些全局变量。将 WiFi SSID 和密码替换为你 WiFi 的相应值。接下来,我们定义了 NTP 服务器的 URL。`gmtOffset_sec` 指的是你的时区相对于 GMT 或密切相关的 UTC 的秒数偏移量。例如,在印度,时区比 UTC 快 5 小时 30 分钟,`gmtOffset_sec` 将为 (5+0.5)*3600 = 19800。

    `daylightOffset_sec` 与实行夏令时的国家相关。在其他国家,可以简单地将其设置为 0。

    const char* ssid       = "YOUR_SSID";
    const char* password   = "YOUR_PASS";
    
    const char* ntpServer = "pool.ntp.org";
    const long  gmtOffset_sec = 3600;
    const int   daylightOffset_sec = 3600;
    

    接下来,你可以看到一个函数 `**printLocalTime()**`。它只是从内部 RTC 获取本地时间并将其打印到串口。

    void printLocalTime()
    {
       struct tm timeinfo;
       if(!getLocalTime(&timeinfo)){
          Serial.println("Failed to obtain time");
          return;
       }
       Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
    }
    

    你可能在这里有三个问题:

    • `struct tm` 在哪里定义?
    • `getLocalTime()` 函数在哪里定义?
    • `%A`、`%B` 等格式化程序是什么?

    `struct tm` 在我们在顶部包含的 `time.h` 文件中定义。事实上,time 库不是 ESP32 专用的库。它是一个与 ESP32 兼容的 AVR 库。你可以在 这里 找到源代码。如果你查看 `time.h` 文件,你会看到 `struct tm`。

    struct tm {
       int8_t   tm_sec; /**< seconds after the minute - [ 0 to 59 ] */
       int8_t   tm_min; /**< minutes after the hour - [ 0 to 59 ] */
       int8_t   tm_hour; /**< hours since midnight - [ 0 to 23 ] */
       int8_t   tm_mday; /**< day of the month - [ 1 to 31 ] */
       int8_t   tm_wday; /**< days since Sunday - [ 0 to 6 ] */
       int8_t   tm_mon; /**< months since January - [ 0 to 11 ] */
       int16_t  tm_year; /**< years since 1900 */
       int16_t  tm_yday; /**< days since January 1 - [ 0 to 365 ] */
       int16_t  tm_isdst; /**< Daylight Saving Time flag */
    };
    

    现在,`getLocalTime` 函数是 ESP32 专用的。它在 `esp32-hal-time.c` 文件中定义。它是 ESP32 的 Arduino 核心的一部分,不需要在 Arduino 中单独包含。你可以在 这里 查看源代码。

    现在,格式化程序的含义如下:

    /*
       %a Abbreviated weekday name
       %A Full weekday name
       %b Abbreviated month name
       %B Full month name
       %c Date and time representation for your locale
       %d Day of month as a decimal number (01−31)
       %H Hour in 24-hour format (00−23)
       %I Hour in 12-hour format (01−12)
       %j Day of year as decimal number (001−366)
       %m Month as decimal number (01−12)
       %M Minute as decimal number (00−59)
       %p Current locale's A.M./P.M. indicator for 12−hour clock
       %S Second as decimal number (00−59)
       %U Week of year as decimal number,  Sunday as first day of week (00−51)
       %w Weekday as decimal number (0−6; Sunday is 0)
       %W Week of year as decimal number, Monday as first day of week (00−51)
       %x Date representation for current locale
       %X Time representation for current locale
       %y Year without century, as decimal number (00−99)
       %Y Year with century, as decimal number
       %z %Z Time-zone name or abbreviation, (no characters if time zone is unknown)
       %% Percent sign
       You can include text literals (such as spaces and colons) to make a neater display or for padding between adjoining columns.
       You can suppress the display of leading zeroes  by using the "#" character  (%#d, %#H, %#I, %#j, %#m, %#M, %#S, %#U, %#w, %#W, %#y, %#Y)
    */
    

    因此,使用我们的格式方案 `**%A, %B %d %Y %H:%M:%S**`,我们可以预期输出类似于以下内容:星期日,2020 年 11 月 15 日 14:51:30。

    现在,来看 `setup` 和 `loop` 函数。在 `setup` 函数中,我们初始化串口,使用我们的 WiFi 连接到互联网,并使用 `configTime()` 函数配置 ESP32 的内部 RTC。正如你所看到的,该函数接受三个参数:`gmtOffset`、`daylightOffset` 和 `ntpServer`。它将从 `ntpServer` 获取 UTC 时间,在本地应用 `gmtOffset` 和 `daylightOffset`,并返回输出时间。这个函数,就像 `getLocalTime` 一样,是在 esp32-hal-time.c 文件中定义的。正如你从文件中看到的,TCP/IP 协议用于从 NTP 服务器获取时间。

    一旦我们从 NTP 服务器获取时间并将其馈送到 ESP32 的内部 RTC,我们就不再需要 WiFi 了。因此,我们断开 WiFi 连接,并在循环中每秒打印一次时间。你可以在串口监视器上看到时间每打印一次就会增加一秒。这是因为 ESP32 的内部 RTC 在获得参考时间后会维护时间。

    void setup()
    {
       Serial.begin(115200);
      
       //connect to WiFi
       Serial.printf("Connecting to %s ", ssid);
       WiFi.begin(ssid, password);
       while (WiFi.status() != WL_CONNECTED) {
          delay(500);
          Serial.print(".");
       }
       Serial.println(" CONNECTED");
      
       //init and get the time
       configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
       printLocalTime();
    
       //disconnect WiFi as it's no longer needed
       WiFi.disconnect(true);
       WiFi.mode(WIFI_OFF);
    }
    void loop() {
      delay(1000);
      printLocalTime();
    }
    

    串口监视器输出将如下所示:

    ESP32 NTP Sketch Output

    就是这样。你已经学习了如何从 NTP 服务器获取正确的时间并配置 ESP32 的内部 RTC。现在,在发送到服务器的任何数据包中,你都可以添加时间戳。

    参考文献

    执行 ESP32 固件的空中升级 (OTA)

    假设你在现场有一千台物联网设备。现在,如果有一天你发现生产代码中存在错误,并希望修复它,你会召回所有一千台设备并在其中刷入新的固件吗?可能不会!你更希望有一种方法可以远程更新所有设备,即进行空中升级。OTA 更新如今非常常见。你不时会收到 Android 或 iOS 智能手机的软件更新。就像软件更新可以远程进行一样,固件更新也可以。在本章中,我们将了解如何远程更新 ESP32 的固件。

    OTA 更新过程

    这个过程非常简单。设备首先分块下载新固件并将其存储在内存的另一个区域中。让我们将这个区域称为“OTA 空间”。让我们将存储当前代码或应用程序代码的内存区域称为“应用程序空间”。一旦整个固件下载完毕并经过验证,设备引导加载程序就会启动。将引导加载程序视为写在内存另一个区域(让我们将其称为“引导加载程序空间”)中的代码,其唯一目的是在每次设备重启时加载应用程序空间中的正确代码。

    因此,每次设备重启时,引导加载程序空间中的代码都会首先执行。大多数情况下,它只是将控制权传递给应用程序空间中的代码。但是,在下载较新的固件后,当设备重启时,引导加载程序会注意到有较新的应用程序代码可用。因此,它会将较新的代码从 OTA 空间刷入应用程序空间,然后将控制权交给应用程序空间中的代码。结果将是设备固件已升级。

    现在,稍微偏离一下,如果应用程序代码已损坏或发送了恢复出厂设置命令,引导加载程序也可以将恢复出厂设置代码从“恢复出厂设置空间”刷入应用程序空间。此外,如果微控制器空间不足,OTA 代码和恢复出厂设置代码通常存储在外部存储设备上,例如 SD 卡或外部 EEPROM 或 FLASH 芯片。但是,在 ESP32 的情况下,OTA 代码可以存储在微控制器的内存中本身。

    代码演练

    我们将使用一个示例代码来讲解本章。你可以在 文件 -> 示例 -> 更新 -> AWS_S3_OTA_Update 中找到它。它也可以在 GitHub 上找到。

    这是 Arduino 上 ESP32 的一个非常详细的示例之一。此草图的作者甚至在注释中提供了草图的预期串口监视器输出。因此,虽然大部分代码通过注释可以自解释,但我们将介绍大致思路并涵盖重要细节。此代码使用了 `Update` 库,就像许多其他库一样,它使 ESP32 的操作非常容易,同时将繁重的工作隐藏在幕后。

    在这个例子中,作者将新的固件二进制文件存储在 AWS S3 存储桶中。详细介绍 AWS S3 超出了本章的范围,但简单来说,S3(Simple Storage Service)是亚马逊网络服务 (AWS) 提供的云存储服务。可以把它想象成 Google Drive。你将文件上传到你的网盘,然后与他人分享链接。类似地,你可以将文件上传到 S3 并通过链接访问它。S3 更受欢迎,因为许多其他 AWS 服务可以与之无缝集成。开始使用 AWS S3 非常简单。你可以通过简单的 Google 搜索找到许多可用的资源来帮助你。在代码开头处的注释中,也提到了几个入门步骤。

    需要注意的一个重要建议是,你应该使用你自己的二进制文件。代码顶部的注释建议你可以使用作者使用的相同二进制文件。但是,下载在另一台机器/另一个版本的 Arduino IDE 上编译的二进制文件有时会在 OTA 过程中导致错误。此外,使用你自己的二进制文件会使你的学习更加“完整”。你可以通过转到“草图” -> “导出已编译的二进制文件”来导出任何 ESP32 草图的二进制文件。二进制文件 (.bin) 将保存到你保存 Arduino (.ino) 文件的同一文件夹中。

    Saving binary

    保存二进制文件后,你只需要将其上传到 S3,并将存储桶链接和二进制文件的地址添加到你的代码中。你保存的二进制文件应该包含一些打印语句,以表明它与你写入 ESP32 的代码不同。例如,“Hello from S3”之类的语句。另外,不要直接在代码中保留 S3 存储桶链接和二进制文件地址。

    好了!废话少说!现在开始逐步讲解。我们将首先包含 WiFi 和 Update 库。

    #include <WiFi.h>
    #include <Update.h>
    

    接下来,我们定义一些变量、常量以及 WiFiClient 对象。请记住添加你自己的 WiFi 凭据和 S3 凭据。

    WiFiClient client;
    
    // Variables to validate
    // response from S3
    long contentLength = 0;
    bool isValidContentType = false;
    
    // Your SSID and PSWD that the chip needs
    // to connect to
    const char* SSID = "YOUR−SSID";
    const char* PSWD = "YOUR−SSID−PSWD";
    
    // S3 Bucket Config
    String host = "bucket−name.s3.ap−south−1.amazonaws.com"; // Host => bucket−name.s3.region.amazonaws.com
    int port = 80; // Non https. For HTTPS 443. As of today, HTTPS doesn't work.
    String bin = "/sketch−name.ino.bin"; // bin file name with a slash in front.
    

    接下来,定义了一个辅助函数 getHeaderValue(),它主要用于检查特定标头的值。例如,如果我们得到标头“Content-Length: 40”,并且它存储在一个名为 headers 的字符串中,getHeaderValue(headers,“Content−Length: ”) 将返回 40。

    // Utility to extract header value from headers
    String getHeaderValue(String header, String headerName) {
       return header.substring(strlen(headerName.c_str()));
    }
    

    接下来是主函数 execOTA(),它执行 OTA。此函数包含与 OTA 相关的全部逻辑。如果你查看 Setup 部分,我们只是连接到 WiFi 并调用 execOTA() 函数。

    void setup() {
       //Begin Serial
       Serial.begin(115200);
       delay(10);
    
       Serial.println("Connecting to " + String(SSID));
    
       // Connect to provided SSID and PSWD
       WiFi.begin(SSID, PSWD);
    
       // Wait for connection to establish
       while (WiFi.status() != WL_CONNECTED) {
          Serial.print("."); // Keep the serial monitor lit!
          delay(500);
       }
    
       // Connection Succeed
       Serial.println("");
       Serial.println("Connected to " + String(SSID));
    
       // Execute OTA Update
       execOTA();
    }
    

    所以你应该理解,理解 execOTA 函数就意味着理解整个代码。因此,让我们开始逐步讲解该函数。

    我们首先连接到我们的主机,在本例中是 S3 存储桶。连接后,我们使用 GET 请求从存储桶中获取 bin 文件(有关 GET 请求的更多信息,请参阅 HTTP 教程)。

    void execOTA() {
       Serial.println("Connecting to: " + String(host));
       // Connect to S3
       if (client.connect(host.c_str(), port)) {
       // Connection Succeed.
       // Fecthing the bin
       Serial.println("Fetching Bin: " + String(bin));
    
       // Get the contents of the bin file
       client.print(String("GET ") + bin + " HTTP/1.1\r\n" +
          "Host: " + host + "\r\n" +
          "Cache-Control: no-cache\r\n" +
          "Connection: close\r\n\r\n");
    

    接下来,我们等待客户端连接。我们最多给连接建立 5 秒的时间,否则我们将表示连接超时并返回。

    unsigned long timeout = millis();
    while (client.available() == 0) {
       if (millis() - timeout > 5000) {
          Serial.println("Client Timeout !");
          client.stop();
          return;
       }
    }
    

    假设代码没有在上一步返回,我们就建立了成功的连接。服务器的预期响应在注释中提供。我们首先解析该响应。逐行读取响应,每行都存储在一个名为 line 的变量中。我们特别检查以下三件事:

    • 响应状态码是否为 200 (OK)

    • Content-Length 是多少

    • 内容类型是否为 application/octet-stream(这是二进制文件的预期类型)

    第一个和第三个是必需的,第二个只是为了提供信息。

    while (client.available()) {
       // read line till /n
       String line = client.readStringUntil('\n');
       // remove space, to check if the line is end of headers
       line.trim();
    
       // if the the line is empty,
       // this is end of headers
       // break the while and feed the
       // remaining `client` to the
       // Update.writeStream();
       if (!line.length()) {
       //headers ended
       break; // and get the OTA started
       }
    
       // Check if the HTTP Response is 200
       // else break and Exit Update
       if (line.startsWith("HTTP/1.1")) {
          if (line.indexOf("200") < 0) {
             Serial.println("Got a non 200 status code from server. Exiting OTA Update.");
             break;
          }
       }
    
       // extract headers here
       // Start with content length
       if (line.startsWith("Content-Length: ")) {
          contentLength = atol((getHeaderValue(line, "Content-Length: ")).c_str());
          Serial.println("Got " + String(contentLength) + " bytes from server");
       }
    
       // Next, the content type
       if (line.startsWith("Content-Type: ")) {
          String contentType = getHeaderValue(line, "Content-Type: ");
          Serial.println("Got " + contentType + " payload.");
          if (contentType == "application/octet-stream") {
             isValidContentType = true;
          }
       }
    }
    

    这样,检查与服务器连接是否成功的 if 块就结束了。后面跟着 else 块,它只是打印我们无法建立与服务器的连接。

    } else {
       // Connect to S3 failed
       // May be try?
       // Probably a choppy network?
       Serial.println("Connection to " + String(host) + " failed. Please check your setup");
       // retry??
       // execOTA();
    }
    

    接下来,如果我们希望从服务器收到正确的响应,我们将得到一个正的 contentLength(记住,我们在顶部将其初始化为 0,因此如果我们 somehow 没有到达解析 Content−Length 标头的行,它仍然为 0)。此外,我们将有 isValidContentType 为 true(记住,我们将其初始化为 false)。因此,我们检查这两个条件是否都为真,如果是,则继续进行实际的 OTA。请注意,到目前为止,我们只使用了 WiFi 库来与服务器交互。现在,如果服务器交互结果正常,我们将开始使用 Update 库,否则,我们只打印服务器响应中没有内容,并刷新客户端。如果响应确实正确,我们首先检查内存中是否有足够的空闲空间来存储 OTA 文件。默认情况下,大约保留 1.2 MB 的空间用于 OTA 文件。因此,如果 contentLength 超过该值,Update.begin() 将返回 false。这个 1.2MB 的数字可能会根据你的 ESP32 分区而改变。

    // check contentLength and content type
    if (contentLength && isValidContentType) {
       // Check if there is enough to OTA Update
       bool canBegin = Update.begin(contentLength);
    

    现在,如果我们确实有空间在内存中存储 OTA 文件,我们将使用 Update.writeStream() 函数将字节写入为 OTA 保留的内存区域(OTA 空间)。如果没有,我们只打印该消息,刷新客户端并退出 OTA 过程。Update.writeStream() 函数返回写入 OTA 空间的字节数。然后我们检查写入的字节数是否等于 contentLength。如果 Update 完成,在这种情况下 Update.end() 函数将返回 true,我们将使用 Update.isFinished() 函数检查它是否已正确完成,即所有字节都已写入。如果它返回 true,表示所有字节都已写入,我们将重新启动 ESP32,以便引导加载程序可以将新代码从 OTA 空间闪存到应用程序空间,并且我们的固件将被升级。如果它返回 false,我们将打印收到的错误。

       // If yes, begin
       if (canBegin) {
          Serial.println("Begin OTA. This may take 2 − 5 mins to complete. Things might be quite for a while.. Patience!");
          // No activity would appear on the Serial monitor
          // So be patient. This may take 2 - 5mins to complete
          size_t written = Update.writeStream(client);
    
          if (written == contentLength) {
             Serial.println("Written : " + String(written) + " successfully");
          } else {
             Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?" );
             // retry??
             // execOTA();
          }
    
          if (Update.end()) {
             Serial.println("OTA done!");
             if (Update.isFinished()) {
                Serial.println("Update successfully completed. Rebooting.");
                ESP.restart();
             } else {
                Serial.println("Update not finished? Something went wrong!");
             }
          } else {
             Serial.println("Error Occurred. Error #: " + String(Update.getError()));
          }
       } else {
          // not enough space to begin OTA
          // Understand the partitions and
          // space availability
          Serial.println("Not enough space to begin OTA");
          client.flush();
       }
    }
    

    当然,你现在应该已经意识到我们不需要在这里做任何循环操作。

    就是这样。你已经成功地远程升级了 ESP32 芯片的固件。如果你对 Update 库的每个函数的作用更感兴趣,可以参考 Update.h 文件中的注释。

    参考文献

    ESP32的应用

    既然你已经相当熟悉 ESP32 了,让我们来看看它的应用。对于这一章,我觉得我不需要多说。在学习了本教程中的各个章节之后,你应该已经在脑海中形成了各种想法。你可能已经创建了一个粗略的应用列表,说明你可以在哪些地方使用 ESP32。好消息是,你列出的许多应用都是可行的。

    但是,对于某些应用来说,ESP32 比其他应用更可行。在本章中,我的重点是让你理解在决定是否为某个应用使用 ESP32 时应考虑的因素。请注意,本章的重点是生产环境,即当我们谈论的是数千甚至数百万台设备的规模时。如果你只需要少量设备并且 ESP32 可以满足需求,那么可以直接使用 ESP32,无需犹豫。此外,对于原型设计/概念验证 (PoC),你可以毫无顾虑地使用 ESP32。

    ESP32 的主要优势之一是内置的 WiFi 和蓝牙协议栈以及硬件。因此,在 WiFi 连接良好的静态应用中,例如实验室中的环境监控应用,ESP32 将是你的首选微控制器。模块本身的 WiFi 协议栈意味着你将节省额外的网络模块的成本。但是,如果你在资产跟踪应用中使用 ESP32,它会不断移动,则必须依赖 GSM 或 LTE 模块来连接到服务器(因为你无法保证 WiFi 的可用性)。在这种情况下,ESP32 会失去竞争优势,你最好使用更便宜的微控制器来满足你的需求。

    同样,拥有用于加密消息的硬件加速器使 ESP32 成为需要安全通信 (HTTPS) 的应用的理想选择。因此,如果你正在处理敏感信息,不想让它落入坏人之手,那么使用 ESP32 比使用不支持加密的其他微控制器更有优势。一个示例应用可以是国防领域的工业物联网。

    两个内核的存在再次使 ESP32 成为处理密集型应用的首选微控制器,例如那些以非常高的波特率接收数据并需要在单独的内核上运行数据处理和传输的应用。在工业物联网中可以找到许多这样的应用。但是对于一个非常轻量的应用,你甚至不需要安全通信,一个具有适中规格的微控制器可能会更有用。毕竟,如果你只需要一个内核,为什么要拥有(并为此付费)两个内核呢?

    另一个需要考虑的因素是 GPIO 和外设的数量。ESP32 有 3 个 UART 通道。如果你的应用需要超过 3 个 UART 通道,你可能需要寻找另一个微控制器。同样,ESP32 有 34 个可编程 GPIO,对于大多数应用来说已经足够了。但是,如果你的应用确实需要更多 GPIO,你可能需要切换到另一个微控制器。

    ESP32 的 1.5 MB 默认 SPIFFS 提供的板载存储空间比大多数其他微控制器都多。如果你的存储需求在 1.5 MB 以内,ESP32 可以为你节省外部 SD 卡或闪存芯片的成本。ESP32 本身会在 SPIFFS 内进行磨损均衡,也为你节省了很多开发工作。但是,如果 ESP32 无法满足你的存储需求,那么它的竞争优势就会消失。

    ESP32 的 520 KB RAM 也足以满足大多数应用的需求。只有对于图像/视频处理等非常繁重的应用,这才会成为瓶颈。

    总而言之,ESP32 的规格足以满足你的大多数应用需求。在扩大生产规模时,你只需要确保这些规格不会对你来说过于高性能。换句话说,如果你可以使用适中的规格获得所需的输出,那么最好使用更便宜的微控制器来节省成本。当你的产量成倍增长时,这些节省将变得非常显著。但是,除了生产之外,ESP32 绝对是原型设计和建立 PoC 的理想微控制器。

    开发人员的下一步

    如果你坚持到了现在,恭喜你!你已经达到了一个阶段,你不仅熟悉 ESP32,而且还具备足够的知识来进一步探索它。事实上,还有很多东西值得探索。如果你了解一些额外的概念,就可以实现许多很酷的应用。在本章中,我只会为你提供探索的方向。事实上,如果我只是列出来,会更好。下面是你可以进一步探索的主题/领域以及可以学习的概念的非详尽列表。

    • 固件

      • 休眠模式 - 在电源匮乏的应用中,你需要了解这些模式

      • 定时器 - 用于计划任务

      • 中断 - 用于由异步事件触发的任务

      • 看门狗超时 - 如果你的微控制器长时间卡在某个地方,请重置它

      • 互斥锁和信号量(与 RTOS 相关) - 当多个线程想要访问共享资源时

      • 调整 ESP32 的分区以提供更多空间给 SPIFFS

    • 传感器接口

      • 使用 ESP32 的触摸传感器

      • 使用 ESP32 的霍尔传感器

      • 使用 ESP32 的 GNSS 接收器(几乎所有物联网设备都使用 GNSS 接收器来获取位置信息)

    • 网络相关

      • 使用 ESP32 的 BLE(低功耗蓝牙)

      • 将 ESP32 连接到外部蓝牙模块

      • 使用ESP32的接入点(AP) WiFi

      • ESP32作为Web服务器

      • 使用ESP32的登录页面(在餐厅或机场,连接WiFi后弹出的要求输入手机号码的页面,这就是登录页面)

      • 使用UDP进行数据传输

      • 使用TCP进行数据传输

      • DNS服务器

      • 通过LoRa进行数据传输

      • 与AWS/Azure上的代理服务器的MQTT连接

    • 外设连接

      • CAN协议(用于汽车应用)

      • 具有多个从机的I2C和SPI

      • ESP32的SD卡接口

      • 单线协议

    • 数据处理

      • ESP32上的FFT

      • 功率计算(RMS值、峰峰值、功率因数等)

    不要被以上列表吓到。您不必一天之内学习所有内容,也不需要学习所有内容。如开头所述,以上列表只是为您提供进一步探索的方向。您可以选择适合您应用的主题。对于所有您有示例代码的主题,没有什么比从示例代码开始更好的了。对于其他主题,您可以阅读与这些主题相关的可用库的文档,查看它们的.h文件,并在互联网上搜索示例。在如此庞大的在线社区中,很难找不到在线帮助。因此,继续学习,继续探索。

    广告