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 SSD1306,I2C接口 | 四针:VCC、GND、SCL、SDA |
| 蜂鸣器 | 有源蜂鸣器模块 | 低电平触发型,DO接STM32引脚 |
| 容器 | 透明塑料水杯 | 壁厚3mm以内效果最好 |
| 杜邦线 | 母对母若干 | I2C接线建议尽量短 |
关于HS-S37A的线序,我手上这颗是标准的三线:红色VCC、黑色GND、蓝色OUT。不同批次可能颜色会有点差异,拿到货第一时间用万用表确认一下,别直接凭颜色接,烧了传感器不划算。
2.2 接线顺序与供电细节
完整接线表放在下面:
| HS-S37A | STM32F103C8T6 | OLED | 蜂鸣器 |
|---|---|---|---|
| VCC | 3.3V(或5V) | VCC | VCC |
| GND | GND | GND | GND |
| OUT | PA0 | - | - |
| - | 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.3V,I2C总线电压也就跟着是3.3V,不会出现电平不匹配的问题。
2.3 贴装位置:效果好坏的关键一步
这部分是实验成败的分水岭,比写代码重要得多。
HS-S37A的正确贴法是这样的:
- 先把塑料杯外壁擦干净,不能有水珠和油污。
- 在传感器背面(没有元件的那一面,也就是感应极板侧)贴一小片双面胶,或者用透明胶带固定。
- 把传感器竖直贴在杯子外壁你要检测的水位高度上,贴的时候稍微用力按压一下,让传感器外壳和杯壁贴合紧密,内部不能有气泡。
- 传感器要竖直贴,感应区域的中线跟水平面平行,这样水位上升时能平稳越过感应区,翻转更干脆。
我第一次实验图省事,直接用手扶着传感器靠在杯子上,结果水位从低到高变化了好几次,电平一直在跳,完全没法用。后来用胶带固定好,重新测试,现象立刻干净了。所以这个步骤真的别偷懒。
提示:杯壁越薄,检测越灵敏。一次性水杯、饮料瓶那种薄壁容器,效果最好;双层玻璃杯、保温杯这种就别试了,基本测不出来。
3. CubeMX工程配置:时钟、GPIO与I2C
我用的是STM32CubeIDE + HAL库,配置在CubeMX里完成。整套配置的逻辑对你以后做别的项目也有复用价值。
3.1 基础时钟树
新建工程时选择芯片型号STM32F103C8T6。时钟配置我直接选了外部8MHz晶振,然后在Clock Configuration里把系统时钟拉到72MHz。
F103的时钟树上有个经典配置方案:
- HSE选Crystal/Ceramic Resonator
- PLL倍频为9倍,得到72MHz系统时钟
- APB1分频器设2,使APB1总线时钟保持36MHz,这样I2C1才能申请到正确的时钟源
3.2 GPIO输入与I2C外设配置
在Pinout & Configuration页面里:
1. PA0配置为GPIO_Input
先在芯片图上点击PA0引脚,选择GPIO_Input。然后在GPIO设置里:
- GPIO mode: Input mode
- GPIO Pull-up/Pull-down: Pull-up
- 这是关键。HS-S37A的输出模式是推挽输出,但它从高阻到高/低电平切换时,如果外部悬空,中间状态会不稳定。内部上拉可以保证电平被钳制在确定值。实际测试中,即使不加上拉也能工作,但上拉后抗干扰性明显好很多。
2. PB6、PB7配置为I2C1
在芯片图上点击PB6,选择I2C1_SCL,点击PB7选择I2C1_SDA,CubeMX会自动配置。然后在I2C1参数设置里:
- I2C Speed Mode: Standard Mode(100kHz)
- 市面上的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); }清屏函数则是向整块显存写0x00:
void 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(); } }有一个非常重要的设计思路:不要在每次主循环都去刷新OLED,OLED的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,而是理解了“传感器选型决定整个系统的设计走向”。非接触式检测虽然看起来只是省掉了开孔的麻烦,但它带来的好处是长期的、结构性的——没有密封问题、没有腐蚀问题、不需要定期清理传感器表面。很多时候,硬件上一个聪明的选型,比软件上写一百行防御代码都管用。希望这篇记录能让你少踩几个坑,把这个小实验一次跑通。