
1. 项目概述为什么我们需要一个自制的空气质量检测仪最近几年大家对室内空气质量的关注度明显提高了。无论是新装修的房子、长时间密闭的办公室还是秋冬季节为了保暖很少开窗的卧室空气里看不见的“隐形杀手”其实不少。PM2.5大家比较熟悉指的是空气中直径小于等于2.5微米的颗粒物它能深入肺部甚至进入血液循环。而二氧化碳CO2浓度则常常被忽视它虽然本身无毒但却是衡量室内通风状况和人员密集度的关键指标。CO2浓度过高会直接导致人感到憋闷、头晕、注意力不集中长期处于高浓度环境下对健康和学习工作效率都有负面影响。市面上的空气质量检测仪选择很多从几十块的“玩具”到几千块的专业设备都有。但作为一个喜欢动手的创客或电子爱好者我总觉得买来的成品少了点“灵魂”——你不知道它的传感器准不准、算法有没有优化、数据是不是真实。更重要的是你无法根据自己的需求去定制功能比如设定一个阈值自动打开空气净化器或者将数据长期记录并可视化。这就是为什么我决定自己动手用Arduino搭建一个PM2.5和CO2的室内空气质量检测仪。这个项目的核心价值在于透明、可定制和可学习。你清楚地知道每一个部件的工作原理可以自由选择不同精度和价位的传感器还能通过编程实现任何你想要的联动功能。整个过程不仅让你获得一个实用的工具更能深入理解传感器技术、数据采集和嵌入式系统开发。接下来我就把从选型、接线、编程到调试的完整过程以及我踩过的那些坑毫无保留地分享给你。2. 核心硬件选型与设计思路自己动手做项目第一步也是最关键的一步就是硬件选型。选对了事半功倍选错了可能连数据都读不出来。我的设计思路是在保证基本精度和稳定性的前提下尽量选择性价比高、资料丰富、易于Arduino驱动的传感器同时整个系统要简洁、低功耗适合长期运行。2.1 传感器选型PM2.5与CO2的探测原理PM2.5传感器激光散射式是主流测量PM2.5主要有两种原理红外LED式和激光散射式。红外式成本极低但精度很差基本只能判断“有灰尘”和“没灰尘”数值参考意义不大。对于室内空气质量监测我强烈推荐使用激光散射式传感器。它的原理是让激光照射通过采样气流颗粒物会使激光发生散射通过检测散射光的强度来反推颗粒物的浓度。这类传感器输出的是经过内部算法计算后的数字信号通常是UART串口或PWM脉冲Arduino直接读取即可。市面上常见的型号有攀藤PlantowerPMS5003/PMS7003系列这是开源项目和DIY圈里的“明星产品”性价比极高。它通过UART输出PM1.0、PM2.5、PM10的浓度值单位μg/m³数据相对可靠。我这次项目用的就是PMS5003它的蓝色塑料外壳很有辨识度。夏普SharpGP2Y1010AU0F这是一个比较老的型号输出的是模拟电压值需要Arduino的模拟引脚读取再通过公式换算。它的缺点是容易受干扰需要外接电路电阻电容且没有PM1.0/PM10的分级数据现在已不推荐作为首选。更专业的传感器如Sensirion的SPS30精度更高还具有颗粒物数量浓度输出但价格也贵不少。注意激光传感器内部有风扇进行主动吸气因此会有轻微噪音和功耗。在选购时务必确认其工作电压通常是5V和通信接口是否与你的主控板匹配。CO2传感器NDIR原理才是真靠谱测量CO2的方法更多但适合民用的主要是NDIR非分散性红外原理。因为CO2对特定波长的红外光有强烈的吸收特性传感器通过测量红外光穿过气室后的衰减程度就能精确计算出CO2浓度。其他如半导体式MQ系列的“CO2传感器”其实对酒精、香烟等多种气体都有交叉敏感性测CO2极不准确千万别买错。常见的NDIR CO2传感器有MH-Z19B/MH-Z19C国产传感器中的佼佼者价格亲民百元级别通过UART或PWM输出量程可达5000ppm自带温度补偿精度在±50ppm5%读数左右完全满足室内监测需求。我用的就是MH-Z19B。SenseAir S8瑞典品牌性能更稳定精度更高但价格也几乎是MH-Z19B的两倍以上适合要求更高的项目。SCD30/SCD40Sensirion的传感器除了CO2还能测量温度和湿度且体积小巧采用I2C通信但价格昂贵属于“发烧级”选择。我的搭配方案是攀藤PMS5003PM2.5 炜盛MH-Z19BCO2两者都使用5V供电UARTTX/RX通信总成本可控性能和可靠性经过了大量项目验证。2.2 主控板与外围设备选择主控板Arduino Uno的经典与ESP32的智能Arduino Uno R3经典之选。引脚丰富5V逻辑电平与上述传感器完美兼容。如果你只想本地读取数据并在串口监视器上查看或者驱动一个LCD屏显示Uno完全够用。它的缺点是只有一组硬件串口Serial而我们的两个传感器都是UART的这就需要用到SoftwareSerial库来创建软串口可能会在高速或复杂通信时有点不稳定。ESP32 DevKit我更加推荐的选择。它是一块功能强大的物联网开发板自带Wi-Fi和蓝牙。除了能完成Uno的所有工作其最大优势是可以轻松地将数据上传到物联网平台如Blynk、ThingsBoard、本地服务器如Home Assistant或者直接显示在网页上。ESP32有多个硬件串口可以更稳定地同时连接两个传感器。本项目后续的联网功能就是基于ESP32实现的。显示与交互设备OLED显示屏SSD1306, 0.96寸I2C接口功耗低显示清晰非常适合显示实时数据。比传统的LCD1602液晶屏好看得多。按键或旋转编码器用于切换显示页面、设置报警阈值等。LED指示灯与蜂鸣器用于超标报警提供声光提示。电源建议使用可靠的5V/2A以上的USB电源适配器供电。如果使用ESP32并开启Wi-Fi功耗会有所增加劣质电源可能导致传感器工作不稳定或重启。3. 电路连接与硬件搭建详解硬件连接是项目的实体骨架务必仔细。我的接线原则是先供电再信号先主控再外设布局清晰避免飞线。3.1 基于Arduino Uno的接线方案基础版如果你使用Arduino Uno由于只有一个硬件串口Pin 0 RX, Pin 1 TX通常用于上传程序和电脑通信我们需要为两个传感器分配软串口。所需材料清单Arduino Uno R3 x1PMS5003 PM2.5传感器 x1MH-Z19B CO2传感器 x10.96寸 I2C OLED显示屏 x1面包板、杜邦线公对公、公对母若干5V/2A USB电源 x1接线步骤供电统一将面包板的正极和负极-排孔分别连接到Arduino的5V和GND。所有模块的VCC和GND都分别连接到面包板的和-排孔上。连接PMS5003PMS5003的VCC- 面包板5VPMS5003的GND- 面包板GNDPMS5003的RX- ArduinoPin 2(我们将把Pin2定义为软串口的TX去连接传感器的RX)PMS5003的TX- ArduinoPin 3(我们将把Pin3定义为软串口的RX去连接传感器的TX)PMS5003的SET引脚悬空即可用于切换模式普通使用不需连接。连接MH-Z19BMH-Z19B的Vin- 面包板5VMH-Z19B的GND- 面包板GNDMH-Z19B的RX- ArduinoPin 4(软串口TX)MH-Z19B的TX- ArduinoPin 5(软串口RX)连接OLED显示屏OLED的VCC- 面包板5VOLED的GND- 面包板GNDOLED的SCL- ArduinoA5(在Uno上A4是SDAA5是SCL)OLED的SDA- ArduinoA4重要提示UART通信是交叉连接即主控的TX接传感器的RX主控的RX接传感器的TX。很多新手会接反导致无法通信。另外MH-Z19B上可能还有一个PWM输出引脚我们不用忽略即可。3.2 基于ESP32的接线方案进阶联网版ESP32的接线更为简洁因为它有多个硬件串口。所需材料清单ESP32 DevKit V1或类似型号 x1PMS5003 PM2.5传感器 x1MH-Z19B CO2传感器 x10.96寸 I2C OLED显示屏 x1面包板、杜邦线若干5V/2A USB电源 x1接线步骤供电注意ESP32的引脚工作电压是3.3V但它的VIN引脚可以接受5V输入板载稳压器会将其降到3.3V。PMS5003和MH-Z19B需要5V供电OLED可以工作在3.3V。最佳实践是使用一个5V电源适配器正极同时接到ESP32的VIN引脚和面包板的5V排孔负极共地。这样ESP32和传感器都获得了合适的电压。连接PMS5003使用ESP32的硬件串口2UART2。PMS5003VCC- 面包板5VPMS5003GND- 公共GNDPMS5003TX- ESP32GPIO16(RX2)PMS5003RX- ESP32GPIO17(TX2)连接MH-Z19B使用ESP32的硬件串口1UART1。注意UART1的默认TX引脚GPIO10有些板子可能未引出我们可以自定义引脚。MH-Z19BVin- 面包板5VMH-Z19BGND- 公共GNDMH-Z19BTX- ESP32GPIO18(自定义为RX)MH-Z19BRX- ESP32GPIO19(自定义为TX)连接OLED显示屏OLEDVCC- ESP323.3V引脚OLEDGND- 公共GNDOLEDSCL- ESP32GPIO22OLEDSDA- ESP32GPIO21硬件搭建心得上电顺序建议先给主控板上电待程序启动完成后再连接传感器电源。特别是MH-Z19B有用户反馈热启动时主控不断电传感器重新上电可能通信异常。最稳妥的方法是所有设备同时上电。线缆整理使用不同颜色的杜邦线区分电源红色5V黑色GND和信号线黄色TX绿色RX等后期调试会轻松很多。防静电与保护传感器激光头和红外气室都是精密光学部件避免用手触摸组装时注意防尘。4. 软件编程与核心代码解析硬件搭好只是成功了一半让它们“活”起来的代码才是灵魂。我将分模块讲解代码并提供完整的示例。这里以功能更强大的ESP32版本为例Uno版本逻辑类似主要区别在于串口初始化。4.1 开发环境与库文件准备安装Arduino IDE从Arduino官网下载并安装最新版IDE。对于ESP32你需要添加开发板支持。添加ESP32开发板在IDE的“文件”-“首选项”的“附加开发板管理器网址”中添加https://espressif.github.io/arduino-esp32/package_esp32_index.json。然后在“工具”-“开发板”-“开发板管理器”中搜索“esp32”并安装。安装必要的库U8g2库用于驱动OLED显示屏功能强大支持多种字体和图形。在“项目”-“加载库”-“管理库”中搜索“U8g2”安装。SoftwareSerial库仅Uno需要Arduino IDE自带。PMS库用于解析PMS5003数据。在库管理器中搜索“PMS”或“Plantower”找到并安装。MH-Z19库用于驱动MH-Z19B传感器。搜索“MH-Z19”安装。4.2 核心代码模块拆解我们将代码分为几个部分传感器初始化、数据读取、数据显示、数据上传可选。第一部分全局定义与库引入#include U8g2lib.h // OLED驱动库 #include PMS.h // PMS传感器库 #include MHZ19.h // MH-Z19传感器库 #include HardwareSerial.h // ESP32硬件串口 // 定义OLED对象根据你的OLED型号修改构造函数常见的是SSD1306 128x64 I2C U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset*/ U8X8_PIN_NONE); // 定义PMS传感器对象使用硬件串口2 PMS pms(Serial2); // ESP32: Serial2的默认引脚是GPIO16(RX), GPIO17(TX) PMS::DATA data; // 定义MH-Z19传感器对象使用自定义引脚的硬件串口1 HardwareSerial mySerial(1); // 创建串口1实例 MHZ19 myMHZ19; // 创建MH-Z19实例 // 传感器数据变量 int pm25, pm10; int co2ppm; float temperature; // MH-Z19B也能读取板载温度 // 定时器相关用于控制读取频率 unsigned long previousMillis 0; const long interval 5000; // 每5秒读取一次传感器PMS需要稳定气流不宜过快第二部分初始化设置setup()void setup() { // 启动串口调试用于在电脑端查看数据 Serial.begin(115200); Serial.println(Air Quality Monitor Starting...); // 初始化OLED u8g2.begin(); u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); // 设置字体 u8g2.drawStr(0, 20, Initializing...); u8g2.sendBuffer(); delay(1000); // 初始化PMS传感器串口 Serial2.begin(9600, SERIAL_8N1, 16, 17); // RX16, TX17 pms.passiveMode(); // 设置为被动模式由我们主动请求数据 // pms.activeMode(); // 主动模式传感器会自动持续发送数据但更耗电 // 初始化MH-Z19传感器串口 mySerial.begin(9600, SERIAL_8N1, 18, 19); // RX18, TX19 myMHZ19.begin(mySerial); // 将串口实例传递给MHZ19库 myMHZ19.autoCalibration(false); // 关闭自动基线校准ABC。对于室内长期固定使用的场景建议关闭否则它会将连续运行几天后的低值作为零点。 // 预热与首次读取 delay(2000); // 给传感器预热时间 readSensors(); // 首次读取 displayData(); // 首次显示 }关键点解析PMS被动模式 vs 主动模式被动模式下MCU需要发送请求命令传感器才返回一帧数据更省电且可控。主动模式下传感器会持续发送数据可能造成串口缓冲区溢出。推荐使用被动模式。MH-Z19自动校准autoCalibration(false)非常重要。如果开启默认是开启的传感器会以连续运行一段时间如7天内的最低浓度值作为400ppm的基准点。这在室内场景很危险如果房间一直通风不良CO2浓度长期在1000ppm以上传感器会误以为1000ppm是“新鲜空气”导致读数严重偏低。对于室内监测务必关闭。第三部分主循环loop()与数据读取函数void loop() { unsigned long currentMillis millis(); // 定时读取传感器 if (currentMillis - previousMillis interval) { previousMillis currentMillis; readSensors(); displayData(); // 可以在这里添加数据上传函数如 uploadToServer(); } } void readSensors() { // 读取PMS5003数据 pms.requestRead(); if (pms.readUntil(data)) { pm25 data.PM_AE_UG_2_5; // 大气环境下PM2.5浓度 (μg/m³) pm10 data.PM_AE_UG_10_0; // 你也可以读取 data.PM_AE_UG_1_0 (PM1.0) } else { Serial.println(PMS read failed.); pm25 pm10 -1; // 标记读取失败 } // 读取MH-Z19B数据 co2ppm myMHZ19.getCO2(); // 获取CO2浓度 temperature myMHZ19.getTemperature(); // 获取温度可选 // 打印到串口监视器用于调试 Serial.print(PM2.5: ); Serial.print(pm25); Serial.print( μg/m³ | ); Serial.print(PM10: ); Serial.print(pm10); Serial.print( μg/m³ | ); Serial.print(CO2: ); Serial.print(co2ppm); Serial.println( ppm); }避坑指南PMS读取失败pms.readUntil(data)可能会超时失败。确保接线正确波特率是9600并且传感器已稳定工作风扇在转。失败时最好有错误处理而不是使用旧数据。MH-Z19读数异常如果返回-1或9999等异常值检查接线TX/RX是否接反、供电是否充足5V电压是否稳定。尝试发送myMHZ19.getCO2(true)请求一个带校验的新读数。第四部分OLED显示函数void displayData() { u8g2.clearBuffer(); // 绘制边框或标题 u8g2.drawFrame(0,0,128,64); // 显示PM2.5数据 u8g2.setFont(u8g2_font_helvB14_tf); u8g2.setCursor(10, 25); u8g2.print(PM2.5); u8g2.setCursor(70, 25); if(pm25 0) { u8g2.print(pm25); u8g2.setFont(u8g2_font_helvB08_tf); u8g2.print( ug/m3); } else { u8g2.print(Err); } // 显示CO2数据 u8g2.setFont(u8g2_font_helvB14_tf); u8g2.setCursor(10, 50); u8g2.print(CO2); u8g2.setCursor(70, 50); u8g2.print(co2ppm); u8g2.setFont(u8g2_font_helvB08_tf); u8g2.print( ppm); // 根据空气质量添加简单提示 u8g2.setFont(u8g2_font_6x10_tf); u8g2.setCursor(5, 62); if (pm25 75 || co2ppm 1000) { u8g2.print( Poor Air ); } else if (pm25 35 || co2ppm 800) { u8g2.print( Moderate ); } else { u8g2.print( Good ); } u8g2.sendBuffer(); // 将缓冲区内容发送到显示屏 }这段代码实现了在OLED上分两行显示PM2.5和CO2的数值并底部有一个简单的空气质量状态提示。你可以发挥创意添加更多图形比如用进度条表示浓度或者显示历史趋势简图。5. 功能扩展与数据可视化实践基础数据显示完成了但一个完整的空气质量监测系统离不开数据记录和可视化。这里我分享两个最实用的扩展方向本地数据记录和云端数据可视化。5.1 本地SD卡数据记录如果你想在不依赖网络的情况下长期记录数据添加一个SD卡模块是最经济实惠的方案。硬件添加购买一个SPI接口的Micro SD卡模块通常几块钱连接到ESP32SD模块 VCC- 5VSD模块 GND- GNDSD模块 MOSI- GPIO23SD模块 MISO- GPIO19 (注意如果和MH-Z19冲突需调整引脚)SD模块 SCK- GPIO18SD模块 CS- GPIO5代码修改你需要包含SD.h和SPI.h库。在setup()中初始化SD卡在readSensors()函数后添加数据写入逻辑。#include SD.h #include SPI.h File dataFile; void setup() { // ... 其他初始化代码 ... if (!SD.begin(5)) { // CS引脚接在GPIO5 Serial.println(SD Card initialization failed!); return; } Serial.println(SD Card initialized.); } void logDataToSD() { dataFile SD.open(/airlog.csv, FILE_APPEND); // 以追加模式打开文件 if (dataFile) { dataFile.print(millis()); // 时间戳 dataFile.print(,); dataFile.print(pm25); dataFile.print(,); dataFile.print(pm10); dataFile.print(,); dataFile.print(co2ppm); dataFile.print(,); dataFile.println(temperature); dataFile.close(); } else { Serial.println(Error opening airlog.csv); } }然后在loop()的定时任务中调用logDataToSD()。数据会以CSV格式保存方便用Excel或Python进行后续分析。5.2 基于ESP32的无线数据传输与云端可视化这才是发挥ESP32真正威力的地方。将数据上传到云端你可以在任何地方的手机或电脑上查看实时数据和历史曲线。方案一使用Blynk平台快速入门Blynk提供了一个非常简单的物联网平台适合快速原型开发。在手机应用商店下载Blynk App创建一个新项目选择ESP32设备获取Auth Token。在项目中添加两个Gauge仪表组件分别绑定虚拟引脚V0和V1。在Arduino代码中安装Blynk库并添加以下代码#define BLYNK_PRINT Serial #include WiFi.h #include BlynkSimpleEsp32.h char auth[] YourAuthToken; // 从App获取 char ssid[] YourWiFiSSID; char pass[] YourWiFiPassword; void setup() { // ... 其他初始化 ... Blynk.begin(auth, ssid, pass); } void loop() { Blynk.run(); // 维持Blynk连接 // ... 你的定时读取传感器代码 ... if (timeToSend) { Blynk.virtualWrite(V0, pm25); // 将PM2.5发送到虚拟引脚V0 Blynk.virtualWrite(V1, co2ppm); // 将CO2发送到虚拟引脚V1 } }上传代码后你就能在Blynk App的仪表盘上看到实时数据了。方案二使用MQTT协议 自建服务器更灵活可控对于想深度掌控数据的开发者MQTTNode-REDInfluxDBGrafana是一套强大的组合拳。MQTT轻量级的物联网消息协议。ESP32使用PubSubClient库向MQTT代理如Mosquitto发布消息。Node-RED图形化编程工具可以订阅MQTT主题对数据进行处理如单位转换、过滤然后写入数据库。InfluxDB专门为时间序列数据优化的数据库存储效率极高。Grafana强大的数据可视化平台从InfluxDB读取数据绘制出精美的实时图表和历史曲线。这套方案需要你在电脑或树莓派等上部署服务有一定门槛但数据完全私有功能无限。你可以创建这样的面板同时显示PM2.5和CO2的实时曲线设置不同颜色的报警阈值线还能计算一天内的平均值、超标时长等统计信息。6. 校准、维护与常见问题排查DIY设备不比商业产品需要一些额外的调校和维护才能获得可靠数据。6.1 传感器校准与准确性提升PM2.5传感器PMS5003激光传感器一般出厂已校准无法手动校准。其准确性受环境湿度影响较大高湿度时水蒸气会被误判为颗粒物。可以通过在干燥天气与一个靠谱的商用检测仪对比得出一个大概的修正系数例如乘以0.7或0.8。更专业的做法是收集一段时间的数据与参考仪器进行线性回归分析。CO2传感器MH-Z19B手动校准零点这是最重要的校准。你需要将传感器带到室外通风良好的地方确保CO2浓度在400ppm左右稳定运行20分钟后通过发送特定指令进行零点校准。注意务必在空气新鲜的环境下操作否则会校准错误。// 在Arduino代码中确保传感器已预热稳定在新鲜空气中 myMHZ19.calibrateZero(); // 发送零点校准命令关闭自动校准ABC如前所述室内应用必须关闭。跨度校准通常不需要。如果你有已知浓度的标准气体如2000ppm可以进行跨度校准以提升线性度。6.2 长期运行维护要点清洁PM2.5传感器的进气口和激光头会积灰建议每3-6个月用压缩气罐或软毛刷轻轻清理。切勿用嘴吹或湿布擦拭。预热传感器每次断电重启后需要一段时间PMS约30秒MH-Z19B约3分钟读数才能稳定。程序里应加入预热延迟或丢弃初期不稳定数据的逻辑。供电稳定使用质量好的5V电源。电压波动可能导致传感器重启或读数异常。数据验证偶尔将你的设备读数与已知准确的设备如朋友家的品牌检测仪进行对比做到心中有数。6.3 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案串口监视器无数据或乱码1. 波特率设置错误2. TX/RX接反3. 供电不足4. 代码中串口引脚定义错误1. 检查Serial.begin()、Serial2.begin()的波特率是否与传感器一致PMS:9600, MH-Z19B:9600。2. 交换TX和RX线再试。3. 尝试单独给传感器外接5V电源或换用电流更大的电源。4. 核对代码中HardwareSerial或SoftwareSerial的引脚定义。PMS5003读数始终为01. 处于被动模式但未发送请求命令2. 风扇未转传感器损坏或供电问题1. 确认代码中调用了pms.requestRead()和pms.readUntil(data)。2. 观察传感器侧面的小风扇是否在微微转动。不转则检查5V供电或传感器可能已损坏。MH-Z19B读数始终为410ppm或固定值1. 通信失败读取到的是上电默认值2. 自动校准ABC开启且处于低浓度环境1. 检查接线和供电。尝试使用myMHZ19.getCO2(true)获取带校验的读数。2. 确认已执行myMHZ19.autoCalibration(false)。CO2读数异常高如持续20001. 传感器暴露在人体呼吸区2. 零点漂移严重1. 将传感器放置在房间中央或高处远离人嘴和通风口。2. 执行一次正确的室外零点校准。OLED屏幕不显示或花屏1. I2C地址不对2. 供电不足ESP32用3.3V3. 库不匹配1. 使用I2C扫描程序确认OLED地址通常是0x3C。2. 确保OLED的VCC接的是3.3VESP32或5VUno。3. 尝试U8g2库提供的不同构造函数。ESP32连接WiFi经常断开1. WiFi信号弱2. 电源干扰1. 让设备靠近路由器或代码中加入WiFi重连逻辑。2. 为ESP32的电源引脚并联一个100-470uF的电解电容滤除因传感器风扇启停造成的电压波动。数据上传到云端失败1. 网络问题2. MQTT/Blynk参数错误3. 代码逻辑问题1. 检查ESP32能否ping通外网。2. 核对服务器地址、端口、用户名、密码、Topic/Auth Token。3. 在数据上传代码前后添加串口打印确认数据已正确准备并检查上传函数的返回值。这个项目从简单的数据读取到复杂的物联网系统有非常广阔的扩展空间。你可以增加温湿度传感器如DHT22、甲醛传感器甚至联动家里的智能插座在空气质量超标时自动打开新风或空气净化器。动手的过程就是学习的过程当你看到自己亲手制作的设备忠实地记录着环境的每一次呼吸那种成就感和对身边环境的全新认知是购买任何成品都无法替代的。希望这份超详细的指南能帮你少走弯路顺利打造出属于你自己的空气质量守护者。