ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32非接触式水位传感器HS-S37A检测实战:OLED显示与蜂鸣器报警

STM32非接触式水位传感器HS-S37A检测实战:OLED显示与蜂鸣器报警 1. 为什么选HS-S37A从一次尴尬的漏水说起先交代一下背景。之前我给家里的净水器加装缺水提醒最早用的是浮球式水位开关结构倒是不复杂但用了两个月就出问题了——浮球卡涩、密封圈老化、水垢把转轴糊住最后不光没提醒反倒让水泵空转烧了一台。后来换了一款贴片式电极检测又因为需要在水箱内部开孔改造工程太大老婆差点没让我继续住在书房。折腾了一圈结论就是能不做接触式就别做接触式非接触才是家用小电子的正确出路。HS-S37A就是在这个背景下进入视野的。它是一颗非接触式水位传感器核心卖点是贴在容器外壁就能测水位不需要跟液体有任何物理接触。配合STM32和一块0.96寸OLED可以做出一个完整的“水位状态监视器”显示水位正常、偏低、缺水等状态还能当作一个独立小实验来练习GPIO输入、I2C通信、HAL库工程配置这些基本功。这个项目适合谁我觉得有三类人值得做一遍刚学完STM32基础外设、想找一个“能落地的小项目”来练手的同学。想做智能家居、鱼缸水位、咖啡机水箱这类非接触检测又不想在容器上打孔的朋友。纯粹想搞明白“非接触式水位传感器到底怎么回事”的好奇派。整体实验用到的硬件不超过四样价格加起来一杯奶茶钱但涉及的知识点覆盖了传感器原理、硬件接线、CubeMX配置、HAL库驱动、OLED显示、状态机设计性价比非常高。1.1 这个实验到底在做什么一句话描述STM32通过GPIO读取HS-S37A输出的电平信号判断当前水位是否到达传感器贴装位置然后把结果实时显示在OLED屏上。说得再具体一点实验分成三层感知层HS-S37A贴在透明水杯外壁的某一高度当杯内水位到达或超过这个高度时传感器输出电平发生跳变。控制层STM32以一定周期采样这个电平做滤波处理滤掉液体晃动和外界干扰引起的毛刺。显示层把检测结果转化成文字和图标通过I2C接口发送给OLED模块刷新出“水位正常”“水位偏低”之类的信息。除了这些我会额外加一个蜂鸣器报警的例子。水位低过传感器位置说明该补水了驱动蜂鸣器响两秒这才是完整的“提醒”逻辑。1.2 非接触式水位传感器的检测原理HS-S37A的原理和常见的超声波、红外水位传感器都不一样它属于电容感应式检测。简化解释是这样传感器内部有一个铜箔感应极板当传感器被贴到非金属容器塑料、玻璃、亚克力外壁时感应极板和容器内部的液体之间会形成一个微小的电容。这个电容的容值跟感应区域的介电常数有关系。空气的介电常数大约是1而水的介电常数大约在80左右差异非常大。水位没到传感器位置时感应区域对应的介质是空气和塑料壁容值小水位到达后介质变成了水和塑料壁容值一下子增大好几倍。传感器内部是一个基于电容变化的振荡/比较电路它检测到容值变化后会翻转输出电平。所以HS-S37A的输出其实是一个开关量水位到了输出高电平或低电平取决于具体型号和触发状态水位没到输出反过来的电平。注意HS-S37A在市面上能买到两个版本一个输出数字电平另一个输出模拟量。本实验以常见的数字量版本为例。如果你手头的是模拟量版本后面我会在扩展部分说明怎么改用ADC读取。明白了这个原理你就能理解几个硬性约束容器必须是绝缘体金属杯、金属水箱直接没戏因为电容感应根本无法穿过金属壁。传感器必须贴在平整外壁贴到弧面或带纹理的表面会影响感应区的贴合度导致误判。贴装面和容器壁之间不能有空气间隙要让传感器外壳和容器壁尽量“粘紧”容值变化才明显。2. 硬件连接与器件选型要点2.1 核心器件清单我用的这套配置如下直接照着买就行器件型号/规格备注主控板STM32F103C8T6最小系统板也可以用F407、G431等代码逻辑一致水位传感器HS-S37A 数字量输出3.3V或5V供电TTL电平输出显示屏0.96寸 OLED SSD1306I2C接口四针VCC、GND、SCL、SDA蜂鸣器有源蜂鸣器模块低电平触发型DO接STM32引脚容器透明塑料水杯壁厚3mm以内效果最好杜邦线母对母若干I2C接线建议尽量短关于HS-S37A的线序我手上这颗是标准的三线红色VCC、黑色GND、蓝色OUT。不同批次可能颜色会有点差异拿到货第一时间用万用表确认一下别直接凭颜色接烧了传感器不划算。2.2 接线顺序与供电细节完整接线表放在下面HS-S37ASTM32F103C8T6OLED蜂鸣器VCC3.3V或5VVCCVCCGNDGNDGNDGNDOUTPA0---PB6 (I2C1_SCL)SCL--PB7 (I2C1_SDA)SDA--PB0非必需-DO两个容易踩的供电细节HS-S37A官方标称工作电压是3.3V~5V。我建议在STM32最小系统板上直接接3.3V因为这样输出电平最高是3.3V和STM32的GPIO兼容性最好。如果你接5V输出高电平是5V虽然F103的引脚标注“兼容5V”但长期跑还是不推荐。OLED模块的VCC接3.3VI2C总线电压也就跟着是3.3V不会出现电平不匹配的问题。2.3 贴装位置效果好坏的关键一步这部分是实验成败的分水岭比写代码重要得多。HS-S37A的正确贴法是这样的先把塑料杯外壁擦干净不能有水珠和油污。在传感器背面没有元件的那一面也就是感应极板侧贴一小片双面胶或者用透明胶带固定。把传感器竖直贴在杯子外壁你要检测的水位高度上贴的时候稍微用力按压一下让传感器外壳和杯壁贴合紧密内部不能有气泡。传感器要竖直贴感应区域的中线跟水平面平行这样水位上升时能平稳越过感应区翻转更干脆。我第一次实验图省事直接用手扶着传感器靠在杯子上结果水位从低到高变化了好几次电平一直在跳完全没法用。后来用胶带固定好重新测试现象立刻干净了。所以这个步骤真的别偷懒。提示杯壁越薄检测越灵敏。一次性水杯、饮料瓶那种薄壁容器效果最好双层玻璃杯、保温杯这种就别试了基本测不出来。3. CubeMX工程配置时钟、GPIO与I2C我用的是STM32CubeIDE HAL库配置在CubeMX里完成。整套配置的逻辑对你以后做别的项目也有复用价值。3.1 基础时钟树新建工程时选择芯片型号STM32F103C8T6。时钟配置我直接选了外部8MHz晶振然后在Clock Configuration里把系统时钟拉到72MHz。F103的时钟树上有个经典配置方案HSE选Crystal/Ceramic ResonatorPLL倍频为9倍得到72MHz系统时钟APB1分频器设2使APB1总线时钟保持36MHz这样I2C1才能申请到正确的时钟源3.2 GPIO输入与I2C外设配置在Pinout Configuration页面里1. PA0配置为GPIO_Input先在芯片图上点击PA0引脚选择GPIO_Input。然后在GPIO设置里GPIO mode: Input modeGPIO Pull-up/Pull-down: Pull-up这是关键。HS-S37A的输出模式是推挽输出但它从高阻到高/低电平切换时如果外部悬空中间状态会不稳定。内部上拉可以保证电平被钳制在确定值。实际测试中即使不加上拉也能工作但上拉后抗干扰性明显好很多。2. PB6、PB7配置为I2C1在芯片图上点击PB6选择I2C1_SCL点击PB7选择I2C1_SDACubeMX会自动配置。然后在I2C1参数设置里I2C Speed Mode: Standard Mode100kHz市面上的OLED模块跑400kHz其实也行但为了稳定首选100kHz。之前我给一块OLED试过400kHz能用但偶尔开机花屏降到100kHz后再没出过问题。3. PB0配置为GPIO_Output给蜂鸣器备用不用的话可以跳过。3.3 生成工程后的必要检查生成工程后打开main.c做两个检查一个是看初始化顺序另一个是看I2C通信是否正常。// 初始化顺序应该是这样 MX_GPIO_Init(); MX_I2C1_Init();把初始化放在最前面是必须的尤其I2C的初始化要发生在任何OLED操作之前。第二个检查就是直接调用HAL库的I2C探测函数uint8_t oled_addr 0x78; // 7位地址0x3C左移一位OLED模块的I2C地址一般是0x3C或0x3D实测之前最好用I2C扫描确认。很多OLED背面有地址电阻可以切换0x3C/0x3D。我手上这块默认是0x3C对应8位地址0x78。还有个细节CubeMX默认生成的I2C初始化里时钟频率是根据APB1总线时钟算出来的。如果你改了APB1分频重新生成工程后要再检查一下I2C的Clock Speed值确保是100000。4. OLED驱动与中文显示一行一行抠出来的代码4.1 SSD1306的基本控制思路0.96寸OLED用的驱动芯片是SSD1306分辨率128x64。它内部有一块1KB的显存对应128x64个像素点。我们要做的所有操作最终都归结为往这块显存里写数据然后通过I2C把显存内容刷到屏幕上。SSD1306的控制命令大致分三类基础设置命令设置显示开关、显示起始行、对比度寻址模式命令设置列地址、页地址数据传输命令连续写显存数据我们不需要把所有寄存器都搞清楚因为市面上有大量成熟的SSD1306 HAL库驱动代码可以直接用。但有一个概念必须理解页寻址。SSD1306把64行像素分成8页每页8行横向128列。写入数据时先设置页地址和列地址然后连续写入的数据会先填满一行自动换到下一列。理解了这一点后面调试显示异常会方便得多。4.2 HAL库下的I2C写入函数我用的OLED驱动基于HAL库核心就一个函数把命令或数据通过I2C发出去。/** * brief 向OLED写入一个字节 * param data: 要写入的数据 * param cmd: 0表示命令1表示数据 */ static void OLED_WriteByte(uint8_t data, uint8_t cmd) { uint8_t buffer[2]; buffer[0] cmd ? 0x40 : 0x00; // 控制字节命令0x00数据0x40 buffer[1] data; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR_8BIT, buffer, 2, 100); }这里的重点是这个控制字节。SSD1306的I2C传输格式是先发设备地址再发一个控制字节后面跟命令或数据。控制字节为0x00表示后面是命令为0x40表示后面是数据。很多初学者卡在这里直接传数据不传控制字节屏幕毫无反应。再往上一层是写命令和写数据的封装void OLED_WriteCmd(uint8_t cmd) { OLED_WriteByte(cmd, 0); } void OLED_WriteData(uint8_t data) { OLED_WriteByte(data, 1); }清屏函数则是向整块显存写0x00void OLED_Clear(void) { for (uint16_t i 0; i 8; i) // 8页 { OLED_WriteCmd(0xB0 i); // 设置页地址 OLED_WriteCmd(0x00); // 设置列地址低4位 OLED_WriteCmd(0x10); // 设置列地址高4位 for (uint16_t j 0; j 128; j) { OLED_WriteData(0x00); // 每列写入空数据 } } }这套“设置页、设置列、写显存”的循环就是SSD1306操作的基本盘。4.3 中文字模的取模与显示显示英文字母和数字一般用ASCII字库网上到处都是。但要显示“水位正常”这种中文就得自己取模了。取模工具我用的是PCtoLCD2002设置上需要注意几个点取模方式选“逐列式”字模选项选“阴码”每行显示数选“16”取模走向选“顺向”勾选“自定义格式”格式为0x%02X,以16x16汉字为例每个汉字占32字节。取模完成后把数组复制到代码里// 以“水”字为例实际取模数据会不同 const uint8_t font_water[32] { 0x00, 0x02, 0x02, 0x02, 0x02, 0x02, 0x82, 0x62, 0x1A, 0x06, 0x02, 0x02, 0x02, 0x02, 0x02, 0x00, // ... 省略 };显示一个16x16汉字的函数逻辑是把32字节数据按页写入OLED。因为汉字是16像素高跨越两个页第0页和第1页所以需要拆成上半部分和下半部分。void OLED_ShowChinese(uint8_t x, uint8_t y, const uint8_t *font) { uint8_t i, j; for (i 0; i 2; i) // 16像素高分2页显示 { OLED_WriteCmd(0xB0 (y / 8) i); OLED_WriteCmd(0x00 | ((x i * 8) 0x0F)); OLED_WriteCmd(0x10 | (((x i * 8) 4) 0x0F)); for (j 0; j 8; j) { OLED_WriteData(font[i * 16 j]); } } }注意这个函数是我为了展示逻辑简化的版本实际上16x16汉字取模后的数据排列是上半页8字节、下半页8字节交叉存储使用时要和你取模工具的设置保持一致。建议先在PCtoLCD2002里生成数据后用串口打印逐字节核对别急着上屏。5. 水位检测主程序的完整逻辑5.1 读取传感器电平HS-S37A接到PA0在HAL库里读取电平只需要一行GPIO_PinState water_state HAL_GPIO_ReadPin(WATER_PIN_GPIO_Port, WATER_PIN_Pin);返回结果是GPIO_PIN_SET高电平或GPIO_PIN_RESET低电平。问题是直接这么读在实际环境中会抖动尤其水泵启动、水流波动的时候电平可能会快速地跳变几次。如果主循环直接拿这个值去刷新OLED屏幕上的状态会闪烁不停。处理办法是加一个简单的软件滤波。我用的是连续多次采样取多数值的方法#define FILTER_COUNT 10 uint8_t ReadWaterLevel(void) { uint8_t high_count 0; for (uint8_t i 0; i FILTER_COUNT; i) { if (HAL_GPIO_ReadPin(WATER_PIN_GPIO_Port, WATER_PIN_Pin) GPIO_PIN_SET) { high_count; } HAL_Delay(2); // 每次采样间隔2ms共采样10次 } return (high_count FILTER_COUNT / 2) ? 1 : 0; }这个滤波虽然简陋但对这种慢变化的液位信号完全够用。如果你要用在要求更高的场合可以用取中值滤波或者一阶低通滤波但那是后话了。5.2 状态判断与OLED刷新策略得到水位状态后要处理成用户能看懂的信息。我定义了一个枚举typedef enum { WATER_LEVEL_LOW, // 缺水 WATER_LEVEL_NORMAL // 水位正常 } WaterLevelStatus;然后写一个状态刷新函数void DisplayWaterStatus(WaterLevelStatus status) { OLED_Clear(); OLED_ShowString(0, 0, Water Monitor, 16); OLED_ShowChinese(0, 2, font_water); // “水” OLED_ShowChinese(16, 2, font_level); // “位” OLED_ShowString(0, 6, Status:, 16); if (status WATER_LEVEL_LOW) { OLED_ShowChinese(48, 6, font_low); // “偏低”或“缺水” BEEP_ON(); } else { OLED_ShowChinese(48, 6, font_normal); // “正常” BEEP_OFF(); } }有一个非常重要的设计思路不要在每次主循环都去刷新OLEDOLED的I2C刷新虽然快但反复整屏刷会占用大量时间而且肉眼根本感知不到这毫秒级的差异。我的做法是仅在状态发生变化时刷新配合一个状态变化检测WaterLevelStatus last_status WATER_LEVEL_NORMAL; while (1) { uint8_t level ReadWaterLevel(); WaterLevelStatus current_status (level 1) ? WATER_LEVEL_NORMAL : WATER_LEVEL_LOW; if (current_status ! last_status) { DisplayWaterStatus(current_status); last_status current_status; } HAL_Delay(100); }这样有两个好处一是屏幕不会闪烁不停二是蜂鸣器不会因为电平抖动而反复响。5.3 完整代码展示把上面所有片段拼起来核心逻辑就是完整的实验程序#include main.h #include i2c.h #include gpio.h // OLED驱动函数见第4节此处省略实现 void OLED_Init(void); void OLED_Clear(void); void OLED_ShowChinese(uint8_t x, uint8_t y, const uint8_t *font); void OLED_ShowString(uint8_t x, uint8_t y, char *str, uint8_t size); #define WATER_PIN_GPIO_Port GPIOA #define WATER_PIN_Pin GPIO_PIN_0 #define BEEP_PIN_GPIO_Port GPIOB #define BEEP_PIN_Pin GPIO_PIN_0 #define BEEP_ON() HAL_GPIO_WritePin(BEEP_PIN_GPIO_Port, BEEP_PIN_Pin, GPIO_PIN_RESET) #define BEEP_OFF() HAL_GPIO_WritePin(BEEP_PIN_GPIO_Port, BEEP_PIN_Pin, GPIO_PIN_SET) #define FILTER_COUNT 10 uint8_t ReadWaterLevel(void) { uint8_t high_count 0; for (uint8_t i 0; i FILTER_COUNT; i) { if (HAL_GPIO_ReadPin(WATER_PIN_GPIO_Port, WATER_PIN_Pin) GPIO_PIN_SET) high_count; HAL_Delay(2); } return (high_count FILTER_COUNT / 2) ? 1 : 0; } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); OLED_Init(); OLED_Clear(); uint8_t last_level 0xFF; while (1) { uint8_t level ReadWaterLevel(); if (level) { OLED_Clear(); OLED_ShowString(0, 0, Water Monitor, 16); OLED_ShowString(0, 3, Level: HIGH, 16); OLED_ShowString(0, 5, Status: OK, 16); BEEP_OFF(); } else { OLED_Clear(); OLED_ShowString(0, 0, Water Monitor, 16); OLED_ShowString(0, 3, Level: LOW, 16); OLED_ShowString(0, 5, Please ADD, 16); BEEP_ON(); HAL_Delay(2000); BEEP_OFF(); } HAL_Delay(200); } }这里为了展示核心逻辑把中文显示简化成了英文显示实际项目里把OLED_ShowString替换成OLED_ShowChinese即可代码结构完全一样。6. 实测效果、信号解读与常见坑6.1 实测现象记录整套系统搭好后现象是非常直观的空杯状态OLED显示“Level: LOW”蜂鸣器隔一秒响一次。往杯子里倒水当水位上升到传感器贴装高度时OLED在两三百毫秒内变为“Level: HIGH”蜂鸣器停止。再倒掉一些水水位下降又变回“LOW”响应几乎没有延迟。传感器电平翻转的“手感”值得说一下。我最初以为水位接近传感器时会有一个渐变过程比如先高阻、再输出不确定电平。实测发现这个传感器的比较电路很干脆水位差一点点到位置输出保持一个状态水位一越过位置输出立刻翻转到另一个状态翻转时间在几十毫秒级别。这种特性非常适合做阈值型检测。用万用表测了一下输出脚电压无水时0V有水时3.28V因为我用3.3V供电电平标准不用做任何转换就能直接进STM32的GPIO。6.2 五个我踩过的坑下面这些坑每一个都是我拿实际调试时间换来的写出来帮你少走弯路。坑一传感器贴歪导致翻转点在错误高度第一次装传感器时没量高度随手贴在杯子中部结果发现水位已经超过预期位置一厘米了传感器还没有翻转。原因是感应区域的中线和实际水位线有偏差。正确做法先把传感器平放在桌面上找出背面的感应极板位置通常是一块铜箔区域透过外壳能隐约看到然后你希望检测的水位高度要对应这个极板的下沿而不是传感器正中间的壳体外缘。坑二双面胶和气泡让检测失效用厚的海绵双面胶固定传感器结果完全测不到水位变化。拆开一看海绵胶太厚传感器和杯壁之间有大量空气间隙电容感应的有效面积被严重削弱。换用透明薄胶带问题立刻消失。如果你用双面胶挑薄的、无泡型的贴完后用手按一按排掉气泡。坑三OLED花屏/白屏I2C OLED最常见的故障就是花屏。排查顺序我建议是先确认地址对不对用I2C扫描再确认I2C总线上拉电阻很多模块板载了上拉但如果你用的是杜邦线比较长超过20cm建议在SCL和SDA上各加一个4.7kΩ上拉到3.3V效果立竿见影。坑四蜂鸣器一直响拔掉杜邦线才好我用的蜂鸣器模块是低电平触发但模块上默认是下拉状态还是悬空我不确定。调试时发现不接蜂鸣器信号线蜂鸣器也会微弱地响。原因是PB0悬空时引脚电平不确定低电平触发模块导致误动作。解决办法在PB0上配置内部上拉让默认状态为高电平蜂鸣器不响只有主动拉低才响。坑五ADC版本的传感器接到GPIO口上显示永远正常如果你买的是HS-S37A模拟量版本它的OUT输出是模拟电压接到数字GPIO上会一直被当成高电平。我一开始没注意版本差点以为是传感器坏了。模拟量版本要接到STM32的ADC引脚如PA1通过ADC采集电压值跟阈值比较判断水位。这块我放在下一节细说。6.3 如何让检测更可靠如果要把这个实验经过简单改造就用在真实场景里我建议再加两个措施多阈值检测一个传感器只能检测“到/没到”某个高度要检测“低水位”和“满水位”两个点就贴两个传感器分别接PA0和PA1。高水位传感器检测满水停止加水低水位传感器检测缺水报警逻辑不复杂但体验完全不一样。防抖时间窗口软件滤波能滤掉脉冲毛刺但滤不掉持续一两秒的波动比如水泵工作引起的水面震荡。可以在状态切换时加入一个2秒的确认窗口如果新状态连续保持2秒才切换显示。宁可响应慢一点也不误报。7. 进阶玩法模拟量版本、多路检测与低功耗改造最后聊几个能让这个项目从“实验”变成“产品原型”的方向都是我自己验过可行的路子。7.1 模拟量版本怎么处理如果你拿到的是模拟量输出的HS-S37A原理差不多只是读取方式从GPIO变成了ADC。接线还是三根线OUT接到STM32的ADC输入引脚比如PA1配置CubeMX时把PA1选为ADC1_IN1开启连续转换模式。代码层面的变化也不大只是把电平判断改成电压阈值判断uint32_t adc_value 0; HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 100); adc_value HAL_ADC_GetValue(hadc1); // 假设12位ADC基准电压3.3V测得电压 adc_value * 3.3 / 4095 if (adc_value 2000) // 具体阈值要实测标定 { // 水位到达 } else { // 水位未到 }模拟量版本的好处是你可以知道“当前电容变化的强度”容纳更多判断逻辑比如贴近程度不好时电压值会偏低可以用来做自检。7.2 多路检测的接线建议多传感器的接线其实很简单每个传感器的VCC和GND并联OUT各接一个独立引脚就行。需要注意的是多个传感器如果共用一条电源线要注意总电流。我实测单个HS-S37A工作电流在几毫安级别三个并联也没压力STM32的3.3V引脚完全扛得住。7.3 低功耗场景的改造思路如果你想把水位监测做成电池供电的小模块有两个地方要改所有外设都从GPIO可控供电。OLED的VCC不要直接接3.3V而是接一个空闲GPIO比如PB1平时输出低电平给OLED断电需要显示时拉高。OLED模块虽然静态功耗不大但长期待机还是浪费。STM32进入STOP模式用RTC唤醒或者外部中断唤醒。HS-S37A接在外部中断引脚比如PA0配置为EXTI水位变化时通过中断唤醒MCU刷新一次OLED后再次进入睡眠。这个低功耗版本我实测过一颗CR2032纽扣电池撑了大约两个月对一块永不停机的“水位监视器”来说算是不错的结果了。如果需求再严苛可以把OLED干脆去掉改用一颗红色LED加蜂鸣器功耗会再降一个量级。做这个项目的过程中我个人最大的收获不是学会了I2C或者GPIO而是理解了“传感器选型决定整个系统的设计走向”。非接触式检测虽然看起来只是省掉了开孔的麻烦但它带来的好处是长期的、结构性的——没有密封问题、没有腐蚀问题、不需要定期清理传感器表面。很多时候硬件上一个聪明的选型比软件上写一百行防御代码都管用。希望这篇记录能让你少踩几个坑把这个小实验一次跑通。
返回列表