ESP32 ESP32 进阶功能完全指南:EEPROM 存储 + UART 串口 + OTA 空中升级
引言
ESP32 不仅仅是一块带 Wi-Fi 和蓝牙的物联网芯片——它内部集成了大量强大却常被忽视的功能,让开发者可以从”能联网的小玩具”一路做到”可量产的产品”。很多初学者只停留在点灯、连 Wi-Fi 的阶段,却错过了 ESP32 真正值得深挖的能力。
本文将完整讲解 3 个 ESP32 开发者必须掌握的进阶功能:
- EEPROM 断电保存数据——让设备重启后仍能记住状态;
- UART 串口通信——用 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 编程的首选开发环境,搭建步骤如下:
- 打开 Arduino IDE,进入 Preferences,在 Additional Boards Manager URLs 中添加:
https://dl.espressif.com/dl/package_esp32_index.json - 进入 Tools > Board > Boards Manager,搜索
esp32并安装对应开发板型号的包(常见开发板有 ESP32-DevKitC、ESP-WROVER-KIT 等)。 - 在 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 具有以下特点:
- 非易失性存储:断电后数据不会丢失,适合长期保存配置信息和状态数据
- 按字节寻址:每个地址存储一个字节(8 位)数据,可通过地址索引直接读写
- 写入次数有限:Flash 模拟的 EEPROM 支持约 100,000 到 1,000,000 次擦写操作
- 通过软件模拟实现:ESP32 使用 EEPROM 库在 Flash 的专用分区上进行读写操作
使用方法
使用 EEPROM 只需 5 步:
- 引入 EEPROM 库:
#include <EEPROM.h> - 初始化 EEPROM:
EEPROM.begin(size)指定所需存储空间大小(单位:字节) - 写入数据:
EEPROM.write(address, value)将数据写入指定地址 - 读取数据:
EEPROM.read(address)从指定地址读取一个字节 - 提交写入:
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)**控制。
发送信息的步骤:
- 将要发送的数据写入 UART 的 TX FIFO 缓冲区
- FSM 自动将数据按照配置的波特率、数据位、停止位和校验位组装成串行帧
- 数据通过 TX 引脚逐位移出发送到接收端
接收信息的步骤:
- RX 引脚检测到起始位(低电平)后,FSM 开始按照波特率对数据位进行采样
- FSM 将采样到的数据位组装成完整字节并存入 RX FIFO 接收缓冲区
- 应用程序从 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 引脚 | 备注 |
|---|---|---|---|
| Serial | GPIO3 | GPIO1 | 一般情况下可直接使用 |
| Serial1 | GPIO9 | GPIO10 | 默认配置下 GPIO 6-12 是 FLASH 接口,不能被其他程序使用 |
| Serial2 | GPIO16 | GPIO17 | 一般情况下可直接使用 |

Serial1 为什么”很难用”?
Serial1 默认使用的 GPIO9 和 GPIO10 位于 GPIO 6-12 区间——这个区间恰好是 ESP32 内部 Flash 的接口引脚。因此如果直接使用 Serial1 默认配置,会与 Flash 存储发生冲突,导致程序无法正常运行。
解决方案有两种:
- 重新映射引脚:通过代码把 Serial1 的 RX/TX 引脚重新映射到不被 Flash 占用的其他 GPIO 上,避开 GPIO 6-12 区域;或者通过 DIO 方式上传代码来释放这些引脚。
- 使用 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_0和ota_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 的 onStart、onEnd、onProgress 和 onError 几个事件的回调:
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”真正走向”可量产的产品”。祝开发顺利!