基于树莓派Pico与MicroPython的空气质量监测系统开发实践

📅 2026/8/19 13:01:45
基于树莓派Pico与MicroPython的空气质量监测系统开发实践
1. 从零开始为什么选择树莓派Pico来搭建空气质量监测器最近几年空气质量成了大家越来越关心的话题尤其是在家里或者办公室里总想知道自己呼吸的空气到底怎么样。市面上的空气质量检测仪不少但要么功能单一要么价格不菲而且数据封闭想自己折腾点花样都不行。作为一名喜欢动手的硬件爱好者我一直在寻找一个既灵活、成本又可控的解决方案。直到我遇到了树莓派Pico这个小小的微控制器板子让我觉得这事儿有戏。树莓派Pico的核心是RP2040微控制器芯片它最大的特点就是性价比极高。几美元的价格却提供了双核ARM Cortex-M0处理器、264KB的SRAM和2MB的板载闪存性能对于处理传感器数据、运行简单逻辑和通过串口输出结果来说绰绰有余。更重要的是它支持MicroPython和C/C编程对于我这种更习惯用Python快速原型开发的玩家来说简直是福音。用MicroPython几行代码就能驱动传感器、读取数据开发效率非常高。那么用Pico来做空气质量监测具体能监测什么呢这完全取决于你接什么传感器。常见的指标包括颗粒物PM2.5/PM10、挥发性有机化合物TVOC、二氧化碳CO2、甲醛HCHO、温度、湿度等。你可以根据你的核心关切来组合传感器。比如如果你主要关心雾霾和粉尘那就重点接一个激光PM2.5传感器如果担心装修污染那么TVOC和甲醛传感器就是必选项。Pico的通用输入输出GPIO引脚和I2C、SPI、UART等通信接口非常丰富可以轻松连接市面上绝大多数数字传感器这种模块化的自由组合能力是成品检测仪很难提供的。这个项目的价值远不止于得到一个读数。通过Pico你可以将数据实时输出到串口监视器也可以连接一个小OLED屏幕做成便携式设备更可以接入Wi-Fi模块如Pico W将数据上传到云端或你自己的服务器实现历史记录、图表分析和远程告警。整个过程从硬件连接到代码编写再到数据可视化完全由你掌控。你不仅得到了一个定制化的空气监测工具更完成了一次完整的嵌入式系统开发实践对物联网IoT的底层逻辑会有更深刻的理解。接下来我就带你一步步实现它。2. 硬件选型与电路设计传感器怎么挑线该怎么接硬件是整个项目的基石选对传感器和设计好电路项目就成功了一半。我们的目标是搭建一个能监测多项关键指标的设备所以需要一套传感器组合。这里我基于性能、易用性和成本推荐一套经过实测的方案。2.1 核心传感器选型与原理首先需要确定监测目标。对于室内空气质量我建议重点关注以下三类指标并对应选择传感器颗粒物PM2.5/PM10这是衡量空气洁净度的最直观指标。我推荐使用攀藤PlantowerPMS5003系列传感器。它采用激光散射原理精度高、稳定性好且直接输出经过校准的数字浓度值单位μg/m³无需复杂的标定。PMS5003通过串口UART通信与Pico连接非常简单。为什么不选更便宜的GP2Y1010AU0F后者是模拟输出需要结合ADC读取并套用公式计算受电压波动影响大数据稳定性远不如数字传感器。挥发性有机化合物与二氧化碳TVOC/eCO2这类气体传感器用于检测有机污染物和估算二氧化碳浓度。Sensirion SGP30是一个非常好的选择。它是一款金属氧化物MOX气体传感器通过I2C接口输出TVOCppb和eCO2ppm等效读数。它的优势在于体积小、功耗相对较低且内置了基线校准算法数据更可靠。需要注意的是eCO2是估算值并非直接测量但对于反映室内人员密度和通风情况足够有效。温湿度温湿度不仅影响体感也与污染物挥发、传感器性能有关。Sensirion SHT30或Aosong DHT22都是成熟的选择。SHT30精度更高、响应更快同样使用I2C接口DHT22性价比突出使用单总线通信。鉴于我们已经使用了I2C接口的SGP30为了简化布线强烈建议选择同为I2C的SHT30它们可以共享同一组I2C总线。2.2 树莓派Pico的引脚分配与连接图树莓派Pico有40个GPIO引脚我们需要合理分配以连接所有传感器并预留显示或通信接口。下面是我的连接方案兼顾了功能隔离和便于布线传感器/模块所需接口Pico GPIO引脚功能说明PMS5003 (PM2.5)UART (TX, RX)GP0 (UART0 TX), GP1 (UART0 RX)传感器TX接Pico RX (GP1)传感器RX接Pico TX (GP0)。需单独提供5V供电。SGP30 (TVOC/eCO2)I2C (SDA, SCL)GP2 (I2C1 SDA), GP3 (I2C1 SCL)共享I2C总线。SGP30工作电压为3.3V。SHT30 (温湿度)I2C (SDA, SCL)GP2 (I2C1 SDA), GP3 (I2C1 SCL)与SGP30共享I2C总线使用不同的I2C地址区分。工作电压3.3V。OLED显示屏 (SSD1306)I2C (SDA, SCL)GP2 (I2C1 SDA), GP3 (I2C1 SCL)可选项用于本地显示。可继续共享I2C总线。电源3.3V, GND3V3(OUT), GND为Pico及所有3.3V传感器供电。PMS5003需接5V。注意PMS5003的工作电压是5V但其串口信号电平是3.3V可以直接与Pico的GPIO耐受3.3V连接无需电平转换。但务必确保只连接了RX/TX/GND三根线传感器的VCC要接到Pico的VBUS5V或外部5V电源上。2.3 电路搭建实操与避坑指南实际焊接或使用面包板搭建时有以下几个关键点需要注意电源是关键所有数字电路都对电源噪声敏感。Pico的3.3V输出引脚3V3(OUT)可以为SGP30、SHT30和OLED供电。但如果你连接了多个设备尤其是OLED屏幕功耗较大时建议使用一个外部的3.3V稳压模块如AMS1117单独供电以避免Pico内部稳压器过载导致电压不稳进而引起传感器读数异常或系统复位。I2C上拉电阻I2C总线SDA和SCL需要上拉电阻到3.3V通常值在2.2kΩ到10kΩ之间。很多传感器模块如SGP30、SHT30的 breakout board和OLED模块已经板载了上拉电阻。如果多个模块都有上拉电阻并联后的总阻值可能会过小导致电流过大。最稳妥的方法是只保留一组上拉电阻例如在Pico这端接两个4.7kΩ电阻或者确认模块的上拉电阻值较大如10kΩ并联后仍在合理范围。PMS5003的启动顺序PMS5003传感器启动时需要一定时间的稳定气流进行自检。上电后最好等待30-60秒再开始读取数据否则最初的数据包可能是无效的。在代码中可以通过忽略前几次读取或添加延时来解决。布线整洁尽量使用短线连接并捆扎固定。对于串口和I2C这类高速信号线杂乱的走线可能引入干扰导致通信失败。使用面包板时确保接触良好。3. 固件开发用MicroPython驱动传感器并读取数据硬件连接好后下一步就是让Pico“活”起来通过编程读取各个传感器的数据。我们选择MicroPython因为它语法简单交互性强非常适合快速开发。你需要先给Pico刷入MicroPython固件这可以通过按住Pico上的BOOTSEL按钮并上电将其识别为U盘然后将下载的.uf2固件文件拖入即可完成。3.1 开发环境搭建与基础库安装我推荐使用Thonny作为集成开发环境IDE。它轻量、免费并且内置了MicroPython支持可以非常方便地连接Pico、上传文件和运行代码。连接Pico后你需要安装一些必要的库。由于Pico的存储空间有限我们只安装核心驱动库。可以通过Thonny的“工具”-“管理包”功能在线安装或者手动下载.mpy文件上传到Pico。核心库包括micropython-sgp30用于驱动SGP30传感器。micropython-sht3x用于驱动SHT3x系列温湿度传感器SHT30是其中一种。ssd1306.py如果使用OLED屏幕需要此驱动。PMS5003传感器因为通信协议简单我们可以直接编写代码解析无需额外库。3.2 分步代码实现与解析让我们分模块编写代码。首先创建一个主文件main.pyPico上电后会自动运行此文件。# main.py - Raspberry Pi Pico Air Quality Monitor import machine import time from machine import Pin, I2C, UART import sgp30 import sht3x # 1. 初始化I2C总线用于SGP30和SHT30 i2c I2C(1, sclPin(3), sdaPin(2), freq100000) # 使用I2C1引脚GP2(SDA), GP3(SCL) # 2. 初始化SHT30温湿度传感器 try: sht_sensor sht3x.SHT3x(i2c, addr0x44) # SHT30的I2C地址通常是0x44或0x45 print(SHT30初始化成功) except Exception as e: print(fSHT30初始化失败: {e}) sht_sensor None # 3. 初始化SGP30气体传感器 try: sgp_sensor sgp30.SGP30(i2c) # SGP30需要初始化并读取基线值如果有存储的话 # 首次使用或长时间断电后需要一段时间的“预热”来获得稳定读数 print(SGP30初始化成功预热中...) # 初始读取一次启动传感器 sgp_sensor.iaq_init() # 这里可以添加从文件系统读取之前保存的基线值的代码以加速稳定过程 except Exception as e: print(fSGP30初始化失败: {e}) sgp_sensor None # 4. 初始化UART串口用于PMS5003 uart UART(0, baudrate9600, txPin(0), rxPin(1)) # 使用UART0引脚GP0(TX), GP1(RX) def read_pms5003(): 读取并解析PMS5003传感器数据 if uart.any() 32: # PMS5003数据帧长度为32字节 data uart.read(32) if data is not None and len(data) 32: # 检查帧头 0x42, 0x4d if data[0] 0x42 and data[1] 0x4d: # 计算校验和 checksum (data[30] 8) | data[31] calc_sum sum(data[0:30]) if calc_sum checksum: # 解析数据 (大端序) pm1_0 (data[10] 8) | data[11] pm2_5 (data[12] 8) | data[13] pm10 (data[14] 8) | data[15] return pm1_0, pm2_5, pm10 return None, None, None def read_all_sensors(): 读取所有传感器数据 sensor_data {} # 读取SHT30 if sht_sensor: try: temp, humidity sht_sensor.get_temp_humi() sensor_data[temperature] temp sensor_data[humidity] humidity except: sensor_data[temperature] sensor_data[humidity] None # 读取SGP30 if sgp_sensor: try: # SGP30需要每秒读取一次以获得有效数据 eco2, tvoc sgp_sensor.iaq_measure() sensor_data[eco2] eco2 sensor_data[tvoc] tvoc except: sensor_data[eco2] sensor_data[tvoc] None # 读取PMS5003 pm1_0, pm2_5, pm10 read_pms5003() sensor_data[pm1_0] pm1_0 sensor_data[pm2_5] pm2_5 sensor_data[pm10] pm10 return sensor_data # 主循环 print(空气质量监测器启动...) while True: data read_all_sensors() # 在串口控制台打印数据 print(f时间: {time.time()}) if data[temperature] is not None: print(f温度: {data[temperature]:.1f}°C, 湿度: {data[humidity]:.1f}%) if data[eco2] is not None: print(feCO2: {data[eco2]} ppm, TVOC: {data[tvoc]} ppb) if data[pm2_5] is not None: print(fPM1.0: {data[pm1_0]} μg/m³, PM2.5: {data[pm2_5]} μg/m³, PM10: {data[pm10]} μg/m³) print(- * 30) time.sleep(2) # 每2秒读取一次3.3 代码关键点解析与调试技巧I2C地址冲突确保SHT30和SGP30的I2C地址不同。SHT30常见地址是0x44或0x45由ADDR引脚决定SGP30的固定地址是0x58。你可以用print(I2C(1).scan())来扫描总线上所有设备地址确认它们都被正确识别。SGP30的预热与基线SGP30上电后需要一段时间可能长达12-24小时读数才能完全稳定。iaq_init()函数会启动测量。更高级的用法是将稳定后的基线值get_iaq_baseline()保存到文件系统中下次上电时通过set_iaq_baseline()载入可以大幅缩短稳定时间。这对于需要快速获得准确读数的应用很重要。PMS5003数据解析其数据帧有固定的结构和校验和。代码中严格检查了帧头0x42, 0x4d和校验和这是确保数据正确性的关键。如果读不到数据首先检查接线RX/TX是否接反、波特率9600以及传感器是否已上电并完成启动。错误处理代码中使用try...except包裹了每个传感器的读取操作。在实际环境中I2C或UART通信可能因干扰偶尔失败良好的错误处理可以防止整个程序因一个传感器故障而崩溃保证其他数据正常输出。4. 数据展示与物联网集成从本地显示到云端看板仅仅在串口打印数据还不够方便我们需要更友好的展示方式。根据你的需求可以选择本地显示或远程云端查看甚至两者兼备。4.1 本地OLED显示屏集成添加一个0.96英寸的I2C OLED屏幕SSD1306驱动是提升设备便携性和即时可读性的好方法。它可以直接共享我们已有的I2C总线。首先确保ssd1306.py库文件已上传到Pico。然后在代码开头导入并初始化import ssd1306 # 初始化OLED128x64分辨率 oled ssd1306.SSD1306_I2C(128, 64, i2c) # 使用同一个i2c对象在主循环中读取数据后添加显示代码def display_on_oled(data): oled.fill(0) # 清屏 oled.text(fPM2.5:{data[pm2_5] or --}, 0, 0) oled.text(fTVOC:{data[tvoc] or --}, 0, 16) oled.text(fCO2:{data[eco2] or --}, 0, 32) oled.text(f{data[temperature] or --}C {data[humidity] or --}%, 0, 48) oled.show() # 在主循环中调用 display_on_oled(data)这样设备就成为一个独立的、带屏幕的监测终端可以放在任何位置。4.2 接入Wi-Fi与数据上云使用Pico W如果你使用的是树莓派Pico W内置Wi-Fi那么就可以轻松实现物联网功能将数据发送到云端服务器或物联网平台实现远程监控和历史数据分析。这里以向一个简单的HTTP服务器例如你自己搭建的Flask应用或像ThingSpeak、Blynk这类物联网平台发送数据为例。你需要先连接Wi-Fi。import network import urequests as requests # Wi-Fi配置 WIFI_SSID 你的Wi-Fi名称 WIFI_PASSWORD 你的Wi-Fi密码 def connect_wifi(): wlan network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print(正在连接Wi-Fi...) wlan.connect(WIFI_SSID, WIFI_PASSWORD) # 等待连接最多10秒 for _ in range(10): if wlan.isconnected(): break time.sleep(1) if wlan.isconnected(): print(网络配置:, wlan.ifconfig()) return True else: print(Wi-Fi连接失败) return False # 在主程序初始化部分调用 if connect_wifi(): print(Wi-Fi连接成功)连接成功后就可以在循环中定期发送数据了。例如发送到ThingSpeak平台THINGSPEAK_API_KEY 你的写入API密钥 THINGSPEAK_URL https://api.thingspeak.com/update def send_to_thingspeak(data): # 构建查询参数 params fapi_key{THINGSPEAK_API_KEY} params ffield1{data.get(pm2_5, 0)} params ffield2{data.get(tvoc, 0)} params ffield3{data.get(eco2, 0)} params ffield4{data.get(temperature, 0)} params ffield5{data.get(humidity, 0)} full_url f{THINGSPEAK_URL}?{params} try: response requests.get(full_url) print(f数据已发送响应: {response.text}) response.close() # 重要关闭连接防止内存泄漏 except Exception as e: print(f发送数据失败: {e}) # 在主循环中可以每15-30秒发送一次避免超过ThingSpeak的速率限制 send_interval 30 last_send_time 0 while True: current_time time.time() data read_all_sensors() # ... 本地显示代码 ... if current_time - last_send_time send_interval: send_to_thingspeak(data) last_send_time current_time time.sleep(2)4.3 数据持久化与本地日志对于没有网络的环境或者作为网络传输的备份可以将数据记录到Pico的本地文件系统中。虽然Pico的闪存空间有限但记录一些关键数据是可行的。import ujson LOG_FILE air_quality_log.txt def log_to_file(data): log_entry { timestamp: time.time(), pm2_5: data.get(pm2_5), tvoc: data.get(tvoc), temperature: data.get(temperature), humidity: data.get(humidity) } try: with open(LOG_FILE, a) as f: f.write(ujson.dumps(log_entry) \n) except: print(写入日志文件失败) # 注意需要定期清理或上传日志文件防止占满存储提示MicroPython的urequests库在发送HTTPS请求时可能会遇到证书问题。对于ThingSpeak使用HTTP URL (http://api.thingspeak.com) 通常可以工作。对于更复杂的平台可能需要使用更底层的usocket库或寻找支持SSL的第三方库。此外频繁的网络请求和文件写入要注意内存管理和错误处理避免程序因网络异常而卡死。5. 外壳制作、校准与长期运行优化一个完整的项目离不开好的“包装”和稳定性优化。这部分将让你的监测器从原型进化成可靠的产品。5.1 设计与制作外壳一个合适的外壳不仅能保护电路还能优化空气流通确保传感器采样准确。对于空气质量监测器外壳设计有几个要点进气与出气孔PMS5003这类激光传感器需要空气自然对流。外壳应在传感器风扇进气口附近开设足够的进气孔并在另一侧开设出气孔形成风道。孔洞大小要能防止大的异物进入同时保证气流顺畅。可以使用细密的网格或防尘海绵覆盖。传感器隔离如果空间允许尽量将PMS5003颗粒物传感器与其他传感器如SGP30稍微隔开。因为PMS5003内部有风扇可能会搅动局部空气影响气体传感器的读数稳定性。屏幕与接口为OLED屏幕开一个透明窗口可以用亚克力板。预留USB电源孔和可能的复位按钮孔。材料选择可以使用3D打印PLA或ABS材料、激光切割亚克力板甚至改造现有的塑料盒。我个人使用3D打印在设计软件如Fusion 360, Tinkercad中根据元件尺寸和上述原则建模效果最好。5.2 传感器校准与数据可信度PMS5003作为激光传感器出厂时已进行校准通常不需要用户校准。其准确性可以通过与专业仪器对比来验证。确保其进气口通畅、风扇运转正常是关键。SGP30这款传感器的核心是基线校准。它通过检测“干净空气”下的信号来建立基准。算法会自动进行但需要时间。将设备放在通风良好的室外或已知空气良好的环境中运行24小时以上可以获得更准确的基线。如前所述保存和加载基线值 (iaq_baseline) 能极大提升重启后的数据质量。SHT30温湿度传感器一般精度较高如有疑虑可以用一个经过校准的温湿度计进行对比如有固定偏差可以在代码中进行偏移补偿。5.3 电源管理与长期运行稳定性电源选择对于长期监测建议使用可靠的5V/2A以上的USB电源适配器供电。避免使用电脑USB口因为电脑休眠可能导致断电。如果想做成便携式可以考虑接一个大的USB充电宝。看门狗定时器为了防止程序因未知原因跑飞可以启用Pico的看门狗定时器WDT。它会在程序卡死时自动重启系统。from machine import WDT wdt WDT(timeout8000) # 8秒超时 # 在主循环中定期喂狗 while True: # ... 你的主循环代码 ... wdt.feed() time.sleep(2)异常重启机制在网络发送或文件操作等可能失败的环节设置合理的超时和重试次数。如果连续多次失败可以主动执行machine.reset()进行软重启这比死机更好。数据发送策略云端传输时采用“队列重试”机制。将待发送数据先暂存到内存列表或文件中发送成功后再移除。如果网络暂时中断数据不会丢失待网络恢复后继续发送。这对于电池供电或网络不稳定的场景尤为重要。经过以上五个步骤你应该已经拥有了一个功能全面、运行稳定的自制空气质量监测器。它不仅能提供实时的本地数据还能将记录同步到云端让你无论身在何处都能掌握家里的空气状况。这个项目的乐趣在于其高度的可定制性——你可以随时增加新的传感器如甲醛传感器改变数据展示方式或者集成到智能家居系统中实现自动化联动如空气质量差时自动打开空气净化器。