简介:面向物联网与环境监测方向的单片机开发者,尤其是使用51系列芯片做水质检测入门的初学者,这套代码资源围绕pH值、TDS总溶解固体与浊度三项关键指标,提供传感器驱动与数据采集的完整实现思路。压缩包共113个文件、约4.1MB,以C源码、汇编启动文件、Keil工程配置(uvproj、uvopt、m51)、HEX烧录文件及PDF传感器说明书为主,另含lst列表、obj目标文件与bak备份,便于直接编译验证与对照硬件接口。资源已有8553人学习下载,说明其在同类项目中具备较高参考价值。读者可从中获得pH电极信号AD转换与温度补偿、电导率换算TDS、光学散射法测浊度的代码框架,以及传感器接线、校准与滤波处理的实践参考,适合快速搭建水质检测原型并理解各模块的数据处理流程。
1. 水质检测三件套:PH、浊度、TDS 传感器代码到底能跑出什么
去年帮一个做水产养殖的朋友看塘口监测,他买回来三个探头——PH、浊度和 TDS,卖家只丢了一份引脚定义,代码全靠自己写。这类场景其实很典型:传感器硬件便宜、资料零散,真正卡住人的不是接线,而是三路模拟量怎么采样、怎么换算成有意义的物理量、怎么在同一个循环里不互相干扰。这份代码资源就是围绕这三个探头展开的,把 PH 的电压-酸碱度映射、浊度的电压-浑浊度换算、TDS 的电导率补偿都落到了可运行的函数里。适合谁?做毕业设计的学生、搞农业物联网原型的开发者、想给鱼缸或净水设备加监测的硬件爱好者。它不追求工业级精度,但把「能读、能算、能打印」这条链路走通了,拿过来改改就能接进自己的采集系统。
2. 三路传感器代码结构拆解:从引脚定义到物理量输出
2.1 目录与文件组织
这类传感器代码通常不会做成庞大的工程,常见做法是一个主程序文件加若干传感器驱动文件,或者干脆全部塞进一个.ino/.c文件里。我拿到的版本是 Arduino 风格的单文件结构,核心就三块:全局的引脚与标定常量、三个独立的读取函数、一个setup+loop的调度骨架。文件顶部先定义引脚:
// 传感器引脚定义,按实际接线修改 #define PH_PIN A0 // PH 模拟输出 #define TURB_PIN A1 // 浊度模拟输出 #define TDS_PIN A2 // TDS 模拟输出 // 标定参数,不同探头出厂值不同,必须实测修正 #define PH_OFFSET 0.0 // PH 零点偏移 #define PH_SLOPE 3.5 // 每单位 PH 对应的电压斜率 #define TURB_CLEAR 4.2 // 清水中浊度传感器电压 #define TDS_K 0.5 // TDS 换算系数引脚用宏定义而不是硬编码在函数里,好处是换板子时只改这一处。标定参数单独拎出来,是因为这三个探头几乎不存在「开箱即准」的情况,PH 探头尤其明显,同一批货零点能差出 0.3 个 PH。把标定值集中放在文件头,改起来不用满文件找魔数。
2.2 PH 读取:电压到酸碱度的线性映射
PH 探头的模拟输出和酸碱度是近似线性关系,代码里做的就是一次线性变换加中值滤波。直接读一次模拟量会有抖动,所以常见做法是连续采多次取中位数:
float readPH() { int samples[10]; // 连续采样 10 次,排序后取中位数,滤掉偶发尖峰 for (int i = 0; i < 10; i++) { samples[i] = analogRead(PH_PIN); delay(10); } // 简单冒泡排序 for (int i = 0; i < 9; i++) for (int j = i + 1; j < 10; j++) if (samples[i] > samples[j]) { int t = samples[i]; samples[i] = samples[j]; samples[j] = t; } int median = samples[5]; // 12 位 ADC、5V 参考,换算成电压 float voltage = median * 5.0 / 1023.0; // 线性映射到 PH 值,再叠加零点偏移 float ph = PH_SLOPE * voltage + PH_OFFSET; return ph; }analogRead返回 0~1023 的整数,5.0 / 1023.0是 10 位 ADC 在 5V 参考下的分辨率。PH_SLOPE和PH_OFFSET需要拿标准缓冲液实测:把探头放进 PH 4.00 和 PH 6.86 的缓冲液里各读一次电压,两点确定一条直线,斜率就是(6.86-4.00)/(V2-V1),偏移量反推。代码里给的 3.5 和 0.0 只是占位,直接烧进去读数一定偏。中位数滤波比平均值更抗单点干扰,探头附近有电机启停时尤其明显。
2.3 浊度读取:电压反比关系的处理
浊度传感器的特性和 PH 相反——水越浑,输出电压越低,所以换算时要做反向处理。代码里用清水电压作为基准,按比例折算:
float readTurbidity() { int raw = analogRead(TURB_PIN); float voltage = raw * 5.0 / 1023.0; // 电压低于清水基准时,按比例换算浊度,单位 NTU float ntu; if (voltage >= TURB_CLEAR) { ntu = 0.0; // 比清水还清,视为 0 } else { // 经验公式,系数需按探头手册或实测修正 ntu = (TURB_CLEAR - voltage) * 100.0; } return ntu; }TURB_CLEAR是探头在纯净水里的输出电压,实测时把探头洗净擦干、放进静置过的清水里读一次即可。(TURB_CLEAR - voltage) * 100.0里的 100 是经验系数,不同型号差异很大,有的探头手册会给标准曲线,那就按曲线拟合,别硬套这个线性式。浊度这块最容易翻车的地方是气泡:探头表面附着气泡时电压会突然掉一截,读数虚高,所以采样前最好让探头在水里静置几秒,或者加个简单的连续采样剔除突变值。
2.4 TDS 读取:温度补偿不能省
TDS 本质是测电导率再换算,而电导率对温度敏感,每升高 1℃ 大约变化 2%。代码里如果只做电压换算不做温度补偿,夏天和冬天读数能差出一大截:
float readTDS(float temperature) { int raw = analogRead(TDS_PIN); float voltage = raw * 5.0 / 1023.0; // 先按 25℃ 基准做温度补偿 float compVoltage = voltage / (1.0 + 0.02 * (temperature - 25.0)); // 电导率换算,再乘系数得到 TDS(ppm) float ec = (compVoltage - 0.0) * TDS_K; float tds = ec * 500.0; return tds; }temperature由外部传入,可以是 DS18B20 这类温度传感器读到的值,没有温度探头时传 25.0 就退化成不补偿。0.02是电导率的温度系数,常见取值 0.02/℃。TDS_K是电导率到 TDS 的转换系数,一般取 0.5 左右,但不同水质(河水、自来水、纯净水)的离子组成不同,这个系数会有偏差,做对比监测时固定一个值就行,做绝对值测量就得用标准液标定。ec * 500.0是把电导率单位换算成 ppm 的常用写法,具体量纲要看探头手册。
2.5 主循环调度:三路采样怎么不打架
三个探头共用 ADC,如果在一个循环里连续读,前一路的采样保持电容还没放完电就读下一路,会出现串扰。代码里的处理是每路之间留间隔,并且把结果统一打印:
void loop() { float ph = readPH(); delay(50); // 给 ADC 切换留出稳定时间 float ntu = readTurbidity(); delay(50); float tds = readTDS(25.0); // 无温度探头时固定 25℃ Serial.print("PH: "); Serial.print(ph, 2); Serial.print(" NTU: "); Serial.print(ntu, 1); Serial.print(" TDS: "); Serial.println(tds, 0); delay(1000); // 采样周期 1 秒 }delay(50)看着不起眼,但省掉它之后读数会飘,这是血泪经验。Serial.print的第二参数是小数位数,PH 保留两位、浊度一位、TDS 取整,够看又不刷屏。采样周期 1 秒对水质监测足够,水质变化本来就慢,没必要高频采。如果后面要接显示屏或上传云端,把打印换成对应的输出函数即可,三个读取函数不用动。
3. 把代码跑起来:接线、烧录与标定流程
3.1 硬件接线与供电注意
三个探头都是模拟输出,接线逻辑一致:VCC 接 5V(部分浊度探头要 3.3~5V,看手册)、GND 共地、信号线接对应的模拟引脚。PH 探头模块通常带一个运放板,输出阻抗低,直接接 ADC 没问题;浊度探头有的型号输出是脉冲式,那就不能直接analogRead,得用中断计数,这点买之前一定要确认输出类型。TDS 探头同理,模拟输出的才能用这份代码。
供电上有个坑:三个探头同时工作电流不大,但如果和电机、继电器共用一路 5V,ADC 读数会跟着抖。常见做法是给传感器单独一路 LDO,或者至少加个 100μF 电容在电源入口。接线尽量短,模拟线远离电源线,这些是老生常谈但真出事的时候都是这些细节。
3.2 烧录与串口验证
Arduino 环境下把代码贴进 IDE,选对板子和端口,点上传。烧录成功后打开串口监视器,波特率设成 9600(和Serial.begin(9600)对应)。正常应该看到每秒一行读数。如果全是 0 或者全是 1023,先查接线和供电,再看引脚宏定义有没有写错——A0写成0在某些板子上是数字引脚,读出来永远是 0。
验证顺序建议是:先只接 PH 探头,确认读数随探头在空气和水中变化;再接浊度,看清水和浑水的差值;最后接 TDS。一次接一个,出问题好定位。三个一起接上再调试,读数不对时根本分不清是哪一路的锅。
3.3 标定:决定读数准不准的关键一步
标定是这份代码能不能用的分水岭。PH 用两点标定:准备 PH 4.00 和 PH 6.86 的缓冲液(网上能买到粉剂自己配),探头洗净后分别泡进去,等读数稳定(一般 30 秒到 1 分钟),记下两次的电压值,代入PH_SLOPE = (6.86-4.00)/(V2-V1)、PH_OFFSET = 4.00 - PH_SLOPE*V1,把算出来的值填回宏定义重新烧录。浊度用清水基准:探头放纯净水里读电压,填进TURB_CLEAR。TDS 如果有标准液(比如 707ppm 的校准液),泡进去读一次,反推TDS_K。
标定完别急着收工,拿自来水测一下做交叉验证。自来水 PH 一般在 7 左右、TDS 在 100~300ppm 之间,读数落在这个区间说明标定方向对了。偏差大就回头查缓冲液有没有污染、探头有没有洗干净。
4. 避坑与排查:三个探头最容易翻车的地方
4.1 读数一直不变或卡在极值
现象:串口打印的 PH 永远是 7.00 或者 0,浊度永远是 0。原因多半是引脚定义和实际接线对不上,或者探头没供电。先拿万用表量探头信号脚对地电压,正常应该在 0~5V 之间随水质变化;如果恒定 0V 或 5V,查供电和线序。还有一种情况是analogRead的引脚号写成了数字口编号,读的是数字电平,自然只有 0 和 1023 两种结果。
4.2 PH 读数漂移、越测越偏
现象:刚标定完是准的,放水里半小时后读数慢慢往上爬。原因是 PH 探头需要浸泡活化,新探头或久置的探头直接干放会失准,用前要在 3M KCl 溶液里泡 24 小时。另外探头表面结垢也会导致漂移,尤其测硬水时,定期用软布蘸稀盐酸轻擦(别用力刮玻璃球泡)。代码层面可以在readPH里加个滑动平均,但治标不治本,探头维护才是根子。
4.3 浊度读数忽高忽低
现象:同一杯水,读数在 20 和 200 之间跳。先排除气泡,探头入水后静置几秒再采。再查电源,浊度探头对供电纹波敏感,和继电器共用电源时继电器吸合瞬间读数会飙。解决方法是给探头单独供电或加大滤波电容,代码里把中位数滤波的采样次数从 10 提到 20 也能压一部分抖动。如果探头是脉冲输出型,那这份模拟读取代码根本不适用,得换中断计数方案。
4.4 TDS 夏天冬天差一大截
现象:同一桶水,中午测和晚上测能差几十 ppm。这就是没做温度补偿的典型表现。确认readTDS的temperature参数传的是真实水温而不是固定 25.0,有 DS18B20 就接上,没有的话至少用个热敏电阻估个大概。补偿系数 0.02 是通用值,如果测的是高纯度水,系数要调小,具体查探头手册。
4.5 三路同时读互相干扰
现象:单独接一路时读数正常,三路一起接就全飘。原因是 ADC 多路切换时采样保持电容残留上一路的电荷。代码里每路之间的delay(50)就是干这个的,如果还不行,把间隔加到 100ms,或者在每次analogRead前先空读一次丢弃。硬件上给每个信号脚加个 0.1μF 对地电容也有帮助,但别加太大,会影响响应速度。
5. 二次开发:从串口打印到可用监测系统的改造路径
这份代码的定位是「能读能算」,离「能用」还差一层。我一般会按三步改:先加数据落盘,再接显示或上传,最后做异常判断。
第一步,把loop里的Serial.print换成结构化输出。比如输出一行 CSV,方便后面用表格或脚本分析:
// 输出 CSV 格式,方便导入表格或上传 Serial.print(millis()); Serial.print(","); Serial.print(ph, 2); Serial.print(","); Serial.print(ntu, 1); Serial.print(","); Serial.println(tds, 0);时间戳用millis(),接 RTC 模块后换成真实时间。CSV 的好处是不用改解析逻辑,Excel 直接能开。
第二步,接显示或通信。本地显示用 OLED 或 LCD,把三个值分行刷上去;要上云就接 ESP8266/ESP32 的 WiFi,把 CSV 行通过 MQTT 发出去。这里有个改造技巧:把三个读取函数和输出逻辑解耦,读取函数只管返回float,输出单独写一个publish()函数,换通信方式时只动publish,传感器代码一行不改。
第三步,加阈值判断和告警。水质监测的价值在于「异常时告诉你」,所以要在主循环里加判断:
// 阈值判断,超限时置标志位 bool phAlarm = (ph < 6.5 || ph > 8.5); bool turbAlarm = (ntu > 50.0); bool tdsAlarm = (tds > 1000.0); if (phAlarm || turbAlarm || tdsAlarm) { // 触发蜂鸣器或上报告警 }阈值按养殖品种或用途定,养鱼和饮用的标准完全不同,别照抄。告警要做迟滞,比如 PH 超过 8.5 触发、回落到 8.3 才解除,否则在阈值附近会反复报警。
还有一个容易被忽略的改造点:标定参数不该写死在代码里。做成运行时可通过串口命令修改,或者存进 EEPROM,换探头时不用重新烧录。我吃过这个亏——现场换了个 PH 探头,手边没电脑,只能干等。从那以后我每次做传感器项目,标定值一律走 EEPROM 或配置命令,烧录只负责逻辑。希望这份拆解帮到你,把三个探头真正跑成一套能用的监测链路。
本文还有配套的精品资源,点击获取