ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ESP32+传感器打造家庭空气质量监测系统:从选型到数据校准全解析

ESP32+传感器打造家庭空气质量监测系统:从选型到数据校准全解析 住在城市里大家对“空气质量”这个词基本不陌生但很多人对它的感知还停留在天气预报里的AQI指数或者是手机上偶尔弹出的污染提醒。直到我家娃有一阵子反复咳嗽医生随口问了一句“家里新风开不开、甲醛测过没有”我才发现自己对每天待得最久的室内环境几乎一无所知。也就是从那天起我动了给自己搞一套家庭空气质量监测系统的念头。这个项目标题叫Home Air Quality Monitoring (Part 1)听起来挺学院派其实说白了就是用传感器把家里看不见摸不着的东西量化出来让自己知道空气到底好不好。Part 1 我定的范围很明确——先做一台能独立运行、实时显示、数据可查的室内空气监测节点把 PM2.5、CO2、温湿度、TVOC 这几个核心指标跑通后续再考虑数据上云和联动控制。这篇文章会把我的硬件选型、接线、代码、踩坑全过程都摊开来讲适合有一定动手基础、想入门物联网或者环境监测的朋友参考。1. 项目整体设计与思路拆解1.1 家庭场景到底需要监测什么很多人一上来就奔着“全都要”去PM2.5、甲醛、CO2、TVOC、温湿度、噪声、光照恨不得把所有能买的传感器都堆上去。我在设计这个项目时反而先做了减法因为家庭空气监测的核心诉求其实就那么几个PM2.5 浓度颗粒物是室内空气污染的“主角”做饭油烟、打扫扬尘、室外渗入都会引起它波动。它直接关系呼吸道健康尤其是家里有老人小孩的这是刚需指标。CO2 浓度CO2 反映的是“通风换气是否充分”。一间卧室关窗睡一夜CO2 轻松破 2000ppm第二天早上头昏脑涨往往不是没睡好而是缺氧了。这个指标对判断“什么时候该开窗”特别有指导意义。温湿度体感舒适度的基础参数也影响其他传感器的读数修正。比如很多 PM2.5 传感器对湿度敏感湿度过高时测量值会偏大没有温湿度数据你很难判断读数是环境真实值还是传感器“失真”。TVOC/甲醛TVOC 是总挥发性有机物的统称装修污染、新家具、香薰、烹饪都会产生。甲醛严格来说需要电化学传感器才测得准但作为家庭日常参考TVOC 指标可以帮助发现“什么时候污染突然升高了”。我的建议是第一版先保证 PM2.5、CO2、温湿度这三个指标稳定可靠TVOC 作为辅助参考。功能做少一点没关系每个数据都准确比什么都有但什么都不准强一百倍。1.2 传感器选型的对比与权衡选传感器是整个项目里最花时间的一步。我前后对比了七八款最终留下来的核心是这三颗传感器测量内容接口精度表现备注Sensirion SEN55PM1.0/PM2.5/PM4/PM10、温湿度、TVOC、NOxI2C/UARTPM2.5 一致性较好自带温湿度校准价格偏高但一颗芯片解决多指标省心Sensirion SCD40CO2I2C精度 ±(50ppm 5%)体积小寿命长适合长期运行SHT40温湿度I2C温度 ±0.2°C湿度 ±1.8%RHSEN55 自带温湿度SHT40 作为独立校验用这里必须说一句如果预算有限把最大头留给 CO2 传感器PM2.5 可以用性价比更高的 Plantower PMS5003。PMS5003 是国内用得最多的颗粒物传感器几十块钱数据也很能打只是体积比 SEN55 大不少。我这次选 SEN55 纯粹是为了缩小整机体积顺便省掉一个温湿度传感器。CO2 传感器有个坑要注意市面上便宜的“CO2 传感器”很多其实是假的用 TVOC 估算出来的 eCO2不是真正测量二氧化碳浓度。真正靠谱的是 NDIR非色散红外原理的传感器比如 SCD40、SCD30、MH-Z19B。选型时认准 NDIR 原理别被“eCO2”这种估算值忽悠了。1.3 主控与系统架构怎么定主控我选了 ESP32理由很直接自带 Wi-Fi 和蓝牙后续要做数据上报、远程查看不需要再外接网络模块。同时它的 ADC、I2C、UART 接口资源充足带 OLED 屏幕完全无压力。架构上我走了“传感器数据采集 本地 OLED 显示 串口日志输出”的最小可用方案没有一上来就接 Home Assistant 或者云平台。为什么这样设计把第一版做得简单是为了先验证硬件链路和数据质量。很多项目的失败不是因为代码写不出来而是传感器数据不准、不稳定后续所有功能都建于流沙之上。先把采集端做扎实再考虑数据去哪。系统框图大致是这样的ESP32 作为主控通过 I2C 总线挂载 SEN55 和 SCD40电源用 5V 输入经稳压给传感器供电ESP32 的 3.3V 给 OLED 屏幕供电。数据采集循环里每 2 秒读取一次传感器数据经过简单的平滑处理后推送到屏幕刷新。整个系统不依赖外部网络上电即用。2. 核心元器件与硬件连接细节2.1 元器件采购清单与避坑建议我列一下我这个项目实际用到的物料方便大家直接照着买ESP32 DevKitC V4 开发板买的时候注意选 CP2102/CP2104 串口芯片版本驱动兼容性好Sensirion SEN55 空气质量传感器模组Sensirion SCD40 CO2 传感器模组0.96 寸 SSD1306 OLED 显示屏I2C 接口128x64 分辨率AMS1117-3.3 稳压模块如果直接用 5V 供电给 ESP32 就不需要额外降压10kΩ 电阻 2 个I2C 上拉虽然大部分模块自带但准备着没坏处面包板、杜邦线若干、外壳我用的 3D 打印这里有几个采购时的注意事项SEN55 和 SCD40 都是 SMD 贴片封装没有现成的插针模组的话焊接起来比较痛苦建议直接买商家做好的转接板版本。SEN55 的转接板一般会引出标准的 1.27mm 间距排针SCD40 转接板则是 2.54mm 标准排针接线方便很多。电源部分我要多说两句。ESP32 的 WiFi 开启瞬间电流会冲到 500mA 左右传感器工作电流小但 SCD40 内部有加热元件启动时也有一个短暂的电流尖峰。如果用稳压模块必须确保它能稳定输出 3.3V/1A 以上我用的是 MP1584 降压模块输入 6-12V输出设定为 5V再通过开发板自带的 AMS1117 转为 3.3V。这样比你直接用 USB 供电要稳不少尤其后面要长期通电运行。2.2 I2C 总线的接线与电平匹配I2C 是这项目的“血管主干道”SEN55 和 SCD40 都挂在同一条 I2C 总线上。接线方式如下传感器引脚ESP32 GPIO说明SEN55 VCC5VSEN55 支持 5V 供电不能接 3.3VSEN55 GNDGND共地SEN55 SDAGPIO21I2C 数据线SEN55 SCLGPIO22I2C 时钟线SEN55 SELGND拉低选择 I2C 模式悬空默认 UARTSCD40 VIN3.3VSCD40 支持 3.3V 供电SCD40 GNDGND共地SCD40 SDAGPIO21与 SEN55 共用 SDASCD40 SCLGPIO22与 SEN55 共用 SCLOLED VDD3.3V屏幕供电OLED SDA/SCLGPIO21/GPIO22同样挂载 I2C 总线接线看起来不复杂但里面藏着两个特别容易踩的坑我一开始就都踩了一遍第一个坑是SEN55 的 SEL 引脚不接直接默认走 UART 模式。这个传感器出厂默认是 UART 输出你把它接上 I2C 总线后扫描设备地址死活找不到它。手册里的原话是SEL 引脚悬空 UART拉低 I2C。我刚开始图省事没接这颗线结果 I2C 扫描只有 SCD40 响应排查了半天才发现是这个问题。第二个坑是I2C 总线上多设备地址冲突。SEN55 的 I2C 地址是 0x69SCD40 是 0x62OLED 是 0x3C三者互不冲突接线前最好确认一下你手上模块的实际地址。如果某块转接板带了 A0/A1 跳线可以改地址改之前先查数据手册不要拍脑袋乱跳线。2.3 传感器上电初始化顺序硬件接好以后上电顺序也有讲究。我的习惯是先给 ESP32 上电等它跑起来后再给传感器通电用杜邦线手动插拔也行或者给传感器加一个由 GPIO 控制的 MOSFET 开关。为什么SEN55 和 SCD40 这类传感器都有启动自检和预热过程SEN55 大约需要 10-15 秒预热SCD40 在首次上电后需要一段时间让内部的参考值稳定。如果所有设备同时上电ESP32 在这段时间里可能已经把 I2C 总线“锁死”了——传感器还没准备好接收指令主控却在拼命重试结果就是 I2C 通信异常。稳妥的做法是先把代码烧录好烧录时断开传感器电源给 ESP32 上电等待 5 秒再给传感器模组通电观察串口日志等传感器返回正常数据后面我为了图省事把传感器直接接在 ESP32 的 5V 引脚上靠开发板统一上电。实测下来只要代码里加了一个 10 秒钟的启动延时就基本不会出问题。启动延时是这里的关键设计不是可有可无的装饰。3. 数据采集代码实现与本地展示3.1 开发环境与依赖库准备软件层面我用的是 Arduino IDE 搭配 ESP32 开发板支持包版本用的 2.x 系列。选 Arduino 而不是 ESP-IDF 的理由很简单这个项目体量不大Arduino 丰富的第三方库能让我把精力集中在业务逻辑上而不是去跟底层协议死磕。需要安装的第三方库Sensirion I2C SEN5xSEN55 官方库SparkFun SCD40 和 SCD41SCD40 官方库U8g2OLED 显示库对各种字体和屏幕驱动支持很好WireArduino 内置的 I2C 库装库的时候有个要注意的点Sensirion 官方库对 Arduino 的适配版本要求比较高如果编译报错先检查库版本是不是太旧。我一开始装的是老版本编译死活过不去升级到 1.0.4 之后问题才解决。3.2 核心代码实现与注释我的代码分三个文件主程序、传感器管理模块、显示模块。这里只贴最关键的部分完整的项目文件我放到文末的资源链接里。首先是主程序的初始化逻辑#include Arduino.h #include Wire.h #include SensirionI2cSen5x.h #include SparkFun_SCD40_Arduino_Library.h #include U8g2lib.h SensirionI2cSen5x sen55; SCD40 scd40; U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset*/ U8X8_PIN_NONE); // 全局变量保存各类传感器读数 struct AirData { float pm25; float co2; float temperature; float humidity; float tvoc; bool valid; }; AirData airData {0, 0, 0, 0, 0, false}; void setup() { Serial.begin(115200); Wire.begin(21, 22); // SDA, SCL // 关键给传感器留足上电启动时间 delay(10000); // 初始化 SEN55 sen55.begin(Wire); sen55.deviceReset(); delay(500); // 初始化 SCD40 scd40.begin(); scd40.setMeasurementMode(SCD40_MEASUREMENT_MODE_PERIODIC); scd40.setPeriodicMeasurement(true); // 开始测量 sen55.startMeasurement(); u8g2.begin(); u8g2.setFont(u8g2_font_6x12_tr); Serial.println(Home Air Quality Monitor v1.0); Serial.println(Sensor init done.); }这段代码里最值得说的是sen55.deviceReset()这一句。SEN55 使用前最好无条件执行一次软复位作用是把传感器恢复到已知状态。如果你之前跑过其他程序、传感器内部状态已经乱了不复位的话接下来可能读到异常的 PM 值。SCD40 则不需要额外复位直接用begin()即可。再看读取逻辑我放在loop()里每 2 秒执行一次void loop() { readSensors(); updateDisplay(); printSerialLog(); delay(2000); } void readSensors() { unsigned long startTime millis(); // 读取 SEN55 数据 float pm1_0, pm2_5, pm4_0, pm10, humidity, temperature, tvoc, nox; uint16_t error; char errorMessage[256]; error sen55.readMeasuredValues( pm1_0, pm2_5, pm4_0, pm10, humidity, temperature, tvoc, nox ); if (error ! NO_ERROR) { Serial.print(SEN55 read failed: ); errorToString(error, errorMessage, 256); Serial.println(errorMessage); airData.valid false; } else { airData.pm25 pm2_5; airData.temperature temperature; airData.humidity humidity; airData.tvoc tvoc; } // 读取 SCD40 数据 if (scd40.measurementReady()) { scd40.readMeasurement(); airData.co2 scd40.getCO2(); } // 防止循环执行过快导致传感器频繁重读 unsigned long elapsed millis() - startTime; if (elapsed 1000) { delay(1000 - elapsed); } }这里有几个性能上的细节SEN55 的读取间隔不要低于 1 秒。它内部的采样频率是 1Hz你 2 秒读一次是合理的选择读取太频繁白白占用 I2C 带宽还可能引发“设备繁忙”错误。SCD40 的measurementReady()机制是它的特色只有数据准备好了才去读避免读到无效值。另外注意sen55.readMeasuredValues()一次调用能拿回全部 8 个指标你不需要分多次读这样既减少了 I2C 通信次数也避免了多次读取之间数据不一致的问题。3.3 OLED 屏幕布局与刷新策略OLED 显示部分我采用了“四象限”布局一屏装下所有核心数据刷起来一眼就能看完。屏幕只有 128x64 像素空间很紧张必须想清楚优先级┌─────────────────────┐ │ PM2.5 028 µg/m³ │ │ CO2 650 ppm │ │ Temp 24.5 °C │ │ RH 56 % │ │ TVOC 120 µg/m³ │ └─────────────────────┘对应的显示代码void updateDisplay() { char line[32]; u8g2.clearBuffer(); snprintf(line, sizeof(line), PM2.5 %05.0f ug/m3, airData.pm25); u8g2.drawStr(0, 12, line); snprintf(line, sizeof(line), CO2 %05.0f ppm, airData.co2); u8g2.drawStr(0, 26, line); snprintf(line, sizeof(line), Temp %04.1f C, airData.temperature); u8g2.drawStr(0, 40, line); snprintf(line, sizeof(line), RH %03.0f %%, airData.humidity); u8g2.drawStr(0, 54, line); u8g2.sendBuffer(); }刷新策略上我踩过一个坑OLED 的 I2C 写入如果太频繁会导致传感器读取的时序被干扰。这是因为I2C 总线上多设备通信时屏幕刷新占用了大量总线时间。我的解决方法是把显示刷新降到 1 秒一次传感器读取保持 2 秒一次两者错开执行。实际效果是屏幕没有出现闪烁传感器数据也没有再出现过丢失。3.4 数据平滑处理为什么不能直接用原始值传感器读回来的原始数据是跳动的。PM2.5 尤其明显你看着屏幕数值从 25 跳到 40 再跳到 30非常影响观感。这不全是传感器的错——空气中的颗粒物本身就是动态分布的激光散射法测的就是瞬时粒子数。我做了三层处理第一层是中值滤波取最近 5 次采样值的中位数。中值滤波对去除脉冲型噪声特别有效比如偶尔一次大颗粒物经过造成的尖峰。相比平均值中位数不会被极端值带偏。第二层是加权滑动平均公式是smoothed smoothed * 0.7 newValue * 0.3这个系数要按传感器响应速度灵活调整0.7/0.3 是我实测下来比较平衡的配置既保留了变化的趋势又不会让数值跳来跳去。如果你觉得显示太迟钝可以改成 0.5/0.5如果觉得太跳就改成 0.85/0.15。第三层是变化量阈值控制如果新采样值与当前平滑值相差不足 2 个单位直接不更新显示。这能避免屏幕数值在低浓度区间频繁跳动。void updateWithSmoothing(float newValue, float smoothedValue) { if (smoothedValue 0) { smoothedValue newValue; return; } smoothedValue smoothedValue * 0.7 newValue * 0.3; if (abs(newValue - smoothedValue) 2.0) { // 变化量过小维持原值 } }4. 传感器校准与数据可靠性实战4.1 校准的意义一颗准的传感器比十颗不准的更重要传感器上电就能读数但读数准不准完全是另一回事。坦白说刚装完这台设备时我对屏幕上的数字是打问号的——PM2.5 和官方监测站的数据对不上CO2 在开窗通风后也没有立刻降到预期值。这时候才意识到校准和验证是系统真正可用的关键一环。校准的思路是用已知参照物给传感器找偏差。室外空气是最容易获取的参照物。在空气质量较好、风速较大的日子里把设备放到窗口通风处让室内外空气充分交换这时候传感器的理论读数应该和所在城市的官方 PM2.5 发布值比较接近。如果偏差超过 20%就要检查传感器内部是否有污染或者气流通道是否被遮挡。CO2 校准更简单。找一个通风良好的清晨把设备放窗边此时室外 CO2 浓度约为 420ppmSCD40 读数如果稳定在 400-450ppm 之间说明传感器状态正常。如果偏差大可以通过 SCD40 提供的setSensorAltitude()接口做高度补偿或者直接使用 FORCE_RECALIBRATE 命令手动校准到 420ppm。一个很多人忽略的点SCD40 还有个自动自校准机制ASC它会根据一段时间内传感器检测到的最低 CO2 值默认认为是新鲜空气约 420ppm自动修正偏移。但这种机制有个前提——传感器必须经历至少 7 天的正常运行且期间确实出现过通风良好的低浓度环境。如果你家长期不开窗ASC 反而可能把基准越拉越偏。我建议前两周关闭 ASC手动校准一次后再开启确保初始基准正确。4.2 放置位置比你想的更影响数据质量传感器放哪直接决定数据有没有参考价值。这里有几条硬规矩不要放在墙角。墙角空气流动最差污染物容易滞留测出来的数值会偏高。不要放在厨房门口正对油烟机的位置。做饭时的 PM 暴增会掩盖你实际想监测的“日常环境浓度”。不要放在窗户正下方。开窗瞬间会有一波涌流读数会剧烈波动。我自己的经验是放在房间的中心区域距离地面 1.2-1.5 米远离空调出风口和加湿器。这个高度接近于人体呼吸带数据会更贴近“人在这个房间里实际呼吸到的空气”。如果家里有多个房间也没关系Part 2 我会讲怎么做多个节点的对比监测一台一台来。还有一点容易忽略的是加热源的距离。CO2 传感器的工作依赖于热导率测量SCD40 内部有一个红外光源它工作时会微热。如果环境温度变化太快比如旁边开了一台取暖器读数会短暂失真。留出至少 30 厘米距离问题就不大了。4.3 湿度对 PM 传感器的影响与补偿策略这是我在实测中发现最典型的现象雨后或者在浴室、厨房等潮湿环境里PM2.5 读数会明显偏大。原因在于水蒸气在颗粒物表面凝结让颗粒物的粒径“变大”了光学传感器把它误判为更多或更大的颗粒物。SEN55 的固件内部其实已经做了湿度补偿这是它贵的一个重要原因。但还是不能完全依赖它我实测在 90%RH 以上高湿环境下读数仍然存在明显偏高的情况。我的补偿策略很简单在数据展示层做一个“可信度降级”的提示。如果湿度超过 75%在屏幕上把 PM2.5 的数值标成黄色或者加一个“*”标记提示当前数据可能受湿度影响。这种做法本质上是坦诚面对传感器在当前环境下的局限性而不是强行修正成你认为“正确”的数值。4.4 与官方监测数据的对比验证前几天外面空气不错我把设备搬到窗台外面人为制造了“室内外同气”的条件跟当地生态环境局的 PM2.5 实时发布数据做了一次对比时间设备读数 PM2.5官方站点 PM2.5偏差10:0012 µg/m³11 µg/m³110:3015 µg/m³14 µg/m³111:0018 µg/m³19 µg/m³-111:3020 µg/m³21 µg/m³-110 分钟内的平均偏差在 ±2µg/m³ 左右整体趋势完全一致。这个精度对于家庭日常监测来说已经足够。如果你测出来偏差特别大先别怀疑传感器检查一下设备是不是放在了空气滞留区或者传感器进气口是不是被灰尘堵住了。5. 常见问题与实操心得5.1 问题速查表按照我这个方案搭建最可能遇到的是以下四类问题我把排查思路整理成一个速查表现象可能原因排查方法I2C 扫描不到 SEN55SEL 引脚未拉低处于 UART 模式确认 SEL 接 GND重启设备I2C 扫描不到 SCD40供电电压不足测量 VIN 对地电压是否稳定 3.3V传感器数据始终为 0上电初始化顺序不对传感器未就绪增加启动延时或改用手动上电PM2.5 数值异常偏高传感器进气口堵塞或传感器受潮检查传感器表面必要时用无水酒精清洁CO2 数值长期不降房间密闭、通风不足或传感器基准偏移开窗通风后观察 30 分钟若仍异常则手动校准5.2 我踩过的三个比较典型的坑坑一是烧录时传感器不吃电。我用的是面包板供电烧录程序时 ESP32 会断开一部分 GPIO 的输出导致传感器瞬间掉电再上电程序开始运行后传感器还在启动中I2C 通信失败。这个是硬件设计层面的问题不是代码 bug。我的解决方法是给传感器单独加了一个带电容的电源滤波模块确保在 USB 供电波动时也能维持稳定的电压输出。坑二是内存堆栈溢出导致死机。SEN55 官方库在频繁调用readMeasuredValues()时会动态分配内存。如果 Arduino 默认的堆栈设置不够大长时间运行后系统会崩溃重启。我通过 Arduino IDE 的EspExceptionDecoder工具分析了崩溃日志发现是堆栈溢出把loopTaskStackSize从默认的 8192 调整到 16384 后问题解决。这个参数在menuconfig或者Arduino-ESP32的编译选项里都可以改新手容易一头雾水搜“ESP32 loopTaskStackSize 调整”就能找到具体步骤。坑三是让传感器一直通电导致的寿命损耗。空气监测是需要长期运行的设备但让传感器 7x24 小时不间断工作风扇一直转寿命会快速衰减。PMS 系列传感器的风扇设计寿命一般在 3-5 年但如果你家 PM2.5 浓度高风扇负担大寿命更短。我最后加了一个简单的“每小时工作 55 分钟休息 5 分钟”的调度逻辑牺牲了极小的采样连续性换来了更长的维护周期。5.3 对 Part 2 的扩展规划Part 1 到这里一个可靠的本地空气质量监测节点算是完成了。它现在能自证数据可信能实时显示能通过串口输出数据。但单机运行的监测节点只解决了“看到数据”的问题离我最初设想的“改善空气”还差得远。Part 2 我会做几件事第一把数据通过 MQTT 协议上报到本地 Home Assistant实现手机端远程查看同时把历史数据存入 InfluxDB用 Grafana 画趋势图。这样就能回答“过去一周室内 CO2 有没有超标”“下雨天 PM2.5 会不会升高等”这类时间维度的问题。第二做联动控制。通过自动化规则当 CO2 超过 1000ppm 时自动打开新风系统PM2.5 超过 75µg/m³ 时自动启动空气净化器。这才是监测的最终价值所在——让数据驱动设备而不是人肉看屏幕再手动操作。第三如果家里面积大可以做多节点布点把卧室、客厅、厨房分别放一个监测设备汇总到一个总控界面。Part 1 的设备直接复用不需要重新设计硬件只需要在软件层面增加上报功能即可。说实话做完这台设备之后我最大的感受是空气净化器和新风系统不是“买了就是赚了”装在哪、什么时候开、开多久完全依赖监测数据才能做出合理决定。以前我习惯性开着净化器现在看到 PM2.5 在 5µg/m³ 以下就果断关掉既省电又避免滤芯浪费以前从不开窗通风现在一看到 CO2 突破 900ppm 就立刻开窗整个人的精神状态都好了不少。这套系统的价值远不止“看个数字”这么简单。
返回列表