首页 / 学习路径 / 第 07 章 · 综合项目:温湿度监测站
07
综合 · 毕业项目

综合项目:温湿度监测站

DHT11 采集 + OLED 显示 + WiFi 上报 + 超温告警。把前六章的能力全部拧成一个能真正用的桌面设备。

阅读时长 约 70 分钟硬件 6 件实战案例 1 个前置要求 第 1~6 章
1

本章要点

DHT11 内部是「电容式湿敏元件 + NTC 热敏电阻 + 一颗单片机」。湿敏元件的介电常数随湿度变化转化为电容值,单片机通过测量充放电时间反推湿度。采样间隔不能少于 2 秒。

I2C 只有两根线(SDA 数据 + SCL 时钟)却能挂多个设备,靠「地址」区分。0x3C 是 SSD1306 OLED 常见的 7 位从机地址,改完内容必须调 display() 才会推到屏幕。

综合项目的难点在于模块之间的时序协调:采样周期、显示刷新、HTTP 处理、告警节拍要互不阻塞。用 millis() 取模产生节拍比 delay() 优雅得多。

2

硬件清单与原理

名称数量作用与原理
DHT111 个电容式湿敏 + NTC 热敏 + 内置单片机,输出数字信号。采样间隔不能少于 2 秒。
SSD1306 OLED 屏1 个0.96 寸 128×64,I2C 接口,7 位地址通常 0x3C。内部有显存,display() 负责刷屏。
蜂鸣器 + LED各 1 个超温告警用。蜂鸣器需有源款,LED 串 220Ω 电阻。
3

接线说明

接线示意 · 温湿度监测站(6 个模块) ESP32 DevKit V1 GPIO4 / 18 / 19 / 21 / 22 GPIO4 DHT11 DATA 温湿度传感器 GPIO22 SCL GPIO21 SDA OLED 0.96 SSD1306 · 0x3C GPIO18 LED GPIO19 BUZ 有源蜂鸣器 GND 共地 I2C 总线原理 SDA 数据线 · GPIO21 SCL 时钟线 · GPIO22 两根线挂多个设备,靠地址区分 OLED 常见地址 0x3C(少数 0x3D) 改完内容必须调 display() 才会刷屏 DHT11 采样间隔不能少于 2 秒 读失败会返回 NaN,需 isnan() 防护
六个模块共用 ESP32,接线前先规划好引脚分配。

DHT11:VCC 接 3V3,DATA 接 GPIO4,GND 接 GND

OLED:VCC 接 3V3,GND 接 GND,SCL 接 GPIO22,SDA 接 GPIO21

① 蜂鸣器正极接 GPIO19,负极接 GND

② LED 阳极(经 220Ω)接 GPIO18,阴极接 GND

提示:I2C 只有两根线却能挂多个设备,靠地址区分。接反不会烧,但屏幕不亮。

4

实战案例 07-1 · 温湿度监测站

功能用途

每 5 秒采集温湿度,OLED 显示 + 串口打印 + 超温告警 + HTTP JSON 接口。

预期现象

OLED 显示 Temp 与 Humi,浏览器访问 /data 可看到 JSON 数据。

ch07_integrated.ino编译通过
/*
 * ============================================================================
 * 第 7 章 · 实战案例 07-1:综合项目 —— 温湿度监测站(WiFi 上报 + OLED 显示 + 蜂鸣器告警)
 * ----------------------------------------------------------------------------
 * 硬件清单:
 *   - ESP32 DevKit V1 × 1
 *   - USB 数据线 × 1
 *   - DHT11 温湿度传感器 × 1(3 脚:VCC / DATA / GND)
 *   - 0.96 寸 SSD1306 OLED 屏 × 1(I2C,4 脚:VCC / GND / SCL / SDA)
 *   - 有源蜂鸣器 × 1
 *   - LED × 1 + 220Ω 电阻 × 1
 *   - 面包板 + 杜邦线若干
 *
 * 接线对应表(这是本教程最关键的一张表):
 * ┌──────────┬─────────────────────────┬──────────────────────────┐
 * │ 模块     │ 模块引脚                 │ ESP32 引脚               │
 * ├──────────┼─────────────────────────┼──────────────────────────┤
 * │ DHT11    │ VCC (3.3V)              │ 3V3                      │
 * │ DHT11    │ DATA                    │ GPIO4(DHT 单总线数据线)│
 * │ DHT11    │ GND                     │ GND                      │
 * │ OLED     │ VCC (3.3V)              │ 3V3                      │
 * │ OLED     │ GND                     │ GND                      │
 * │ OLED     │ SCL (时钟)              │ GPIO22(I2C SCL 默认脚)  │
 * │ OLED     │ SDA (数据)              │ GPIO21(I2C SDA 默认脚)  │
 * │ 蜂鸣器   │ 正极                     │ GPIO19                   │
 * │ 蜂鸣器   │ 负极                     │ GND                      │
 * │ LED      │ 阳极(经 220Ω)           │ GPIO18                   │
 * │ LED      │ 阴极                     │ GND                      │
 * └──────────┴─────────────────────────┴──────────────────────────┘
 *
 * 元器件原理:
 *   - DHT11:内部是「电容式湿敏元件 + NTC 热敏电阻 + 一颗单片机」。
 *     湿敏元件的介电常数随湿度变化,转化为电容值;单片机再把这个电容充放电,
 *     通过测量充放电时间来反推湿度,原理是「RC 充放电时间与电容值成正比」。
 *   - SSD1306 OLED:I2C 总线只有两根线(SDA 数据 + SCL 时钟),
 *     采用「地址 + 寄存器」的方式读写,0x3C 是常见的 7 位从机地址。
 *   - 蜂鸣器:温度超过阈值时报警。
 *
 * 依赖库(Arduino IDE 库管理器搜索并安装):
 *   - DHT sensor library  by Adafruit
 *   - Adafruit Unified Sensor      by Adafruit
 *   - Adafruit SSD1306             by Adafruit
 *   - Adafruit GFX Library        by Adafruit
 *
 * 功能用途:
 *   做一个「桌面气象站」:
 *   - 每 5 秒采集一次温湿度
 *   - 实时显示在 OLED 屏上
 *   - 串口打印同样的数据
 *   - 温度超过 30℃ 时:蜂鸣器报警 + LED 闪烁
 *   - 同时在内部 HTTP 服务器上提供一个 JSON 数据接口
 *
 * 预期现象:
 *   OLED 屏幕显示 class="st">"Temp: xx.x C" 与 class="st">"Humi: xx.x %",
 *   串口同步打印,浏览器访问 http://<ESP32的IP>/data 可看到 JSON。
 * ============================================================================
 */

#include <Arduino.h>        // Arduino 核心库
#include <WiFi.h>           // WiFi 驱动
#include <WebServer.h>      // 轻量 HTTP 服务器
#include <DHT.h>            // DHT11 温湿度驱动
#include <Wire.h>           // I2C 总线库
#include <Adafruit_GFX.h>   // 绘图库
#include <Adafruit_SSD1306.h> // SSD1306 显示驱动

/* ==========================================================================
 * 一、必须修改的配置区
 * ======================================================================== */
const char* WIFI_SSID     = class="st">"YOUR_WIFI_NAME";      // ← 改成你的 WiFi 名称
const char* WIFI_PASSWORD = class="st">"YOUR_WIFI_PASSWORD";  // ← 改成你的 WiFi 密码

/* ==========================================================================
 * 二、引脚定义
 * ======================================================================== */
#define DHT_PIN      4      // DHT11 数据脚
#define BUZZER_PIN  19     // 蜂鸣器
#define LED_PIN     18     // 状态指示 LED
#define OLED_SDA    21     // OLED I2C 数据线
#define OLED_SCL    22     // OLED I2C 时钟线
#define OLED_ADDR   0x3C   // OLED 的 7 位 I2C 地址(多数模块是 0x3C,少数是 0x3D)

/* ==========================================================================
 * 三、参数常量
 * ======================================================================== */
const class="kw">uint8_t DHT_TYPE   = DHT11;       // 传感器型号:DHT11 或 DHT22
const class="kw">unsigned long READ_INTERVAL = 5000; // 采样间隔 5 秒
const float TEMP_ALARM_THRESHOLD = 30.0; // 温度告警阈值(摄氏度)

// OLED 显示屏参数:128x64 像素,I2C 模式
#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);  // -1 表示用默认的 SDA/SCL 引脚

// 创建 DHT 传感器对象。
// DHT 是一个类,库里的 .h 文件只提供类定义,必须在程序里「实例化」出一个具体对象,
// 才能调用它的 readHumidity() / readTemperature() 方法。
// 两个参数分别是:数据引脚编号、传感器型号(DHT11 或 DHT22)。
DHT dht(DHT_PIN, DHT_TYPE);

// HTTP 服务器
WebServer server(80);

/* ==========================================================================
 * 四、状态变量
 * ======================================================================== */
float    temperature = 0.0;    // 最新温度(℃)
float    humidity    = 0.0;    // 最新湿度(%)
class="kw">unsigned long lastReadTime = 0; // 上次采集时刻
bool     alarmActive = false; // 是否处于告警状态
bool     buzzerActive = false; // 蜂鸣器是否正在响

/* ==========================================================================
 * 五、自定义函数:刷新 OLED 显示
 * ======================================================================== */
void updateDisplay() {
  display.clearDisplay();   // 先清空上一帧内容,否则会残留

  // setTextSize(1) 表示 6x8 像素的小字;setTextColor(SSD1306_WHITE) 设置字色
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);          // 光标移到左上角
  display.println(class="st">"ESP32 Weather");  // 标题
  display.println(class="st">"---------------");

  // setCursor(x, y) 设置光标位置,y 每行约 8~16 像素
  display.setCursor(0, 24);
  display.print(class="st">"Temp: ");
  display.print(temperature, 1);     // 保留 1 位小数
  display.println(class="st">" C");

  display.setCursor(0, 40);
  display.print(class="st">"Humi: ");
  display.print(humidity, 1);
  display.println(class="st">" %");

  // 告警时在最下方用一行标记(这里用 # 号表示,OLED 画中文需要额外字库)
  if (alarmActive) {
    display.setCursor(0, 56);
    display.print(class="st">"!! HIGH TEMP !!");
  }

  display.display();   // 把缓冲区内容真正推送到屏幕(不写这行屏幕不会更新)
}

/* ==========================================================================
 * 六、自定义函数:返回 JSON 数据
 * ======================================================================== */
class="kw">String buildJson() {
  class="kw">String json = class="st">"{";
  json += class="st">"\"device\":\"esp32-weather-station\"";
  json += class="st">",\"temperature\":" + class="kw">String(temperature, 1);
  json += class="st">",\"humidity\":" + class="kw">String(humidity, 1);
  json += class="st">",\"alarm\":" + class="kw">String(alarmActive ? class="st">"true" : class="st">"false");
  json += class="st">",\"uptime\":" + class="kw">String((millis()) / 1000);
  json += class="st">"}";
  class="kw">return json;
}

/* ==========================================================================
 * 七、自定义函数:更新告警状态
 * ======================================================================== */
void updateAlarmState() {
  bool shouldAlarm = (temperature > TEMP_ALARM_THRESHOLD);   // 比较运算的结果就是 bool

  if (shouldAlarm != alarmActive) {
    alarmActive = shouldAlarm;
    Serial.print(class="st">"[ALARM] 告警状态变更 → ");
    Serial.println(alarmActive ? class="st">"触发(温度过高)" : class="st">"解除(温度恢复正常)");
  }
}

/* ==========================================================================
 * 八、HTTP 路由
 * ======================================================================== */
void handleRoot() {
  // 简单首页,直接跳到 JSON 接口说明
  class="kw">String html = class="st">"<!DOCTYPE html><html><head><meta charset='UTF-8'>";
  html += class="st">"<meta name='viewport' content='width=device-width,initial-scale=1'>";
  html += class="st">"<title>ESP32 气象站</title><style>";
  html += class="st">"body{font-family:monospace;background:#0b0f14;color:#e6edf3;padding:24px}";
  html += class="st">"h1{color:#22d3ee}pre{background:#111820;border:1px solid #22303c;padding:16px;border-radius:12px}";
  html += class="st">"</style></head><body><h1>ESP32 温湿度监测站</h1>";
  html += class="st">"<p>实时 JSON 数据:<a href='/data' style='color:#f5a524'>/data</a></p>";
  html += class="st">"<pre>" + buildJson() + class="st">"</pre></body></html>";
  server.send(200, class="st">"text/html", html);
}

void handleData() {
  server.send(200, class="st">"application/json", buildJson());   // 返回 JSON 格式数据
}

void handleNotFound() {
  server.send(404, class="st">"text/plain", class="st">"404 Not Found");
}

/* ==========================================================================
 * 九、初始化
 * ======================================================================== */
void setup() {
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println(class="st">"[SYSTEM] 温湿度监测站启动中...");

  /* ---- 外设初始化 ---- */
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);   // 有源蜂鸣器,低电平不响
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);     // LED 低电平点亮,故初始写 HIGH

  // DHT 传感器初始化。
  // 注意:DHT 库 v1.4+ 的 begin() 不再接收引脚和型号参数——
  // 引脚和型号已经在创建对象时传给了构造函数,所以这里直接无参调用即可。
  dht.begin();

  // I2C 引脚在 Adafruit_SSD1306 构造函数里传入 -1 时使用 Wire 的默认引脚,
  // 但为了严谨,这里显式再指定一次 GPIO21=SDA、GPIO22=SCL
  Wire.begin(OLED_SDA, OLED_SCL);

  // OLED 初始化:128x64,内存分配方式用 128*64/8 字节
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println(class="st">"[SYSTEM] OLED 初始化失败!请检查 SDA/SCL 接线与地址(0x3C / 0x3D)");
    // 不 return,继续跑,至少串口和 HTTP 还能用
  } else {
    Serial.println(class="st">"[SYSTEM] OLED 初始化成功");
    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0, 0);
    display.println(class="st">"ESP32 Weather");
    display.println(class="st">"Initializing...");
    display.display();
  }

  /* ---- WiFi 联网 ---- */
  Serial.print(class="st">"[WIFI] 正在连接 ");
  Serial.print(WIFI_SSID);
  Serial.println(class="st">" ...");

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  class="kw">unsigned long startTime = millis();
  class="kw">while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < 15000) {
    delay(500);
    Serial.print(class="st">".");
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println(class="st">"[WIFI] 连接成功");
    Serial.print("[WIFI] 访问地址: http://");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println(class="st">"[WIFI] 连接失败,传感器数据将只在串口输出");
  }

  /* ---- HTTP 服务 ---- */
  server.on(class="st">"/", handleRoot);
  server.on(class="st">"/data", handleData);
  server.onNotFound(handleNotFound);
  server.begin();

  Serial.println(class="st">"[SYSTEM] 初始化完成,进入主循环");
}

/* ==========================================================================
 * 十、主循环
 * ======================================================================== */
void loop() {
  class="kw">unsigned long now = millis();

  /* ---- 1. 定时采集传感器 ---- */
  if (now - lastReadTime >= READ_INTERVAL) {
    lastReadTime = now;

    // readHumidity 返回湿度(%),内部同时会更新温度值
    humidity = dht.readHumidity();
    // readTemperature 返回温度(℃)
    temperature = dht.readTemperature();

    // DHT 读取失败时会返回 NaN,必须检查,否则显示会是乱七八糟的
    if (isnan(humidity) || isnan(temperature)) {
      Serial.println(class="st">"[DHT] 读取失败!请检查 DHT11 的 VCC/DATA/GND 接线");
    } else {
      Serial.print(class="st">"[DHT] 温度: ");
      Serial.print(temperature);
      Serial.print(class="st">" ℃   湿度: ");
      Serial.print(humidity);
      Serial.println(class="st">" %");
    }

    updateDisplay();      // 刷新屏幕
    updateAlarmState();   // 更新告警状态
  }

  /* ---- 2. 告警指示:LED 快速闪烁 ---- */
  if (alarmActive) {
    // 用 millis 取模来产生 100ms 一次的节拍,避免 delay 阻塞
    if ((millis() / 100) % 2 == 0) {
      digitalWrite(LED_PIN, LOW);    // 点亮
    } else {
      digitalWrite(LED_PIN, HIGH);   // 熄灭
    }
    // 蜂鸣器间歇鸣叫(0.5 秒响,0.5 秒停),比一直响更不扰人
    if ((millis() / 1000) % 2 == 0) {
      digitalWrite(BUZZER_PIN, HIGH);
    } else {
      digitalWrite(BUZZER_PIN, LOW);
    }
  } else {
    digitalWrite(LED_PIN, HIGH);     // 正常状态 LED 常灭
    digitalWrite(BUZZER_PIN, LOW);    // 正常状态蜂鸣器静音
  }

  /* ---- 3. 处理 HTTP 请求 ---- */
  server.handleClient();
}
完整源码 319 行 · ch07_integrated/ch07_integrated.inoarduino-cli 编译通过
5

本章小结

① DHT11 是「传感器 + 单片机」一体化模块,输出数字信号,必须用专用库。

② DHT11 采样间隔不能少于 2 秒,库内部也做了限制。

③ OLED 改完内容必须调 display() 才会推到屏幕,不调用就白改。

④ isnan() 检查是所有数字传感器的必备防护,DHT 通信失败会返回 NaN。

⑤ 用 millis() 取模产生节拍比 delay() 优雅得多,是做多任务节奏的基础。

6

本章练习

练习 1

把采样间隔改成 2 秒,验证 DHT11 是否仍能正常读数。

练习 2

把告警阈值从 30 度改成 25 度,观察蜂鸣器响应。

练习 3

OLED 增加「信号强度」一行,从 WiFi.RSSI() 取值。

练习 4

挑战:把整个项目改成 FreeRTOS 多任务,各跑各的。

7

下一章

01
RESTART · 回到起点

恭喜完成全部七章

你已经具备独立完成物联网项目的能力。接下来可以挑战 MQTT、ESP-NOW、传感器阵列这些进阶方向。

回到起点