本章要点
DHT11 内部是「电容式湿敏元件 + NTC 热敏电阻 + 一颗单片机」。湿敏元件的介电常数随湿度变化转化为电容值,单片机通过测量充放电时间反推湿度。采样间隔不能少于 2 秒。
I2C 只有两根线(SDA 数据 + SCL 时钟)却能挂多个设备,靠「地址」区分。0x3C 是 SSD1306 OLED 常见的 7 位从机地址,改完内容必须调 display() 才会推到屏幕。
综合项目的难点在于模块之间的时序协调:采样周期、显示刷新、HTTP 处理、告警节拍要互不阻塞。用 millis() 取模产生节拍比 delay() 优雅得多。
硬件清单与原理
| 名称 | 数量 | 作用与原理 |
|---|---|---|
| DHT11 | 1 个 | 电容式湿敏 + NTC 热敏 + 内置单片机,输出数字信号。采样间隔不能少于 2 秒。 |
| SSD1306 OLED 屏 | 1 个 | 0.96 寸 128×64,I2C 接口,7 位地址通常 0x3C。内部有显存,display() 负责刷屏。 |
| 蜂鸣器 + LED | 各 1 个 | 超温告警用。蜂鸣器需有源款,LED 串 220Ω 电阻。 |
接线说明
DHT11:VCC 接 3V3,DATA 接 GPIO4,GND 接 GND
OLED:VCC 接 3V3,GND 接 GND,SCL 接 GPIO22,SDA 接 GPIO21
① 蜂鸣器正极接 GPIO19,负极接 GND
② LED 阳极(经 220Ω)接 GPIO18,阴极接 GND
提示:I2C 只有两根线却能挂多个设备,靠地址区分。接反不会烧,但屏幕不亮。
实战案例 07-1 · 温湿度监测站
每 5 秒采集温湿度,OLED 显示 + 串口打印 + 超温告警 + HTTP JSON 接口。
OLED 显示 Temp 与 Humi,浏览器访问 /data 可看到 JSON 数据。
/* * ============================================================================ * 第 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(); }
本章小结
① DHT11 是「传感器 + 单片机」一体化模块,输出数字信号,必须用专用库。
② DHT11 采样间隔不能少于 2 秒,库内部也做了限制。
③ OLED 改完内容必须调 display() 才会推到屏幕,不调用就白改。
④ isnan() 检查是所有数字传感器的必备防护,DHT 通信失败会返回 NaN。
⑤ 用 millis() 取模产生节拍比 delay() 优雅得多,是做多任务节奏的基础。
本章练习
把采样间隔改成 2 秒,验证 DHT11 是否仍能正常读数。
把告警阈值从 30 度改成 25 度,观察蜂鸣器响应。
OLED 增加「信号强度」一行,从 WiFi.RSSI() 取值。
挑战:把整个项目改成 FreeRTOS 多任务,各跑各的。