基于ESP32-C3与多传感器的开源便携空气质量监测仪DIY全攻略
2026/8/20 2:20:31 网站建设 项目流程

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,主要基于以下几点考量:

  1. 极致紧凑的尺寸:它的核心板尺寸只有25*20.5mm,比一枚硬币还小。这对于追求便携性的设备来说是决定性优势,为后续设计小巧的外壳提供了基础。
  2. 合理的性能与功耗平衡:ESP32-C3芯片基于RISC-V架构,主频160MHz,内存400KB SRAM,性能足以流畅运行传感器数据采集、Wi-Fi连接和Web服务。同时,它支持深度睡眠模式,在电池供电场景下至关重要。
  3. 丰富的通信接口:板载Wi-Fi 4和蓝牙5.0,满足数据无线传输的所有需求。特别是Wi-Fi,可以让我们直接通过浏览器访问设备,无需额外APP。
  4. 良好的开发环境:完美支持Arduino IDE和ESP-IDF,社区资源丰富,遇到问题容易找到解决方案。
  5. 内置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,采用UART(TX/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库让这个过程变得简单。
  • SHT40:Sensirion新一代的温湿度传感器,精度高(±1.8%RH, ±0.2°C),响应快,同样采用I2C接口。它的一个重要角色是为SGP40提供精确的环境补偿数据。
  • 选型对比:为什么不选常见的BME280或DHT22?BME280虽然也测温湿度,但它的气压功能在此项目中非必需,且其VOC/气体检测能力远不如专用的SGP40。DHT22的精度和响应速度则不如SHT40。SGP30是SGP40的前代产品,算法和性能略有不及。

2.2.3 电源方案:锂电池与充放电管理

便携意味着必须使用电池。我选择了一块常见的3.7V 1000mAh锂电池。整个系统的电源架构如下:

  1. 充电:Beetle ESP32C3板载的IP5306芯片负责通过USB-C口为锂电池充电。这是最省事的方式。
  2. 升压:系统中有需要5V供电的器件(PMS5003)。因此,我需要一个DC-DC升压模块,将锂电池的3.7V(范围约3.0V-4.2V)稳定升压到5V。我选用了一款微型MT3608升压模块,其效率高,静态功耗低。
  3. 供电路径:锂电池正负极直接连接到Beetle ESP32C3的“BAT+”和“BAT-”焊盘。同时,锂电池正负极也接入MT3608升压模块的输入端。MT3608输出的5V,供给PMS5003的VCC引脚。
  4. 电平转换注意: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 VCC
OUT- (GND)PMS5003 GND
PMS5003VCCMT3608 OUT+5V供电
GNDMT3608 OUT-共地
TXGPIO5 (通过电平转换)ESP32接收颗粒物数据
RXGPIO4ESP32发送指令(可悬空,通常不需要)
SGP40VCC3.3V
GNDGND
SDAGPIO6I2C数据线
SCLGPIO7I2C时钟线
SHT40VCC3.3V可与SGP40共用电源线
GNDGND
SDAGPIO6与SGP40并联
SCLGPIO7与SGP40并联

提示:I2C总线(SDA, SCL)上可以挂载多个设备,每个设备有唯一的地址(SHT40地址0x44,SGP40地址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数据。
  • WebServerWiFi:ESP32内置库,用于创建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); // RX=GPIO5, TX=GPIO4 // 初始化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 html><html><head><meta charset=‘utf-8’>”; html += “<meta name=‘viewport’ content=‘width=device-width, initial-scale=1’>”; html += “<title>便携空气质量监测仪</title>”; html += “<style>body {font-family: sans-serif; text-align: center;}</style>”; html += “<script>”; html += “function fetchData() {“; html += “ fetch(‘/data’).then(r=>r.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></head><body>”; html += “<h1>🌍 实时空气质量</h1>”; // 注意:实际输出时请移除Emoji html += “<p>温度: <span id=‘temp’>–</span></p>”; html += “<p>湿度: <span id=‘humi’>–</span></p>”; html += “<hr>”; html += “<h2>颗粒物</h2>”; html += “<p>PM2.5: <strong id=‘pm25’>–</strong> μg/m³</p>”; html += “<p>PM10: <strong id=‘pm10’>–</strong> μg/m³</p>”; html += “<hr>”; html += “<h2>气体指数</h2>”; html += “<p>VOC指数: <span id=‘voc’>–</span></p>”; // VOC指数无单位 html += “<p>CO₂当量: <span id=‘co2’>–</span> ppm</p>”; html += “<p><small>最后更新: <span id=‘time’>–</span></small></p>”; html += “<script>fetchData(); setInterval(()=>{“; html += “document.getElementById(‘time’).innerHTML=new 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进行建模,它对个人用户免费,功能强大且学习资源丰富。设计时重点考虑以下几点:

  1. 紧凑性:所有元器件(Beetle ESP32C3、电池、升压模块、传感器)需要合理布局,尽量减少整体体积。我采用了上下盖的卡扣式设计。
  2. 散热:PMS5003和ESP32芯片在工作时都会发热。必须在对应位置设计足够的通风孔。特别是PMS5003的进气口和出气口不能被遮挡。
  3. 信号:Wi-Fi天线区域(通常在Beetle板子的特定一侧)应尽量避免被金属或大面积塑料遮挡,外壳此处可做薄或开窗。
  4. 可维护性:采用卡扣或螺丝固定,方便拆装更换电池或维修。我设计了四个自攻螺丝柱。
  5. 传感器开口:为PMS5003的激光腔开一个进气窗;为SGP40和SHT40开一些小孔让空气流通。
  6. 接口开口:预留USB-C口和PMS5003的串口(可选,用于调试)的开孔。
  7. 打印友好性:避免大面积的悬空结构,需要加支撑的地方要考虑到。设计合适的脱模斜度。

4.2 关键结构设计步骤

  1. 导入参考模型:可以从DFRobot官网或其他社区找到Beetle ESP32C3、PMS5003、电池的STEP或STL文件,导入Fusion 360作为参考,确保开孔和安装位置精准。
  2. 创建底板:根据所有元器件的平面尺寸,绘制一个矩形底板。在对应位置创建安装柱。安装柱中间需要留出螺丝孔(如M2自攻螺丝),柱子高度要略高于元器件本身。
  3. 设计侧壁与卡扣:向上拉伸侧壁。在侧壁内侧设计卡扣钩,在上盖对应位置设计卡扣槽。卡扣需要一定的弹性,通常设计0.3-0.5mm的干涉量。
  4. 开孔与通风设计
    • 使用“偏移平面”工具在PMS5003进气口对应位置创建一个工作面,然后用“网格”工具绘制蜂窝状或栅格状的通风孔并拉伸切割。
    • 在顶盖上方为SGP40/SHT40开一些小圆孔阵列。
    • 在侧面或底面为USB-C口开一个矩形槽。
  5. 创建上盖:设计一个与下盖匹配的上盖,同样包含卡扣结构、通风孔和必要的加强筋。
  6. 导出为STL:分别将上盖和下盖导出为STL文件,准备进行切片。

4.3 切片与打印实战

我使用PrusaSlicer进行切片,打印机是Creality Ender-3。

  • 材料PLA,因为它易于打印,强度足够,且没有异味。避免使用ABS,因为其打印过程有气味,且可能干扰气体传感器。
  • 层高:0.2mm,在打印速度和表面光洁度间取得平衡。
  • 填充密度:15%-20%。外壳不需要太高的填充,节省材料和时间。
  • 支撑:对于卡扣的悬空部分和通风孔内部的桥接,需要生成支撑。我选择“仅在接触构建板处生成支撑”,以减少后期处理。
  • 打印方向:将下盖的底部(安装柱朝上)平放在打印床上,这样安装柱的顶部需要支撑,但整体强度最好。上盖则内侧朝下放置。
  • 实操心得
    • 第一层附着是关键:确保热床平整,喷头高度合适,第一层线宽均匀贴合。
    • 通风孔可能堵住:如果通风孔的直径小于喷嘴直径(通常0.4mm),切片软件可能会忽略它。建议通风孔设计为至少1mm宽。
    • 卡扣的干涉量需要测试:第一次打印可以只打印一个带有卡扣的测试块,验证松紧度,再调整模型。太紧可能掰断,太松会脱落。
    • 打印后处理:小心移除支撑,用锉刀或砂纸打磨毛刺,特别是USB-C口和传感器开口处,确保没有塑料碎屑阻碍插拔或空气流通。

5. 组装、校准与最终测试

所有部件准备就绪,进入最后的组装调试阶段。

5.1 焊接与内部组装

  1. 焊接连接线:根据之前的接线表,将杜邦线或硅胶导线焊接到Beetle ESP32C3、传感器和升压模块上。强烈建议给电源线(VCC、GND)使用不同颜色(如红、黑),避免接反。I2C和串口线也最好用颜色区分。
  2. 固定元器件:使用M2*6mm的自攻螺丝,将Beetle ESP32C3、升压模块固定在下壳的安装柱上。PMS5003可以用螺丝固定,也可以用强力双面胶粘贴。电池可以放在空余位置,用尼龙扎带或泡棉胶固定。
  3. 理线与测试:将所有线材整理好,用扎带固定,避免杂乱。在合上外壳前,先上电测试!通过串口监视器查看传感器数据是否正常输出,手机能否连接到设备IP并显示网页。

5.2 传感器校准与数据解读

  • PMS5003:这款传感器出厂已校准,通常无需用户校准。但要注意,它测量的是“空气动力学当量直径”的颗粒物浓度。在极端干燥或潮湿环境下,读数可能会有轻微偏差。确保进气口通畅,远离风扇、空调直吹。
  • SHT40:精度很高,一般也无需校准。首次读数如果与环境温湿度计差异很大,检查是否接触不良或代码中精度设置是否正确。
  • SGP40:这是校准的重点。VOC传感器需要一段时间的“磨合”和“背景校准”。
    • 首次运行:将设备放在通风良好的室外或已知空气良好的环境中,通电运行至少24小时。这个过程让传感器基线稳定下来。
    • 背景校准:Sensirion的算法库本身具备自动背景校准功能,它会在设备长时间运行时,自动学习环境背景VOC水平,并以此为基础计算VOC指数。因此,让设备持续通电运行是获得准确VOC趋势的关键。短时间开关机,其指数可能不准确。
    • 数据解读:VOC指数是一个无量纲的值,通常范围在1-500。值越低代表空气质量越好。CO2当量值是基于VOC和温湿度估算的,单位为ppm。它可以作为室内二氧化碳浓度趋势的一个参考,但不能替代真正的NDIR CO2传感器进行精确测量。

5.3 功能扩展与优化想法

基础版本完成后,你可以根据兴趣进行扩展:

  1. 数据上传与可视化:将数据定期上传到ThingsBoardBlynkHome Assistant,制作历史数据图表和仪表盘。
  2. 阈值告警:在固件中加入逻辑,当PM2.5超过某个值(如75μg/m³)或VOC指数过高时,让板载的RGB LED闪烁红灯,或在Web页面上弹出警告。
  3. 低功耗优化进阶:使用硬件定时器(RTC)唤醒,实现真正的深度睡眠,使待机电流降至100μA以下,极大延长电池续航。
  4. 增加显示屏:使用一块小型OLED屏幕(如0.96寸),直接显示实时数据,脱离手机也能查看。
  5. 外壳美化:对3D模型进行倒角、添加纹理,或者使用多色打印,让设备更具个性。

这个项目从构思到实现,花费了我几个周末的时间,期间遇到了电源干扰、I2C地址冲突、Web服务器内存泄漏等问题,但逐一解决后的成就感是无与伦比的。现在,这个我自己制作的小设备就放在我的电脑旁,默默守护着我呼吸的每一口空气。它给出的数据,让我对室内环境有了更直观的认识,也促使我养成了定时开窗通风的习惯。如果你也对硬件DIY和物联网感兴趣,不妨动手试一试,这个过程本身,就是最好的学习。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询