简介:这份资源是面向HarmonyOS开发者的空气质量检测项目完整源代码,基于KHDVK-3861开发板实现,适合具备嵌入式与鸿蒙设备开发基础、希望实践多传感器融合与短距通信的工程师或学生。项目通过TP-401PW传感器采集氨气、氢气、酒精、一氧化碳、甲烷等有机挥发气体及烟雾浓度,并借助CCS811传感器经I2C获取CO2与TVOC数值,最终在OLED上显示浓度值与空气质量等级。压缩包共37个文件,约1.84MB,以14个C源文件和10个头文件为核心,辅以7个GN构建脚本、PNG示意图、JSON配置与GNNI文件,覆盖蓝牙、NFC、OLED、传感器采集及组件化编译等模块。已有125人学习下载。读者可从中获得从ADC电压转换到空气质量分级、I2C数据读取、蓝牙与手机通信、NFC拉起应用等完整实现思路,并参考其工程目录组织方式,快速搭建自己的鸿蒙空气质量检测方案。
1. 从一块 KHDVK-3861 开发板说起:空气质量检测到底在测什么
手里拿到一块 KHDVK-3861 开发板,第一反应往往是「先点个灯再说」。但如果你真正想做的是一个能落地的空气质量检测终端,点灯只是热身,真正决定项目成败的是传感器选型、气体标定和 HarmonyOS 侧的数据链路设计。KHDVK-3861 是润和基于海思 Hi3861 芯片推出的鸿蒙开发板,板载 WiFi、丰富的 GPIO 和 ADC 接口,天然适合做物联网传感节点。而空气质量检测这个场景,核心要解决的是:把氨气、氢气、酒精、一氧化碳、甲烷这类挥发性气体浓度,稳定地采集上来、传出去、展示出来。
很多人以为接个 MQ 系列传感器读个 ADC 就完事了,实际做下来会发现:读数飘、温湿度一变化就偏、不同气体之间还互相干扰。这套基于 KHDVK-3861 的 HarmonyOS 空气质量检测项目,本质上是一个「多路模拟气体传感器 + ADC 采样 + 鸿蒙任务调度 + 数据上报」的完整链路。它适合想入门鸿蒙硬件开发、又希望做点有实际意义项目的嵌入式工程师,也适合做环境监测类产品原型的团队直接参考。接下来我会把这条链路从原理到代码拆开讲清楚,让你能照着复现,也能看清哪些地方容易翻车。
2. 传感器选型与信号链:为什么 MQ 系列是绕不开的起点
2.1 氨气、氢气、酒精、一氧化碳、甲烷分别对应哪类传感器
做多气体检测,第一道坎就是选型。市面上能同时覆盖氨气、氢气、酒精、一氧化碳、甲烷这几类气体的方案,最常见、成本最低的就是 MQ 系列半导体气敏传感器。它的原理是:气敏材料(通常是二氧化锡 SnO2)在洁净空气中电导率较低,当还原性气体存在时,表面吸附氧被消耗,电导率上升,通过负载电阻转换成电压信号输出。浓度越高,输出电压越大。
具体到每种气体,常见的对应关系是这样的:
| 目标气体 | 常用型号 | 检测范围(参考) | 加热电压 | 回路电压 |
|---|---|---|---|---|
| 氨气 | MQ-135 | 10~1000 ppm | 5V | ≤24V |
| 氢气 | MQ-8 | 100~10000 ppm | 5V | ≤24V |
| 酒精 | MQ-3 | 0.04~4 mg/L | 5V | ≤24V |
| 一氧化碳 | MQ-7 | 10~1000 ppm | 5V(需高低压循环) | ≤24V |
| 甲烷 | MQ-4 | 300~10000 ppm | 5V | ≤24V |
这里要特别注意 MQ-7 一氧化碳传感器,它需要加热电压在高低压之间循环切换(通常 5V 加热 60 秒,1.4V 加热 90 秒),否则读数会严重漂移。这是新手最容易忽略的一点。而 MQ-135 虽然对氨气敏感,但它其实是个「广谱」传感器,对苯、烟雾、酒精都有响应,所以如果你的项目要区分多种气体,单靠一个 MQ-135 是不够的,必须多传感器组合,再配合标定和算法做区分。
KHDVK-3861 开发板本身提供多路 ADC 接口,Hi3861 芯片内置 12 位 ADC,理论上可以接多路模拟传感器。但要注意,Hi3861 的 ADC 参考电压和输入范围有限,MQ 系列传感器输出在负载电阻上的电压可能超过 ADC 量程,所以硬件上需要分压或者用运放做电平适配。这是信号链设计里必须提前算清楚的一步。
2.2 从传感器到 ADC:负载电阻和分压怎么算
MQ 传感器的基本测量电路是:传感器串联一个负载电阻 RL,加热丝单独供电,信号输出从 RL 两端取。传感器电阻 Rs 随气体浓度变化,输出电压 Vout = Vc × RL / (Rs + RL),其中 Vc 是回路电压。Rs 在洁净空气中有一个基准值 R0,浓度和 Rs/R0 之间是一条对数曲线。
问题来了:Hi3861 的 ADC 输入范围通常是 0~2.4V 左右(具体看开发板的分压设计),而 MQ 传感器在 Vc=5V、RL=10kΩ 时,洁净空气下 Vout 可能在 1~3V 之间波动。所以你需要:
- 选择合适 RL,让目标浓度范围内 Vout 落在 ADC 量程内;
- 如果 Vout 可能超过 ADC 上限,加电阻分压;
- 在软件里做电压到 Rs/R0 的换算,再查曲线得到浓度。
我一般会先用可调电阻做台架测试,把 RL 调到目标气体浓度下 Vout 接近 ADC 满量程的 70%~80%,这样分辨率最好。下面是一段在 Hi3861 上读取 ADC 并换算电压的示例代码,基于 HarmonyOS 的 IoT 外设接口:
#include "iot_adc.h" #include "iot_errno.h" #define ADC_CHANNEL 0 #define ADC_VREF 2.4f // Hi3861 ADC 参考电压,单位 V #define ADC_FULL_SCALE 4095.0f // 12 位 ADC 满量程 #define DIVIDER_RATIO 1.0f // 分压比,如果用了分压电阻,这里填实际比例 float read_sensor_voltage(void) { unsigned int ret; unsigned short data = 0; float voltage; // 初始化 ADC 通道 ret = IoTAdcInit(ADC_CHANNEL); if (ret != IOT_SUCCESS) { return -1.0f; } // 读取原始 ADC 值 ret = IoTAdcRead(ADC_CHANNEL, &data); if (ret != IOT_SUCCESS) { return -1.0f; } // 换算成电压,再乘分压比还原传感器输出 voltage = (float)data / ADC_FULL_SCALE * ADC_VREF * DIVIDER_RATIO; return voltage; }这段代码的逻辑很直接:初始化 ADC 通道,读取 12 位原始值,按参考电压换算成电压,再根据硬件分压比还原到传感器实际输出。参数说明:ADC_VREF必须和开发板实际参考电压一致,Hi3861 常见的是 2.4V 或 3.3V,填错会导致所有读数系统性偏移;DIVIDER_RATIO如果你在传感器和 ADC 之间加了分压电阻,比如 10k 和 20k 分压,比例就是 3.0,没加分压就填 1.0。IoTAdcInit只需要在任务初始化时调用一次,不要每次读都调,否则会反复占用资源。
2.3 多路传感器在 KHDVK-3861 上的接线与通道分配
KHDVK-3861 开发板引出的 ADC 通道数量有限,如果你要同时接氨气、氢气、酒精、一氧化碳、甲烷五路传感器,通道可能不够。常见的做法有两种:一是用模拟多路复用器(如 CD4051)扩展通道,通过 GPIO 切换;二是分时复用,不同传感器轮流加热和采样,但这会影响响应速度。
我一般会优先选多路复用方案,因为 MQ 传感器需要持续加热才能稳定工作,分时断电会导致基线漂移。接线时注意:所有传感器的加热丝供电要独立走线,避免加热电流在共用地上产生压降,干扰信号采样。地线最好单点接地,模拟地和数字地在电源入口处汇合。
通道分配上,假设你用 CD4051 扩展 8 路,可以用 3 个 GPIO 做通道选择,代码里封装一个select_channel函数,切换后延时几毫秒再采样,让多路复用器输出稳定。这个延时不能省,否则通道间串扰会让你怀疑人生。
3. HarmonyOS 侧的任务调度与数据采集框架
3.1 用鸿蒙任务和消息队列组织多传感器采样
在 HarmonyOS 的轻量级系统上,Hi3861 跑的是 LiteOS 内核,任务调度和消息队列是核心机制。多传感器采样如果全塞在一个循环里,会导致某个传感器加热周期被耽误,或者 ADC 读取阻塞其他任务。合理的做法是:创建一个传感器采样任务,用消息队列接收采样命令,另一个任务负责数据处理和上报。
下面是一个任务创建和消息队列的示例:
#include "cmsis_os2.h" #include "iot_gpio.h" #define SENSOR_TASK_STACK_SIZE 0x1000 #define SENSOR_TASK_PRIO 25 #define QUEUE_SIZE 8 osMessageQueueId_t g_sensor_queue; typedef struct { uint8_t sensor_id; float voltage; } sensor_msg_t; void sensor_sample_task(void *arg) { sensor_msg_t msg; osStatus_t status; while (1) { // 阻塞等待采样消息,超时 100ms status = osMessageQueueGet(g_sensor_queue, &msg, NULL, 100); if (status == osOK) { // 根据 sensor_id 读取对应通道 float v = read_sensor_voltage_by_id(msg.sensor_id); msg.voltage = v; // 把结果发给处理任务,这里省略发送逻辑 } } } void sensor_task_init(void) { g_sensor_queue = osMessageQueueNew(QUEUE_SIZE, sizeof(sensor_msg_t), NULL); osThreadAttr_t attr = { .name = "sensor_sample", .stack_size = SENSOR_TASK_STACK_SIZE, .priority = SENSOR_TASK_PRIO, }; osThreadNew(sensor_sample_task, NULL, &attr); }逻辑说明:osMessageQueueGet带超时参数,避免任务永久阻塞导致看门狗复位。sensor_msg_t里带sensor_id,让同一个任务能处理不同通道。参数上,任务栈大小 0x1000 是经验值,如果你在任务里调用了浮点运算或 printf,建议加到 0x2000。优先级 25 在 LiteOS 里属于中等偏上,不要设得比 WiFi 任务还高,否则网络收发会被拖慢。
3.2 数据滤波:滑动平均和中值滤波怎么选
MQ 传感器原始读数噪声很大,直接上报会看到数值乱跳。常见的滤波方式有两种:滑动平均和中值滤波。滑动平均适合抑制随机噪声,但对突发尖峰无能为力;中值滤波能干掉脉冲干扰,但会引入相位滞后。我一般会先用中值滤波去掉明显异常值,再做滑动平均平滑。
#define FILTER_WINDOW 5 float median_filter(float *buf, int len) { float tmp[FILTER_WINDOW]; memcpy(tmp, buf, len * sizeof(float)); // 简单冒泡排序,窗口小够用 for (int i = 0; i < len - 1; i++) { for (int j = 0; j < len - 1 - i; j++) { if (tmp[j] > tmp[j + 1]) { float t = tmp[j]; tmp[j] = tmp[j + 1]; tmp[j + 1] = t; } } } return tmp[len / 2]; } float moving_average(float *buf, int len) { float sum = 0; for (int i = 0; i < len; i++) { sum += buf[i]; } return sum / len; }参数说明:FILTER_WINDOW取 5 是折中值,窗口太大响应慢,太小滤波效果差。中值滤波的排序用冒泡就行,窗口小的时候开销可以忽略。滑动平均的缓冲区要用环形队列维护,避免每次搬移数据。注意:滤波后的值仍然需要标定曲线换算,滤波不能替代标定。
3.3 把浓度数据通过 WiFi 上报到本地服务
Hi3861 的 WiFi 能力是它做物联网节点的最大优势。HarmonyOS 提供了 WiFi 配网和 socket 接口,你可以把处理后的浓度数据通过 TCP 或 UDP 发到本地服务器。常见做法是:设备启动后自动连接预设 WiFi,然后周期性地把 JSON 格式的数据 POST 到局域网内的 HTTP 服务。
#include "lwip/sockets.h" #include "cJSON.h" void report_data(float nh3, float h2, float alcohol, float co, float ch4) { int sock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server = {0}; server.sin_family = AF_INET; server.sin_port = htons(8080); inet_pton(AF_INET, "192.168.1.100", &server.sin_addr); if (connect(sock, (struct sockaddr *)&server, sizeof(server)) != 0) { closesocket(sock); return; } cJSON *root = cJSON_CreateObject(); cJSON_AddNumberToObject(root, "nh3", nh3); cJSON_AddNumberToObject(root, "h2", h2); cJSON_AddNumberToObject(root, "alcohol", alcohol); cJSON_AddNumberToObject(root, "co", co); cJSON_AddNumberToObject(root, "ch4", ch4); char *json_str = cJSON_PrintUnformatted(root); send(sock, json_str, strlen(json_str), 0); cJSON_free(json_str); cJSON_Delete(root); closesocket(sock); }这段代码每次上报都新建连接,适合低频场景。如果你要高频上报,建议保持长连接,否则 TCP 三次握手开销会拖累系统。参数上,服务器 IP 和端口要按你的实际环境改,JSON 字段名和后台解析保持一致。注意 cJSON 的内存释放,cJSON_PrintUnformatted返回的字符串要手动 free,否则跑久了内存会漏光。
4. 标定与浓度换算:把电压变成可信的 ppm
4.1 Rs/R0 曲线和对数拟合怎么做
MQ 传感器手册里给的是 Rs/R0 与浓度的对数曲线。实际使用中,你需要先测出洁净空气中的 R0,然后根据当前 Rs 算出 Rs/R0,再查曲线得到浓度。曲线通常可以拟合成 log-log 线性关系:log(ppm) = a × log(Rs/R0) + b。
标定步骤:
- 把传感器放在洁净空气中预热 24 小时(新传感器必须做);
- 读取输出电压 Vout,计算 Rs = (Vc - Vout) / Vout × RL;
- 这个 Rs 就是 R0;
- 用标准气体或已知浓度环境,测多个点,拟合 a 和 b。
import numpy as np # 假设标定数据:Rs/R0 和对应浓度 ppm ratio = np.array([0.5, 1.0, 2.0, 4.0, 8.0]) ppm = np.array([1000, 500, 200, 80, 30]) log_ratio = np.log10(ratio) log_ppm = np.log10(ppm) # 线性拟合 log(ppm) = a * log(ratio) + b a, b = np.polyfit(log_ratio, log_ppm, 1) print(f"a={a:.3f}, b={b:.3f}") def voltage_to_ppm(vout, vc=5.0, rl=10000.0, r0=5000.0): rs = (vc - vout) / vout * rl ratio = rs / r0 return 10 ** (a * np.log10(ratio) + b)参数说明:r0是洁净空气基准电阻,必须实测,不能用手册典型值代替。a和b因传感器个体差异而不同,批量生产时要么逐个标定,要么选一致性好的批次。拟合用的数据点至少 5 个,覆盖目标浓度范围。
4.2 温湿度补偿为什么不能省
半导体气敏传感器对温湿度非常敏感。温度升高,传感器电阻下降;湿度升高,也会影响表面吸附。如果你不做补偿,夏天和冬天同一浓度的读数可能差 30% 以上。常见做法是加一个温湿度传感器(如 SHT30),在软件里做补偿。
补偿模型可以用经验公式:Rs_corrected = Rs × (1 + α × (T - 25) + β × (RH - 50%)),其中 α 和 β 需要实验拟合。更简单的做法是查表法,把不同温湿度下的修正系数做成表格,运行时插值。我一般会先用查表法快速上线,后续再用数据拟合优化。
4.3 多传感器交叉干扰的工程处理
MQ 系列传感器选择性差,氨气传感器对酒精也有响应,一氧化碳传感器对氢气也敏感。如果你的项目要同时检测多种气体,必须处理交叉干扰。工程上有几种做法:
- 用多个传感器组成阵列,通过模式识别算法(如 PCA 或简单神经网络)区分气体;
- 在硬件上加滤光片或催化层,提高选择性;
- 在软件里做补偿矩阵,用标定数据建立响应矩阵,反解浓度。
对于 KHDVK-3861 这种资源有限的平台,我建议先用响应矩阵做线性补偿,计算量小,效果够用。矩阵系数通过标定实验获得:单独通入每种气体,记录所有传感器的响应,组成矩阵求逆。
5. 避坑与排查:那些让项目翻车的细节
5.1 传感器预热时间不够,读数一直飘
现象:刚上电时读数很高,慢慢下降,半小时后才稳定。原因:MQ 传感器需要加热到工作温度,气敏材料表面吸附氧需要时间达到平衡。新传感器还有老化过程。解决:新传感器首次使用前连续加热 24~48 小时;每次上电至少预热 5~10 分钟再采信数据;代码里加一个预热标志,预热期间不上报。
5.2 ADC 读数跳变严重,怀疑是电源噪声
现象:同一浓度下 ADC 值跳动几十个 LSB。原因:Hi3861 的 ADC 对电源噪声敏感,MQ 加热丝电流波动会耦合到模拟供电。解决:加热丝供电和芯片供电分开走线,加 LC 滤波;ADC 参考电压加去耦电容;软件上做多次采样取平均,比如连续读 16 次去掉最大最小再平均。
5.3 WiFi 上报时任务阻塞导致采样丢点
现象:开启 WiFi 上报后,传感器采样间隔变得不均匀。原因:socket 发送是阻塞操作,如果网络慢,采样任务被卡住。解决:把上报放到独立任务,用消息队列传递数据;socket 设置超时;或者用非阻塞发送。任务优先级上,采样任务要高于上报任务。
5.4 标定曲线用错,浓度差一个数量级
现象:读数看起来合理,但和标准仪器对比差 10 倍。原因:Rs/R0 计算时 RL 值填错,或者 R0 没实测。解决:用万用表实测负载电阻;R0 在洁净空气中实测,不要用手册值;拟合时检查 log 底数是否一致,有的手册用 ln,有的用 log10。
5.5 长时间运行后内存不足复位
现象:设备跑几小时后重启。原因:cJSON 或 socket 相关内存未释放,或者任务栈溢出。解决:每次 cJSON_Print 后必须 free;socket 用完关闭;用osThreadGetStackSpace检查任务栈余量;开启内存统计,定期打印剩余堆空间。
6. 进阶技巧:用响应矩阵和在线标定提升长期稳定性
6.1 响应矩阵的标定与求逆
多传感器交叉干扰的补偿,核心是建立一个响应矩阵 H,其中 H[i][j] 表示第 j 种气体对第 i 个传感器的灵敏度。标定时,单独通入每种标准气体,记录所有传感器的 Rs/R0 变化,归一化后组成矩阵。运行时,测得响应向量 y,浓度向量 c = H⁻¹ × y。
import numpy as np # 假设 5 个传感器对 5 种气体的响应矩阵(行:传感器,列:气体) H = np.array([ [1.00, 0.05, 0.10, 0.02, 0.01], # 氨气传感器 [0.08, 1.00, 0.15, 0.03, 0.02], # 氢气传感器 [0.12, 0.10, 1.00, 0.05, 0.04], # 酒精传感器 [0.03, 0.20, 0.06, 1.00, 0.10], # 一氧化碳传感器 [0.02, 0.04, 0.03, 0.08, 1.00], # 甲烷传感器 ]) H_inv = np.linalg.inv(H) def compensate(response): return H_inv @ response参数说明:响应矩阵必须实测,上面的数值只是示例。矩阵条件数如果太大,求逆会不稳定,可以用伪逆或正则化。在 Hi3861 上跑矩阵求逆不现实,所以标定和求逆在 PC 上完成,把 H_inv 硬编码到固件里。
6.2 在线基线校正:用滑动最小值跟踪 R0 漂移
传感器长期运行后 R0 会漂移,固定 R0 会导致读数越来越偏。在线基线校正的思路是:在洁净空气时段(比如凌晨),用滑动窗口的最小值作为 R0 的估计,定期更新。实现上维护一个长度为 N 的环形缓冲区,每次采样后更新最小值,如果当前值接近最小值且持续一段时间,就更新 R0。
#define BASELINE_WINDOW 100 static float baseline_buf[BASELINE_WINDOW]; static int baseline_idx = 0; static float current_r0 = 5000.0f; void update_baseline(float rs) { baseline_buf[baseline_idx] = rs; baseline_idx = (baseline_idx + 1) % BASELINE_WINDOW; float min_rs = baseline_buf[0]; for (int i = 1; i < BASELINE_WINDOW; i++) { if (baseline_buf[i] < min_rs) { min_rs = baseline_buf[i]; } } // 缓慢跟踪,避免突变 current_r0 = current_r0 * 0.99f + min_rs * 0.01f; }这个方法的假设是:设备在大部分时间里处于洁净空气,或者至少有一段时间暴露在洁净空气中。如果设备长期在高浓度环境,基线会被拉偏,所以最好加一个「洁净空气检测」逻辑,只在确认洁净时才更新。
6.3 验证方法:用标准气体和交叉比对
做完标定和补偿,怎么验证?我一般会做三件事:一是用标准气体(比如 100 ppm 氨气)直接测试,看读数误差是否在 ±15% 以内;二是用两台设备放在同一环境,对比读数一致性;三是和商用空气质量检测仪做交叉比对,记录偏差曲线。如果偏差随浓度变化呈系统性趋势,说明拟合曲线有问题;如果偏差随机,说明噪声或干扰没处理好。
最后说个血泪经验:我最早做这个项目时,为了省事没做温湿度补偿,结果夏天实验室和冬天实验室的读数差了一倍,被客户当场质疑。后来加了 SHT30 和查表补偿,才把一致性做上去。这个方向值得做,但别指望接上传感器就能出数,标定和补偿才是真正花时间的地方。希望帮到你。
本文还有配套的精品资源,点击获取