☰
基于51单片机的水位检测自动抽水系统设计与Proteus仿真实现
2026/10/15 16:41:20 网站建设 项目流程

简介:这套基于51单片机的水位检测自动抽水系统仿真资料包,面向电子、嵌入式方向的初学者及课程设计、毕业设计学生,旨在帮助读者快速掌握利用单片机读取水位传感器、控制继电器实现自动抽水的完整开发流程,适用于农业灌溉、家庭供水等小规模自动控制场景。压缩包共35个文件,大小约9.6MB,包含C语言源码、Proteus仿真电路、Keil工程、Hex烧录文件、原理图与PCB文档、芯片PDF资料、答辩常见问题解答以及wmv视频文件等,方便对照学习、修改调试和二次开发。已有512人学习使用。通过此套资料,读者可以熟悉51单片机的基本结构与I/O口操作、中断处理、水位阈值比较、Proteus仿真验证、硬件组装与调试技巧,掌握自动控制系统从传感器信号采集到执行设备控制的完整设计思路,是实用性与理论性兼备的入门项目。

1. 基于51单片机的水位检测自动抽水系统,到底在做一个什么系统

不少人觉得水位检测自动抽水就是把一个浮球开关丢进水箱的事,真到基于51单片机的水位检测自动抽水系统在 Proteus 里跑仿真时,才发现连续水位采集、阈值判断、继电器驱动每一步都有细节。这个项目对应的是经典的单片机课程设计:AT89C51 通过 ADC0808 采集模拟水位信号,低水位自动开水泵,高水位自动停泵,同时用 LCD1602 显示实时水位和泵的状态,还带手动/自动两种模式。解决的是「水位不能直接给单片机读,开关量又扛不住临界抖动」这两个真实痛点。适合正在做课设,或者想一次打通传感器采集→逻辑判断→执行器控制这条完整链路的人。拿到手的资料包里就是源码工程、仿真工程和器件清单,先把这几样对照起来看,后面每一步都不容易走偏。

2. 系统组成与全自动抽水逻辑:水位信号怎么变成水泵动作

2.1 水位检测方案的三种选型,为什么最后用电压模拟

水位检测在实验室和工程现场有完全不同的做法,放在 Proteus 仿真里更要考虑有没有现成模型。先看三种主流方案:

方案输出信号优点缺点Proteus 里的替代做法
电极式水位探头电阻/电压连续变化成本低、无机械结构电极易极化,水质影响读数电位器分压模拟连续电压
浮球开关开关量(通/断)结构简单、可靠性高只能判断固定点,无法测连续水位手动开关或按键模拟
压力变送器4~20mA 电流或电压精度高、连续性好成本高,需要变送电路外部电压源直接给模拟量

有同学一上来就选浮球,觉得两个浮球一高一低就能控制。这个思路在实物里确实能用,但在 Proteus 里它表现成两个开关量,看起来不直观,而且体现不出「水位采集」这个课程设计考点。所以这里我倾向于用电极式传感器的替代方案:电极插入水中的深度不同,两电极间等效电阻不同,经过分压电路变成 0~5V 电压。这正好对上 ADC0808 的输入范围,逻辑上也更接近真实的水位连续检测。在仿真里,POT-HG 电位器中抽头的输出电压就是等效的传感器信号。

选型理由要说透:51 单片机(AT89C51)内部没有 ADC,所以必须在外部挂一颗 ADC 芯片。ADC0808 是 8 位、8 通道逐次逼近型 ADC,Proteus 里有现成模型,这是它成为课设首选的根本原因。8 位分辨率对应 0~255,映射到 0~5V,每一档约 19.6mV,对水箱水位这种变化缓慢的物理量完全够用。

2.2 水位量程映射:电压、ADC 数值和水位百分比怎么换算

这是整个系统最关键的标定关系,也是新手最容易算错的地方。设定传感器输出的 0V 对应水位 0%,5V 对应水位 100%,ADC0808 是 8 位,所以转换结果满足:

adc_value = (Vin / 5.0) * 255; water_level = (adc_value * 100) / 255;

换算结果直接用查表法记下来更方便:

水位(%)对应电压(V)ADC 原始值(约)
00.000
251.2564
502.50128
753.75191
1005.00255

在 Proteus 里拖动电位器,让中抽头输出电压停在 1.25V,水位显示就应该是 25%。如果你发现显示值和理论值差得远,优先怀疑电压测量点接错了位置,而不是程序问题。这个映射关系后面写代码时直接写成宏定义,不要在while循环里每次都用浮点运算,8 位单片机算浮点慢,而且仿真里容易出现肉眼可见的卡顿。

2.3 自动抽水规则:为什么必须用两套阈值而不是一个点

控制逻辑本身很简单:水位低到一定程度开泵,高到一定程度停泵。但如果你只设一个阈值,比如水位低于 50% 开泵,高于 50% 停泵,就会出现临界抖动。水泵一开,水位从 49% 上升到 51%,立刻触发停止;停止后水位又回落到 49%,又启动。整个系统在水位临界点附近反复启停,继电器触点很快报废。

正确的做法是滞回控制(Hysteresis),设置两个阈值点:

工作模式触发条件水泵动作
自动水位 ≤ 30%启动水泵
自动水位 ≥ 70%停止水泵
自动30% < 水位 < 70%保持上次状态
手动按键切换每次按下翻转一次

30% 是启动阈值,70% 是停止阈值,中间 40% 的区间就是滞回带。进入这个区间后系统维持当前状态,不会因为轻微水位波动来回切换。仿真里验证方法很简单:把电位器从 0V 慢慢往上调,水泵启动后不要动,等电压越过 3.5V 才停。如果水泵在 2.5V 附近就停了,说明你的阈值宏定义写反了或者判断条件用了if而不是else if。

手动模式是这套系统里容易被忽略但考试必问的部分。它本质上是一个防抖按键输入加状态翻转逻辑:按键按下一次,水泵状态翻转一次。实现时一定要做软件消抖,不然仿真里看不出来,移植到实物按钮上会一次按出多次翻转。

3. 硬件电路与 Proteus 仿真搭建:从空白工程到能跑的最小系统

3.1 器件清单与引脚分配表,搭之前先对好接线

先把元件清单列全,Proteus 里少一个都跑不起来:

器件型号/取值数量作用
单片机AT89C511主控
ADCADC08081模拟量转数字量
液晶LCD16021显示水位和泵状态
电位器POT-HG1模拟水位传感器输出
时钟DCLOCK1给 ADC 提供时钟
三极管2N2222(NPN)1驱动继电器线圈
继电器RELAY-SPST1隔离控制回路
直流电机MOTOR-DC1等效水泵负载
排阻RESPACK-81P0 口上拉
晶振电路12MHz 晶振 + 2×33pF 电容1 套单片机主时钟
复位电路10μF 电容 + 10kΩ 电阻1 套上电复位

引脚分配是搭仿真前必须先定死的一件事,不然画到一半发现引脚冲突,返工很痛苦。我这里给一套验证过能跑通的分配:

单片机引脚连接对象方向
P1.0~P1.7ADC0808 D0~D7输入(ADC 并行数据)
P2.0~P2.2ADC0808 A/B/C(通道地址)输出
P2.3ADC0808 ALE + START(合并接)输出
P2.4ADC0808 OE输出
P2.5ADC0808 EOC输入(查询)
P2.6三极管基极(经 1kΩ 电阻)输出(水泵控制)
P3.0 / P3.1 / P3.2LCD1602 RS / RW / E输出
P0.0~P0.7LCD1602 DB0~DB7,接排阻上拉双向

看清楚一个关键点:ADC0808 的数据输出接的是 P1 口,不是 P0 口。P0 口留着给 LCD 数据线,并且必须加上拉排阻,否则 LCD 上电后全是乱码。这是很多仿真翻车的第一个坑。

3.2 按信号连接表搭出最小系统:五步操作

Proteus 里连线不需要把所有线都画出来,用网络标号(Wire Label)会让图纸清晰得多。按顺序操作:

第一步,新建工程,在器件模式输入AT89C51,放置单片机。双击单片机,把 Program File 指向后面 Keil 生成的 HEX 文件,晶振频率填 12MHz。这个选项不填,仿真直接无法运行。

第二步,放置 ADC0808、POT-HG、DCLOCK。DCLOCK 双击设置频率为 500kHz,接到 ADC0808 的 CLOCK 脚。ADC0808 的时钟不是单片机晶振,是独立给的,频率范围典型值在 640kHz 附近,仿真里 500kHz 足够稳。

第三步,放置 LCD1602、RESPACK-8。排阻一端接 P0 口,公共端接 +5V。LCD 的 VSS 接地、VDD 接 +5V、V0 通过一个 10kΩ 电位器接地(对比度调节),标准接法。

第四步,放置 2N2222、RELAY-SPST、MOTOR-DC。三极管基极经 1kΩ 电阻接 P2.6,发射极接地,集电极接继电器线圈的一端,线圈另一端接 +5V。继电器触点一端接 +12V(或直接接 +5V,看电机额定电压),另一端接 MOTOR-DC 正极,电机负极接地。

第五步,用网络标号给同名引脚连线。把 ADC0808 的 IN0 接到电位器中抽头,EOC 标号接到 P2.5,ALE/START 合并后接到 P2.3。这样图纸清爽,也不会因为飞线太多导致仿真器报连接错误。

提示:Proteus 里按住电位器键选,点 A 键降低输出电压,点 D 键升高,仿真时可以实时看到水位变化。

3.3 元件的放置参数与等效模型的四个细节

第一个细节:ADC0808 的 START 和 ALE 在仿真里可以直接接在一起。实际芯片手册里 ALE 是锁存通道地址,START 是启动转换,时序上一个在前一个在后;但 Proteus 的模型对这两个信号的合并接法兼容性很好,只要能产生一个下降沿就触发。代码里用一个引脚同时控制两者,少一根线,逻辑也更简洁。

第二个细节:RELAY-SPST 线圈电压默认值和你的驱动电源必须匹配。常见的 Proteus 继电器模型默认按 12V 线圈设计,如果你直接给线圈接 5V,它吸合不了。双击继电器,查线圈参数,额定电压改成 5V,或者把线圈供电改成 12V。我习惯改成 5V,这样整板统一供电。

第三个细节:MOTOR-DC 是直流电机模型,启动瞬间电流很大。在仿真里没有电流保护这个概念,但在继电器触点容量上要注意:触点额定电流选大一点,比如 2A 以上,避免仿真里出现奇怪的压降。继电器不吸合时电机两端电压为零,吸合后才有电压输入,这才是正常的开关行为。

第四个细节:复位电路。AT89C51 的 RST 引脚接 10kΩ 电阻到 GND,再接 10μF 电容到 VCC,这是教科书标准复位。有些人的仿真里不接复位也能跑,那是因为 Proteus 默认初始状态是确定的;但接了更贴近实物,尤其是后面要移植到开发板上时,复位电路必须存在。

4. Keil 源码实现:ADC 采集、模式切换与水泵控制

4.1 工程建立与引脚宏定义:先把 IO 分配写进代码

Keil 里新建工程时芯片型号选 AT89C51,编译时勾选 Output 标签下的 Create HEX File,否则 Proteus 找不到烧录文件。工程建好后,先把引脚定义和阈值写成宏,这样后面调参数只改一处:

/* 水位检测自动抽水系统 - 主程序头部 */ #include <reg51.h> #include <intrins.h> #define LOW_LEVEL 30 // 低水位阈值,单位:% #define HIGH_LEVEL 70 // 高水位阈值,单位:% /* ADC0808 控制引脚 */ sbit ADC_ALE = P2^3; // 地址锁存 + 启动转换脉冲 sbit ADC_OE = P2^4; // 输出使能,高电平有效 sbit ADC_EOC = P2^5; // 转换结束标志,高电平表示结束 /* 水泵控制引脚 */ sbit PUMP = P2^6; // 1 启动水泵,0 停止水泵 sbit KEY_MODE = P3^2; // 自动/手动切换按键,低电平有效 sbit KEY_PUMP = P3^3; // 手动模式下泵启停按键 /* LCD 控制引脚 */ sbit LCD_RS = P3^0; sbit LCD_RW = P3^1; sbit LCD_EN = P3^2; #define LCD_DATA P0 // P0 口作为 8 位数据总线

这段代码的逻辑说明:LOW_LEVEL和HIGH_LEVEL是核心设计参数,分别是启动抽水和停止抽水的临界水位百分比,中间区间留给滞回控制。sbit声明把单片机的某个位引脚命名成可读的符号,之后所有代码里都不要直接写引脚编号,避免后期改接线时需要全局搜索。

参数调整说明:LOW_LEVEL设 30、HIGH_LEVEL设 70,对应 40% 的滞回区间;如果水箱实际容量很小,允许把区间缩小到 50/70,但最小不要少于 15%,否则临界抖动问题会重新出现。如果用的是增强型 51 或者 STC 系列带内置 ADC 的单片机,ADC 部分的引脚定义需要全部重写,阈值宏定义可以保留。

4.2 ADC0808 读取函数:时序怎么写成代码

ADC0808 典型的读取流程是:选通道地址 → 拉高 START 产生脉冲 → 等待 EOC 变低再变高 → 使能 OE → 读 P1 口。转换成代码:

/* 读取 ADC0808 指定通道的 8 位转换结果 */ unsigned char read_adc(unsigned char ch) { unsigned char val; ADC_ALE = 0; /* 先确保控制信号复位 */ P2 &= 0xF8; /* 清 P2.0~P2.2,保留其他引脚状态 */ P2 |= (ch & 0x07); /* 写入通道地址,只允许 0~7 */ ADC_ALE = 1; /* 产生一个高电平脉冲 */ _nop_(); _nop_(); ADC_ALE = 0; /* 下降沿锁存地址并启动转换 */ while (ADC_EOC == 1); /* 等待 EOC 拉低,表示转换开始 */ while (ADC_EOC == 0); /* 等待 EOC 恢复高电平,表示转换完成 */ ADC_OE = 1; /* 打开输出使能 */ val = P1; /* 从 P1 口读取并行数据 */ ADC_OE = 0; /* 关闭使能 */ return val; }

逻辑说明:P2 &= 0xF8这行很多人看不懂,它把 P2.0~P2.2 清零,同时保留 P2.3~P2.7 的当前值。如果直接写P2 = ch,会把水泵控制位 P2.6 一起清掉,导致正在运行的泵突然停掉。ADC_ALE = 1; _nop_(); ADC_ALE = 0;产生一个约 1μs 的脉冲,这个下降沿就是转换开始信号。

时序参数说明:ADC0808 的转换需要 64 个时钟周期,时钟 500kHz 时一次转换约 128μs。所以两个while等待循环在仿真里最多等几百微秒就会通过,不会卡死。EOC 在芯片空闲时是高电平;下降沿触发转换后先变低,转换完成再变回高。两个 while 的先后顺序不能换,这是新手最容易抄错的地方:如果你只写了while (ADC_EOC == 0),第一次上电会因为 EOC 本来就是高电平而直接跳过等待,读到的是一个无效的旧数据。

4.3 主控制逻辑:滞回比较与手动/自动切换

主循环里要做三件事:周期采样、刷新显示、执行控制。代码按这个顺序写,避免因为显示耗时导致控制滞后:

void main(void) { unsigned int sum; unsigned char i; lcd_init(); lcd_print_str(0, 0, "Water Level"); while (1) { /* 连续采样 8 次取平均,抑制电位器信号毛刺 */ sum = 0; for (i = 0; i < 8; i++) { sum += read_adc(0); /* 一直读 IN0 通道 */ } adc_value = sum / 8; /* 0~255 映射为 0~100 水位百分比 */ water_level = (unsigned char)((unsigned int)adc_value * 100 / 255); /* 按键切换手动/自动模式,带消抖 */ if (KEY_MODE == 0) { delay_ms(20); if (KEY_MODE == 0) { auto_mode = !auto_mode; while (KEY_MODE == 0); /* 等待松手,防止连续翻转 */ } } /* 按模式执行控制 */ if (auto_mode) { auto_control(); } else { manual_control(); } /* 刷新 LCD 显示 */ lcd_show_status(); delay_ms(10); } }

逻辑说明:采样 8 次平均是软件滤波里最轻量的一种,能有效消除电位器调节时输出电压的微小抖动。注意adc_value和water_level都是全局变量,显示函数和控制函数都要读它们,所以要先完成采样和计算,再去执行控制和显示。

参数说明:主循环一个周期大约 80~120ms 左右,其中大头是 LCD 写入的延时。这个刷新率对水位检测足够,因为水位本身是慢变量,不需要毫秒级响应。如果后续要加电机转速控制,必须把 LCD 刷新改成分时任务,否则 PWM 频率会被拖垮。

模式切换按键的消抖逻辑要理解:第一次检测到KEY_MODE == 0时延时 20ms 再确认一次,避免机械按键抖动造成误触发。while (KEY_MODE == 0)是一段简单的松手检测,很多实物按键没有这段,按一下会切换好几次模式。

4.4 自动控制和手动控制的具体实现

/* 自动模式:滞回比较控制水泵 */ void auto_control(void) { if (water_level <= LOW_LEVEL) { PUMP = 1; /* 低水位,开泵 */ } else if (water_level >= HIGH_LEVEL) { PUMP = 0; /* 高水位,停泵 */ } /* 中间区间:保持原状态,不重复启停 */ } /* 手动模式:按键每按一次翻转一次水泵状态 */ void manual_control(void) { static bit last_key = 0; if (KEY_PUMP == 0 && last_key == 0) { delay_ms(20); /* 消抖 */ if (KEY_PUMP == 0) { PUMP = !PUMP; } } last_key = KEY_PUMP; }

这个控制函数是整个系统的核心,auto_control()看起来不到十行,但删掉else if改成两个独立的if,行为立刻就不对了。两个独立的if会在水位 29% 时触发开泵,同时也在 71% 时触发停泵,中间状态没有维持能力。用else if才能构成滞回结构:水位在 30%~70% 之间时,两个条件都不成立,函数什么都不做,水泵保持当前状态。

manual_control()里static bit last_key是一个静态局部变量,它的生命周期贯穿整个程序运行,但只在函数内部可见。按键断开瞬间last_key会被更新,下一次循环如果按键仍按住,因为last_key已经是 1,不会重复翻转。这是处理按键翻转状态的标准写法。

4.5 LCD 显示函数:最少代码实现水位和状态一屏显示

LCD1602 驱动完整写出来很长,这里给出一个精简但可用的版本,重点看写数据和写命令的时序,不用读忙标志,用延时替代:

/* 向 LCD 写命令 */ void lcd_cmd(unsigned char cmd) { LCD_DATA = cmd; LCD_RS = 0; LCD_RW = 0; LCD_EN = 1; _nop_(); LCD_EN = 0; delay_ms(2); } /* 向 LCD 写一个字符 */ void lcd_data(unsigned char dat) { LCD_DATA = dat; LCD_RS = 1; LCD_RW = 0; LCD_EN = 1; _nop_(); LCD_EN = 0; delay_ms(1); } /* LCD 初始化:8 位模式,2 行显示 */ void lcd_init(void) { delay_ms(15); lcd_cmd(0x38); /* 8 位总线,2 行,5x7 字体 */ lcd_cmd(0x0C); /* 显示开启,不显示光标 */ lcd_cmd(0x01); /* 清屏 */ lcd_cmd(0x06); /* 写入后地址自动加 1 */ } /* 刷新一行状态显示 */ void lcd_show_status(void) { unsigned char disp_buf[4]; lcd_cmd(0x80 | 0x40); /* 第二行起始地址 */ disp_buf[0] = water_level / 100 + '0'; disp_buf[1] = water_level % 100 / 10 + '0'; disp_buf[2] = water_level % 10 + '0'; disp_buf[3] = '\0'; lcd_print_string(disp_buf); lcd_cmd(0x80 | 0x46); /* 第二行第 7 列 */ if (PUMP) { lcd_print_string("PUMP ON "); } else { lcd_print_string("PUMP OFF"); } }

lcd_cmd(0x80 | 0x40)表示把光标移动到第二行起始位置。LCD1602 的 DDRAM 第一行起始地址是 0x00,第二行是 0x40,加上最高位 1 表示写 DDRAM 地址。当关掉光标后,这个地址命令不会被直观观察到,但位置对不对直接决定显示内容出现在哪一行。三位的百分比拼接是纯整数运算,不依赖编译器库函数,比用sprintf省出一大截代码空间。

注意:这里 LCD 的delay_ms(2)是按 12MHz 晶振设计的。如果你把晶振改成 24MHz,延时时间会缩短一半,$LCD1602 对写操作的最小脉宽要求可能不满足,仿真里偶尔会白屏,实物里概率更高。

5. Proteus 仿真模拟的常见问题与避坑:5 个让系统罢工的细节

5.1 现象:ADC 读数固定为 255 或 0,转动电位器毫无反应

原因排查顺序:第一,ADC0808 的 CLOCK 引脚没有接时钟源。很多人只给单片机接了晶振,忽略 ADC 是需要独立时钟的。第二,通道地址没有真正锁存进 ADC,ALE 信号没有产生有效的脉冲。第三,IN0 引脚没有接到电位器中抽头,或者接在了电位器的固定端。

解决:先确认 DCLOCK 存在并设置为 500kHz;再用电压表点一下 IN0 引脚,转动电位器观察电压是否在 0~5V 之间变化;最后在代码里给read_adc()加一个断点,单步执行看看有没有卡在while (ADC_EOC == 1)。如果卡住,说明 START 脉冲时序不对,检查引脚定义是不是 P2.3 和 P2.4 接反了。

5.2 现象:LCD1602 显示全黑方块或第一行乱码

原因:P0 口是开源输出的,内部没有上拉电阻。LCD 的数据线接在 P0 上,而 P0 悬空时读进来的是高阻态,数据全是随机值。另一种原因是 LCD 的 RW 引脚忘了接地,或者接到了高电平,导致写入操作变成了读操作。

解决:给 P0 口加 RESPACK-8 排阻,公共端接 +5V。检查 RS、RW、E 三个控制脚不要复用,尤其是 RW,固定接地(写模式)可以省一根控制线,但代码里LCD_RW = 0必须保留。Proteus 里如果对比度引脚 V0 悬空,也可能出现全黑,接一个电位器中抽头并调节到合适电压。

5.3 现象:水位在临界点附近水泵来回启停,像抖个不停

原因:这是最经典的「单一阈值」问题。如果你把启动和停止都写成water_level < 50%开泵、water_level >= 50%停泵,那么水位在 49%~50% 之间波动时,系统会在开和停之间反复切换。

解决:把控制逻辑改成 4.4 节里的滞回结构,LOW_LEVEL和HIGH_LEVEL之间留至少 15% 的区间。仿真验证时故意把电位器慢慢调到 50% 附近,观察水泵是否稳定保持原状态。另外检查阈值宏定义用的是<=还是<,建议统一用<=和>=,避免边界值刚好卡在阈值点上的歧义。

5.4 现象:继电器不吸合,或者吸合了但电机不转

原因:继电器线圈额定电压与供电不匹配是最常见的。Proteus 里部分继电器模型默认线圈电压是 12V,线圈接 5V 时吸合力不足。三极管驱动端的问题也很常见:基极没有串电阻,导致 IO 口电流过大,Pin 电平被拉低;或者把 NPN 管接成了射极跟随器(发射极接负载),Ib 太小无法饱和导通。

解决:双击继电器确认线圈电压,改成 5V。确认 2N2222 的发射极接地,集电极接继电器线圈,基极接 1kΩ 电阻到 P2.6。用电压表量继电器线圈两端,在 PUMP=1 时应该有接近 5V 的电压。电机不转时检查触点侧电源:电机一端接继电器公共端(COM),另一端接 GND,继电器吸合后 COM 和 NO 导通,电机才有供电回路。

5.5 现象:仿真能跑,但全速运行时偶尔卡死,单步却正常

原因:这是时序竞争问题。read_adc()里两个while轮流等待 EOC 翻转,但 Proteus 的时序模型在极短脉冲下可能不稳定,特别是ADC_ALE = 1; ADC_ALE = 0;之间只有两个_nop_()时,EOC 还没来得及响应就已经启动了下一个状态。

解决:在两次while等待之间加一个小的超时保护,比如用一个循环变量计数,超过 1000 次就强制退出:

unsigned int timeout = 1000; while (ADC_EOC == 1 && --timeout); timeout = 1000; while (ADC_EOC == 0 && --timeout);

这种写法不会因为 EOC 时序异常而死循环,出问题时会跳过本次采样,用上一次的旧值继续运行。移植到实物里也推荐保留超时保护,硬件故障时程序至少不会卡死。

6. 进阶验证:均值滤波改造、空转保护与仿真到实物的迁移

前面的系统已经能实现基本的水位自动抽水,但离工程可用还差几步。第一个升级是在采样环节做去极值平均滤波:连续采 5 次,去掉最大值和最小值,剩下 3 次取平均。这比简单 8 次平均更能抵抗偶发尖峰,代码改动很小:

unsigned char filter_adc(unsigned char ch) { unsigned char buf[5]; unsigned char i, j, tmp; unsigned int sum = 0; for (i = 0; i < 5; i++) { buf[i] = read_adc(ch); } /* 冒泡排序,取中间 3 个值平均 */ for (i = 0; i < 4; i++) for (j = 0; j < 4 - i; j++) if (buf[j] > buf[j+1]) { tmp = buf[j]; buf[j] = buf[j+1]; buf[j+1] = tmp; } for (i = 1; i < 4; i++) { sum += buf[i]; } return (unsigned char)(sum / 3); }

第二个升级是空转保护。水泵在无水状态下持续运行会烧毁电机,工程上必须加保护逻辑。思路是:当水位低于 5% 且水泵处于启动状态时,连续检测超过一定时间就直接关泵并点亮报警指示:

#define DRY_RUN_LEVEL 5 #define DRY_RUN_LIMIT 50 unsigned int dry_cnt = 0; if (water_level < DRY_RUN_LEVEL && PUMP) { if (++dry_cnt > DRY_RUN_LIMIT) { PUMP = 0; /* 强制停泵 */ ALARM = 1; /* 报警输出 */ dry_cnt = 0; } } else { dry_cnt = 0; }

DRY_RUN_LIMIT取 50,对应主循环约 5 秒的时间窗口,具体秒数取决于你主循环一个周期的实际长度。这个保护逻辑在仿真里验证时,先把电位器调到 0V 让水泵启动,再保持 5 秒以上,观察泵是否自动关闭。如果秒数不对,要优先量主循环周期而不是改阈值。

从仿真迁移到实物时,最需要替换的是水位传感器。电位器是电压模拟,实物通常用两根不锈钢电极插入水中,利用水的导电性形成分压,中间要加一个隔离用的三极管跟随器,否则电极极化会导致测量值漂移。继电器驱动部分如果控制的是交流水泵,必须换成带灭弧功能的接触器,仿真里的 RELAY 模型没有电弧和触点寿命概念。ADC 部分直接用 STC89C52 这类带内置 ADC 的芯片时,软件要砍掉所有 ADC0808 相关代码,改读 P1 口的模拟输入通道。

自己第一次搭这套系统时,read_adc()里的两个 while 写反了,结果仿真看起来一切正常,但采样值永远是上一次的残留数据。后来越过 EOC 查询直接延时读,又发现临界抖动。最后才明白,这类系统最值钱的部分从来不是那几条if判断,而是时序、滤波和保护逻辑这三个不起眼的角落。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询