简介:这套小熊派多传感器采集工程面向物联网入门者与嵌入式开发者,帮助解决浊度、溶解性总固体、酸碱度及温湿度四类参数的读取与数据解析问题,适合环境监测、水质检测类项目快速起步。压缩包共包含两千个文件,以C语言源码与头文件为主体,另有超文本说明页面、Python辅助脚本及Markdown文档,覆盖驱动配置、数据转换和通信示例的完整代码结构,整体大小十六兆左右,便于查阅与移植。工程已有156人学习,可结合配套文档直接搭建可运行的环境监测原型。资源中提供了传感器初始化、模拟量采集、数值标定及数据上传等关键模块,并附带相关脚本与说明,可帮助读者理解从硬件信号到应用数据的完整链路,二次开发也较为方便。
1. 用鸿蒙小熊派读浊度、TDS、pH 和 DHT11,先明白谁在给你添乱
手里的小熊派如果还只点过灯、跑过串口打印,把这个标题拆开看,会发现真正的题目是:让一块板子同时喂饱四路传感器,其中三路必须走 ADC。浊度、TDS、pH 这三支探头输出的都是模拟电压,电压值随介质变化,但电压不等于你要的 NTU、ppm 和 pH 数值;DHT11 虽然是数字传感器,走的却是严格依赖微秒级定时的单总线。所以下面按“先解决模拟量,再补温度补偿,最后调 DHT11 时序”的顺序,把鸿蒙小熊派读取这四路数据的最小闭环讲清楚。新手照步骤能跑通,老手看参数和坑点也能少试错几轮。
2. 浊度、TDS、pH 和 DHT11 的信号类型与接线,决定你后续怎么调
2.1 先按输出类型把四路传感器分成两组
浊度模块内部是红外 LED 加光电池,水越浑,透过的光越少,光电池输出电压跟着变;TDS 探头靠持续的电导测量,模块把探针间的电阻变化换成模拟电压;pH 电极输出的是高阻毫伏信号,模块负责放大并整定到 ADC 可读的范围。这三路本质上都是“电压随被测对象变化”,而且不是线性关系,必须在软件里做二次换算和温度修正。
DHT11 则是纯数字单总线,数据线被主机拉低再释放后,传感器自己用一串微秒级高低电平把 40 位数据吐出来。它不需要 ADC,但要求 GPIO 方向和延时都非常精确。
| 传感器 | 输出类型 | 典型输出范围 | 供电 | 信号调理重点 |
|---|---|---|---|---|
| 浊度 | 模拟电压 | 0~3.3V 或 0~5V | 3.3~5V | 电压与 NTU 非线性,需查表或拟合 |
| TDS 探头 | 模拟电压 | 0~3.3V 或 0~5V | 3.3~5V | 温度系数约 0.02/℃,TDS 由 EC 换算 |
| pH 电极 | 模拟毫伏 | 模块输出 0~3.3V | 5V | 高阻输入,pH7 对应中点电压 |
| DHT11 | 单总线数字 | 0/1 电平 | 3.3~5V | 微秒级时序,必须做校验和校验 |
把四路传感器分成“三路模拟 + 一路单总线”之后,后续所有问题都落在两个地方:ADC 通道上的电压稳不稳,以及温度变化时换算公式里的系数修不修。
2.2 小熊派 ADC 通道与 GPIO 的推荐接线
鸿蒙小熊派的 ADC 通道数量有限,常见的版本上并不是所有 GPIO 都能做模拟采样。我的习惯是把三路模拟量放在 ADC0、ADC1、ADC2,DHT11 放在一个空闲 GPIO,避免与 I2C、UART 调试口抢占。
| 功能 | 推荐引脚 | 说明 |
|---|---|---|
| 浊度 | ADC0 / GPIO05 | 尽量远离 WiFi 天线,电源噪声小 |
| TDS | ADC1 / GPIO06 | 与浊度分时采样,通道间隔至少 20ms |
| pH | ADC2 / GPIO07 | 高阻探头,采样端并联 0.1uF 陶瓷电容 |
| DHT11 | GPIO08 | 任意普通 GPIO,数据线接 4.7k 上拉到 3.3V |
代码里把这些映射做成宏,后面采样和调参只改一处:
#define TURBIDITY_ADC_CHANNEL ADC_0 #define TDS_ADC_CHANNEL ADC_1 #define PH_ADC_CHANNEL ADC_2 #define DHT11_GPIO IOT_GPIO_IO_08注意这里ADC_0到ADC_2是对应 SDK 里的AdcChannel枚举,不同版本的小熊派扩展板丝印可能有差异,接线前先用板卡原理图确认引脚复用关系。不要凭板子上的“A0”丝印直接接,很多扩展板的 A0 是经过分压、滤波之后的信号,已经不是探头原始电压。
2.3 供电与共地:模拟量漂移查不出原因的十个里有八个是地没共好
三块模拟探头尽量使用同一个电源域,传感器的 GND 必须和小熊派的 GND 直接相连。如果传感器用独立电源,负极没有共地,ADC 读到的不是传感器输出和板端地之间的电位差,而是浮动电压,数据会随机跳。
另一个常被忽略的点是上电时序。TDS 探头和 pH 电极都有极化过程,传感器上电后至少要等 30 秒再开始连续采样,否则前几百个点会缓慢漂移。如果你把小熊派和传感器同时上电,采集任务立刻执行,前面一段数据大概率是废的。解决办法是任务里加一个TaskMsleep(1000)的预热循环,或者把采样任务延时启动。
提示:如果模块输出 5V 电平,而小熊派 ADC 输入范围低于 5V,务必先看模块是否带板载调理电路,不要直接把 5V 输出接进 GPIO。
3. 用鸿蒙小熊派的 ADC 读三路模拟量:滤波和电压换算决定精度
3.1 HarmonyOS ADC 读取的最小可用代码
鸿蒙小熊派 SDK 里 ADC 的封装比较薄,本质上就是读寄存器,然后按配置的采样时间返回一个 12 位原始值。最小读取代码如下:
#include "iot_adc.h" #include "iot_gpio.h" static uint16_t ReadAdcValue(AdcChannel channel) { uint16_t data = 0; AdcRead(channel, &data, ADC_READ_TYPE_2_MS, 8); return data; }这里第一个参数是 ADC 通道枚举,第二个参数是输出原始值的指针,第三个参数ADC_READ_TYPE_2_MS表示采样窗口长度,最后一个8是防止模拟开关切换后电压还没稳定的延时档位。如果读到连续跳变,可以把它从 8 调到 16 或 32,但代价是单次采样耗时变长。
ADC 原始值不是电压,这点容易踩。小熊派上常见的 ADC 是 12 位,满量程 4096,下一步必须把原始值换算成毫伏。换算前先确认你手里板子的参考电压,有的版本是 1.8V,有的是 3.3V,不能看到别人代码写4096 * 3.3就照抄。
| 参数 | 建议值 | 为什么 |
|---|---|---|
| ADC 位数 | 12 bit | 原始值范围 0~4095 |
| 参考电压 Vref | 按板卡实际值 | 错误会导致全量程比例错误 |
| 单通道采样次数 | 10 | 兼顾速度与稳定 |
| 通道切换间隔 | ≥20ms | 等待模块输出端 RC 滤波建立 |
3.2 滑动平均滤波与突变数据剔除
模拟量采样不能只读一次,水泵运行、气泡经过、探头接触不良都会造成瞬时毛刺。工程上最省资源的是“去极值滑动平均”:连续采 10 次,去掉一个最大和一个最小,剩下 8 次求平均。
#define ADC_SAMPLE_NUM 10 static uint32_t FilterAdc(AdcChannel channel) { uint32_t sum = 0; uint16_t maxV = 0; uint16_t minV = 4095; for (int i = 0; i < ADC_SAMPLE_NUM; ++i) { uint16_t v = ReadAdcValue(channel); sum += v; if (v > maxV) maxV = v; if (v < minV) minV = v; } sum = sum - maxV - minV; return sum / (ADC_SAMPLE_NUM - 2); }这段代码的逻辑是先完整累加,再减掉一个最大值和一个最小值,最后除以 8。注意没有用排序,成本低,适合 RTOS 任务里高频调用。如果你要跟踪快速变化的浊度,可以把采样数降到 6,实时性更好,但抗毛刺能力弱一些;用在鱼缸、水塔这类慢变场景,10 次是稳妥值。
3.3 电压换算:参考电压和分压比必须同时写清楚
拿到滤波后的原始值,下一步转毫伏:
#define ADC_VREF_MV 1800 // 按板子实际改,单位 mV #define ADC_RES_MAX 4096 static uint32_t RawToMv(uint32_t raw) { return (raw * ADC_VREF_MV) / ADC_RES_MAX; }这个公式本身很简单,真正的风险在前置电路。如果传感器输出 0~3.3V,而板载 ADC 参考电压只有 1.8V,超过 1.8V 的部分会被削顶,pH 碱性段和 TDS 高浓度段全部读到 4095。这种情况下,先在模块输出端做电阻分压,比如串联 10k、并联 10k 把 3.3V 压低一半,软件再把采样电压乘 2 还原。分压比也要做成宏,别在公式里到处写魔法数字。
注意:分压电阻会降低输入阻抗,对高阻 pH 电极影响尤其明显。如果 pH 模块输出阻抗较高,优先选具备缓冲输出(运放跟随)的 pH 模块,再接分压电路。
4. 浊度传感器温度补偿公式与 TDS、pH、DHT11 的工程量换算
4.1 浊度传感器温度补偿公式与非线性查表
浊度模块给的是电压,不是 NTU。同一型号传感器,水温和光源衰减都会让电压漂移。常见做法是先用标准浊度液做一条“电压-浊度”标定表,再用线性插值;如果手里只有 25℃ 下标定好的数据,按一阶温度补偿公式折回来:
NTU = NTU25 / (1 + 0.02 * (T - 25))这里NTU25是 25℃ 下标定的浊度值,T是当前水温,0.02是水样本的通用浊度温度系数。温度越高,水体中颗粒对光的散射越强,所以高于 25℃ 时要把读数向下修正。
一组典型标定点如下:
| 电压(mV) | NTU |
|---|---|
| 2800 | 0 |
| 2200 | 50 |
| 1400 | 300 |
| 600 | 1000 |
插值代码可以写成这样:
static float TurbidityMvToNtu(uint32_t mv, float temp) { static const uint16_t cal_mv[4] = {600, 1400, 2200, 2800}; static const uint16_t cal_ntu[4] = {1000, 300, 50, 0}; float ntu = 0; if (mv >= cal_mv[3]) { ntu = cal_ntu[3]; } else if (mv <= cal_mv[0]) { ntu = cal_ntu[0]; } else { for (int i = 0; i < 3; ++i) { if (mv >= cal_mv[i] && mv <= cal_mv[i + 1]) { ntu = cal_ntu[i] + (float)(cal_ntu[i + 1] - cal_ntu[i]) * (float)(mv - cal_mv[i]) / (cal_mv[i + 1] - cal_mv[i]); break; } } } return ntu / (1.0f + 0.02f * (temp - 25.0f)); }这组标定点只演示插值逻辑,不同品牌模块差异很大,必须用 0 NTU 纯水和已知浊度标准液重新标定。插值比拟合一条二次多项式更稳,因为多项式在区间外容易反翘,线性插值最多只是斜率不连续。
4.2 TDS 的 0.02 温度系数和电极常数
TDS 本质是电导率的换算。大多数模块已经把探头电阻变成了电压,代码要做的是把电压转换成电导率 EC,再做温度补偿,最后乘电极系数。
电导率随温度升高而升高,室温标定的数据在夏天会偏高。最常见的一阶补偿式:
EC25 = ECt / (1 + 0.02 * (T - 25))其中ECt是当前温度下测到的电导率,EC25是折算到 25℃ 的值,0.02是淡水的经验温度系数。水越脏、离子浓度越高,系数越接近 0.024;淡水场景用 0.02 足够。
static float CalcTds(float mv, float temp, float mv_zero, float ec_per_mv) { float ec = (mv - mv_zero) * ec_per_mv; float ec25 = ec / (1.0f + 0.02f * (temp - 25.0f)); float tds = ec25 * 0.5f; return tds; }参数里mv_zero是电导率为零时模块输出的电压,ec_per_mv是模块的电压-电导率斜率,这两个值必须用标准液标定。0.5f是 TDS 与 EC 的经验换算系数,NaCl 溶液接近 0.5,混合盐溶液常见 0.6~0.7。如果不确定水样成分,用 0.64 起步,再用烘干称重法定标。
提示:TDS 探头是电化学器件,长时间直流激励会极化。模块内部如果已经做了交流激励,软件侧不要再给探头供电,否则寿命明显缩短。
4.3 pH:能斯特方程的逐点温度修正
pH 电极输出的毫伏值与 pH 值呈线性,但斜率是温度的函数。25℃ 下理论斜率为 59.16 mV/pH,温度每升高 1℃,斜率约增加 0.2%。很多只做常温校准的代码把斜率写死为 59.16,冬天和夏天的读值能差 0.3 个 pH。
简化后的能斯特修正公式:
slope = 59.16 * (T + 273.15) / 298.15 pH = 7.0 - (V - V7) / slope代码实现如下:
static float CalcPh(float mv, float temp, float ph7_mv) { double slope = 59.16 * (temp + 273.15) / 298.15; double ph = 7.0 - ((double)mv - ph7_mv) / slope; return (float)ph; }这里的ph7_mv是探头放进 pH 7.0 缓冲液后读取到的模块输出电压。按上面公式,pH7 时电压差为 0,pH 输出正好是 7.0;pH4 时电压差约为 +177mV,计算结果是 4.0。注意符号方向:如果ph7_mv没校准对,整个 pH 区间都会平移。
实际使用中,两点校准比一点校准可靠。先泡 pH7 缓冲液校准零点,再泡 pH4.0 或 pH10.0 缓冲液校准实际斜率。如果标定出来的斜率偏离 59.16 超过 10%,说明电极老化或内部参比液污染,靠软件修正补偿不了。
4.4 DHT11 温湿度传感器:单总线时序和校验
DHT11 的难点不在读,而在时序。规格书要求主机先把数据线拉低至少 18ms,再拉高 20~40us 等待响应;传感器回一个 80us 低电平和 80us 高电平,然后开始吐 40 位数据。每一位由 50us 低电平开始,高电平持续 26~28us 表示 0,持续 70us 表示 1。
判断 0 和 1 的阈值取 50us:高电平超过 50us 判 1,低于 50us 判 0。最后没有校验位的数据直接丢弃,防止传输错误污染温度补偿。
| DHT11 信号阶段 | 时间要求 | 常见失败现象 |
|---|---|---|
| 起始拉低 | ≥18ms | 传感器不响应 |
| 等待响应低 | 约 80us | 一直读到高电平 |
| 等待响应高 | 约 80us | 一直读到低电平 |
| 高电平判 0/1 | 50us 阈值 | 全部读成 0 或全部读成 1 |
static int WaitPinLevel(IotGpioValue level, int us_max) { int cnt = 0; while (IoTGpioRead(DHT11_GPIO) != level) { hi_udelay(1); if (++cnt >= us_max) { return 0; } } return 1; } static int DHT11_Read(int8_t *temp, uint8_t *humi) { uint8_t bits[5] = {0}; uint16_t cnt = 0; IoTGpioSetDir(DHT11_GPIO, IOT_GPIO_DIR_OUT); IoTGpioWrite(DHT11_GPIO, IOT_GPIO_VALUE0); hi_udelay(20000); // 拉低 20ms,保证传感器识别起始信号 IoTGpioWrite(DHT11_GPIO, IOT_GPIO_VALUE1); hi_udelay(30); // 拉高 30us IoTGpioSetDir(DHT11_GPIO, IOT_GPIO_DIR_IN); if (!WaitPinLevel(IOT_GPIO_VALUE0, 100)) return -1; if (!WaitPinLevel(IOT_GPIO_VALUE1, 100)) return -1; for (int i = 0; i < 40; ++i) { if (!WaitPinLevel(IOT_GPIO_VALUE0, 100)) return -1; cnt = 0; while (IoTGpioRead(DHT11_GPIO) == IOT_GPIO_VALUE1 && cnt < 100) { cnt++; hi_udelay(1); } bits[i / 8] <<= 1; if (cnt > 30) bits[i / 8] |= 1; } if ((uint8_t)(bits[0] + bits[1] + bits[2] + bits[3]) != bits[4]) { return -1; } *humi = bits[0]; *temp = bits[2]; return 0; }这段代码里hi_udelay是微秒级忙等延时,适合在独立采集任务里使用;不要在定时器中断或需要严格实时响应的上下文里直接调用,20ms 的忙等会把任务调度卡住。DHT11 本体精度一般,温度 ±2℃、湿度 ±5%RH,测出来只适合做参考,不适合拿去修正对精度要求高的 TDS 和 pH 实验。
5. 联调顺序、校准方法和串口 JSON 输出:让四路数据经得起复测
5.1 先校 pH,再校 TDS,最后校浊度
四路传感器一起跑的时候,校准顺序不能乱。pH 和 TDS 都需要温度值参与计算,所以先用 DHT11 读数确认环境温度稳定;浊度的温度补偿同样依赖温度。我的顺序是:先把 DHT11 跑通,再校 pH,接下来校 TDS,最后校浊度。
pH 校准用 7.00 和 4.00 两种缓冲液,记录两个电压,算出实际斜率和零点。TDS 校准时先用 1413uS/cm 标准液标出ec_per_mv,再用纯水标mv_zero。浊度校准最直接:用 0NTU 纯水标最高电压点,用已知浊度标液标中段,电压低于高点太多时还要检查探头遮光帽是否漏光。
5.2 输出一个可解析的串口 JSON 帧
调试时不要只打印裸电压,把换算后的工程量和原始 ADC 值一起输出。这样上位机既能按 JSON 解析,也能在数据异常时回查原始采样值。
printf("{\"ntu\":%u,\"tds\":%.1f,\"ph\":%.2f,\"temp\":%d,\"humi\":%u}\r\n", ntu, tds, ph, temp, humi);这里temp和humi是 DHT11 读到的整数温度和百分比湿度,ntu是四舍五入后的浊度整数,tds和ph保留一位和两位小数。\r\n不能省,很多串口上位机按行解析,只发\n在某些终端里会粘帧。
5.3 用标准液和重复采样验证数据
判断数据可靠,不是看串口有没有数字输出,而是看三点:第一,pH 在缓冲液里 1 小时内漂移不超过 0.05;第二,TDS 在标准液里连续读 10 次,极差不超过平均值的 5%;第三,浊度在纯水里读数不跳动超过 5NTU。如果 ADC 原始值频繁出现在 0 或 4095,先查供电和分压,不是滤波参数的问题。
最后留一个调参技巧:把FilterAdc的采样次数降到 3,临时打印滤波前的原始值,观察跳变范围,再决定要不要加大中值剔除数量。这样能快速把“传感器本身噪声”和“电路接触不良”区分开,避免对着平均窗口调半天。
本文还有配套的精品资源,点击获取