简介:这份资源是面向HarmonyOS物联网开发者的空气质量检测项目完整源代码,基于KHDVK-3861开发板实现,适合具备嵌入式与鸿蒙设备开发基础、希望动手实践多传感器融合采集的工程师或高年级学生。项目通过TP-401PW传感器检测氨气、氢气、酒精、一氧化碳、甲烷等有机挥发气体及烟雾,并借助CCS811传感器经I2C采集二氧化碳浓度与TVOC值,最终输出优、良、中、差四级空气质量等级。压缩包共37个文件,约1.84MB,以14个C源文件与10个头文件为核心,辅以7个GN构建脚本、PNG示意图、JSON配置与Markdown说明,覆盖蓝牙通讯、NFC拉起应用、OLED显示驱动、ADC电压转换及HiLink组件集成等模块。目前已有125人学习,读者可据此掌握多传感器数据采集、外设驱动与鸿蒙组件化编译的完整实现思路。
1. 一块 3861 开发板,怎么把五种挥发气体读成可上云的数据
很多人第一次拿到 KHDVK-3861 这块 HarmonyOS 开发板,第一反应是点个灯、连个 WiFi,然后就没有然后了。但如果你手上正好有一个空气质量检测的需求——比如检测氨气、氢气、酒精、一氧化碳、甲烷这些挥发气体——这块板子其实能撑起一个完整的端侧采集方案。它跑的是 OpenHarmony 轻量系统,自带 ADC、GPIO、I2C 这些外设接口,配合一颗多通道气体传感器模组,就能把空气里这几类还原性气体的浓度变化,变成可上报的数字量。这篇文章不讲空泛的物联网概念,只讲一件事:基于 KHDVK-3861 的空气质量检测项目,从传感器选型、ADC 采样、浓度换算,到 HarmonyOS 侧的数据上报,整条链路怎么落地。适合手里有这块板子、想把它用在一个真实检测场景里的嵌入式开发者,也适合正在找 HarmonyOS 端侧传感器实战案例的人。
2. 气体检测的硬件链路:从传感器到 3861 的 ADC 引脚
2.1 为什么这类项目普遍选半导体式气体传感器
氨气、氢气、酒精、一氧化碳、甲烷这五种气体,有一个共同点:它们都是还原性气体,在高温下会与金属氧化物半导体表面发生氧化还原反应,导致传感器电阻下降。这就是半导体式气体传感器(比如常见的 MQ 系列)的工作基础。选它的理由很直接:便宜、驱动简单、寿命够用、对多种还原性气体都有交叉响应。代价是选择性差——一颗传感器没法只测一种气体,它输出的是“这几种气体的综合浓度趋势”。
所以这类项目的典型做法是:用一颗多通道传感器模组,或者用几颗不同敏感材料的传感器组成阵列,分别对氨气、氢气、酒精、一氧化碳、甲烷做相对响应标定。KHDVK-3861 这边要做的,就是把每一路传感器的模拟输出,接到不同的 ADC 通道上,分别采样。
注意:半导体气体传感器需要预热。冷启动时读数会从高阻态缓慢下降,通常要预热 3 到 5 分钟,数据才稳定。项目上电后的前几分钟数据不要直接上报,否则会触发误报警。
2.2 KHDVK-3861 的 ADC 采样配置与引脚映射
KHDVK-3861 基于 Hi3861 芯片,ADC 是 12 位精度,参考电压典型值 1.8V(具体以板子原理图为准,不同底板分压网络可能不同)。传感器模块的输出电压范围一般在 0 到 3.3V 之间,所以中间通常要加一个分压电阻网络,把电压压到 ADC 量程以内。
下面是一段在 HarmonyOS 侧读取 ADC 并换算电压的代码示例。这里假设你用的是 OpenHarmony 的 IoT 硬件接口,ADC 通道号根据实际接线调整。
#include "iot_adc.h" #include "iot_errno.h" #define ADC_CHANNEL_AMMONIA 0 // 氨气传感器接 ADC0 #define ADC_CHANNEL_HYDROGEN 1 // 氢气传感器接 ADC1 #define ADC_CHANNEL_ALCOHOL 2 // 酒精传感器接 ADC2 #define ADC_CHANNEL_CO 3 // 一氧化碳传感器接 ADC3 #define ADC_CHANNEL_METHANE 4 // 甲烷传感器接 ADC4 #define ADC_REF_VOLTAGE 1.8f // ADC 参考电压,单位 V #define ADC_MAX_VALUE 4095 // 12 位 ADC 满量程 // 读取单个 ADC 通道并换算成电压值 float read_sensor_voltage(unsigned int channel) { unsigned int data = 0; // 读取 ADC 原始值,返回 0 表示成功 if (IoTAdcRead(channel, &data, IOT_ADC_EQU_MODEL_1, IOT_ADC_CUR_BAIS_DEFAULT, 0) != 0) { return -1.0f; // 读取失败,返回负值便于上层判断 } // 原始值转电压:data / 4095 * 参考电压 float voltage = (float)data / ADC_MAX_VALUE * ADC_REF_VOLTAGE; return voltage; }这段代码的逻辑很直白:先调IoTAdcRead拿原始 ADC 值,再用满量程和参考电压做线性换算。参数上要注意三个点。第一,IOT_ADC_EQU_MODEL_1是均值滤波模型,读一次实际会采多次取平均,能压掉一部分电源纹波噪声;如果你发现数据跳得厉害,可以换成更高阶的均值模型,但响应会变慢。第二,IOT_ADC_CUR_BAIS_DEFAULT是电流偏置,默认值即可,除非你的分压电阻特别大导致驱动能力不足。第三,参考电压一定要以你手上板子的原理图为准,Hi3861 的 ADC 参考电压不是固定 3.3V,用错参考电压,后面所有浓度换算全错。
2.3 从电压到浓度:标定曲线的拟合方式
拿到电压之后,下一步是换算成气体浓度。半导体传感器的电阻和气体浓度之间不是线性关系,常见做法是用幂函数拟合:
Rs/R0 = a * (C)^b其中 Rs 是传感器在当前气体浓度下的电阻,R0 是传感器在洁净空气中的基准电阻,C 是气体浓度(ppm),a 和 b 是拟合常数,由传感器厂家给出或自己标定。实际项目里,我一般会先把电压转成 Rs,再除以 R0,然后反推浓度。
#include <math.h> // 负载电阻阻值,单位欧姆,根据实际电路填写 #define LOAD_RESISTANCE 10000.0f // 根据传感器分压计算 Rs // voltage: 传感器输出电压 // vcc: 传感器供电电压,通常 5V 或 3.3V float calc_sensor_resistance(float voltage, float vcc) { if (voltage <= 0.01f) { return -1.0f; // 电压过低,视为异常 } // 分压公式:Rs = (Vcc - Vout) / Vout * RL return (vcc - voltage) / voltage * LOAD_RESISTANCE; } // 根据 Rs/R0 比值反推浓度,a、b 为标定常数 float calc_gas_concentration(float rs, float r0, float a, float b) { if (rs <= 0 || r0 <= 0) { return -1.0f; } float ratio = rs / r0; // 由 ratio = a * C^b 反解 C float concentration = powf(ratio / a, 1.0f / b); return concentration; }这里的关键参数是 R0。R0 必须在洁净空气中标定,而且要在传感器充分预热之后测。我的习惯是:上电预热 5 分钟,连续采 100 个点取平均,算出洁净空气下的 Rs 作为 R0,写进代码或者存到 flash 里。a 和 b 这两个常数,如果厂家数据手册给了曲线图,可以用两点法粗略拟合;要求高的话,就用标准气体做多点标定,再用最小二乘拟合。别小看这一步,a 和 b 差一点,高浓度段的误差会放大得很明显。
3. HarmonyOS 侧的数据采集任务与串口上报
3.1 用任务和消息队列组织多通道采样
五个通道如果在一个循环里顺序读,会遇到一个问题:ADC 读取是阻塞的,五个通道轮一遍的时间可能到几十毫秒,再加上传感器预热和滤波,整个采样周期会被拉长。更合理的做法是开一个独立的采样任务,用消息队列把采样结果发给上报任务,采样和上报解耦。
#include "cmsis_os2.h" #include "iot_gpio.h" #define SAMPLE_TASK_STACK_SIZE 0x1000 #define SAMPLE_TASK_PRIO 25 #define QUEUE_SIZE 8 typedef struct { unsigned int channel; float voltage; float concentration; } gas_sample_t; static osMessageQueueId_t g_sample_queue; // 采样任务:轮询五个通道,把结果丢进队列 static void gas_sample_task(void *arg) { (void)arg; gas_sample_t sample; while (1) { for (unsigned int ch = 0; ch < 5; ch++) { float v = read_sensor_voltage(ch); if (v < 0) { continue; // 读取失败跳过 } sample.channel = ch; sample.voltage = v; // 浓度换算需要各通道的 R0 和标定常数,这里简化处理 sample.concentration = 0.0f; // 非阻塞入队,队列满则丢弃最旧数据 osMessageQueuePut(g_sample_queue, &sample, 0, 0); } osDelay(200); // 采样周期 200ms } } // 初始化采样任务和队列 void gas_sample_init(void) { g_sample_queue = osMessageQueueNew(QUEUE_SIZE, sizeof(gas_sample_t), NULL); osThreadAttr_t attr = { .name = "gas_sample", .stack_size = SAMPLE_TASK_STACK_SIZE, .priority = SAMPLE_TASK_PRIO, }; osThreadNew(gas_sample_task, NULL, &attr); }这段代码里,osMessageQueuePut的超时参数设为 0,表示不等待,队列满就直接丢。为什么这么设计?因为空气质量检测是持续采样场景,旧数据比新数据价值低,宁可丢旧数据也不要阻塞采样任务。采样周期 200ms 是一个折中:太快了传感器响应跟不上,太慢了会漏掉浓度突变。osDelay的单位是 tick,HarmonyOS 轻量系统默认 1 tick 等于 1ms,具体以系统配置为准。
3.2 串口协议设计:让上位机看懂五种气体数据
采样数据要传给上位机或者网关,最直接的方式是串口。协议不用太复杂,但要有帧头、通道号、浓度值和校验,否则上位机解析时容易错位。下面是一个简单的帧格式示例。
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2 字节 | 固定 0xAA 0x55 |
| 通道号 | 1 字节 | 0 到 4 分别对应氨气、氢气、酒精、一氧化碳、甲烷 |
| 浓度值 | 4 字节 | float 小端序,单位 ppm |
| 校验和 | 1 字节 | 前面所有字节累加和取低 8 位 |
| 帧尾 | 1 字节 | 固定 0x0D |
// 打包一帧气体数据 void pack_gas_frame(unsigned char *buf, unsigned char channel, float ppm) { int idx = 0; buf[idx++] = 0xAA; buf[idx++] = 0x55; buf[idx++] = channel; // float 按字节拆开,小端序 unsigned char *p = (unsigned char *)&ppm; for (int i = 0; i < 4; i++) { buf[idx++] = p[i]; } unsigned char checksum = 0; for (int i = 0; i < idx; i++) { checksum += buf[i]; } buf[idx++] = checksum; buf[idx++] = 0x0D; // 最终 buf 长度为 9 字节 }上位机收到帧后,先找 0xAA 0x55,再读通道号和四个字节的 float,最后校验累加和。这个协议的好处是定长、好解析,坏处是没有重传机制。如果串口线受干扰丢了一帧,上位机只能等下一帧。对于空气质量检测这种慢变量场景,丢一两帧不影响趋势判断,所以够用。如果你要做报警联动,建议在应用层加一个序号字段,方便判断是否丢帧。
3.3 数据上报前的滑动平均滤波
半导体传感器输出天生带噪声,直接上报原始浓度,上位机曲线会像心电图。我一般会在上报前做一次滑动平均,窗口大小取 5 到 10 个采样点。窗口太小滤波效果差,窗口太大响应延迟明显。下面是一个固定窗口的滑动平均实现。
#define FILTER_WINDOW 8 typedef struct { float buf[FILTER_WINDOW]; int index; int count; float sum; } moving_avg_t; // 初始化滤波器 void moving_avg_init(moving_avg_t *f) { f->index = 0; f->count = 0; f->sum = 0.0f; } // 输入新值,返回滤波后的值 float moving_avg_update(moving_avg_t *f, float value) { if (f->count < FILTER_WINDOW) { f->buf[f->index] = value; f->sum += value; f->count++; } else { // 减去最旧的值,加上最新的值 f->sum -= f->buf[f->index]; f->buf[f->index] = value; f->sum += value; } f->index = (f->index + 1) % FILTER_WINDOW; return f->sum / f->count; }这个滤波器的参数只有一个:窗口大小。8 个点、200ms 采样周期,相当于 1.6 秒的平滑窗口,对空气质量这种缓变信号足够。如果你检测的是酒精喷雾这种突变场景,窗口要调小到 3 到 4,否则峰值会被削平。滤波后的值再打包上报,曲线会干净很多。
4. 避坑与排查:气体检测项目里最容易翻车的五个点
4.1 预热时间不够,上电就报警
现象:板子刚上电,氨气通道浓度直接飙到几百 ppm,触发报警,过几分钟自己又降下来。
原因:半导体传感器冷态电阻很低,分压后 ADC 读到的电压偏高,换算出来的浓度自然虚高。这不是传感器坏了,是物理特性。
解决:在应用层加一个预热状态机。上电后前 180 秒标记为预热期,数据只采集不上报,或者上报时带一个预热标志位。预热结束后再进入正常检测模式。预热时间具体看传感器型号,我一般留 3 到 5 分钟余量。
4.2 参考电压用错,所有通道浓度整体偏移
现象:五个通道的浓度值都比实际值高出一大截,或者低得离谱,但趋势是对的。
原因:ADC 换算时用了 3.3V 参考电压,实际板子的 ADC 参考是 1.8V,导致电压算出来偏大,浓度跟着偏大。
解决:拿万用表量一下 ADC 引脚在已知输入下的电压,反推实际参考电压。或者直接看板子原理图,确认 ADC 参考来源。KHDVK-3861 不同底板设计可能有差异,不要照搬网上的代码。
4.3 传感器交叉响应导致误判
现象:喷酒精消毒,一氧化碳通道也跟着涨;或者氨气通道对香烟烟雾很敏感。
原因:半导体传感器没有选择性,对多种还原性气体都有响应。这是原理决定的,不是代码问题。
解决:两个方向。一是做通道间比值判断,比如酒精通道涨幅远大于一氧化碳通道,就判定为酒精事件;二是如果项目要求高,换用电化学传感器或者增加温湿度补偿。对于空气质量趋势检测,交叉响应可以接受,但报警阈值要留足余量。
4.4 串口丢帧导致上位机曲线断点
现象:上位机收到的数据曲线偶尔缺一段,或者解析出乱码。
原因:串口没有硬件流控,采样任务和上报任务同时写串口,或者波特率太高线材质量差。
解决:第一,串口发送加互斥锁,保证一帧数据完整发出;第二,波特率降到 115200 或更低;第三,上位机解析时加超时重置,收到帧头后一定时间内没收到帧尾就丢弃缓冲区重新找帧头。
4.5 电源纹波干扰 ADC 读数
现象:浓度值在小范围内高频跳动,滤波也压不住。
原因:传感器加热丝电流较大,和 ADC 共用电源时,加热丝通断会在电源上产生纹波,串到 ADC 参考上。
解决:传感器加热丝供电和 ADC 参考供电分开走线,中间加 LC 滤波。如果板子已经打样了,可以在软件上把 ADC 采样时刻避开加热丝通断瞬间,或者增加采样次数取中位数。硬件问题软件只能缓解,下一版记得改电源。
5. 用温度补偿把浓度精度再提一档
半导体传感器的 Rs/R0 曲线会随环境温度漂移,夏天和冬天同一浓度的读数能差百分之十几。如果你的项目部署在室外或者温差大的车间,不做温度补偿,数据只能看趋势,不能看绝对值。补偿的思路不复杂:在传感器旁边加一颗数字温度传感器,比如 I2C 接口的,读到的温度用来修正 R0 或者直接修正浓度值。
具体做法是:在标定阶段,记录不同温度下的 R0,拟合一条 R0 随温度变化的曲线。运行时根据当前温度查表或代入曲线,得到修正后的 R0,再参与浓度换算。下面是一个简化的温度补偿系数表。
| 温度区间 | R0 修正系数 |
|---|---|
| 0 到 10 度 | 1.15 |
| 10 到 20 度 | 1.08 |
| 20 到 30 度 | 1.00 |
| 30 到 40 度 | 0.93 |
| 40 度以上 | 0.87 |
这张表只是示例,实际系数必须用你的传感器在恒温箱里实测。没有恒温箱的话,至少在不同季节的典型温度下各标定一次,取几个点做线性插值,也比不补偿强。
验证补偿效果的方法:把板子和一个手持式标准检测仪放在同一个密闭空间里,人为制造浓度变化(比如喷一点酒精),对比两条曲线。如果补偿后两条曲线的跟随度明显提高,说明系数方向对了。如果反而更差,检查温度传感器是不是被加热丝烤热了,读到的不是环境温度。
我自己的习惯是,每做一个气体检测项目,都会在代码里留一个调试串口命令,可以手动开关温度补偿、修改 R0 和标定常数,不用重新烧录就能现场调参。这个习惯帮我省了很多来回跑现场的时间。希望帮到你。
本文还有配套的精品资源,点击获取