基于ESP32-C3与多传感器的开源便携空气质量监测仪DIY全攻略
1. 项目缘起为什么我们需要一个便携式空气质量监测仪空气质量这个词听起来有点宏大但落到我们每个人的日常生活中其实就是你每天呼吸的那一口空气。我住在城市里平时通勤、周末去公园甚至在家里开窗通风都免不了会想现在的空气到底怎么样PM2.5高不高有没有什么有害气体市面上的专业监测设备要么太贵要么体积庞大不便携要么数据封闭不透明。作为一个喜欢动手折腾的硬件爱好者我萌生了自己做一个的想法。这个项目的核心目标很明确打造一个完全开源、成本可控、便于携带且能实时查看数据的个人空气质量监测仪。我选择了Beetle ESP32C3作为主控因为它集成了Wi-Fi和蓝牙尺寸极小功耗控制得也不错非常适合嵌入式物联网项目。外壳则通过3D打印实现可以自由设计造型兼顾美观和功能性。监测的指标我聚焦在最常见、也最关乎健康的几项PM2.5/PM10颗粒物浓度、温湿度、以及总挥发性有机物TVOC和二氧化碳CO2当量浓度。这样一个小设备可以放在书桌、床头、车内或者随身携带通过手机网页就能随时查看数据甚至还能设置阈值告警。对于硬件爱好者来说这是一个绝佳的练手项目涵盖了单片机开发、传感器应用、3D建模与打印、物联网通信和简单的Web界面开发。对于普通用户它提供了一个透明、可信的数据来源帮助你更科学地管理生活环境。接下来我就把从硬件选型、电路设计、固件开发到外壳制作的完整过程以及我踩过的那些坑毫无保留地分享出来。2. 核心硬件选型与设计思路做一个项目硬件是骨架。选型不是拍脑袋每个元器件的选择背后都有其权衡和理由。我的核心思路是在满足功能、精度和稳定性的前提下尽可能追求小型化、低功耗和易用性。2.1 主控芯片为什么是Beetle ESP32C3主控的选择决定了项目的可能性。市面上常见的ESP32开发板很多比如NodeMCU、Wemos D1 mini等。我最终选择DFRobot的Beetle ESP32C3主要基于以下几点考量极致紧凑的尺寸它的核心板尺寸只有25*20.5mm比一枚硬币还小。这对于追求便携性的设备来说是决定性优势为后续设计小巧的外壳提供了基础。合理的性能与功耗平衡ESP32-C3芯片基于RISC-V架构主频160MHz内存400KB SRAM性能足以流畅运行传感器数据采集、Wi-Fi连接和Web服务。同时它支持深度睡眠模式在电池供电场景下至关重要。丰富的通信接口板载Wi-Fi 4和蓝牙5.0满足数据无线传输的所有需求。特别是Wi-Fi可以让我们直接通过浏览器访问设备无需额外APP。良好的开发环境完美支持Arduino IDE和ESP-IDF社区资源丰富遇到问题容易找到解决方案。内置USB-C接口与充电管理板子直接集成了USB-C口和锂电池充电管理芯片IP5306这意味着你可以直接用一块3.7V的锂电池供电并通过USB口充电极大简化了电源系统设计。注意市面上有一些更便宜的ESP32-C3模组但需要自行焊接天线、设计USB转串口和电源电路对于想快速验证原型的新手来说Beetle ESP32C3这种高度集成的板子是更优选择省去了大量底层硬件调试工作。2.2 传感器阵列如何精准“感知”空气空气质量监测传感器是眼睛和鼻子。我选择了三款主流传感器来覆盖核心指标。2.2.1 颗粒物传感器PMS5003监测PM2.5和PM10我选用的是攀藤科技的PMS5003。这是一款激光散射原理的传感器性价比和可靠性在DIY圈子里有口皆碑。为什么选它相比更便宜的GP2Y1010AU0F灰尘传感器PMS5003直接通过串口输出经过算法处理的PM1.0、PM2.5、PM10的浓度值单位μg/m³数据更准确、稳定无需自己进行复杂的电压-浓度换算和校准。相比更高端的SDS011它体积更小功耗也更低。关键参数与接口工作电压5V采用UARTTX/RX串口通信。只需要连接VCC、GND、TX、RX四根线即可。它的数据帧格式固定解析起来非常方便。实操心得PMS5003内部有风扇启动时会有“嗡”的一声。在代码中通常需要等待30秒左右让传感器稳定再读取数据。它的发热量不小在外壳设计时必须考虑散热孔。2.2.2 气体与温湿度传感器SGP40 SHT40为了监测VOC挥发性有机物来自装修材料、清洁剂等和室内空气质量指数IAQ以及精确的温湿度我选择了Sensirion的SGP40VOC传感器和SHT40温湿度传感器的组合套件。也有将两者集成在一起的模块如SEN55但分开选择更灵活成本也稍低。SGP40这是一款基于MOX金属氧化物技术的数字VOC传感器通过I2C接口通信。它输出的是一个“VOC指数”原始信号需要结合温度和湿度数据这就是为什么需要SHT40通过官方的算法库才能转换为有意义的VOC浓度和CO2当量浓度值。官方提供的libsensirion_gas_index_algorithm库让这个过程变得简单。SHT40Sensirion新一代的温湿度传感器精度高±1.8%RH, ±0.2°C响应快同样采用I2C接口。它的一个重要角色是为SGP40提供精确的环境补偿数据。选型对比为什么不选常见的BME280或DHT22BME280虽然也测温湿度但它的气压功能在此项目中非必需且其VOC/气体检测能力远不如专用的SGP40。DHT22的精度和响应速度则不如SHT40。SGP30是SGP40的前代产品算法和性能略有不及。2.2.3 电源方案锂电池与充放电管理便携意味着必须使用电池。我选择了一块常见的3.7V 1000mAh锂电池。整个系统的电源架构如下充电Beetle ESP32C3板载的IP5306芯片负责通过USB-C口为锂电池充电。这是最省事的方式。升压系统中有需要5V供电的器件PMS5003。因此我需要一个DC-DC升压模块将锂电池的3.7V范围约3.0V-4.2V稳定升压到5V。我选用了一款微型MT3608升压模块其效率高静态功耗低。供电路径锂电池正负极直接连接到Beetle ESP32C3的“BAT”和“BAT-”焊盘。同时锂电池正负极也接入MT3608升压模块的输入端。MT3608输出的5V供给PMS5003的VCC引脚。电平转换注意PMS5003的串口是5V逻辑电平而Beetle ESP32C3的GPIO是3.3V逻辑。虽然很多ESP32引脚耐5V但为稳妥起见我建议在PMS5003的TX连接ESP32的RX线上加一个简单的电平降压电路如两个电阻分压或者使用一款双向逻辑电平转换模块。2.3 电路连接总图将所有部分连接起来下面是详细的接线表元件引脚/接口连接到 Beetle ESP32C3说明锂电池正极BAT供电与充电负极BAT-MT3608升压模块IN锂电池正极为PMS5003提供5V电源IN-锂电池负极OUT (5V)PMS5003 VCCOUT- (GND)PMS5003 GNDPMS5003VCCMT3608 OUT5V供电GNDMT3608 OUT-共地TXGPIO5 (通过电平转换)ESP32接收颗粒物数据RXGPIO4ESP32发送指令可悬空通常不需要SGP40VCC3.3VGNDGNDSDAGPIO6I2C数据线SCLGPIO7I2C时钟线SHT40VCC3.3V可与SGP40共用电源线GNDGNDSDAGPIO6与SGP40并联SCLGPIO7与SGP40并联提示I2C总线SDA, SCL上可以挂载多个设备每个设备有唯一的地址SHT40地址0x44SGP40地址0x59所以它们可以并联在同一组引脚上。务必在SDA和SCL线上各接一个4.7kΩ的上拉电阻到3.3V这是I2C总线稳定工作的关键。3. 固件开发从数据采集到Web服务硬件连接好后就需要赋予它灵魂——程序。我使用Arduino IDE进行开发因为其库生态丰富上手快。3.1 开发环境搭建与库安装首先在Arduino IDE中添加ESP32开发板支持。在“首选项”的“附加开发板管理器网址”中输入https://espressif.github.io/arduino-esp32/package_esp32_index.json然后在开发板管理器中搜索安装“ESP32”由Espressif Systems提供。安装后在开发板中选择“DFRobot Beetle ESP32-C3”。接下来通过库管理器安装必要的库Adafruit SHT4x Library用于驱动SHT40。Sensirion I2C SGP40用于驱动SGP40。Sensirion Gas Index Algorithm这是将SGP40原始信号转换为VOC/CO2指数的核心算法库。PMS Library推荐使用miguel5612开发的PMS库方便解析PMS5003数据。WebServer和WiFiESP32内置库用于创建Web服务器和连接Wi-Fi。ArduinoJson用于生成Web页面显示的JSON数据可选但推荐。3.2 核心代码逻辑剖析程序的主逻辑是一个状态机循环执行数据采集、处理和响应网络请求。3.2.1 初始化与Wi-Fi连接#include WiFi.h #include WebServer.h #include Adafruit_SHT4x.h #include SensirionI2CSgp40.h #include VOCGasIndexAlgorithm.h #include PMS.h // 网络凭证 const char* ssid “你的Wi-Fi名称”; const char* password “你的Wi-Fi密码”; // 传感器对象 Adafruit_SHT4x sht4 Adafruit_SHT4x(); SensirionI2CSgp40 sgp40; VOCGasIndexAlgorithm voc_algorithm; PMS pms(Serial1); // 使用Serial1连接PMS5003 PMS::DATA data; // Web服务器对象 WebServer server(80); void setup() { Serial.begin(115200); Serial1.begin(9600, SERIAL_8N1, 5, 4); // RXGPIO5, TXGPIO4 // 初始化SHT40 if (!sht4.begin()) { Serial.println(“SHT40未找到!”); while (1); } sht4.setPrecision(SHT4X_HIGH_PRECISION); sht4.setHeater(SHT4X_NO_HEATER); // 初始化SGP40 sgp40.begin(Wire); uint16_t serialNumber[3]; sgp40.getSerialNumber(serialNumber); // 可选的序列号读取用于验证连接 // 连接Wi-Fi WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(“.”); } Serial.println(“\nWi-Fi已连接IP地址: ”); Serial.println(WiFi.localIP()); // 设置Web服务器路由 server.on(“/”, handleRoot); // 访问根目录显示主页面 server.on(“/data”, handleData); // 获取JSON格式的传感器数据 server.begin(); }3.2.2 数据采集与处理函数这是项目的核心。我们需要定时例如每10秒读取所有传感器并进行处理。float temperature, humidity; float voc_index, co2_equivalent; uint16_t pm25, pm10; void readSensors() { // 1. 读取SHT40温湿度 sensors_event_t humidity_event, temp_event; sht4.getEvent(humidity_event, temp_event); temperature temp_event.temperature; humidity humidity_event.relative_humidity; // 2. 读取SGP40 VOC原始信号并利用温湿度补偿计算VOC指数和CO2当量 uint16_t sraw_voc; uint16_t default_compensation 0x8000; // 初始补偿 // 将温湿度转换为SGP40要求的格式 uint16_t compensation_ticks (uint16_t)((humidity * 65535) / 100); compensation_ticks 16; compensation_ticks | (uint16_t)((temperature 45) * 65535 / 175); sgp40.measureRawSignal(compensation_ticks, sraw_voc); voc_index voc_algorithm.process(sraw_voc); // CO2当量值是基于VOC指数的一个估算使用算法库中的另一函数 // 注意SGP40本身不直接测CO2这是算法估算值可用于趋势参考 co2_equivalent voc_algorithm.get_co2_equivalent(); // 3. 读取PMS5003颗粒物 if (pms.read(data)) { pm25 data.PM_AE_UG_2_5; // PM2.5浓度 pm10 data.PM_AE_UG_10_0; // PM10浓度 } }3.2.3 Web服务器与前端界面我们创建一个简单的Web服务器当用户用手机浏览器访问设备IP时显示一个实时数据的仪表盘。void handleRoot() { String html “!DOCTYPE htmlhtmlheadmeta charset‘utf-8’”; html “meta name‘viewport’ content‘widthdevice-width, initial-scale1’”; html “title便携空气质量监测仪/title”; html “stylebody {font-family: sans-serif; text-align: center;}/style”; html “script”; html “function fetchData() {“; html “ fetch(‘/data’).then(rr.json()).then(data{“; html “ document.getElementById(‘temp’).innerHTML data.temp ‘ °C’;“; html “ document.getElementById(‘humi’).innerHTML data.humi ‘ %’;“; html “ document.getElementById(‘pm25’).innerHTML data.pm25 ‘ μg/m³’;“; html “ document.getElementById(‘pm10’).innerHTML data.pm10 ‘ μg/m³’;“; html “ document.getElementById(‘voc’).innerHTML data.voc.toFixed(0);“; html “ document.getElementById(‘co2’).innerHTML data.co2.toFixed(0) ‘ ppm’;“; html “ });}”; html “setInterval(fetchData, 5000); // 每5秒更新一次“; html “/script/headbody”; html “h1 实时空气质量/h1”; // 注意实际输出时请移除Emoji html “p温度: span id‘temp’–/span/p”; html “p湿度: span id‘humi’–/span/p”; html “hr”; html “h2颗粒物/h2”; html “pPM2.5: strong id‘pm25’–/strong μg/m³/p”; html “pPM10: strong id‘pm10’–/strong μg/m³/p”; html “hr”; html “h2气体指数/h2”; html “pVOC指数: span id‘voc’–/span/p”; // VOC指数无单位 html “pCO₂当量: span id‘co2’–/span ppm/p”; html “psmall最后更新: span id‘time’–/span/small/p”; html “scriptfetchData(); setInterval((){“; html “document.getElementById(‘time’).innerHTMLnew Date().toLocaleTimeString();},1000);/script”; html “/body/html”; server.send(200, “text/html”, html); } void handleData() { readSensors(); // 处理前先读取一次最新数据 String json “{“; json “\”temp\”:” String(temperature, 1) “,”; json “\”humi\”:” String(humidity, 1) “,”; json “\”pm25\”:” String(pm25) “,”; json “\”pm10\”:” String(pm10) “,”; json “\”voc\”:” String(voc_index) “,”; json “\”co2\”:” String(co2_equivalent); json “}”; server.send(200, “application/json”, json); } void loop() { server.handleClient(); // 处理客户端请求 // 可以在这里添加低功耗睡眠逻辑例如每10秒唤醒一次进行测量 // delay(10000); }3.3 功耗优化与深度睡眠对于便携设备功耗至关重要。ESP32-C3的深度睡眠模式可以大幅降低待机电流。基本思路设备每5分钟唤醒一次读取所有传感器数据然后开启Wi-Fi将数据上传到某个物联网平台如ThingsBoard、Blynk或自建服务器上传完成后再次进入深度睡眠。但这样我们就无法实时通过Web访问了。折中方案本项目采用设备持续运行但通过代码优化降低功耗。例如在loop()中如果没有网络请求让ESP32-C3进入light sleep模式。降低CPU频率。关闭不用的外设如蓝牙。为PMS5003设计一个由GPIO控制的电源开关只在需要测量时通电因其功耗较大。一个简单的light sleep示例#include “driver/rtc_io.h” void enterLightSleep(int seconds) { esp_sleep_enable_timer_wakeup(seconds * 1000000); // 微秒 esp_light_sleep_start(); } void loop() { if (server.hasClient()) { // 有客户端连接正常处理 server.handleClient(); } else { // 无请求时进入轻度睡眠1秒 enterLightSleep(1); } // 每10秒主动读取一次传感器数据更新全局变量 static unsigned long lastRead 0; if (millis() - lastRead 10000) { readSensors(); lastRead millis(); } }4. 3D建模与打印打造专属外壳硬件和软件都跑通了接下来就是给它一个“家”。一个好的外壳不仅要美观更要考虑散热、天线信号、按键/接口开口、安装柱位等工程细节。4.1 设计考量与工具选择我使用Fusion 360进行建模它对个人用户免费功能强大且学习资源丰富。设计时重点考虑以下几点紧凑性所有元器件Beetle ESP32C3、电池、升压模块、传感器需要合理布局尽量减少整体体积。我采用了上下盖的卡扣式设计。散热PMS5003和ESP32芯片在工作时都会发热。必须在对应位置设计足够的通风孔。特别是PMS5003的进气口和出气口不能被遮挡。信号Wi-Fi天线区域通常在Beetle板子的特定一侧应尽量避免被金属或大面积塑料遮挡外壳此处可做薄或开窗。可维护性采用卡扣或螺丝固定方便拆装更换电池或维修。我设计了四个自攻螺丝柱。传感器开口为PMS5003的激光腔开一个进气窗为SGP40和SHT40开一些小孔让空气流通。接口开口预留USB-C口和PMS5003的串口可选用于调试的开孔。打印友好性避免大面积的悬空结构需要加支撑的地方要考虑到。设计合适的脱模斜度。4.2 关键结构设计步骤导入参考模型可以从DFRobot官网或其他社区找到Beetle ESP32C3、PMS5003、电池的STEP或STL文件导入Fusion 360作为参考确保开孔和安装位置精准。创建底板根据所有元器件的平面尺寸绘制一个矩形底板。在对应位置创建安装柱。安装柱中间需要留出螺丝孔如M2自攻螺丝柱子高度要略高于元器件本身。设计侧壁与卡扣向上拉伸侧壁。在侧壁内侧设计卡扣钩在上盖对应位置设计卡扣槽。卡扣需要一定的弹性通常设计0.3-0.5mm的干涉量。开孔与通风设计使用“偏移平面”工具在PMS5003进气口对应位置创建一个工作面然后用“网格”工具绘制蜂窝状或栅格状的通风孔并拉伸切割。在顶盖上方为SGP40/SHT40开一些小圆孔阵列。在侧面或底面为USB-C口开一个矩形槽。创建上盖设计一个与下盖匹配的上盖同样包含卡扣结构、通风孔和必要的加强筋。导出为STL分别将上盖和下盖导出为STL文件准备进行切片。4.3 切片与打印实战我使用PrusaSlicer进行切片打印机是Creality Ender-3。材料PLA因为它易于打印强度足够且没有异味。避免使用ABS因为其打印过程有气味且可能干扰气体传感器。层高0.2mm在打印速度和表面光洁度间取得平衡。填充密度15%-20%。外壳不需要太高的填充节省材料和时间。支撑对于卡扣的悬空部分和通风孔内部的桥接需要生成支撑。我选择“仅在接触构建板处生成支撑”以减少后期处理。打印方向将下盖的底部安装柱朝上平放在打印床上这样安装柱的顶部需要支撑但整体强度最好。上盖则内侧朝下放置。实操心得第一层附着是关键确保热床平整喷头高度合适第一层线宽均匀贴合。通风孔可能堵住如果通风孔的直径小于喷嘴直径通常0.4mm切片软件可能会忽略它。建议通风孔设计为至少1mm宽。卡扣的干涉量需要测试第一次打印可以只打印一个带有卡扣的测试块验证松紧度再调整模型。太紧可能掰断太松会脱落。打印后处理小心移除支撑用锉刀或砂纸打磨毛刺特别是USB-C口和传感器开口处确保没有塑料碎屑阻碍插拔或空气流通。5. 组装、校准与最终测试所有部件准备就绪进入最后的组装调试阶段。5.1 焊接与内部组装焊接连接线根据之前的接线表将杜邦线或硅胶导线焊接到Beetle ESP32C3、传感器和升压模块上。强烈建议给电源线VCC、GND使用不同颜色如红、黑避免接反。I2C和串口线也最好用颜色区分。固定元器件使用M2*6mm的自攻螺丝将Beetle ESP32C3、升压模块固定在下壳的安装柱上。PMS5003可以用螺丝固定也可以用强力双面胶粘贴。电池可以放在空余位置用尼龙扎带或泡棉胶固定。理线与测试将所有线材整理好用扎带固定避免杂乱。在合上外壳前先上电测试通过串口监视器查看传感器数据是否正常输出手机能否连接到设备IP并显示网页。5.2 传感器校准与数据解读PMS5003这款传感器出厂已校准通常无需用户校准。但要注意它测量的是“空气动力学当量直径”的颗粒物浓度。在极端干燥或潮湿环境下读数可能会有轻微偏差。确保进气口通畅远离风扇、空调直吹。SHT40精度很高一般也无需校准。首次读数如果与环境温湿度计差异很大检查是否接触不良或代码中精度设置是否正确。SGP40这是校准的重点。VOC传感器需要一段时间的“磨合”和“背景校准”。首次运行将设备放在通风良好的室外或已知空气良好的环境中通电运行至少24小时。这个过程让传感器基线稳定下来。背景校准Sensirion的算法库本身具备自动背景校准功能它会在设备长时间运行时自动学习环境背景VOC水平并以此为基础计算VOC指数。因此让设备持续通电运行是获得准确VOC趋势的关键。短时间开关机其指数可能不准确。数据解读VOC指数是一个无量纲的值通常范围在1-500。值越低代表空气质量越好。CO2当量值是基于VOC和温湿度估算的单位为ppm。它可以作为室内二氧化碳浓度趋势的一个参考但不能替代真正的NDIR CO2传感器进行精确测量。5.3 功能扩展与优化想法基础版本完成后你可以根据兴趣进行扩展数据上传与可视化将数据定期上传到ThingsBoard、Blynk或Home Assistant制作历史数据图表和仪表盘。阈值告警在固件中加入逻辑当PM2.5超过某个值如75μg/m³或VOC指数过高时让板载的RGB LED闪烁红灯或在Web页面上弹出警告。低功耗优化进阶使用硬件定时器RTC唤醒实现真正的深度睡眠使待机电流降至100μA以下极大延长电池续航。增加显示屏使用一块小型OLED屏幕如0.96寸直接显示实时数据脱离手机也能查看。外壳美化对3D模型进行倒角、添加纹理或者使用多色打印让设备更具个性。这个项目从构思到实现花费了我几个周末的时间期间遇到了电源干扰、I2C地址冲突、Web服务器内存泄漏等问题但逐一解决后的成就感是无与伦比的。现在这个我自己制作的小设备就放在我的电脑旁默默守护着我呼吸的每一口空气。它给出的数据让我对室内环境有了更直观的认识也促使我养成了定时开窗通风的习惯。如果你也对硬件DIY和物联网感兴趣不妨动手试一试这个过程本身就是最好的学习。