STM32实战:非接触式水位传感器HS-S37A与OLED显示系统
作为一个在嵌入式圈子里泡了快十年的老开发我其实挺少专门为一颗水位传感器写一篇完整实验笔记的。但 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/5VGND电源负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 外设对应的引脚为 PB6SCL和 PB7SDA直接把 OLED 的 SCL 接到 PB6SDA 接到 PB7VCC 接 3.3VGND 共地。关于 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 输入 8MHzPLL 倍频到 72MHz总线分配它会自动处理我们只需要确认最终 SYSCLK 显示 72MHz 即可。3.2 ADC 配置的细节与参数说明ADC 这里我是把 PA0 配置为 ADC1 的 IN0 通道用来读取传感器的模拟电压输出。在 CubeMX 的 Analog → ADC1 设置里把 IN0 勾选上然后打开参数配置Resolution12 bits这是 F103 ADC 的默认精度数值范围 0~4095AlignmentRight alignment右对齐即可Continuous ConversionDisabled用单次转换配合轮询读取Scan Conversion ModeDisabledSampling 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_INPUTGPIO_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 分步实施步骤我习惯把实验流程拆成八个步骤每一步都能验证通过了再往下走避免最后一次性联调失败时不知道是哪个环节出问题。按第 2 章的接线表接好全部硬件通电前用万用表确认一下 VCC 和 GND 没有短路。ST-Link 通过 SWD 接口连接 STM32接线顺序是 SWDIO、SWCLK、GND如果需要板载供电就再接一个 3.3V。打开 CubeMX按第 3 章的配置建立工程生成 Keil MDK 代码。先编译一个空白工程下载进去确认芯片能正常下载、调试器能连接上。把 OLED 驱动加进来写一个循环让它显示“Hello STM32”确认 I2C 通讯完全正常。再写 ADC 读取函数把值通过串口打印到电脑上确认数据不再悬空乱跳。最后把 HS-S37A 的 DO 逻辑和 OLED 显示整合到一起做完整的交互界面。用空瓶、装一半水的瓶子、装满水的瓶子分别做测试记录数据和阈值。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μF104 电容滤波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我也会在代码里把阈值标定和滤波逻辑单独抽成两个函数因为一到现场调设备的时候你才会发现这些都是能救命的接口。