☰
STM32实战:非接触式水位传感器HS-S37A与OLED显示系统
2026/10/26 2:43:21 网站建设 项目流程

作为一个在嵌入式圈子里泡了快十年的老开发,我其实挺少专门为一颗水位传感器写一篇完整实验笔记的。但 HS-S37A 这颗模块有点特别:它属于非接触式电容水位检测,和传统把两根电极泡进水里的浮子式、电阻式传感器完全两码事,接线只有两三根,却能直接挂在 STM32 上配合 OLED 做出一套看得见、摸得着的显示系统。这篇文章就把我实际踩过的坑、调过的参数和最终的代码逻辑全部摊开讲,适合刚学完 STM32 基础外设、想搞一个综合小项目的朋友直接照着做。

1. 为什么用 HS-S37A 做这个水位检测实验

1.1 这个实验解决什么问题

先说清楚 HS-S37A 在项目里的定位:它不是一个用来精确测量“水位高度”的高精度液位计,而是一个用来判断“有没有水”的开关型传感器,部分型号还会额外输出一路模拟电压,用来粗略估算水量多少。它的核心优势是非接触式,传感器探头贴在塑料水箱、水管或玻璃容器外壁就能检测到内部液体状态,不直接接触水体,所以不存在电极腐蚀、污染液体、结垢短路之类的问题,在食品、医疗、净水器这类场合特别吃香。

做这个实验的意义不只是“点亮一个 OLED”,而是把一条完整的信号链路跑通:物理量变化先被传感器转换成电信号,STM32 再通过 ADC 和 GPIO 读进来,最后经过逻辑处理和显示驱动,把结果呈现在屏幕上。这一套流程几乎覆盖了 STM32 开发中最常用的三大外设:GPIO、ADC、I2C,做完之后你对多外设协同工作的理解会明显上一个大台阶。我对这套组合的评价是:难度适中、成本极低、出效果快,特别适合作为学习 HAL 库开发的里程碑项目。

1.2 器件选型背后的逻辑

传感器选 HS-S37A,主要是看中它的通用性和上手成本。市面上水位传感器五花八门,电极式的虽然便宜,但要在水里通电,用久了阳极氧化特别明显;浮子式的机械结构容易卡死;超声波和激光式的精度倒是高,但价格和调试复杂度都上去了。HS-S37A 属于电容式非接触检测,逻辑输出直接给高低电平,MCU 拿来做“有水/无水”判断几乎零成本。部分模块还带一个 AO 模拟输出脚,能让我在实验里顺带演示 ADC 采集,所以选它做教学特别合适。

主控选 STM32F103C8T6 也是多年的老朋友了。这颗芯片号称电子 DIY 界的“万金油”,72MHz 主频,自带 ADC、I2C、USART、SPI,网上参考资料多得看不完。OLED 则选了最常见的 0.96 寸 SSD1306 驱动模块,I2C 版本只要两根信号线,相比 SPI 版本少接 4 根线,代码资源也最丰富。物联网项目里经常需要显示设备状态,这个小屏几乎成了调试必备件。整套硬件成本压到 30 元以内完全没问题,实验失败顶多损失几根杜邦线。

2. 硬件准备与接线原理详解

2.1 完整硬件清单

这里把我实际用的配置完整列出来,照着买基本不会踩坑:

器件型号/规格数量备注
主控板STM32F103C8T6 最小系统板1蓝丸板即可
水位传感器HS-S37A 非接触式模块1带 DO 和 AO 输出
OLED0.96 寸 SSD1306 I2C1四线制 I2C 版本
调试器ST-Link V21SWD 方式下载
杜邦线公对公、公对母若干线尽量短,避免干扰
USB 转 TTLCH340 模块1备用,串口打印调试

实验被测物我建议准备一个空塑料水瓶和一满瓶水,或者一段透明塑料软管加上注射器。注意,HS-S37A 检测的是“介质变化引起的电容变化”,金属容器基本测不了,这一点在第五章实测里我会再详细讲。

2.2 HS-S37A 引脚定义与工作原理

HS-S37A 模块虽然厂家很多,但引脚定义基本统一,常见的有 3 个或 4 个引脚:VCC、GND、DO、AO。其中 DO 是数字量输出,模块内部已经集成了阈值比较电路,探头附近没有水时输出一种电平,有水时输出另一种电平,单片机只需要读 GPIO 就行。AO 是模拟量输出,会随探头附近的介质变化产生电压波动,适合接 ADC 做粗略的水量判断。

它的内部原理说起来其实不复杂:模块内部有一个高频振荡电路,探头部分相当于一个电容极板,当非金属容器中的水位上升时,探头附近的等效介电常数会改变,从而导致振荡频率或幅度变化,模块检测到变化后,再通过比较器或逻辑电路输出对应的电平信号。你可以把它想象成“用手隔着衣服摸口袋里的钥匙”,不需要真的伸进去,就能感知到里面有没有东西。

接线表我直接给出来:

HS-S37A 引脚引脚功能接 STM32备注
VCC电源正3.3V先确认模块支持电压,很多板子兼容 3.3V/5V
GND电源负GND与 STM32 共地
DO数字输出PA2读 GPIO 电平即可
AO模拟输出PA0接 ADC 输入,不用可悬空

这里特别提醒:不同卖家出货的模块丝印顺序可能不一样,有的标注是 VCC、GND、DO、AO,有的却是 VCC、DO、AO、GND,插线前一定仔细看模块上的印刷标识。我见过不止一次把电源和信号接反导致板子冒烟的案例,别问我怎么知道的。

2.3 OLED 接线与 I2C 地址判断

OLED 这边就简单多了,四个引脚:VCC、GND、SCL、SDA。我用的是 STM32 的 I2C1 外设,对应的引脚为 PB6(SCL)和 PB7(SDA),直接把 OLED 的 SCL 接到 PB6,SDA 接到 PB7,VCC 接 3.3V,GND 共地。

关于 I2C 地址,这里有个新手最容易懵的点:SSD1306 的 7 位地址常见是 0x3C,但很多库或代码习惯把地址写成 8 位的 0x78。0x3C 左移一位就是 0x78,本质上是同一个地址的不同写法。还有部分模块把 I2C 地址选择电阻做了跳线,切到另一个位置就变成 0x7A/0x3D。我第一次实验白屏查了半天,最后发现是模块背面的 0Ω 电阻焊错位,导致地址是 0x7A。所以在跑通讯之前,先用 I2C 扫描函数确认一下设备真实地址,能省掉大量冤枉时间。

另外 F103 的 PB6/PB7 在 I2C 模式下需要上拉电阻到 3.3V。很多开发板上已经自带上拉电阻,但便宜的最小系统板不一定有,如果 I2C 一直通信失败,别忘了外接两个 4.7kΩ 上拉电阻试试。

3. STM32 工程配置:CubeMX 里把外设全点亮

3.1 建立工程的基础配置

这个实验我用的是 STM32CubeMX 加 Keil MDK 的组合,HAL 库版本,相比于标准外设库,CubeMX 的图形化配置能省掉大量初始化代码的时间,尤其适合管脚多、外设多的情况。第一步打开 CubeMX,新建工程,选择芯片 STM32F103C8Tx,然后进行以下基础配置:

配置项设置值说明
SYS → DebugSerial Wire保留 SWD 调试,否则下载一次后可能就找不到芯片了
RCC → HSECrystal/Ceramic Resonator使用外部 8MHz 晶振
Clock ConfigurationSYSCLK 72MHz通过 PLL 倍频到最高主频

时钟树配置是很多人容易忽略的细节。默认配置下 F103 的 SYSCLK 只有 8MHz,如果不把 APB1 和 APB2 的分频系数设置好,后面 ADC 和 I2C 的工作时钟可能达不到预期,I2C 频率计算也会跟着出问题。CubeMX 里直接选择 HSE 输入 8MHz,PLL 倍频到 72MHz,总线分配它会自动处理,我们只需要确认最终 SYSCLK 显示 72MHz 即可。

3.2 ADC 配置的细节与参数说明

ADC 这里我是把 PA0 配置为 ADC1 的 IN0 通道,用来读取传感器的模拟电压输出。在 CubeMX 的 Analog → ADC1 设置里,把 IN0 勾选上,然后打开参数配置:

  • Resolution:12 bits,这是 F103 ADC 的默认精度,数值范围 0~4095
  • Alignment:Right alignment,右对齐即可
  • Continuous Conversion:Disabled,用单次转换,配合轮询读取
  • Scan Conversion Mode:Disabled
  • Sampling Time:建议选 55.5 Cycles 或更长,我实际用 239.5 Cycles

采样时间这个参数值得多说两句。ADC 内部等效模型是一个采样电容,在采样阶段需要从外部信号源抽取电流,如果外部信号源的输出阻抗比较高,采样时间太短会导致电容还没充满就开始转换,读出来的数值会偏小且跳动剧烈。HS-S37A 的 AO 输出本身就不是低阻抗的强驱动输出,加上实验用的杜邦线又细又长,导线电阻和分布电容都会有影响,把采样时间拉长可以明显降低读数抖动。

实际读 ADC 的代码就是我后面主循环里用的这种轮询方式:

uint16_t read_adc_value(void) { uint16_t adc_raw = 0; HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { adc_raw = HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); return adc_raw; }

每次转换完要主动调用 HAL_ADC_Stop,否则外设会一直处于使能状态,干扰后续的采集流程。这个习惯在写多通道 ADC 时尤其重要。

3.3 GPIO 数字输入与去抖逻辑

PA2 配置成 GPIO_Input,用来连接 HS-S37A 的数字输出 DO 脚。下拉方式我选择的是上拉,这样传感器没有明确输出时 GPIO 也能保持在确定电平,不至于悬空乱跳。具体在 CubeMX 里的配置是:PA2 → GPIO_MODE_INPUT,GPIO_Pull 选择 Pull-up。

但 GPIO 配置只是第一步,真正需要花心思的是“去抖”。HS-S37A 的 DO 输出在探头贴近水管、水流冲击或者周围有振动时会瞬间跳变,如果不做处理,OLED 上的状态就会一直在“有水/无水”之间闪烁,看着特别难受。我的做法是连续采样多次,只有连续 5 次结果一致才更新状态:

uint8_t get_water_level_debounced(void) { uint8_t last_sample = 0; uint8_t stable_count = 0; uint8_t i; for (i = 0; i < 8; i++) { uint8_t sample = HAL_GPIO_ReadPin(HS_GPIO_Port, HS_GPIO_Pin); if (sample == last_sample) { stable_count++; } else { stable_count = 0; last_sample = sample; } HAL_Delay(1); } if (stable_count >= 5) { return last_sample; } return current_level; // 返回上一次的稳定值 }

4. OLED 显示驱动与水位可视化

4.1 SSD1306 驱动移植

OLED 驱动用的库是网上流传最广的 SSD1306 HAL 版本,这个库在 GitHub 和各种博客里都能找到,基本拿到手改一下 I2C 地址就能用。核心就是把初始化序列、清屏、画点、画矩形、显示字符这些函数封装好,然后由 SSD1306_UpdateScreen 函数把显存中的内容一次性刷到屏幕上。

这里有一个原则问题:先用软件 I2C 跑通,再换硬件 I2C,还是直接上硬件 I2C?我个人的实际经验是:新手建议先直接用硬件 I2C,因为 CubeMX 已经生成好了初始化代码,I2C 发送函数也封装好了,如果遇到一直不通的问题,再退回到模拟 I2C 来排查,反而能更快确认是外设配置问题还是上拉电阻问题。F103 的硬件 I2C 偶尔会被传各种“玄学”问题,但绝大多数情况下只要上拉电阻正常、地址正确,跑通是没问题的。

初始化里最容易坑人的是 Charge Pump(电荷泵)设置。SSD1306 内部需要升压电路给 OLED 屏幕供电,有些库的初始化代码默认关闭这个功能,或者对比度设置太低,结果屏幕完全没反应。务必确认初始化序列中包含下面这两条命令:

SSD1306_Command(0x8D); // Charge Pump Setting SSD1306_Command(0x14); // Enable Charge Pump

如果屏幕还是不亮,再查一下对比度寄存器 0x81,把值调到 0x7F 再看看效果。

4.2 画面布局:数字、文字、图形条怎么摆

我的 OLED 画面布局是这样的:第一行显示当前状态,比如“水位状态:正常”或“水位状态:偏低”;第二行显示 ADC 原始值;第三行显示推算出的水量百分比;第四行画一个横向的进度条,动态反映水量多少。这样既能看到实时数据,又能直观地看到图形反馈,比单纯打印一串数字要形象得多。

显示的核心逻辑其实就是一个状态机的思想。传感器返回的数字量决定了第一行的文字判断,ADC 采集值经过归一化后决定百分比和进度条宽度。OLED 的显示更新不要太频繁,否则会有明显的闪烁感,我把刷新间隔设置成 200ms 左右,人眼看起来非常流畅。

画进度条用的就是 SSD1306 的矩形填充函数,先画一个外框,再按百分比填充内部区域。关键代码如下:

uint8_t percent = 0; if (adc_raw > EMPTY_ADC) { percent = (uint32_t)(adc_raw - EMPTY_ADC) * 100 / (FULL_ADC - EMPTY_ADC); } if (percent > 100) percent = 100; SSD1306_DrawRectangle(0, 48, 127, 56, SSD1306_NORMAL); SSD1306_DrawRectangle(0, 48, 127 * percent / 100, 56, SSD1306_FILLED); SSD1306_UpdateScreen();

这里的 EMPTY_ADC 和 FULL_ADC 是标定出来的阈值,不是随便拍脑袋定的,具体标定方法我放在第 5 章实测环节里讲。

4.3 OLED 汉字显示思路

OLED 标配的字库只含英文字母和数字,想显示“水位”“正常”这类汉字就得自己做字模。常用的工具是 PCtoLCD2002 取模软件,取模方式选择“逐行式”,字体大小用 16×16,取模方向、逐行式、低位在前,这些参数要和代码里的字模读取函数匹配上。生成出来的字模是一个十六进制数组,放进工程后,调用画点函数逐位扫描就能点出汉字。

汉字显示会让作品的专业感提升非常多,也是很多毕业设计里导师爱看的一个点。我的建议是只提取自己需要用到的几个汉字,比如“水”“位”“状”“态”“正”“常”“偏”“低”,不要贪多,否则 flash 空间消耗比较大。显示函数本身很简单,核心就是两层循环按 bit 判断有没有点,有就在对应坐标画点,没有就跳过。

5. 完整实验流程与联调实测

5.1 分步实施步骤

我习惯把实验流程拆成八个步骤,每一步都能验证通过了再往下走,避免最后一次性联调失败时不知道是哪个环节出问题。

  1. 按第 2 章的接线表接好全部硬件,通电前用万用表确认一下 VCC 和 GND 没有短路。
  2. ST-Link 通过 SWD 接口连接 STM32,接线顺序是 SWDIO、SWCLK、GND,如果需要板载供电就再接一个 3.3V。
  3. 打开 CubeMX,按第 3 章的配置建立工程,生成 Keil MDK 代码。
  4. 先编译一个空白工程下载进去,确认芯片能正常下载、调试器能连接上。
  5. 把 OLED 驱动加进来,写一个循环让它显示“Hello STM32”,确认 I2C 通讯完全正常。
  6. 再写 ADC 读取函数,把值通过串口打印到电脑上,确认数据不再悬空乱跳。
  7. 最后把 HS-S37A 的 DO 逻辑和 OLED 显示整合到一起,做完整的交互界面。
  8. 用空瓶、装一半水的瓶子、装满水的瓶子分别做测试,记录数据和阈值。

5.2 实测数据与显示效果

我在实际操作中拿到的一组典型数据如下,注意不同批次模块、不同供电电压下数值会有差异,这份数据只是用来帮助你理解标定思路:

测试场景PA0 ADC 值DO 输出OLED 显示结果
探头悬空无任何物体1125高电平无水状态 0%
探头贴在空塑料瓶外壁1190高电平无水状态 0%
探头贴在装 1/3 水的瓶壁2150低电平有水量 35%
探头贴装满水的瓶壁3150低电平有水量 78%

如果你仔细观察就会发现,空瓶和悬空两种状态的 ADC 值并不完全一样,这是因为塑料瓶壁本身的介电常数和空气不同,传感器感知到的“介质变化”比单纯“有没有水”更敏感。这也是为什么我不建议直接在代码里写死一个理论阈值,而是一定要手动画出一个“空瓶基准值”和一个“满水基准值”,然后在这两个值之间做百分比映射。这个标定过程看着不起眼,但实际项目里能否稳定工作,往往就取决于这个基准选得准不准。

6. 常见问题排查与避坑记录

6.1 高频问题速查表

现象可能原因解决办法
OLED 完全不亮I2C 地址不对、Charge Pump 没开启、模块供电不足先用 I2C 扫描确定真实地址;检查初始化序列
I2C 扫描不到设备SDA/SCL 缺少上拉电阻、接线顺序接反、模块是 SPI 版本外接 4.7kΩ 上拉;对照丝印重新接线
DO 输出一直在抖动探头贴得太紧、周围有振动、电源纹波大程序去抖;给模块单独加 100μF+104 电容滤波
ADC 读数跳变严重采样时间太短、杜邦线过长、供电不稳定把采样时间调到 239.5 Cycles;换短杜邦线
下载一次后找不到目标芯片SWD 调试口被误配置成普通 GPIOCubeMX 里 SYS 选择 Serial Wire;按住复位重新下载
上电后前 1 秒数据偏大传感器内部 RC 振荡器还没稳定上电延时 500ms 后再做首次 ADC 采集

6.2 现场排查思路与一个实战案例

我调试这类系统有一个固定套路:先电源,再接线,最后才是代码。很多新手一遇到问题就反复改代码,改到天亮也找不出毛病,实际上八成的坑都出在物理层。

印象最深的一次是 OLED 白屏,我一开始怀疑是 I2C 地址配置问题,把 0x3C、0x78 试了个遍都没用。后来才发现模块背面有两个焊盘选择地址的 0Ω 电阻,出厂默认是 0x7A,和代码里写的 0x78 刚好差一位。这种问题无论是看代码还是换库都解决不了,最后解决靠的是拿起模块仔细看丝印。所以我现在装机前都会先做一件事:用 I2C 扫描函数把所有设备地址打出来,90% 的通讯故障都能当场确定。

6.3 老生常谈的注意事项

  • 传感器探头不要直接贴在金属表面上,金属会完全改变电容场分布,导致输出混乱甚至永久失效。
  • 如果实验里同时带了水泵、继电器这类大功率器件,一定要和传感器分开供电,或者在电源上并联大电容滤波。共用一个电压源时,水泵一启动 ADC 值就会乱跳。
  • 传感器 DO 输出模块的阈值电位器出厂值不一定适合你的场景,不要迷信默认值,用螺丝刀微调后用万用表实测输出电平再定。
  • 长期贴在潮湿环境使用时,建议给传感器背面做一个绝缘隔离贴片,防止凝结水走线短路。

7. 后续还能怎么玩

做完这个基础实验,你已经掌握了“传感器采集 + STM32 处理 + OLED 展示”的完整链路,剩下的就是纯粹想象力的活了。我的建议是把这套系统扩展成水箱自动补水控制:在 HS-S37A 判断缺水时,由 STM32 控制一个继电器驱动小型水泵补水,水满后自动断电。OLED 上加一个倒计时或者水泵运行状态指示,放在鱼缸或者盆栽自动化场景里,效果非常直观。

如果还想再上一个档次,可以把 OLED 换成带触摸功能的大屏,或者加上 ESP8266/ESP32 模块,把水位状态上报到手机端。对 RS485 或者 PLC 工业总线熟悉的朋友,也能通过一个简单的电平转换芯片把 DO 信号传给上位机,组成一套多传感器检测网络。我个人的体会是,做这类小项目最重要的是把第一步完整跑通,只要传感器数据可信、显示逻辑清晰,后面加通信、加控制都只是时间问题。最后再分享一个我保留至今的习惯:哪怕只是一个简单 DEMO,我也会在代码里把阈值标定和滤波逻辑单独抽成两个函数,因为一到现场调设备的时候你才会发现,这些都是能救命的接口。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询