这项目是我整理的STM32水质检测开源资料,包含完整代码、原理图和Proteus仿真文件,尤其适合刚学完单片机基础、想做点有闭环感小项目的朋友。常见的水质检测DIY方案大多只给个TDS模块的串口打印,这套不太一样,它把pH值、TDS溶解性固体、水温三个指标做进了同一块板子,数据走OLED显示,同时通过串口往电脑端传,还带超限报警。你既能拿来当学习模板逐行读代码,也能直接打样做一套放在鱼缸或小型养殖场景里用。
我要先说明一点,这套系统属于“传感器信号采集+数据处理+人机交互”的典型单片机综合应用,难度中等偏上,但拆开看每个模块都不复杂。只要你有STM32裸机开发基础,能跑通点灯和串口打印,跟上这篇文章完全没问题。下面我按从需求到硬件的逻辑,把整个系统的架构、原理图设计、代码组织、仿真验证和实物调试全部过一遍。
1. 这套检测系统在做什么:从应用场景倒推硬件选型
1.1 系统的构成与工作流程
先看整体框架。这套水质检测系统的核心采集对象是三个物理量:pH酸碱度、TDS溶解性总固体、水温。其中TDS可以粗略理解成水中可溶性杂质的总量,单位是ppm或mg/L,数值越高说明水里溶解的矿物质、盐分越多。
整套系统的数据流是这样的:三路传感器信号进入STM32F103系列主控芯片,芯片内部ADC完成模拟量转换,经过滤波和换算后得到真实物理量,然后同时做三件事:刷新OLED屏幕上的实时数据、通过串口发送给上位机、判断数值是否超过设定的报警阈值并驱动蜂鸣器。供电部分用USB 5V输入,板载AMS1117稳压到3.3V给主控,传感器部分看模块要求决定走5V还是3.3V。
这个流程看起来常规,但真正决定项目质量的是后面几个细节:模拟前端怎么设计、换算公式怎么标定、滤波怎么处理。我建议你不要把目标只定在“跑起来”,而是想清楚每个指标背后的物理意义,这样移植到其他场景也只是换传感器的问题。
1.2 传感器选型对比:为什么选这三类
先看pH传感器。市面上常见有两种方案:一种是不带变送模块的复合电极,输出的是毫伏级电压信号,需要自己搭高输入阻抗的放大电路;另一种是集成变送模块,直接把pH值换算成0~3.3V或0~5V的模拟电压,单片机直接采样就行。这套开源项目用的是模块化方案,原因很简单:给初学者的项目如果卡在模拟前端调试上,很容易劝退。模块输出的电压和pH值近似线性对应,代码里做一次线性映射就能显示。
TDS传感器基本就是电导率测量的变种。典型方案是给两个探针施加交流激励信号,通过检测溶液中的电流大小推算电导率,再换算成TDS。这里有个很关键的点:绝对不能加直流,直流会导致探针极化,测量值会一直漂。开源的TDS模块已经把交流激励和I/V转换电路做好了,输出也是模拟电压,对单片机来说又是一个ADC通道的事。
温度传感器我推荐DS18B20而不是NTC热敏电阻。DS18B20是数字输出,走单总线协议,不需要ADC通道,而且它在这里还有个额外作用:TDS和pH都受温度影响,用同一个MCU读到的温度值去做软件补偿,比模块自带的简陋补偿要方便。至于为什么不选NTC,因为需要自己搭电桥、标定B值、查表,精度还容易受自热影响,徒增工作量。
1.3 主控选型:STM32F103C8T6够用吗
主控选择的是STM32F103C8T6,这颗芯片几乎成了开源项目的默认配置:72MHz主频,64KB Flash,20KB RAM,3个USART,2个ADC各有10个通道,绰绰有余。我们需要的资源如下:
- ADC:至少2路(pH信号和TDS信号),如果用带变送模块的传感器就只需要2路模拟输入。
- 定时器:做ADC采样触发、DS18B20时序延时,至少1个通用定时器。
- USART:1路用于调试和上位机通讯。
- I2C或SPI:驱动OLED屏,I2C方式只占用2个引脚。
- GPIO:控制蜂鸣器、读取按键。
F103C8T6的成本低、资料多,Keil工程模板网上遍地都是,用这块芯片最大的好处是遇到问题你能搜到无数解决方案。真没必要上F407或者G030之类的,性能过剩对学习不是好事,代码复杂度和调试点都会变多。
2. 原理图拆解:传感器接口、电源树与ADC采样链路上的关键细节
2.1 电源分配:3.3V/5V轨与参考电压稳定性
原理图里最容易翻车的不是信号链路,而是电源。这套系统我设计的是USB供电,经过一个自恢复保险丝之后分为两路:一路直接给AMS1117-3.3稳压,给主控、OLED、DS18B20供电;另一路5V给pH变送模块和TDS模块供电。
这里插一个重要经验:ADC采样参考电压不稳,采集值就会飘。很多初学者把VDDA和VDD直接连在一起,板子上没有做任何滤波,结果发现采样数据在几个LSB之间乱跳。正确做法是VDDA通过一个磁珠或10Ω电阻接到3.3V,然后在VDDA引脚对地并联一个1uF陶瓷电容加一个0.1uF高频退耦电容。这套原理图里我特意加了这两个电容,布局的时候要尽量靠近MCU的VDDA引脚,不要走太远的过孔。
另外就是地线处理。pH和TDS模块的模拟地不要单独铺一块“干净地”然后再单点连接,在两层小板子上这样搞往往适得其反,直接共地反而噪声更小。只要模块输出信号线和GND双绞走线,远离PWM驱动的蜂鸣器走线区域就可以了。
2.2 pH前端:为什么不能把传感器直接怼到MCU引脚上
就算你买的是带变送模块的pH传感器,我还是建议你在原理图里预留一个电压跟随级。pH玻璃电极的内阻极高,动辄几十兆欧到几百兆欧,如果后级电路的输入阻抗不够高,信号会被分压吃掉,测出来的pH值大概率是错的。
具体做法是用一个轨到轨输入的运算放大器,比如LMV321或MCP6001,接成电压跟随器,输入接pH模块的电压输出,输出接MCU的ADC引脚。运放在这里起到两个作用:缓冲阻抗和隔离。如果用的是不带变送模块的裸电极,那这个运放前面还要加一级偏置电路,把正负几百毫伏的电极电压抬升到0~3.3V范围内,常用的加法电路就行。我见过太多新手第一次用pH裸电极,直接上ADC,采样结果不是零就是满量程,就是这个原因。
代码里对pH模块输出电压的换算公式也很直白:pH = (Vout_offset - Vref) / slope,这里的Vref在25度时约等于1.23V(对应pH=7中性点的模块输出电压),slope约为每pH单位59mV乘以电路增益。不同模块的基准点不一样,强烈建议拿到模块后先测一下pH=7标定液的实际输出电压,再把系数写进程序。
2.3 TDS探头:交流激励和I/V转换是怎么回事
如果你买的是成品TDS模块,原理图里基本不用额外处理,输出电压范围在0~2.3V左右,直接进MCU的ADC就行。但我还是要说说模块内部做了什么,不然测出来的数据出问题你根本没法排查。
TDS模块内部有一个方波发生器,产生大约1kHz的交变信号加到两个不锈钢探头上。溶液相当于一个电阻,阻值范围从几百欧兆几兆欧不等。通过该电阻的电流经过运算放大器做I/V转换,输出一个和电导率成比例的交流电压,再通过整流滤波变成直流电平给MCU。这个方案的核心就是利用交流信号防止电解极化。
在这套系统里,TDS模块输出的模拟电压接入到STM32的一个ADC通道。测量范围一般能到0~1000ppm甚至更宽,具体看探头常数。代码中换算用的是电导率到TDS的经验公式,我后面会详细给出。
2.4 引脚分配与外围电路速查表
我把这套系统用到的引脚分配整理成一个表,方便你画原理图时对照,也方便后面读代码时快速对应。
| 功能模块 | MCU引脚 | 接口类型 | 备注 |
|---|---|---|---|
| pH变送模块 | PA0 | ADC1_IN0 | 电压跟随器输出接入 |
| TDS模块 | PA1 | ADC1_IN1 | 模块GND与MCU共地 |
| DS18B20 | PA2 | 单总线 | 4.7k上拉电阻到3.3V |
| OLED I2C | PB6/ PB7 | I2C1 | 地址常见0x3C或0x3D |
| 蜂鸣器 | PB8 | 推挽输出 | 三极管驱动,低电平有效或高电平有效以实物为准 |
| 按键S1/S2 | PB9/PB10 | 上拉输入 | 用于页面切换和阈值调整 |
| USART1 | PA9/PA10 | TTL串口 | 连接到USB转串口模块 |
蜂鸣器电路要注意:不要直接拿MCU引脚驱动蜂鸣器,引脚带不动,反电动势还会打坏芯片。用一颗NPN三极管或一颗NMOS管做开关,蜂鸣器并联一个续流二极管,方向要接反(二极管阴极接电源正,阳极接集电极),这个电路几乎每个项目都要用,我建议你直接背下来。
3. 固件侧核心逻辑:从原始电压到水质指标的计算与处理
3.1 工程结构与模块划分
代码工程我按功能拆成了几个文件,不搞all-in-one的main.c大杂烩:
- main.c:初始化外围设备、调度主循环。
- adc.c:ADC初始化、多通道采样、平均值滤波。
- ds18b20.c:单总线时序、温度读取。
- oled.c:SSD1306驱动、字符显示。
- sensor.c:pH和TDS的电压到物理量换算、温度补偿。
- uart.c:串口格式化输出。
- alarm.c:阈值判断和蜂鸣器控制。
这种一个功能一个文件的习惯,一定要从这种小项目就开始培养。后面你做大一点的项目,比如加Wi-Fi模块或者SD卡存储,每个文件依然只有单一的职责,维护起来不费劲。
主循环我建议写成无阻塞轮询结构,不要在延时函数里浪费CPU。比如OLED刷新可以放到1Hz的频率,串口打印放到2Hz,而ADC采样用定时器触发,采样周期1ms。这样整个系统后续再加按键扫描、菜单逻辑才不会卡顿。
3.2 ADC多通道采样与滑动平均滤波:怎么去掉毛刺
ADC这里的代码逻辑比很多人想象中要绕一点。F103的ADC有两个,每个有多路通道,但如果要同时采pH和TDS两个通道,一般就是在同一次转换中做规则序列扫描,或者用两次转换分别读取。
我用的方案是定时器触发ADC,以1kHz的速率连续采样,每次转换完成在中断里把两个通道的数据放进各自的环形缓冲区,然后主循环里做100次平均。这么做的原因是水质传感器的输出不是瞬时值,它有响应时间,而且水体本身在不停流动,单次采样会叠加很多随机噪声。
滤波不要用很激进的算法,比如中值滤波(窗口长度不好定)或者卡尔曼滤波(物理模型不好建)。滑动平均在这个场景下最简单可靠:队列长度设为16或32,每次采完新值踢掉最旧的值,算平均值。实测下来,原始ADC跳动能到±10个LSB的信号,滑动平均之后能压在±2个LSB以内。注意滑动平均的窗口长度不要设太大,否则对真实的水质变化响应会滞后得很明显,鱼缸里加了净水剂都得等半天才看到数值变化。
下面是核心代码片段,使用HAL库风格:
#define ADC_BUF_SIZE 16 uint16_t ph_buf[ADC_BUF_SIZE]; uint16_t tds_buf[ADC_BUF_SIZE]; uint8_t buf_index = 0; void ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { ph_buf[buf_index] = adc_val[0]; tds_buf[buf_index] = adc_val[1]; buf_index = (buf_index + 1) % ADC_BUF_SIZE; } } uint16_t getAverage(uint16_t *buf) { uint32_t sum = 0; for (int i = 0; i < ADC_BUF_SIZE; i++) { sum += buf[i]; } return (uint16_t)(sum / ADC_BUF_SIZE); }3.3 物理量换算公式:pH、TDS和温度补偿到底怎么算
ADC原始值变成电压很简单,12位ADC,参考电压3.3V,voltage = adc_value / 4096.0 * 3.3。接下来才是关键。
pH换算:假设pH变送模块在25℃,pH=7时输出电压V_ref,而每变化1个pH单位电压变化V_step(约59.16mV/0.25≈0.25V,取决于模块增益)。代码里用线性关系计算:
float adcToPH(float voltage_mv) { // V_ref是中性点输出电压,V_step是每pH对应的毫伏数 // 具体数值需根据模块实测标定 float ph = 7.0f + (voltage_mv - PH_V_REF) / PH_V_STEP; return ph; }这个公式里的PH_V_REF和PH_V_STEP一定要自己标定,不同模块差异很大。一个土办法是买pH=4.00、6.86、9.18三种标准缓冲液,测出三点电压后做线性拟合。不加这一步,你测出来的pH显示值可能整体偏移0.5以上,测矿泉水显示pH=5这种明显不合理的结果,都是标定系数不对导致的。
TDS换算:TDS模块输出的电压V_tds(单位V)换算成TDS值的常用经验公式来自开源社区:
float adcToTDS(float voltage) { // 0~2.3V对应0~1000ppm,近似线性,指数补偿更准 float tds = (133.42f * voltage * voltage * voltage - 255.86f * voltage * voltage + 857.39f * voltage) * 0.5f; return tds; }这个三次多项式本质上是对电导率曲线的拟合,而且最后乘以0.5是把电导率换算成TDS的系数。如果你用的模块官方给了另一个多项式,以官方为准。要注意这个公式的适用范围一般在0~1000ppm,超出范围时最好稀释测量。
温度补偿:水温对电导率的影响大约是每度2%,pH的电极斜率也会随温度变化。代码里做了一个简化补偿,以25℃为基准:
float tempCompensate(float value, float temp, float coeff) { return value / (1.0f + coeff * (temp - 25.0f)); }coeff对TDS一般取0.02,对pH的电极斜率取0.003。这个做法的意思是以25℃时的测量值为基准,温度高于25度时电导率升高,就往回调小一点。对学习项目来说,这个精度已经够了。
3.4 显示与串口输出逻辑:让人一眼看懂数据的排版
OLED显示用的是SSD1306驱动芯片的0.96寸屏,I2C接口。这类屏幕驱动代码网上很多,核心就是初始化序列 + 显存操作 + 刷新。我建议用带显存缓冲的驱动方式:程序在内存里维护一个128x8字节的缓冲区,所有绘制操作先写进缓冲区,最后一次I2C批量发送到屏幕。这种方式虽然占用1KB RAM,但避免了频繁调用I2C导致的显示闪烁。
显示页面我做了两页:第一页显示pH和温度,第二页显示TDS和系统状态。用按键S1切换页面,S2长按进入阈值设置模式。这样屏幕信息不拥挤,也顺带演示了菜单状态机的写法。
串口协议我用最简单的文本格式:
PH: 7.32, TDS: 245ppm, TEMP: 25.6C每500ms发送一行,直接用printf重定向到USART1,波特率115200。如果你接了ESP8266模块,后续改成JSON格式发到云平台,就是物联网水质监测了。
4. Proteus仿真驱动整条链路:搭建、加载固件与验证水位
4.1 仿真电路搭建要点:STMCube、Proteus和固件的连接方式
Proteus仿真文件里,核心器件是STM32F103C8芯片,右侧放一个电位器或者电压源模拟pH模块的输出,再放一个电压源模拟TDS模块的输出,DS18B20用Proteus自带的模型,OLED用I2C虚拟显示器代替。
搭建时最容易踩的坑是:Proteus里的STM32模型不认编译出来的.hex文件,需要先加载到芯片模型里才能跑。双击芯片模型,在Program File一栏选择Keil编译生成的hex文件。如果文件选错或者芯片型号不对,仿真会一直停在0x08000000处不动,看起来就像卡死了,实际上芯片根本没跑起来。
仿真环境里没有真实的传感器,所以pH和TDS信号是用电压源模拟的。你可以把pH信号源的电压设成1.23V看看显示是不是7.00,设成2.00V看看是不是pH值向下偏移,用来验证代码的线性映射逻辑是否正确。TDS信号源设成0V、1V、2V分别验证数值范围,这样你就等于把固件里的信号链路单独抽出来测了一遍。
4.2 模拟传感器信号与整机验证流程
我给一个可以照着做的仿真验证流程,这样你加载完固件不会迷茫,知道每一步该看到什么:
- 静态验证:pH信号源输出1.23V,TDS信号源输出0.5V,DS18B20设成25℃。OLED第一页应显示pH≈7.0,第二页TDS≈90ppm左右(具体以公式为准)。串口虚拟终端能收到数据帧。
- 动态验证:缓慢调节pH信号源电压,每调0.1V观察显示值是否按比例变化。注意响应会有几百毫秒延迟,这不是Bug,是滑动平均滤波导致的,说明滤波生效了。
- 报警验证:把TDS信号源电压调到2.2V以上,对应超过1000ppm的阈值,蜂鸣器应该响,OLED上可以加一个超限提示。
仿真和实物的差异你心里要有数:Proteus里的电压源是理想信号,没有噪声,所以ADC读取值非常稳定。实物上会有工频干扰、电源纹波、传感器极化等问题,所以仿真通过不算完,实物还要再调一遍滤波参数。
4.3 仿真结果怎么解读:从虚拟终端和OLED反推代码问题
我在调试过程中总结了一个“先看值,再看量,最后看变化”的思路。
先说先看值:虚拟终端或者OLED显示的数值如果差得离谱,检查代码里的参考电压是不是3.3V,如果参考电压设成5V而实际芯片供电是3.3V,所有采样读数都会偏高50%,这类错误非常高发。
再看量:如果显示的数值在合理范围内,但小数点后面一直跳,检查滤波队列长度是不是太短。如果只有1~2个LSB的跳动,属于正常的采样噪声,不要再加滤波了,否则响应会越来越慢。
最后看变化:手动改变电压源,看显示值是否有响应。如果完全不动,查ADC通道配置和DMA有没有开着,曾经就有人查了半天发现DMA的buffer地址写到了只读区,数据一直没更新。
5. 实物调试与避坑:采集噪声、标定步骤和那些文档没写的事
5.1 采样噪声排查:为什么实物数据比仿真跳得厉害
从仿真转到实物,第一反应通常都是“怎么噪声这么大”。我的排查顺序是先电源后信号再布局。
电源方面的坑我前面说过了,VDDA没加滤波电容是最常见的。第二个常见问题是用USB供电时,电脑的USB口噪声比较大,可以换一个质量稍好的5V适配器试试。如果板子上有其他数字器件,比如按键或者通信芯片的翻转噪声耦合到了模拟电源,可以在模拟电源入口加一个LC滤波器,效果立竿见影。
信号线上的问题主要是共模噪声。pH和TDS模块的输出信号线如果和蜂鸣器控制线、按键线扎在一起走,蜂鸣器一旦动作,ADC采样值就会突跳。解决办法是信号线远离数字线,或者加一个100Ω电阻串联到ADC引脚做低通滤波,配合引脚对地的100nF电容,RC截止频率大概在16kHz左右,能滤掉大部分高频噪声。
5.2 传感器的标定与探头保养:决定数据可信度的隐藏环节
传感器标定这步强烈建议别省。pH探头买回来第一次用前要在3M KCl溶液里浸泡几个小时,让电极活化。标定步骤是:
- 把探头放入pH=6.86的标准缓冲液,等示数稳定,记录ADC电压值。
- 把探头放入pH=4.00的标准缓冲液,稳定后记录第二个电压值。
- 用这两点计算斜率和零点,填到代码的
PH_V_STEP和PH_V_REF。
TDS探头标定相对简单,配置一盆标准TDS溶液(通常是氯化钠溶液),或者在已知TDS的水里校正系数。我的经验是TDS模块的绝对精度本来就一般,拿来做趋势监测非常合适,但如果你要求它测出来的ppm值和专业水质检测笔完全一致,比较难,误差10%以内已经算不错了。
探头保养直接影响寿命,比标定还重要。pH探头不能干放,不用的时候要泡在保护液里。TDS探头上的电极用久了会结垢,要用软毛刷轻轻刷掉水垢,不要用硬物刮,也不要泡在洗涤剂里。这些细节如果忽略,探头的寿命可能从一年缩短到一个月。
5.3 从仿真到量产的预期管理:这套系统的精度和定位
最后给你交个底,这套系统的精度定位是“监测级”而不是“计量级”。什么叫监测级?就是你能看到水质的变化趋势,能用来判断鱼缸该不该换水、净水器滤芯是不是失效了、配置营养液时浓度对不对。但你不能指望它达到实验室分析仪的精度,毕竟工业pH计一台要几千块,探头就几百块。
我实际测下来,pH显示值和手持pH计之间的误差在0.2个pH单位以内是可能的,前提是标定得好。TDS的误差稍大,尤其是超过500ppm以后,三次多项式会明显偏离真实电导率。所以这套系统更适合做相对变化的监测,一旦数值发生明显偏移,你再去用专业仪器确认,这个定位比较合理。
另外提醒一下长期运行时的维护周期:TDS传感器建议一个月清洗一次,pH探头每月重新标定一次,如果你发现读数开始系统性偏移,先不要怀疑电路坏了,大概率是探头需要清洗或标定了。这套系统的电路结构比较简单,故障率最高的元器件其实是蜂鸣器前面的三极管和OLED屏的排线,备几个原件备着就行。
实际做完这整套流程,我对“开源项目”的理解又深了一层:开源的本质不是给你一个答案,而是把整个思考链路摊开给你看。源码、原理图、仿真文件都是静态的,真正有价值的是那些配平电路时的考量、选型取舍的权衡、以及从仿真走向实物时碰到的种种意外。把这篇项目的每一个环节都走通之后,你再去看其他STM32传感器套件,会发现底层逻辑都是相通的——无非是信噪比、标定和交互这三件事。这也是我当初把这个项目整理成完整开源包分享出来的原因,希望这篇文章能帮你在复现的过程中少走几步弯路。