
简介本资源是一套基于HarmonyOS与KHDVK-3861开发板实现的多气体空气质量检测项目源码面向嵌入式开发者、物联网初学者及高校课程实践者解决挥发性有机气体氨气、氢气、酒精、一氧化碳、甲烷等与CO₂/TVOC综合监测的软硬件协同开发问题。压缩包含37个文件涵盖14个C源文件如TP_401PW.c、CCS811.c、oled_demo.c、10个头文件含传感器驱动与协议定义、7个GN构建脚本支撑HiLink组件化编译、3张界面/硬件示意图PNG以及README.md和配置JSON等总大小1.84MB。已有124人学习下载项目结构清晰分层Bluetooth模块实现手机端蓝牙通信NFC支持APP唤醒与数据显示OLED驱动完成本地实时可视化TP-401PW与CCS811分别承担多气体等级判定与精准VOCs参数采集hilink_3861_Air作为可复用的工程级编译组件便于快速集成到HarmonyOS IoT解决方案中。 手头正好在做一块基于 HarmonyOS 的智能家居环境监测节点核心板用的是 KHDVK-3861也就是海思 Hi3861V100 那颗 Wi-Fi SoC 的开发板。项目本身不算复杂但把气体检测、HarmonyOS 分布式能力、端侧数据处理串起来之后能玩出的花样其实挺多的。这篇就完整复盘一下整个空气质量检测项目的设计思路、硬件选型、代码实现和踩坑记录源代码层面的关键逻辑都会拆开讲方便直接参考复现。1. 项目出发点和整体设计思路1.1 为什么选 KHDVK-3861 这块板子先说说为什么要用 KHDVK-3861 而不是 ESP32 或者 STM32。这颗 Hi3861V100 是海思面向物联网市场推的 RISC-V 架构 MCU亮点在于它原生支持 HarmonyOS 的 LiteOS-M 内核和分布式软总线。这意味着节点采集到的数据不仅可以本地显示还能通过 HarmonyOS 的组网能力直接同步到手机、手表或者其他搭载 HarmonyOS 的设备上不需要自己折腾 MQTT 服务器和 App 端的通信协议。另外一个原因就是开发板的外设接口很齐全。KHDVK-3861 上面引出了 2 路 UART、6 路 PWM、7 路 ADC、2 路 I2C 和 3 路 SPI足够同时挂多个气体传感器、一个温湿度传感器和一块 OLED 屏。板载的 WiFi 模块工作在 2.4GHz 频段支持 802.11 b/g/n做室内环境监测节点完全没有瓶颈。电源设计上这块板子也省心官方支持 5V Micro-USB 供电板上已经做了 3.3V 和 5V 的电源稳压电路。MQ 系列传感器大部分需要 5V 加热电压如果你选的是 3.3V 供电的传感器模组直接引板载 3.3V 也行。不过要注意MQ 系列传感器加热丝工作电流普遍在 150mA 上下如果用 3.3V 供电模组建议独立供电避免和 MCU 抢电流导致 ADC 参考电压抖动。提醒一句Hi3861 的 ADC 输入范围是 0~1.9V 左右超过这个电压需要分压电阻网络否则会烧 ADC 引脚。我在项目里就用两个 10k 电阻做了分压把传感器输出的 0~4V 模拟信号缩到 0~2V 以内。1.2 气体传感器选型逻辑MQ 系列怎么挑空气质量检测的核心是气体传感器选型。这个项目需要检测氨气、氢气、酒精、一氧化碳、甲烷这几类挥发气体实际对应的传感器方案如下氨气、甲烷、酒精、烟雾等混合气体用 MQ-135这是目前最常用的空气质量监测传感器对 NH3、NOx、酒精、苯、烟雾都有响应成本低、响应快。一氧化碳用 MQ-7专门针对 CO 检测设计内部有两级加热电路1.5V 和 5V 交替用来区分 CO 和氢气的干扰。氢气用 MQ-8对 H2 灵敏度高同时对酒精、甲烷等有较好的选择性抑制。酒精严格来说 MQ-3 是专用酒精传感器但 MQ-135 对酒精也有响应如果预算充足并且要单独展示酒精浓度可以加一路 MQ-3。选型的时候要理解一个关键点MQ 系列传感器本质上是电阻型气敏元件它的输出不是线性电压而是电导率随气体浓度变化。传感器模组上一般会集成一个比较器和一个可调电位器输出的数字量TTL 高低电平用来做阈值告警很方便但要做浓度标定就必须读模拟量输出AO 引脚。如果只是做演示 Demo直接读 AO 引脚映射成 0~100 的“空气污染指数”就行。但如果你要做相对准确的 PPM 浓度显示就得查传感器 datasheet 里的灵敏度特性曲线做线性回归或者分段拟合。这个后面在代码实现部分会详细讲。1.3 整体系统架构从传感器到 HarmonyOS 应用整个系统的信息流是这样的MQ传感器阵列5V供电 ↓ 模拟电压信号 分压电阻网络10k10k缩到0~2V ↓ Hi3861 ADC通道GPIO4/5/6/7等 ↓ 软件滤波 标定换算 浓度值 / 污染指数 ↓ OLED屏幕实时显示I2C接口 ↓ HarmonyOS 分布式软总线 ↓ 手机/平板上的超级终端查看数据这套架构的妙处在于数据链路端到端都是 HarmomyOS 原生的。你不需要在 MCU 端写 TCP/IP socket也不需要配 MQTT broker只要把传感器数据封装成分布式数据服务打开手机上的“超级终端”就能看到。这在家庭场景里非常实用相当于天然自带 App不用额外开发。软件部分我拆成了两个大块底层驱动层包括 MQ 系列传感器的 ADC 采样、软件滤波、浓度换算、阈值告警状态机。应用交互层包括 OLED 显示驱动、分布式数据发布、本地按键配置校准。接下来一个一个过。2. 硬件搭建与传感器接线细节2.1 完整硬件清单先列一下我自己实际用到的物料都是常见模块采购没什么门槛KHDVK-3861 开发板Hi3861V100 芯片MQ-135 模组 ×1氨气、甲烷、酒精、烟雾MQ-7 模组 ×1一氧化碳MQ-8 模组 ×1氢气0.96 寸 SSD1306 OLED 屏 ×110k 电阻 ×6做三路分压面包板 杜邦线若干5V/2A 电源适配器或者直接用电脑 USB 口但建议适配器接线表如下传感器/模块引脚连接到 KHDVK-3861MQ-135 AO模拟输出GPIO4ADC0MQ-7 AO模拟输出GPIO5ADC1MQ-8 AO模拟输出GPIO6ADC2OLED SDAI2C数据GPIO8OLED SCLI2C时钟GPIO9所有传感器 VCC电源5V所有传感器 GND地GNDOLED VCC电源3.3V需要特别说明上面的 GPIO 编号是按我用的 OpenHarmony 3.0 版本映射来的不同固件版本对 GPIO 的 ADC 复用配置可能有差异烧录前先在官方文档里确认一下对应关系。实际调试时我因为 GPIO 编号没对上一度以为 ADC 坏了最后发现是复用配置写错了。2.2 MQ 传感器工作原理为什么上电要先等几分钟MQ 系列传感器的核心是一个二氧化锡SnO2半导体气敏层。在洁净空气中SnO2 表面吸附的氧分子会捕获电子形成势垒使传感器电阻较高。当还原性气体比如氨气、氢气、CO接触到传感器表面时会与吸附氧发生反应释放电子导致传感器电导率上升。简单说就是气体浓度越高传感器电阻越低输出模拟电压越高。这里有个新手很容易忽略的点MQ 传感器内部有一根加热丝持续对 SnO2 陶瓷基片加热到 200~300 摄氏度的工作温度气体分子才能与表面吸附氧发生反应。上电瞬间加热丝从室温升到工作温度需要时间而且 SnO2 表面的氧吸附平衡也需要时间稳定。所以全新传感器第一次上电一定要先预热 12~24 小时之后每次冷启动至少等 3~5 分钟再读取数据否则读数会严重漂移。我实测过 MQ-135冷启动后 1 分钟时读到的电压是 2.8V10 分钟后稳定在 1.5V 左右。如果你不考虑预热时间就直接做阈值告警大概率上电就误报而且搜不到原因。2.3 分压电阻网络别让 ADC 烧坏KHDVK-3861 的 ADC 参考电压是 1.9V也就是说 ADC 引脚的输入电压不能超过 1.9V。MQ 传感器模组的 AO 引脚最大输出电压通常在 4V 左右5V 供电时所以不能直接接。我的做法是用两个 10k 电阻组成分压Vout Vin × R2 / (R1 R2) Vout Vin × 10 / (10 10) Vin / 2这样传感器输出 4V 时ADC 引脚实际收到 2V略微超过 1.9V还有一点风险。更稳妥的做法是把 R2 换成 9.1k分压比变成 9.1/(109.1) ≈ 0.4774V 输入时输出约 1.91V。或者直接用 3.3V 给传感器模组供电这样输出范围天然被限制在 3.3V 以内分压后 1.65V安全得多。但要注意MQ 传感器加热丝如果供电不足传感器的工作温度就达不到响应速度和灵敏度都会下降。MQ-135 模组上一般有两个电源引脚一个是加热丝电源H 引脚一个是信号电路电源VCC。有些模组把两个电源分开引出来这时候可以把加热丝接 5V信号电路接 3.3V兼顾安全性和性能。3. 软件工程与核心代码实现3.1 HarmonyOS 工程结构与必要配置开发环境我用的是 DeviceOpenStudio也就是现在主流的 HarmonyOS 设备端开发 IDE。新建工程时选择 “Smart Vision Kit” 模板或者空工程模板都可以关键是要配置好 chip 类型和 target。工程里最核心的目录结构如下. ├── BUILD.gn ├── src/ │ ├── app/ │ │ ├── BUILD.gn │ │ ├── air_quality.c // 主逻辑 │ │ ├── adc_reader.c // ADC读取 │ │ ├── gas_sensor.c // 传感器换算 │ │ ├── oled_display.c // OLED显示 │ │ └── sensor_publish.c // 分布式发布 │ ├── module.json │ └── ohos.build └── vendor/ └── hisilicon/ └── hi3861/ // 板级支持包BUILD.gn 里要引入 ADC、I2C 和分布式数据管理相关的依赖库import(//build/lite/config/component/lite_component.gni) lite_component(app) { features [ //src/app:air_quality, ] }这个工程在编译的时候会自动链接 LiteOS 内核和 HarmonyOS 的硬件抽象层。第一次编译时间会有点久耐心等就行。3.2 ADC 数据读取Hi3861 的 ADC 接口用法Hi3861 的 ADC 读取接口封装在iot_adc.h里核心流程是开启 ADC 通道 → 设置采样次数 → 读取原始值 → 转换为电压值。#include iot_adc.h #include iot_gpio.h #define ADC_CHANNEL_0 0 // GPIO4 对应 ADC0 #define ADC_CHANNEL_1 1 // GPIO5 对应 ADC1 #define ADC_CHANNEL_2 2 // GPIO6 对应 ADC2 float get_adc_voltage(uint8_t channel) { uint16_t raw_value 0; // 初始化ADC通道采样次数设为6次取平均值更稳 unsigned int ret IoTAdcRead(channel, raw_value, IoT_ADC_CHANNEL_6, IoT_ADC_WIDTH_12_BIT, IoT_ADC_ATTEN_4_DB); if (ret ! 0) { printf(ADC read failed, channel%d, ret%d\n, channel, ret); return -1.0f; } // 12位ADC参考电压1.9V注意这是分压后的电压 float voltage raw_value * 1.9f / 4095.0f; // 还原为传感器实际输出电压除以分压系数0.5 float sensor_voltage voltage / 0.5f; return sensor_voltage; }这里有几个参数需要解释一下IoT_ADC_CHANNEL_6是采样次数配置不是采样通道编号。采样次数越多内部硬件平均效果越好但单次读取耗时也越长。6 次是个比较平衡的取值。IoT_ADC_WIDTH_12_BIT是采样精度Hi3861 支持 12 位 ADC对应 0~4095 的整数。分辨率约 0.46mV完全够用。IoT_ADC_ATTEN_4_DB是衰减配置4dB 衰减对应的量程是 0~1.9V。软件层面我额外加了一层滑动平均滤波#define FILTER_SIZE 10 float sensor_reading_history[FILTER_SIZE]; int sensor_reading_index 0; float moving_average_filter(float new_value) { static float sum 0.0f; sum - sensor_reading_history[sensor_reading_index]; sum new_value; sensor_reading_history[sensor_reading_index] new_value; sensor_reading_index (sensor_reading_index 1) % FILTER_SIZE; return sum / FILTER_SIZE; }注意滑动平均滤波的本质是让历史数据平滑过渡所以温度、浓度这类缓变信号用这种滤波效果很好。但如果你后面做的是气体泄漏报警希望响应快一点就要把滤波窗口调小或者直接用采样瞬间的原始值做门槛判断这个在需求设计阶段就要想清楚不能既要平滑又要灵敏。3.3 气体浓度换算从电压到 PPM 的工程化处理这是整个项目里最容易踩坑、也最能体现水平的部分。MQ 传感器在 datasheet 里会给一条灵敏度特性曲线横坐标是气体浓度PPM纵坐标是 Rs/R0。Rs 是传感器当前电阻R0 是在洁净空气中的基准电阻。这条曲线在双对数坐标下近似是一条直线所以要换算浓度得用对数关系log(Rs/R0) m × log(ppm) b ppm 10 ^ ((log(Rs/R0) - b) / m)实际代码里Rs 是通过分压测出来的Rs (Vc - Vout) / Vout × RL其中 Vc 是传感器供电电压Vout 是 AO 引脚输出电压RL 是模组上的负载电阻。我在实现里参照 MQ-135 的典型特性参数做了拟合大约是#define VC_VOLTAGE 5.0f // 传感器供电电压 #define RL_RESISTANCE 10.0f // 模组负载电阻kΩ #define R0_AIR_VALUE 10.0f // 洁净空气下的基准值 float rs (VC_VOLTAGE - sensor_voltage) / sensor_voltage * RL_RESISTANCE; float ratio rs / R0_AIR_VALUE; // MQ-135 对 氨气 的近似拟合参数 float ppm_nh3 powf(10.0f, (logf(ratio) - (-1.102f)) / 0.606f);这几个拟合参数我是从 datasheet 灵敏度曲线图上取点拟合出来的精度只能说够做趋势参考不能说绝对准确。如果有条件用标准气体做多点标定一定要自己重新拟合。关于 R0 的标定一个实用的做法是在通风良好的户外环境中让传感器稳定运行 30 分钟以上记录此时的 Rs 值作为 R0。注意室内湿度、温度会影响 R0所以至少在不同天气条件下多做几次取平均。3.4 阈值告警与状态机什么时候该报警气体检测项目光把数据显示出来是不够的告警逻辑才是真正体现产品化思维的地方。我设计了一个简单的三级状态机typedef enum { STATE_NORMAL 0, // 正常 STATE_WARNING, // 警告浓度偏高提示开窗 STATE_ALARM // 报警浓度超过安全值声光报警 } AirQualityState; typedef struct { float nh3_ppm; float co_ppm; float h2_ppm; float pollution_index; } AirQualityData; AirQualityState check_alarm_state(AirQualityData *data) { int warning_count 0; int alarm_count 0; // 单气体持续超标也会触发报警避免多气体互相掩盖 if (data-co_ppm 50.0f) alarm_count; // CO 安全阈值 if (data-pollution_index 80.0f) alarm_count; if (data-nh3_ppm 25.0f) alarm_count; if (alarm_count 0) return STATE_ALARM; if (data-pollution_index 50.0f) warning_count; if (data-h2_ppm 100.0f) warning_count; if (warning_count 0) return STATE_WARNING; return STATE_NORMAL; }这里有个重要的工程细节告警要带迟滞hysteresis否则气体浓度在阈值附近抖动时告警会反复触发屏幕闪烁、蜂鸣器响一阵停一阵体验非常差。迟滞逻辑就是进入告警状态的触发阈值是 50但是解除告警状态的恢复阈值要低一些比如 35。代码里这样处理AirQualityState current_state STATE_NORMAL; AirQualityState update_state_with_hysteresis(float value) { switch (current_state) { case STATE_NORMAL: if (value 50.0f) { current_state STATE_WARNING; printf(Enter WARNING state\n); } break; case STATE_WARNING: if (value 80.0f) { current_state STATE_ALARM; printf(Enter ALARM state\n); } else if (value 35.0f) { current_state STATE_NORMAL; printf(Back to NORMAL state\n); } break; case STATE_ALARM: if (value 50.0f) { current_state STATE_WARNING; printf(Back to WARNING state\n); } break; default: break; } return current_state; }3.5 UI 显示与数据上报本地可视化和分布式发布本地显示我用的是 SSD1306 OLED走 I2C 接口驱动逻辑网上有很多现成代码核心是往显存写像素数据然后刷新屏幕。为了适配空气质量展示我写了一个简单的仪表盘布局第一行显示当前空气质量指数0~100数字越大越差。第二行显示 NH3、CO、H2 的浓度值。第三行显示当前状态Normal / Warning / Alarm。刷新频率不需要太高1 秒刷一次完全够OLED 本身刷新太快还会闪烁。注意 I2C 总线是共享的如果你的系统里还有别的 I2C 设备比如温湿度传感器 SHT30读写的时候注意地址不能冲突。分布式数据发布这块是 HarmonyOS 的特色功能。在 Hi3861 上发布数据核心是调用分布式数据管理接口#include distributed_data_mgr.h void publish_air_quality_data(float nh3, float co, float h2, int level) { // 封装数据到 key-value 结构 const char* keys[] {nh3_ppm, co_ppm, h2_ppm, quality_level}; char values[4][16]; snprintf(values[0], sizeof(values[0]), %.1f, nh3); snprintf(values[1], sizeof(values[1]), %.1f, co); snprintf(values[2], sizeof(values[2]), %.1f, h2); snprintf(values[3], sizeof(values[3]), %d, level); // 发布到指定数据包 DistributedDataMgrPut(air_quality, keys[0], values[0]); DistributedDataMgrPut(air_quality, keys[1], values[1]); DistributedDataMgrPut(air_quality, keys[2], values[2]); DistributedDataMgrPut(air_quality, keys[3], values[3]); }设备端发布之后手机上的“超级终端”或者鸿蒙应用侧通过同样的分布式数据接口就能主动拉取不需要额外开发服务端。这也是这个项目最出彩的地方一套代码搞定设备端和端侧交互。4. 常见问题与排查技巧实录4.1 传感器读数漂移严重排查这五个点传感器读数漂移是我在整个调试过程中遇到最多的问题原因不外乎这几个预热时间不足。冷启动前 5 分钟内读数完全没有参考价值严重的还会触发误报警。供电不稳。如果传感器和 MCU 共用 USB 口供电电机、WiFi 模块等大功率外设工作时会产生电压跌落直接影响传感器加热温度进而影响读数。R0 基准值漂移。随着传感器老化R0 会缓慢变化一般每 3~6 个月需要重新标定一次。环境湿度突变。SnO2 传感器对湿度有一定的交叉敏感湿度大幅变化时读数会跟着波动。这个问题很难完全消除只能尽量在算法里加入湿度补偿。传感器被污染。在厨房、吸烟室等油烟环境中传感器表面可能被油污覆盖导致响应退化这种情况只能更换传感器。如果读数稳定但不准确优先怀疑标定问题重新做一次零点校准。如果读数剧烈跳动优先查供电和接地。4.2 ADC 读数一直是 0 或满量程八成是分压或复用问题ADC 读数异常是最让人头大的问题我排查了大半天最后发现是 GPIO 复用配置没写对。Hi3861 的引脚是支持功能复用的用 ADC 之前要先把对应的 IO 配置成 ADC 模式IoTGpioInit(4); IoSetFunc(4, 0); // 根据芯片手册设置GPIO4为ADC功能 IoTGpioSetDir(4, IOT_GPIO_DIR_IN);另外分压电路如果没有共地ADC 读取的电压就是悬浮的读数会随机跳。检查一下传感器模组的 GND 和开发板的 GND 是不是接在同一根地线上如果没有共地其他环节再对也没用。4.3 编译烧录失败别忽略依赖库版本HarmonyOS 设备端开发一个比较烦的问题就是 SDK 版本和依赖库版本容易对不上。我最初用的是 OpenHarmony 2.2 的 SDK编译的时候一直报分布式数据管理相关的头文件缺失后来换成 3.0 版本才解决。编译失败时优先看ohos.build文件里的依赖项声明。如果报的是distributed_data_mgr.h找不到检查一下deps里有没有加上//foundation/distributeddatamgr:distributed_data_mgr。这类问题本质上都是工程配置问题不是代码逻辑问题排查方向不要搞错。另外烧录的时候注意串口号选择Windows 下如果驱动没装好设备管理器里可能显示为未知设备。Hi3861 的烧录工具是海思的 HiBurn波特率选 921600烧录前按住开发板上的复位键进入烧录模式不同板子进入方式略有差异。4.4 长时间运行的稳定性优化这个项目如果要做成长期部署的节点稳定性优化就很重要了。我实测下来有几个要注意的点WiFi 重连机制。Hi3861 在路由器重启或者 WiFi 信号波动后可能不会自动重连。需要在主循环里定期检查 WiFi 连接状态断了就主动重连。看门狗。LiteOS 里可以启用硬件看门狗防止死循环导致系统卡死。正常运行时定时喂狗超过一定时间没喂就触发复位。Flash 磨损。如果频繁往 Flash 里写校准数据会加速 Flash 老化。建议只在标定完成后写一次平时只读不写。传感器功耗控制。如果做电池供电MQ 传感器的加热功耗是个大头不能一直开启。可以考虑间歇式工作模式每 10 分钟给传感器上电一次稳定 30 秒后采样然后断电进入低功耗状态。我自己的部署方案是插电运行重点在 WiFi 重连和看门狗上做了处理。目前连续运行了一个多月没出过问题读数稳定手机端拉数据也一直正常。5. 一些个人心得与扩展方向这个项目做完之后我对端侧开发和设备端开发的差异有了更深的体会。设备端代码跑在资源受限的 MCU 上内存、CPU、功耗都是紧约束。比如滑动平均滤波这个最简单的算法在 PC 上写就是几行代码的事但在设备端要考虑历史数组的大小、中断采样时机、主循环的周期是否够用。另外一个深刻的感受是硬件项目的“可复现性”比纯软件项目差得多。同样的代码换一块板子、换一个传感器批次表现可能就不一样。所以代码里要尽量把参数集中在头文件里定义方便调整标定过程也要写清楚最好代码里预留命令行的标定入口方便现场操作。如果后续要扩展这个项目的接口已经留好了几个方向加 SHT30 温湿度传感器完善环境数据面板同时给气体传感器做温湿度补偿。加 SSD1306 之外的 HTTP 上报能力把数据同步到云平台做成可通过云端查看的历史曲线。HarmonyOS 的 Wi-Fi 接口已经帮我封装好了底层协议直接调 HTTP 客户端 API 就行。加蜂鸣器和 RGB 灯做更直观的声光提示配合阈值状态机使用效果更好。把多块 KHDVK-3861 节点组网分布在不同房间利用 HarmonyOS 分布式能力实现跨房间数据汇总展示。最后再分享一个小技巧调试 MQ 传感器的时候不要盯着单个传感器的绝对读数而是看相对变化趋势。比如你点一根火柴靠近 MQ-135读数应该迅速跃升拿开之后逐渐回落。用这种方式验证功能是否正常比纠结绝对精度快得多也直观得多。真正要追求精度的时候再回到标准气体标定这个环节一步一个脚印来。本文还有配套的精品资源点击获取