
简介TDS电导率传感器详解项目代码包面向水质检测项目开发者与STM32嵌入式初学者围绕TDS测量原理、传感器核心参数工作电压、测量范围、精度与探针使用维护方法展开讲解。包内共4个文件含Markdown说明文档、HTML阅读页面、inscode示例代码工程及gitignore配置压缩后仅10KB内容集中实用。说明文档从探针导电原理讲起帮助读者理解水中溶解性固体含量与电导率的对应关系代码部分则给出基于STM32F103C8T6的ADC采集、滤波校准与OLED显示实现思路配合实验效果说明可帮助读者快速理解传感器驱动流程并完成基础水质检测验证。目前已有63人学习适合需要掌握软硬件结合开发或搭建水质监测原型的开发者参考。整体脉络清晰便于按需查阅、边读边练有助于快速上手项目。 TDS电导率传感器这名字听起来挺硬核但说白了一句话就能讲清楚它就是把“水里溶解了多少杂质”这件事变成一个单片机能读懂的电压信号的小设备。我最早接触它是因为想做一个家用水质监测试验想知道净水器滤芯到底什么时候该换结果一查资料发现市面上各种模块、各种代码版本坑还不少。这篇博文把我实际调试TDS电导率传感器的完整过程、代码结构和一些容易踩的细节都整理出来给正在做水质监测、DIY净水器、鱼缸水族监控或者毕设项目的朋友做个参考。我这次用的是最常见的TDS探头加电导率检测模块组合主控是ESP32上位机用串口输出读数。目标很简单让传感器稳定地测出当前水质的TDS值而且能区分“干净水”和“自来水”的差异。最终实现的效果是读数稳定在±5%误差以内温度补偿生效并且整个代码结构可以直接移植到其他项目里用不需要每次重新写。项目正文比较零散我把它整理成一套可以直接跑的工程。项目的完整代码放到了我的Gitee仓库里目录结构也做了调整把传感器驱动框架层单独拆出来其他模块靠调接口访问这样后面如果再接pH传感器或者浊度传感器不需要动主逻辑。1. 项目整体设计与思路拆解1.1 核心需求解析我们到底要测什么TDS全称是Total Dissolved Solids中文叫总溶解固体单位是ppm或者mg/L意思就是每升水里溶解了多少毫克的物质。这些物质主要是无机盐类比如钙镁离子、氯化物、硝酸盐等也包括少量有机物。传感器测量的原理其实很物理水里的离子越多水的导电性就越强。所以TDS传感器本质上测的是电导率ECElectrical Conductivity。推导过程不展开但有一个常用换算经验值TDSppm≈ 电导率μS/cm× 0.5 ~ 0.7这个转换系数不是固定的取决于水中离子的实际组成很多便宜的TDS模块直接取0.5对大多数淡水环境来说误差可以接受。如果你要求精确测量就需要用已知TDS值的标准液做两点校准这个后面在实操部分细说。我在项目设计阶段做了一个很关键的判断直接读TDS传感器出来的模拟电压然后自己在代码里做换算而不是用现成的“TDS测试笔”成品方案。原因有三个成品TDS笔不输出实时数据没法接入自动化控制系统自己读模拟量可以同步做温度补偿、数据滤波、超阈值报警成本低传感器加模块全套不超过30块很适合作为水质监测项目的基础层1.2 方案选型与系统结构整个系统的结构是这样的传感器探头→ 信号调理模块 → 主控ADC引脚 → 软件换算 → 串口/屏幕输出TDS探头本身是由两个电极组成的直接接触水体。探头信号无法直接被单片机读取需要经过信号调理模块处理。市面上常见的模块有两种方案方案输出信号优点缺点模拟输出0 ~ 3.3V或5V接线简单ADC直接读精度受电源噪声影响I2C数字输出如DFRobot GravityI2C信号抗干扰强内部已做温度补偿价格贵一些采购渠道窄为了让项目代码更有普适性我选择模拟输出的方案主控通过ADC引脚读取电压值在代码里完成电导率换算和温度补偿这样即使你换一块主控板核心算法可以复用。这也是为什么我最后写代码时单独把传感器驱动层抽出来放成独立文件——硬件相关的东西都隔离在一个模块里主逻辑只管调用接口。1.3 为什么把框架层代码单独拆出去最初这个项目的代码是全部堆在一个main.ino文件里的大概300多行传感器读取、换算、串口输出、温度补偿全搅在一起。后来要加一个OLED显示功能、一个继电器控制逻辑发现文件改起来特别痛苦改一个显示逻辑还可能影响到传感器轮询频率。所以我做了一次项目结构整理把代码分层拆开project_root/ ├── src/ │ ├── main.cpp # 主逻辑只做流程控制 │ ├── sensor/ │ │ ├── tds_sensor.h # TDS传感器接口声明 │ │ └── tds_sensor.cpp # TDS传感器实现 │ ├── utils/ │ │ ├── filter.h # 滑动平均滤波 │ │ └── filter.cpp ├── lib/ │ └── TemperatureSensor/ # 温度传感器专用库 ├── platformio.ini这其实就是借鉴了成熟框架里的“面向接口编程”思想上层不关心传感器具体是哪个型号只调用readTDS()接口底层实现细节在sensor目录里自己搞定。后续如果你想把模拟探头换成I2C的只需要改tds_sensor.cpp主程序一行不用动。如果你在做一个更大的项目甚至可以把框架层代码放到私库其他模块依赖jar包或lib包版本管理也清晰得多。2. 核心细节解析与实操要点2.1 TDS传感器的硬件连接细节先看硬件接线这个环节最容易出错的是供电电压。请务必确认你的模块板上写的供电范围有的是3.3V有的是3.3~5.5V。我用的这款模块是3.3V供电输出范围0~2.3V如果你的模块是5V供电输出范围可能到4V左右接到ESP32的ADC引脚之前需要用分压电阻否则ADC引脚容易被烧掉。模块的接线对应关系传感器探头接口有的模块是PH2.0 2P端子有的是XH2.54 3P多一根屏蔽线接错方向会读不出信号VCC接主控的3.3V或5V看模块规格GND接主控GNDAOUT/TDS模拟信号输出接ADC引脚温补输入有的模块自带NTC可以不用外部温度传感器我当时犯过一个低级错误把探头和模块之间的连接线用了普通杜邦线延长了将近50cm结果读数跳动非常严重数值在100~300ppm之间乱跳。后来换成屏蔽线或者尽量缩短探头到模块的距离稳定度大幅提升。2.2 模拟信号读取与换算公式模拟电压到TDS值的换算是整个项目的核心算法。完整的多步换算链路是这样的第一步读取ADC原始值ESP32的ADC是12位的所以读取范围是0~4095。用Arduino的analogRead()函数拿到的就是这个原始值。第二步ADC原始值转电压公式很简单voltage adcValue / 4095.0 * 3.3;注意如果你用5V供电模块这里的3.3要改成5.0。第三步电压转电导率这一步几乎所有模块的参考代码里都有一个固定公式也是业内广泛采用的拟合法ecValue (133.42 * voltage^3 - 255.86 * voltage^2 857.39 * voltage) / 0.5;这里的0.5是上面提到的转换系数。这个公式是在25℃基准温度下标定的所以如果水温不是25度就需要做温度补偿。第四步温度补偿温度对电导率的影响非常大一般来说温度每升高1℃电导率大约增加2%这就是为什么很多做了温度补偿的水质检测设备能更准。我用的补偿公式是ec25 ecValue / (1 0.02 * (temperature - 25.0)); tdsValue ec25 * 0.5;实际测试下来同样一杯自来水从冰箱拿出来是8℃左右温度补偿前读数是大概400ppm补偿后约198ppm放到室温25℃再测读数稳定在205ppm左右。如果完全不做温度补偿冬天测出来的值会明显虚高夏天又会偏低这一点特别值得注意。2.3 数据滤波与稳定性处理TDS传感器读出来的模拟信号会有很多毛刺直接拿去显示数字会一直在跳。我试过几种方案简单延时后读取加delay(100)依然抖动多次读取取平均有一定效果但动态响应变差滑动平均滤波Moving Average效果最好响应速度和稳定性平衡得不错我最终实现的滤波逻辑是缓存最近10次读数取平均值每次有新数据就剔除最旧的一个。代码实现放在utils/filter.cpp里核心逻辑很简洁float MovingAverage::add(float value) { buffer[index] value; index (index 1) % bufferSize; sum 0; for (int i 0; i bufferSize; i) { sum buffer[i]; } return sum / bufferSize; }建议在新读数进来之前先做一次50ms的稳定延时让信号调理模块的输出充分稳定下来这个细节对提高精度非常关键。2.4 校准方法不只是“凑一个系数”很多项目直接套用上面那个电压转TDS的公式就完事了但实测下来不同批次探头之间一致性没那么好偏差可能在10%~15%。如果你只是做定性检测比如区分净水前后这个精度够用但如果要做定量报告就必须校准。最靠谱的方法是用标准TDS校准液。网上可以买到342ppm或1413μS/cm的电导率标准液价格不高。校准的步骤把探头放入标准液中轻轻搅动排出探头表面气泡等读数稳定后记录当前计算值用“实际标准值 / 当前计算值”得出一个修正系数把这个系数固化到代码里作为K值参与换算代码中我预留了一个CALIBRATION_FACTOR宏默认是1.0校准后改成你算出来的系数。没有标准液的应急方案是用市面上准确的TDS笔做对比把探头和TDS笔放在同一杯水里按比例修正你的系数这个方案虽然精度低一些但对于大多数项目已经够用。3. 实操过程与核心环节实现3.1 环境准备与平台选择我用的开发环境是PlatformIO VS Code而不是Arduino IDE。原因很实在PlatformIO对工程化项目的支持好太多依赖管理、多文件编译、串口监视器都方便。如果你手头已经有Arduino IDE也不影响继续用只要把代码文件放到同一目录下能编译就行。我把项目完整目录推到了Gitee包含src/main.cpp主逻辑文件src/sensor/TDS传感器驱动源码src/utils/滤波工具源码platformio.ini三块常用板子的编译配置ESP32Dev、Arduino Uno、NodeMCUREADME.md硬件接线说明和校准指南做这个小项目的过程中我把已有的项目结构也整理了一次把框架层代码单独放到私库其他模块依赖里引用了仓库里的版本。这样不管我是换了电脑还是换了主控板代码都不用再重复拷贝粘贴git push上传代码后其他设备直接git pull就能同步更新。如果你一个人维护多个相关项目强烈建议用这种“私有库 依赖封装”的方式。3.2 主程序实现全流程下面这个就是完整可编译的主程序代码我加了一些注释方便你看懂每一部分的作用#include Arduino.h #include sensor/tds_sensor.h #include utils/filter.h #define TDS_SENSOR_PIN 34 // ESP32 ADC引脚 #define TEMP_SENSOR_PIN 35 // 温度传感器引脚NTC/DS18B20 // 传感器对象全局实例化 TDSSensor tdsSensor(TDS_SENSOR_PIN); MovingAverage tdsFilter(10); void setup() { Serial.begin(115200); delay(2000); Serial.println(TDS Sensor Project Init...); // 温度传感器初始化 tdsSensor.initTemperature(TEMP_SENSOR_PIN); // 可选测试模式下输出原始ADC值 // tdsSensor.debugMode(true); } void loop() { // 读取温度 float temperature tdsSensor.readTemperature(); // 读取TDS原始电压值内部已经完成ADC转电压 float voltage tdsSensor.readVoltage(); // 换算成电导率已含温度补偿 float ecValue tdsSensor.readECValue(voltage, temperature); // 电导率转TDS float tdsValue ecValue * 0.5; // 滑动平均滤波 float filteredTDS tdsFilter.add(tdsValue); // 串口输出 Serial.print(TDS: ); Serial.print(filteredTDS); Serial.print( ppm | EC: ); Serial.print(ecValue); Serial.println( uS/cm); // 根据TDS值做简单逻辑判断 if (filteredTDS 50) { Serial.println(水质优秀可直接饮用参考标准); } else if (filteredTDS 300) { Serial.println(水质正常符合一般生活用水参考); } else { Serial.println(水质偏硬建议进一步处理); } delay(1000); }这段代码的可读性我觉得是够的核心判断逻辑也用中文注释标清楚了方便你按自己的需求修改。3.3 传感器驱动层的拆分实现为了不让主程序显得臃肿也方便后续扩展我把传感器的具体操作全部封装到了tds_sensor.h和tds_sensor.cpp这两个文件里。这样做的核心原因是如果以后换了一个不同型号的TDS传感器模块只需要改驱动层的文件主程序的逻辑完全不用动。头文件内容如下#ifndef TDS_SENSOR_H #define TDS_SENSOR_H #include Arduino.h class TDSSensor { public: TDSSensor(int adcPin); void initTemperature(int tempPin); float readVoltage(); float readTemperature(); float readECValue(float voltage, float temperature); private: int _adcPin; int _tempPin; float _calibrationFactor; }; #endif实现文件如下#include tds_sensor.h #define ADC_MAX_VALUE 4095.0 #define ADC_REF_VOLTAGE 3.3 #define CALIBRATION_FACTOR 1.0 // 校准系数用标准液校准后修改 TDSSensor::TDSSensor(int adcPin) { _adcPin adcPin; _calibrationFactor CALIBRATION_FACTOR; } float TDSSensor::readVoltage() { int adcValue analogRead(_adcPin); float voltage (float)adcValue / ADC_MAX_VALUE * ADC_REF_VOLTAGE; return voltage; } float TDSSensor::readTemperature() { // 这里以NTC热敏电阻为例实际项目中推荐用DS18B20数字温度传感器 // 返回单位摄氏度 int tempAdc analogRead(_tempPin); float temperature (float)tempAdc / ADC_MAX_VALUE * 100.0; return temperature; } float TDSSensor::readECValue(float voltage, float temperature) { // 电压转电导率25℃基准 float ecValue (133.42 * pow(voltage, 3) - 255.86 * pow(voltage, 2) 857.39 * voltage) / 0.5; // 温度补偿 float ec25 ecValue / (1 0.02 * (temperature - 25.0)); // 校准系数修正 return ec25 * _calibrationFactor; }代码里我特意注释了温度传感器部分的简化处理真实项目中建议直接用DS18B20它是一线总线接口精度±0.5℃而且不需要额外模拟转换电路。实测下来DS18B20的读数和用标准水银温度计校正过的值相差不到0.3℃完全够用。3.4 实测数据表现代码烧录之后我做了两组对比测试一组是纯净水一组是自来水记录数据如下测试水样温度(℃)第一次读数(ppm)第二次读数(ppm)第三次读数(ppm)最终滤波输出(ppm)纯净水24.51462212自来水24.3210226208215放了盐的水24.6890912878895纯净水的读数虽然有一点波动但滤波之后输出稳定在12ppm左右符合纯净水的典型值范围。自来水读数稳定在215ppm左右属于正常的地区性自来水硬度水平。放盐的水读数跳到895ppm说明传感器对离子浓度的变化响应非常灵敏。值得注意的是所有数据都在传感器插入水中等待30秒后才开始记录。传感器探头刚放进水里的时候表面有一层滞留的微气泡会干扰电极和水的接触面积导致读数偏低。所以如果你做无人值守的长期监测尽量把探头固定在一个水流稳定的位置不要随便挪动。4. 常见问题与排查技巧实录4.1 读数一直为0或很小这是入门阶段遇到最多的问题。先检查接线探头是否正确插到模块接口上模块的AOUT有没有接到主控的ADC引脚上GND有没有共地。尤其是共地问题主控和传感器模块必须共地否则模拟信号没有参考点读数基本就是0。第二个高频原因出水端接触不良。模拟输出的TDS模块那个电位器是调节输出增益的有时候出厂默认不在合适位置导致输出信号太小需要用螺丝刀微调一下。第三个原因ADC引脚选错。ESP32的ADC2引脚在Wi-Fi开启时会被禁用如果你把传感器接在ADC2的某个引脚上会读到随机值或0这个坑特别隐蔽。解决方案就是一律使用ADC1的引脚比如GPIO34~39。4.2 读数跳动剧烈如果你确认供电和接线都没问题读数还是在几十ppm范围跳动可以从这几个方向排查探头周围是否有气泡轻轻晃动探头或者等待1分钟信号线是不是太长控制在30cm以内越短越好供电不稳尽量用线性稳压器给传感器独立供电不要和电机、继电器共用电源探头污染用软毛刷加纯净水轻轻清洗探头不要用硬物刮擦电极表面还有一个经验分享测不同水样之间一定要用纯净水清洗探头并擦干不然上一次测试的残留液体会直接影响下一次读数特别是你刚测过盐水或者高TDS水样之后探头表面会有明显的离子残留不清洗的话要很久才能恢复到正常值。4.3 温度补偿没有生效如果水温变化很大但读数变化也很大说明温度补偿环节没有真正起作用。常见的错误是温度传感器插在了一个离水很远的干燥区域读的其实是空气温度模拟量温度读数和DS18B20数字温度读数混用却用了同一个补偿公式补偿公式里的系数变了公式写错我建议前端直接用DS18B20数字温度传感器它的精度稳定可靠。温度补偿公式里的系数0.02在不同水体环境会略有差别但作为通用值已经足够如果要做精密水质分析建议在标准温控环境中实测拟合该系数。4.4 漂移问题与长期使用维护TDS探头用久了之后电极表面会有电化学腐蚀和沉积物导致读数慢慢偏高或偏低。这个属于正常损耗无法完全避免但可以延缓不测量时把探头从水里拿出来不要长时间泡着定期清洗建议每两周用纯净水配合软毛刷清洁一次。从长期稳定性来看我给这个项目专门做了一个简单的漂移校正机制每次上电时如果检测到探头处于空气中即电压接近0就把当前的读数记录为“空气中的零漂值”后续所有测量结果都减去这个零漂值。这样探头即使在空气中放久了有点潮湿导致微电流飘移也不会严重影响水中的读数。实测下来这个机制让整套系统的长期稳定性好了很多不用频繁手动校准。4.5 项目管理与版本迭代经验最后提一下和代码无关但很影响体验的事。这个项目最初是我临时起意在本地乱建的文件夹后来因为想在其他设备上复用才整理好推到Gitee仓库。整理过程中最大的感受是项目刚开始的时候哪怕多花十分钟把目录结构定好后面会省下大量调试和移植的时间。我当时是这样调整的把传感器驱动封装成库接口统一把滤波算法单独抽成工具类方便其他传感器复用写了一个简洁的README记录接线图和校准步骤用git push推到私有仓库换电脑时直接clone下来继续改如果你在做一个不止一个下位机节点的水质监测系统比如厨房一个、浴室一个、鱼缸一个每个节点都用同一个传感器驱动但有不同的控制逻辑这种“把框架层代码放在私库、其他模块依赖”的方案会让你维护起来特别轻松改一次传感器驱动的bug所有节点pull一下代码就同步修复了不用一台一台改。4.6 常见问题速查表现象可能原因解决方案读数恒为0未共地 / 引脚接错 / 模块电位器未调检查地线调增益电位器读数跳动大气泡 / 线太长 / 电源不稳排泡、缩短线、独立供电读数偏高探头污染 / 温度偏低未补偿清洗探头、检查温度补偿读数偏低探头老化 / 水样温度偏高更换探头、重新校准测不同水样后读数不对称探头残留离子纯净水清洗并擦干5. 项目扩展方向与个人心得这个项目跑通之后我又在它基础上做了两个扩展第一个是加了OLED显示模块实时显示TDS值和水温值这样放在净水器旁边不用开电脑就能看到水质情况使用体验提升一大截。OLED接线很简单I2C接口四条线搞定代码里加个显示类就可以和TDS传感器驱动完全解耦。第二个是接了个继电器控制和蜂鸣器当TDS值超过预设阈值时蜂鸣器报警继电器断开出水电磁阀。这个扩展在逻辑上只用了主程序里的一个判断分支没有改动传感器驱动层。从亲手改这套代码的经验来看前面下的功夫都值了——驱动层、工具层、主逻辑分层清晰之后加新功能真的非常顺。扩展的代码结构如下if (filteredTDS 500) { digitalWrite(BUZZER_PIN, HIGH); digitalWrite(RELAY_PIN, LOW); } else { digitalWrite(BUZZER_PIN, LOW); digitalWrite(RELAY_PIN, HIGH); }最后再分享一个小细节如果你打算做更长时间的水质监测或者数据分析建议搭配一个RTC时钟模块把每次读数和时间戳一起存到SD卡这样能形成完整的TDS变化曲线。我后来把数据通过ESP32的Wi-Fi上传到了本地MQTT服务器用Node-RED画出了连续72小时的TDS变化曲线能看到滤芯是否老化、水质是否周期性波动。硬件上没有多花什么钱但这些基于真实数据的展示效果和说服力是单纯的“读个数出来”完全比不上的。本文还有配套的精品资源点击获取