Chapter 12 · Arduino 入门

综合实战:智能环境监测站

把前 11 章串起来:ESP32 + DHT11 + OLED + 按键,做一个会显示、能联网上报、可切换页面的完整作品。

课程进度100%

这个项目用到了哪些知识

项目目标
做一台桌面环境监测站:实时采集温湿度(DHT11)显示在 OLED 上,按键切换"实时/统计"两个页面,每隔一段时间通过 WiFi 把数据上报到云,LED 在超阈值时报警。这是一个能落地、可扩展的完整物联网小作品。
整合的知识点
第 6 章传感器(DHT11 读温湿度)、第 8 章显示(U8g2 驱动 OLED)、第 3 章按键(消抖 + 页面切换)、第 9 章非阻塞定时(millis 让采集/显示/上报各走各的节奏)、第 11 章 WiFi(HTTPClient 上报)。一个真实项目就是把这些零件拼成整体。
硬件清单
ESP32 开发板 ×1、DHT11 温湿度模块 ×1、0.96" SSD1306 OLED(I2C)×1、轻触按键 ×1、LED + 330Ω 电阻 ×1、面包板 + 杜邦线若干。总成本约几十元,全是前面章节出现过的元件。
非阻塞架构
整个程序不用一个 delay。采集(每 2s)、刷屏(每 200ms)、上报(每 30s)各配一组 millis 计时变量,loop 飞速空转、各任务时间到才执行。这样按键随时响应、屏幕流畅、联网不卡——这是可靠嵌入式程序的骨架。
状态机 (State Machine)
用一个变量 page 表示当前显示哪一页(0=实时、1=统计),按键切换它,刷屏函数根据 page 画不同内容。这种"用状态变量 + 事件驱动切换"的写法就是状态机,是组织复杂交互逻辑的利器,比一堆嵌套 if 清晰得多。
数据统计与阈值报警
程序维护最高/最低温度等统计值,并设温度阈值:超过就点亮报警 LED。这体现了"采集→处理→决策→执行"的完整闭环——不只是显示数据,还能根据数据自动动作。
模块化组织
把功能拆成 readSensor()updateDisplay()uploadData()handleButton() 等独立函数,loop 里只做"调度"。函数职责单一、命名清晰,项目大了也不乱——这是从"能跑"到"好维护"的分水岭。
可扩展方向
这套骨架能长成很多东西:换 MQTT 接入语音助手、加光照/PM2.5 传感器、加继电器联动风扇/加湿器、数据存 SD 卡或云端画曲线、做成多节点组网。掌握了整合方法,剩下的只是往框架里加零件。

系统接线图

智能环境监测站 · 整体接线 ┌──────────────── ESP32 ────────────────┐ │ │ DHT11 ─────┤ GPIO4 ◀── DATA(温湿度) │ OLED ──┬───┤ GPIO21 ◀─▶ SDA ┐ │ └───┤ GPIO22 ◀─▶ SCL ┘ I2C 屏(地址 0x3C) │ 按键 ──────┤ GPIO5 ◀── 一端接 GPIO,另一端接 GND │ LED ───────┤ GPIO2 ──▶ 串 330Ω 再到 LED 到 GND │ │ │ 电源 ──────┤ 5V / GND ── 所有模块 VCC/GND 共电共地 │ └────────────────────────────────────────┘ 全部 GND 必须连在一起(共地),I2C 屏与主板共用 SDA/SCL。

整合状态机

loop 调度 + 页面状态机 loop() 每轮做四件"检查"(都不阻塞): ┌─ 每 2s ? → readSensor() 读温湿度、更新统计、判阈值 ├─ 每 200ms?→ updateDisplay() 按 page 画当前页 ├─ 每 30s ? → uploadData() WiFi 上报到云 └─ 随时 → handleButton() 检测按键(消抖)切换 page 页面状态机: [page 0 实时页] ──按键──▶ [page 1 统计页] ▲ │ └──────────按键────────────┘

完整代码

下面是可直接编译上传的完整工程(ESP32,需装 U8g2、DHT sensor library)。填入你的 WiFi 后即可运行:

#include <WiFi.h>
#include <HTTPClient.h>
#include <U8g2lib.h>
#include <DHT.h>

// ---------- 引脚与配置 ----------
#define DHTPIN    4
#define DHTTYPE   DHT11
const int btnPin = 5;
const int ledPin = 2;
const float TEMP_ALARM = 30.0;   // 温度报警阈值 ℃

const char* ssid = "你的WiFi名";
const char* pass = "你的WiFi密码";

// ---------- 对象 ----------
DHT dht(DHTPIN, DHTTYPE);
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0);

// ---------- 全局状态 ----------
float temp = 0, humi = 0;
float tMax = -99, tMin = 99;
int page = 0;                 // 0=实时页 1=统计页
bool alarm = false;

// 三组非阻塞定时器
unsigned long tSensor = 0, tDisp = 0, tUpload = 0;
unsigned long lastBtn = 0;
bool lastBtnState = HIGH;

// ---------- 功能函数 ----------
void readSensor() {
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  if (isnan(t) || isnan(h)) return;   // 读失败就跳过
  temp = t; humi = h;
  if (t > tMax) tMax = t;             // 更新统计
  if (t < tMin) tMin = t;
  alarm = (t >= TEMP_ALARM);           // 判阈值
  digitalWrite(ledPin, alarm ? HIGH : LOW);
}

void updateDisplay() {
  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_ncenB08_tr);
    if (page == 0) {                  // ---- 实时页 ----
      u8g2.drawStr(0, 12, "== Live ==");
      char buf[24];
      sprintf(buf, "Temp: %.1f C", temp);
      u8g2.drawStr(0, 32, buf);
      sprintf(buf, "Humi: %.0f %%", humi);
      u8g2.drawStr(0, 48, buf);
      if (alarm) u8g2.drawStr(0, 63, "! ALARM !");
    } else {                          // ---- 统计页 ----
      u8g2.drawStr(0, 12, "== Stats ==");
      char buf[24];
      sprintf(buf, "Max: %.1f C", tMax);
      u8g2.drawStr(0, 32, buf);
      sprintf(buf, "Min: %.1f C", tMin);
      u8g2.drawStr(0, 48, buf);
    }
  } while (u8g2.nextPage());
}

void uploadData() {
  if (WiFi.status() != WL_CONNECTED) return;
  HTTPClient http;
  http.begin("http://example.com/api/data");
  http.addHeader("Content-Type", "application/json");
  String body = "{\"temp\":" + String(temp) +
                ",\"humi\":" + String(humi) + "}";
  http.POST(body);
  http.end();
}

void handleButton() {
  bool s = digitalRead(btnPin);
  // 下降沿 + 50ms 消抖 → 切换页面
  if (lastBtnState == HIGH && s == LOW && millis() - lastBtn > 50) {
    page = 1 - page;              // 0↔1 翻转
    lastBtn = millis();
  }
  lastBtnState = s;
}

// ---------- 初始化 ----------
void setup() {
  pinMode(btnPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
  dht.begin();
  u8g2.begin();

  WiFi.begin(ssid, pass);       // 后台连,不阻塞主流程太久
}

// ---------- 主循环:纯调度,无 delay ----------
void loop() {
  unsigned long now = millis();

  if (now - tSensor >= 2000)  { tSensor = now; readSensor(); }
  if (now - tDisp   >= 200)   { tDisp   = now; updateDisplay(); }
  if (now - tUpload >= 30000) { tUpload = now; uploadData(); }
  handleButton();   // 每轮都查,按键最灵敏
}
读代码的正确姿势

先看 loop()——它只有四行调度,一眼看清"每隔多久干什么"。再顺着点进每个函数看细节。这种"顶层调度极简、细节封装进函数"的结构,正是你未来读别人代码、写大项目时该追求的。任何复杂系统,拆开都是这样一层层的小函数。

调试与移植提示

① OLED 不亮先跑第 8 章的 I2C 扫描确认地址;② DHT11 读到 NaN 检查间隔≥1s 和上拉;③ 想在 Uno 上跑?去掉 WiFi/上报部分即可(Uno 无 WiFi),显示+按键+采集完全通用;④ 把 uploadData 换成 PubSubClient 发 MQTT,就能接入巴法云被小爱同学播报。整个骨架高度可裁剪、可扩展。

从这里继续:进阶路线

方向怎么加用到的章
接语音助手上报改 MQTT + 巴法云第 11 章
多传感器加光照/气压/PM2.5(I2C)第 6、10 章
自动联动超阈值继电器开风扇/加湿器第 3、7 章
数据可视化ThingSpeak/自建后端画曲线第 11 章
本地网页WebServer 手机看实时数据第 11 章
课程总结:你已经会做完整项目了

恭喜走到最后一章!这个环境监测站把整门课串成了闭环:用 DHT11 采集(第 6 章)、U8g2 驱动 OLED 显示(第 8 章)、按键消抖切换页面(第 3 章)、状态机组织交互、millis 非阻塞让采集/刷屏/上报三节奏并行(第 9 章,全程无 delay)、WiFi + HTTPClient 上报云端(第 11 章),还实现了统计与阈值报警的完整"采集→处理→决策→执行"闭环。真实项目的精髓不是某个炫技,而是把零件拼成可维护的整体:顶层 loop 只做调度、功能拆成单一职责的小函数、共享状态清晰、绝不用 delay 阻塞。掌握了这套方法,后面接 MQTT、加传感器、做联动、上云画曲线,都只是往这个框架里添砖加瓦。从第 1 章点亮第一颗 LED,到现在做出联网监测站——你已经具备独立完成 Arduino 创客项目的能力了。接下来,去动手做点自己想做的东西吧。

⚒️ 配套开发者工具箱 · 17 款免费在线工具