尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32驱动HS-S37A非接触式水位传感器+OLED显示完整实验

STM32驱动HS-S37A非接触式水位传感器+OLED显示完整实验 STM32驱动HS-S37A水位传感器OLED完整实验讲解好久没写这种“从零到一”的完整实验了。前两天收拾柜子翻出一块HS-S37A非接触式水位传感器和一块0.96寸I2C接口的OLED屏正好手边有块STM32F103C8T6最小系统板索性就把这套组合完整地跑了一遍传感器采集水位状态STM32读取信号再把结果实时显示在OLED上。整个过程从接线、写驱动、调参到最终显示一路踩了几个不大不小的坑这里把完整思路和代码细节都整理出来给正在做类似小项目的朋友一个可以直接参考的样板。这套方案的核心价值在于用非接触式传感器替代传统的浮球或电极式水位检测传感器贴在容器外壁就能感知水位变化不接触液体、不污染水质、也不会被腐蚀非常适合鱼缸水位监测、热水器防干烧、智能灌溉、饮水机缺水提醒这类场景。而STM32负责信号采集与逻辑判断OLED提供直观的人机交互界面三者组合起来就是一个很典型的“传感器主控显示”嵌入式最小系统拿来练手或者改造成毕设项目都非常合适。先说清楚这个实验对基础的要求不算高你需要会建STM32工程标准库或者HAL库都行本文用标准库、能烧录程序、看得懂GPIO和I2C的基本概念就行。如果你这些还不太熟也不着急文章里我会把关键部分的原理掰开揉碎地讲照着抄也能跑起来。1. HS-S37A水位传感器非接触检测的核心原理与接线细节1.1 非接触式水位检测到底是怎么实现的HS-S37A本质上是一个基于电容感应原理的非接触式液位传感器。很多人第一次听到“非接触”都会觉得玄乎不碰到水怎么知道有没有水原理其实不复杂。传感器内部有一个高频振荡电路当传感器贴附在容器比如亚克力水箱、塑料水管的管壁外侧时传感器与管壁、管壁内的水共同构成一个电容。水的介电常数大约80和空气的介电常数约1差异巨大因此当管壁内的水位到达或超过传感器感应区域时整个结构的等效电容会发生明显变化这个变化会引起内部振荡电路的振荡频率或幅度发生改变传感器内部电路检测到这个变化后通过比较器输出对应的电平信号。HS-S37A的输出是数字电平当感应区域检测到有水时输出低电平没水时输出高电平。它的工作电压范围比较宽典型5V但3.3V也能工作输出的数字信号可以直接接到STM32的GPIO上不需要额外的信号调理电路。为了验证这个原理我专门做了个简单测试把传感器贴在透明亚克力杯外侧用注射器往杯里注水。实测发现当水面上升到传感器感应区中部时输出电平跳变非常干脆几乎没有中间状态这对数字采集来说非常友好。不过要注意传感器的感应区域是有方向的一般壳体上会有一条标记线或者感应面标识安装时必须让感应面紧贴容器壁且保证感应区域和液面升降方向垂直。1.2 实际接线传感器、STM32和OLED的电气连接我这次实验用到的硬件清单如下器件型号/规格数量主控板STM32F103C8T6最小系统板1水位传感器HS-S37A非接触式水位传感器3线制1显示模块0.96寸OLEDI2C接口SSD1306驱动1下载器ST-Link V2或USB转TTL1电源5V USB供电或AMS1117-3.3稳压1辅助材料亚克力水箱/塑料水管杜邦线面包板若干HS-S37A三根线的定义是红线接电源正极VCC黄线接信号输出OUT黑线接地GND。在这里我踩了一个很多新手容易忽略的坑传感器虽然可以5V供电但信号输出引脚的电平是和供电电压一致的。如果传感器接5V那输出高电平就是5V直接进STM32的GPIO虽然大多数情况下能识别但长期运行对引脚不友好。稳妥的做法是把传感器接在3.3V上或者用电阻分压/电平转换电路把信号降到3.3V。我实测下来HS-S37A在3.3V供电下工作完全正常输出跳变依然干脆所以建议直接接3.3V。OLED屏是标准的0.96寸I2C接口模块四根线VCC接3.3VGND接地SCL接PB6I2C1时钟线SDA接PB7I2C1数据线。STM32F103C8T6的I2C1引脚默认就复用在这两个脚上用标准库直接配置即可。完整的接线关系如下表所示STM32引脚外设引脚说明3.3VOLED VCC、HS-S37A VCC电源GNDOLED GND、HS-S37A GND公共地PB6OLED SCLI2C1 时钟线PB7OLED SDAI2C1 数据线PA0HS-S37A OUT传感器信号输入配置为上拉输入这里补充一下为什么把传感器信号接在PA0上。PA0在F103上可以配置成EXTI0外部中断输入后续如果做低功耗场景可以直接用外部中断唤醒MCU而不是一直轮询。这次实验我先用轮询的方式读取但在代码结构上做了预留方便扩展。注意不同卖家提供的HS-S37A模块线序颜色可能不一样接线前务必对照商家给的规格书确认不要想当然按颜色接。2. 软件架构从STM32工程搭建到OLED驱动与传感器轮询2.1 基于标准库的工程初始化本次代码使用STM32标准外设库Standard Peripheral Library版本是3.5开发环境用Keil MDK5。工程创建这里不展开细说重点讲几个关键初始化部分。首先是GPIO初始化PA0作为传感器输入需要配置为浮空输入或上拉输入。考虑到传感器输出高电平是推挽输出而低电平是开漏/强拉低用上拉输入更保险避免悬空时电平漂移。void GPIO_InitConfig(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能GPIOA和GPIOB时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // PA0: 水位传感器信号输入上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); // PB6: I2C1_SCL 复用开漏输出 // PB7: I2C1_SDA 复用开漏输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); }I2C1外设的初始化也要一并完成配置为标准模式100KHz速率void I2C_InitConfig(void) { I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_ClockSpeed 100000; // 100kHz I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_Address 0x00; // 主模式地址无所谓 I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }2.2 OLED驱动核心逻辑SSD1306的I2C时序和显存操作OLED屏的控制芯片是SSD1306它只认识I2C时序的命令和数据。这里有一个新手很容易卡住的点SSD1306的I2C地址到底是什么不同厂家的模块地址可能是0x3C也可能是0x3D具体取决于模块上DC引脚或SA0引脚的电平设置。我手上这块地址是0x3C代码里封装了一个宏定义万一你的屏幕不亮第一个要排查的就是这个地址对不对。SSD1306的通信流程是主机发送起始位发送从机地址7位地址左移一位加上读写位然后连续发送控制字节和数据字节。控制字节为0x00表示后续是命令0x40表示后续是显示数据。这个细节非常重要因为SSD1306通过控制字节来区分命令和数据的。我用标准库的I2C外设封装了两个最底层的函数// 向OLED写一个字节mode0表示命令mode1表示数据 void OLED_WriteByte(uint8_t data, uint8_t mode) { while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, OLED_ADDR, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); if (mode 0) I2C_SendData(I2C1, 0x00); // 控制字节命令 else I2C_SendData(I2C1, 0x40); // 控制字节数据 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1, data); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); }SSD1306内部有一块128x64位的显存GDDRAM每8个像素点组成一页一共8页。写入显存后屏幕并不会自动刷新而是需要设置列起始地址和页地址然后按顺序写入显示数据。这就是为什么驱动代码里初始化函数后面跟了一长串地址设置命令的原因。显示字符串的时候我维护了一个全字模库ASCII字符集用取模软件生成8x16大小通过查表方式逐字符写入显存。核心思想是先把OLED的显存理解为一个坐标空间x范围0~127y范围0~63每显示一个字符需要先设置好这个字符左上角的坐标。// 显示一个字符串 void OLED_ShowString(uint8_t x, uint8_t y, char *str) { while (*str ! \0) { OLED_ShowChar(x, y, *str); x 8; if (x 120) // 超出右边界换行 { x 0; y 2; } str; } }2.3 传感器轮询逻辑软件滤波与状态判断水位传感器的读取在逻辑上很简单读PA0的电平低电平代表检测到水高电平代表没水。但实际应用中液面晃动、气泡经过感应区、传感器贴附不紧密等情况都可能导致电平短暂抖动因此我加了一个简单的软件滤波连续读取10次如果10次中低电平出现了8次以上才判定为“有水”否则判定为“无水”。uint8_t WaterLevel_Detect(void) { uint8_t count 0; uint8_t i; for (i 0; i 10; i) { if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 0) count; Delay_Ms(5); // 每5ms读一次 } if (count 8) return 1; // 有水 else return 0; // 无水 }这里的5ms延时不是随便写的。一方面10次共耗时50ms这个时间足够覆盖液面抖动的常见周期另一方面这个延时不会对OLED显示刷新造成明显影响因为人眼本来就需要几十毫秒才能感知画面变化。如果你要做更快速的响应可以适当减少采样次数或缩短间隔但要注意别引入误判。3. 完整代码实现与屏幕显示设计3.1 主循环数据采集、状态判断与显示刷新主程序逻辑非常清爽初始化外设清屏进入主循环每轮循环做一次水位检测然后把结果和附加信息通过OLED显示出来。int main(void) { uint8_t waterLevel; char displayBuf[20]; Delay_Init(); GPIO_InitConfig(); I2C_InitConfig(); OLED_Init(); OLED_Clear(); // OLED显示固定提示信息 OLED_ShowString(0, 0, HS-S37A Sensor); OLED_ShowString(0, 2, STM32F103C8T6); while (1) { waterLevel WaterLevel_Detect(); if (waterLevel) { OLED_ShowString(0, 4, Water: YES); OLED_ShowString(0, 6, Level: HIGH); } else { OLED_ShowString(0, 4, Water: NO ); OLED_ShowString(0, 6, Level: LOW ); } // 显示当前引脚电平值方便调试 sprintf(displayBuf, PA0:%d, GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)); OLED_ShowString(0, 5, displayBuf); Delay_Ms(200); } }这里的OLED坐标参数需要注意OLED_ShowString的第二个参数y是页索引0代表第0页屏幕最顶端6代表第6页屏幕靠下的位置。一行字符的高度是2页所以坐标0、2、4、6就是从上到下四行8x16字符的显示位置。这个设计保证每行字符不会重叠。3.2 OLED初始化的关键配置序列解析很多人在移植OLED驱动时ifdef比较多看着就头疼。其实SSD1306的初始化序列是固定的这里我给出一个精简版并标注每一条命令的含义void OLED_Init(void) { Delay_Ms(100); // 上电等待屏幕内部复位完成 OLED_WriteByte(0xAE, 0); // 关闭显示 OLED_WriteByte(0xD5, 0); // 设置时钟分频因子 OLED_WriteByte(0x80, 0); OLED_WriteByte(0xA8, 0); // 设置驱动路数 OLED_WriteByte(0x3F, 0); // 64路 OLED_WriteByte(0xD3, 0); // 设置显示偏移 OLED_WriteByte(0x00, 0); OLED_WriteByte(0x40, 0); // 设置起始行 OLED_WriteByte(0x8D, 0); // 电荷泵开关 OLED_WriteByte(0x14, 0); // 开启电荷泵 OLED_WriteByte(0x20, 0); // 设置内存寻址模式 OLED_WriteByte(0x02, 0); // 页寻址模式 OLED_WriteByte(0xA1, 0); // 段重映射 OLED_WriteByte(0xC8, 0); // 扫描方向 OLED_WriteByte(0xDA, 0); // 设置COM硬件配置 OLED_WriteByte(0x12, 0); OLED_WriteByte(0x81, 0); // 设置对比度 OLED_WriteByte(0xCF, 0); OLED_WriteByte(0xD9, 0); // 设置预充电周期 OLED_WriteByte(0xF1, 0); OLED_WriteByte(0xDB, 0); // 设置VCOMH电压倍率 OLED_WriteByte(0x40, 0); OLED_WriteByte(0xA4, 0); // 显示使用GDDRAM内容 OLED_WriteByte(0xA6, 0); // 正常显示非反色 OLED_WriteByte(0xAF, 0); // 开启显示 OLED_Clear(); }这条初始化序列有几个关键点电荷泵必须开启0x8D命令配合0x14参数为屏幕内部DC-DC转换器供电。如果这一步没做屏幕上什么都显示不出来。这是OLED模块最常见的不亮原因之一。对比度设置0x81命令后跟0xCF这个值可以在0~255之间调整。如果你觉得显示偏暗或偏亮可以修改这个值效果立竿见影。页寻址模式0x20命令后跟0x02表示使用页寻址。这种模式下你写完一页128字节数据后列地址会自动回到起始位置页地址保持不变或由软件控制切换便于逐页刷新。3.3 字模与取模方式显示汉字和特殊符号需要字模库。我用的是PCtoLCD2002软件设置如下取模方式选择“逐行式”取模走向选择“顺向”输出数制“十六进制”每行显示数“16”。以16x16大小的汉字为例每个汉字占32字节。这里我举例如果要显示“水”字字模数组大致长这样const uint8_t HZ_Water[32] { 0x00, 0x00, 0x7F, 0xFC, 0x08, 0x20, 0x08, 0x40, 0x08, 0x80, 0x09, 0x00, 0x0A, 0x00, 0x0C, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00 };字模显示函数要比显示字符多一个步骤先写上半部分8行取前16字节再写下半部分8行取后16字节因为16x16的汉字在8页结构中占两页。提示如果你是第一次用PCtoLCD2002注意一定要把取模的“字节内位序”设为“从高位开始”否则字会“反”着显示。这也是新手非常容易踩的坑。4. 实测过程、调参经验与踩坑记录4.1 实测场景从亚克力杯到塑料水管我先在亚克力杯上做第一次实测。传感器用双面胶贴在杯壁外侧感应面朝向杯内垂直放置。杯里的水位用注射器逐步增加每次增加5mm左右同时观察OLED显示状态。结果发现一个问题当水位低于传感器感应区下边缘约2mm时OLED已经显示“Water: YES”了。也就是说传感器存在一个“提前触发”现象。原因在于传感器的感应区域不是一个绝对的点而是一个有一定高度的区域而且液面靠近但尚未完全到达时电容变化已经足以触发内部比较器翻转。这个现象本身不影响使用但如果你做的是“水位上限报警”类应用就需要知道传感器触发点对应的实际水位高度必要时在安装位置上做补偿。比如希望水位到达10cm位置时报警实际上传感器在9.8cm左右就触发了那你可以把传感器稍微上移一点来校准。随后我又把传感器贴在透明塑料水管外壁测试。这种细管场景下管内水量少传感器的感应灵敏度会下降。实测外径20mm、壁厚2mm的塑料管传感器贴着管壁时依然能可靠检测到管内是否充满水。但如果管子壁厚超过5mm或者管道直径小于8mm触发就会变得不稳定。这种情况下建议选择灵敏度更高的型号或把传感器贴在管径较粗的部位。4.2 干扰与误判的排查链路实验过程中遇到过两次“传感器输出状态和实际水位对不上”的情况排查过程值得分享。第一次液面明明没有到达传感器感应区OLED却显示“有水”。我第一反应是怀疑GPIO配置问题但用万用表量传感器输出脚发现确实是低电平说明传感器自己触发晚了或者说提前了。进一步观察发现当时传感器附近的管壁外表面有水流淌过后留下的水膜这层水膜改变了传感器附近的介质环境导致等效电容变大。用干布擦净管壁后现象消失。这提醒我非接触式传感器虽然不接触液体但传感器感应面周围不能有积水或高介电常数物质安装时要注意保持感应面清洁干燥。第二次传感器输出高电平无水状态但实际水箱里明明有半箱水。排查后发现双重原因叠加传感器贴在容器上的位置偏高而水位虽然过半但还没到达传感器的感应区此外容器壁是磨砂亚克力厚度偏厚导致灵敏度下降。重新调整安装位置后问题解决。4.3 OLED显示异常的几个经典坑OLED部分的坑主要出现在初次上电阶段第一坑屏幕全黑背光也没有。用万用表量OLED模块的VCC和GND之间电压发现只有1.8V检查后发现是杜邦线接触不良。OLED模块电流虽然不大但压降不容忽视尤其是用了较长杜邦线的时候。建议3.3V和GND各用两根杜邦线并联供电或者直接焊接能显著减少接触电阻。第二坑屏幕亮但显示乱码。这是因为I2C速率不匹配或地址错误。我先用逻辑分析仪抓包发现从机地址发送的是0x3D7位地址模式下读写位和地址没拼对改成0x3C后正常。标准库里I2C_Send7bitAddress函数会自动处理读写位所以地址参数直接填0x3C即可不需要自己左移一位。第三坑某些区域出现“鬼影”显示内容残留。这是SSD1306的显存更新不完整导致的。我的解决办法是每次刷新前先调用OLED_Clear()清屏虽然会引入一点点闪烁但能确保显示内容干净。如果你对闪烁敏感可以把静态内容如标题行和动态内容如水位状态分开区域刷新只在动态区域做清屏和重写。5. 拓展思路如何把实验做成一个完整的应用项目5.1 增加声光报警功能在水位监测的基础上很容易扩展报警功能。当检测到水位过低时可以驱动一个无源蜂鸣器发声同时让一个LED闪烁。硬件上只需要把蜂鸣器接在某个PWM输出引脚上用定时器产生不同频率的方波即可。代码层面在主循环检测到水位为0时控制PB1输出不同占空比的PWM信号或者用GPIO翻转模拟。这样做之后这套系统就可以直接改造成热水器防干烧提醒器。5.2 接入Wi-Fi模块实现远程水位监控如果你有ESP8266或ESP32模块可以通过串口与STM32通信STM32把水位状态封装成简单的协议帧比如帧头是0xAA数据位0x01表示有水、0x00表示无水再加上帧尾0x55。ESP8266通过AT指令或MQTT协议把状态上报到云平台手机端就能实时查看。这种方案在智能鱼缸、远程灌溉系统里非常常见。5.3 用多个HS-S37A实现多水位监测一个鱼缸或者水箱如果只检测一个水位很多时候不够用。比如需要监控“低水位报警”和“高水位停机”两个状态那就需要两个传感器分别贴在容器的上、下两个位置。每个传感器的信号线接一个GPIO程序里分别读取并做逻辑判断。STM32F103C8T6的GPIO资源非常充足接四五个传感器一点问题都没有。不过要注意多个传感器同时工作时要确保它们的供电稳定避免共地干扰导致误判。5.4 从轮询改成外部中断模式在低功耗场景中MCU不能一直醒着轮询传感器。可以把PA0配置成EXTI0外部中断在下降沿检测到水和上升沿水位撤离时唤醒MCU其他时间进入Stop模式。这样一来平均功耗能大幅下降适合电池供电的户外灌溉装置。代码改造也不复杂主要是配置EXTI中断线0和NVIC中断优先级在中断服务函数里置一个标志位主循环查询标志位后刷新显示。5.5 HS-S37A与其他传感器的组合应用水位传感器单独用略显单薄如果加上DS18B20温度传感器可以做一个“水位温度”一体化的养殖水箱监测器。DS18B20是单总线协议占用一个GPIO和OLED、水位传感器用同一个STM32完全不成问题。显示界面可以分两屏一屏显示水位状态一屏显示实时水温用按键或自动轮播切换。这种组合项目的完成度比单片传感器高很多作为毕业设计或者课程设计作品拿出来说服力强不少。6. 调试工具与代码问题定位的个人经验整个实验做下来最花时间的不是写代码而是查问题。这里分享几个非常实用的调试工具和方法。逻辑分析仪是排查I2C问题的神器。我用的是一根USB逻辑分析仪8通道24MHz采样率在OLED不显示的时候用它抓一下SCL和SDA上的波形马上能看出有没有数据在传输、时序是否正确、ACK位是否正常。相比用示波器逻辑分析仪的优势是可以解码协议直接看到主机发了什么地址、什么数据。串口打印在传感器调参阶段帮了大忙。我习惯在传感器状态变化的瞬间通过UART1打印一条带时间戳的日志方便复盘。如果你手头没有USB转TTL模块也可以用OLED临时充当调试输出把关键变量打印到屏幕上。这里要注意sprintf在STM32标准库里比较占Flash和RAM如果空间紧张可以用简单的数字转字符串函数替代。代码版本管理方面虽然这种小实验可能不需要上Git但至少要在改代码前备份一份能跑的版本。有一次我调整了OLED的初始化时序结果屏幕死活不亮来回改了一个多小时最终发现是漏了一条命令。如果当时有备份直接对比差异几秒钟就能定位问题。我想特别强调一下延时函数的准确性。标准库的Delay_Ms如果有微秒级的偏差在OLED初始化、传感器轮询采样这些对时序有要求的场景里会产生一些莫名其妙的问题。建议用定时器做一个严谨的延时比如用SysTick做1ms基准而不是用简单的空循环。实测中如果Delay不准确传感器滤波采样的时间间隔会漂移偶尔会出现判断结果抖动的现象。整个系统跑起来后功耗表现也挺有意思。STM32F103C8T6在72MHz主频下再加上OLED和传感器的工作电流整体功耗大约在30mA左右。如果后续你用电池供电别忘了让OLED进入休眠模式通过0xAE命令关闭显示并降低MCU主频。对于大多数桌面供电场景这点功耗可以忽略。最后说一个很多人会忽略的小事传感器的固定方式。双面胶临时测试没问题但长期使用会因为水汽、温度变化导致感应面贴合不均匀进而影响检测的一致性。建议在正式项目中用硅胶或环氧树脂把传感器牢固地贴在容器壁指定的感应位置确保感应面和容器壁之间没有空气间隙。空气的介电常数低如果存在间隙相当于在水和传感器之间加了一层“绝缘垫”会明显降低检测灵敏度。这个细节直接决定你设备的长期稳定性。这个实验做下来从最底层的GPIO配置到传感器数据的采集判断再到OLED的驱动显示虽然每一部分拆开看都不复杂但组合在一起就是一套完整的嵌入式应用开发流程。如果你正在学STM32对传感器和显示模块的配合还不太熟这个组合是个特别好的练手项目。照着我上面的步骤和代码走一遍相信你能很快跑通并在跑通的基础上做出自己的扩展功能。
返回列表