ESP32 自制空气质量监测器:烟霾季追踪 PM2.5 趋势与 CO2

Sensor debu aluminium dengan kabel merah, ESP32 NodeMCU hitam, modul SCD30 hijau dan OLED biru di atas meja dengan tingkap berjerebu

要用 ESP32 自制空气质量监测器,把 GP2Y1010AU0F 光学灰尘 sensor 接到 ADC1 pin 读 PM2.5 趋势,SCD30 CO2 sensor 接到 I2C bus,再把两组读数显示在 0.96 寸 OLED 上。整个项目只需一个 sketch——完整代码在下面。这个装置显示的是你家里空气的趋势,不能代替官方 API(Indeks Pencemar Udara,马来西亚官方空气污染指数)读数。

为什么这个项目要用两个 sensor?

GP2Y1010AU0F 灰尘 sensor 和 SCD30 CO2 sensor 回答的是两个不同的问题。烟霾季节,问题是”到底有多少细颗粒飘进我家”——这是灰尘 sensor 的工作,它靠红外光在空气中颗粒上的反射来检测。而关紧门窗开冷气的房间,问题变成另一个:”这里的新鲜空气够不够”——CO2 正是最可靠的通风指标。

先认清每个零件的样子。灰尘 sensor 是铝制外壳,中间有个圆孔,配一条 6 芯红色排线、白色 JST 插头。SCD30 则是绿色 breakout PCB,上面有黑色 sensor 模块——它的 castellation 焊盘没有预装排针,需要自己动手焊。如果这是你第一次焊接,先看我们的排针焊接教程。大脑是 NodeMCU-32S(micro-USB,CP2102 USB 芯片),显示则用 0.96 寸 OLED——白色像素、蓝色 PCB。

有一点很重要:这个灰尘 sensor 必须搭配一颗 150Ω 电阻和一颗 220µF 电解电容——这两个数值是 Sharp datasheet 明文规定的,不是随便选的。两者都是普通的工作台零件,大多数 maker 手上早就有存货,所以我们没有放进上面的清单。想常备这类基础零件,我们的 component starter kit 是最省事的起点。

这个装置能给出像官方报告那样的 API 读数吗?

马来西亚的 API 由环境局(Jabatan Alam Sekitar)根据经过校准的参考监测站计算得出。它不是原始的 PM2.5 读数——每种主要污染物(包括 PM2.5、PM10、臭氧、SO2、NO2 和 CO)各有一个分指数,最高的那个分指数才是 API 数字。而 PM2.5 的分指数是按 24 小时平均值计算的,不是当下那一秒的读数。

GP2Y1010AU0F 这类红外光学 sensor 给出的是相对趋势,不是校准过的测量值。它的读数会受湿度影响——而马来西亚正好常年潮湿——所以千万别把它的数字当成”我家的 API”来引用。

那它有什么用?其实用处很多。邻居烧垃圾时,你能亲眼看到房间空气变差;可以验证关窗、开 air purifier 是不是真的有效;可以观察一整天的趋势;也可以为学校科学项目收集真实数据。SCD30 讲的则完全是另一回事:它是通风指标——这个房间闷不闷——和外面的烟霾是两个不同的问题。别把两者混为一谈。

所有 sensor 的线要怎么接?

ESP32 有两组 ADC pin,这里选对 pin 非常关键。ADC2 的 pin 与 WiFi 射频共用——WiFi 一启用,ADC2 的读数就会卡住。这个项目以后多半会加 WiFi,所以灰尘 sensor 的模拟输出必须接 ADC1(GPIO32 到 GPIO39)——我们用 GPIO34。

还有一点:Sharp datasheet 显示灰尘浓度高时 sensor 输出可以冲到 3.4V 以上,超过 ESP32 的 3.3V 输入上限。解决方法是一个简单的分压电路——10kΩ 串联、20kΩ 接 GND——让 ADC 只看到实际电压的三分之二(最高约 2.3V,稳稳落在 ADC 量程内)。

零件 Pin / 线 接到
灰尘 sensor(6 芯,按 JST 顺序) 线 1(V-LED) 150Ω 电阻 → VIN(5V)。220µF 电容从线 1 接到 GND,负极脚在 GND
线 2(LED-GND) GND
线 3(LED) GPIO25
线 4(S-GND) GND
线 5(Vo) 10kΩ → GPIO34,再从 GPIO34 用 20kΩ 接到 GND
线 6(Vcc) VIN(5V)
SCD30 VDD 3V3
GND GND
TX/SCL GPIO22
RX/SDA GPIO21
OLED 0.96 寸 GND GND
VCC 3V3
SCL GPIO22
SDA GPIO21
铝制灰尘 sensor 的六条红色线接到黑色 ESP32 板的卡通接线图,旁边有电阻和电容
灰尘 sensor 的六条线:两条接 GND,一条接 GPIO25,输出经过分压电路进 GPIO34,5V 供电串 150Ω 电阻并配 220µF 电容。

注意我们这款 OLED 排针印的顺序是 GND、VCC、SCL、SDA——不太常见。VCC 和 GND 一旦接反可能当场烧掉模块,通电前务必再核对一遍。

SCD30 和 OLED 挂在同一条 I2C bus 上(SDA=GPIO21、SCL=GPIO22——ESP32 的默认 I2C pin),地址不同:SCD30 是 0x61,OLED 是 0x3C,所以互不冲突。SCD30 上的 SEL 焊盘要留空——那正是选择 I2C 模式的开关。技术小注:SCD30 使用 I2C clock stretching(每次读取 30ms,每天一次可长达 150ms)——ESP32 默认的 100kHz 正好是 SCD30 的上限,通常运行顺畅;如果读取经常失败,Sensirion 建议降到 50kHz 或更低(在 Wire.begin() 之后加一行 Wire.setClock(50000);)。

绿色 SCD30 breakout 和蓝色 OLED 共用两条 I2C 线接到 ESP32 的卡通图
SCD30 和 OLED 在 GPIO21 和 GPIO22 上共用同一条 I2C bus——地址不同,互不冲突。

同时读取 PM2.5 和 CO2 的代码怎么写?

这个 sketch 需要从 Arduino Library Manager 装三个 library:Adafruit SCD30(弹窗询问时接受 Adafruit BusIO 和 Adafruit Unified Sensor 两个 dependency)、Adafruit GFX LibraryAdafruit SSD1306。基本套路和我们的 DHT11 + OLED 温湿度监测器一样。

最关键的部分是红外 LED 的脉冲。Sharp datasheet 规定周期 10ms、脉宽 0.32ms,采样点在 LED 点亮后 0.28ms。读数变成乱码的头号原因,就是这个 timing 弄错了。

// 空气质量监测器:灰尘(GP2Y1010AU0F)+ CO2(SCD30)+ OLED
// Board:ESP32 Dev Module / NodeMCU-32S
#include <Wire.h>
#include <Adafruit_SCD30.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const int PIN_LED_IR = 25;        // 灰尘 sensor 线 3(IR LED 控制,低电平有效)
const int PIN_VO     = 34;        // 线 5 -> 分压电路 -> GPIO34(ADC1)
const float FAKTOR_DIVIDER = 1.5; // 10k/20k 分压:实际电压 = 读数 x 1.5

Adafruit_SCD30 scd30;
Adafruit_SSD1306 oled(128, 64, &Wire, -1);

float voltanBaseline = 3.3;       // 自动跟踪:最低电压 = 最干净的空气
float co2 = 0, suhu = 0, lembap = 0;

// 按 Sharp datasheet 打一个脉冲:LED ON -> 0.28ms -> 采样 -> LED OFF -> 补满 10ms
float bacaVoltanDebu() {
  digitalWrite(PIN_LED_IR, LOW);              // LOW = 点亮 IR LED
  delayMicroseconds(280);                     // 等 0.28ms 再采样
  uint32_t mv = analogReadMilliVolts(PIN_VO); // ADC 读数,单位毫伏(已校准)
  digitalWrite(PIN_LED_IR, HIGH);             // 熄灭 LED(脉宽约 0.32ms)
  delayMicroseconds(9620);                    // 补满约 10ms 的周期
  return (mv * FAKTOR_DIVIDER) / 1000.0;      // Vo pin 上的实际电压
}

float purataVoltanDebu() {
  float jumlah = 0;
  for (int i = 0; i < 30; i++) jumlah += bacaVoltanDebu(); // 30 个脉冲取平均
  return jumlah / 30.0;
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_LED_IR, OUTPUT);
  digitalWrite(PIN_LED_IR, HIGH);   // 先确保 IR LED 熄灭
  Wire.begin(21, 22);               // SDA=21, SCL=22(ESP32 默认)

  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // OLED 的 I2C 地址
    Serial.println("OLED tak dijumpai - semak wiring");
    while (true) delay(10);
  }
  if (!scd30.begin()) {             // SCD30 的 I2C 地址是 0x61
    Serial.println("SCD30 tak dijumpai - semak solder dan wiring");
    while (true) delay(10);
  }

  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setTextColor(SSD1306_WHITE);
  oled.setCursor(0, 0);
  oled.println("Sensor memanas...");
  oled.display();
  delay(2000);
}

void loop() {
  float voltanDebu = purataVoltanDebu();

  // 干净空气 baseline:出现过的最低电压
  if (voltanDebu < voltanBaseline) voltanBaseline = voltanDebu;

  // 灰尘估算:典型灵敏度 0.5V per 0.1mg/m3(Sharp datasheet)
  float debu = (voltanDebu - voltanBaseline) / 0.005; // ug/m3 - 仅供趋势参考的估算值
  if (debu < 0) debu = 0;

  if (scd30.dataReady() && scd30.read()) {
    co2    = scd30.CO2;
    suhu   = scd30.temperature;
    lembap = scd30.relative_humidity;
  }

  oled.clearDisplay();
  oled.setCursor(0, 0);
  oled.print("Debu: "); oled.print(debu, 0);   oled.println(" ug/m3");
  oled.print("CO2 : "); oled.print(co2, 0);    oled.println(" ppm");
  oled.print("Suhu: "); oled.print(suhu, 1);   oled.println(" C");
  oled.print("RH  : "); oled.print(lembap, 0); oled.println(" %");
  oled.display();

  Serial.printf("V=%.2fV debu=%.0fug/m3 CO2=%.0fppm\n", voltanDebu, debu, co2);
  delay(2000);
}

怎么看懂读数、又要怎么 calibrate?

GP2Y1010AU0F 即使在干净空气中也会输出典型约 0.9V(依个体差异在 0 到 1.5V 之间),之后每 0.1mg/m³ 灰尘大约升高 0.5V。所以干净空气的 baseline 至关重要:先在家里最干净的房间开机,让 sketch 把最低电压记录为零点。没有 baseline,你的 µg/m³ 数字就是悬在半空的。

看趋势,别抠数字。烟霾天开窗后显示读数翻倍,那是真实的信号;至于它到底是 80 还是 95 µg/m³,不是这个级别的 sensor 能回答的问题。高湿度也会推高读数,所以清晨潮湿时段的飙升不必惊慌。

至于 SCD30,Sensirion datasheet 标明在 400 到 10,000ppm 范围内精度为 ±(30ppm + 读数的 3%)——做通风决策绰绰有余。它的 automatic self-calibration(ASC)功能则有前提:sensor 需要连续通电至少 7 天,而且每天至少 1 小时吸到新鲜空气(约 400ppm)。如果你的装置 24 小时待在密闭房间,ASC 就没有参考点——每天开一会儿窗,或者干脆让 ASC 保持关闭(sensor 的出厂默认就是关闭)。

卡通插画:房间里的空气质量监测器和 air purifier,窗外是烟霾
真正的用途:看趋势——关上窗、开着 purifier,到底有没有把读数压下来?

我们最常见到哪些错误?

GP2Y1010AU0F 和 SCD30 出问题的套路总是那几个,怀疑 sensor 坏掉之前,先对照这份清单。

跳过电阻和电容。没有那组 150Ω/220µF 网络,IR LED 的脉冲不稳定,读数会完全随机。这不是”锦上添花”的零件——它是 sensor 电路的一部分。

灰尘输出接在 ADC2 pin。测试时一切正常,一开 WiFi 读数突然死掉。搬到 GPIO32 到 39,问题立刻消失。

OLED 的 VCC/GND 接反。GND、VCC、SCL、SDA 这个顺序确实容易让习惯 VCC 排头的人搞混——而这个错误可能当场烧掉模块。

SCD30 在 0x61 检测不到。几乎都是 castellation 焊盘上的冷焊。把每个焊点重新加热一遍再试。

给灰尘 sensor 供 3.3V。它是 5V 的 sensor——USB 供电时要从 VIN pin 取电,输出电压才有意义。

Arduino IDE 完全认不到板子。NodeMCU-32S 用的是 CP2102 芯片,需要 SiLabs driver。照我们的 ESP32 无法识别 / 上传失败教程走一遍。

常见问题

我可以用这些读数来报 API 吗?

不可以。API 是环境局参考监测站计算出来的指数,取多种污染物中最高的分指数,PM2.5 用的是 24 小时平均值。这个光学 sensor 给的是未经校准的相对趋势——对你家非常有用,但不是 API 数字。

为什么房间看起来很干净,灰尘读数还是上下跳?

三个常见原因:湿度高(细小水滴也会反射 IR 光)、baseline 还没稳定、ADC 天生的噪声。Sketch 里 30 个脉冲取平均已经帮了不少——想更平稳可以再加移动平均。

这些 sensor 需要 calibrate 吗?

灰尘 sensor 只需要干净空气 baseline——在干净的地方开机,sketch 会自动记录。SCD30 出厂就校准好了;ASC 是可选功能,只有 sensor 经常吸到新鲜空气才有效。

可以把读数发到手机吗?

可以——所以我们才选自带 WiFi 的 ESP32。把读数发到 Blynk、Home Assistant 或 MQTT broker 之类的平台。记得让灰尘 sensor 的输出留在 ADC1 pin,WiFi 和 ADC 才不会打架。

适合学校项目或 FYP 吗?

非常适合。它结合了脉冲式模拟读取、两个设备共用的 I2C bus 和实时显示——烟霾季节收集的趋势数据更是报告里货真价实的分析素材。只是要在报告里诚实写明:这是趋势指示器,不是参考级仪器。

最后更新:2026年8月。有问题?WhatsApp 联系我们

Leave a Reply

Your email address will not be published. Required fields are marked *