ARTICLE DETAIL

资讯详情

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

STM32驱动HS-S37A非接触式水位传感器与OLED显示完整教程

STM32驱动HS-S37A非接触式水位传感器与OLED显示完整教程 用STM32做传感器采集最容易栽跟头的不是代码晦涩而是器件选完了发现方案压根不成立。我前阵子给家里一个小型储水容器做水位提醒翻了一圈器件最终选了HS-S37A这颗非接触式水位传感器配上一块0.96寸I2C接口的OLED屏把采集、显示、阈值报警一次跑通。整套实验从接线到写完逻辑实际用时不到一个下午中间踩了几个很有代表性的坑今天专门写一篇完整的讲解把原理、接线、CubeMX配置、HAL库驱动、实测现象和排查技巧全部摊开讲。适合正在做STM32入门、准备毕业设计或者想给鱼缸、净水器水箱、加湿器这类场景加“水位提示”的开发者参考。1. 为什么选非接触式水位传感器 显示器来搭这套实验1.1 这个实验到底想解决什么问题先说场景。鱼缸缺水报警、花盆自动滴灌水箱、饮水机水槽、加湿器水箱这些地方都需要判断“还有没有水”或者“水位到哪了”。传统做法是浮球开关结构简单但需要容器开孔机械部件长时间泡水后容易卡滞而且浮球本身会接触水体在饮水设备里还要考虑卫生问题。更早的方案是电极式两根金属棒泡在水里靠水的导电性通断判断水位这种方案最大痛点是电极长期电解腐蚀隔一段时间就得换。HS-S37A属于非接触式电容感应方案传感器贴在容器外壁就能检测内部水位变化。容器不用开孔、探头不接触液体、没有机械活动件维护成本几乎为零。对生活类项目来说这套方案最大的优势就是“干净、不影响原容器结构”。对学习类项目来说它恰好覆盖了嵌入式开发里非常典型的三个外设方向ADC模拟量采集、I2C通信、显示屏驱动一个实验把三块内容全串起来性价比很高。1.2 方案选型为什么最终选了HS-S37A这种非接触传感器我整理过一份常见的液位检测方案对比列出来给大家参考方案类型是否接触液体安装方式维护成本典型问题浮球开关接触容器开孔或顶部悬挂中机械卡滞、密封老化电极式接触开孔伸入液体高电极氧化、电解污染水质超声波非接触容器上方安装中成本高、泡沫影响大非接触电容式HS-S37A非接触贴容器外壁低受壁厚和材质影响需标定我选HS-S37A的核心原因有三个。第一它没有机械动作部件不存在卡死或者磨损的问题。第二传感器体积小一颗模块贴在容器侧面不占地方适合鱼缸、小型水箱这类安装空间有限的场景。第三它输出的是模拟信号能给出“水多一点还是少一点”的连续变化量而不只是“有/无”两种状态配合ADC采集可以把水位百分比做出来实验的呈现效果和后续功能扩展空间都更大。另外要提醒一点HS-S37A这类模块有些版本是模拟输出有些版本是数字电平输出买的时候看清商品描述或者问一下店家。如果是数字输出版本它就只输出高/低电平只能判断有没有水模拟输出版本才能通过ADC读连续值。我这边用的是模拟输出版本下面的代码也是按这个写的。1.3 显示端为什么选OLED而不是LCD1602或串口屏显示端我反复比较过几种方案。LCD1602是很多教科书里教的经典外设但它需要至少6个IO引脚驱动而且只能显示字符想画一个水位百分比进度条很难做到。串口屏虽然显示效果出色但一块屏动不动几十块上百块对一个小实验来说严重超预算而且串口屏需要独立的配置软件学习成本也不低。0.96寸的I2C OLED模块是这套实验里最顺手的方案。硬件上只需要SDA、SCL两根信号线加上电源和地一共四根线和STM32连接非常省引脚。软件方面OLED内置SSD1306驱动芯片是基于I2C通信的MCU往它的显存区域写数据屏幕就会对应显示内容逻辑清晰。128x64的分辨率足够显示一行主数据、一行百分比和一个进度条图形。而且OLED是自发光屏幕对比度高在室内光线环境下显示效果非常直观很适合实验场景。1.4 开发方式为什么建议用HAL库 CubeMX组合STM32开发目前主要分标准库、HAL库、寄存器直接操作三个流派。寄存器开发能让你对芯片内部结构理解透彻但效率低一个外设初始化就要查一堆寄存器对新手不友好。标准库封了一层写起来比寄存器舒服但这些年新出的芯片和新版工具链支持已经逐渐弱化。HAL库配合STM32CubeMX图形化配置工具是目前综合效率最高的方案也是社区里资料最丰富的路线。这个项目涉及两个外设I2C和ADC如果用标准库手写初始化代码要自己查参考手册出问题排查也相对费劲。用CubeMX先在图形界面里把I2C1、ADC1、时钟树、调试口配置好它自动生成初始化代码我只关心业务逻辑开发节奏完全不一样。入门阶段强烈建议把HAL库作为主路线这不是说标准库不好而是在同样时间内HAL能让你把更多精力放在理解传感器和通信协议本身。2. 三种核心器件的原理拆解2.1 HS-S37A水位传感器非接触式电容检测原理First先看这张“非接触式水位传感器工作原理图”。传感器内部有一个振荡电路和一块感应极板感应极板贴在容器外壁它和容器内的水、空气以及外部环境之间会构成一个等效电容。空气的相对介电常数大约是1而水大约是80两者差异极大。当容器内液面升到感应区域时探头附近的介电常数明显变大等效电容随之变化内部电路把这个变化转换成电信号输出。用生活化的方式理解可以把感应区想象成一个特殊的“电子秤”空气和水在它面前就是两种不同密度的物体水位一上来“称重”结果就变传感器把这个变化的信号送到单片机。之所以能做到完全不接触液体是因为它依靠的是电场感应不是物理接触。但“非接触”不等于“随便贴”容器的材质和壁厚对灵敏度有直接影响塑料、玻璃、亚克力这类非金属薄壁容器效果最理想金属容器基本不适用。实际使用中HS-S37A模块输出的电压会随水位状态变化空杯和液面浸没感应区时的电压存在可测的差异但这个差异不会特别大所以项目中需要配置好ADC的分辨率和采样稳定性后面我会详细讲标定的思路。2.2 OLED显示模块看起来简单的屏里面的有机像素层其实很讲究OLED之所以薄、亮、省电和它的像素结构分不开。OLED屏的每个像素点其实是由多层有机功能薄膜夹在透明阳极ITO和金属阴极之间构成的典型结构依次是空穴注入层、空穴传输层、发光层、电子传输层、电子注入层每一层都只有几十纳米厚整个叠加起来不到几百纳米。工作时阳极注入空穴、阴极注入电子电子和空穴在发光层相遇复合产生激子激子退激时释放能量能量以光的形式发出。这个结构解释了OLED为什么不需要背光源因为它自己就是光源。这也是它在室内看比LCD更通透的原因。SSD1306驱动芯片内部有一块1KB的显存正好对应128x64像素主控通过I2C往这块显存写字节屏幕就显示对应的点阵内容。对实验来说我们只需要关心“往哪个坐标写什么”不需要管物理层怎么发光但了解像素是由多层薄膜构成这件事能帮你之后再遇到“屏幕某个区域亮度不一致”或“显示残影”时快速判断不是代码问题而是屏体质问题。2.3 STM32的ADC模拟量怎么变成能参与计算的数字HS-S37A输出的是模拟电压信号STM32要读懂它就得用内部的模数转换器。STM32的ADC是逐次逼近型结构原理可以理解为“用天平称重”内部有一组比较器和由电容阵列构成的DAC网络启动转换后先从最高位开始猜假设输入电压落在满量程的一半以上就置1否则置0逐位逼近经过12次比较就得到一组12位的数字。12位分辨率意味着把参考电压分成4096个等级。以3.3V参考电压为例每个量化等级约0.8mV也就是说ADC读到的数值每增加1对应电压增加大约0.8mV。这个精度对HS-S37A这种传感器是完全够用的。实际配置时要注意两点模拟输入引脚要设置为模拟模式否则内部引脚保护结构会影响采样采样周期不要选太快传感器输出阻抗不低采样时间太短电容充不满读出来的数值会稳定度差后面我在CubeMX配置里会给具体参数。3. 实验前的准备工作硬件清单与连线3.1 硬件清单和备选方案我做这套实验用的核心硬件如下全部是常见器件很容易凑齐器件型号/规格数量备注主控板STM32F103C8T6最小系统板1其他F1系列也可引脚会不同水位传感器HS-S37A非接触式模块1确认是模拟输出版本OLED屏0.96寸 128x64 SSD1306 I2C接口1最好选4针模块调试器ST-Link V21下载和调试都用它连接线杜邦线母对母/母对公若干建议颜色区分信号线容器500mL塑料瓶或塑料杯1壁越薄越灵敏测试液体清水适量用于改变水位万用表可选1排查问题时会用到ST-Link这里多说一句市面上便宜的克隆版ST-Link V2也可以用但驱动版本要注意我遇到过一次插上电脑后设备管理器里显示黄色感叹号的情况后面问题排查章节会专门讲。3.2 连线表与供电注意事项我使用的是STM32F103C8T6的I2C1外设默认引脚是PB6SCL和PB7SDA但不同开发板把I2C引脚映射到的地方可能不一样。我手上的板子把I2C1引出到PB6/PB7所以连线表按照这个来。ADC用PA0引脚它对应ADC1的通道0。HS-S37A传感器接STM32VCC3.3V或5V以模块标识为准GNDGNDOUTPA0ADC1_IN0OLED模块接STM32VCC3.3VGNDGNDSCLPB6SDAPB7供电方面有两点必须注意OLED和传感器都要和STM32共地否则I2C通信会异常ADC采集值也会乱跳。另外不要用同一个劣质USB口同时给开发板和一个大功率电机供电这种传感器的功耗不大但ADC对电源纹波有点敏感供电不够干净时采集值会明显抖动。实验阶段用一个单独的5V充电器或电脑USB口给开发板供电就够了。3.3 传感器安装位置与贴装技巧这个细节很容易被忽视但它对实验成败的影响比代码还大。HS-S37A传感器感应面要贴在容器外壁安装前先把表面擦干净用薄双面胶或热熔胶固定尽量让感应面和容器壁之间没有空气间隙。我调试时遇到过读数剧烈跳变最后发现是双面胶太厚中间夹了一个气泡电容感应完全被气泡干扰后来换成薄胶带紧贴一下就稳定了。安装高度取决于你想检测什么如果做缺水报警传感器贴在容器靠近底部的位置如果做溢出报警贴在靠近顶部的位置如果想检测中间水位就要布置多个传感器或者选一款感应面积更大的型号。实验调试阶段我建议先把传感器贴在塑料瓶侧面中部用手动改变水位来观察ADC值变化这样最容易理解传感器特性。4. 工程搭建STM32CubeMX配置与HAL库代码实现4.1 CubeMX里的关键配置项我以STM32F103C8T6为例打开STM32CubeMX新建工程选择对应芯片后需要配置以下几个地方首先是SYS标签页Debug选项选择Serial Wire否则烧录完程序后ST-Link可能识别不到芯片这个问题非常典型。然后是RCC标签页HSE选择Crystal/Ceramic Resonator外部高速晶振用于系统时钟。时钟树里把系统主频配置到72MHzF103的最高频率就是72MHz。I2C1标签页使能I2C1Speed Mode选择Standard Mode默认100kHz即可。OLED并不要求高通信速率100kHz稳定可靠反而400kHz快速模式在杜邦线较长时容易出现时序问题没必要为了快一点速度给自己找麻烦。ADC1标签页选择通道0也就是IN0对应PA0引脚。Resolution选择12位采样周期尽量选大一些比如55.5 Cycles这样等效输入阻抗要求低采样更稳定。Scan Conversion Mode关闭Continuous Conversion Mode关闭因为只需要单次转换软件每次启动一次采样拿一个值就够了。4.2 OLED驱动代码的移植思路OLED驱动是I2C应用层的经典例子。SSD1306初始化时主控需要写入一串配置命令。我整理了一份可以直接用的初始化序列static void OLED_Init(void) { uint8_t init_cmd[] { 0xAE, // 关闭显示 0xD5, 0x80, // 显示时钟分频/振荡器频率 0xA8, 0x3F, // 多路复用比64 0xD3, 0x00, // 显示偏移0 0x40, // 显示起始行0 0x8D, 0x14, // 使能电荷泵 0x20, 0x02, // 内存寻址模式页寻址 0xA1, // 段重映射列地址127映射到SEG0 0xC8, // COM扫描方向重映射 0xDA, 0x12, // COM引脚硬件配置 0x81, 0xCF, // 对比度207 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH 电压 0xA4, 0xA6, // 正常显示非反显 0xAF // 开启显示 }; for (uint8_t i 0; i sizeof(init_cmd); i) { OLED_WR_CMD(init_cmd[i]); } OLED_Clear(); }写命令和写数据函数的区别在于I2C发送的数据第一个字节控制字节为0x00表示后续是命令0x40表示后续是显存数据。void OLED_WR_CMD(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, 2, 100); } void OLED_WR_DATA(uint8_t dat) { uint8_t buf[2] {0x40, dat}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, 2, 100); }OLED_ADDR宏定义需要注意SSD1306的7位从机地址通常是0x3C对应8位写地址0x78。但有一部分模块是0x3D也就是8位地址0x7A。如果你照着网上代码写发现屏幕一直白屏先确认地址是否一致。HAL库的HAL_I2C_Master_Transmit接口传参要求传7位地址这一点很多人第一次容易搞混。显示字符需要准备ASCII字模表网上可以找到标准8x6大小的ASCII字模数组。显示一个字符时先计算字符在字模表中的偏移然后按页寻址方式写入两页数据。显示字符串是它的循环版本。想显示汉字的话需要额外准备16x16点阵字模用PCtoLCD2002取模软件生成取模方式要选“行列式”然后按16x16像素分两页写入。4.3 ADC采集与水位百分比计算传感器输出的模拟电压经过ADC转换后得到一个0到4095的整数。但工程上不能把这个原始值直接当成水位百分比用因为不同容器、不同壁厚、不同供电电压下同一个水位的ADC原始值相差很大。正确做法是先标定两个基准点。干燥无水的状态下读取一次ADC值记为empty_val把容器加满水到传感器感应高度再读一次ADC值记为full_val。之后的水位百分比用线性映射uint8_t GetWaterPercent(uint16_t adc_value, uint16_t empty_val, uint16_t full_val) { int32_t percent 0; if (full_val empty_val) { percent (int32_t)(adc_value - empty_val) * 100 / (full_val - empty_val); } if (percent 0) percent 0; if (percent 100) percent 100; return (uint8_t)percent; }注意映射方向和传感器输出极性有关。有些模块水位越高输出电压越高有些是反的代码里的判断可以按实际测试方向调整。为了减少ADC值跳动我用平均值滤波每次取5次采样求平均避免单次采样中的随机噪声造成屏幕数字乱跳。uint16_t GetADC_Average(uint8_t times) { uint32_t sum 0; for (uint8_t i 0; i times; i) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 100); sum HAL_ADC_GetValue(hadc1); HAL_Delay(2); } return (uint16_t)(sum / times); }标定的过程建议在代码里预留两个变量方便临时修改。实际做项目时这两个值可以存储到STM32内部Flash里做成“一键校准”模式不过实验阶段先定义为宏或者全局变量就够了。4.4 主循环程序的整体流程主循环的思路很清晰周期性读取ADC值计算百分比刷新OLED显示。我用HAL_Delay做简单的200ms延时控制刷新率这个刷新率对水位检测场景完全够用也不会因为屏幕刷新太快造成视觉闪烁。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_ADC1_Init(); OLED_Init(); OLED_Clear(); uint16_t empty_val 1750; // 空杯标定值实际烧录前按实测调整 uint16_t full_val 2100; // 满水标定值 while (1) { uint16_t adc GetADC_Average(5); uint8_t percent GetWaterPercent(adc, empty_val, full_val); OLED_Clear(); OLED_ShowString(0, 0, Water Level:); OLED_ShowNum(8, 2, percent, 3); OLED_ShowString(68, 2, %); OLED_ShowHLine(0, 56, 127); // 进度条底线 OLED_ShowBar(0, 56, percent); // 按百分比填充进度条 HAL_Delay(200); } }LCD上显示的进度条是我自己写的一个简单函数用循环把像素点亮。OLED的页寻址模式下画线、画矩形都很直观。这个主循环里包含了MCU默认的HAL_Delay阻塞延时在实验里没问题如果以后要接按键扫描或者多任务再改成定时器中断驱动。5. 实测过程与现象记录5.1 烧录与首次上电检查接线无误后用ST-Link连接开发板的SWD下载口打开工程编译没有报错就能直接下载。第一次上电时OLED会先全屏清除一次然后显示“Water Level: 0%”。如果你看到的是白屏或者花屏大概率是I2C地址不对或者接线问题直接跳到下一章排查。需要注意的是首次烧录后如果OLED全黑不要急着怀疑代码。SSD1306上电后默认是关闭显示状态的只有执行了0xAF命令才会点亮。如果代码里初始化序列没执行成功屏幕就是黑的和“坏屏”现象一模一样。先用万用表量一下OLED的VCC和GND之间电压是否正常再查I2C波形或者地址。5.2 空杯、半杯、满杯的实测数据我用的是一只500mL塑料瓶传感器贴在瓶子侧面中部用USB线连接开发板供电。实测数据如下状态ADC原始值对应电压显示百分比空杯干燥17501.42V0%水位到探头中部20501.66V58%满杯浸泡感应区21001.71V100%注意这组数据只是我手头这套环境的实测值换一个瓶子、换一块传感器、换一块开发板数值都会变。这也是我一直强调标定的原因。hs-s37a的输出电压变化跨度在0.3V左右相比常见的温度传感器要小不少如果ADC配置不好很容易被采样噪声淹没。5.3 显示效果与报警阈值设计把百分比映射做好之后OLED第一行显示“Water Level: 58%”第二行是我画的水位进度条。实际观察下来往瓶子里加水时百分比单调上升放水时单调下降体感响应速度很快200ms刷新一次看不到延迟。传感器的响应本身是即时的不像有些传感器需要几秒稳定。这个实验还可以顺手加上报警逻辑百分比低于20%时PB0引脚输出高电平点亮一颗LED或者驱动蜂鸣器。代码逻辑就是加一个if判断if (percent 20) { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); }阈值不要写死在显示函数里单独定义成宏后面想改成15%或者25%改一处就行了。6. 常见问题与排查技巧实录6.1 下载和烧录环节的三大拦路虎烧录阶段遇到最多的问题就是下载时提示“Error: No STM32 Target Found! If your product embeds debug authentication, please ...”。这个报错先不要慌按顺序检查几件事ST-Link和板子的SWDIO、SWCLK、GND三根线是否接对个别克隆版ST-Link还需要接3.3V给板子供电然后检查开发板是否正常上电电源指示灯是否亮再查BOOT0跳线是否拉低如果BOOT0被拉高芯片会进入系统存储器模式不执行用户程序下载会受影响。第二种常见情况是电脑提示“ST-LINK USB communication error”这类问题多半是USB线质量问题或者驱动混乱。我试过多根线都不行最后换了一根短粗的USB线就好了。驱动方面克隆ST-Link一般用STSW-LINK009驱动装完后设备管理器里能看到ST-Link设备没有黄色感叹号。第三种是同时装了Keil和STM32CubeIDE版本不同的驱动产生了冲突。建议只保留一个工具的ST-Link驱动或者在Keil的Settings里手动选择正确的下载方式。6.2 OLED显示相关的几个高频坑OLED白屏是最多人问的问题。除了I2C地址错误之外还有一个隐藏问题OLED模块的复位引脚。标准SSD1306模块通常有一个RST脚有些四针模块把RST和CS都内部处理好了但有些模块需要你把RST引脚接MCU的一个GPIO拉低再拉高复位一下。如果你的模块有RST引脚但悬空初始化时就可能失败。另外“OLED屏连上电源就亮吗”这个问题也常有人问。答案是SSD1306控制的OLED屏上电后默认不显示内容必须由主控通过I2C发送初始化命令和显存数据后才有画面。如果你看到一个OLED模块单独接电源后没有任何反应不代表它坏了它只是等待主控命令。显示汉字的问题主要集中在字模取法。用PCtoLCD2002取16x16汉字字模时取模方式要选择“阴码、行列式、顺向”每行左到右否则显示出来的汉字是反的或者乱的点阵。OLED是8页结构写汉字时要先计算汉字在屏幕上的行列位置再定位页地址和列地址两个字节一组写入。6.3 ADC数值不准、乱跳的排查方向ADC数值一直显示4095或者0通常是配置问题。先检查CubeMX里PA0是否被设置成了ADC模式如果引脚被设置为GPIO输出内部结构就不一样。接线方面HS-S37A的输出线如果插错位置或者VCC接到了3.3V但模块实际需要5V输出都会异常。用万用表量一下传感器OUT引脚对地电压如果水面变化时电压纹丝不动说明传感器本身没工作。ADC数值乱跳有时是采样间隔太短引起电容充放电不充分。我实验中把采样周期设置为55.5 Cycles后稳定性明显提升。另一个方向是数据处理5次平均还不够就提高到10次或者用中值滤波去掉最大值最小值再求平均。传感器贴装有气泡时也会有跳变重新贴紧就好。还有一个容易被忽略的问题如果OLED和传感器共用一条地线OLED刷新时会在数字地上产生噪声ADC采样时如果恰好赶上屏幕刷新读数可能跳一个字。实验里把ADC采样放在OLED刷新前或者降低刷新率都能明显改善。6.4 容易被新手忽略的细节清单最后整理一份我在实验中踩过坑之后归纳的细节清单。OLED模块如果I2C总线上没有上拉电阻通信会不稳定但绝大多数成品模块都板载了上拉电阻购买时留意一下如果自己搭模块I2C需要外加4.7k上拉电阻到3.3V。传感器标定不能只记录一次。我实际操作时发现同一个水位下连续读取会有几个字的偏差标定值可以取10次平均这样映射出来的百分比不会在边界反复跳。另外HAL_Delay不要放在中断服务函数里用容易卡死。如果你想用按键中断或者其他外设中断主循环里的延时逻辑要改成状态机或者用定时器计数这是新手从实验代码走向项目代码必经的一步。写在最后的几点实操体会这套实验做完之后我最大的一点感受是传感器的安装位置和标定方法决定了整个项目的可靠性代码反而是最不容易出错的部分。HS-S37A这类非接触式传感器的输出变化幅度本来就不大如果安装贴合度不够、容器壁太厚、电源纹波偏大任何一个环节出问题都会让ADC读数变得非常难看。先硬件后软件先确认模拟量变化再写映射逻辑这是调试这类项目最稳的路径。还有一个我一直在用的小习惯正式写显示逻辑之前先把ADC原始值和电压值通过串口打印出来观察一两天确认数据变化规律后再去标定、再去画界面。这样一旦显示屏最终效果不符合预期你能快速定位是显示问题还是采集问题。想让这套系统继续往工程化方向走还可以在现有工程上扩展把标定值存入Flash实现一键校准加入定时器中断取代阻塞延时或者用LVGL做一个更好看的图形界面。如果你准备做毕业设计在这个基础上加一颗ESP8266进行数据上云就是一套完整的智能液位监测系统框架。顺着这个思路一步一步来比从头翻手册高效得多。
返回列表