
简介这份资源是面向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 是气体浓度ppma 和 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的单位是 tickHarmonyOS 轻量系统默认 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.1510 到 20 度1.0820 到 30 度1.0030 到 40 度0.9340 度以上0.87这张表只是示例实际系数必须用你的传感器在恒温箱里实测。没有恒温箱的话至少在不同季节的典型温度下各标定一次取几个点做线性插值也比不补偿强。验证补偿效果的方法把板子和一个手持式标准检测仪放在同一个密闭空间里人为制造浓度变化比如喷一点酒精对比两条曲线。如果补偿后两条曲线的跟随度明显提高说明系数方向对了。如果反而更差检查温度传感器是不是被加热丝烤热了读到的不是环境温度。我自己的习惯是每做一个气体检测项目都会在代码里留一个调试串口命令可以手动开关温度补偿、修改 R0 和标定常数不用重新烧录就能现场调参。这个习惯帮我省了很多来回跑现场的时间。希望帮到你。本文还有配套的精品资源点击获取