做单片机项目,传感器采集完了怎么显示,永远是一个绕不开的环节。最近在调试一套非接触式水位检测方案,拿STM32F103C8T6做核心,配上HS-S37A水位传感器和一块0.96寸的I2C接口OLED屏,从接线、CubeMX配置、底层驱动到显示界面,把整套流程完整跑了一遍。做完之后我觉得这套实验很有代表性,它几乎涵盖了单片机开发最常见的几个知识点:ADC采集、I2C通信、OLED驱动、软件滤波、标定换算。不管你是刚学STM32的朋友,还是想给现有项目加水位检测功能,这篇都可以直接拿去参考。
先说我为什么选这套组合。HS-S37A是非接触式的,传感器不用接触水,隔着塑料或玻璃容器壁就能检测,干净卫生也几乎没有维护成本;OLED用I2C接口,只占两根IO线,驱动成熟。整个系统的核心价值一句话就能说清:把"容器里有多少水"这件事,变成屏幕上能直观看到的百分比和进度条。下面按我的实验顺序,把硬件、软件和踩坑过程都展开讲。
1. HS-S37A传感器的原理与输出特性,先搞清楚再动手
1.1 非接触式电容检测是怎么回事
HS-S37A这类非接触式水位传感器,本质上是一个电容感应装置。水和空气的介电常数差异非常大,空气的介电常数约为1,水大约是80。当容器内的水位上升、水靠近传感器感应面时,传感器电极与周围环境构成的等效电容会发生明显变化。模块内部的信号调理电路把这种电容变化转换成电压变化,最终从输出引脚送出来。
这个原理和手机触摸屏的电容感应有些类似。手指(含水导体)靠近时引起电容变化,屏幕判断出触摸位置。HS-S37A则是用水位高度影响电容,从而给出一个随水位连续变化的电信号。因为检测的是电场变化,传感器不需要接触液体本身,隔着非金属容器壁就能工作。
这个"传感器表面与容器壁之间不能有金属"是硬性条件,后面我会详细说。另外传感器感应面需要紧贴容器外壁,中间尽量不要留空气间隙。我用透明的亚克力水箱测试,效果很稳定;如果容器壁太厚,灵敏度会下降,实测超过10mm后信号变化就不太明显了。
1.2 输出引脚定义与电气特性
HS-S37A模块通常引出四个引脚,有些板子还会标注丝印,用之前最好用万用表确认一下,避免接错:
| 引脚 | 功能 | 说明 |
|---|---|---|
| VCC | 电源正极 | 常见支持3.3V~5V,具体看批次和模块丝印 |
| GND | 电源地 | 与STM32共地 |
| DO | 数字输出 | 开关量,水位超过模块上电位器设定的阈值时翻转 |
| AO | 模拟输出 | 连续电压,随水位变化,接STM32的ADC引脚 |
模块上一般会有一个小电位器,专门用来调节DO比较阈值。用螺丝刀旋转可以改变触发点,适合做报警器。AO输出的电压范围大致在0V到供电电压之间,但具体是水位越高电压越高还是越低,不同批次会有差异,需要实测确认。
这里有一个新手最容易踩的坑:把AO接到了STM32的普通GPIO上,然后等一个变化电平,结果读到的全是0或1。DO和AO必须分清,做连续水位显示用的是AO,做阈值报警用DO。还有一种做法是DO和AO一起用,阈值触发中断、模拟量做连续监测,后面扩展部分会提到。
1.3 跟接触式、浮球式水位传感器的选择差异
做水位检测的方案很多,接触式电极、浮球开关、光电式、压力式都见过。对比一下各自的优缺点,你就知道非接触式的价值在哪里:
| 类型 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|
| 浮球开关 | 结构简单便宜 | 机械运动易卡死、有残留水垢 | 水箱高低水位报警 |
| 接触式电极 | 成本低 | 电极易腐蚀、水垢影响精度 | 导电液体检测 |
| 光电式 | 响应快、无机械件 | 需要透光容器或开孔安装 | 管路液位检测 |
| 非接触式电容 | 不接触液体、无腐蚀、免维护 | 精度受容器壁材质和厚度影响 | 饮水机、加湿器、医疗设备 |
我这次选HS-S37A的直接原因是不用开孔。之前用浮球开关装在水箱侧壁,开孔处时间长了总担心漏水。非接触式是贴装方案,安装后想挪位置也方便。代价是它的连续测量精度一般,用来做阈值判断或者粗略的百分比显示完全够用,但如果你想精确到毫米级的水位高度,它并不适合。
2. 实验接线与工程初始化,这一步错了后面全白搭
2.1 完整硬件清单
- STM32F103C8T6最小系统板一块(最常见的蓝色板子)
- HS-S37A非接触式水位传感器模块一个
- 0.96寸I2C接口OLED屏一块,SSD1306主控,128x64分辨率
- ST-Link V2调试器一个,用于下载和调试
- USB转串口模块一个(CH340或CP2102都行),UART调试打印用
- 杜邦线若干、面包板或者洞洞板
- 一个非金属容器(亚克力水箱或塑料瓶),测试用水
- 万用表、螺丝刀
关于调试器多说一句,ST-Link V2和J-Link都可以,但驱动要装对。ST-Link用官方的ST-Link驱动,J-Link用SEGGER的JLink驱动。如果你电脑上两个驱动都装过,插上设备后建议去设备管理器确认枚举出来的是哪一类,避免调试器识别混乱。
2.2 接线对照表
| 部件 | 引脚 | 接STM32 |
|---|---|---|
| HS-S37A | VCC | 3.3V |
| HS-S37A | GND | GND |
| HS-S37A | AO | PA1 |
| OLED | VCC | 3.3V |
| OLED | GND | GND |
| OLED | SCL | PB6(I2C1_SCL) |
| OLED | SDA | PB7(I2C1_SDA) |
| ST-Link | SWDIO | PA13 |
| ST-Link | SWCLK | PA14 |
| ST-Link | GND | GND |
| ST-Link | 3.3V | 3.3V(可选) |
| USB转串口 | TX | PA10(USART1_RX) |
| USB转串口 | RX | PA9(USART1_TX) |
| USB转串口 | GND | GND |
PA1是ADC1_IN1,这是F103上很常用的ADC通道。OLED接在I2C1上,PB6/PB7是默认复用功能。
这里要特别提醒几个点:第一,传感器VCC我建议接3.3V而不是5V。如果模块5V供电,AO输出范围可能会到5V,直接进STM32的ADC会超量程,有烧引脚风险。第二,USB转串口的GND必须和STM32共地,不然串口打印全是乱码。第三,ST-Link的SWDIO/SWCLK接线反了会直接导致连接失败,后面下载时报错。
2.3 STM32CubeMX配置要点:ADC通道与I2C外设
用STM32CubeMX生成工程,版本不同界面略有差异但逻辑一致。以下是几个关键配置项,每个我都会解释为什么这么设置。
SYS -> Debug:Serial Wire。这个必须选,不选的话,生成的工程会把SWD引脚当普通IO处理,第一次程序下载进去后,第二次下载就可能报"no stm32 target found"。我在实验里专门故意踩过一次这个坑,后面排错部分详细讲。
RCC -> HSE:Crystal/Ceramic Resonator。使用板载8MHz晶振作为HSE时钟源,之后通过PLL倍频到72MHz主频。
ADC1 配置:
- Mode:IN1,对应PA1
- ADC参数里,Resolution选12位(默认)
- Sampling Time建议选55.5 Cycles以上,我实际用239.5 Cycles,读值明显更稳。原因后面讲
- 开启连续转换模式(Continuous Conversion Mode)
I2C1 配置:
- I2C Speed Mode:Fast Mode,400KHz
- 地址模式默认7-bit
USART1 配置:
- Mode:Asynchronous
- Baud Rate:115200,8数据位,1停止位,无校验
时钟树里确认APB1总线时钟是36MHz,I2C1挂在APB1上,CubeMX会自动算好分频系数,不用手动调。生成代码时记得选"Generate peripheral initialization as a pair of .c/.h files per peripheral",这样每个外设独立成文件,后面加代码好管理。
3. OLED驱动的核心逻辑:懂了这个才能自由改显示
3.1 SSD1306初始化序列该怎么配
SSD1306是一款非常经典的OLED控制芯片,几乎所有0.96寸OLED模块都用它。它内部有一块128x64的显存(GDDRAM),每个bit对应屏幕上一个像素点。I2C模式下,外部主控通过I2C总线往这块显存写数据,屏幕就显示出来了。
上电之后,SSD1306默认是不显示的,需要发送一串初始化命令。命令序列各家代码略有差异,但有几个命令是必须的,少了显示屏就不工作。我用的初始化序列如下:
static uint8_t ssd1306_init_cmds[] = { 0xAE, // 关闭显示 0xD5, 0x80, // 显示时钟分频/振荡器频率 0xA8, 0x3F, // 多路复用率:64行 0xD3, 0x00, // 显示偏移:0 0x40, // 起始行:0 0x8D, 0x14, // 电荷泵使能 0x20, 0x00, // 水平寻址模式 0xA1, // 段重映射 0xC8, // COM扫描方向 0xDA, 0x12, // COM引脚硬件配置 0x81, 0xCF, // 对比度 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH电压 0xA4, // 按显存内容显示 0xA6, // 正常显示模式 0xAF // 开启显示 };其中0x8D 0x14是电荷泵使能,这一步尤其重要。OLED工作需要内部升压电路,如果没开电荷泵,屏幕会一直黑着,无论你往显存里写多少数据都没用。我曾经调一块新屏幕,检查了所有接线都没问题,最后发现是初始化序列里漏了这条命令。
3.2 HAL库发送命令和数据的两种方式
I2C传输SSD1306数据时,第一个字节是控制字节,用来区分后续内容是命令还是数据:
- 0x00:后面的字节是命令
- 0x40:后面的字节是显存数据
在HAL库下,写命令有两种常见方式。我用的是HAL_I2C_Master_Transmit:
void oled_write_cmd(uint8_t cmd) { uint8_t buf[2]; buf[0] = 0x00; buf[1] = cmd; HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR << 1, buf, 2, 100); }写数据类似,只是控制字节改成0x40:
void oled_write_data(uint8_t *data, uint16_t len) { uint8_t buf[1025]; buf[0] = 0x40; memcpy(buf + 1, data, len); HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR << 1, buf, len + 1, 100); }注意OLED_ADDR << 1这步。SSD1306的7位I2C地址通常是0x3C,而HAL库的Master_Transmit接口需要的是8位地址(即7位地址左移一位),所以实际传入0x78。很多新手在这里卡住,以为OLED地址是0x78直接传进去,结果通信一直失败。如果模块上SA0引脚被拉高,地址会变成0x3D,左移后是0x7A。
还有一种方式是用HAL_I2C_Mem_Write,它内部会自动处理寄存器地址,但本质上也是先发控制字节再发数据。两种方式效果一样,我习惯用Master_Transmit,因为逻辑更直观。
3.3 显存缓冲:画点、画线、进度条的实现思路
SSD1306的显存按页划分,纵向8个像素为一页,所以128x64的屏幕共8页,每页128字节,总共1024字节。水平寻址模式下,往显存写数据时地址自动递增,从左上角开始横向逐字节填充。
我采用显存缓冲的方式:在内存中维护一个1024字节的buffer,所有绘图操作先改buffer,最后一次性刷到屏幕。这样可以避免频繁的I2C写操作,减少闪烁。
uint8_t oled_buf[1024]; void oled_draw_pixel(uint8_t x, uint8_t y, uint8_t on) { if (x > 127 || y > 63) return; uint16_t idx = x + (y / 8) * 128; if (on) oled_buf[idx] |= (1 << (y % 8)); else oled_buf[idx] &= ~(1 << (y % 8)); }全量刷新函数:
void oled_refresh(void) { oled_write_cmd(0x21); oled_write_cmd(0); oled_write_cmd(127); oled_write_cmd(0x22); oled_write_cmd(0); oled_write_cmd(7); oled_write_data(oled_buf, 1024); }字符显示可以做一个5x8的ASCII字库,存放在const数组里,取模方式网上很多工具可以生成。汉字显示则需要16x16点阵字模,显示的时候按2字节一行、逐页写入。进度条绘制也很简单:先画一个矩形边框,然后按百分比计算填充宽度,逐列用垂直直线填充。
4. 核心代码实现:从ADC采集到水位百分比的计算全过程
4.1 ADC采样与滑动滤波的实现
ADC每次转换的原始值并不稳定,尤其当传感器信号本身有波动时。直接把这个值拿来换算百分比,屏幕上会跳得没法看。我用了20次滑动平均,效果很理想。
#define FILTER_LEN 20 uint16_t filter_buf[FILTER_LEN]; uint8_t filter_idx = 0; uint32_t filter_sum = 0; void filter_init(void) { memset(filter_buf, 0, sizeof(filter_buf)); } void filter_add(uint16_t val) { filter_sum -= filter_buf[filter_idx]; filter_buf[filter_idx] = val; filter_sum += val; filter_idx = (filter_idx + 1) % FILTER_LEN; } uint16_t filter_get_avg(void) { return (uint16_t)(filter_sum / FILTER_LEN); }为什么采样周期要设置得很长?STM32F103的ADC是逐次逼近型,内部有采样保持电容。如果信号源输出阻抗较大,采样时间太短会导致保持电容充不满,转换结果整体偏大且不稳定。把采样时间从默认的1.5 Cycles调到239.5 Cycles后,原始ADC值的跳动范围能缩小一半以上。代价是采样速度下降,但水位检测本来就不需要高采样率,稳定性优先。
在主循环里定时取一次ADC值,加入滤波器:
if (HAL_GetTick() - last_sample > 50) { last_sample = HAL_GetTick(); HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { filter_add(HAL_ADC_GetValue(&hadc1)); } HAL_ADC_Stop(&hadc1); }这里用HAL_ADC_Start+PollForConversion而不是一直开着连续转换,是为了让ADC采样和主循环刷新节奏一致。实际上CubeMX里也可以直接开连续转换模式,主循环里直接读取转换完成标志,两种方式都行。
4.2 两点标定法:把ADC值映射成水位百分比
拿到稳定的ADC值之后,需要把它转换成人类能看懂的水位百分比。这一步就是标定。
我在实验里用的是两点线性标定:
- 把容器里的水全部倒空,等传感器输出稳定,记录ADC值,记为
ADC_EMPTY - 把水加到想要标定的最高水位(比如容器满水),记录ADC值,记为
ADC_FULL - 之后任意时刻,ADC值在这两个点之间做线性映射
#define ADC_EMPTY 1250 #define ADC_FULL 2670 uint8_t map_to_percent(uint16_t adc_value) { int32_t percent; percent = ((int32_t)adc_value - ADC_EMPTY) * 100 / (ADC_FULL - ADC_EMPTY); if (percent < 0) percent = 0; if (percent > 100) percent = 100; return (uint8_t)percent; }要注意一个问题:不同批次的HS-S37A,AO输出方向可能不一样。有的模块水位越高电压越高(ADC值变大),有的则是水位越高电压越低。所以标定前先在OLED或串口上看一下ADC原始值的变化方向,确认是正相关还是负相关,再决定映射公式。如果发现方向反了,把分子改成ADC_EMPTY - adc_value就行。
另外,两个标定点不能相隔太近。假设空水时ADC值为1500,满水时1600,中间只有100个LSB的跨度,那1%水位只对应1个LSB,稍微有点噪声就会在百分比上剧烈跳动。最好让满水时的ADC值比空水时至少大300~500,灵敏度才够用。实测中如果发现变化量太小,检查传感器是否贴紧容器壁、容器壁是否太厚,或者水量是否真的覆盖到了感应区域。
4.3 主循环逻辑与OLED实时刷新
主循环做的事情很清晰:定时采集ADC值,做滤波,换算电压和百分比,刷新OLED显示。电压值可以直接算:
float voltage = (float)adc_value * 3.3f / 4095.0f;电压显示用于排查供电和接线问题,很有用。如果电源电压正常,传感器输出电压应该在预期范围内波动,一旦出现0V或满量程3.3V不变,基本可以判断是传感器没贴好或者信号线接触有问题。
OLED显示布局我设计成四行:
- 第0行:标题字符串
- 第2行:ADC原始值和电压值
- 第4行:水位百分比
- 第5行:进度条图形
while (1) { uint16_t adc = filter_get_avg(); float volt = (float)adc * 3.3f / 4095.0f; uint8_t percent = map_to_percent(adc); char buf[20]; oled_clear(); oled_show_string(0, 0, "Water Level:"); sprintf(buf, "ADC=%d %.2fV", adc, volt); oled_show_string(0, 2, buf); sprintf(buf, "%d%%", percent); oled_show_string(0, 4, buf); oled_draw_progress(0, 5, 128, percent); oled_refresh(); printf("ADC=%d, %.2fV, %d%%\r\n", adc, volt, percent); HAL_Delay(100); }刷新间隔我放在100ms,人眼看进度条很平滑,同时也给I2C和ADC采样留足时间。如果你打算把间隔缩短到10ms,要注意I2C每次全量刷新1024字节在400KHz下大约需要20ms,刷新太快反而会积压通信,显示效果变差。
oled_draw_progress函数实现进度条:
void oled_draw_progress(uint8_t x, uint8_t y, uint8_t width, uint8_t percent) { uint8_t fill = (uint16_t)width * percent / 100; // 画边框 for (uint8_t i = 0; i < width; i++) { oled_draw_pixel(x + i, y, 1); oled_draw_pixel(x + i, y + 7, 1); } for (uint8_t j = 0; j < 8; j++) { oled_draw_pixel(x, y + j, 1); oled_draw_pixel(x + width - 1, y + j, 1); } // 填充 for (uint8_t i = 1; i < fill; i++) { for (uint8_t j = 1; j < 7; j++) { oled_draw_pixel(x + i, y + j, 1); } } }进度条直观,一眼就能看出当前容器里大概有多少水。
5. 实测中的意外与排查,这些坑我替你踩过了
5.1 "no stm32 target found"下载失败的本质原因
我调试过程中遇到的第一个大坑就是下载程序时弹出error: no stm32 target found!。这个报错,本质上是调试器通过SWD接口和芯片建立通信时,目标芯片没有任何回应。
排查顺序我建议这样来:
- 确认驱动和设备枚举正常。插上ST-Link,打开设备管理器,应该能看到对应的驱动设备。如果显示黄色感叹号,重装驱动。J-Link和ST-Link驱动不要混装,容易冲突。
- 检查接线。SWDIO必须接PA13,SWCLK必须接PA14,GND必须共地。用万用表蜂鸣档量一下杜邦线两端的导通性,排除线断了的情况。
- 确认目标板供电。用万用表量芯片VDD引脚和GND之间是否有3.3V。ST-Link的3.3V输出可以给板子供电,但要注意有些大功率板子只靠调试器的供电不够稳定。
- 检查BOOT0。如果BOOT0被拉高,芯片上电后进入系统存储器模式,不运行用户程序。SWD在系统存储器模式下通常还能连上,但如果跳线接触不良、电压飘忽,也会导致连接失败。正常运行时BOOT0应该接低电平。
- 复位线。部分ST-Link固件和调试配置下,连接时需要访问NRST引脚,尤其是芯片进入低功耗模式或者SWD引脚被复用成普通IO时。把ST-Link的NRST接到板子的NRST引脚,能解决很多顽固问题。
最隐蔽的情况是SWD引脚被用户代码复用成普通GPIO。比如你第一次下载程序后,程序运行起来把PA13/PA14配置成了普通输出模式,SWD调试接口就被占用了,这会导致第二次下载时调试器找不到目标。解决方法是:按住板子上的复位键不放,点击Keil的下载按钮,在芯片复位期间(用户程序还没跑起来)抢到总线控制权。或者把BOOT0拉高再上电,让芯片进入系统存储器模式绕过用户代码,然后执行整片擦除。
我当时就遇到过一次,最后是拉高BOOT0、连接后在ST-Link Utility里执行擦除,芯片恢复正常。所以CubeMX里SYS->Debug选Serial Wire这一步,真的不要漏。
5.2 传感器输出电压反跳、漂移的处理
第二个常见问题是ADC读值不稳定,甚至在某个范围内反复跳。我遇到过三种情况:
电源噪声。如果传感器和STM32共用一个电源,而电源纹波比较大,ADC结果会明显跳动。实验中我在传感器VCC引脚和GND之间加了一个0.1uF陶瓷电容,再用一个10uF电解电容并联,读数立刻稳定了不少。如果条件允许,ADC的参考电压VREF+也可以加一级LC滤波,效果更好。
接线过长和线材质量。杜邦线超过20cm且没有屏蔽时,容易引入电磁干扰。传感器AO信号线尽量短,不要和电源线、电机驱动线绑扎在一起。实验板旁边如果有水泵、继电器之类的感性负载,切换瞬间会产生尖峰干扰,水位计的读数会在那一刻跳变。
软件滤波策略不当。滑动平均窗口太小滤不掉脉冲噪声,窗口太大会让响应变迟钝。我试过10次、20次、50次窗口,20次在这个场景下比较平衡。如果你遇到偶发的尖峰干扰,还可以在滑动平均之前加一个简单的中值滤波,去掉最大和最小值再平均,效果更干净。
5.3 OLED不出字或花屏的几种可能
OLED显示异常,优先级最高的怀疑对象是I2C地址和初始化序列。我用I2C扫描程序把总线上所有设备地址打印出来,非常管用:
for (uint8_t addr = 1; addr < 127; addr++) { if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 1, 10) == HAL_OK) { printf("Found I2C device at 0x%02X\r\n", addr); } }如果地址扫描不到设备,大概率是接线或者上拉电阻的问题。OLED模块的板载I2C上拉电阻通常已经有,不需要额外加。如果自己飞线连接裸屏,SDA和SCL必须接2.2K~4.7K上拉电阻到VCC,否则I2C通信不稳定。
屏幕能亮但显示乱码或花屏,通常是显存地址错位。检查初始化命令里的起始行、段重映射和COM扫描方向配置是否正确,尤其注意0x21列地址设置命令的起始和结束参数是否正确。显存刷新时如果页地址范围写错,显示内容会整体偏移或者上下颠倒。
还有一个容易忽略的点:HAL库的I2C超时时间。全量刷新1024字节显存时,如果超时参数设得太短(比如默认的10ms),I2C传输会超时中断,刷新显示的内容残缺。我统一把超时设成100ms,全量刷新能稳定完成。如果你用的是中断或者DMA方式传输,还要处理好传输完成的同步标志,否则刷新过程中数据会被截断。
6. 实验扩展:这套架构还能直接改造出哪些小项目
6.1 加水泵自动控水系统
最直接的扩展是加一个继电器模块控制小型水泵或电磁阀。设定一个低水位阈值和一个高水位阈值,形成滞回控制:
- 水位低于30%时,打开水泵加水
- 水位达到85%时,关闭水泵
滞回区间防止水泵在下限附近频繁启停。继电器线圈需要外部供电,触点侧驱动水泵,注意电磁干扰对传感器的屏蔽。这个方案用在鱼缸自动补水、加湿器水箱管理上都行。
DO输出引脚正好可以配合这个场景:DO调好阈值后,直接接外部中断,水位到达报警点瞬间触发回调,而不是靠主循环轮询,响应更快也更省CPU。
6.2 多点水位检测
如果容器比较高,单一传感器只能检测一个局部区域的水位。可以在不同高度贴多片HS-S37A,各自接到不同的ADC通道,程序里做分段判断。比如三片传感器分别贴在低、中、高三个位置,组合状态能粗略判断出"空、低、中、满"四个档位。这种方案比连续百分比显示更可靠,因为每片传感器只做"有水/无水"的开关判断,不受连续测量线性度不高的限制。
6.3 界面升级与无线化
显示部分可以继续丰富:用16x16点阵显示汉字标题;在OLED上绘制一条水位历史曲线,每隔一段时间采样一个点,滚动显示最近半小时的水位变化趋势。曲线绘制其实就是把水位百分比映射到纵坐标,左移绘制,逻辑不复杂。
如果想把数据上报到手机,可以用ESP8266或ESP32通过串口和STM32通讯,把ADC值、电压值、水位百分比打包成简单的自定义协议发出去。这样OLED本地显示保留,手机端远程查看,就从一个实验变成一个完整的小项目了。
整套实验做完,我对STM32的HAL库工程结构、ADC采样特性和I2C协议的理解比之前深了不少。尤其是标定这一步,它让我意识到传感器不是插上去就能用的,手册给的电气参数只是基础,实际安装环境才是决定精度的关键。如果你也打算复刻这个实验,建议先不要把精力放在优化显示界面上,而是用串口把ADC原始值打出来,观察不同水位下数值的变化规律,等你看懂了传感器的"脾气",后面的百分比映射、报警阈值、自动控制都是水到渠成的事。