说句大实话,玩嵌入式这些年,我觉得最容易被新手低估的传感器就是“水位检测”。传统的浮球开关和电极式探头,要么机械结构容易卡死,要么金属电极长期泡水里会电解腐蚀,放到饮水机、鱼缸、加湿器这些场合,卫生和寿命都是大问题。所以当我在项目里用到 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 SSD1306 | IIC接口,7针或4针均可 |
| ST-Link V2 下载器 | 烧录和调试 |
| 面包板、杜邦线若干 | 搭建电路 |
| 塑料水杯 | 最好用薄壁无金属的杯子,壁厚2~3mm以内效果最好 |
引脚分配我直接给结论:
| HS-S37A 引脚 | STM32引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 传感器供电,3.3V~5V均可 |
| GND | GND | 共地 |
| SCL | PB6 | I2C1_SCL |
| SDA | PB7 | I2C1_SDA |
| AO | PA0 | ADC1_IN0,读取模拟电压 |
| OLED 引脚 | STM32引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 模块供电 |
| GND | GND | 共地 |
| SCL | PB6 | 与传感器共用I2C1_SCL |
| SDA | PB7 | 与传感器共用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_EMPTY和V_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 驱动,数字地与模拟地单点连接,问题才解决。如果你只是做显示实验,问题不大,但只要接水泵、电磁阀这类执行器,供电设计一定要提前规划。