ARTICLE DETAIL

资讯详情

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

基于Arduino的智能空气质量检测仪:PM2.5实时监测与通风建议

基于Arduino的智能空气质量检测仪:PM2.5实时监测与通风建议 去年冬天家里老人一直纠结一个问题窗外灰蒙蒙的到底能不能开窗开了怕脏空气进来不开又觉得闷。商场里几百块的空气质量检测仪不少但要么就显示一个数字要么只亮个灯给不出明确结论。后来我干脆自己动手做了一个智能空气质量仪用 Arduino 做主控配一颗激光 PM2.5 传感器再加一块 OLED 屏程序里写好空气质量等级划分和基于浓度、温湿度的通风建议逻辑。设备一上电就能看到当前 PM2.5 浓度、对应等级以及一句直接可执行的开窗建议比如“建议通风10分钟”或者“不建议开窗”。这个项目适合刚入门单片机的朋友也适合自己家里做环境监测的玩家成本不高代码逻辑也不复杂但做完之后真的能解决一个具体的生活问题。1. 项目整体设计与技术选型思路1.1 核心需求拆解项目标题拆开来看其实是三件事检测 PM2.5 浓度按等级显示空气质量给出开窗通风建议。第一件事靠传感器硬件第二件事靠程序里的等级映射第三件事是一套简单的决策规则。实际做下来第三件事才是整个项目里最有意思的部分因为“能不能开窗”不只是看 PM2.5 一个参数还牵扯到温度、湿度、污染趋势、甚至当前时间段。围绕这三个需求我在设计时把系统分成四块传感器采集模块、数据处理模块、显示模块、建议生成模块。每一块都尽量做到独立后面想改哪一块都不至于推倒重来。比如传感器从 PMS5003 换成 SDS011只需要改数据解析部分的代码显示从 OLED 换成 LCD也只需要替换显示函数。1.2 硬件选型取舍为什么用激光传感器PM2.5 传感器的方案最常见的有两种一种红外散射一种激光散射。红外散射传感器很便宜十几二十块钱就能买到但测量精度较差对微小颗粒的响应也不够灵敏而且输出大多是模拟电压还得自己标定。激光散射传感器比如我用的攀藤 PMS5003内部有激光二极管和光电探测器配合恒流风扇让空气稳定流过测出来的数据稳定性好很多而且直接通过串口输出数字帧里面同时包含 PM1.0、PM2.5、PM10 三种粒径的浓度用起来非常方便。有些朋友可能会问既然有 PMS7003 这种更小更新的型号为什么选 PMS5003其实两个都可以PMS7003 体积更小、功耗更低价格略高一点PMS5003 则更皮实、资料更多、网上能搜到的例程一大把。对第一个 DIY 项目来说资料多就是最大的优势。我手头正好有 PMS5003就直接拿它做了。如果你要装进很小的外壳里选 PMS7003 也没问题代码几乎不用改数据帧解析保持一致就行。1.3 系统工作流程从数据到建议的全链路整个程序的核心流程可以概括为上电初始化→读取传感器数据帧→校验→提取 PM2.5 值→滑动平均滤波→根据阈值映射等级→结合温湿度生成通风建议→刷新 OLED 显示→循环。在 Arduino 上这个循环跑得很快但显示刷新没必要太频繁我设置成每 2 秒刷新一次 OLED数据采集则每秒钟做一次然后放到一个长度为 10 的缓冲区里做滑动平均用平均值作为最终显示和判断的输入。这样做的目的很简单传感器单次读数波动大直接显示会让人以为机器坏了滤波之后数值平滑得多也更容易看出趋势。为什么用滑窗长度 10如果窗口太短比如取 3 次平均数据还是跳动明显如果太长比如取 30 次数据响应很迟钝室内开窗后 PM2.5 骤降可能等了半分钟屏幕才反应过来。10 次差不多是稳定性和响应速度的折中这个参数后面还可以随时调整。2. 硬件搭建与传感器数据帧解析2.1 激光散射原理传感器是怎么算浓度的PMS5003 内部有一个微型气泵或者风扇把空气抽进测量腔体激光二极管发射一束激光照在气流上。空气中的颗粒物经过激光束时会发生散射散射光的强度与颗粒物浓度有一定的对应关系。光电探测器把散射光信号转成电信号再由内部的 MCU 经过算法换算成质量浓度单位是微克每立方米。这个过程还涉及到一个概念叫“CF1”和“大气环境”数据帧里会同时给出这两组数值。CF1 是传感器在特定标准粒子下的校准结果更适合做对比测试大气环境值则是在真实环境下的等效浓度。做日常显示用“大气环境”那组数据更符合实际感知。这一点在解析数据帧时要注意别取错字段。2.2 引脚连接与供电注意事项PMS5003 的接口很简单一共 8 个引脚但真正用到的就 4 个VCC、GND、TXD、RXD。VCC 接 5VGND 接 GNDTXD 接 Arduino 的接收引脚RXD 接 Arduino 的发送引脚。如果你想用硬串口就把 TXD 接 Arduino 的 RX0 号引脚、RXD 接 TX1 号引脚如果想用软件串口就接到任意两个数字引脚上比如 10 和 11。供电是很多新手踩坑的地方。PMS5003 工作时电流在 100mA 左右峰值可能更高如果直接从一个 USB 转串口模块或者 Arduino 的 3.3V 引脚取电很容易出现电压跌落表现就是传感器连接不稳定、数据时好时坏。我一般直接给 Arduino 供 5V然后从 5V 引脚给传感器供电如果 USB 口供电质量不好就换一个输出电流 1A 以上的电源适配器。长时间运行一定要注意电源的余量。接线表我整理一下PMS5003 VCC - Arduino 5VPMS5003 GND - Arduino GNDPMS5003 TXD - Arduino D10软串口 RXPMS5003 RXD - Arduino D11软串口 TXOLED VCC - Arduino 3.3V / 5V看模块OLED SDA - Arduino A4OLED SCL - Arduino A5如果用的是 ESP32IO 口电平是 3.3V但 PMS5003 的 TXD 输出也是 3.3V 电平所以可以直接对接VCC 接 5V 倒也没问题但要注意共地。2.3 数据帧结构与解析代码PMS5003 每次输出固定 32 字节帧头是两个固定字节 0x42、0x4D对应 ASCII 字符的 BM。第 3、4 字节是帧长度0x00 0x1C 就是 28表示后面数据有 28 字节。从第 5 字节开始是各项数据每项占 2 字节高位在前。关键字段第 5、6 字节PM1.0 浓度CF1第 7、8 字节PM2.5 浓度CF1第 9、10 字节PM10 浓度CF1第 11、12 字节PM1.0 浓度大气环境第 13、14 字节PM2.5 浓度大气环境第 15、16 字节PM10 浓度大气环境帧末尾两字节是校验和前面所有字节相加取低 16 位应该等于校验和。校验能帮你过滤掉串口传输过程中的错帧。我建议用状态机方式解析循环读取串口字节如果读到 0x42紧接着读下一个字节如果是 0x4D就认为帧头对齐了再继续读后面的 30 字节。这种写法比一上来就 readBytes 要稳因为串口数据可能从帧中间开始必须通过帧头重新对齐。解析代码示例#include SoftwareSerial.h SoftwareSerial pmsSerial(10, 11); uint8_t buf[32]; uint16_t pm25_aqi 0; bool readPMSData(uint16_t *pm25) { int idx 0; int startTime millis(); while (millis() - startTime 500) { if (pmsSerial.available()) { uint8_t c pmsSerial.read(); if (idx 0 c ! 0x42) continue; if (idx 1 c ! 0x4D) { idx 0; continue; } buf[idx] c; if (idx 32) break; } } if (idx 32) return false; uint16_t sum 0; for (int i 0; i 30; i) sum buf[i]; uint16_t check (buf[30] 8) | buf[31]; if ((sum 0xFFFF) ! check) return false; *pm25 (buf[13] 8) | buf[14]; return true; }这段代码的关键点是先找帧头再完整接收 32 字节最后做校验和校验通过才把大气环境下的 PM2.5 浓度保存下来。如果校验不过直接丢弃这一帧不参与后续计算。3. 程序核心实现滤波、等级判定与通风建议3.1 为什么必须做滤波处理PMS5003 的原始读数在短时间内波动其实挺大的静止状态下可能会在 35 到 55 之间来回跳这并不意味着空气质量在剧烈变化而是颗粒物在空气中本来就有随机性加上传感器自身也有噪声。如果直接把原始值丢给屏幕看起来就是数字不停跳动体验很差。我用滑动平均来解决维护一个长度为 10 的环形缓冲区每次读到新的有效 PM2.5 值就存进去然后计算缓冲区里所有值的平均值。这个平均值才是后面一切判断的输入。和简单算术平均相比滑动平均的好处是不需要缓存所有历史数据内存固定实时性好每次来了新数据就能立刻算出一个新值。如果你希望抗突发毛刺更狠一点可以在滑动平均之前再加一层中值滤波取 5 个值排序后取中间值再去平均。不过实测下来对家用场景滑动平均已经足够中值滤波会多引入一点计算量收益有限。3.2 空气质量等级判定阈值和分级逻辑分级我参考的是日常环境中比较常用的空气质量分级标准把 PM2.5 浓度分成六个等级优、良、轻度污染、中度污染、重度污染、严重污染。具体阈值如下等级PM2.5浓度范围μg/m³显示状态优0~35绿良35~75黄轻度污染75~115橙中度污染115~150红重度污染150~250紫严重污染250褐红程序里用一个函数做映射输入滤波后的 PM2.5 值返回一个枚举类型对应不同的等级。后面显示、建议、甚至 LED 灯色都是根据这个枚举来。这样做的好处是逻辑分层清晰以后想改阈值只需要改这一个函数。3.3 开窗通风建议规则设计不只是看 PM2.5这部分是项目里最值得琢磨的地方。单纯根据室内 PM2.5 等级给建议其实不够聪明因为要不要开窗还要考虑温湿度、污染趋势、甚至时段。我设计的规则分成三层第一层基础规则。根据当前室内 PM2.5 等级给一个初步结论。优或良默认建议正常通风轻度污染建议开窗短时间通风中度及以上建议避免开窗优先使用空气净化器。第二层温湿度修正。如果温度低于 5℃ 或者高于 35℃即使等级是良也建议缩短开窗时间比如从 15 分钟缩短到 5 分钟湿度大于 80% 时不建议开窗因为高湿环境下开窗容易让室内霉菌滋生尤其南方回南天的时候这个修正很实用。第三层污染趋势修正。如果 PM2.5 浓度在短时间内快速上升比如 5 分钟内涨幅超过 20%说明室内正在产生污染源做饭、烧香、抽烟或者室外污染正在渗入这时候就要小心。如果是做饭导致的建议关闭门窗等油烟消散后再通风如果是室外污染输入开窗反而会让情况恶化更不建议开窗。这个规则我直接做成一个函数输入是滤波后的 PM2.5、温度、湿度、最近几分钟的趋势斜率输出是一句中文建议字符串。不同等级的基础建议文本我整理在下面的表格里可以直接抄输入条件通风建议文本PM2.5等级优/良温度适宜空气质量优良建议开窗通风15分钟PM2.5等级优/良温度过低或过高当前温湿度不适宜长时间开窗建议通风5分钟PM2.5等级轻度污染轻度污染可开窗通风5~10分钟PM2.5等级中度污染及以上不建议开窗建议使用空气净化器湿度80%湿度过高不建议开窗防止室内返潮浓度快速上升PM2.5正在上升检查污染源后再决定是否通风这个建议逻辑不是很复杂的 AI 算法但实测下来非常接地气用户看到屏幕上直接出现一句话比看数字直观多了。3.4 OLED 显示与完整主程序显示部分我用了一块 0.96 寸的 SSD1306 OLED128x64 像素I2C 接口。OLED 上分三行显示第一行显示 PM2.5 浓度值第二行显示空气质量等级第三行显示通风建议。等级文本我顺便用不同颜色标注SSD1306 单色屏没法变颜色就通过增加一个 RGB LED 来做颜色指示绿黄橙红对应不同等级这样用户扫一眼灯色就知道大概情况。主程序结构就是前面说的流程代码如下去掉了部分细节#include Wire.h #include Adafruit_SSD1306.h #include SoftwareSerial.h #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_ADDR 0x3C SoftwareSerial pmsSerial(10, 11); Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, -1); float pm2History[10]; int historyIdx 0; int historyCount 0; enum AirLevel { LEVEL_GOOD, // 优 LEVEL_MODERATE, // 良 LEVEL_LIGHT, // 轻度污染 LEVEL_MIDDLE, // 中度污染 LEVEL_HEAVY, // 重度污染 LEVEL_SEVERE // 严重污染 }; AirLevel getLevel(float pm25) { if (pm25 35) return LEVEL_GOOD; if (pm25 75) return LEVEL_MODERATE; if (pm25 115) return LEVEL_LIGHT; if (pm25 150) return LEVEL_MIDDLE; if (pm25 250) return LEVEL_HEAVY; return LEVEL_SEVERE; } const char* levelText(AirLevel level) { switch (level) { case LEVEL_GOOD: return 优; case LEVEL_MODERATE: return 良; case LEVEL_LIGHT: return 轻度污染; case LEVEL_MIDDLE: return 中度污染; case LEVEL_HEAVY: return 重度污染; case LEVEL_SEVERE: return 严重污染; } } const char* ventilationAdvice(AirLevel level, float temp, float humi, float trend) { if (humi 80) return 湿度高,暂不开窗; if (level LEVEL_MIDDLE || level LEVEL_HEAVY || level LEVEL_SEVERE) { return 不建议开窗,用净化器; } if (trend 20) return PM2.5上升,查污染源; if (level LEVEL_LIGHT) return 轻度,开窗5-10分钟; if (temp 5 || temp 35) return 温湿度不适,通风5分钟; return 优良,建议通风15分钟; } void setup() { Serial.begin(9600); pmsSerial.begin(9600); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { Serial.println(OLED init failed); } display.clearDisplay(); } void loop() { uint16_t pm25Raw 0; if (readPMSData(pm25Raw)) { pm2History[historyIdx] pm25Raw; historyIdx (historyIdx 1) % 10; if (historyCount 10) historyCount; float sum 0; for (int i 0; i historyCount; i) sum pm2History[i]; float pm25Avg sum / historyCount; AirLevel level getLevel(pm25Avg); float trend 0; if (historyCount 5) { int n historyCount 10 ? historyCount : 10; float first pm2History[(historyIdx - n 10) % 10]; float last pm2History[(historyIdx - 1 10) % 10]; trend (last - first); } // 这里温湿度先用固定值演示实际可以接 DHT22 const char* advice ventilationAdvice(level, 25.0, 50.0, trend); display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.print(PM2.5: ); display.print(pm25Avg, 1); display.print( ug/m3); display.setCursor(0, 16); display.print(Level: ); display.print(levelText(level)); display.setCursor(0, 32); display.print(advice); display.display(); } delay(1000); }代码里的 readPMSData 就是前面写的解析函数。温湿度部分我暂时用固定值演示如果你接了 DHT22把实际温度湿度传进去就行。趋势计算我用的是环形缓冲区里当前最新值和最旧值的差值作为这个周期内的变化量这里其实是一次演示逻辑你可以根据自己的采样间隔调整。4. 常见问题与排查技巧实录4.1 串口读不到数据或数据乱码这个问题我遇到得最多。先看几个点接线是否共地供电是否稳定传感器有没有独立供电软串口引脚有没有选对Arduino Uno 上软串口尽量不要用 0、1 脚波特率是不是 9600PMS5003 默认就是这个速率。还有一种情况是传感器需要预热刚上电的前 20 秒数据可能会偏大或者读到异常帧程序里做一下“跳过启动前 30 秒数据”的处理就顺滑很多。如果还是读不到用示波器或者逻辑分析仪看 TXD 引脚有没有波形是最直接的办法没有波形说明传感器没启动或者供电有问题。4.2 PM2.5 数值跳变剧烈如果传感器读数从 30 一下跳到 200然后又掉回来大概率不是程序问题而是传感器附近有局部污染源或者气流扰动比如有人在旁边抽烟、炒菜、或者开窗瞬间有风灌进来。先确认传感器的摆放位置不要放在空调风口、窗户缝隙、厨房排气扇边上。还有一种情况是传感器内部镜片脏了用久了需要清洁进风口特别是家里灰尘大的环境下这个容易被忽略。清洁的时候用棉签蘸少量酒精轻轻擦拭进风口不要拆开传感器内部等酒精挥发了再上电。4.3 通风建议不准确的排查思路通风建议输出了但用户觉得不靠谱最常见的两个原因一是建议只用了室内 PM2.5没有室外的数据二是没有考虑污染物来源。如果室外 PM2.5 本来就很高室内等级是良你让他开窗结果越开越差。解决办法有两种。第一种是给系统加一个室外传感器放在窗外或者阳台程序把室内外浓度对比之后再决定要不要开窗。第二种是联网获取室外空气质量数据用 ESP32 联网拉接口数据这个方案成本很低但依赖网络。我个人更推荐第一种因为空气质量现场测量更可靠而且不依赖外部服务。以后扩展的时候可以把“室内外浓度差”作为一个判断参数比如室外 PM2.5 比室内低 20 以上才建议开窗。4.4 OLED 和传感器同时工作不稳定OLED 闪烁、数据卡顿多半是电源纹波问题。传感器电机启动瞬间电流变化会让 USB 供电的 5V 电压出现波动OLED 屏幕跟着闪。解决办法是传感器电源和 OLED 电源分开一点或者加一个 100uF 和 0.1uF 的电容在传感器电源旁边做去耦。如果用的是 ESP323.3V 供电要注意总电流别让 OLED 和传感器都挤在同一个 LDO 上。还有一个容易忽略的小问题OLED 的 I2C 地址不一定是 0x3C有些模块是 0x3D。如果屏幕不亮可以先跑一个 I2C 扫描程序把地址确认一下再写死到代码里。5. 扩展方向从一个监测仪到家庭环境系统做完这个基础版本之后可以扩展的方向挺多。硬件上加 DHT22 温湿度传感器就能把温度湿度一起纳入通风建议代码改动很小。加 CCS811 或者 SGP30可以同时看 CO2 浓度判断室内空气闷不闷PM2.5 只是空气质量的其中一个指标CO2 对“要不要开窗”的影响其实更直接。如果换用 ESP32就能连上家里的 Wi-Fi数据定时上传到本地 MQTT配合 Home Assistant 可以做自动化比如检测到室内 PM2.5 超标且室外浓度低于室内就自动打开窗户电机检测到 CO2 超标就自动开启新风系统。这些听起来高大上但逻辑底子还是我在这个项目里搭的那套阈值判断和趋势判断。另外一个很重要的点是校准。PMS5003 这类传感器出厂时做过标定但长期使用后或者不同个体之间会有偏差。有条件的话可以和附近公开的空气质量站点数据做一个对比如果发现系统读数偏高或者偏低可以在程序里加一个偏移修正参数。比如实测下来我的设备比站点数据高 10%就在滤波之后乘以 0.9 再显示。这个修正系数设置好之后设备读数和体感就更接近了。最后再分享一个我个人的小经验传感器放置位置真的非常影响数据质量。我之前把设备放在电视柜上旁边正好有个加湿器结果湿度一大PM2.5 读数就飙到 100 多以为是传感器坏了后来才发现是加湿器的超声波水雾被传感器当成颗粒物统计进去了。把加湿器挪开后数据马上恢复正常。所以安装设备时尽量远离加湿器、香薰、油烟口离地高度建议 1.2 米到 1.5 米这样采样到的空气更有代表性。项目做完之后我把它放在客厅靠近阳台的位置每天屏幕上显示一句通风建议家里人照着做开窗这件事终于不用再靠猜了。
返回列表