ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32实战:WaterSensor水位传感器与OLED显示完整方案

2026/9/9 7:22:20 拓冰建站 浏览量
STM32实战:WaterSensor水位传感器与OLED显示完整方案 搞嵌入式的小伙伴迟早都会撞上一个需求判断水箱里有没有水、水位大概多高。我之前做鱼缸自动补水装置时选了一块很经典的WaterSensor水位传感器模块搭配手头现成的STM32F103C8T6最小系统板和一块0.96寸OLED屏拼了一套完整的水位监测显示方案。这篇文章就把整个项目的硬件原理、接线方式、软件实现和实测标定过程完整写一遍给准备做类似项目的朋友一个能直接参考的范本。这不是什么高深的东西但里面坑不少——传感器电解腐蚀、ADC读数跳变、OLED I2C白屏我都踩过一遍。文章会按“硬件原理→选型接线→软件机制→完整代码→实测标定→进阶避坑”的顺序来写新手可以照着做老手也能在避坑部分找到点共鸣。1. 先说清楚WaterSensor水位传感器到底是个什么东西1.1 模块外观与工作原理市面上最常见的WaterSensor水位传感器模块蓝色PCB表面排着两行像梳子一样交错裸露的铜箔走线板上还带一枚LM393比较器芯片和一个蓝色电位器。官方名字一般叫“Water Sensor Module”被很多人当成简易雨滴传感器用其实就是同一个原理。工作原理一句话能说清两排梳齿状的铜箔在潮湿环境下等效为一个可变电阻水位越高、水浸没的梳齿面积越大电极之间的导电通路越宽等效电阻越小。模块内部再接一个电阻做分压把这变化的电阻转换成变化的电压从AO口模拟输出。因为水是导电的本质上它测的不是“多深”而是“水浸湿了多少面积”这一点后面标定时很重要。模块一般有四个对外接口VCC供电脚支持3.3V到5V、GND地、AO模拟输出脚、DO数字输出脚。DO口是板上LM393比较器的输出端当AO电压超过电位器设的阈值时DO会跳变触发适合用来做“有没有水”这类开关量报警不需要占用ADC。1.2 这种传感器的硬伤电极电解腐蚀先说一个必须给大家打预防针的事。这类电阻式水位传感器最典型的死法不是摔坏也不是进水短路而是长期直流供电导致的电极电解腐蚀。因为检测原理默认两排电极之间有水充当电解质直流电加在上面时间一长铜箔表面的铜离子会持续发生电化学反应往水里跑。用一段时间你会发现梳齿发黑发绿表面出现绿锈一样的氧化物测量值越来越漂到最后传感器直接“失灵”。这不是模块质量问题是电阻式湿度检测的基本物理限制。所以我在项目里一贯的做法是不要给模块长时间连续通电。用STM32的一个GPIO作为传感器VCC的电源开关测量时给电采完样马上断电。这样既能延长探头寿命也能减少电解副产物对水质的影响。就算不加开关也能靠程序控制每次上电后延时50ms再采样采完立刻把供电脚拉低效果很明显。后面第6章我会详细说这个电路怎么做。2. 实验硬件选型与接线把三样东西安全地连起来2.1 这套方案里我用了什么先把自己的实验配置列出来给准备复现的人一个参考器件型号/规格用途主控板STM32F103C8T6最小系统板ADC采样、I2C控制、逻辑处理水位传感器Water Sensor模块水位信号采集显示屏0.96寸OLEDI2C接口SSD1306驱动显示水位百分比和状态杜邦线母对母若干连接各模块电源USB转TTL模块输出3.3V或板载稳压3.3V供电OLED选SSD1306驱动的0.96寸屏是因为这屏功耗低、接线少、库成熟。128x64分辨率虽然不大但显示“水位 45%”这种信息绰绰有余。如果手头是SPI接口的OLED原理类似只是接线多了两根DC和CS代码改动也不大。2.2 完整接线表按照CubeMX的默认引脚映射来最省事WaterSensor引脚接到STM32引脚说明VCC3.3V建议3.3V供电后面细说为什么GNDGND共地AOPA0模拟输出接ADC1_IN0DOPB0数字输出做阈值报警测试OLED引脚接到STM32引脚说明VCC3.3V显示屏供电GNDGND共地SCLPB6I2C1时钟线SDAPB7I2C1数据线PA0作为ADC1的通道0是STM32F103上最顺手的一个ADC引脚。PB6和PB7硬件上就是I2C1的外设引脚不用软件模拟I2C省心。2.3 为什么传感器供电建议用3.3V这是很多新手容易踩的坑。WaterSensor模块说明书上写着供电3.3V到5V都能工作但AO输出是从自己的供电电压分压出来的。如果用5V给模块供电水位很高的时候AO电压会接近5V远超STM32 ADC的3.3V输入范围轻则采样值永远是满量程4095严重的话直接损伤芯片引脚。所以在这套实验里传感器VCC一律接3.3V让AO输出电压被限制在0~3.3V范围内ADC不用加额外的分压电路。你如果非要用5V供电AO输出之后必须加电阻分压或者用运放做电平转换但实在没必要给自己找这种麻烦。DO口同样建议用3.3V供电这样DO输出的高电平就是3.3V可以直接接STM32的GPIO。接线时注意共地所有模块的GND都必须连到同一根地线上否则电平参考点不一致读数会乱跳。3. 软件层面先搞懂两件事ADC采样与SSD1306驱动3.1 STM32的ADC采样原理STM32F103C8T6内部集成的ADC是12位逐次逼近型可以把0到3.3V的模拟电压量化成0到4095的数字值。理解起来就是一把电压尺4096个刻度分辨率大约是3.3V除以4096约0.8mV。水位传感器的AO输出电压随水位上升而增大ADC值也跟着增大。代码里最核心的一步就是把ADC原始值换算成电压float voltage (float)adc_value * 3.3f / 4095.0f;搞明白这个换算关系之后做标定、做百分比显示就顺理成章了。ADC配置里有几个关键参数要留意首先是分辨率F103固定12位其次是采样时间我强烈建议把采样周期拉长比如选239.5个周期。传感器AO输出不是理想稳压源液体表面的波动、微弱的电化学反应都会让电压有一些缓慢起伏采样时间拉长等于每个采样点让内部采样电容充分充电读数更稳。3.2 OLED显示的核心SSD1306与I2C协议SSD1306是一颗常见的OLED驱动芯片内置128x64的GRAM也就是每一像素的亮灭状态都对应显存里一个bit。我们往SSD1306的GRAM写数据屏幕就会按这个显存内容刷新显示。I2C协议只需要两根线时钟线SCL和双向数据线SDA。所有设备都挂在同一条总线上每个设备有一个从机地址SSD1306默认地址一般是0x3C少数是0x3D。STM32作为主机通过HAL库的HAL_I2C_Mem_Write函数向SSD1306写入命令或数据即可。驱动逻辑可以分为三步发送初始化命令序列、清屏、更新指定显示区域。市面上开源的SSD1306驱动代码很多找一份能跑通的改改引脚配置就能直接用。关于OLED显示还有一个常见的需求是显示汉字。SSD1306本身不带字库要自己准备字模数组。用PCtoLCD2002或者取模软件设置成横向取模、16x16点阵把“水位”“报警”这样的字转成十六进制数组再在驱动里实现一个画点函数把字模数据搬到GRAM对应位置。实测下来128x64的屏同时显示两行16x16汉字加一行数字完全够用。3.3 为什么不能直接读ADC原始值当水位有些新手直接把ADC值和0到100做映射比如percent adc_value * 100 / 4095然后发现读数在一两秒内反复跳好几个百分点怀疑是传感器或者接线坏了。其实这是正常现象——水面本身在晃、水在电极之间形成的水膜厚度在变化、以及ADC本身有量化噪声都会导致原始数据抖动。正确做法是采样后做软件滤波。最实用的是两种平均值滤波连续采几十次去掉最高最低后求平均适合水位变化缓慢的场景滑动中值滤波每次采集都替换队列里最旧的值取中位数作为输出响应速度比平均值滤波快顶级不增、谷底不降。水位监测这种场景变化很慢我用的是20次采样取平均值加去极值效果很好数字稳定在±1%以内。4. 完整代码实现从ADC采集到OLED显示4.1 CubeMX里的关键配置我习惯先用STM32CubeMX生成工程框架再补业务逻辑。关键配置项如下RCCHSE选择Crystal/Ceramic Resonator外部晶振板子一般都有SYSDebug选Serial Wire不然下载一次后调试接口被关了很麻烦ADC1通道IN0PA0打开Continuous Conversion采样时间调到最大239.5 cyclesI2C1Standard Mode速率100kHz不用改时钟树SYSCLK设为72MHzADC预分频设为6分频ADC时钟12MHzF103要求ADC时钟最大14MHz12MHz比较稳。配置完直接生成MDK-ARM工程我用的开发环境是Keil5VSCodeCMake也大同小异。4.2 核心代码段一ADC采集与滤波函数在main.c里我单独写了一个采集函数负责执行一次完整的多点采样并返回平均值#define ADC_SAMPLE_COUNT 20 uint16_t adc_read_average(void) { uint32_t sum 0; uint16_t samples[ADC_SAMPLE_COUNT]; uint16_t max_val 0, min_val 4096; for (uint8_t i 0; i ADC_SAMPLE_COUNT; i) { HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { samples[i] HAL_ADC_GetValue(hadc1); if (samples[i] max_val) max_val samples[i]; if (samples[i] min_val) min_val samples[i]; sum samples[i]; } HAL_ADC_Stop(hadc1); } sum - max_val min_val; /* 去掉极值 */ return (uint16_t)(sum / (ADC_SAMPLE_COUNT - 2)); }这个函数对ADC抖动非常管用。HAL_ADC_Start和HAL_ADC_Stop每次都做一次完整启停虽然效率不高但稳定性好适合水位这种低频采样场景。4.3 核心代码段二水位换算与OLED显示拿到稳定的ADC平均值后换算成水位百分比。这里我不是简单线性映射而是用实测标定得到的一组特征点做分段线性换算防止饱和区算出的百分比失真。在还没有标定数据之前可以先用一个临时公式跑通流程float voltage adc_read_average() * 3.3f / 4095.0f; uint8_t percent (uint8_t)((voltage - water_min_vol) / (water_max_vol - water_min_vol) * 100.0f); if (percent 100) percent 100; if (percent 0) percent 0;其中water_min_vol是干燥状态下的电压值water_max_vol是传感器完全浸没时的电压值这两个值一定要实测确认不能拍脑袋填。OLED显示部分直接用现成的SSD1306驱动接口oled_clear(); oled_show_string(0, 0, Water Level:, 12); oled_show_num(0, 24, percent, 3, 16); oled_show_string(64, 26, %, 16); if (percent 15) oled_show_string(0, 48, LOW, 12); else if (percent 80) oled_show_string(0, 48, OK, 12); else oled_show_string(0, 48, HIGH, 12);我习惯在主循环里定时调用采样和刷新周期500ms避免OLED刷新太频繁造成闪烁和I2C总线负担。HAL_Delay(500)最简单但做实际项目时应该用定时器中断或者RTOS延时这个不展开讲了。4.4 DO口报警的附加实现如果只是想要“水满了就报警”其实可以完全不用ADCOLED上也只需要显示开关量。把DO口接到PB0配置成GPIO输入模式内部上拉打开。模块上电后AO电压低于电位器阈值时DO输出高电平有些模块是低电平有效最好用万用表先量一下水位升高超过阈值后DO翻转。PB0读到电平变化时OLED显示“Tank Full”汉字提醒。DO口方式还有个额外好处LM393内置迟滞比较器输出不会像ADC值那样剧烈抖动做溢流报警非常可靠。5. 实测标定与现象分析拿矿泉水瓶做实验5.1 实验过程记录搭好硬件、烧录程序后我拿一个普通矿泉水瓶和一段橡皮管做了标定实验。过程很简单把传感器竖直放在瓶子里从干燥状态开始一笔一笔加水每加一定水位停顿几十秒等OLED读数稳定后记录ADC原始值和显示百分比。用手机拍下屏上的数值手动记录在表格里。这里贴一组我实测得到的参考数据注意不同模块批次、不同水质、不同供电情况下差异很大必须自己重新标定传感器状态ADC值AO电压(V)对应水位%完全干燥1780.140底部刚触碰水5120.418浸没约1/4梳齿12401.0032浸没约1/2梳齿22151.7863浸没约3/4梳齿28702.3185完全浸没32102.5997我用的自来水。如果是蒸馏水或者去离子水因为几乎没有自由离子导电性差到离谱传感器读数基本不会变别拿它们做测试。5.2 线性度分析与饱和问题这组数据显示出一个关键规律水位从干燥涨到浸没一半时ADC值近似线性增长很好用但超过一半后增长率开始放缓完全浸没时ADC值趋于饱和不再随水深继续明显变化会停在一个最大值附近不同供电电压下饱和ADC值不同3.3V供电时大概在3000到3500之间。原因不复杂完全浸没以后两排电极之间已经被水大面积短路再往深了加电阻变化越来越小分压比变化自然进入平台期。所以这类传感器天生不适合做高精度的连续液位测量。真正适合它的场景就三类无水报警、低水位提醒、高水位溢出保护。你要测精确到厘米级的液位老老实实去用超声波传感器或者压力式液位计。如果一定要用这模块做百分比显示就要在程序里做非线性补偿不能做直接的0~4095线性映射。我在上面的换算函数里只用了干燥和完全浸没两个电压点中间靠线性近似所以在60%到80%区间显示值会和实际水位偏高。更讲究的做法是把第5.1小节的数据表做成查表数组在表值之间做插值显示误差能压到3%以内。5.3 调整DO阈值电位器DO口的使用需要先调板子上的蓝色电位器。方法把传感器放在目标报警水位用万用表量AO端口电压记下这个阈值电压再用一字螺丝刀慢慢拧电位器拧到DO输出刚好翻转为止。这里有个非常容易踩的坑手捏着螺丝刀拧电位器时人体会带入感应电压导致阈值判断偏差。正确的操作顺序是每拧一点就松手等两三秒再观察DO电平变化不要一边拧一边盯着电平表看。另外DO输出的有效电平方向在模块之间不统一有的模块有水时DO输出低电平有的输出高电平接线前先看模块背面丝印或者用万用表量一遍。6. 实际项目中的进阶设计与我踩过的坑6.1 如何延长传感器寿命电源开关电路前面说过长期给WaterSensor模块通直流电几天到几周后电极就会腐蚀发黑。为了解决这个问题我在VCC通路上加了一个小功率MOS管做电子开关STM32用一个GPIO控制MOS管的Gate。电路思路很简单模块VCC通过MOS管接到3.3VGPIO输出高电平拉通MOS管传感器得电采样结束后GPIO拉低传感器彻底断电。程序里采样流程变成开电源→延时50ms等待电压稳定→连续采样→关电源→更新OLED。实测下来同样一块传感器常通电方案用半个月读数就开始漂这套开关方案用了三个多月ADC空载和满水值基本没变。不想加MOS管的人也可以用三极管做开关或者干脆用一个空闲GPIO输出高电平给传感器VCC但GPIO的驱动能力有限实测部分模块工作电流十几毫安勉强够用长期使用还是建议加个MOS管。6.2 多路水位检测的扩展方案项目扩展开以后可能要同时监测多个储水容器。方案有三条路每个容器放一个WaterSensor模块各自AO接不同的ADC引脚PA0、PA1、PA2这类引脚都行。但STM32F103C8T6只有10个ADC通道接多了引脚不够。用CD4051多路模拟开关把8路AO导入同一路ADC分时切换采集。省引脚但需要额外控制线。关键容器用独立的DO口做报警不需要精确液位而是阈值量。OD口直接接GPIO可以搞好几路。我最常用的是第三类精确测液位只选最关键的容器其他容器一律用DO报警简单粗暴还省电。6.3 我踩过的几个具体的坑第一OLED白屏或者显示乱码。大多数情况并不是代码问题而是I2C总线的上拉电阻缺失。部分OLED模块自己带了上拉有些没有没有上拉时SDA和SCL信号一直在低电平附近抖动SSD1306完全无法识别。解决方案在SCL和SDA上各接一个4.7kΩ电阻到3.3V问题立刻消失。第二HAL_I2C_Mem_Write返回HAL_ERROR。除了接线松了、地址填错这些常规原因之外还有一个隐蔽问题是时钟太快。我把I2C时钟从100kHz调到了400kHz个别OLED模组分压不足就罢工回到100kHz就稳定了。SSD1306虽然支持400kHz快速模式但很多没牌子的OLED模组走线工艺一般跑400kHz容易出问题。第三ADC读数有一个明显的50Hz抖动。这个问题是我在鱼缸附近做实验时发现的水泵一开显示屏上的数字就规律性跳。原因很简单家里的电网通过水泵电机把工频干扰耦合进了电源AO信号也跟着抖。解决办法是ADC采样次数加倍并且把采样时刻和市电周期错开或者干脆做滑动平均效果立竿见影。再加一个100nF的滤波电容在传感器AO和GND之间也有明显改善。第四传感器用久了出现“干的时候读数也很大”的情况。这通常是电极表面已经形成一层导电的水垢或氧化物相当于干燥状态下也有漏电流路径。处理办法是断电后用棉签蘸酒精或者白醋轻轻擦拭梳齿表面把氧化层擦掉再用纯净水冲净晾干多数能救回来。如果擦完还是漂那就说明腐蚀穿透了铜箔换新的吧。前面这些内容都是我在实际项目里验证过或者踩过的坑。尤其是传感器电解腐蚀和ADC读数抖动这两件事基本每个第一次用这类模块的人都会撞上。如果你正准备做鱼缸自动补水、水箱液位提醒或者阳台雨滴感应这类项目这套“STM32WaterSensorOLED”的流程可以直接照搬原理搞清楚之后换成其他型号的MCU也只是改引脚配置的问题。最后那句老话送给大家想让传感器活得久别让它长时间在带电状态下泡水澡比什么优化都管用。