基于ESP32与多传感器的DIY空气质量检测器全流程实现 1. 项目概述为什么我们需要一个自己的空气质量检测器最近几年空气质量成了大家茶余饭后、甚至日常决策里绕不开的话题。无论是新家装修后担心甲醛残留还是办公室密闭导致二氧化碳飙升让人昏昏欲睡又或是秋冬季节的雾霾天我们都需要一个客观的“裁判”来告诉我们呼吸的这口空气到底怎么样。市面上的空气质量检测设备不少从几十块的“玩具”到几千上万的“专业仪器”都有但用久了你会发现痛点便宜的精度堪忧数据看看就好专业的又太贵而且功能固定想加个远程查看或者数据长期记录要么没有要么得加钱买云服务。所以自己动手做一个“空气质量检测器”的想法就冒出来了。这不仅仅是一个DIY手工更是一个融合了传感器技术、嵌入式开发和数据可视化的综合性项目。它的核心目标是用可控的成本获得可靠、透明且可扩展的空气质量监测能力。你可以把它放在儿童房监测甲醛和TVOC总挥发性有机物放在书房监测二氧化碳和PM2.5甚至集成到智能家居系统中当空气质量变差时自动开启空气净化器或新风机。这个项目适合谁如果你是电子爱好者、创客、智能家居玩家或者单纯是对身边环境数据好奇、愿意动手的技术控那么这个项目会给你带来巨大的乐趣和实用价值。它不要求你有极高的电子工程背景但需要你有一点焊接和编程的耐心。接下来我会把我从选型、组装到调试、优化的全过程以及踩过的坑和收获的经验毫无保留地分享给你。2. 核心设计思路与方案选型自己搭建一个检测器第一步不是急着买零件而是想清楚我要测什么精度要求多高数据怎么呈现预算多少想明白了这些方案自然就清晰了。2.1 检测目标与传感器选型解析空气质量的指标很多我们需要抓住最核心、与健康和生活品质最相关的几项颗粒物PM2.5/PM10这是雾霾的“元凶”。检测原理主流是激光散射法。传感器方面攀藤PMS系列和夏普GP2Y1010AU0F是经典选择。攀藤的如PMS5003、PMS7003通过串口输出数字信号精度和稳定性在消费级里很不错是首选。夏普的传感器输出的是模拟电压需要单片机进行AD转换和公式计算受环境影响大一些但价格更低。挥发性有机物与甲醛TVOC/HCHO主要来自装修材料、家具。这里要特别注意甲醛传感器非常昂贵且寿命短。市面上几百块的“甲醛检测仪”用的多是电化学式VOC传感器它对多种有机气体都有反应将其读数标定为“甲醛等效浓度”。虽然不精准但用于判断“有无污染”和“污染趋势”是足够的。常见的如炜盛科技的ZE08-CH2O专门测甲醛和SGP30、SGP40测TVOC和eCO2。SGP30这类金属氧化物MOX传感器性价比高通过I2C通信非常容易集成。二氧化碳CO2浓度过高直接导致困倦、注意力下降。最准的是NDIR非分散红外原理传感器。SenseAir S8和MH-Z19B是代表产品后者价格亲民通过串口或PWM输出精度能满足室内监测需求误差±50ppm 3%读数。注意有些传感器如SCD30也能测CO2但价格更高。温湿度这是环境基本参数而且温湿度数据能帮助解读其他传感器读数例如高温高湿可能加剧VOC释放。DHT22或更精确的SHT31是绝佳选择数字接口使用简单。选型心得对于DIY项目我强烈推荐“数字接口”优先。像PMS5003PM2.5、MH-Z19BCO2、SGP30TVOC、SHT31温湿度都是通过UART或I2C输出已经处理好的数字信号极大减少了模拟电路设计和信号调理的麻烦把精力集中在数据处理和逻辑上。2.2 主控单元与系统架构设计传感器确定了就需要一个“大脑”来读取数据、处理并展示。主控芯片选择Arduino Uno/Nano入门最简单生态丰富但性能有限同时接多个串口传感器可能比较吃力且网络功能需要外加模块如ESP8266。ESP32这是本项目的最优解。理由很充分它自带Wi-Fi和蓝牙性能强大有多个硬件串口能轻松同时驱动PMS5003和MH-Z19B丰富的GPIO和I2C接口可以连接其他传感器还能运行简单的Web服务器实现无线数据传输和网页显示。性价比极高。树莓派 Pico W性能也不错有Wi-Fi但生态相对ESP32稍弱在传感器库的支持广度上略逊一筹。显示与交互方案本地显示一块OLED显示屏SSD1306 0.96寸足以显示核心数据。它体积小、功耗低、接口简单I2C非常适合嵌入式设备。远程查看这是ESP32的强项。我们可以让它连接家庭Wi-Fi定期将数据发送到私有服务器如部署在本地或云主机上的InfluxDB时序数据库 Grafana数据可视化实现专业级的仪表盘。开源物联网平台如Home Assistant通过MQTT协议接入直接融入智能家居生态。简易Web服务器ESP32自身可以托管一个简单的网页在手机或电脑浏览器输入设备IP就能看到实时数据图表。供电与结构设计供电首选5V USB方便且稳定。如果要做成电池供电的便携式则需要考虑低功耗设计如让传感器间歇性工作并使用锂电池和充放电管理模块。结构上需要为传感器设计合理的风道。特别是激光PM传感器需要空气自然对流或通过小风扇引导气流经过检测腔否则数据会不准。外壳可以使用3D打印或者找一个合适尺寸的塑料盒钻孔改造。我的最终方案定为ESP32作为主控集成PMS5003PM2.5、SGP30TVOC/eCO2、SHT31温湿度和OLED显示屏数据通过Wi-Fi上传至本地Home Assistant并显示在OLED屏上。MH-Z19BCO2作为可选升级项因为SGP30估算的eCO2在室内人员稳定情况下有一定参考性若需高精度CO2再额外添加。3. 硬件连接与电路详解方案定了接下来就是把它们正确地连接起来。这一步看似是简单的连线但细节决定成败。3.1 元器件清单与引脚分配以下是核心元器件清单元器件型号接口用途说明主控板ESP32 DevKit V1-核心处理单元提供Wi-Fi颗粒物传感器攀藤 PMS5003UART (RX/TX)检测PM1.0, PM2.5, PM10气体传感器Sensirion SGP30I2C (SDA/SCL)检测TVOC和等效CO2 (eCO2)温湿度传感器Sensirion SHT31I2C (SDA/SCL)检测温度、湿度显示屏OLED SSD1306 (128x64)I2C (SDA/SCL)本地数据显示二氧化碳传感器可选Winsen MH-Z19BUART (RX/TX)高精度检测CO2浓度ESP32的引脚非常多我们需要合理分配避免冲突。最重要的一点是硬件串口UART的分配。PMS5003需要一组硬件UART。我们可以使用UART2对应ESP32的引脚GPIO16 (RX2)和GPIO17 (TX2)。连接时ESP32的RX2接传感器的TX ESP32的TX2接传感器的RX。MH-Z19B如果选用也需要一组硬件UART。可以使用UART1引脚GPIO9 (RX1)/GPIO10 (TX1)但注意这些引脚在有些ESP32板子上可能用于内部Flash使用前需确认。更稳妥的方法是使用UART2以外的其他引脚通过“软件串口SoftwareSerial”库模拟但这会占用CPU资源。如果只用一个串口传感器就简单多了。I2C总线SGP30、SHT31、OLED可以共享一组I2C。ESP32的默认I2C引脚是GPIO21 (SDA)和GPIO22 (SCL)。将它们全部并联到这两根线上即可每个设备有唯一的I2C地址不会冲突。SHT31地址0x44 或 0x45取决于ADDR引脚电平SGP30地址0x58固定SSD1306地址0x3C常见3.2 电路连接图与焊接注意事项由于无法绘制图形我用文字描述连接关系电源部分将所有设备的VCC或Vin连接到ESP32开发板的5V或3.3V引脚。务必查阅每个传感器的数据手册PMS5003和MH-Z19B通常是5V供电但它们的串口信号电平可能是3.3V兼容的。SGP30、SHT31、OLED一般是3.3V设备。最安全的做法是所有传感器都使用3.3V供电。如果传感器只支持5V并确认其TX输出为5V电平则需要在ESP32的RX引脚前加一个简单的电平转换电路如分压电阻防止损坏ESP32的3.3V GPIO。地线GND将所有设备的GND与ESP32的GND连接在一起共地是电路正常工作的基础。信号线连接PMS5003:TX-GPIO16 (RX2);RX-GPIO17 (TX2);VCC-3.3V;GND-GND.SGP30:SDA-GPIO21;SCL-GPIO22;VCC-3.3V;GND-GND.SHT31:SDA-GPIO21;SCL-GPIO22;VCC-3.3V;GND-GND.OLED:SDA-GPIO21;SCL-GPIO22;VCC-3.3V;GND-GND.焊接与布线实操心得杜邦线测试在最终焊接前务必用杜邦线连接所有部件进行功能测试确认每个传感器都能正常读数。这能避免焊接后发现问题拆焊麻烦。电源去耦在ESP32的3.3V输出引脚附近焊接一个10uF的电解电容和一个0.1uF的陶瓷电容到地可以平滑电源减少因传感器功耗波动导致的系统复位或读数异常。I2C上拉电阻ESP32的内部上拉电阻可能不够强尤其是当总线较长或设备较多时。建议在SDA和SCL线上各接一个4.7kΩ的外部上拉电阻到3.3V确保信号稳定。传感器摆放PM传感器进气口周围不能有遮挡要预留至少2-3厘米的空间。气体传感器SGP30应避免正对通风口以免气流过快影响检测。4. 软件编程与核心逻辑实现硬件搭好了接下来就是赋予它灵魂的软件部分。我们将使用Arduino IDE进行开发因为它对ESP32和各类传感器库的支持非常好。4.1 开发环境搭建与库文件安装首先确保你的Arduino IDE已安装ESP32开发板支持。打开Arduino IDE点击文件-首选项在“附加开发板管理器网址”中输入https://espressif.github.io/arduino-esp32/package_esp32_index.json打开工具-开发板-开发板管理器搜索“esp32”安装“Espressif Systems”的ESP32平台。安装必要的库。点击项目-加载库-管理库...搜索并安装以下库Adafruit SGP30 Sensor(用于SGP30)Adafruit SHT31 Library(用于SHT31)Adafruit SSD1306和Adafruit GFX Library(用于OLED)PMS LibrarybyMikal Hart(用于PMS5003 这个库很好用)WiFi和WebServer(ESP32内置无需额外安装)如果使用MQTT安装PubSubClient4.2 主程序结构与数据读取逻辑程序的核心逻辑是初始化 - 连接Wi-Fi - 循环读取各传感器数据 - 本地显示 - 远程发送。下面分解关键代码段。初始化与传感器对象创建#include Wire.h #include Adafruit_SGP30.h #include Adafruit_SHT31.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h #include PMS.h // 定义引脚 #define PMS_RX_PIN 16 #define PMS_TX_PIN 17 // 创建传感器对象 PMS pms(Serial2); // 使用ESP32的Serial2 (UART2) PMS::DATA data; Adafruit_SGP30 sgp; Adafruit_SHT31 sht31 Adafruit_SHT31(); Adafruit_SSD1306 display(128, 64, Wire, -1); // OLED对象 // 传感器数据全局变量 float temperature, humidity; uint16_t tvoc, eco2; uint16_t pm1_0, pm2_5, pm10; void setup() { Serial.begin(115200); // 用于调试输出 Wire.begin(21, 22); // 初始化I2C指定SDA21, SCL22 // 初始化OLED if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F(SSD1306 allocation failed)); for(;;); // 死循环阻止继续执行 } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0,0); display.println(Air Monitor Init...); display.display(); // 初始化SHT31 if (!sht31.begin(0x44)) { Serial.println(Could not find SHT31); while (1) delay(1); } // 初始化SGP30 if (!sgp.begin()){ Serial.println(SGP30 not found); while (1) delay(1); } // SGP30需要初始基线值首次运行需要一段“预热”时间 Serial.print(SGP30 Serial #); Serial.println(sgp.serialnumber[0], HEX); Serial.println(sgp.serialnumber[1], HEX); // 初始化PMS传感器 Serial2.begin(9600, SERIAL_8N1, PMS_RX_PIN, PMS_TX_PIN); // 初始化UART2 pms.passiveMode(); // 设置为被动模式由我们主动请求数据 // 连接Wi-Fi connectToWiFi(); }循环读取数据在loop()函数中我们需要错开读取不同传感器的时间因为有些传感器如SGP30的读取需要间隔且避免同时进行I2C和UART操作可能带来的微小冲突。void loop() { unsigned long currentMillis millis(); // 每2秒读取一次SHT31温湿度 static unsigned long shtTimer 0; if (currentMillis - shtTimer 2000) { shtTimer currentMillis; temperature sht31.readTemperature(); humidity sht31.readHumidity(); if (isnan(temperature) || isnan(humidity)) { Serial.println(Failed to read from SHT31!); } } // 每1秒读取一次SGP30TVOC/eCO2 static unsigned long sgpTimer 0; if (currentMillis - sgpTimer 1000) { sgpTimer currentMillis; if (sgp.IAQmeasure()) { tvoc sgp.TVOC; eco2 sgp.eCO2; } else { Serial.println(SGP30 read failed); } // 每30秒测量一次原始信号用于基线计算长期稳定性 static unsigned long sgpBaselineTimer 0; if (currentMillis - sgpBaselineTimer 30000) { sgpBaselineTimer currentMillis; uint16_t baselineCO2, baselineTVOC; if (sgp.getIAQBaseline(baselineCO2, baselineTVOC)) { // 可以将基线值保存到EEPROM下次上电后调用sgp.setIAQBaseline()来恢复 } } } // 每5秒请求并读取一次PMS数据 static unsigned long pmsTimer 0; if (currentMillis - pmsTimer 5000) { pmsTimer currentMillis; pms.requestRead(); if (pms.readUntil(data)) { pm1_0 data.PM_AE_UG_1_0; pm2_5 data.PM_AE_UG_2_5; pm10 data.PM_AE_UG_10_0; } } // 更新OLED显示 updateDisplay(); // 发送数据到服务器例如每10秒一次 static unsigned long uploadTimer 0; if (currentMillis - uploadTimer 10000) { uploadTimer currentMillis; sendDataToServer(); } delay(10); // 短暂延时释放CPU控制权 }4.3 数据上传与远程访问实现数据上传有两种主流方式HTTP POST到自定义API或者通过MQTT发布到主题。这里以MQTT接入Home Assistant为例因为它更轻量、实时。MQTT配置你需要一个MQTT BrokerHome Assistant内置的Mosquitto插件就很好用。ESP32端PubSubClient库编程#include PubSubClient.h #include WiFi.h const char* mqtt_server 192.168.1.100; // 你的MQTT服务器IP WiFiClient espClient; PubSubClient client(espClient); void reconnectMQTT() { while (!client.connected()) { if (client.connect(ESP32_AirMonitor)) { // 客户端ID client.publish(airmonitor/status, online); Serial.println(MQTT Connected); } else { Serial.print(failed, rc); Serial.print(client.state()); Serial.println( try again in 5s); delay(5000); } } } void sendDataToServer() { if (!client.connected()) { reconnectMQTT(); } client.loop(); // 维持MQTT连接 // 构建JSON格式数据 String payload {; payload \temperature\: String(temperature, 1) ,; payload \humidity\: String(humidity, 1) ,; payload \tvoc\: String(tvoc) ,; payload \eco2\: String(eco2) ,; payload \pm25\: String(pm2_5) ,; payload \pm10\: String(pm10); payload }; // 发布到MQTT主题 client.publish(airmonitor/sensors, payload.c_str()); }在Home Assistant的configuration.yaml中配置MQTT传感器就可以自动发现并显示这些数据了。5. 校准、调试与性能优化设备跑起来了但数据准不准稳不稳定这才是项目从“能用”到“好用”的关键。5.1 传感器校准与基线管理SGP30的基线Baseline这是影响SGP30长期读数稳定性的关键。传感器在洁净空气中运行12-24小时后会计算出稳定的基线值。代码中我们每30秒获取一次sgp.getIAQBaseline()。关键操作是将稳定运行后的基线值保存到ESP32的EEPROM或Flash中。下次开机时先调用sgp.setIAQBaseline(baselineCO2, baselineTVOC)再开始测量。这能保证重启后数据快速恢复准确而不是重新经历漫长的预热漂移。PMS5003的校准激光PM传感器一般出厂已校准无需用户操作。但其读数受湿度影响。在湿度极高70%时水蒸气可能被误判为颗粒物导致读数虚高。可以在软件端做一个简单的湿度补偿需查找该传感器的补偿公式或系数或者直接注明高湿环境下数据仅供参考。MH-Z19B的零点和跨度校准如果需要极高精度可以按说明书进行手动校准但通常室内应用其出厂精度已足够。5.2 数据稳定性处理与滤波算法传感器原始数据常有微小跳动。直接显示会让人眼花缭乱我们需要滤波。移动平均滤波最简单有效。例如为每个数据维护一个数组存储最近N次读数求平均值作为输出。这能平滑掉随机噪声。#define FILTER_SIZE 5 float pm25Buffer[FILTER_SIZE]; int bufferIndex 0; float filteredPM25(float newValue) { pm25Buffer[bufferIndex] newValue; bufferIndex (bufferIndex 1) % FILTER_SIZE; float sum 0; for (int i 0; i FILTER_SIZE; i) { sum pm25Buffer[i]; } return sum / FILTER_SIZE; }一阶低通滤波指数加权平均更节省内存公式为filteredValue alpha * newValue (1 - alpha) * filteredValue。alpha是滤波系数0alpha1越小越平滑但响应越慢。阈值判断对于突变异常值如PM2.5瞬间从10跳到500又回来可以设置一个合理的最大变化率超过此变化率则视为干扰丢弃或用上次有效值代替。5.3 功耗优化与长期运行稳定性如果考虑电池供电功耗至关重要。深度睡眠模式ESP32可以配置为定时唤醒。例如让设备每5分钟唤醒一次读取所有传感器数据通过Wi-Fi上传然后立即重新进入深度睡眠。这能极大降低平均功耗。但注意Wi-Fi重新连接耗时且耗电需要权衡。传感器供电控制对于功耗较大的传感器如PMS5003工作电流约100mA可以通过MOSFET或三极管开关电路由ESP32的GPIO控制其电源仅在需要测量时上电。OLED屏控制显示一段时间后自动关闭背光。软件看门狗使用ESP32的硬件看门狗或软件定时器复位功能防止程序跑飞导致死机。长期运行心得我的设备是7x24小时插电运行的。最大的稳定性挑战来自Wi-Fi连接。网络波动可能导致MQTT断开。我的解决方案是在loop()中加强重连逻辑并在Wi-Fi断开时在OLED屏上显示错误状态。另外定期重启是个“笨”但有效的方法。可以设置设备每运行24小时自动重启一次清理内存碎片防止潜在的内存泄漏问题。这可以通过在loop()里判断millis()是否超过设定时间来实现。6. 外壳制作与部署实践一个裸露的电路板既不安全也不美观更影响传感器采样。一个好的外壳是项目的最后一步也是点睛之笔。6.1 3D打印外壳设计与风道考量我使用Fusion 360设计了外壳。设计要点分体结构分为底盖和上盖方便安装和维修。用螺丝柱固定ESP32和传感器小板。PM传感器风道在PMS5003的进气口和出气口对应位置设计格栅。关键点进气格栅面积要足够内部空间要能让空气自然对流。有人会加一个小型静音风扇5V制造微弱气流这样数据响应更快尤其是监测突然的污染源如点烟时。风扇可以接在ESP32的GPIO上用PWM控制转速。气体传感器开孔SGP30等气体传感器需要接触空气但不能直接对着风口。在外壳侧面开一些小孔即可。屏幕开窗为OLED屏设计透明亚克力板或直接开窗。线材管理内部预留走线槽或扎线孔让接线整洁。将设计好的STL文件用3D打印机如PLA材料打出然后进行组装。6.2 部署位置选择与数据解读设备做好了放哪里也有讲究避免位置不要放在墙角、窗帘后、空调出风口正对面、厨房油烟机附近、窗户边受室外影响剧烈波动。理想位置放在房间中央的桌面上或离地1-1.5米高的墙面上这是人体呼吸区高度。确保周围空气流通但无强风直吹。数据解读PM2.5室内优良应小于35μg/m³。炒菜、吸烟时会瞬间飙升。TVOC浓度低于0.6mg/m³为佳。新家具、油漆后会升高通风后下降。eCO2 (或CO2)室外约400ppm。室内低于1000ppm感觉舒适超过1500ppm会感到闷、困倦。是判断是否需要开窗通风的极佳指标。温湿度结合其他数据看。高温高湿可能让体感更闷也可能加速污染物散发。部署好后你就可以在Home Assistant的仪表盘上看到漂亮的历史曲线图设置自动化如CO21000ppm自动开窗通风真正让数据服务于生活。7. 常见问题排查与进阶玩法即使按照步骤来也可能会遇到问题。这里汇总一些我踩过的坑和解决办法。7.1 硬件连接与通信故障问题现象可能原因排查步骤OLED不显示电源接反/没接I2C地址错误接线松动1. 检查VCC/GND。2. 用I2C扫描程序确认地址通常是0x3C或0x3D。3. 按压接线头。SGP30/SHT31读数失败I2C上拉电阻问题供电不足传感器损坏1. 在SDA/SCL上加4.7k上拉电阻。2. 确保使用3.3V稳定供电可并联一个电容。3. 运行I2C扫描看地址是否出现。PMS5003无数据串口引脚接反波特率不对模式不对1. 确认TX/RX交叉连接。2. 确认波特率为9600。3. 代码中是否调用了pms.wakeUp()和pms.requestRead()ESP32无法连接Wi-Fi密码错误路由器设置如MAC过滤信号太弱1. 用Serial打印Wi-Fi连接状态。2. 检查路由器日志。3. 尝试将ESP32靠近路由器。数据上传失败MQTTMQTT服务器地址/端口错网络不通客户端ID冲突1.ping一下MQTT服务器IP。2. 检查防火墙设置。3. 更换一个唯一的客户端ID。7.2 软件与数据异常数据偶尔跳变到极大或极小值这是典型的读数未完成或校验失败就使用了数据。务必在每次sgp.IAQmeasure()或pms.readUntil(data)返回true成功后才使用对应的数据变量。SGP30读数长时间不变或变化缓慢这是正常的。MOX气体传感器需要预热初始几十小时读数可能不准且变化慢。持续通电运行并正确保存/加载基线后会逐渐稳定和灵敏。ESP32频繁重启可能是电源带载能力不足所有传感器同时工作峰值电流大或代码中有内存泄漏如频繁动态分配字符串未释放。确保使用足额如2A的5V USB电源并检查代码中字符串处理部分。7.3 项目扩展与进阶思路这个基础平台有很大的扩展潜力增加更多传感器如二氧化碳MH-Z19B、甲醛ZE08-CH2O、大气压BMP280甚至噪音传感器。本地告警增加一个蜂鸣器或RGB LED灯。当PM2.5超标时红灯闪烁CO2过高时黄灯亮起。多房间组网制作多个节点分别放在客厅、卧室、书房。每个节点通过ESP-NOW一种低功耗的ESP间直接通信协议将数据汇总到一个“主节点”再由主节点统一上传节省每个节点都连Wi-Fi的配置和功耗。太阳能供电搭配小太阳能板和锂电池实现完全无线化、户外部署监测阳台或小院的空气质量。数据分析与预测将长期数据导出用Python分析不同时间、天气、活动如做饭、聚会下的空气质量变化规律甚至可以尝试简单的预测模型。这个自制的空气质量检测器带给你的不仅仅是一组数字更是一种对生活环境的感知力和掌控感。从挑选传感器时对比参数的纠结到焊接第一根线时的小心翼翼从代码调试成功看到第一行数据时的喜悦到将它放在家中默默守护时的安心整个过程充满了创造的乐趣。它可能没有商业产品外观精致但它的每一行代码、每一个数据你都了如指掌这种透明和可定制性正是DIY项目的魅力所在。希望我的这份详细记录能帮你少走弯路更快地享受到自己动手的成果。如果在制作中遇到任何问题随时可以带着你的现象和代码来交流很多时候一个细节的调整就能让一切变得不同。