资讯动态

STM32流体水位传感器实战:从选型到ADC采集与滤波

发布时间:2026/10/3 20:18:17 来源:尧图企业网站定制
学习STM32的流体水位传感器做水位传感器之前先问自己一个问题你到底要测什么是只想知道水箱里“有没有水”还是想知道“有多少水”这两种需求对应的传感器方案完全不一样成本能差出十倍不止。我在做鱼缸水位监测和户外水箱液位远程上报时把市面上主流的几种方案都试了一圈踩了不少坑这篇就把整个学习与实操过程完整记录下来从传感器选型、硬件接线、STM32端代码实现到常见的坑和排查方法一次说清楚。这套项目非常适合刚入门STM32的同学拿来练手。它不像电机控制那样需要调PID、不像以太网通信那样上来就要啃协议栈水位传感器涉及的只是GPIO读取、ADC采集、简单的滤波算法和串口输出全是STM32最基础的外设应用。但你别小看它里面涉及的传感器原理、信号调理、抗干扰处理都是工业现场实打实会用到的东西。做完这个项目你对“传感器数据是怎么一步步变成单片机里能用的数字量”这件事会有一个完整的认识。1. 项目整体拆解先搞清楚你要测的是“有”还是“量”1.1 水位测量方案的根本差异市面上的流体水位传感器说白了分成两大类。第一类是离散量输出只告诉你“触发了”或者“没触发”典型代表是浮球开关、光电式液位开关、电极式水位开关。第二类是模拟量或数字量输出能告诉你“当前液位大概在什么位置”典型代表是压力式液位变送器、超声波液位计、电容式液位传感器。这个项目我建议你两类都做一遍。原因很简单离散量那类电路简单、逻辑清晰适合第一天就上手你把GPIO配置、上下拉、中断这些概念全过一遍模拟量那类需要ADC采样、滤波、标定直接把你从“点灯选手”拉升到“会处理真实模拟信号”的段位。把这两套都跑通了市面上90%的传感器你拿到手都知道该怎么接、怎么读了。1.2 传感器类型的横向对比与选型逻辑先把市面上常见的几种流体水位传感器拉个表对比这是我在选型时整理出来的传感器类型输出信号精度成本寿命适合场景浮球开关开关量通/断无极低一般机械活动件易卡死水箱低液位报警、水泵防空转光电式液位开关开关量无低长无机械活动件医疗设备、家电水箱电极式水位传感器开关量/电阻变化无/低极低中电极易极化腐蚀导电液体、最简单的水位报警电容式液位传感器模拟电压/数字I2C中中长非接触式管道内液位、腐蚀性液体压力式液位变送器4-20mA / 0-3.3V高中高长水塔、深井、工业储罐超声波液位计串口/PWM/模拟高高长大型容器、开放式水池选型的核心逻辑就两条第一你的液体是什么性质。导电液体可以用电极式的纯水、盐水都行但酒精、油这类不导电的液体就别想电极了。第二你要测的是一个点还是一个连续值。要是只想防止水泵空转浮球开关几十块钱就解决问题没必要上超声波。我最初贪便宜买了不带信号调理的电容式传感器直接接STM32的ADC引脚读数飘得像得了帕金森。后来才明白很多传感器模块和裸传感器完全是两码事模块上已经帮你把信号调理电路做好了输出阻抗低、带滤波接单片机直接就能用。新手做项目除非你想顺便把模电也学了否则我建议直接买模块不要一上来就啃裸传感元件。1.3 整体系统架构设计这个项目的系统架构并不复杂属于典型的单采集节点水位传感器 → 信号调理模块自带或外接 → STM32 引脚GPIO/ADC ↓ 数据处理滤波/标定 ↓ 串口输出 → 上位机/调试助手 ↓ 阈值判断 → 报警/继电器驱动STM32在这个系统里干的事很纯粹采集、处理、输出。核心的技术点就三个GPIO读取开关量、ADC采集模拟量、UART输出结果。再加上一个定时器做软件定时采样整个项目的代码量控制在四五百行以内特别适合作为学习STM32外设的综合性练习。2. 硬件连接与原理从传感器到STM32引脚2.1 开关量传感器的接线方式先讲最简单的开关量水位传感器。浮球开关内部就是一个干簧管或者微动开关串在两根线之间。你只需要把一端接STM32的GND另一端通过一个10kΩ上拉电阻接到3.3V同时引到STM32的一个GPIO引脚上。这里有个关键细节必须加上拉电阻。不加上拉电阻开关断开时GPIO引脚就是悬空的读到的电平完全不确定有时候是高有时候是低能把人逼疯。加了上拉电阻之后开关断开时引脚被电阻拉到高电平开关闭合时引脚被拉到GND逻辑就干净了。STM32的GPIO配置代码我用的标准库写法是这样GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);用HAL库的话更简单直接在CubeMX里把PB0配置成GPIO_InputPull-up/Pull-down选Pull-up生成代码之后读引脚状态就行。注意一个容易错的地方STM32的GPIO输入模式配置成上拉输入之后引脚默认被拉到高电平。所以浮球开关的逻辑我建议做成读到低电平表示“水位到达”读到高电平表示“水位未到达”。因为开关闭合水位到达会强行把引脚拉低这个信号比高电平可靠得多。2.2 模拟量传感器的核心原理模拟量水位传感器就高级一些了输出的电压值随液位变化。以最常见的压力式液位变送器为例它的原理是传感器内部有一个感压膜片液体的静压作用在膜片上膜片变形导致内部的电阻应变片阻值变化经过电桥电路转成微弱的电压信号再经过放大器放大最后通过电压-电流转换电路输出标准的4-20mA电流信号或者0-3.3V/0-5V的电压信号。这里面有一个必须理解的概念4-20mA对应0-100%液位。4mA表示空20mA表示满。为什么不用0-20mA因为4mA这个下限可以用来判断断线如果电流变成0mA说明传感器坏了或者线断了而不是液位为零。工业现场这个设计非常巧妙。如果拿到的是4-20mA输出的传感器STM32的ADC引脚不能直接接因为你测的是电流不是电压需要一个250Ω的精密电阻把电流转换成1-5V的电压。但注意STM32的ADC输入范围是0-3.3V1-5V直接接进去会把ADC烧掉。解决办法是用电阻分压把5V按比例降到3.3V以下或者干脆买那种0-3.3V输出的小量程变送器更省事。2.3 电容式与光电式传感器的补充说明顺手说一下另外两种很多人会碰到的传感器。电容式液位传感器我用来测鱼缸水位的那种原理是利用水和空气介电常数不同导致传感器周围的电容值变化内部电路把这个电容变化转化成电压或数字信号输出。我用的那款是1-Wire总线的和DS18B20温度传感器用同样的通信协议占用一个GPIO就能读数据。这种传感器最大的好处是非接触测量探头防腐蚀放在鱼缸外侧就能感知水位。坏处是容易受温度影响夏天的漂移比冬天明显需要做温度补偿。光电式液位开关内部有一个红外发光管和一个光敏接收管光线通过棱镜反射回接收管。有液体时光线被液体散射掉接收管收不到信号输出状态翻转。这种传感器没有机械活动部件寿命比浮球长得多而且响应极快适合需要频繁检测的场合就是价格稍贵。关于1-Wire总线的驱动网上现成代码很多我不建议从零写时序把别人调通的延时版本改一改能用就行。水位的实时性要求没那么高驱动代码别死磕性能稳定就行。3. STM32端软件实现从ADC采集到数据滤波3.1 开发环境与项目初始化我做这个项目用的开发板是市面上最常见的STM32F103C8T6最小系统板就是所谓“蓝色药丸”那块板子。开发环境用的是Keil MDK5配合STM32CubeMX做初始化代码生成。我知道很多人刚开始都用标准外设库但我的建议是尽早切到HAL库。理由很简单标准外设库官方已经停止维护很久了HAL库虽然代码啰嗦一点但是CubeMX可以帮你生成底层配置尤其是引脚复用和时钟树手写太容易出错。CubeMX里的配置流程是这样的选好芯片型号STM32F103C8Tx在Pinout视图里把PA0配置为ADC1_IN0PA9和PA10配置为USART1的TX和RX然后RCC里把HSE设成Crystal/Ceramic ResonatorSYS里Debug选Serial Wire不然你会发现下载完程序之后第二次就无法下载了这是新手最常见的坑之一。时钟树直接选最大72MHz生成的代码就能跑。3.2 ADC采集代码实现用HAL库读取ADC的值代码非常简洁// 在main.c中 uint16_t adc_value 0; float voltage 0.0f; float water_level 0.0f; while (1) { // 软件触发ADC转换等待转换完成 HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { adc_value HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); // 将ADC原始值转换为电压值STM32的ADC是12位的参考电压3.3V voltage (float)adc_value / 4095.0f * 3.3f; // 假设传感器输出0.5V对应空2.5V对应满 water_level (voltage - 0.5f) / (2.5f - 0.5f) * 100.0f; if (water_level 0.0f) water_level 0.0f; if (water_level 100.0f) water_level 100.0f; printf(ADC Value: %d, Voltage: %.2fV, Water Level: %.1f%%\r\n, adc_value, voltage, water_level); HAL_Delay(500); }注意我用了printf这需要重定向。在Keil里用微库的话重定向函数写成#include stdio.h int fputc(int ch, FILE *f) { while (USART1-SR (1 7) 0); // 等待发送寄存器空 USART1-DR ch; return ch; }这是在标准库模式下重定向串口输出的常见写法配合串口助手就能看到打印数据了。3.3 数据滤波真实采集数据的痛点如果你照着上面的代码做完大概率会遇到一个让你头大的问题ADC读到的数据来回跳液位明明没变数值却在上下波动。这是所有做模拟采集的人都要过的第一关叫噪声。噪声的来源有三个主要路径。一是电源纹波STM32的3.3V供电如果不干净参考电压就不稳定ADC自然跟着跳。二是传感器本身的信号波动压力传感器的膜片受到液体扰动输出有小幅抖动是正常的。三是电磁干扰导线靠近电机、继电器这些感性负载时会把干扰耦合进来。解决方案从软到硬分三层。硬件上在传感器电源和地之间加100nF去耦电容靠近STM32的VREF引脚也要加。软件上就要上滤波算法。我实测下来最稳的是中位值滤波加滑动平均的复合滤波法。思路也很朴素先连续采5个值取中间的那个去掉最大最小值避免突发干扰。然后把有效值放进一个长度为10的队列每次取平均。这样既滤掉了随机噪声又能对真实的液位变化保持一定的响应速度。核心代码实现#define FILTER_BUFFER_SIZE 10 uint16_t filter_buffer[FILTER_BUFFER_SIZE] {0}; uint8_t filter_index 0; uint16_t sensor_filtered_read(void) { // 中位值滤波取5次采样的中间值 uint16_t samples[5]; for (int i 0; i 5; i) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 100); samples[i] HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); HAL_Delay(10); } // 简单的排序找中间值 for (int i 0; i 4; i) { for (int j i 1; j 5; j) { if (samples[j] samples[i]) { uint16_t temp samples[i]; samples[i] samples[j]; samples[j] temp; } } } uint16_t median samples[2]; // 滑动平均滤波 filter_buffer[filter_index] median; filter_index (filter_index 1) % FILTER_BUFFER_SIZE; uint32_t sum 0; for (int i 0; i FILTER_BUFFER_SIZE; i) { sum filter_buffer[i]; } return (uint16_t)(sum / FILTER_BUFFER_SIZE); }你说这个算法是必要的吗如果只是Demo演示确实是锦上添花。但如果你要把这套东西用在控制水泵上一天到晚因为噪声导致误动作不是水泵频繁启停损坏电机就是该报警时没报警水漫金山。真实项目里软件的抗干扰能力比硬件的性能参数更值钱。3.4 多点监测与阈值控制只测一个水位点还是有点浪费我后面扩展成了三路监测一路在鱼缸底部防漏水干烧、一路在中间正常水位范围、一路在顶部防止溢流。每一路接一个独立的浮球开关分别接到STM32的三个GPIO引脚。三路监测的代码逻辑如下#define SENSOR_BOTTOM_PIN GPIO_PIN_0 // PB0 #define SENSOR_MIDDLE_PIN GPIO_PIN_1 // PB1 #define SENSOR_TOP_PIN GPIO_PIN_2 // PB2 typedef enum { LEVEL_LOW, // 低液位需要补水 LEVEL_OK, // 正常液位 LEVEL_HIGH // 高液位需要停止补水/报警 } WaterLevelStatus; WaterLevelStatus check_water_level(void) { uint8_t bottom GPIO_ReadInputDataBit(GPIOB, SENSOR_BOTTOM_PIN); uint8_t middle GPIO_ReadInputDataBit(GPIOB, SENSOR_MIDDLE_PIN); uint8_t top GPIO_ReadInputDataBit(GPIOB, SENSOR_TOP_PIN); // 注意开关闭合时引脚为低电平(0)表示该位置已接触水 if (top 0) return LEVEL_HIGH; if (middle 0) return LEVEL_OK; if (bottom 0) return LEVEL_LOW; return LEVEL_LOW; }有了这个状态机之后你在主循环里定期检查然后根据状态决定是否需要打开补水泵或者触发声光报警器这套逻辑和工业上水塔全自动液位控制是同一个思路只不过工业上会用PLC来做可靠性要求更高。4. 调试过程与问题排查我踩过的那些坑4.1 串口输出乱码的真相做调试第一步肯定是把数据打印出来看但我第一次上电串口助手输出的是满屏乱码。排查过程倒是很快因为常识告诉我这类问题要么是波特率不对要么是时钟频率不对。我用的是USART1CubeMX里配置的波特率是115200串口助手也选的115200这没问题。问题出在时钟上——外部晶振没焊或者虚焊的时候系统会默认用内部8MHz时钟而CubeMX代码是按72MHz生成的所有外设时序配置实际跑8MHz算出来的波特率自然就是错的。判断方法很简单看串口的实际波特率。如果预期115200实际499200左右多半就是这个问题。解决办法是检查开发板上的8MHz晶振是否焊好或者直接把CubeMX里的时钟源改成内部时钟配合重新计算外设参数。4.2 ADC所有引脚都读到4095的排查还有一个特别坑的现象ADC读出来永远是4095满量程。不管怎么改变量液位数值纹丝不动。排查过程比较曲折首先怀疑传感器坏了单独测传感器发现有输出排除。然后怀疑ADC配置问题重新检查CubeMX配置也没错。最后发现问题出在供电上。我用的那块运放信号调理模块需要12V供电我用的是一个劣质DC-DC模块纹波巨大导致整个系统的GND电平不稳。ADC的参考电压虽然还是3.3V但相对于传感器信号的地已经被抬高了不少读出来自然就是满量程。这是个常见到不能再常见的问题共地。传感器、信号调理模块、STM32开发板、电源模块所有设备的GND必须可靠地连接在一起。如果有任何一个设备的GND是浮空的模拟信号采集就会出错。我在后来做一个温湿度计项目时也碰到过一模一样的现象排查步骤刻在脑子里了。4.3 水位数据的温度漂移问题电容式传感器在夏天使用时会遇到另一个麻烦温度漂移。气温从25℃升到35℃即使水位一点没变ADC读数也会自己往下掉。这是因为水的介电常数受温度影响温度越高介电常数越低传感器感知到的电容变化随之变小。解决办法有两个方向。硬件方向是在传感器信号输出端加一个高通滤波器只让液位变化的交流分量通过把缓慢的温度漂移滤掉但这样对需要静态精确测量的场景不友好。软件方向是加温度传感器做补偿实测出温度和水位读数的对应关系做一个线性拟合在代码里边查边补。如果只是做学习项目我不建议在温度补偿上花太多时间。你只要知道这个现象存在并且在系统设计的时候预留一个温度传感器的引脚就够了。4.4 常见故障速查表整理一个表格拿到手直接对着排查故障现象可能原因排查顺序与解决措施串口输出乱码时钟配置异常、波特率不匹配先核对CubeMX时钟树是否72MHz再看串口助手波特率与代码一致ADC读到固定满量程/零传感器未接、共地不良、参考电压不稳先万用表量传感器输出再检查所有设备的GND是否共地读数跳动大电源纹波、引脚悬空、软件未滤波加去耦电容确认引脚不悬空上中位值滑动平均滤波浮球开关不触发上拉电阻缺失、引脚配置错误万用表测开关通断检查GPIO是否配置成上拉输入传感器接上后系统复位引脚灌入大电流、电源供电不足检查信号电平是否超过3.3V提高电源功率下载程序后第二次无法下载SWD引脚被禁用或占用在CubeMX SYS里选Serial Wire按住复位键尝试下载4.5 抗干扰处理的关键细节这部分是我自己反复吃过亏才总结出来的经验价值要比前面所有代码加起来都高。第一传感器的信号线尽量用双绞线或者屏蔽线屏蔽层单端接地。我最初图省事用普通的杜邦线把传感器连接到开发板走线还和继电器控制线绑在一起结果只要继电器一动作ADC读数就剧烈跳动。第二STM32的电源脚要加两个电容一个大容量电解电容100μF和一个高频去耦电容100nF并联。电解电容保证瞬态电流需求去耦电容滤掉高频噪声。别觉得这是玄学我见过太多项目最后卡在电源不稳定上。第三如果你要用继电器驱动水泵注意继电器的线圈是一个大电感断电瞬间会产生反向电动势这个电压可以达到上百伏。必须在继电器线圈两端反向并联一个1N4007二极管把反向电动势泄放掉。不然轻则导致系统复位重则击穿STM32的GPIO引脚。5. 拓展玩法从单点监测到智能控制系统5.1 联动补水泵和声光报警器做完基本的液位检测之后我很自然就想让它干点实际的活于是给它接上了继电器模块驱动一个12V的小水泵做自动补水。控制逻辑是这样的当检测到LEVEL_LOW时打开继电器补水当检测到LEVEL_OK时关闭继电器停止补水当检测到LEVEL_HIGH时除了关闭继电器还要拉响蜂鸣器报警提示有溢流风险。继电器模块的控制端是一个低电平有效的输入引脚用三极管或光耦隔离驱动。接到STM32之后代码逻辑非常直接// 假设继电器控制引脚是PB12 #define RELAY_PIN GPIO_PIN_12 void control_pump(WaterLevelStatus level) { switch(level) { case LEVEL_LOW: GPIO_SetBits(GPIOB, RELAY_PIN); // 高电平 继电器断开 水泵停止 break; case LEVEL_OK: GPIO_ResetBits(GPIOB, RELAY_PIN); // 低电平 继电器吸合 水泵启动 break; case LEVEL_HIGH: GPIO_SetBits(GPIOB, RELAY_PIN); // 同时触发蜂鸣器报警 break; } }这里有个很关键的防抖问题浮球开关在水面波动的时候会反复通断如果直接控制继电器继电器会跟着不停地吸合释放几天就烧坏了。必须做软件防抖——连续多次读取到相同状态才认为状态真的变了。最简单的实现是连续采样10次如果全部是低电平才判定为水位到达中间有任何一次是高电平就重置计数。5.2 增加远程监控与数据记录串口打印数据只在开发调试时有用真正使用环境里人不可能一直盯着串口助手。我在后期把USART1的数据通过一个TTL转WiFi模块ESP8266发到了局域网内手机浏览器打开一个网页就能看到当前水位和温度数据。其实就是在ESP8266里做了一个极简的HTTP服务器STM32通过串口把封装好的数据帧发给ESP8266ESP8266解析之后渲染成网页。这里不展开细说但把通信协议设计一下// 协议帧格式帧头 设备ID 数据长度 水位值 温度值 状态字 校验和 // 帧头: 0xAA 0x55 // 示例: AA 55 01 04 00 64 1E 01 93STM32端每隔500ms就发一帧ESP8266收齐一帧之后校验通过更新页面上的数据。这套协议的写法虽然简单但涵盖了通信协议设计的核心要素帧同步、数据校验、状态上报。后面如果你想接云平台或者用MQTT上报数据思维模型是一样的。5.3 学习路径建议做完这个项目下一步该学什么如果你顺着这篇博客把代码都跑通了我建议你按下面几个方向继续深入。第一把模拟量的压力式液位传感器换成MODBUS协议输出的工业传感器学习RS485通信和MODBUS-RTU协议。STM32的USART支持方向控制接一个MAX485芯片就能搞定。这个方向对应的是工业现场的真实应用场景找工作面试能聊的东西就多了。第二用定时器输入捕获功能做超声波液位计。超声波模块输出一个与距离成正比的脉宽信号定时器捕获脉宽就能算出距离联动物位高度。这个项目能帮你把STM32定时器这个核心外设吃透比干看视频学定时器有意思多了。第三增加一块OLED屏幕做本地显示把水位、电量、报警状态全部可视化。STM32驱动I2C接口的OLED几乎是必学技能代码量不大但能锻炼你读时序图和调试软件I2C的能力。6. 实操心得从这套项目里总结出的经验整个项目做下来我最深的体会是三件事。第一传感器选型比代码重要。代码写得再漂亮传感器选错了一切白搭。接触被测量之前先搞清楚液体的性质、温度范围、容器形状、量程精度再决定用哪种传感器。我见过不少人花几百块买超声波液位计结果用在一个要测0.5米小水箱的场景里超声波盲区就有0.2米完全测不了。选型这事一定是先算需求再掏钱。第二做嵌入式开发一定要先把手上的开发环境搞稳定。Keil MDK5加STM32CubeMX的组合我用了很多年中间踩过的坑包括Keil5安装STM32芯片包时要选对应版本否则找不到芯片型号CubeMX生成的工程默认编译优化级别可能导致局部变量乱优化调成-O0能省掉很多莫名其妙的bug下载器用ST-Link V2时如果提示固件升级千万别手贱取消不然下一次连接就会报错。开发环境不顺项目连开始都开始不了。第三数据滤波不是简单调参而是理解信号本身。刚开始调滤波参数的时候我总想找一组“万能参数”结果发现水位变化快的时候要求响应速度快水位稳定时要求滤波力度大根本不可能两全。后来我把采样频率和滤波窗口分开考虑采样频率提高之后滑动平均窗口对真实变化的延迟缩小了滤波效果也能保持。这个过程中你对十进制转二进制、浮点运算精度、CPU周期开销的理解都会被逼着加深这些才是嵌入式开发最有价值的部分。我在做这个项目的过程中翻阅了大量网上资料其中江科大B站UP主的STM32入门视频对我的帮助很大尤其是对GPIO和定时器的讲解通俗易懂、实操性强。虽然那套视频是基于标准外设库的但里面的外设原理部分完全通用配合现在的HAL库学习效果很好。最后再分享一个实用小技巧做液位监测的时候别只盯着传感器数据本身。你去观察水箱的水位变化曲线其实能判断很多系统状态——水位掉了又涨是有人在用水水位直线下降是漏水了水位长时间不动可能是传感器卡住了。学会了看数据、判断状态你的代码才真正转化为解决问题的能力。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑