ESP32 实战教程:智能环境监测站(温湿度 + OLED + MQTT)

一、项目背景

随着物联网技术在智慧家居、智慧农业和工业监控领域的快速普及,环境数据的实时采集与远程可视化已成为嵌入式开发者的核心技能之一。本项目基于乐鑫 ESP32 双核芯片,集成 DHT22 温湿度传感器与 SSD1306 OLED 显示屏,通过 MQTT 协议将数据上报至云端 Broker(如 EMQ X、 Mosquitto),构成一个低门槛、高复用性的"端—边—云"完整链路。

整个系统的核心价值在于:

整个项目代码量约 150 行,硬件成本低于 30 元,非常适合作为 ESP32 入门到进阶的过渡项目。

二、硬件清单

序号 元器件 型号 / 规格 数量 参考单价
1 主控板 ESP32-DevKitC V4(30P 引脚版) 1 ¥18
2 温湿度传感器 DHT22 / AM2302(数字单总线) 1 ¥7
3 OLED 显示屏 0.96 寸 SSD1306 I2C(128×64) 1 ¥8
4 杜邦线 母对母 20cm(4P) 2 排 ¥2
5 面包板 400 孔 1 ¥3
6 电阻 10kΩ(上拉 DHT 数据线) 1 ¥0.1
7 Micro-USB 线 数据线 1 自备

软件环境:

三、接线图说明

实物图布局:把面包板横放,ESP32 插在中央偏左,OLED 插在右侧,DHT22 用三根杜邦线从左侧接入。


       ESP32-DevKitC                 面包板右区
  ┌──────────────────┐         ┌──────────────────┐
  │  3V3 ●───────┐   │         │  SSD1306 OLED    │
  │  GND ●───┐   │   │         │  VCC ── 3V3      │
  │  GPIO21●─┼───┼───┼─────────┤── SDA            │
  │  GPIO22●─┼───┼───┼─────────┤── SCL            │
  │          │   │   │         │  GND ── GND      │
  │  GPIO4 ●─┼───┼───┼─────────┤── DHT22 DATA     │
  │          │   │   │         │  VCC ── 3V3      │
  │  GND  ●─┼───┘   │         │  GND ── GND      │
  └──────────┼──────┘         └──────────────────┘
             │
            [10kΩ 上拉电阻,介于 3V3 与 GPIO4 之间]

**关键引脚对照表**:

模块引脚 ESP32 GPIO 说明
DHT22 VCC 3V3 供电 3.3V,**严禁接 5V**
DHT22 GND GND 共地
DHT22 DATA GPIO4 需 10kΩ 上拉至 3V3
OLED VCC 3V3 I2C 屏可直接 3.3V 供电
OLED GND GND 共地
OLED SDA GPIO21 ESP32 默认 I2C SDA
OLED SCL GPIO22 ESP32 默认 I2C SCL
注意:DHT22 数据线闲置时为高阻态,必须外接上拉电阻,否则采样会出现 NaN 错误。OLED 屏地址一般为 0x3C,若不显示可用 I2C Scanner 验证。

四、完整代码(Arduino 框架)


#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>

/* ========== WiFi 配置 ========== */
const char* ssid     = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

/* ========== MQTT 配置 ========== */
const char* mqtt_server   = "broker.emqx.io";   // 公共测试 Broker
const int   mqtt_port     = 1883;
const char* mqtt_topic    = "home/esp32/env";
const char* mqtt_user     = "";                 // 测试 Broker 可留空
const char* mqtt_pass     = "";

/* ========== DHT22 配置 ========== */
#define DHTPIN  4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

/* ========== OLED 配置 ========== */
#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

WiFiClient espClient;

unsigned long lastMsg = 0;
const long interval = 5000;   // 5 秒上报一次

/* ========== WiFi 连接 ========== */
void setup_wifi() {
  delay(10);
  Serial.print("\nConnecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected, IP: ");
}

/* ========== MQTT 接收回调(可选) ========== */
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Msg arrived [");
  Serial.print(topic);
  Serial.print("]: ");
  for (unsigned int i = 0; i < length; i++) Serial.print((char)payload[i]);
  Serial.println();
}

/* ========== MQTT 重连 ========== */
void reconnect() {
  while (!client.connected()) {
    String cid = "ESP32Client-";
    cid += String(random(0xffff), HEX);
    if (client.connect(cid.c_str(), mqtt_user, mqtt_pass)) {
      Serial.println(" OK");
    } else {
      Serial.print(" failed rc=");
      Serial.print(client.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  setup_wifi();
  client.setCallback(callback);

  dht.begin();

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    for (;;);
  }
  display.display();
  delay(2000);
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.display();
}

void loop() {
  if (!client.connected()) reconnect();
  client.loop();

  unsigned long now = millis();
  if (now - lastMsg > interval) {
    lastMsg = now;

    float t = dht.readTemperature();
    float h = dht.readHumidity();
    if (isnan(t) || isnan(h)) {
      Serial.println("DHT read error!");
      return;
    }

    /* ---- OLED 显示 ---- */
    display.clearDisplay();
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.setCursor(0, 16);
    display.setCursor(0, 32);
    display.print("Humi : "); display.print(h, 1); display.println(" %");
    display.setCursor(0, 52);
    display.display();

    /* ---- JSON 序列化 ---- */
    StaticJsonDocument<200> doc;
    doc["device"]      = "esp32_env_01";
    doc["temperature"] = t;
    doc["humidity"]    = h;
    doc["rssi"]        = WiFi.RSSI();
    doc["uptime_ms"]   = millis();

    char buf[256];
    serializeJson(doc, buf);
  }
}

将代码中 `YOUR_WIFI_SSID` 与 `YOUR_WIFI_PASSWORD` 替换为实际值后,选择开发板为 "ESP32 Dev Module",波特率 115200,烧录即可。

五、调试要点

  1. **串口监视器无输出**:检查 USB 线是否为数据线(部分充电线无数据通道);确认开发板端口在设备管理器中正常识别。
  2. **OLED 黑屏**:用 I2C 扫描例程确认地址是 0x3C 还是 0x3D;检查 SDA/SCL 是否接反。
  3. **DHT 一直返回 NaN**:99% 是缺上拉电阻,或采样间隔太短(官方建议 ≥ 2 秒)。
  4. **MQTT 反复重连**:先用 MQTTX、MQTTBox 等客户端测试 broker 可达;公司网络常屏蔽 1883 端口,可改用 8883 + SSL。
  5. **JSON 过长导致发布失败**:MQTT 默认包上限 256 字节,若字段过多需在 `PubSubClient` 中调用 `setBufferSize(512)`。

常用调试命令:


# 订阅查看实时数据(需安装 mosquitto-clients)
mosquitto_sub -h broker.emqx.io -t "home/esp32/env" -v

六、进阶方向

**方向一:低功耗 + 太阳能**

利用 ESP32 的 Deep-Sleep 模式(`esp_deep_sleep_start()`),将上报周期改为每 5 分钟一次,平均功耗可降至 0.8 mA 以下。配合 6V/2W 太阳能板 + 18650 电池 + TP4056 充放电模块,可实现户外长期部署。

**方向二:多传感器融合**

在 I2C 总线上级联 BH1750(光照)、SGP30(eCO₂/TVOC)、BMP280(气压),OLED 可分页轮询显示;MQTT 主题拆分为 `home/esp32/env/temp`、`home/esp32/env/light` 等,方便下游按需订阅。

**方向三:边缘 AI 异常检测**

将 TinyML 框架(如 EloquentTinyML)部署到 ESP32,在本地对采集到的温度曲线做滑动窗口分析,检测"温度突变 + 湿度异常"组合,识别开窗、漏水等场景并通过 MQTT 主动告警,彻底摆脱对云端推理的依赖。

通过以上三步迭代,原始的"采集—显示—上报"Demo 就可升级为可商用的边缘节点原型。后续也可将 Arduino 框架迁移到 ESP-IDF,借助 FreeRTOS 多任务与 NVS 存储,进一步提升稳定性与可维护性。