|
ESP32 进阶功能完全指南:EEPROM 存储 + UART 串口 + OTA 空中升级

ESP32 进阶功能完全指南:EEPROM 存储 + UART 串口 + OTA 空中升级

引言

ESP32 不仅仅是一块带 Wi-Fi 和蓝牙的物联网芯片——它内部集成了大量强大却常被忽视的功能,让开发者可以从”能联网的小玩具”一路做到”可量产的产品”。很多初学者只停留在点灯、连 Wi-Fi 的阶段,却错过了 ESP32 真正值得深挖的能力。

本文将完整讲解 3 个 ESP32 开发者必须掌握的进阶功能:

  1. EEPROM 断电保存数据——让设备重启后仍能记住状态;
  2. UART 串口通信——用 3 个硬串口与传感器、模块稳定通信;
  3. Wi-Fi OTA 空中升级——不插 USB 线也能远程更新固件。

每个部分都会附上完整的 Arduino 代码和常见问题解答,建议收藏后反复实践。

第一部分:ESP32 编程基础

在正式深入进阶功能之前,我们先快速回顾一下 ESP32 的硬件能力和开发环境搭建,为后面的内容打好基础。

ESP32 核心规格

ESP32 是乐鑫信息科技(Espressif Systems)推出的低成本、低功耗系统级芯片(SoC),集成了 Wi-Fi 802.11 b/g/n 和蓝牙 v4.2 + BLE。其芯片架构的关键参数如下:

  • 处理器:高性能 Xtensa® 32 位 LX6 双核微处理器,主频高达 240 MHz
  • 存储:448 KB ROM + 520 KB SRAM + 16 KB SRAM 缓存
  • 工作电压:2.2V ~ 3.6V
  • 工作温度:-40°C ~ +125°C

丰富的外设接口使 ESP32 可以胜任各种应用场景:

  • 34 个可编程 GPIO
  • 电容式触摸传感器
  • Hall 传感器
  • SD 卡接口、以太网
  • 高速 SPI、I2S、I2C、UART、ADC 和 DAC

开发环境搭建

Arduino IDE 是 ESP32 编程的首选开发环境,搭建步骤如下:

  1. 打开 Arduino IDE,进入 Preferences,在 Additional Boards Manager URLs 中添加:
    https://dl.espressif.com/dl/package_esp32_index.json
  2. 进入 Tools > Board > Boards Manager,搜索 esp32 并安装对应开发板型号的包(常见开发板有 ESP32-DevKitC、ESP-WROVER-KIT 等)。
  3. Tools 中选择开发板型号、正确串口(COM 端口),并安装 CP210x USB to UART Bridge VCP 驱动程序

硬件连接需要:USB 数据线(编程+供电)、电源模块、天线(保证 Wi-Fi/蓝牙信号)、以及 LED、按键、传感器等外设。

WiFi 连接示例

#include <WiFi.h>

const char* ssid = "your_wifi_ssid";
const char* password = "your_wifi_password";

void connectWiFi(){
  WiFi.begin(ssid, password);
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
    Serial.print(".");
  }
  Serial.println("WiFi Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
}

void setup(){
  Serial.begin(115200);
  connectWiFi();
}

void loop(){}

蓝牙串口示例

#include "BluetoothSerial.h"

BluetoothSerial ESP_BT;

void setup(){
  Serial.begin(115200);
  ESP_BT.begin("ESP32_BT");
  Serial.println("Bluetooth Started!");
}

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

有了这些基础,我们就可以进入 EEPROM、UART 和 OTA 三大进阶主题了。

第二部分:EEPROM 断电保存数据

什么是 EEPROM

EEPROM 全称 Electrically Erasable Programmable Read-Only Memory(可擦除可编程只读存储器),是一种电子存储器,可在不需要外部设备的情况下擦除和重写。它是一种非易失性存储器(NVM),即使断电数据也不会丢失,广泛用于计算机、手机、数码相机等各类电子设备中。

ESP32 中 EEPROM 的特点

需要特别注意的是,ESP32 并不具有内置的硬件 EEPROM,而是通过 Flash 内存来模拟 EEPROM。Flash 与 EEPROM 非常类似,同样是非易失性存储器,非常适合存储需要长期保存的数据。ESP32 中模拟的 EEPROM 具有以下特点:

  1. 非易失性存储:断电后数据不会丢失,适合长期保存配置信息和状态数据
  2. 按字节寻址:每个地址存储一个字节(8 位)数据,可通过地址索引直接读写
  3. 写入次数有限:Flash 模拟的 EEPROM 支持约 100,000 到 1,000,000 次擦写操作
  4. 通过软件模拟实现:ESP32 使用 EEPROM 库在 Flash 的专用分区上进行读写操作

使用方法

使用 EEPROM 只需 5 步:

  1. 引入 EEPROM 库:#include <EEPROM.h>
  2. 初始化 EEPROM:EEPROM.begin(size) 指定所需存储空间大小(单位:字节)
  3. 写入数据:EEPROM.write(address, value) 将数据写入指定地址
  4. 读取数据:EEPROM.read(address) 从指定地址读取一个字节
  5. 提交写入:EEPROM.commit() 将缓冲区的数据实际写入 Flash,此步骤不可省略

完整代码示例:LED 状态断电保存

以下代码展示如何保存 LED 在关机前的状态。使用 NodeMCU 开发板,默认 BUILDIN_LED 为 GPIO2。按下 IO0 按钮切换 LED 状态,状态会被实时写入 EEPROM,断电重启后仍能恢复。

// 引入头文件
#include <EEPROM.h>

// 定义 EEPROM 容量
#define EEPROM_SIZE 1
// 定义引脚,使用开发板的 IO0 按钮,默认被拉高
#define BUTTON_PIN 0
// 使用板载 LED 灯
#define BUILDIN_LED 2

// LED 状态
int ledState = HIGH;
// 按钮状态
int buttonState;
// 之前的按钮状态
int lastButtonState = HIGH;

// 上次被触发的时间
unsigned long lastDebounceTime = 0;
// 防抖动计时器
unsigned long debounceDelay = 50;

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

  // 初始化 EEPROM
  EEPROM.begin(EEPROM_SIZE);

  // 使用 GPIO0 需要设置默认上拉
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUILDIN_LED, OUTPUT);

  // 读取 LED 在 EEPROM 的状态
  ledState = EEPROM.read(0);
  // 写入关机前的状态
  digitalWrite(BUILDIN_LED, ledState);
}

void loop() {
  // 读取按钮状态
  int reading = digitalRead(BUTTON_PIN);

  // 检查按钮状态是否改变
  if (reading != lastButtonState) {
    // 重置防抖计时器
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    // 需要按下时间大于防抖时间,才会进入改变状态
    // 如果按钮状态改变
    if (reading != buttonState) {
      buttonState = reading;

      // 只有等新的状态是 LOW,才改变 LED 状态
      if (buttonState == LOW) {
        ledState = !ledState;
      }
    }
  }
  // 保存已经读取的状态
  lastButtonState = reading;

  // 如果 LED 状态和当前状态不一样
  if (digitalRead(BUILDIN_LED) != ledState) {
    Serial.println("State changed");
    // 改变 LED 状态
    digitalWrite(BUILDIN_LED, ledState);
    // 保存 LED 状态到 EEPROM
    EEPROM.write(0, ledState);
    EEPROM.commit();
    Serial.println("State saved in flash memory");
  }
}

常见使用场景

  • WiFi 凭据保存:设备重启后自动重连上次配置的 WiFi
  • 设备配置参数:传感器阈值、采样间隔、设备 ID 等
  • 校准数据:温湿度传感器校准偏移值、ADC 零点校准等
  • 运行计数器:开机次数、错误计数、工作时长统计

第三部分:UART 串口通信

什么是 UART

UART 全称为 Universal Asynchronous Receiver/Transmitter(通用异步接收器/发送器),是一种硬件功能,可使用广泛采用的异步串行通信接口(如 RS232、RS422、RS485)处理通信的时序要求和数据帧。UART 提供了一种廉价且广泛应用的方法,实现不同设备之间的全双工或半双工数据交换。

ESP32 芯片拥有 3 个 UART 控制器(端口),每个控制器都具有一组相同的寄存器,既简化了编程,又提供了更大的灵活性。

串口通信过程

串行通信由每个 UART 控制器的**有限状态机(FSM)**控制。

发送信息的步骤:

  1. 将要发送的数据写入 UART 的 TX FIFO 缓冲区
  2. FSM 自动将数据按照配置的波特率、数据位、停止位和校验位组装成串行帧
  3. 数据通过 TX 引脚逐位移出发送到接收端

接收信息的步骤:

  1. RX 引脚检测到起始位(低电平)后,FSM 开始按照波特率对数据位进行采样
  2. FSM 将采样到的数据位组装成完整字节并存入 RX FIFO 接收缓冲区
  3. 应用程序从 RX FIFO 缓冲区中读取已接收到的数据

应用程序只需使用 uart_write_bytes()uart_read_bytes() 操作特定缓冲区,其余工作均由 FSM 完成。更重要的是,ESP32 把 FSM 的流控制(Flow Control)做到了芯片内部,并且专门分配了 DMA(Direct Memory Access) 用于串口数据处理,因此使用硬串口的数据处理效率远高于软串口,并且更少地占用 CPU 资源。

不同 ESP32 芯片的串口能力对比

虽然 ESP32 文档声称配备了 3 个串口控制器,但影响性能的关键在于是否配备相应的 Flow Control 硬件和 DMA 硬件——这些并不是所有型号都具备的:

配备两个独立 Flow Control + DMA 的芯片:

  • ESP32-S(安可信 NodeMCU 版本)
  • ESP32-S2
  • ESP32-C3

配备三个独立 Flow Control + DMA 的芯片:

  • ESP32(ESP32-D0WD-V3、ESP32-D0WDR2-V3)
  • ESP32-S3

ESP32 三个硬串口引脚定义

ESP32 在 Arduino 框架里定义的串口分别为 Serial、Serial1、Serial2,对应引脚如下:

串口RX 引脚TX 引脚备注
SerialGPIO3GPIO1一般情况下可直接使用
Serial1GPIO9GPIO10默认配置下 GPIO 6-12 是 FLASH 接口,不能被其他程序使用
Serial2GPIO16GPIO17一般情况下可直接使用

Serial1 为什么”很难用”?

Serial1 默认使用的 GPIO9 和 GPIO10 位于 GPIO 6-12 区间——这个区间恰好是 ESP32 内部 Flash 的接口引脚。因此如果直接使用 Serial1 默认配置,会与 Flash 存储发生冲突,导致程序无法正常运行。

解决方案有两种:

  1. 重新映射引脚:通过代码把 Serial1 的 RX/TX 引脚重新映射到不被 Flash 占用的其他 GPIO 上,避开 GPIO 6-12 区域;或者通过 DIO 方式上传代码来释放这些引脚。
  2. 使用 SoftwareSerial(软串口)库:将任意普通 GPIO 模拟为串口使用,但波特率不宜过高,适合低速通信场景。

完整代码示例:使用 Serial2

// 如果要重新映射端口,可以修改下面的 GPIO 口定义
// 如果使用 Serial1 时,可以通过 DIO 方式上传代码
//#define RXD1 9
//#define TXD1 10
#define RXD2 16
#define TXD2 17

void setup() {
  Serial.begin(115200);
  // Serial1.begin(9600, SERIAL_8N1, RXD1, TXD1);
  Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2);
}

void loop() {
  // 当 Serial2 收到数据时,通过 Serial 串口显示出来
  while (Serial2.available()) {
    Serial.print(char(Serial2.read()));
  }
}

第四部分:Wi-Fi OTA 空中升级

什么是 OTA

OTA 全称 Over-The-Air,即空中下载技术(空中升级)。早期被广泛应用于手机行业,终结了手机软件升级需要连接电脑、下载软件、再安装更新的繁复操作。

对于 ESP32 来说,OTA 就是”空中上传固件”——你不需要通过 USB 线对 ESP32 上传固件,只要 ESP32 和你的电脑处于同一网络(Wi-Fi 网络或蓝牙网络),甚至可以通过互联网在网页端进行固件更新。

为什么需要 OTA

OTA 是相当实用的技术。除了方便之外,对于一些 USB 上传存在问题的 ESP32 开发板更是救星——由于各厂家用料和设计不尽相同,USB 上传经常会出现各种各样的问题(如自动烧录超时等)。如果在项目一开始就写入了 OTA 功能,即使后续 USB 接口无法正常上传固件,仍然可以通过 OTA 进行更新。

OTA 的工作原理

OTA 升级机制可以让设备在固件正常运行时,根据接收的数据(通过 Wi-Fi 或蓝牙)进行自我更新。要运行 OTA 机制,需要配置设备的分区表,该分区表至少包括:

  • 两个 OTA 应用程序分区ota_0ota_1
  • 一个 OTA 数据分区

OTA 功能启动后,系统向当前未用于启动的 OTA 应用分区写入新的应用固件镜像。镜像验证后,OTA 数据分区更新,指定在下一次启动时使用新镜像。下图说明了不同阶段下 ESP32 使用分区的情况:

通过 UDP OTA 进行更新

Basic OTA 使用 ArduinoOTA 框架来支持 OTA 操作。支持 Basic OTA 的 ESP32 通过向网络发送 UDP 包,让电脑可以发现支持 OTA 的设备。在 Arduino IDE 中选择了网络中的设备作为端口以后,就能进行空中升级固件:

如果你使用的是 PlatformIO,只要把原来写入端口的位置改成 IP 地址,也能进行空中升级。可以修改或增加 platformio.ini 中的 upload_port 配置:

upload_port = 192.168.31.143

也可以运行 pio 命令指定 upload_port:

pio run -t upload --upload-port 192.168.31.143

同理,上传文件到 ESP32 的 SPIFFS 或 LittleFS 也可以用同样的方法。

完整代码示例

要写一个支持 OTA 的程序,只需要加入少量代码并进行配置。以下是分步讲解:

第一步:加入头文件

#include <WiFi.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

第二步:配置 SSID 和密码

const char* ssid = "";
const char* password = "";

第三步:连接 Wi-Fi

void setup() {
  Serial.begin(115200);
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }

第四步:设置 hostname,便于识别设备

  ArduinoOTA.setHostname("esp32_ota_test");

第五步:增加 OTA 回调处理程序,并启动 OTA 服务

这段程序处理 OTA 的 onStartonEndonProgressonError 几个事件的回调:

  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH)
        type = "sketch";
      else // U_SPIFFS
        type = "filesystem";

      // 注意:如果更新 SPIFFS,这里需要用 SPIFFS.end() 卸载
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });

  ArduinoOTA.begin();
}

第六步:在 loop 方法中加入 OTA 处理

void loop() {
  ArduinoOTA.handle();
}

常见问题解答

问题 1:OTA 过程中意外断电或中断,是否会影响原来的程序?

回答:不会影响原来程序。ESP32 会划分一个 OTA 数据分区,只有当 OTA 成功后才会指定下次启动时使用 OTA 的数据分区。因此 OTA 过程中的意外中断并不会破坏原有固件,设备仍然会正常启动旧版本程序。

问题 2:是否所有 ESP32 都支持 OTA?

回答:不是的。在写入 ESP32 时,如果配置了 “No OTA” 的分区方案,就无法使用 OTA 功能。支持 OTA 意味着 ESP32 需要同时准备 APP 数据区和 OTA 数据区——你的程序空间需要占用双倍空间才能支持 OTA。因此,并不是所有开发人员都愿意牺牲双倍空间来支持 OTA,需要根据项目实际需求权衡。

总结

本文完整介绍了 ESP32 开发者必须掌握的 3 个进阶功能:

  • EEPROM 断电保存数据:让设备拥有”记忆”,可以持久化存储 WiFi 配置、校准参数、运行状态等关键数据,是产品化落地的基础;
  • UART 串口通信:ESP32 拥有 3 个带 Flow Control 和 DMA 的硬串口,理解引脚冲突与重映射技巧,才能高效地与各类传感器和模块通信;
  • Wi-Fi OTA 空中升级:免 USB 线远程更新固件,配合分区表机制保证升级安全,是物联网设备大规模部署后的运维利器。

掌握这三大功能,你的 ESP32 项目就能从”能跑的 demo”真正走向”可量产的产品”。祝开发顺利!