基于嵌入式Linux与传感器技术的空气质量监测系统实战开发
1. 项目缘起:从一块“过时”的开发板说起
几年前,英特尔推出了一款名为 Edison 的微型计算模块,它集成了双核 Atom 处理器、1GB 内存、Wi-Fi 和蓝牙,尺寸却只有一张邮票大小。当时,它被寄予厚望,是物联网(IoT)和创客领域的明星产品。然而,随着技术迭代,Edison 逐渐淡出了主流视野,官方支持也慢慢减少。很多人可能觉得,现在再去折腾它,是不是有点“考古”的味道?我手头恰好有这么一块“吃灰”已久的 Edison 开发板,与其让它继续沉睡,不如用它来做点有意思又有实际意义的事情。于是,一个想法诞生了:用它来搭建一个空气质量监测站。
这个想法并非空穴来风。首先,空气质量是关乎每个人健康的重要环境指标,PM2.5、VOCs(挥发性有机物)、温湿度等数据的实时监测,对于家庭、办公室甚至小型工作坊都很有价值。其次,Edison 虽然“老”,但其 x86 架构、完整的 Linux 系统(Yocto Linux)和相对丰富的接口(如 I2C、UART、GPIO),让它依然具备强大的项目承载能力。最后,这个过程本身就是一个绝佳的嵌入式 Linux 开发实战案例,涉及系统定制、传感器驱动、数据采集、网络通信和轻量级服务部署,每一步都充满了挑战和学习的乐趣。所以,这不仅仅是一个监测项目,更是一次对经典硬件潜力的深度挖掘和一次完整的嵌入式开发旅程。
2. 硬件选型与核心传感器解析
搭建空气质量监测站,传感器的选择是核心。我们需要监测的典型参数包括:颗粒物浓度(PM2.5/PM10)、有害气体(如甲醛、TVOC)、温度、湿度以及大气压力。针对 Edison 的接口特性和项目复杂度,我选择了以下组合方案。
2.1 颗粒物传感器:攀藤 PMS5003
PM2.5 监测是空气质量的核心。我选择了攀藤(Plantower)的 PMS5003 激光粉尘传感器。这是一款在创客和开源项目中经久不衰的型号,其稳定性和性价比得到了广泛验证。
- 工作原理:它采用激光散射原理。传感器内部有一个激光二极管发射激光束,当空气中的颗粒物通过检测区域时,会散射激光。散射光被光电探测器接收并转换为电信号,经过算法处理,即可计算出不同粒径(如 PM1.0, PM2.5, PM10)的颗粒物数量浓度,并最终转换为质量浓度(μg/m³)。
- 接口与通信:PMS5003 使用 UART(串口)通信,这对于 Edison 来说非常友好。Edison 板载了多个 UART 接口,我们只需要连接 RX、TX、GND 和 VCC(5V)四根线即可。其数据协议是公开的,每一条数据帧包含起始字符、帧长度、各浓度数据及校验和,解析起来相对简单。
- 选型理由:相比一些更初级的基于红外原理的传感器(如 GP2Y1010AU0F),激光传感器的精度和可靠性要高得多。而相比一些工业级或更昂贵的型号,PMS5003 在满足民用级精度要求的同时,保持了较低的成本和功耗,非常适合本项目。
2.2 气体与温湿度传感器:博世 BME680
为了监测挥发性有机化合物(VOC)、室内空气质量指数(IAQ)、温度、湿度和气压,我选择了博世 Sensortec 的 BME680。这是一款高度集成的环境传感器。
- 核心功能:
- 温度与湿度:高精度测量,为空气质量数据提供环境补偿参考。
- 气压:可用于粗略海拔估算或天气趋势分析。
- 气体传感:这是其核心特色。它内置了一个金属氧化物(MOX)气体传感器,主要对 VOCs 敏感,如乙醇、甲醛、一氧化碳等。通过复杂的算法,芯片能输出一个“室内空气质量指数”(IAQ),范围通常为 0-500(数值越低越好),以及等效的二氧化碳当量(eCO2)和 VOC 当量。
- 接口与通信:BME680 使用 I2C 或 SPI 接口。我选择了 I2C,因为它接线更简单(只需 SDA、SCL、VCC、GND),且 Edison 的 I2C 驱动成熟。其寄存器配置和数据读取有标准的库支持。
- 选型理由:BME680 将四种传感器合而为一,极大简化了硬件设计和空间占用。其气体传感器虽不能区分具体气体种类,但提供的 IAQ 指数对于评估整体空气污染水平非常有价值。它的功耗也经过优化,适合长期运行。
2.3 主控与连接:英特尔 Edison 开发板
Edison 在本项目中扮演着“大脑”和“网关”的角色。
- 计算能力:双核 Atom 处理器足以流畅运行一个定制的 Linux 系统、驱动多个传感器、运行数据采集程序,甚至托管一个简单的 Web 服务器。
- 操作系统:官方提供的 Yocto Linux 镜像是一个高度可定化的嵌入式 Linux 发行版。我们可以根据需要裁剪系统,移除不必要的服务,让系统更轻量、启动更快。
- 连接性:板载 Wi-Fi 和蓝牙是项目的关键。Wi-Fi 用于将采集到的数据上传到云端或本地服务器,也可以直接提供 Web 访问界面。蓝牙则可以用于近距离配置或调试。
- 扩展接口:通过底部的 70 针扩展接口,我们可以方便地连接 UART(用于 PMS5003)和 I2C(用于 BME680)等外设。
硬件连接清单:
| 组件 | 接口类型 | 连接到 Edison 引脚 | 备注 |
|---|---|---|---|
| 攀藤 PMS5003 | UART | RX -> J17-1 (GPIO 130), TX -> J17-3 (GPIO 131), VCC -> 5V, GND -> GND | 需在系统中配置 UART 设备 |
| 博世 BME680 | I2C | SDA -> J17-5 (I2C-6 SDA), SCL -> J17-7 (I2C-6 SCL), VCC -> 3.3V, GND -> GND | Edison 有多个 I2C 总线,需确认设备号 |
| 电源 | DC 5V/2A | 通过 Micro USB 或扩展板供电 | 确保电源稳定,传感器同时工作时电流可能超过1A |
注意:Edison 的引脚功能需要通过
sysfs或设备树(Device Tree)进行配置,才能将某个 GPIO 复用为 UART 功能。这是使用 Edison 进行嵌入式开发的第一道坎。
3. 软件环境搭建与系统定制
拿到硬件只是第一步,让 Edison 的 Linux 系统“认识”并驱动我们的传感器,需要一番配置。官方提供的默认镜像功能齐全但略显臃肿,我们可以进行精简。
3.1 刷写与定制 Yocto Linux 镜像
- 获取基础镜像:从英特尔开源技术中心(OSTC)的仓库下载针对 Edison 的最新 Yocto 镜像。使用
flashall.sh脚本通过 USB OTG 口将系统刷写到 Edison 上。 - 首次启动与配置:刷写完成后,通过串口(使用 FTDI USB 转串口模块连接 Edison 的 J3 接口)登录系统。默认用户名为
root,无密码。首先配置 Wi-Fi:使用configure_edison --setup命令交互式地设置网络、主机名和密码。 - 系统精简:登录后,移除一些不需要的包以节省空间和内存。例如,如果不需要蓝牙功能,可以移除相关的服务和库。
同样,可以移除一些不用的开发工具或文档。opkg remove bluez5-dev bluez5-noinst-tools pulseaudio-module-bluetooth
3.2 配置传感器硬件接口
这是最关键的一步,需要在内核层面正确配置引脚功能。
配置 UART 给 PMS5003:Edison 的 GPIO 130 和 131 默认可能不是 UART 功能。我们需要手动配置。
- 登录 Edison,查看当前引脚复用情况:
cat /sys/kernel/debug/gpio。 - 将 GPIO 130 和 131 配置为 UART 模式。这可以通过写
sysfs接口实现。创建一个脚本setup_uart.sh:#!/bin/bash # 将 GPIO130 和 GPIO131 从 GPIO 模式释放,并设置为 UART 模式 echo 130 > /sys/class/gpio/unexport 2>/dev/null echo 131 > /sys/class/gpio/unexport 2>/dev/null # 具体设置命令可能因内核版本而异,有时需要操作 /sys/devices/platform/... 下的文件 # 更可靠的方法是使用设备树覆盖(Device Tree Overlay),但这需要重新编译内核或模块。 # 一个常见的方法是使用 MRAA 库(英特尔IoT库)来配置。
实际上,对于 Edison,更推荐使用MRAA库或UPM库(它们封装了底层的引脚操作)来简化这个过程。我们可以先安装 MRAA:
opkg update opkg install mraa然后编写一个 Python 脚本,利用 MRAA 初始化 UART。
import mraa import serial # 使用 mraa 确认引脚,但实际通信使用 pyserial # 首先确保系统已识别出 tty 设备,如 /dev/ttyMFD1 uart = serial.Serial('/dev/ttyMFD1', baudrate=9600, timeout=2)- 踩坑点:Edison 的 UART 设备名可能是
/dev/ttyMFD1,/dev/ttyMFD2等,具体哪个对应我们连接的物理引脚,需要查阅 Edison 的引脚映射表或通过试验确定。dmesg | grep tty命令在连接传感器后查看内核信息有助于识别。
- 登录 Edison,查看当前引脚复用情况:
配置 I2C 给 BME680:I2C 的配置通常比 UART 简单。
- 加载 I2C 驱动模块(通常已内置):
modprobe i2c-dev。 - 安装 I2C 工具:
opkg install i2c-tools。 - 扫描 I2C 总线,查看 BME680 是否被识别:
i2cdetect -y -r 6(假设使用 I2C-6 总线)。如果看到地址0x76或0x77(BME680 的默认地址),说明连接成功。 - 安装 Python 的 smbus 库以便操作:
opkg install python-smbus。
- 加载 I2C 驱动模块(通常已内置):
3.3 安装必要的软件包
我们需要 Python 环境以及相关的传感器库。
opkg update opkg install python3 python3-pip python3-setuptools pip3 install pyserial # 用于UART通信 pip3 install bme680 # 官方的BME680 Python库,或者使用Adafruit的库 # 对于PMS5003,可以安装现成的库,如 pms5003 pip3 install pms5003如果现成的 PMS5003 库不兼容,我们就需要根据其数据手册,手动编写串口读取和解析的代码,这其实是一个很好的学习过程。
4. 数据采集程序的核心实现
有了硬件和基础环境,接下来就是编写核心的数据采集程序。这个程序需要完成以下任务:初始化传感器、定时读取数据、处理数据(如单位转换、简单滤波)、将数据打包并发送出去。我将采用 Python 来实现,因为它开发效率高,库丰富。
4.1 传感器驱动与数据读取
PMS5003 数据读取: PMS5003 会主动以大约每秒一次的频率通过串口发送数据帧。我们的程序需要监听串口,读取完整的数据帧并进行校验。
import serial import struct import time class PMS5003Reader: def __init__(self, port='/dev/ttyMFD1', baudrate=9600): self.ser = serial.Serial(port, baudrate=baudrate, timeout=2) # 数据帧起始字节 self.START_BYTES = b'\x42\x4d' def _read_frame(self): """读取并解析一帧数据""" # 寻找起始字节 while True: start1 = self.ser.read(1) if start1 == self.START_BYTES[0:1]: start2 = self.ser.read(1) if start2 == self.START_BYTES[1:2]: break # 读取帧长度(高字节+低字节) frame_len_bytes = self.ser.read(2) frame_len = struct.unpack('>H', frame_len_bytes)[0] # 大端序 # 读取数据部分和校验和 raw_data = self.ser.read(frame_len) # 包含数据+校验和 # 校验(此处省略校验和计算代码) # 解析数据 # 数据格式:PM1.0标准(2字节), PM2.5标准(2字节), PM10标准(2字节), ... data = struct.unpack('>'+'H'*(frame_len-2), raw_data[:-2]) pm1_0_std, pm2_5_std, pm10_std = data[0], data[1], data[2] # ... 解析其他数据 return {'pm1_0': pm1_0_std, 'pm2_5': pm2_5_std, 'pm10': pm10_std} def read_data(self): """获取一次有效的传感器数据""" return self._read_frame()- 经验之谈:串口读取时,一定要处理好帧同步和校验。如果校验失败,应该丢弃这一帧数据,重新同步。此外,PMS5003 在刚上电或风扇启动时,前几十秒的数据可能不稳定,程序最好能忽略这段时间的数据,或做一个简单的启动延迟。
BME680 数据读取: 使用
bme680库会简单很多。import bme680 import time class BME680Reader: def __init__(self, i2c_addr=0x77, i2c_bus=6): self.sensor = bme680.BME680(i2c_addr, i2c_bus) # 配置传感器参数 self.sensor.set_humidity_oversample(bme680.OS_2X) self.sensor.set_pressure_oversample(bme680.OS_4X) self.sensor.set_temperature_oversample(bme680.OS_8X) self.sensor.set_filter(bme680.FILTER_SIZE_3) self.sensor.set_gas_status(bme680.ENABLE_GAS_MEAS) self.sensor.set_gas_heater_temperature(320) # 摄氏度 self.sensor.set_gas_heater_duration(150) # 毫秒 self.sensor.select_gas_heater_profile(0) # 气体传感器需要预热和稳定 self._burn_in_time = 300 # 预热300秒 self._start_time = time.time() def read_data(self): """获取一次传感器数据,气体读数需要稳定周期""" if self.sensor.get_sensor_data(): data = { 'temperature': self.sensor.data.temperature, 'pressure': self.sensor.data.pressure, 'humidity': self.sensor.data.humidity, } # 处理气体读数 if self.sensor.data.heat_stable: data['gas_resistance'] = self.sensor.data.gas_resistance # 可以在这里根据温湿度补偿计算 IAQ、eCO2 等(库可能提供,或需自己实现) # 例如,使用一个简单的算法将 gas_resistance 和 humidity 转换为 IAQ 分数 iaq_score = self._calculate_iaq(data['gas_resistance'], data['humidity']) data['iaq'] = iaq_score else: data['gas_resistance'] = None data['iaq'] = None return data return None def _calculate_iaq(self, gas_resistance, humidity): """一个简化的 IAQ 计算示例(非博世官方算法)""" if gas_resistance is None: return None # 这是一个非常粗略的示例,真实算法复杂且依赖校准 # 假设 gas_resistance 越高,空气质量越好 base_score = (gas_resistance / 10000.0) # 归一化 # 湿度补偿(高湿可能影响MOX传感器) humidity_factor = 1.0 + (humidity - 40.0) * 0.01 # 示例系数 iaq = max(0, min(500, (500 - base_score * 50 * humidity_factor))) return int(iaq)注意:BME680 的气体传感器(MOX)需要一段“预热”时间(burn-in period)才能输出稳定的读数,通常需要几分钟。在此期间,
heat_stable标志为 False,气体电阻值会剧烈变化,不应采用。官方库和算法通常包含了复杂的补偿和计算,上述_calculate_iaq函数仅为示意,生产环境应使用更可靠的算法或直接依赖传感器芯片内部计算(如果支持)。
4.2 主循环与数据整合
主程序将两个传感器读取器结合起来,以固定的时间间隔(例如每 5 秒)采集一次数据,并整合成一个 JSON 对象。
import json import time from datetime import datetime def main_loop(): pm_reader = PMS5003Reader() bme_reader = BME680Reader() # 等待BME680预热 print("等待BME680气体传感器预热...") time.sleep(bme_reader._burn_in_time) while True: try: # 读取数据 pm_data = pm_reader.read_data() bme_data = bme_reader.read_data() # 整合数据 sensor_data = { 'timestamp': datetime.utcnow().isoformat() + 'Z', 'pm1_0': pm_data.get('pm1_0'), 'pm2_5': pm_data.get('pm2_5'), 'pm10': pm_data.get('pm10'), 'temperature': bme_data.get('temperature'), 'humidity': bme_data.get('humidity'), 'pressure': bme_data.get('pressure'), 'iaq': bme_data.get('iaq'), 'gas_resistance': bme_data.get('gas_resistance'), } # 打印到控制台(调试用) print(json.dumps(sensor_data, indent=2)) # 调用数据发送函数 send_data_to_backend(sensor_data) time.sleep(5) # 采集间隔 except KeyboardInterrupt: print("程序退出。") break except Exception as e: print(f"数据采集出错:{e}") time.sleep(10) # 出错后等待稍长时间再试5. 数据上云与本地服务部署策略
采集到的数据需要被存储、展示和分析。这里有两种主流思路:一是将数据发送到云端物联网平台;二是在 Edison 本地部署轻量级服务,实现数据存储和可视化。
5.1 方案一:数据上云(以 MQTT + 云平台为例)
这是更通用和可扩展的方案。Edison 作为 MQTT 客户端,将数据发布到 MQTT 代理(Broker),云端服务器订阅这些主题并处理数据。
- 安装 MQTT 客户端库:
pip3 install paho-mqtt - 编写 MQTT 发布函数:
import paho.mqtt.client as mqtt import json MQTT_BROKER = "your.broker.address" # 例如:mqtt.eclipseprojects.io(公共测试) MQTT_PORT = 1883 MQTT_TOPIC = "edison/air_quality/data" CLIENT_ID = f"edison-airmon-{os.uname()[1]}" client = mqtt.Client(client_id=CLIENT_ID) def on_connect(client, userdata, flags, rc): if rc == 0: print("连接到 MQTT Broker 成功!") else: print(f"连接失败,返回码 {rc}") client.on_connect = on_connect client.connect(MQTT_BROKER, MQTT_PORT, 60) client.loop_start() # 启动网络循环线程 def send_data_to_backend(data): payload = json.dumps(data) result = client.publish(MQTT_TOPIC, payload, qos=1) if result.rc == mqtt.MQTT_ERR_SUCCESS: print("数据发布成功") else: print(f"数据发布失败: {result}") - 云端处理:你可以使用像Node-RED(部署在树莓派或云服务器上)、ThingsBoard、阿里云物联网平台或AWS IoT Core等平台来接收 MQTT 数据,进行存储、可视化(创建仪表盘)和设置报警规则。
5.2 方案二:本地部署轻量级服务
如果希望数据完全在本地网络处理,可以在 Edison 上运行一个轻量级的 Web 服务器和数据库。
- 本地数据库:使用SQLite是最简单的选择。
opkg install sqlite3,然后在 Python 中使用sqlite3库创建表并插入数据。 - Web 服务器与 API:使用Flask或Bottle这样的微型 Python Web 框架,创建一个简单的 REST API 来接收和提供数据。
from flask import Flask, jsonify, request, render_template import sqlite3 import json app = Flask(__name__) DATABASE = '/home/root/air_quality.db' def init_db(): conn = sqlite3.connect(DATABASE) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS sensor_data (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, pm2_5 REAL, temperature REAL, humidity REAL, iaq INTEGER)''') conn.commit() conn.close() @app.route('/api/data', methods=['POST']) def receive_data(): data = request.json # 将数据存入 SQLite conn = sqlite3.connect(DATABASE) c = conn.cursor() c.execute("INSERT INTO sensor_data (timestamp, pm2_5, temperature, humidity, iaq) VALUES (?, ?, ?, ?, ?)", (data['timestamp'], data['pm2_5'], data['temperature'], data['humidity'], data.get('iaq'))) conn.commit() conn.close() return jsonify({'status': 'success'}), 201 @app.route('/api/latest') def get_latest(): conn = sqlite3.connect(DATABASE) c = conn.cursor() c.execute("SELECT * FROM sensor_data ORDER BY timestamp DESC LIMIT 1") row = c.fetchone() conn.close() if row: # 转换为字典返回 pass return jsonify(row) @app.route('/') def index(): return render_template('index.html') # 一个简单的图表页面 if __name__ == '__main__': init_db() app.run(host='0.0.0.0', port=5000, debug=False) - 前端可视化:在
index.html中,可以使用Chart.js或ECharts等 JavaScript 图表库,通过 AJAX 定期从/api/latest或/api/history获取数据,动态更新图表。
5.3 方案对比与选择
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| MQTT + 云平台 | 扩展性强,可连接多设备;云端计算和存储能力强;有成熟的可视化和告警方案;数据可远程访问。 | 依赖互联网;可能涉及服务费用(部分平台);数据隐私性取决于平台。 | 多设备集中管理、需要复杂分析、远程监控、与其它智能家居系统集成。 |
| 本地服务 | 数据完全本地,隐私性好;不依赖外网;延迟低。 | Edison 计算和存储资源有限;开发和维护全套服务(DB+后端+前端)工作量较大;难以实现远程访问(需内网穿透)。 | 对数据隐私要求极高、网络环境不稳定或不可用、作为单一独立监测站使用。 |
对于本项目,我推荐混合方案:在 Edison 本地运行数据采集和 MQTT 发布客户端,在家庭局域网内的另一台性能稍强的设备(如树莓派、旧笔记本或 NAS)上部署 MQTT Broker(如 Mosquitto)、Node-RED 和 InfluxDB/Grafana。这样既利用了 Edison 的传感能力,又将数据存储和展示的重任交给了更适合的设备,平衡了性能、隐私和复杂度。
6. 系统优化与长期运行考量
要让这个监测站稳定地 7x24 小时运行,还需要解决一些实际问题。
6.1 电源管理与看门狗
Edison 和传感器需要稳定的电源。一个可靠的 5V/2A 电源适配器是基础。对于长期运行,有两个关键点:
- 软件看门狗:编写一个简单的看门狗脚本,定期检查主采集程序是否在运行。如果程序崩溃,看门狗负责重启它。可以用
systemd服务来实现这一点,这是更规范的方法。 - 硬件看门狗:Edison 模块本身支持硬件看门狗定时器。我们需要在内核中启用它,并编写一个用户空间程序定期“喂狗”。如果系统完全挂起,硬件看门狗会在超时后强制重启整个系统。这需要修改内核配置并编写相应的守护进程。
6.2 数据存储与日志
即使数据发送到云端,本地也应保留一份最近的日志,用于故障排查。
- 循环日志:使用 Python 的
logging模块,将程序运行日志和采集到的原始数据(可选)写入文件。配置RotatingFileHandler,防止日志文件无限增大占满存储空间。 - 简易本地缓存:在发送数据到云端前,可以先写入一个本地 SQLite 数据库或简单的 CSV 文件。如果网络暂时中断,数据不会丢失,待网络恢复后可以进行断点续传(需要更复杂的逻辑)。
6.3 传感器校准与维护
传感器,尤其是气体传感器,需要定期维护才能保证数据准确性。
- PMS5003:激光传感器窗口可能会积灰,影响精度。需要定期用棉签和气吹清洁进气口和激光腔(如果设计允许)。它的风扇也有寿命,长时间运行后噪音可能变大。
- BME680:MOX 气体传感器会随时间老化,其灵敏度会变化。博世建议每 12 个月进行一次“基线校准”。具体做法是将传感器置于已知的良好空气环境(如室外空气)中连续运行 48 小时,让其重新建立“干净空气”的基准电阻值。一些高级的驱动库(如
bme680库的某些分支)支持保存和加载基线值的功能。 - 数据漂移处理:在软件层面,可以对采集到的数据进行滑动平均滤波,以平滑短时的噪声波动。对于长期监测,可以设定一个“参考值”修正机制,例如,在已知空气质量良好的时段(如深夜通风后),手动记录一组数据作为临时基准进行偏移补偿。
6.4 将程序封装为系统服务
为了让采集程序在 Edison 启动时自动运行,并在崩溃后自动重启,最好将其配置为systemd服务。
- 创建一个服务文件
/etc/systemd/system/airquality.service:[Unit] Description=Air Quality Monitoring Service After=network.target [Service] Type=simple User=root WorkingDirectory=/home/root/air_quality ExecStart=/usr/bin/python3 /home/root/air_quality/main.py Restart=on-failure RestartSec=10s StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target - 启用并启动服务:
systemctl daemon-reload systemctl enable airquality.service systemctl start airquality.service - 查看服务状态和日志:
systemctl status airquality.service,journalctl -u airquality.service -f。
通过以上步骤,一个基于英特尔 Edison 的、功能相对完善的空气质量监测站就搭建完成了。它不仅输出了有价值的环境数据,更让我们重温了嵌入式 Linux 开发的完整流程:从硬件接口调试、驱动层配置,到应用层逻辑编写,再到系统服务部署和运维考量。虽然 Edison 已不是最新硬件,但通过这样的项目,其价值得以延续,而我们获得的实战经验,对于理解当代物联网设备的开发逻辑,依然具有很高的参考价值。
