需求洞察

你有没有过这种体验:冬天关窗开空调,写代码两小时就开始打哈欠、脑子转不动,以为是自己"困了",其实多半是 CO₂ 浓度从 600ppm 飙到了 1500ppm+;夏天 35°C 闷热得不行,但拿温度计一量却只有 28°C——是湿度 80% 让体感失真;早晨觉得空气"挺清新的",出门才知道 PM2.5 已经 150。空气不会说话,但你的身体一直在用疲劳、头晕、打喷嚏"回应"。

市面上的空气检测仪普遍不便宜:小米米家空气检测仪 ¥349,青萍空气检测仪 ¥599,霍尼韦尔 PM2.5 检测仪 ¥899。对于一个"就想知道桌面 PM2.5 多少"的极客来说,价格离谱。而 DIY 一个简易的桌面空气质量监测站,BOM 物料 122 元就够,而且可以二次开发(阈值、报警逻辑、显示内容都在 Arduino 代码里改)。

本项目的目标是用 ¥122 的物料,做出一个能实时显示温度、湿度、PM1.0、PM2.5、PM10 五项数据,并在异常时通过 LED + 蜂鸣器报警的桌面空气监测站——所有数据本地化,零云端依赖,零隐私泄露。

核心痛点

方案设计

本项目采用「感知 + 主控 + 显示 + 反馈」四件套架构,以 ESP32 为中心,外围 4 个模块各司其职:

📐 系统架构与模块选型
模块型号接口功能
主控ESP32 DevKit V1双核 240MHz,34 个 GPIO,3.3V 逻辑
温湿度DHT22(AM2302)单总线 GPIO 4-40~80°C ±0.5°C,0~100%RH ±2%
PM2.5PMS5003(G7 激光)UART2 GPIO 16/170~1000 μg/m³ 激光散射,国产检测仪同款
显示SSD1306 OLED 0.96"I2C GPIO 21/22128x64 像素,白色,实时显示 4 项数据
报警三色 LED 模块 + 有源蜂鸣器GPIO 25/26/27 + 123 级状态指示(绿/黄/红)+ 声光报警

三级报警阈值(参考国标 GB/T 18801)

🚨 报警阈值与触发逻辑
等级触发条件LED 状态蜂鸣器用户感知
🟢 正常PM2.5 ≤ 35 且 温湿正常绿色常亮静音安心工作
🟡 警告温/湿异常 或 PM2.5 35-75黄色常亮静音开窗/加湿
🔴 报警PM2.5 > 75红色闪烁2kHz 间歇响立刻开窗 + 戴口罩

阈值定义:温湿异常 = 温度 > 30°C 或 < 16°C,或 湿度 > 70% 或 < 30%;PM2.5 阈值参考国标 GB/T 18801-2022 空气质量分指数:≤35 为优,35-75 为良,>75 为轻度污染需关注。CO₂ 监测本项目暂不接入(专业 CO₂ 传感器 SCD30 单价 ¥80+,本方案用 PM2.5 间接反映通风状况——PM2.5 升高通常意味着关窗/通风差,CO₂ 往往同步升高)。

技术亮点

物料清单

总成本 ¥122,比小米米家空气检测仪便宜 65%。所有物料在淘宝/嘉立创都能买到,BOM 9 项:

🛒 物料清单(2026 年 6 月淘宝均价)
数量物料单价小计
1ESP32 DevKit V1(Type-C 口,30 针版)¥30¥30
1DHT22 温湿度传感器(AM2302,单总线)¥10¥10
1PMS5003 激光 PM2.5 传感器(G7 模组,含 1m 线)¥40¥40
1SSD1306 OLED 0.96 寸显示屏(I2C 4 针版)¥12¥12
1三色 LED 模块(共阴极,3 GPIO 控制)¥3¥3
1有源蜂鸣器模块(3.3V/5V 通用,100mA)¥2¥2
1 包杜邦线(公-公 40 根 + 公-母 40 根)¥5¥5
1面包板 400 点(SYB-170)¥5¥5
15V 3A 电源适配器(DC 5.5x2.1 母头,PMS5003 专用)¥15¥15
总计¥122

采购建议

接线与结构

ESP32 DevKit V1 的丝印实际位置说明(不是抽象 GPIO 编号,避免新手对着丝印找不着脚):

ESP32 DevKit V1 排针位置参考

完整接线清单(带颜色 + 丝印)

🔌 接线表(5 要素:模块+引脚+颜色+目标+丝印)
模块引脚颜色连接目标丝印位置
DHT22DATA(数据)🟢 绿线ESP32 GPIO 4右侧排针第 5 脚,标 D4
DHT22VCC(电源)🔴 红线ESP32 3.3V右侧排针第 1 脚,标 3V3
DHT22GND(地)⚫ 黑线ESP32 GND右侧排针第 2 脚
PMS5003TX(数据输出)🟢 绿线ESP32 GPIO 16(RX2)右侧排针第 6 脚,标 RX2
PMS5003RX(数据输入)🟡 黄线ESP32 GPIO 17(TX2)右侧排针第 7 脚,标 TX2
PMS5003VCC(电源)🔴 红线外部 5V 电源正极DC 5.5x2.1 母头内芯(务必 5V,不能 3.3V)
PMS5003GND(地)⚫ 黑线ESP32 GND + 外部 5V 电源负极ESP32 右侧排针第 2 脚;务必共地!
OLED 0.96"VCC(电源)🔴 红线ESP32 3.3V右侧排针第 1 脚,标 3V3
OLED 0.96"GND(地)⚫ 黑线ESP32 GND右侧排针第 2 脚
OLED 0.96"SDA(数据)🟡 黄线ESP32 GPIO 21右侧排针第 11 脚,标 D21
OLED 0.96"SCL(时钟)⚪ 白线ESP32 GPIO 22右侧排针第 14 脚,标 D22
三色 LED 模块R(红色)🔴 红线ESP32 GPIO 25左侧排针第 8 脚,标 D25
三色 LED 模块Y(黄色)🟡 黄线ESP32 GPIO 26左侧排针第 7 脚,标 D26
三色 LED 模块G(绿色)🟢 绿线ESP32 GPIO 27左侧排针第 6 脚,标 D27
三色 LED 模块GND(地)⚫ 黑线ESP32 GND左侧排针第 2 脚
蜂鸣器I/O(信号)🟡 黄线ESP32 GPIO 12左侧排针第 4 脚,标 D12
蜂鸣器VCC(电源)🔴 红线ESP32 3.3V右侧排针第 1 脚,标 3V3
蜂鸣器GND(地)⚫ 黑线ESP32 GND右侧排针第 2 脚

⚠️ 关键提醒(不踩这些坑就别想跑通)

3D 打印外壳建议(可选)

如果觉得裸板太乱,可以 3D 打印一个 100mm x 80mm 的外壳(PMS5003 单独开孔露出进气口)。嘉立创 3D 打印 ¥30 起。

核心代码

使用 Arduino 框架,需要安装 4 个库:DHT sensor library(Adafruit)、PMS library(mariuszkacki)、Adafruit_SSD1306、Adafruit_GFX。建议 Arduino IDE 2.x + ESP32 core 2.0.14+。

// =============================================================
// 桌面温湿度空气质量监测站
// ESP32 + DHT22 + PMS5003 + OLED + 三色 LED + 蜂鸣器
// 库:Adafruit DHT sensor library / PMS / Adafruit_SSD1306
// =============================================================

#include <DHT.h>
#include <PMS.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// ── 引脚定义 ──
#define DHT_PIN    4        // DHT22 数据线(单总线)
#define DHT_TYPE   DHT22

#define LED_RED    25       // 红色 LED(PM2.5 超标报警)
#define LED_YELLOW 26       // 黄色 LED(温湿异常警告)
#define LED_GREEN  27       // 绿色 LED(正常)
#define BUZZER     12       // 有源蜂鸣器(高电平触发)

#define PMS_RX     16       // ESP32 RX2 接 PMS5003 TX
#define PMS_TX     17       // ESP32 TX2 接 PMS5003 RX

// ── 报警阈值(参考 GB/T 18801-2022)──
#define PM25_WARN    35.0   // PM2.5 > 35 警告
#define PM25_ALARM   75.0   // PM2.5 > 75 报警
#define TEMP_HIGH    30.0   // 温度 > 30°C 警告
#define TEMP_LOW     16.0   // 温度 < 16°C 警告
#define HUMI_HIGH    70.0   // 湿度 > 70% 警告
#define HUMI_LOW     30.0   // 湿度 < 30% 警告

// ── OLED 配置 ──
#define SCREEN_W  128
#define SCREEN_H  64
#define OLED_ADDR 0x3C
#define OLED_RESET -1

// ── 初始化对象 ──
DHT dht(DHT_PIN, DHT_TYPE);
HardwareSerial pmsSerial(2);     // ESP32 硬件串口 2
PMS pms(pmsSerial);
PMS::DATA pmsData;
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, OLED_RESET);

// ── 全局变量 ──
float temperature = 0.0;
float humidity    = 0.0;
float pm1_0       = 0.0;
float pm2_5       = 0.0;
float pm10        = 0.0;
unsigned long lastSample = 0;
const unsigned long SAMPLE_INTERVAL = 2000;  // 2 秒采样一次

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println(F("=== Air Monitor Booting ==="));

  // LED 引脚模式
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_YELLOW, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  pinMode(BUZZER, OUTPUT);
  digitalWrite(LED_RED, LOW);
  digitalWrite(LED_YELLOW, LOW);
  digitalWrite(LED_GREEN, LOW);
  digitalWrite(BUZZER, LOW);

  // DHT22 初始化
  dht.begin();

  // PMS5003 初始化(Serial2, 9600 波特率, RX=GPIO16, TX=GPIO17)
  pmsSerial.begin(9600, SERIAL_8N1, PMS_RX, PMS_TX);
  pms.passiveMode();    // 切被动模式:按需读取,传感器不会主动上报
  delay(1000);          // 等待 PMS5003 启动

  // OLED 初始化
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println(F("OLED init failed"));
    while (true);  // 卡住,方便排查
  }
  display.display();         // 显示 Adafruit logo
  delay(1500);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  // 开机自检:三色 LED 依次亮
  testLeds();

  Serial.println(F("=== Boot Complete ==="));
}

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

  // 每 2 秒采一次样
  if (now - lastSample >= SAMPLE_INTERVAL) {
    lastSample = now;

    // 1. 读 DHT22 温湿度
    float t = dht.readTemperature();
    float h = dht.readHumidity();
    if (!isnan(t) && !isnan(h)) {
      temperature = t;
      humidity    = h;
    }

    // 2. 读 PMS5003(主动 wakeup + 读取)
    pms.wakeUp();
    delay(30);
    if (pms.read(pmsData)) {
      pm1_0 = pmsData.PM_AE_UG_1_0;
      pm2_5 = pmsData.PM_AE_UG_2_5;
      pm10  = pmsData.PM_AE_UG_10_0;
    }
    pms.sleep();

    // 3. OLED 显示
    displayData(temperature, humidity, pm1_0, pm2_5, pm10);

    // 4. 报警判断
    controlAlerts(temperature, humidity, pm2_5);

    // 5. 串口打印(方便调试)
    Serial.print("T:"); Serial.print(temperature, 1); Serial.print("C  ");
    Serial.print("H:"); Serial.print(humidity, 0);    Serial.print("%  ");
    Serial.print("PM1.0:"); Serial.print(pm1_0, 0);  Serial.print("  ");
    Serial.print("PM2.5:"); Serial.print(pm2_5, 0);  Serial.print("  ");
    Serial.print("PM10:");  Serial.println(pm10, 0);
  }
}

// ── 辅助函数:OLED 显示 ──
void displayData(float t, float h, float pm1, float pm25, float pm10v) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  // 第 1 行:温度
  display.setCursor(0, 0);
  display.print(F("Temp : "));
  display.print(t, 1);
  display.println(F(" C"));

  // 第 2 行:湿度
  display.setCursor(0, 12);
  display.print(F("Humi : "));
  display.print(h, 0);
  display.println(F(" %"));

  // 第 3 行:PM2.5
  display.setCursor(0, 24);
  display.print(F("PM2.5: "));
  display.print(pm25, 0);
  display.println(F(" ug/m3"));

  // 第 4 行:PM1.0 / PM10
  display.setCursor(0, 36);
  display.print(F("PM1/10: "));
  display.print(pm1, 0);
  display.print(F("/"));
  display.print(pm10v, 0);

  // 底部状态栏
  display.setCursor(0, 54);
  if (pm25 > PM25_ALARM) {
    display.println(F("[!!] ALARM  OPEN WINDOW"));
  } else if (pm25 > PM25_WARN || t > TEMP_HIGH || t < TEMP_LOW
             || h > HUMI_HIGH || h < HUMI_LOW) {
    display.println(F("[~]  WARN  CHECK AIR"));
  } else {
    display.println(F("[OK] GOOD  WORK HARD"));
  }

  display.display();
}

// ── 辅助函数:三色 LED + 蜂鸣器报警 ──
void controlAlerts(float t, float h, float pm25) {
  // 先关所有灯
  digitalWrite(LED_RED, LOW);
  digitalWrite(LED_YELLOW, LOW);
  digitalWrite(LED_GREEN, LOW);
  noTone(BUZZER);

  bool pmBad = (pm25 > PM25_ALARM);
  bool thBad = (t > TEMP_HIGH || t < TEMP_LOW
                || h > HUMI_HIGH || h < HUMI_LOW);
  bool pmWarn = (pm25 > PM25_WARN && pm25 <= PM25_ALARM);

  if (pmBad) {
    // PM2.5 严重超标:红色闪烁 + 蜂鸣器间歇响
    digitalWrite(LED_RED, HIGH);
    tone(BUZZER, 2000, 100);    // 2kHz 响 100ms
  } else if (thBad || pmWarn) {
    // 温湿异常 或 PM2.5 中度污染:黄色常亮
    digitalWrite(LED_YELLOW, HIGH);
  } else {
    // 全部正常:绿色常亮
    digitalWrite(LED_GREEN, HIGH);
  }
}

// ── 辅助函数:开机 LED 自检 ──
void testLeds() {
  digitalWrite(LED_RED, HIGH);   delay(200);
  digitalWrite(LED_RED, LOW);
  digitalWrite(LED_YELLOW, HIGH); delay(200);
  digitalWrite(LED_YELLOW, LOW);
  digitalWrite(LED_GREEN, HIGH);  delay(200);
  digitalWrite(LED_GREEN, LOW);
}

代码说明

调试步骤

按以下顺序逐步调试,每步验证通过再进下一步。避免一次接 8 根线发现全错——这种痛苦经历过的都懂。

第 1 步:DHT22 自检(5 分钟)

  1. 接线:仅接 DHT22 的 3 根线(VCC/GND/DATA),接 ESP32 3.3V/GND/GPIO 4
  2. 烧录下面这段最小代码(Adafruit DHT 库自带的 DHTtester 示例改 1 行:把 DHT11 改成 DHT22
  3. 打开串口监视器(波特率 115200),看到 Humidity: xx.x% Temperature: xx.x*C → 接线正确
  4. 看到 Failed to read from DHT sensor → 检查:VCC/GND 是否接反、DATA 引脚是否接对、上拉电阻是否虚焊
  5. 手捂 DHT22,湿度数字应明显上升(验证响应正常)

第 2 步:PMS5003 自检(8 分钟)

  1. 接线:PMS5003 的 4 根线(VCC/GND/TX/RX)全接,VCC 必须接外部 5V,GND 务必和 ESP32 共地
  2. 确认 ESP32 RX2=GPIO 16 接 PMS 的 TX,ESP32 TX2=GPIO 17 接 PMS 的 RX(不能反)
  3. 烧录 PMS 库自带的 PMS_Basic 示例(改成 Serial2 即可)
  4. 打开串口监视器,看到 PM1.0/PM2.5/PM10 三个数字(非 0 非 65535)→ 接线正确
  5. 看到 0 或 65535 → 检查:TX/RX 是否接反、GND 是否共地、5V 电源是否通电(万用表测电源正负极应有 5V)
  6. 对着 PMS5003 进气口哈气,PM2.5 数字应瞬间飙升到 100+(验证响应)

第 3 步:OLED 自检(3 分钟)

  1. 接线:OLED 的 4 根线全接(VCC/GND/SDA/SCL),与 ESP32 GPIO 21/22 接好
  2. 烧录 Adafruit_SSD1306 库自带的 ssd1306_128x64_i2c 示例
  3. 看到 OLED 显示一个跳动的方块 + 数字统计 → 接线正确
  4. 黑屏 → 用 I2C Scanner 扫地址(确认是 0x3C 还是 0x3D),检查 SDA/SCL 是否接反、VCC/GND 是否反

第 4 步:三色 LED + 蜂鸣器自检(5 分钟)

  1. 接线:LED 4 根线(R/Y/G/GND)+ 蜂鸣器 3 根线(I/O/VCC/GND)全接
  2. 烧录 testLeds() 函数(核心代码中已包含),观察三色 LED 是否依次亮 200ms
  3. 三个灯都能亮 → LED 接线正确
  4. 某灯不亮 → 检查对应的信号线是否接对、模块是共阴还是共阳(极性反了不亮)
  5. 蜂鸣器能否响 → 单独用 digitalWrite(BUZZER, HIGH); delay(500); digitalWrite(BUZZER, LOW); 测试
  6. 蜂鸣器不响但电压有 → 模块坏了或三极管没焊好;裸蜂鸣器 100mA 会烧 ESP32 GPIO

第 5 步:整合测试(10 分钟)

  1. 烧录上面的完整核心代码
  2. 打开串口监视器(波特率 115200),看到 5 项数据(温度/湿度/PM1.0/PM2.5/PM10)持续输出
  3. OLED 同步显示 4 项数据 + 底部状态栏(OK / WARN / ALARM)
  4. 正常状态:绿色 LED 常亮,底部显示 [OK] GOOD WORK HARD
  5. 用打火机在 PMS5003 进气口点一下(不要持续打火,会烧坏传感器),PM2.5 应瞬间飙升,红色 LED 闪烁 + 蜂鸣器响 → 报警逻辑正常
  6. 用嘴对 DHT22 哈气,湿度应上升到 80%+,黄色 LED 应亮 → 温湿异常判断正常
  7. 串口监视器持续输出数据,无乱码(乱码 = PMS5003 电源不稳或 GND 没共地)

常见故障排查(先看这 5 条)

设计图

系统架构图(ESP32 中心 + 4 个外围)

🔧 硬件架构图
       ┌──────────────────────┐
       │   ESP32 DevKit V1    │
       │   (主控 + WiFi 可选)  │
       └──────────┬───────────┘
                  │
      ┌───────────┼───────────┬──────────────┐
      │           │           │              │
   GPIO 4      I2C(21/22)   GPIO 25/26/27   GPIO 12
      │           │           │              │
      ▼           ▼           ▼              ▼
  ┌────────┐  ┌──────────┐  ┌─────────┐  ┌────────┐
  │ DHT22  │  │  SSD1306 │  │三色 LED │  │ 蜂鸣器 │
  │温湿度  │  │ 0.96 OLED│  │ 红/黄/绿│  │ 2kHz   │
  │±0.5°C │  │  128x64  │  │ 3 GPIO  │  │ 100mA  │
  └────────┘  └──────────┘  └─────────┘  └────────┘
                                    ▲
                                    │
                          ┌────────┴────────┐
                          │ PMS5003 PM2.5  │ ← 5V 独立电源
                          │  UART2 16/17   │    (必共地)
                          │  0-1000μg/m³  │
                          └────────────────┘

数据流与报警状态机

🔄 报警判断状态机
当前状态触发条件切换到视觉反馈
OK 正常PM2.5 升至 35-75WARN 警告🟡 黄色常亮
OK 正常温/湿越界(T>30/T<16/H>70/H<30)WARN 警告🟡 黄色常亮
WARN 警告PM2.5 升至 >75ALARM 报警🔴 红闪 + 蜂鸣器
WARN 警告所有数据回正常OK 正常🟢 绿色常亮
ALARM 报警PM2.5 降至 <35OK 正常🟢 绿色常亮
ALARM 报警PM2.5 降至 35-75(仍污染)WARN 警告🟡 黄色常亮

状态机优先级ALARM > WARN > OK,即只要任一严重条件满足,状态取最高级。这避免了"PM2.5 95 但温度刚好正常"被错误归为 OK 的问题。

PM2.5 健康影响对照表(参考 WHO 2021 指南)

🌫️ PM2.5 浓度等级与健康影响
PM2.5 (μg/m³)等级AQI 颜色健康影响LED 状态
0 - 12🟢 绿无影响,正常活动绿色常亮
12 - 35🟡 黄绿敏感人群注意绿色常亮
35 - 75轻度污染🟡 黄儿童/老人应减少户外黄色常亮
75 - 115中度污染🟠 橙所有人应减少户外红色闪烁 + 蜂鸣器
115+重度污染🔴 红应戴 N95,开空气净化器红色闪烁 + 蜂鸣器

功耗与采样频率对照表

⚡ 模块功耗与采样策略
模块工作电压工作电流采样频率节能策略
ESP32 主控3.3V~80mA(WiFi 关)关闭 WiFi/BT 可降至 20mA
DHT223.3-5V2.5mA(采样峰值)2 秒/次功耗低,无需节能
PMS50035V~100mA(工作)2 秒/次(被动)采样后 pms.sleep() 降至 1mA
OLED3.3-5V~20mA(显示时)夜间可 display.dim() 降亮度
三色 LED3.3V~60mA(全亮)报警时亮,正常时只亮 1 颗 ~20mA
蜂鸣器3.3-5V~100mA(响时)间歇响 100ms/次
合计~280mA 峰值建议 5V/3A 电源(PMS5003 必须 5V 独立)

扩展玩法

基础版 ¥122 跑通后,下面这些二次开发都很容易:

金句

"空气不会说话,但 PM2.5 会让你打喷嚏;ESP32 不会呼吸,但能替你看护每一次呼吸。"

本项目总成本 ¥122,比小米米家空气检测仪便宜 65%,数据本地化无云端依赖,三级报警机制覆盖 90% 室内空气异常场景(DHT22 + PMS5003 + OLED + LED + 蜂鸣器全打通)。所有阈值、显示内容、报警逻辑都可在 Arduino 代码里改——动手做,让数据为每一次呼吸负责。