ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

水质检测三件套:PH、浊度、TDS传感器代码实战与标定指南

水质检测三件套:PH、浊度、TDS传感器代码实战与标定指南

简介:面向物联网与环境监测方向的入门开发者及51单片机学习者,这套代码资源围绕pH值、TDS总溶解固体与浊度三项水质参数,给出从传感器接口到数据读取的完整实现思路。内容涉及pH电极信号经AD转换与温度补偿处理、电导率法换算TDS值、以及基于光学散射原理的浊度测量,并配套传感器说明书,便于理解校准与接线细节。压缩包共113个文件、约4.1MB,以c与h源码、uvproj与uvopt工程配置、hex烧录文件、pdf与txt说明书为主,另含lst、obj等编译中间文件及少量备份文件,工程结构完整可直接参考。目前已有8558人学习下载,适合希望快速搭建水质检测原型、对照源码理解采样与显示流程的读者取用。

1. 水质检测三件套:PH、浊度、TDS 传感器代码到底能跑出什么

手里有一块开发板、三支探头,想做一个能实时读出水质数据的小装置,最卡人的往往不是接线,而是三支传感器各自的输出信号完全不是一回事:PH 探头输出的是毫伏级电压,浊度探头输出的是随浑浊程度变化的模拟量,TDS 探头输出的是电导率换算出来的浓度值。这份「水质检测—PH、浊度、TDS 传感器代码」资源,解决的就是把这三路异构信号统一采集、换算、输出成可读数值的问题。它适合做毕业设计、课程实验、鱼缸或小型水处理监测原型的开发者,也适合已经焊好电路、只差一段能跑通的采集代码的人。下面按「能干什么 → 怎么跑 → 怎么改」的顺序,把这份代码拆开讲清楚。

2. 技术栈与目录结构:三路模拟信号怎么被统一收口

2.1 为什么是「模拟采集 + 分段标定」这套组合

三支传感器里,PH 和浊度基本都是模拟电压输出,TDS 虽然内部是电导率测量,但对外同样给一个模拟电压。这意味着整套系统的核心不是复杂的通信协议,而是 ADC 采样精度和标定曲线。常见做法是用一块带 12 位 ADC 的主控(比如常见的 Arduino 系、ESP32 系或 STM32 系),把三路信号分别接到三个模拟输入脚,代码里做三件事:多次采样取中值滤波、按标定公式把电压换算成物理量、按固定周期串口输出。

选这套组合的原因很实际:模拟采集链路短、成本低、不需要额外协议栈,新手接线就能出数;分段标定则是因为 PH 和 TDS 的电压-数值关系在两端并不完全线性,单点斜率会在量程边缘翻车。代码里如果只写一个线性公式,读数在中间段看着挺准,一到酸性或高浓度区就飘,这是这类项目最常见的血泪经验。

2.2 目录结构长什么样

这类资源通常是一个可直接烧录的工程目录,结构不会太深,大致如下:

water-quality-sensor/ ├── src/ │ ├── main.ino / main.c # 主循环:采样调度与串口输出 │ ├── ph_sensor.* # PH 采样、温度补偿、电压转 PH │ ├── turbidity_sensor.* # 浊度采样与 NTU 换算 │ ├── tds_sensor.* # TDS 采样、温度补偿、ppm 换算 │ └── calibration.h # 标定系数、引脚定义、采样参数 ├── docs/ │ └── calibration.md # 标定步骤与记录表 └── README.md # 接线说明与运行步骤

calibration.h是整个工程里最该先看的文件,引脚号、采样次数、标定斜率与截距都集中在这里,改硬件或换探头基本只动这一个头文件。main里负责调度,三个传感器模块各自封装采样与换算,互不干扰,这种拆分方式对二次开发很友好——想加第四路(比如溶解氧)只要照着现有模块复制一份。

2.3 主循环的调度逻辑

主循环不复杂,但采样节奏有讲究。三路信号不能同时采,否则模拟多路复用切换会有串扰;也不能采太快,探头本身有响应时间。常见做法是轮询加间隔:

void loop() { // 每路间隔采样,避免多路复用串扰 float phVoltage = readAverage(PH_PIN, SAMPLE_COUNT); delay(SAMPLE_GAP_MS); float turbVoltage = readAverage(TURB_PIN, SAMPLE_COUNT); delay(SAMPLE_GAP_MS); float tdsVoltage = readAverage(TDS_PIN, SAMPLE_COUNT); // 统一换算并输出 float ph = voltageToPH(phVoltage, waterTempC); float ntu = voltageToNTU(turbVoltage); float ppm = voltageToTDS(tdsVoltage, waterTempC); Serial.print(ph); Serial.print(","); Serial.print(ntu); Serial.print(","); Serial.println(ppm); delay(LOOP_INTERVAL_MS); // 整轮采样周期 }

readAverage内部做的是多次 ADC 读取后取中值再平均,SAMPLE_COUNT一般取 10 到 30,取太少读数跳,取太多响应慢。SAMPLE_GAP_MS是两路之间的切换间隔,几毫秒到十几毫秒即可。LOOP_INTERVAL_MS决定整轮输出频率,做实时显示时 500ms 到 1s 比较合适。这里最容易忽略的是waterTempC——PH 和 TDS 都强依赖水温,没有温度补偿时,同一杯水夏天和冬天能差出可观的数值,代码里留了这个参数入口,实际使用时要么接一支温度探头,要么先写死室温。

3. 三路传感器核心代码拆解:从电压到物理量的换算链路

3.1 PH 通道:两点标定与温度补偿

PH 探头的输出经过调理板后,通常是「中性点对应某个中间电压,酸性偏低、碱性偏高」的关系。代码里最稳的做法是两点标定:用标准缓冲液(比如 4.00 和 7.00)各测一次电压,算出斜率和截距,再代入温度补偿。

// calibration.h 中记录两点标定结果 #define PH_VOLT_AT_4 3.05f // 4.00 缓冲液实测电压 #define PH_VOLT_AT_7 2.52f // 7.00 缓冲液实测电压 float voltageToPH(float voltage, float tempC) { // 由两点求斜率:每单位 PH 对应的电压变化 float slope = (PH_VOLT_AT_4 - PH_VOLT_AT_7) / (4.00f - 7.00f); float intercept = PH_VOLT_AT_7 - slope * 7.00f; float phRaw = (voltage - intercept) / slope; // 温度补偿:25℃ 为基准,偏离时按系数修正 float tempCoeff = 1.0f + 0.003f * (tempC - 25.0f); return phRaw * tempCoeff; }

PH_VOLT_AT_4和PH_VOLT_AT_7必须用你手上这支探头实测填入,直接抄别人的值大概率不准,因为调理板的增益和偏置每块都不同。slope算出来通常是负值(酸性电压高、碱性电压低,具体看调理板极性),符号错了读数会整个反过来。tempCoeff里的 0.003 是常见经验系数,不同探头略有差异,做高精度时应该查探头手册。这段代码的坑在于:标定液温度必须和被测水接近,否则两点标定本身就带误差,后面补偿再准也救不回来。

3.2 浊度通道:非线性曲线与 NTU 换算

浊度探头的输出电压和 NTU 不是直线关系,低浊度段灵敏、高浊度段趋于饱和。代码里常见两种处理:分段线性插值,或者用拟合公式。分段插值更直观,也方便现场补标定点。

// 标定点:电压 -> NTU,按电压降序排列 static const float voltTable[] = {4.10f, 3.80f, 3.20f, 2.50f, 1.60f}; static const float ntuTable[] = {0.0f, 50.0f, 200.0f, 500.0f, 1000.0f}; float voltageToNTU(float voltage) { int n = sizeof(voltTable) / sizeof(voltTable[0]); if (voltage >= voltTable[0]) return ntuTable[0]; // 高于最高标定点 if (voltage <= voltTable[n - 1]) return ntuTable[n - 1]; // 低于最低标定点 for (int i = 0; i < n - 1; i++) { if (voltage <= voltTable[i] && voltage > voltTable[i + 1]) { // 线性插值 float ratio = (voltage - voltTable[i + 1]) / (voltTable[i] - voltTable[i + 1]); return ntuTable[i + 1] + ratio * (ntuTable[i] - ntuTable[i + 1]); } } return -1.0f; // 异常值标记 }

voltTable和ntuTable必须一一对应且同序,顺序搞反插值结果会完全错乱。表里的数值要用标准浊度液逐点实测,没有标准液时至少用「清水 + 逐步加浊」的方式记录几个点,比纯理论公式靠谱。返回-1.0f是给上层一个异常标记,方便判断探头是否脱落或短路。注意浊度探头对气泡非常敏感,采样时水面有气泡读数会突然飙高,这是物理层面的问题,代码滤波只能缓解不能根治。

3.3 TDS 通道:电导率换算与温度归一

TDS 探头输出的是电导率相关电压,标准换算是先把电压转成电导率,再乘一个经验系数得到 ppm。温度对电导率影响很大,通常要归一到 25℃。

#define TDS_K_VALUE 0.5f // 探头常数,查手册或标定得到 #define TDS_PPM_COEFF 0.5f // 电导率转 ppm 的经验系数 float voltageToTDS(float voltage, float tempC) { // 电压转电导率(单位 us/cm),公式随调理板而异 float ec = (voltage / TDS_K_VALUE) * 1000.0f; // 温度归一:每偏离 1℃ 修正约 2% ec = ec / (1.0f + 0.02f * (tempC - 25.0f)); // 电导率转 ppm float ppm = ec * TDS_PPM_COEFF; return ppm > 0 ? ppm : 0; }

TDS_K_VALUE是探头常数,不同型号差别不小,填错会让整体读数成比例偏移。TDS_PPM_COEFF取 0.5 是常见近似,实际系数和水中离子组成有关,做粗略监测够用,做精确测量需要用标准液反推。温度归一那一步的 0.02 也是经验值,水温每升 1℃ 电导率大约涨 2%,不归一的话热水读数会虚高。这段代码最容易被误用的是把ec直接当 ppm 输出,单位差了 2 倍,数值看着「合理」其实全错。

4. 跑起来:接线、烧录与串口验证的完整步骤

4.1 接线与供电检查

三路模拟信号接线前先确认调理板的供电电压和主控 ADC 参考电压一致,常见是 5V 或 3.3V,混用会直接烧脚或读数满量程。PH 调理板一般输出 0 到 5V 范围,如果主控是 3.3V ADC,中间要加分压或电平适配。浊度和 TDS 同理。接线顺序建议先只接一路,串口看到稳定读数后再接第二路,三路一起上出问题时很难定位是哪一路的锅。

供电上有个细节:TDS 探头工作时会在水里加一个交流激励,和 PH 探头靠太近会互相干扰,物理上尽量拉开距离,代码里也可以错开采样时刻,这也是前面主循环里加SAMPLE_GAP_MS的原因之一。

4.2 烧录与串口观察

工程用常见 IDE 打开后,先改calibration.h里的引脚定义,再编译烧录。烧录完打开串口监视器,波特率按代码里Serial.begin的设置填。正常现象是每秒输出一行「PH,NTU,ppm」三个数。第一次跑不要急着看数值准不准,先看稳定性:探头静置在清水里,读数波动应该在合理范围内,如果个位数乱跳,多半是采样次数太少或电源纹波大。

验证顺序建议是:先用清水看三路是否都有合理基线,再用 PH 缓冲液验证 PH 通道,用浊度标准液验证浊度通道,最后用 TDS 标准液验证 TDS 通道。每验证一路就把该路的标定值回填到calibration.h,不要三路一起调,否则改一个参数影响一片,根本分不清是谁的问题。

4.3 输出格式与上位机对接

串口输出用逗号分隔是为了方便上位机解析。如果要做图表显示,可以把输出改成 JSON 或带时间戳的格式,接收端按行解析即可。常见做法是主控只负责采集和输出,绘图和存储放在电脑端或另一块带屏的主控上,这样采集端代码保持轻量,出问题也好排查。

提示:串口输出频率不要设太高,探头响应跟不上时高频输出只是把同一个值重复打印,还会让上位机图表看起来「很卡」。

5. 避坑与排查:这类水质采集代码最容易翻车的五件事

5.1 读数一直不动或满量程

现象是串口输出的某个值恒定不变,或者一直顶在最大值。原因通常是模拟脚接错、调理板没供电,或者 ADC 参考电压配置和实际不符。解决方法是先用万用表量探头调理板输出脚对地电压,确认有变化再查代码引脚定义;如果电压正常但读数不动,检查analogRead的引脚号是否和接线一致,以及参考电压宏有没有被改过。

5.2 PH 读数整体偏移但趋势正常

现象是酸液测出来偏碱、碱液测出来偏酸,或者整体差一个固定值。原因是两点标定用的电压值不是本探头实测的,或者标定液温度与被测水差太多。解决办法是重新用新鲜缓冲液做两点标定,标定前让探头在标定液里稳定几十秒,并把waterTempC设成标定液实际温度。偏移是系统性的,趋势正常说明斜率没大问题,优先怀疑截距。

5.3 浊度读数随气泡剧烈跳动

现象是水面有气泡或探头刚入水时 NTU 突然飙到很高,静置一会儿又回落。原因是浊度是光学测量,气泡会强烈散射光。解决办法是入水后等待稳定再读数,代码里可以加一个「连续 N 次采样方差过大则丢弃」的判据,或者把探头安装角度调整到气泡不易附着的位置。这是物理问题,纯靠滤波只能压不能消。

5.4 TDS 和 PH 互相干扰

现象是两路同时工作时读数都比单独工作时差。原因是 TDS 的交流激励串到了 PH 的高阻输入上。解决办法是物理拉开两支探头的距离,采样时刻错开,必要时给 PH 通道加屏蔽线。代码层面能做的有限,主要是别让两路在同一毫秒内采样。

5.5 长时间运行后数值缓慢漂移

现象是刚上电很准,跑几个小时后整体偏移。原因是探头极化、调理板温漂,或者标定系数本身只在特定温度下成立。解决办法是定期用标准液复标,代码里可以留一个「单点校准」入口,用已知标准液现场修正截距,不必每次做完整两点标定。长期部署时把复标周期写进维护流程,比指望代码自适应靠谱。

6. 二次开发:加一路温度补偿与把标定做成可配置

6.1 把写死的温度换成实测温度

现有代码里waterTempC是个外部传入的参数,实际跑起来多半是写死的室温。要提升精度,最直接的一步是接一支防水温度探头(常见的是单总线数字温度传感器),把实测水温喂给 PH 和 TDS 的换算函数。改动量很小:加一个温度读取模块,在主循环里先读温度,再传给两个换算函数。这样夏天冬天、冷水热水都能自动补偿,不用手动改参数。

float waterTempC = readWaterTemperature(); // 新增:实测水温 float ph = voltageToPH(phVoltage, waterTempC); float ppm = voltageToTDS(tdsVoltage, waterTempC);

注意温度探头本身也有响应延迟,测温和测水质最好在同一位置、同一深度,否则补偿用的温度和实际水样温度对不上,补偿反而引入误差。

6.2 把标定系数搬到运行时配置

calibration.h里的宏在编译期固定,现场复标就得重新烧录,很不方便。二次开发时可以把标定系数存到 EEPROM 或 Flash 的配置区,启动时读入,串口收到特定命令时进入标定模式并写回。这样现场维护人员不用带电脑和 IDE,发几条串口指令就能完成单点修正。实现上定义一个结构体存三路的斜率和截距,加一个简单的命令解析即可,工作量不大但实用性提升明显。

改造点改动位置收益
实测水温补偿主循环 + 温度模块消除季节性系统偏差
标定系数运行时配置配置区 + 串口命令现场复标免烧录
异常值标记上报各换算函数返回值上位机能识别探头故障
采样方差判据readAverage内部抑制气泡等瞬时干扰

6.3 验证改造是否真的有效

改完之后别只看「感觉准了」,要有可复现的验证方法:用同一杯标准液,在改造前后各测一组数据,记录均值和波动范围;再换不同温度的水样重复,看补偿是否让偏差收敛。我一般会准备三个已知浓度的样杯,改造前后各跑十轮,把偏差和标准差列出来对比,数据不收敛就说明补偿系数或标定流程有问题。这套验证习惯是从一次「改完看着挺好、换杯水就露馅」的翻车里留下来的——从那以后我每次动标定相关代码,都强制先用标准液跑一遍对照,再谈其他功能。希望这份拆解能帮你把三路水质数据稳稳跑起来,少走几段弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进