news 2026/9/9 10:28:58

STM32驱动HS-S37A非接触式水位传感器并OLED显示完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32驱动HS-S37A非接触式水位传感器并OLED显示完整实战

说句大实话,玩嵌入式这些年,我觉得最容易被新手低估的传感器就是“水位检测”。传统的浮球开关和电极式探头,要么机械结构容易卡死,要么金属电极长期泡水里会电解腐蚀,放到饮水机、鱼缸、加湿器这些场合,卫生和寿命都是大问题。所以当我在项目里用到 HS-S37A 这种非接触式电容水位传感器时,第一反应是:这东西早该普及了。这次就完整记录一遍我用 STM32 驱动 HS-S37A 水位传感器,再把水位数据实时显示到 OLED 屏幕上的全过程,从原理、接线、CubeMX 配置到 HAL 库代码和踩坑记录,一次性讲透。

1. 实验整体设计:为什么选HS-S37A和OLED

1.1 HS-S37A原理:非接触式电容水位检测

HS-S37A 的核心原理,说白了就是“水的介电常数和空气差太多”。空气的介电常数大约是 1,而水在常温下能到 80 左右。传感器探头上有一对铜箔电极,电极之间就等效于一个电容器。当容器里的水位升高,水覆盖到探头贴着的区域时,电极间的等效介电常数会明显变大,电容值也随之上升。这个电容变化非常微小,所以模块里集成了高精度的电容检测芯片,先把电容值转成数字量,再通过内部算法输出水位状态。

我经常给新手打的比方是:你拿两手放在一个感应式台灯旁边,手靠得越近,灯的亮度变化越明显。HS-S37A 就是这个原理,只不过它感应的对象换成了水,而且感知的是“贴着容器外壁的那层液体”。

非接触式水位传感器工作原理图里最关键的检测链路是:铜箔电极感知电容变化 -> 内部CDC电容数字转换器采集 -> MCU逻辑判断水位等级 -> IIC/模拟量输出。因为探头不用接触液体,就没有电极腐蚀和污染的问题,容器里想放什么液体都行,这也是它被大量用在即热饮水机、咖啡机、加湿器里的原因。

1.2 系统架构:传感器、主控、显示怎么连

这套实验的硬件架构非常清晰:HS-S37A 水位传感器作为采集端,STM32 作为主控核心,OLED 作为显示端。传感器本身提供两路输出,一路是 IIC 数字接口,可以直接读到水位百分比;另一路是模拟量输出 AO,电压会随着水位变化,可以接到 STM32 的 ADC 引脚做二次确认。

有人会问:既然 IIC 已经能读水位了,为什么还要模拟量?我实测下来的体会是,IIC 读到的是模块内部算法处理后的等级数据,直接用很方便;而模拟量是原始信号线性映射出来的电压值,更适合自己做标定和细分。如果你想把水位精确到 10% 一档,或者想接一个变量来表示“半杯水”,模拟量会比固定等级更灵活。我在实验里两路都接了,目的就是互相校验。

OLED 我选的是最常见的 0.96 寸 SSD1306 驱动模块,IIC 接口,和 HS-S37A 挂在同一条 I2C 总线上。这样接线非常省事,整个系统只需要用到 STM32 的 4 个引脚(2 个 I2C,1 个 ADC,1 个地),哪怕是面包板上搭实验,线也不会乱。

STM32 这边我选了 F103C8T6,也就是大家俗称的“蓝丸”,价格便宜、资料多,HAL 库工程用 CubeMX 生成,开发效率很高。如果你用的是其他型号,比如 C6T6、G431,或者国产 APM32 这类兼容芯片,思路完全一样,CubeMX 里重新选型号生成工程就行。

2. 硬件接线与工程配置

2.1 材料清单与引脚分配

我这次实验用到的材料清单如下:

材料说明
STM32F103C8T6 最小系统板主控,建议选带ST-Link下载口的
HS-S37A 水位传感器模块非接触式电容水位检测,带探头
0.96寸 OLED SSD1306IIC接口,7针或4针均可
ST-Link V2 下载器烧录和调试
面包板、杜邦线若干搭建电路
塑料水杯最好用薄壁无金属的杯子,壁厚2~3mm以内效果最好

引脚分配我直接给结论:

HS-S37A 引脚STM32引脚说明
VCC3.3V传感器供电,3.3V~5V均可
GNDGND共地
SCLPB6I2C1_SCL
SDAPB7I2C1_SDA
AOPA0ADC1_IN0,读取模拟电压
OLED 引脚STM32引脚说明
VCC3.3V模块供电
GNDGND共地
SCLPB6与传感器共用I2C1_SCL
SDAPB7与传感器共用I2C1_SDA

这里要注意,两个设备共用 I2C 总线的前提是地址不冲突。OLED 的 SSD1306 常见地址是 0x3C,HS-S37A 默认地址按我手头模块的 datasheet 是 0x02(7位地址)。你拿到不同批次的模块可能地址有差异,所以我在后面的排查部分会专门讲怎么扫描 I2C 地址。

2.2 CubeMX配置细节

使用 STM32 HAL 库点亮 OLED 屏幕全流程,以及整个工程的生成,都是从 CubeMX 开始的。打开 CubeMX 新建工程,芯片型号选 STM32F103C8Tx,接下来按这几个关键点配置。

SYS 选项卡里,Debug 一定要选 Serial Wire,否则第一次下载后 SWD 引脚被禁用,下次就连接不上了。Timebase Source 推荐改成 TIM1 或者 SysTick 之外的定时器。为什么?因为 HAL_Delay 依赖 SysTick,如果后面你在中断里调用 HAL_Delay 或者某些外设回调用了延时,SysTick 优先级处理不当会导致 delay 卡死,这是一个非常经典的坑。

RCC 选项卡把 HSE 设成 Crystal/Ceramic Resonator,外部晶振用 8MHz。说到晶振,有人问过 STM32 晶振电容计算,简单说一下:如果晶振负载电容 CL 是 12.5pF,那么匹配电容大约取 C = 2 * CL - 杂散电容(杂散一般估 3~6pF),算下来大概 18~22pF。对低速 I2C 和普通跑代码来说,用 20pF 左右的电容基本不会出问题。

I2C1 开启后,把 I2C Speed Mode 设为 Standard Mode,速度 100kHz。100kHz 对两个设备来说都够用,而且长线抗干扰更好。ADC1 里使能 Channel0(对应 PA0),采样时间我这里建议选 239.5 Cycles,读数会更稳。时钟树里把主频配置到 72MHz,这个不用多说,F103 的常规操作。

生成工程的时候,Toolchain 选 MDK-ARM,版本选你 Keil 能打开的就行。代码生成那边,我习惯勾选 “Generate peripheral initialization as a pair of .c/.h files per peripheral”,这样 I2C 和 ADC 的初始化代码会单独拆出来,后续改起来思路清楚。

2.3 编译环境常见坑:Keil、ST-Link与驱动

工程生成后,用 Keil5 打开,先设置一下烧录器。Options for Target -> Debug 里选择 ST-Link Debugger,然后进 Settings,如果能看到 SW Device 里识别出芯片,说明连接正常。

很多新手第一次插上 ST-Link,电脑设备管理器里出现一个带黄色感叹号的“STM32 Virtual ComPort”,然后怀疑自己板子坏了。其实这个感叹号大多不影响 SWD 下载,它只是 ST-Link 的虚拟串口驱动没装好。你先下载安装 STSW-LINK009 驱动,如果还感叹号,可以用驱动强制更新或者换一根 USB 线试试,我之前遇到过 ST-Link 固件版本过旧导致串口枚举不正常,升级 ST-Link 固件后问题消失。

真正麻烦的报错是:error: no stm32 target found! if your product embeds debug authentication, pl... 这种。我碰到的情况基本就三类:第一,ST-Link 没接好,检查 SWDIO、SWCLK、GND 三根线;第二,程序里禁用了 SWD,或者 CubeMX 中 Debug 没选 Serial Wire,解决办法是按住板子复位键,点下载的同时松开复位,有时候能抢在程序跑飞前连上;第三,目标板供电不稳,单独给板子供 5V,并且 ST-Link 的 3.3V 输出不要同时给太多模块供电。

还有一个容易忽略的点,就是 HS-S37A 和 OLED 都挂在 3.3V 下,如果蓝丸板子上的 LDO 电流输出能力一般,同时带动传感器、OLED、ST-Link 会显得吃力。我建议 ST-Link 供电只给板子,传感器和 OLED 从板子的 3.3V 引脚取电,但杜邦线尽量短、粗一点。

3. 软件实现:从寄存器到OLED显示

3.1 IIC读取传感器数据

先把 HS-S37A 当成一个 IIC 从机设备来看。它的寄存器映射不同厂家略有差异,我手头这个模块,读水位状态的时候,是先发设备地址加寄存器地址,然后连续读两个字节,低字节就是水位百分比。写代码之前,我习惯先写一个简单的“IIC扫描函数”,把总线上所有能响应 ACK 的地址打印出来。这样不用猜设备地址,非常实用。

#define HS37A_ADDR (0x02 << 1) /* 7位地址0x02,HAL中要左移1位 */ #define HS37A_REG (0x01) /* 以手头模块为例的水位寄存器 */ uint8_t hs37a_read_level(void) { uint8_t buf[2] = {0}; HAL_StatusTypeDef status; status = HAL_I2C_Mem_Read(&hi2c1, HS37A_ADDR, HS37A_REG, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); if (status != HAL_OK) { return 0xFF; /* 读取失败统一返回0xFF,调用方自行处理 */ } return buf[0]; /* 低字节是水位百分比 */ }

这里解释一个关键参数:HAL_I2C_Mem_Read中第二个参数是“8位地址”,也就是把 7 位地址左移一位后的值。很多新手在这里纠结,看到 0x01 或者 0x02 就直接填进去,结果通信失败。记住一条规则:ST 的 HAL 库要求传的是“写地址”或“读地址”的完整字节,所以 7 位地址都要<< 1。同理,OLED 的 0x3C 在实际代码里要写成0x3C << 1

读回来的水位百分比,我这里模块是 0~100 的整数,步进 25%,也就是 0、25、50、75、100 这五档。你手里的模块如果读出来是别的格式,比如寄存器高位低位组合,或者连续几个水位阈值,一定以厂家 datasheet 为准。我见过有兄弟不看手册,强行按网上别人的代码来读,读出来全是 255,其实就是寄存器地址和字节序对不上。

3.2 ADC读取模拟量做二次确认

除了 IIC,我还把 AO 引脚接到了 PA0,用 ADC1 通道 0 去读。这里不是为了折腾,而是为了验证传感器读数的一致性。接好水杯后,空杯状态下 AO 电压大约 0.5V,满水时大约 2.2V,不同模块会有差异,所以我没有硬编码电压范围,而是先采集空杯和满水两个端点,再线性映射。

HAL 库的 ADC 读取函数封装得很简单:

uint16_t get_adc_raw(void) { uint16_t value = 0; HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { value = HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); return value; } float get_adc_voltage(void) { return get_adc_raw() * 3.3f / 4095.0f; }

换算成水位百分比时,我用线性映射:

#define V_EMPTY (0.50f) /* 实测空杯电压 */ #define V_FULL (2.20f) /* 实测满杯电压 */ int adc_to_percent(float voltage) { float percent = (voltage - V_EMPTY) / (V_FULL - V_EMPTY) * 100.0f; if (percent < 0) percent = 0; if (percent > 100) percent = 100; return (int)percent; }

这个电压值记得加上滑动平均滤波。我实验中直接读 ADC 时,虽然采样时间选得比较长,但电源上有一点纹波,读数偶尔会跳 1~2 个百分比。我加了一个简单的 8 次平均,数据就非常稳了。

很多人觉得模拟量输出是多余的,但在 HS-S37A 自校准出问题的时候,模拟量能帮你快速判断是传感器硬件问题还是算法问题。因为模拟量直接从原始信号变换过来,不受内部逻辑判断的影响,这在我后面排查故障时帮了大忙。

3.3 OLED显示水位与进度条

OLED 显示这部分,我用的标准 SSD1306 驱动,GND 和 VCC 接好,SCL/SDA 接到 I2C1 总线上。初始化函数网上有大量现成的,核心是把 SSD1306 的显存配置写对,128x64 分辨率、I2C 地址 0x3C。我在工程里做得很简单:先清屏,然后定时读取 HS-S37A 的水位值,刷新屏幕文字和一个简易的百分比进度条。

void oled_show_water(uint8_t level) { char buf[16]; OLED_ShowString(0, 0, "Water Level:"); sprintf(buf, "IIC:%3d%%", level); OLED_ShowString(0, 2, buf); /* 画一个简易进度条:20列像素为一格,满格100% */ uint8_t blocks = level / 5; /* 0~100映射到0~20格 */ OLED_ShowString(0, 4, "[ ]"); for (uint8_t i = 0; i < blocks; i++) { OLED_DrawPoint(i + 1, 4 * 8, 1); /* 逐点画实心格 */ } }

代码里我只写了关键逻辑,实际工程中 OLED 的 128x64 显示字符串、画点函数,用你熟悉的任意 SSD1306 库都可以。重点讲两个细节:第一,OLED 和 HS-S37A 共用 I2C,读传感器和刷屏不要太频繁。我实测 100ms 读一次传感器、200ms 刷一次屏幕完全足够,刷太快既浪费 MCU 资源,也可能因为总线上连续占用导致传感器偶发 NACK。第二,OLED 的显示内容要和自己做的标定一致,别 IIC 读到的是 75%,ADC 换算出来是 70%,看起来就矛盾了。我在实验里把两路数据显示成两行,只是方便观察,真正用的时候选一路作为主数据源。

OLED 显示汉字也是一个常见需求,SSD1306 要显示汉字,需要自己做取模。取模软件生成 16x16 点阵数据,然后在驱动里写一个可以把点阵数据映射到显存的函数。这个内容展开又是一个大话题,这次实验里我用英文就能把状态讲清楚,所以先不折腾中文显示。

4. 实测效果与问题排查

4.1 实测现象与标定

把 HS-S37A 的探头贴在塑料杯外壁,用透明胶带固定,注意探头要与杯壁贴合紧密,中间不能有气泡。上电后,屏幕先显示 0%,然后我往杯子里倒水,水位每上升一截,IIC 读到的百分比会跳变一档。从 0 到 25 到 50 到 75 到 100,响应时间大概在几百毫秒,这个速度对水箱液位监控来说完全够用。

我同时盯着 ADC 换算出来的电压值,它随着水位上升是连续变化的,不会像 IIC 那样跳变。这正好说明了一个设计技巧:如果你要做“水满自动停”这种控制,用 IIC 的 100% 档位判断就足够了;但如果你想做“剩余水量百分比实时显示”,用 ADC 电压线性映射会更平滑,视觉体验更好。

标定的流程我也建议你记录一次。先把空杯状态下的 AO 电压记下来,再把探头贴在杯壁,缓慢加水到接近杯口,记录满水电压。这两个数值填入前面代码里的V_EMPTYV_FULL,整个系统就完成标定了。不同的杯子壁厚、杯子材质、探头贴法都会影响这两个端点,所以拿到传感器别急着抄网上别人的标定值,自己实测一次最靠谱。

4.2 高频问题速查表

我把这次实验以及群里朋友反馈过的典型问题整理成表,方便你直接对照排查:

现象可能原因解决方法
OLED 不显示I2C 地址填错、供电不足、接线松动先扫描 I2C 地址,确认 0x3C;检查 OLED VCC 是否 3.3V;杜邦线重新插拔
传感器读回 0xFF从机地址错误、寄存器地址不对、I2C 总线冲突确认 HS37A 地址,参考 3.1 的扫描方法;核对寄存器映射
水位一直显示 100% 或 0%传感器探头没贴紧容器壁、杯壁太厚、有金属干扰重新贴探头,选择薄壁塑料容器,远离金属物体
IIC 通信偶发失败上拉电阻缺失、总线过长STM32 内部上拉一般够用,但长杜邦线建议外接 4.7k 上拉到 3.3V
加水后读数不变传感器误触发自校准,把当前状态当成了基准断电重新上电,空杯状态下等待稳定后再加水
OLED 和传感器同时使用但屏幕闪总线地址冲突或刷屏太频繁检查地址,把显示刷新间隔拉长到 200ms 以上
ADC 读数跳变电源纹波大、未加滤波加滑动平均滤波;给传感器单独去耦电容

4.3 HS-S37A自校准带来的两个坑

HS-S37A 有一个“自校准”功能,这个功能本来是方便用户免配置的,但它也给实际使用埋了两个坑。

第一个坑是误校准。模块上电后会把当前状态作为初始基准。如果容器里已经有一半水,而你在这个时候上电,模块可能会把“半水”误认为 0% 基准,导致加满水也显示不到 100%。解决办法很简单:每次系统上电时保证容器处于空杯或已知状态,等待几秒让传感器完成校准,再进行水位检测。我用在产品里的时候,会在上电后加一个延时 2 秒再开始读取,并且把传感器初始化状态通过 OLED 打出来。

第二个坑是长期漂移。传感器检测电容变化,本质是对“当前值和基准值差异”的判定。如果环境湿度、容器内的液体介质发生较大变化,可能导致基准漂移。比如你上午测的是清水,下午换成盐水或者牛奶,介电常数不一样,读数就会异常。做项目时一定要明确 HS-S37A 适合的场景是“检测同一种介质的两个状态(有水/无水)”,而不是“精确测量不同液体的水位”。

5. 扩展思路与个人心得

5.1 这套实验还能往哪些方向扩展

这套 STM32 + HS-S37A + OLED 的实验,稍微改一改就能变成很多实际项目的基础。比如热词里有人提到“基于 stm32 的智能台灯”,你可以把水位传感器换成监测桌面水杯,如果水杯没水了,OLED 显示提醒,台灯灯光颜色也自动变化;又比如“stm32 鱼缸”场景,HS-S37A 贴在鱼缸外壁,检测水位低于阈值时,驱动一个小水泵自动补水,这个组合做出来就是一套全自动的鱼缸水位控制器。

如果加上无线模块,比如 ESP8266,或者接机智云这类云平台,就变成一套远程水箱监测系统。手机端能看到当前水位,水少了推送提醒,这已经是一个很完整的物联网课设或者毕业设计方向了。再进阶一点,用多个 HS-S37A 探头分布在不同高度,就能判断“1/4 水位”“1/2 水位”“满水”,每个探头对应一个补水阶段,控制器可以分级加水,避免一次加太多导致溢出。

我在单位做过一次智能加湿器的预研,用的就是类似思路:HS-S37A 贴在储水箱外壁,ST 单片机检测水位,低于 25% 时开进水阀,高于 90% 时关阀,OLED 显示实时水位。整个模块在加湿器原型机上跑了半个多月,没出现电极腐蚀问题,这也侧面说明非接触式方案在消费家电里的实用性。

5.2 关于HS-S37A的两个经验细节

最后分享两个容易被忽略的小细节,都是实际操作中攒下的经验。

第一个是探头贴装。HS-S37A 的探头本身是柔性或半刚性的铜箔电极,贴到容器外壁后,一定要保证贴合面没有空隙。我试过用热熔胶固定和用透明胶带固定,热熔胶固定效果最好,因为它能把边缘也压紧;透明胶带如果粘不牢,中间留一点空气,读数就会明显偏低。还有一点,杯子里的水如果晃动剧烈,水位读数也会跟着波动,做实验的时候等水面静止后再读数,做产品的时候建议加一点软件滤波或者滞后判断。

第二个是电源隔离。HS-S37A 的模拟量输出 AO 比较敏感,如果和电机、继电器、水泵共用电源,水位的模拟量读数会跳得很难看。我在一次实验里把水泵直接接到同一个 3.3V 上,水泵一启动,ADC 读数直接从 3 成飙到 8 成,差点以为传感器坏了。后来把水泵单独用 5V 和 MOSFET 驱动,数字地与模拟地单点连接,问题才解决。如果你只是做显示实验,问题不大,但只要接水泵、电磁阀这类执行器,供电设计一定要提前规划。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 10:28:46

程序员做抖音技术博主:从0到10万粉的实操涨粉策略

做技术博主的人其实都明白一个尴尬现状&#xff1a;代码写得再好&#xff0c;放在GitHub上也就几百个star&#xff0c;但抖音上一条15秒的报错解决视频&#xff0c;播放量可能直接破百万。2026年这个节点&#xff0c;短视频平台的流量分配机制已经相当成熟&#xff0c;程序员、…

作者头像 李华
网站建设 2026/9/9 10:27:10

提示工程自动化测试:架构师视角下的回归体系设计与实践

这两年带智能客服和知识助手项目&#xff0c;我最常跟人讲的一句话是&#xff1a;如果一条 prompt 不会因为改动而上线前自动跑一遍回归&#xff0c;那你还没开始认真做提示工程。听上去有点像测试同学在宣示主权&#xff0c;但作为长期做系统架构的人&#xff0c;我恰恰认为这…

作者头像 李华
网站建设 2026/9/9 10:26:44

单语言依赖的隐形成本:架构锁死与多语言破局之道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 10:26:22

计及碳排放的多微电网分布式优化:ADMM原理与Matlab实现

最近在做多微电网方向的研究&#xff0c;被问得最多的问题就是“计及碳排放的分布式优化到底怎么落地”。翻了一圈已有的开源资料&#xff0c;不是只给集中式求解代码&#xff0c;就是分布式算法只停留在理论推导&#xff0c;真正能跑通的参考实现少之又少。这篇文章把我自己梳…

作者头像 李华
网站建设 2026/9/9 10:25:58

基于CKEditor 5的汽车工艺文档在线编辑与Word导出方案

上周有个搞汽车零部件的老哥找我&#xff0c;说他们准备上一套工艺文件在线编制平台&#xff0c;问我在线编辑器用哪个比较省事。我第一反应是这需求听着不难&#xff0c;无非就是个富文本编辑器嘛。结果他给我发了他们现用的工艺卡模板&#xff0c;我当场就不吭声了——一张工…

作者头像 李华
网站建设 2026/9/9 10:22:06

AI助手豆包助力FPGA开发:Vivado实战提效指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华