ESP32从入门到精通:从点亮第一颗LED到构建完整物联网系统

 

ESP32不只是一块“可以联网的Arduino”。它是一颗集成处理器、Wi-Fi、蓝牙、丰富外设、实时操作系统和安全能力的物联网SoC。掌握ESP32,意味着你可以独立完成从传感器采集、设备控制,到联网通信、远程升级和低功耗设计的一整套嵌入式系统。

很多人第一次接触ESP32,通常从“点亮LED”开始;学了一段时间后,却容易停留在复制代码、调用库函数的阶段。真正的进阶,不是记住更多API,而是建立一套完整的工程思维:

  • • 如何选择合适的ESP32型号;
  • • 如何正确连接传感器和执行器;
  • • 如何设计非阻塞程序;
  • • 如何处理Wi-Fi断线、任务并发和数据存储;
  • • 如何降低功耗、实现OTA升级;
  • • 如何从Arduino原型迁移到ESP-IDF工程;
  • • 如何让设备长期、稳定、安全地运行。

本文将沿着“入门—进阶—工程化—实战”的路线,系统讲清ESP32开发中的核心知识。


一、ESP32到底是什么?

ESP32是乐鑫科技推出的一系列32位微控制器和物联网SoC。经典ESP32通常集成以下能力:

  • • 32位处理器;
  • • 2.4 GHz Wi-Fi;
  • • Bluetooth Classic和Bluetooth Low Energy;
  • • GPIO、ADC、DAC、PWM、UART、I²C、SPI、I²S等外设;
  • • 定时器、看门狗、RTC和低功耗模式;
  • • FreeRTOS实时操作系统;
  • • 安全启动、Flash加密和硬件加密加速等安全能力。

与传统Arduino Uno相比,ESP32的优势非常明显:

对比项 Arduino Uno 经典ESP32
处理器 8位AVR 32位处理器
主频 16 MHz 最高可达240 MHz
无线连接 无 Wi-Fi、蓝牙
多任务 基本不具备 原生运行FreeRTOS
GPIO与外设 较少 更丰富
内存 较小 更大,可选外部PSRAM
典型用途 基础电子实验 物联网、智能家居、机器人、数据采集、边缘设备

ESP32既可以作为一台独立运行的微型计算机,也可以作为其他主控芯片的无线通信协处理器。


二、ESP32家族怎么选?

“ESP32”现在已经不只是某一颗芯片,而是一个庞大的产品家族。不同型号在处理器架构、无线协议、USB、AI加速和功耗方面存在差异。

型号 主要特点 适合场景
经典ESP32 双核、Wi-Fi、Bluetooth Classic、BLE,资料丰富 初学、传统蓝牙音频、通用物联网
ESP32-C3 单核RISC-V、Wi-Fi、BLE,成本低 智能插座、传感器节点、低成本产品
ESP32-S3 双核、Wi-Fi、BLE、原生USB、向量指令,可配大容量PSRAM 摄像头、语音、显示屏、轻量AI、USB设备
ESP32-C6 Wi-Fi 6、BLE、IEEE 802.15.4 Matter、Thread、Zigbee、现代智能家居
ESP32-H2 BLE与IEEE 802.15.4,不集成Wi-Fi Thread/Zigbee低功耗终端
ESP32-P4 高性能RISC-V、丰富显示与多媒体外设,不以集成无线为核心 高性能人机界面、边缘计算、复杂显示设备

初学者推荐

如果你的目标是学习基础GPIO、Wi-Fi、传感器和Web控制,建议选择:

  1. 1. 经典ESP32 DevKit开发板:教程最多、兼容性成熟;
  2. 2. ESP32-C3开发板:价格低、RISC-V架构、适合简单联网设备;
  3. 3. ESP32-S3开发板:适合后续做摄像头、屏幕、USB和AI项目。

本文大部分Arduino示例以常见ESP32 DevKit为参考。不同开发板的LED引脚、Flash容量、PSRAM配置和可用GPIO可能不同,运行前应查看开发板原理图与引脚图。


三、开始之前,需要准备什么?

1. 基础硬件

建议准备:

  • • 一块ESP32开发板;
  • • 一根支持数据传输的USB线;
  • • 面包板与杜邦线;
  • • LED与220Ω~1kΩ限流电阻;
  • • 按键;
  • • DHT22、BME280或其他传感器;
  • • 有条件可增加OLED、继电器模块和逻辑分析仪。

2. 为什么USB线经常导致失败?

很多“无法下载程序”的问题并不是驱动或代码错误,而是USB线只能充电,不能传输数据。出现以下现象时,应优先更换数据线:

  • • 电脑没有出现新串口;
  • • 下载过程一直显示Connecting...;
  • • 开发板供电正常,但无法识别;
  • • 串口连接时断时续。

3. 供电必须注意

ESP32工作在3.3V逻辑电平。多数GPIO不具备5V容忍能力,不能把5V信号直接输入GPIO。

对于继电器、电机、舵机和大功率LED:

  • • 不要直接用GPIO供电;
  • • 使用三极管、MOSFET或驱动模块;
  • • 感性负载要加续流二极管;
  • • 外部电源与ESP32通常需要共地;
  • • 电机和无线发射可能造成电源波动,应增加滤波和足够的电源余量。

四、搭建Arduino开发环境

Arduino框架适合快速入门和原型开发。它隐藏了许多底层细节,但仍然可以调用ESP32的大部分硬件能力。

1. 安装Arduino IDE

安装Arduino IDE 2.x后,打开:

文件 → 首选项 → 其他开发板管理器地址

加入ESP32开发板索引:

https://espressif.github.io/arduino-esp32/package_esp32_index.json

然后进入:

工具 → 开发板 → 开发板管理器

搜索并安装esp32 by Espressif Systems。

2. 选择开发板和串口

常见经典开发板可以尝试选择:

ESP32 Dev Module

然后在“工具 → 端口”中选择开发板对应串口。

3. 常见下载参数

初学阶段可使用:

  • • Upload Speed:460800或115200;
  • • Flash Frequency:默认值;
  • • Flash Mode:默认值;
  • • Partition Scheme:Default;
  • • Core Debug Level:None或Info。

如果高速下载不稳定,可把Upload Speed降低为115200。


五、第一个程序:点亮LED

#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(500);

  digitalWrite(LED_BUILTIN, LOW);
  delay(500);
}

上传完成后,LED应以500毫秒间隔闪烁。

程序结构

Arduino程序包含两个核心函数:

void setup() {
  // 上电或复位后执行一次
}

void loop() {
  // setup结束后不断循环执行
}

pinMode()设置引脚模式,digitalWrite()输出高低电平。

为什么有时LED逻辑相反?

有些开发板上的LED采用低电平点亮,即:

digitalWrite(LED_BUILTIN, LOW);  // 点亮
digitalWrite(LED_BUILTIN, HIGH); // 熄灭

这不是程序错误,而是LED接法不同。


六、GPIO:最基础,也最容易踩坑

GPIO是ESP32与外部世界交互的入口,但并不是所有引脚都能随意使用。

以经典ESP32为例:

  • • GPIO34~GPIO39通常只能输入;
  • • GPIO6~GPIO11通常连接片上Flash,不应使用;
  • • GPIO0、GPIO2、GPIO5、GPIO12、GPIO15属于启动配置相关引脚;
  • • GPIO1和GPIO3通常用于串口下载与调试;
  • • 某些模块还会占用GPIO16和GPIO17连接PSRAM或Flash;
  • • 不同ESP32型号的限制并不相同。

因此,不要只根据“数字编号存在”就判断某个GPIO一定可用。

推荐的学习用GPIO

在常见经典ESP32开发板上,GPIO18、19、21、22、23、25、26、27、32、33通常比较适合一般数字输入输出。但仍应以具体开发板原理图为准。


七、按键输入与软件消抖

机械按键按下或松开时,不会瞬间产生一次干净的电平变化,而可能在几毫秒内快速抖动。直接读取可能导致一次按键被识别成多次。

下面使用millis()实现非阻塞消抖:

const int BUTTON_PIN = 4;
const int LED_PIN = 2;

bool ledState = false;
bool stableState = HIGH;
bool lastReading = HIGH;
unsigned long lastChangeTime = 0;
const unsigned long debounceDelay = 30;

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  bool reading = digitalRead(BUTTON_PIN);

  if (reading != lastReading) {
    lastChangeTime = millis();
    lastReading = reading;
  }

  if (millis() - lastChangeTime > debounceDelay &&
      reading != stableState) {
    stableState = reading;

    if (stableState == LOW) {
      ledState = !ledState;
      digitalWrite(LED_PIN, ledState);
    }
  }
}

这里使用INPUT_PULLUP启用内部上拉,因此:

  • • 未按下:HIGH;
  • • 按下并接地:LOW。

这种接法通常比悬空输入更稳定。


八、告别阻塞:用millis()代替delay()

delay()简单直观,但会暂停当前Arduino任务中的后续逻辑。项目一复杂,长时间delay()会导致:

  • • 按键响应迟钝;
  • • 网络连接维护不及时;
  • • 多个设备无法同时工作;
  • • 状态机难以扩展。

非阻塞闪烁示例:

const int LED_PIN = 2;
unsigned long previousMillis = 0;
const unsigned long interval = 500;
bool ledState = false;

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  unsigned long now = millis();

  if (now - previousMillis >= interval) {
    previousMillis = now;
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
  }

  // 这里还可以继续处理按键、网络、传感器等任务
}

关键思想是:不要等待时间过去,而是不断检查时间是否已经到达。


九、ADC:读取模拟传感器

ESP32可以通过ADC读取电位器、光敏电阻和模拟传感器输出。

const int ADC_PIN = 34;

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
}

void loop() {
  int raw = analogRead(ADC_PIN);
  Serial.println(raw);
  delay(200);
}

12位ADC的理论数字范围通常为0~4095,但要注意:

  1. 1. ADC并非高精度测量仪器;
  2. 2. 不同芯片和量程下可能存在非线性;
  3. 3. 电源噪声会影响读数;
  4. 4. 需要更高精度时,应校准或使用外部ADC;
  5. 5. 在经典ESP32上,使用Wi-Fi时应优先选择ADC1引脚,因为ADC2与Wi-Fi存在资源冲突限制。

简单滤波

int readAverage(int pin, int samples) {
  long sum = 0;

  for (int i = 0; i < samples; i++) {
    sum += analogRead(pin);
    delayMicroseconds(200);
  }

  return sum / samples;
}

移动平均能减小随机波动,但会增加响应延迟。滤波不是“越强越好”,需要根据传感器变化速度确定采样窗口。


十、PWM:控制LED亮度与电机速度

ESP32通过LEDC外设生成PWM信号。Arduino-ESP32 3.x推荐使用新的引脚式API:

const int PWM_PIN = 2;
const int PWM_FREQUENCY = 5000;
const int PWM_RESOLUTION = 8;

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

  if (!ledcAttach(PWM_PIN, PWM_FREQUENCY, PWM_RESOLUTION)) {
    Serial.println("LEDC初始化失败");
  }
}

void loop() {
  for (int duty = 0; duty <= 255; duty++) {
    ledcWrite(PWM_PIN, duty);
    delay(5);
  }

  for (int duty = 255; duty >= 0; duty--) {
    ledcWrite(PWM_PIN, duty);
    delay(5);
  }
}

如果你看到旧教程使用:

ledcSetup(...);
ledcAttachPin(...);

那通常是Arduino-ESP32 2.x时代的API。使用3.x核心时,应根据迁移文档改为ledcAttach()、ledcAttachChannel()和以引脚为参数的ledcWrite()。

驱动电机时的注意事项

PWM只能输出控制信号,不能让GPIO直接驱动电机。应使用:

  • • 小型直流电机:MOSFET或电机驱动芯片;
  • • 双向直流电机:H桥驱动器;
  • • 舵机:独立稳定电源,信号线连接GPIO;
  • • 步进电机:专用步进驱动器。

十一、常用通信接口:UART、I²C、SPI

1. UART

UART常用于:

  • • 串口调试;
  • • GPS模块;
  • • 指纹模块;
  • • RS485收发器;
  • • 与另一个MCU通信。
HardwareSerial sensorSerial(2);

void setup() {
  Serial.begin(115200);
  sensorSerial.begin(9600, SERIAL_8N1, 16, 17);
}

void loop() {
  while (sensorSerial.available()) {
    Serial.write(sensorSerial.read());
  }
}

参数中的16和17分别是RX与TX,可根据开发板调整。

2. I²C

I²C使用SDA和SCL两根信号线,可以在同一总线上连接多个不同地址的设备。

经典ESP32常见默认引脚:

  • • SDA:GPIO21;
  • • SCL:GPIO22。

I²C扫描器:

#include <Wire.h>

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);

  Serial.println("开始扫描I2C设备...");

  for (uint8_t address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    uint8_t error = Wire.endTransmission();

    if (error == 0) {
      Serial.printf("发现设备:0x%02X\n", address);
    }
  }
}

void loop() {
}

如果找不到设备,应检查:

  • • SDA、SCL是否接反;
  • • 是否共地;
  • • 设备电压是否正确;
  • • 上拉电阻是否存在;
  • • I²C地址是否被修改;
  • • 连接线是否过长。

3. SPI

SPI通常用于高速外设:

  • • TFT显示屏;
  • • SD卡;
  • • 高速ADC;
  • • 无线模块;
  • • 外部Flash。

SPI一般包含SCLK、MOSI、MISO和CS。多个从设备可以共享时钟和数据线,但通常需要独立CS引脚。


十二、连接Wi-Fi

ESP32最常见的用途就是连接无线网络。

#include <WiFi.h>

const char* WIFI_SSID = "你的WiFi名称";
const char* WIFI_PASSWORD = "你的WiFi密码";

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.persistent(false);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  Serial.print("正在连接Wi-Fi");
  unsigned long startTime = millis();

  while (WiFi.status() != WL_CONNECTED &&
         millis() - startTime < 15000) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\n连接成功");
    Serial.print("IP地址:");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("\n连接超时");
  }
}

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

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.reconnect();
  }

  delay(1000);
}

Wi-Fi的三种常见模式

  1. 1. STA模式:ESP32连接现有路由器;
  2. 2. AP模式:ESP32自己创建热点;
  3. 3. AP+STA模式:同时创建热点并连接其他路由器。

产品首次配网时,常采用AP模式或BLE配网;配置完成后再切换到STA模式。

不要把密码直接提交到公开仓库

示例中把Wi-Fi密码写进源码只是为了便于理解。真实项目可以使用:

  • • 单独的secrets.h并加入.gitignore;
  • • NVS保存配网信息;
  • • BLE或SoftAP配网;
  • • 生产阶段写入设备专属配置;
  • • 安全存储和密钥管理方案。

十三、搭建一个Web控制页面

下面让ESP32运行一个简单Web服务器,通过浏览器控制LED。

#include <WiFi.h>
#include <WebServer.h>

const char* WIFI_SSID = "你的WiFi名称";
const char* WIFI_PASSWORD = "你的WiFi密码";

const int LED_PIN = 2;
WebServer server(80);

String buildPage() {
  String html;
  html += "<!DOCTYPE html><html lang='zh-CN'><head>";
  html += "<meta charset='UTF-8'>";
  html += "<meta name='viewport' content='width=device-width,initial-scale=1'>";
  html += "<title>ESP32控制台</title></head><body>";
  html += "<h1>ESP32 LED控制</h1>";
  html += "<p><a href='/on'><button>打开</button></a></p>";
  html += "<p><a href='/off'><button>关闭</button></a></p>";
  html += "</body></html>";
  return html;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.print("访问地址:http://");
  Serial.println(WiFi.localIP());

  server.on("/", []() {
    server.send(200, "text/html; charset=utf-8", buildPage());
  });

  server.on("/on", []() {
    digitalWrite(LED_PIN, HIGH);
    server.sendHeader("Location", "/");
    server.send(302);
  });

  server.on("/off", []() {
    digitalWrite(LED_PIN, LOW);
    server.sendHeader("Location", "/");
    server.send(302);
  });

  server.onNotFound([]() {
    server.send(404, "text/plain; charset=utf-8", "页面不存在");
  });

  server.begin();
}

void loop() {
  server.handleClient();
}

这只是局域网内的入门示例。真实产品还需要考虑:

  • • 身份认证;
  • • HTTPS;
  • • 输入校验;
  • • CSRF与权限控制;
  • • 请求超时;
  • • 并发连接;
  • • 日志与错误处理。

不要把没有身份认证的控制接口直接暴露到公网。


十四、HTTP、MQTT与WebSocket怎么选?

HTTP

适合:

  • • 请求一次、返回一次;
  • • 调用REST API;
  • • 上传传感器数据;
  • • 下载配置或固件。

优点是通用、容易调试;缺点是每次请求开销相对较大。

MQTT

适合:

  • • 大量物联网设备;
  • • 发布/订阅模型;
  • • 云端下发控制命令;
  • • 设备状态与遥测数据上报。

典型主题设计:

devices/{deviceId}/telemetry
devices/{deviceId}/state
devices/{deviceId}/command
devices/{deviceId}/status

建议为每台设备分配唯一身份,不要让所有设备共享同一组长期凭据。

WebSocket

适合:

  • • 浏览器实时仪表盘;
  • • 双向低延迟控制;
  • • 高频状态更新;
  • • 实时日志输出。

ESP-NOW

适合多个ESP设备近距离低延迟通信,不依赖传统路由器。它非常适合遥控器、传感器网络和点对点控制,但仍需要设计可靠性、配对、重发与安全策略。


十五、蓝牙:Bluetooth Classic与BLE

经典ESP32通常同时支持Bluetooth Classic和BLE,但不同家族成员支持情况不同。例如很多较新的C系列和S系列重点支持BLE,不一定支持Bluetooth Classic。

Bluetooth Classic适合

  • • 传统串口透传;
  • • 某些音频协议;
  • • 与旧设备兼容。

BLE适合

  • • 低功耗传感器;
  • • 手机配网;
  • • 智能锁与穿戴设备;
  • • Beacon;
  • • 基于GATT的控制与数据交换。

BLE中几个重要概念:

  • • Peripheral:通常负责广播并提供服务;
  • • Central:扫描并连接外设;
  • • Service:一组相关功能;
  • • Characteristic:具体数据项;
  • • Notify:设备主动向客户端发送更新;
  • • UUID:服务或特征的唯一标识。

对于只需要BLE的资源受限项目,可以考虑较轻量的NimBLE协议栈。


十六、使用Preferences保存配置

ESP32断电后,普通变量会消失。Arduino框架中的Preferences库使用NVS保存键值数据,适合:

  • • Wi-Fi配置;
  • • 设备名称;
  • • 阈值;
  • • 校准参数;
  • • 启动次数;
  • • 少量用户设置。
#include <Preferences.h>

Preferences preferences;

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

  preferences.begin("device", false);

  unsigned int bootCount = preferences.getUInt("bootCount", 0);
  bootCount++;
  preferences.putUInt("bootCount", bootCount);

  String deviceName = preferences.getString("name", "esp32-node");

  Serial.printf("设备名:%s\n", deviceName.c_str());
  Serial.printf("启动次数:%u\n", bootCount);

  preferences.end();
}

void loop() {
}

NVS适合许多小型键值数据,不适合频繁写入大型文件。大量数据可使用LittleFS、FAT文件系统、SD卡或外部存储。

Flash存在擦写寿命限制。高频采样数据不要每次变化都立即写入,应采用缓存、批量写入、循环日志或磨损均衡策略。


十七、FreeRTOS:ESP32进阶的分水岭

ESP32的Arduino框架底层运行FreeRTOS。即使你只写setup()和loop(),程序也已经处于任务调度环境中。

1. 为什么需要任务?

当系统同时需要:

  • • 读取传感器;
  • • 刷新屏幕;
  • • 维持网络;
  • • 接收命令;
  • • 控制电机;
  • • 保存数据;

把所有逻辑塞进一个巨大的loop()会越来越难维护。FreeRTOS可以把职责拆分为多个任务。

2. 创建两个任务

void sensorTask(void* parameter) {
  while (true) {
    Serial.println("读取传感器");
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

void networkTask(void* parameter) {
  while (true) {
    Serial.println("维护网络连接");
    vTaskDelay(pdMS_TO_TICKS(2000));
  }
}

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

  xTaskCreate(
    sensorTask,
    "SensorTask",
    4096,
    nullptr,
    1,
    nullptr
  );

  xTaskCreate(
    networkTask,
    "NetworkTask",
    4096,
    nullptr,
    1,
    nullptr
  );
}

void loop() {
  vTaskDelay(pdMS_TO_TICKS(1000));
}

3. 任务优先级不是越高越好

优先级过高且不主动阻塞的任务,可能长期占用CPU,使其他任务无法运行。

任务循环中通常应使用:

vTaskDelay(...);

或者等待队列、信号量、事件组和通知,而不是空转。

4. 用队列传递数据

QueueHandle_t sensorQueue;

void producerTask(void* parameter) {
  int value = 0;

  while (true) {
    value++;
    xQueueSend(sensorQueue, &value, portMAX_DELAY);
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

void consumerTask(void* parameter) {
  int receivedValue;

  while (true) {
    if (xQueueReceive(sensorQueue, &receivedValue, portMAX_DELAY) == pdTRUE) {
      Serial.printf("收到数据:%d\n", receivedValue);
    }
  }
}

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

  sensorQueue = xQueueCreate(10, sizeof(int));
  if (sensorQueue == nullptr) {
    Serial.println("队列创建失败");
    return;
  }

  xTaskCreate(producerTask, "Producer", 3072, nullptr, 1, nullptr);
  xTaskCreate(consumerTask, "Consumer", 3072, nullptr, 1, nullptr);
}

void loop() {
  vTaskDelay(pdMS_TO_TICKS(1000));
}

队列比多个任务直接共享全局变量更安全。共享资源仍可能需要互斥锁。

5. 中断服务程序要短

中断中不要进行:

  • • 长时间计算;
  • • 网络请求;
  • • 文件写入;
  • • 大量串口打印;
  • • 阻塞等待。

常见做法是:中断只记录标志、发送任务通知或把数据放入专用队列,复杂工作交给普通任务。


十八、低功耗设计:从“能运行”到“能用一年”

电池设备的核心不是让程序“睡一下”,而是优化整个能量预算。

ESP32常见功耗状态包括:

  • • Active:CPU与外设正常工作;
  • • Modem-sleep:无线调制解调部分按条件休眠;
  • • Light-sleep:CPU暂停,较多状态可保留;
  • • Deep-sleep:大部分数字系统关闭,仅保留RTC域等必要部分。

定时唤醒示例

#include <Arduino.h>

constexpr uint64_t MICROSECONDS_PER_SECOND = 1000000ULL;
constexpr uint64_t SLEEP_SECONDS = 60;

RTC_DATA_ATTR uint32_t bootCount = 0;

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

  bootCount++;
  Serial.printf("第%u次启动\n", bootCount);

  // 在这里读取传感器、连接网络并上报数据

  esp_sleep_enable_timer_wakeup(
    SLEEP_SECONDS * MICROSECONDS_PER_SECOND
  );

  Serial.println("进入深度睡眠");
  Serial.flush();
  esp_deep_sleep_start();
}

void loop() {
}

降低功耗的正确顺序

  1. 1. 明确设备多久采样一次;
  2. 2. 缩短唤醒后的工作时间;
  3. 3. 避免每次都进行耗时DNS和长连接;
  4. 4. 降低传感器、稳压器和指示灯的静态功耗;
  5. 5. 关闭不用的外设;
  6. 6. 选择合适的唤醒源;
  7. 7. 使用电流表或功耗分析仪实测;
  8. 8. 根据真实数据计算电池寿命。

开发板上的USB转串口芯片、电源指示灯和稳压器静态电流,可能远高于ESP32芯片深睡电流。做低功耗产品时,不能只测开发板整体后就简单归因于芯片。


十九、OTA:让设备无需拆机升级

OTA允许设备在正常运行时通过网络接收新固件,并在重启后运行新版本。

Arduino环境下可以使用ArduinoOTA快速搭建局域网升级:

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

const char* WIFI_SSID = "你的WiFi名称";
const char* WIFI_PASSWORD = "你的WiFi密码";

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

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }

  ArduinoOTA.setHostname("esp32-device");

  ArduinoOTA.onStart([]() {
    Serial.println("开始OTA");
  });

  ArduinoOTA.onEnd([]() {
    Serial.println("OTA完成");
  });

  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("OTA错误:%u\n", error);
  });

  ArduinoOTA.begin();
}

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

局域网示例并不等于生产级OTA。正式产品应考虑:

  • • 固件签名与来源验证;
  • • HTTPS下载;
  • • A/B分区;
  • • 升级失败回滚;
  • • 版本兼容性;
  • • 防降级;
  • • 断电保护;
  • • 分批发布;
  • • 设备升级状态上报。

一次错误OTA可能让大批设备离线,因此发布流程与固件本身同样重要。


二十、看门狗、异常与稳定性

一个演示程序运行10分钟没有问题,不代表设备能连续运行一年。

常见故障来源

  • • Wi-Fi路由器重启;
  • • DNS临时失败;
  • • 服务器不可用;
  • • 内存泄漏或碎片化;
  • • 任务栈溢出;
  • • 电源跌落;
  • • 传感器死锁;
  • • I²C总线被拉低;
  • • Flash写入异常;
  • • 看门狗复位;
  • • 温度和电磁干扰。

工程上应具备的能力

  • • 每个网络操作都设置超时;
  • • 使用指数退避重连;
  • • 关键状态持久化;
  • • 记录复位原因;
  • • 监控剩余堆内存;
  • • 避免无限增长的String拼接;
  • • 为任务预留合理栈空间;
  • • 对传感器错误设置重试与降级;
  • • 通过日志区分警告、错误和致命故障。

查看内存:

Serial.printf("Free heap: %u bytes\n", ESP.getFreeHeap());
Serial.printf("Min free heap: %u bytes\n", ESP.getMinFreeHeap());

查看复位原因可使用ESP-IDF提供的系统接口。发现设备反复复位时,首先记录复位原因和启动日志,而不是盲目增加延时。


二十一、安全:物联网设备不能只考虑“能连上”

一台联网设备通常保存Wi-Fi凭据、设备密钥、用户数据或控制权限。安全设计应从项目开始阶段考虑。

1. 通信安全

  • • 云端通信优先使用TLS;
  • • 正确验证服务器证书;
  • • 不要为了“方便”永久关闭证书校验;
  • • 设备应拥有独立身份;
  • • 密钥不要硬编码在公开源码中;
  • • MQTT和HTTP接口要进行身份认证与授权。

2. 固件安全

ESP32平台可提供:

  • • Secure Boot:只允许启动经过授权签名的固件;
  • • Flash Encryption:降低直接读取外部Flash内容的风险;
  • • 签名OTA:验证升级包来源;
  • • eFuse:保存不可逆配置或密钥摘要;
  • • 硬件加密加速器。

启用安全启动和Flash加密会影响下载、调试和量产流程,而且部分eFuse配置不可逆。必须先在测试设备上完整验证,再进入生产模式。

3. 接口安全

量产设备还要评估:

  • • UART下载口是否保留;
  • • JTAG是否关闭或受控;
  • • 调试日志是否泄露凭据;
  • • 恢复出厂设置是否需要物理确认;
  • • 配网流程是否容易被劫持;
  • • Web管理页是否有默认密码;
  • • API是否存在越权操作。

安全不是加一个密码,而是从启动链、固件、通信、身份、调试接口到云端权限的完整体系。


二十二、从Arduino迁移到ESP-IDF

Arduino适合快速开发,但当项目需要更强控制力时,ESP-IDF通常更合适。

什么时候应该考虑ESP-IDF?

  • • 需要精细管理内存、任务和组件;
  • • 需要官方最新功能;
  • • 需要复杂分区与OTA策略;
  • • 需要安全启动和Flash加密;
  • • 需要量产、自动化测试与持续集成;
  • • 需要深入调试系统行为;
  • • 项目规模已经超过单文件草图。

ESP-IDF基本工作流

安装ESP-IDF后,一个典型流程是:

idf.py set-target esp32
idf.py menuconfig
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

macOS串口可能类似:

/dev/cu.usbserial-xxxx
/dev/cu.SLAB_USBtoUART
/dev/cu.usbmodemxxxx

Windows通常类似:

COM3
COM5

ESP-IDF最小示例

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void app_main(void)
{
    while (1) {
        printf("Hello, ESP-IDF!\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

ESP-IDF入口是app_main(),而不是Arduino的setup()与loop()。

ESP-IDF组件化目录示意

my_project/
├── CMakeLists.txt
├── sdkconfig
├── partitions.csv
├── main/
│   ├── CMakeLists.txt
│   └── main.c
└── components/
    ├── sensor/
    ├── network/
    ├── storage/
    └── device_control/

组件化的价值在于:

  • • 降低模块耦合;
  • • 便于单元测试;
  • • 可以复用驱动与业务模块;
  • • 便于多人协作;
  • • 构建依赖更清晰。

Arduino也可以作为ESP-IDF组件使用,因此迁移并不一定要一次性重写全部代码。


二十三、一个完整项目应该如何分层?

以“智能环境监测节点”为例,设备需要读取温湿度、显示数据、上传云端、接收配置并支持OTA。

建议分层:

┌──────────────────────────────┐
│        业务逻辑层            │
│ 阈值判断、告警、状态机、策略  │
├──────────────────────────────┤
│        服务层                │
│ 网络、MQTT、OTA、存储、时间   │
├──────────────────────────────┤
│        驱动层                │
│ 传感器、屏幕、按键、继电器    │
├──────────────────────────────┤
│        硬件与RTOS层          │
│ GPIO、I2C、SPI、FreeRTOS     │
└──────────────────────────────┘

建议的任务划分

任务 职责 典型周期
SensorTask 采集并滤波传感器数据 1~10秒
DisplayTask 刷新本地界面 100~500毫秒
NetworkTask 连接维护、MQTT收发 事件驱动
StorageTask 批量保存配置和日志 按需
ControlTask 执行控制策略 10~100毫秒
OTATask 固件升级 按需

任务不是越多越好。过度拆分会增加上下文切换、同步和调试成本。拆分标准应是职责、时间约束和资源访问边界。


二十四、状态机:复杂设备的核心组织方式

很多设备程序可以表示为状态机:

BOOT
  ↓
LOAD_CONFIG
  ↓
CONNECT_NETWORK ──失败──→ RETRY_WAIT
  ↓成功                    │
RUNNING ←──────────────────┘
  ↓收到升级命令
OTA_UPDATING
  ↓
REBOOT

简单枚举:

enum class DeviceState {
  Boot,
  Connecting,
  Running,
  Error
};

DeviceState state = DeviceState::Boot;

void loop() {
  switch (state) {
    case DeviceState::Boot:
      // 初始化
      state = DeviceState::Connecting;
      break;

    case DeviceState::Connecting:
      // 尝试联网
      if (WiFi.status() == WL_CONNECTED) {
        state = DeviceState::Running;
      }
      break;

    case DeviceState::Running:
      // 正常业务
      break;

    case DeviceState::Error:
      // 故障恢复
      break;
  }
}

状态机能避免大量相互嵌套的if,让异常恢复路径更明确。


二十五、实战项目:智能环境监测节点

项目目标

实现一台设备:

  • • 读取温湿度;
  • • OLED显示;
  • • 通过Wi-Fi连接MQTT服务器;
  • • 周期上报数据;
  • • 云端下发采样间隔;
  • • 配置保存到NVS;
  • • 网络中断后自动恢复;
  • • 支持OTA;
  • • 电池模式下进入深睡眠。

数据结构

struct SensorData {
  float temperature;
  float humidity;
  uint32_t timestamp;
  bool valid;
};

上报JSON示例

{
  "deviceId": "env-001",
  "temperature": 24.6,
  "humidity": 52.1,
  "rssi": -58,
  "firmware": "1.3.0"
}

必须处理的异常

  • • 传感器返回无效值;
  • • Wi-Fi断开;
  • • MQTT服务器不可达;
  • • 时间同步失败;
  • • 配置数据损坏;
  • • OLED未连接;
  • • OTA下载中断;
  • • 电池电压过低。

推荐策略

  1. 1. 传感器读取失败时保留上一份有效数据,但标记为过期;
  2. 2. 网络重连使用指数退避,例如1、2、4、8、16、30秒;
  3. 3. 采样与上报解耦,通过队列传递数据;
  4. 4. 配置修改后延迟合并写入NVS;
  5. 5. OTA升级时暂停非必要任务;
  6. 6. 发布新固件后先小范围灰度升级;
  7. 7. 设备启动后上报复位原因、固件版本和健康状态。

这类项目完成后,你就已经跨过了“会写ESP32代码”和“会设计ESP32设备”之间的分界线。


二十六、常见错误与排查方法

1. 一直显示Connecting

检查:

  • • USB线是否支持数据;
  • • 串口是否选对;
  • • 开发板型号是否正确;
  • • 是否需要按住BOOT再开始下载;
  • • GPIO0是否被外部电路错误拉低或拉高;
  • • 串口是否被其他软件占用;
  • • 下载速度是否过高。

2. Brownout detector was triggered

说明供电电压下降,常见原因:

  • • USB口供电不足;
  • • 线材压降过大;
  • • Wi-Fi发射瞬间电流增加;
  • • 电机或继电器与ESP32共用不稳定电源;
  • • 稳压器输出能力不足。

解决重点是改善电源,而不是关闭欠压检测。

3. Guru Meditation Error

通常表示程序出现异常,例如:

  • • 空指针访问;
  • • 数组越界;
  • • 栈溢出;
  • • 非法内存访问;
  • • 并发访问错误。

应保留完整异常输出,使用异常解码、符号表或调试器定位调用栈。

4. Wi-Fi连接后ADC读数异常

经典ESP32的ADC2与Wi-Fi存在资源冲突,应优先改用ADC1通道。

5. I²C偶尔卡死

可能原因:

  • • 线太长;
  • • 上拉阻值不合适;
  • • 电源不稳定;
  • • 某个从设备复位后持续拉低总线;
  • • 多任务同时访问I²C而未加互斥锁。

6. 程序运行一段时间后重启

检查:

  • • 剩余堆内存是否持续下降;
  • • 任务栈是否不足;
  • • 是否存在长时间阻塞;
  • • 看门狗日志;
  • • 电源波形;
  • • 网络库与第三方库版本;
  • • 是否频繁创建和销毁大对象。

二十七、代码质量:从能跑到可维护

不推荐

void loop() {
  // 几百行混合所有功能
}

推荐

void loop() {
  updateButtons();
  updateSensors();
  updateNetwork();
  updateDisplay();
  updateControlLogic();
}

进一步拆分为类或组件:

src/
├── main.cpp
├── config/
├── drivers/
├── services/
├── models/
└── app/

工程习惯

  • • 常量使用constexpr;
  • • 引脚集中定义;
  • • 网络操作统一超时;
  • • 错误码不要忽略;
  • • 日志包含模块、级别与时间;
  • • 协议数据定义版本;
  • • 配置结构增加校验;
  • • 对关键模块编写测试;
  • • 使用Git管理源码;
  • • 固件版本可追踪到提交记录。

二十八、ESP32学习路线图

第一阶段:基础电子与Arduino

掌握:

  • • 数字输入输出;
  • • 上拉、下拉、限流;
  • • 按键消抖;
  • • ADC与PWM;
  • • 串口调试;
  • • millis()非阻塞编程。

项目:

  • • 呼吸灯;
  • • 光控灯;
  • • 按键计数器;
  • • 温湿度显示器。

第二阶段:外设与网络

掌握:

  • • I²C、SPI、UART;
  • • Wi-Fi STA/AP;
  • • HTTP、MQTT、WebSocket;
  • • BLE基础;
  • • NVS与文件系统。

项目:

  • • Web控制继电器;
  • • MQTT环境监测;
  • • BLE配网设备;
  • • 局域网仪表盘。

第三阶段:实时系统与稳定性

掌握:

  • • FreeRTOS任务;
  • • 队列、互斥锁、事件组;
  • • 中断;
  • • 看门狗;
  • • 日志与故障恢复;
  • • 内存和任务栈分析。

项目:

  • • 多任务数据采集器;
  • • 实时机器人控制器;
  • • 多传感器网关。

第四阶段:产品化

掌握:

  • • Deep-sleep与功耗预算;
  • • OTA与回滚;
  • • 分区表;
  • • Secure Boot与Flash Encryption;
  • • 设备身份与密钥;
  • • 量产烧录、测试与追踪。

项目:

  • • 电池供电无线传感器;
  • • 可远程升级的智能家居终端;
  • • Matter/Thread设备;
  • • 带云端运维能力的产品原型。

第五阶段:ESP-IDF与底层能力

掌握:

  • • 组件系统与CMake;
  • • Kconfig与menuconfig;
  • • 原生驱动API;
  • • 自定义分区;
  • • 性能分析;
  • • JTAG调试;
  • • 自动化构建与CI/CD。

走完这条路线,你掌握的就不再只是ESP32,而是一整套现代嵌入式物联网开发方法。


二十九、学习ESP32最重要的五条原则

原则一:先看原理图,再写代码

引脚冲突、电平错误和供电问题,不是靠修改库函数能解决的。

原则二:优先写非阻塞程序

delay()可以用于第一个实验,但不应成为复杂系统的主要调度方式。

原则三:所有外部操作都可能失败

Wi-Fi会断、服务器会超时、传感器会失联、Flash会写失败。稳定系统必须设计失败路径。

原则四:用测量代替猜测

功耗要测电流,时序要看逻辑分析仪,电源要看示波器,内存要看运行数据。

原则五:安全和升级必须提前设计

OTA、分区和安全启动如果等到产品完成后再添加,往往会引发大规模架构返工。


结语

ESP32的学习过程,可以分为三个层次:

  1. 1. 让硬件动起来:点灯、读按键、接传感器;
  2. 2. 让设备连起来:Wi-Fi、蓝牙、HTTP、MQTT;
  3. 3. 让系统长期可靠运行:FreeRTOS、状态机、低功耗、OTA、安全和工程化。

真正的“精通”不是能背出多少API,而是面对一个实际需求时,能够完成硬件选型、软件架构、通信协议、异常恢复、功耗优化和安全设计,并最终交付一台可以持续运行的设备。

从点亮第一颗LED开始,但不要停在LED。


官方资料

  • • [ESP-IDF Programming Guide] https://docs.espressif.com/projects/esp-idf/en/stable/esp32/
  • • [ESP-IDF Get Started] https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/
  • • [Arduino-ESP32 Documentation] https://docs.espressif.com/projects/arduino-esp32/en/latest/
  • • [ESP32 GPIO Documentation] https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/gpio.html
  • • [ESP32 Sleep Modes] https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/sleep_modes.html
  • • [ESP32 OTA Documentation] https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/ota.html
  • • [ESP32 Secure Boot v2] https://docs.espressif.com/projects/esp-idf/en/stable/esp32/security/secure-boot-v2.html
  • • [ESP32 Flash Encryption] https://docs.espressif.com/projects/esp-idf/en/stable/esp32/security/flash-encryption.html
  • • [Espressif SoC Product Overview] https://www.espressif.com/en/products/socs

 

发表评论