水质监测这件事,说大可以大到江河湖泊的生态治理,说小也能小到自家鱼缸的日常维护。我这些年折腾过不少嵌入式采集类的小系统,从温湿度记录仪到土壤墒情节点,但真正让我觉得“接地气又有技术含量”的,还是基于STM32的水质水深检测系统这一类项目。它把模拟信号采集、传感器标定、数据处理、本地显示和远程上报这几件事串成了一条完整的链路,既考验硬件功底,又考验软件架构,特别适合拿来练手,也特别适合做成能真正落地的小型监测终端。
这篇文章我想聊的不是某个现成的产品,而是这类系统从零到跑通的全过程:为什么选STM32而不是别的MCU,水深和水质这两类信号该怎么采集才靠谱,标定环节有哪些坑,显示和通信怎么组织,以及实际部署时会遇到哪些在实验室里根本想不到的问题。不管你是刚学完单片机基础、想找个综合项目练手的学生,还是需要快速搭一套监测原型的工程人员,下面这些内容应该都能直接拿去参考。我会尽量把每个选择的“为什么”讲清楚,而不是只丢一堆代码和接线图。
1. 系统整体设计与方案选型思路
1.1 为什么是STM32,而不是更便宜或更强大的方案
先说说主控选型这件事。很多人一上来会问:不就是采几个模拟量、驱动个屏幕吗,用8位机不行吗?理论上行,但实际做下来会发现几个绕不过去的坎。水质检测里常见的参数包括pH、浊度、溶解氧、电导率,这些传感器的输出大多是模拟电压,而且信号幅度往往很小、内阻不低,对ADC的分辨率和稳定性要求不低。8位机的10位ADC在量程分配上会很紧张,稍微有点噪声就跳得厉害,标定出来的曲线也不够平滑。
STM32的12位ADC配合DMA多通道扫描,可以一次性把多路传感器数据搬进内存,CPU不用逐通道等待转换,采样节拍稳定得多。再加上它普遍带硬件浮点或者至少运算能力充裕,做温度补偿、多点线性插值这些运算不会吃力。我一般会选STM32F103系列做入门方案,资源够用、资料多、成本低;如果要做低功耗野外部署,会考虑L系列;如果要做复杂算法或者带RTOS,F4系列更从容。这个梯度选择本身就是一种工程判断:先看采样精度和通道数需求,再看功耗和算力,最后才看价格。
水深检测这块,方案差异更大。常见的有压力式、超声波式和浮子式。压力式水深传感器本质是个压力变送器,把水压转换成4-20mA或者0-5V信号,成本低、安装简单,但需要做大气压补偿;超声波式非接触、不怕泥沙,但受水面波动和温度影响大,盲区也明显;浮子式结构简单但精度和响应速度一般。我在原型阶段一般用压力式,因为它和STM32的ADC接口最直接,标定逻辑也最容易讲清楚。选型时我会把量程、精度、输出形式、供电电压、防护等级这几个参数列成表对比,而不是拍脑袋决定。
| 方案类型 | 典型输出 | 优点 | 主要短板 | 适用场景 |
|---|---|---|---|---|
| 压力式水深 | 0.5-4.5V / 4-20mA | 成本低、接口简单、可连续测量 | 需气压补偿、长期漂移 | 水池、河道、水箱 |
| 超声波水深 | 串口/模拟 | 非接触、抗泥沙 | 受波动和温度影响、有盲区 | 明渠、污水池 |
| 浮子式水深 | 开关量/电阻 | 结构简单、无需供电 | 精度低、易卡滞 | 粗略液位报警 |
1.2 系统分层:把“采集—处理—呈现—上报”拆开看
我习惯把这类系统按功能分成四层来设计,这样每一层职责清晰,调试时也容易定位问题。最底层是传感器与信号调理层,负责把物理量变成MCU能吃的电信号;第二层是采集与处理层,也就是STM32内部的ADC采样、滤波、标定换算;第三层是人机交互层,包括本地屏幕显示、按键设置、报警指示;第四层是通信层,负责把数据通过串口、无线模块或者存储卡送出去。
这么分层的好处是,任何一层出问题都不会牵连全局。比如屏幕花屏了,不影响采集和上报;通信断了,本地照样记录。实际做项目时,我见过不少人把所有逻辑塞进一个while循环里,采样、显示、通信互相阻塞,结果屏幕刷新一慢,采样节拍就乱了,标定数据全废。分层之后,采集用定时器触发+DMA,显示用独立的刷新节拍,通信用状态机或者中断,各跑各的,互不干扰。
这里有个细节值得强调:水深和水质虽然都是模拟量,但它们的采样节奏需求不一样。水深变化相对慢,1秒采一次足够;pH和浊度在搅拌或者进水瞬间会剧烈波动,可能需要更快的采样来做平均。所以我在设计时会给不同通道分配不同的采样周期,而不是一刀切。这个思路在代码里体现为每个通道有自己的累加器和计数窗口,达到窗口长度才做一次有效值更新。
1.3 供电与防护:野外部署绕不开的现实问题
实验室里用USB供电跑得好好的系统,搬到现场经常第一天就出问题。水质水深检测系统很多是要长期放在户外或者潮湿环境里的,供电和防护必须提前想清楚。供电方面,如果是有市电的位置,用隔离电源模块把传感器供电和MCU供电分开,能有效避免传感器侧的干扰串进来;如果是野外,太阳能板加蓄电池是常见组合,这时候MCU的低功耗设计就很关键,STM32L系列在 STOP 模式下能做到微安级,配合定时唤醒采样,续航能拉长很多。
防护方面,传感器探头本身的防护等级要够,接线端子要做防水处理,电路板最好灌胶或者至少三防漆处理。我踩过一次坑:一个放在水箱边的采集板,没做防潮,梅雨季节过后ADC读数整体偏移,查了半天才发现是板子表面凝露导致漏电流。从那以后,凡是潮湿环境的板子,我都会在ADC输入端加RC低通滤波,PCB布局上把模拟地和数字地分开,传感器走线尽量短,这些看起来是“老生常谈”的做法,真到现场就是救命细节。
2. 核心硬件细节与信号采集要点
2.1 模拟前端:小信号放大与滤波怎么做才不白费ADC
STM32的ADC参考电压一般是3.3V,12位分辨率下每个LSB约0.8mV。水质传感器里,pH模块的输出经过内部放大后通常是0-3V左右,还算好处理;但浊度传感器和电导率传感器的原始信号可能只有几十毫伏,直接进ADC的话有效位数会掉得很厉害。这时候就需要模拟前端做放大和滤波。
我的常规做法是:传感器输出先经过一级电压跟随器(用轨到轨运放),提高输入阻抗,避免传感器内阻和ADC采样电容形成分压误差;然后经过一级同相放大,把信号抬到ADC量程的中段;最后加一个RC低通,截止频率设在信号带宽的几倍左右。举个具体例子,如果浊度信号的有效带宽大概在10Hz以内,我会把RC截止频率设在50Hz左右,电阻取10kΩ、电容取0.33μF,算下来截止频率约48Hz,既能滤掉高频噪声,又不会明显拖慢响应。
注意:ADC输入阻抗和采样时间要匹配。STM32的ADC采样保持电容需要足够的充电时间,如果前端阻抗偏高,必须把采样时间调长,否则读数会偏低。我一般会把采样周期设到239.5个ADC时钟,宁可慢一点也要保证稳定。
另外,多路模拟信号进ADC时,通道间的串扰也要防。做法是每个通道采样前留一点建立时间,或者在DMA扫描序列里把变化快的通道和变化慢的通道交错排列,减少相互影响。这些细节在数据手册里都有,但真正做项目时很容易被忽略,等到读数互相“污染”了才回头查。
2.2 水深信号的特殊处理:气压补偿与零点漂移
压力式水深传感器的原理很直白:探头在水下受到的压力等于水柱压力加上大气压,传感器测的是总压,要得到水深就得把大气压减掉。所以严格来说,系统里需要一个大气压参考。做法有两种:一种是用一个额外的绝压传感器测环境大气压,另一种是假设水面就是大气压,在探头入水前做一次零点标定。前者更准,后者更省。
我在原型里一般先用零点标定法:安装前让探头暴露在空气中,记录此时的ADC值作为零点,之后测得的压力减去零点再换算成水深。换算公式是 水深 = 压力差 / (水的密度 × 重力加速度)。假设传感器输出0.5V对应0米、4.5V对应10米,那么每米对应0.4V,ADC读数每变化1个LSB约0.8mV,对应水深变化约2mm,这个分辨率对大多数场景够用了。
但零点漂移是个真实存在的问题。温度变化、传感器老化都会让零点慢慢偏移。我的应对策略是定期做“空管校准”:如果系统知道某个时刻探头一定不在水里(比如检修时),就自动记录当前值更新零点。没有这个条件的话,至少要在软件里留一个手动校准入口,通过按键或者串口命令触发。这个设计看起来不起眼,但长期部署时非常必要。
2.3 数字传感器与模拟传感器的混合接入
实际项目里,传感器不一定都是模拟的。pH模块很多是模拟输出,但浊度、溶解氧有些是串口输出的数字传感器,直接给的是经过内部处理的值。这种混合接入的情况很常见,设计时要把接口资源规划好。
我的做法是:模拟通道统一走ADC+DMA,数字通道走USART或者I2C,各自有独立的接收缓冲和解析状态机。关键是不要让数字传感器的等待阻塞主循环。比如某个串口传感器响应慢,就用中断接收+环形缓冲,主循环里只做解析,解析不完整就等下一轮。这样即使某个传感器掉线,系统整体也不会卡死。
| 传感器类型 | 接口方式 | 数据特点 | 处理要点 |
|---|---|---|---|
| pH模块 | 模拟电压 | 高内阻、需缓冲 | 电压跟随+多点标定 |
| 浊度传感器 | 模拟/串口 | 易受气泡影响 | 多次采样取中值 |
| 水深压力传感器 | 模拟电压 | 慢变、需零点校准 | 气压补偿+滑动平均 |
| 数字水质探头 | RS485/串口 | 协议帧、有校验 | 状态机解析+超时重试 |
混合接入时还要注意电源的干净程度。数字传感器通信瞬间会有电流突变,如果和模拟传感器共用一路电源且去耦不足,模拟读数会跟着跳。我的经验是模拟部分单独用一颗LDO供电,数字部分另一路,中间用磁珠或者0欧电阻单点连接,这个成本很低但效果明显。
3. 软件架构与核心算法实现
3.1 采样调度:定时器触发+DMA才是稳定之道
软件部分我最想强调的就是采样调度。很多人习惯在main的while里轮流调用ADC采集函数,这种轮询方式在系统简单时能用,但一旦加了屏幕刷新和通信,采样间隔就变得不可预测,做平均滤波时窗口长度对应的实际时间忽长忽短,标定曲线自然不准。
正确做法是用一个定时器产生固定频率的触发信号,去触发ADC转换,ADC配置成扫描模式配合DMA,转换结果自动存到数组里。这样采样节拍由硬件保证,和CPU在忙什么完全无关。我一般会把定时器触发频率设在100Hz,DMA缓冲开一个环形数组,主循环里按需取最近N个点做处理。水深通道因为变化慢,可以每100个点取一次平均;pH通道取最近20个点的中值再平均,兼顾响应和抗干扰。
代码结构上,我会把采样相关的东西封装成一个模块,对外只暴露“获取最新水深”“获取最新pH”这样的接口,内部怎么滤波、怎么标定都不影响调用方。这种封装在后期换传感器或者改算法时特别省事。
/* 定时器触发ADC+DMA的简化配置思路 */ void ADC_DMA_Init(void) { /* 使能DMA时钟与ADC时钟 */ /* 配置DMA为循环模式,外设到内存,半字宽度 */ /* ADC配置为扫描模式、连续转换关闭、外部触发 */ /* 触发源选择定时器TRGO */ /* 使能ADC的DMA请求 */ } /* 主循环中取平均值的示意 */ float Get_WaterDepth(void) { uint32_t sum = 0; for (int i = 0; i < DEPTH_WINDOW; i++) { sum += depth_buf[(depth_idx + i) % DEPTH_WINDOW]; } return Convert_To_Depth((float)sum / DEPTH_WINDOW); }3.2 标定:两点标定够不够,什么时候需要多点
标定是这类系统里最容易被低估的环节。理论上,很多传感器输出和物理量是线性关系,两点标定就能确定斜率和截距。但实际传感器的线性度往往没那么理想,尤其是pH和浊度,在量程两端容易弯曲。我的经验是:水深用两点标定基本够,因为压力和水深的关系物理上就是线性的;pH至少三点,覆盖酸性、中性、碱性;浊度建议多点,因为不同浊度区间的散射特性不一样。
两点标定的计算很直接:假设两个标准点 (ADC1, PH1) 和 (ADC2, PH2),斜率 k = (PH2-PH1)/(ADC2-ADC1),截距 b = PH1 - k×ADC1,之后每个读数代入 y = kx + b。多点标定则用分段线性插值,把标定点按ADC值排序,落在哪一段就用哪一段的斜率和截距。分段线性插值实现简单、计算量小,对STM32来说毫无压力,效果比强行拟合一条曲线更贴合实际。
标定时有个操作细节必须注意:标准液和传感器探头要充分接触并稳定。pH探头从一种缓冲液换到另一种,读数需要几十秒甚至更久才能稳定,急着记录会导致标定点偏移。我一般会等读数在30秒内波动小于设定阈值再采信。另外,标定用的标准液温度要和实际测量环境接近,否则温度补偿没做好会引入系统误差。
3.3 温度补偿:一个不能省的修正环节
水质参数对温度很敏感。pH电极的斜率随温度变化,电导率更是直接跟温度挂钩,水深虽然受温度影响小,但压力传感器本身也有温漂。所以系统里最好有一个温度传感器,测水温或者环境温度,用于补偿计算。
以pH为例,能斯特方程给出的理论斜率在25℃时约59.16mV/pH,温度每变化1℃,斜率变化约0.2mV/pH。实际补偿时,很多pH模块内部已经做了部分补偿,但外接温度传感器做二次修正仍然值得。做法是把标定得到的斜率和截距按温度做修正,或者更简单地在换算后加一个温度修正项。补偿公式不必追求极致精确,工程上把主要温漂压下去就够了。
温度传感器我一般用DS18B20或者NTC热敏电阻。DS18B20是数字接口,接线简单、精度够,缺点是转换需要时间,读取时要注意时序;NTC便宜但要自己做ADC采集和查表换算。原型阶段用DS18B20省心,量产考虑成本再换NTC。
3.4 显示与交互:本地界面怎么设计才不添乱
本地显示我一般用0.96寸OLED或者2.4寸TFT,前者便宜省IO,后者信息量大。界面设计的原则是:主界面只放最关键的几个数——当前水深、pH、浊度、温度,外加一个状态指示(正常/报警/故障)。不要在主界面堆太多信息,现场人员一眼能看清才是好界面。
按键交互我通常设计三个键:设置、加、减。设置键进入菜单,加减键切换和修改参数。菜单里放标定入口、报警阈值、通信参数这些不常改的东西。这里有个小心得:所有参数改动都要立即写入EEPROM或者Flash的模拟存储区,并且带一个简单的校验,防止掉电后参数丢失或者读出错乱值。我见过因为参数没持久化,每次断电都要重新标定的系统,用起来非常折磨。
报警逻辑也要想清楚。水深超限、pH越界、传感器断线,这几种情况的处理方式不一样。传感器断线时读数可能是0也可能是满量程,不能简单当成真实值报警,而要结合信号合理性判断。我的做法是给每个通道设一个合理范围,超出范围就标记为“无效”,显示上给出明确提示,而不是显示一个误导性的数字。
4. 通信上报与数据管理
4.1 本地存储:SD卡还是片上Flash
数据要不要存、存多久,取决于使用场景。短期演示可以不存,但真正部署的系统,历史数据是分析趋势、回溯异常的依据。存储方案上,SD卡容量大、取数据方便,适合数据量大或者需要频繁导出的场景;片上Flash或者EEPROM容量小但可靠,适合存参数和少量关键记录。
我用得比较多的是“SD卡存历史+Flash存参数”的组合。SD卡通过SPI接入,文件系统用FatFs,按天建文件,每条记录带时间戳。这里要注意SD卡的写入时机:不要每次采样都写,而是攒够一批再写,减少文件系统开销和卡磨损。另外,写卡过程中如果断电,文件可能损坏,所以要么定期关闭文件重新打开,要么用双文件轮换的方式降低风险。
4.2 无线上报:把数据送出去的最后一段路
如果需要远程查看,就要加通信模块。常见的选择有串口转无线的透传模块、LoRa、NB-IoT等,具体选哪种看距离、功耗和现场有没有网络覆盖。原型阶段我一般先用串口透传模块把数据发到本地网关或者上位机,验证数据链路和协议没问题,再换成适合现场的网络制式。
协议设计上,我倾向于用简单的自定义帧格式:帧头+设备号+数据长度+数据内容+校验+帧尾。数据内容里把水深、pH、浊度、温度按固定顺序打包,校验用CRC16。这种格式解析简单、容错好,比直接发JSON省带宽,对资源有限的MCU更友好。上报周期根据场景定,水质变化慢的场景几分钟一次足够,异常时可以临时提高频率。
注意:无线模块的供电要单独考虑。很多模块在发射瞬间电流能到几百毫安,如果和MCU共用一路小电流LDO,发射时电压会被拉低导致MCU复位。解决办法是给模块单独供电或者加大电容缓冲,这个坑我踩过不止一次。
4.3 数据合理性校验:让系统自己知道“数据不对劲”
一套能长期运行的系统,必须具备判断数据是否可信的能力。我的做法是在软件里加一层合理性校验:每个通道设定物理上可能的范围,超出就标记无效;相邻两次采样变化超过设定斜率也标记可疑;多个相关参数之间做交叉验证,比如水深突然归零但温度正常,很可能是水深传感器断线而不是真的没水。
这些校验规则不需要很复杂,但能大幅减少误报和脏数据。校验结果可以体现在本地显示的状态灯上,也可以随数据一起上报,让后台知道这条数据可不可信。实际运维时,这个设计能省下大量排查时间。
5. 常见问题与排查技巧实录
5.1 读数跳动大,先查电源还是先查信号
ADC读数跳动是最常见的问题。我的排查顺序是:先看电源纹波,再看信号链路,最后看软件滤波。用示波器看ADC参考电压和模拟供电,如果有明显纹波,先解决电源;电源干净的话,看传感器输出本身是否稳定,可以用万用表对比;如果信号源稳定但ADC读数跳,再检查采样时间和前端阻抗匹配。
有个容易被忽略的点:数字地和模拟地的处理。如果地平面分割不当,数字部分的回流会污染模拟地,表现为读数随通信或屏幕刷新而跳动。这种问题的典型特征是“有规律地跳”,比如每次串口发送时跳一下。遇到这种情况,重点检查地线布局和去耦电容。
5.2 传感器标定后过几天又偏了
标定后短期又偏移,通常有几个原因:传感器探头污染、标准液失效、温度变化未补偿、零点漂移。排查时先确认探头是否清洁,pH探头表面结垢会明显影响响应;再确认标准液是否在有效期内且保存得当;然后看温度补偿是否生效;最后考虑是否需要重新做零点校准。
长期部署的系统,我建议把“定期校准提醒”做进软件里,按运行时间或者按数据漂移趋势提示维护。这个功能不复杂,但对保证数据质量很有用。
5.3 通信时断时续怎么定位
通信不稳定先分清是模块问题、供电问题还是协议问题。我的排查表是这样的:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 发射瞬间复位 | 供电电流不足 | 测发射时电压跌落,加大电容或独立供电 |
| 数据偶发错误 | 校验不足或干扰 | 加CRC校验,检查天线位置和屏蔽 |
| 长时间无数据 | 模块死机或链路断开 | 加心跳和超时重连机制 |
| 距离近正常远就断 | 天线或功率配置 | 检查天线匹配和发射功率设置 |
5.4 现场部署的独家避坑心得
最后分享几条从实际部署里换来的经验。第一,传感器线缆尽量短,必须延长时用屏蔽线并且屏蔽层单端接地,长线缆是干扰的天然天线。第二,所有接插件都要做防呆和防水,现场维护人员不一定按规范操作,插反、进水都是常事。第三,系统要留一个“恢复出厂设置”的物理或者软件入口,参数配乱了能一键回到可用状态。第四,日志要记,哪怕只是把关键事件写进一个小文件,出问题时这些记录就是排查的线索。
我个人在实际操作中的体会是,这类系统的难点从来不在某一个单点技术,而在于把采集、处理、显示、通信、防护这些环节都做到“够用且稳定”。每一个环节单拿出来都不复杂,但要让它们长期协同工作,就需要在设计和调试时多想一步。比如采样调度用硬件触发而不是软件轮询,比如参数改动立即持久化,比如给每个通道加合理性校验,这些做法单独看都是小事,合在一起就决定了系统是“能演示”还是“能部署”。如果你正准备做类似的项目,建议先把标定和电源这两块打扎实,剩下的部分会顺很多。