基于51单片机和ADS1110的水位监测报警系统设计
2026/9/12 15:40:55 网站建设 项目流程

简介:基于51单片机的水位监测报警系统是一套完整可用的课程设计/毕业设计参考方案,面向单片机初学者、电子爱好者及嵌入式开发者。系统支持水位上下限设置,当水位超过阈值时自动触发声光报警,适用于水库、水箱、蓄水池等场景。压缩包共包含44个文件,约840KB,主要涵盖C语言源代码、Keil工程文件、Proteus仿真工程、原理图、PDF文档、元件清单及流程图等,从软件到硬件设计均有覆盖;目前已有135人学习下载。代码中用LCD1602进行显示、ADS1110完成数据采集,并给出阈值比较、按键设置、报警控制等关键逻辑,配合仿真工程可快速复现系统运行效果;原理图、流程图与物料清单相互对应,方便照图搭建与排查问题,文件分类存放,适合课设或毕设参考,也可在此基础上扩展远程上报、自动排水等功能。

1. 水位报警不是拿阈值硬比:ADS1110 和 51 的资源分配才是关键

水库水位报警这个题目,看着只是“超过上限响一声、低于下限响一声”,但实际做起来最烦的不是报警本身,而是水面波动引起的反复触发。工业里水泵、电磁阀频繁启停,多数不是设备坏了,而是阈值判断没考虑滞回。这个基于 51 单片机的水位监测报警工程,把感知、显示、设置、报警四段链路走完了:ADS1110 负责采集液位变送器输出的模拟量,LCD1602 显示实时水位,按键可现场修改上下限,超限时蜂鸣器与 LED 联动。工程设计上最大的亮点是弃用 ADC0809 这类并行 ADC,改用 I2C 接口的 ADS1110,为后续扩展(比如多路采集、串口上传)留出了大量 I/O 余量。对正在做 51 单片机课程设计的同学来说,这套 Proteus 仿真和源代码能直接复现,而且 ADS1110 的使用边界、阈值存储、流程图画法都是可以写进答辩稿的内容。

2. 硬件信号链:ADS1110 配置、液位变送器和物料清单

2.1 ADC 选型:为什么是 ADS1110 而不是 ADC0809 或电压比较器

早期水位报警方案常用 LM393 电压比较器:两个电位器分别设定上下限,水位电压落在区间外就翻转输出。这种电路响应快、成本极低,但有两个硬伤:上限和下限靠电位器旋钮反复校准,无法数字化保存;水面波动时输出在阈值点附近抖动,蜂鸣器会像“喘气”一样断续响,必须另加 RC 迟滞电路才能稳住。而用 ADC0809 虽然能读数值,但 8 路模拟输入是并行总线,DB0-DB7 直接占掉 P0 整个端口,还得处理 ALE 锁存时序,留给 LCD1602 的引脚就很紧张。

这个工程里出现的是 ADS1110,一颗 16 位 ΔΣ ADC,I2C 接口只占 SCL、SDA 两根线,内置 2.048V 基准电压,PGA 增益可编程(1/2/4/8)。对水库水位这种变化缓慢的物理量,12.5~15SPS 的采样率绰绰有余。更重要的是 Proteus 的元件库里能直接搜到 ADS1110,仿真行为和真片基本一致,程序可以原样移植到实物。选型逻辑很清晰:慢速物理量 + 需要数值显示 + I/O 资源紧张,I2C ADC 就是比并行 ADC 合适。

物料清单(BOM)可以按下面这份准备,元件型号和 Proteus 原理图里保持一致:

序号元件规格/型号数量用途
1单片机STC89C52RC1主控,与 AT89C52 引脚兼容
2ADCADS11101I2C 接口 16 位 ΔΣ ADC
3显示屏LCD16021显示水位电压与上下限
4晶振11.0592MHz1系统时钟
5电容30pF2晶振负载电容
6电解电容10uF / 16V1复位电路定时
7电阻10kΩ4I2C 上拉、按键下拉
8排阻1kΩ×81P0 口上拉
9三极管9012 PNP1蜂鸣器驱动
10蜂鸣器5V 有源1报警输出
11LED红色1报警指示灯
12按键轻触 4pin3SET / UP / DOWN
13电位器10kΩ1仿真中模拟液位传感器输出
14液位变送器4-20mA 两线制1实物采集水位(仿真用可省略)
15采样电阻50Ω 精密14-20mA 转 0.2-1V

注意 P0 口在 51 单片机里是开漏输出,驱动 LCD1602 数据线时必须要加 1kΩ 排阻上拉,否则 LCD 可能只亮不显示。素材里原理图在 PC 端用 Altium Designer 打开 Sheet1.SchDoc 就能看到排阻位置。

2.2 ADS1110 配置字与量程计算

2.2.1 配置寄存器

ADS1110 的配置寄存器是 8 位,上电后主机通过 I2C 写一个字节即可完成设置。位定义如下:

Bit名称说明
bit7SC0=连续转换,1=单次转换后掉电
bit6-5DR1 DR0数据速率选择:00=240SPS,01=60SPS,10=30SPS,11=15SPS
bit4-3PGA1 PGA0增益:00=1,01=2,10=4,11=8
bit2-0保留写入 0

针对水位监测这种缓变信号,我一般配置连续转换、15SPS、PGA=1,对应的配置字是 0x1C。15SPS 时片内数字滤波器对工频干扰有较好的抑制效果,240SPS 虽然快,但噪声明显变大。这个配置字在 ADS1110.C 初始化函数里通过 I2C 写入,代码会在第 4 章展开。

2.2.2 电压换算

ADS1110 输出是 16 位二进制补码。PGA=1 时满量程差分输入为 ±2.048V,LSB 大小是 2.048V / 32767 ≈ 62.5uV。把 ADC 原始值换算成真实电压的公式为:

电压(mV) = 原始值 * 2.048 * 1000 / 32768

代码里一般先取 ADC 原始值,乘 1000 得到毫伏,再除以 32768;如果要显示成“水位百分比”,再按满量程做线性映射。这样处理的好处是主程序里不会出现浮点数,51 单片机做浮点运算会拖慢节奏,整数毫伏运算最适合 Keil C51。

2.3 传感器信号链:4-20mA 变送器如何接入 ADS1110

实物场景里最常用的是两线制 4-20mA 液位变送器。它输出的是电流信号,不能直接进 ADC,需要在外部并联一个精密采样电阻,把电流转成电压。很多人习惯并 250Ω 电阻得到 1-5V,但 ADS1110 的量程只有 2.048V,1-5V 进去直接削顶。我一般用 50Ω 精密电阻,4mA 时产生 0.2V,20mA 时产生 1V,对应的 ADC 读数范围在满量程的 10%~50% 之间,既留足了线性裕量,又不会浪费分辨率。

仿真工程里不需要纠结变送器模型,直接用 Proteus 的电位器,中心抽头接 ADS1110 的 AIN+,AIN- 接地,电压范围调到 0-2.048V 以内就行。电位器从 0% 旋到 100% 的电压变化,模拟的就是水库从低水位涨到高水位的全过程,报警阈值也就有了调试空间。

3. 上下限设置:阈值存放策略、按键状态机与长按连加

3.1 阈值存放在哪:main.c 全局变量还是外部 EEPROM

这套工程的源码目录里只有 main.c、ADS1110.C、lcd1602.c,没有单独的 EEPROM 驱动文件,说明上下限阈值的默认方式是写在 main.c 里的全局变量中。全局变量方案在断电后会恢复成编译时的默认值,好处是逻辑简单,适合课程设计答辩时演示;坏处是现场改了阈值,一断电就丢。

如果要做“可掉电保存的上下限”,常见做法有两种。第一种是 STC89C52 片内自带 2KB EEPROM,通过 ISP/IAP 指令读写,不需要额外硬件;第二种是外挂 AT24C02,用 I2C 访问,但这里 I2C 总线已经挂了 ADS1110,地址上需要区分(ADS1110 的 7 位地址是 0x48,AT24C02 的默认地址是 0xA0,两者不冲突)。我的建议是:课程设计用全局变量即可,答辩时主动讲一句“如果产品化可以移到 STC 内部 EEPROM”,既展示了扩展思路,又不用实际增加代码量。

阈值初始化时要注意一个边界条件:上限必须大于下限,否则逻辑会在“同时触发两种报警”的状态里卡死。我在 main.c 里会加一段保护判断:

#define DEFAULT_HIGH 2400 // 上限 2400mV #define DEFAULT_LOW 800 // 下限 800mV unsigned int high_limit = DEFAULT_HIGH; unsigned int low_limit = DEFAULT_LOW; void threshold_init(void) { if (high_limit <= low_limit) { high_limit = DEFAULT_HIGH; low_limit = DEFAULT_LOW; } }

threshold_init() 这段代码的作用是上电时做一次合法性校验:只要存储的上下限关系不合理,立刻恢复成默认值。参数 DEFAULT_HIGH 和 DEFAULT_LOW 的单位是毫伏,2400mV 对应 2.4V 的水位电压,800mV 对应 0.8V,具体数值要跟传感器的量程标定结果匹配,这里只是占位。

3.2 三按键状态机实现

上下限设置用 3 个按键完成:SET 负责在“运行 / 设置上限 / 设置下限 / 保存退出”之间切换,UP 和 DOWN 负责加数值、减数值。用状态机来组织这套交互逻辑是最清晰的,枚举定义和核心切换代码可以这样写:

typedef enum { MODE_RUN = 0, // 运行态:显示实时水位 MODE_SET_H, // 设置上限 MODE_SET_L // 设置下限 } SYS_MODE; SYS_MODE sys_mode = MODE_RUN; void key_set_scan(void) { if (KEY_SET == 0) { delay_ms(20); // 简单消除机械抖动 if (KEY_SET == 0) { if (sys_mode == MODE_RUN) { sys_mode = MODE_SET_H; // 进入上限设置 } else if (sys_mode == MODE_SET_H) { sys_mode = MODE_SET_L; // 切换为下限设置 } else { threshold_save(); // 保存并退出 sys_mode = MODE_RUN; } while (KEY_SET == 0); // 等待按键释放 } } }

这段代码里,sys_mode 既是状态变量,也是 LCD1602 显示内容的控制信号。在 MODE_RUN 状态下按下 SET 会进入上限设置;再按一次切到下限设置;第三次按下就调用 threshold_save() 保存当前修改并回到运行态。while (KEY_SET == 0) 的作用是防止一次按下被重复触发多次,配合 delay_ms(20) 完成消抖。注意这里 delay_ms(20) 只用在按键处理上,因为按键是低频事件,阻塞 20ms 不会影响 ADC 采集。

3.3 按键去抖与长按连加

设置上下限时,要调到 2400mV 这种精确值,靠一次一次按不太现实,必须支持长按连加。长按的通用实现方案是用定时器产生 10ms 中断,在中断里扫描按键并计数,而不是把扫描放在主循环里用 delay 拖时间。

unsigned int key_hold_cnt = 0; unsigned int *p_set_value = NULL; // 指向当前要修改的阈值 void timer0_isr(void) interrupt 1 { TH0 = 0xDC; // 重装初值,10ms 中断一次 TL0 = 0x00; if (KEY_UP == 0) { key_hold_cnt++; if (key_hold_cnt == 10) // 按住 100ms 触发第一次加 { (*p_set_value) += 10; } else if (key_hold_cnt > 50) // 按住超过 500ms 后连续加 { if (key_hold_cnt % 5 == 0) // 每 50ms 加一次,避免跳太快 { (*p_set_value) += 10; } } } else { key_hold_cnt = 0; // 按键释放,计数清零 } if (*p_set_value > 2000) *p_set_value = 2000; // 上限钳位 if (*p_set_value < 100) *p_set_value = 100; // 下限钳位 }

key_hold_cnt 变量是核心:它记录按键持续按下的时间片数量。计时到 10 次(100ms)时触发第一次数值增加,超过 50 次(500ms)后进入连加模式,并且用取模运算控制每隔 5 个中断(50ms)才加一次,这样连加时数值不会瞬间冲到边界。钳位判断保证设置值永远落在 100mV 到 2000mV 的合理区间内,防止用户把上限设成远超量程的数值。p_set_value 是全局指针,进入 MODE_SET_H 时让它指向 high_limit,进入 MODE_SET_L 时让它指向 low_limit,这样 UP/DOWN 按键逻辑只需要写一份。

矩阵键盘在这个场景里不是必须的,因为 3 个按键足够完成所有交互。但如果你的设计里增加了模式切换、手动/自动切换功能,按键超过 4 个,就值得用 4×4 矩阵键盘加状态扫描来扩展,核心判断逻辑与上面这套状态机完全一致,只是按键扫描换成行列反转法。

4. 主程序这样写:I2C 采集、滞回比较和 LCD1602 刷新

4.1 I2C 读写子程序与 ADS1110 读取时序

ADS1110 的 I2C 时序看起来简单,但有两个细节容易栽跟头:地址的高低位方向和 ACK 应答位。先从底层函数开始梳理。

sbit SCL = P3^5; sbit SDA = P3^4; void i2c_start(void) { SDA = 1; SCL = 1; delay_5us(); SDA = 0; delay_5us(); SCL = 0; } void i2c_stop(void) { SDA = 0; SCL = 1; delay_5us(); SDA = 1; } bit i2c_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { SDA = (dat & 0x80) ? 1 : 0; // 高位先出 dat <<= 1; SCL = 1; delay_5us(); SCL = 0; } SDA = 1; // 释放 SDA,准备接收 ACK SCL = 1; delay_5us(); return SDA; // 返回从机应答位 } unsigned char i2c_read_byte(bit ack) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { SCL = 1; dat <<= 1; if (SDA) dat |= 0x01; SCL = 0; } SDA = ack; // 主机发送 ACK(0) 或 NACK(1) SCL = 1; delay_5us(); SCL = 0; SDA = 1; return dat; }

i2c_start() 和 i2c_stop() 是 I2C 总线最基本的时序:SCL 高电平期间 SDA 产生下降沿是起始条件,上升沿是停止条件。i2c_write_byte() 每次发送 8 位数据,最高位在前,发送完第 8 位后释放 SDA 线并拉高 SCL,读取从机的 ACK 应答位。i2c_read_byte(bit ack) 接收数据时,在时钟高电平期间读取 SDA 电平拼装成字节;读完一个字节后,通过参数 ack 控制主机发送应答还是非应答。这两层函数是所有 I2C 器件驱动的地基,ADS1110 和 AT24C02 都能复用。

读取 ADS1110 的完整初始化与采样函数如下:

#define ADS_WR_ADDR 0x90 // 0x48 左移 1 位 + 写位 0 #define ADS_RD_ADDR 0x91 // 0x48 左移 1 位 + 读位 1 void ads_init(void) { i2c_start(); i2c_write_byte(ADS_WR_ADDR); // 发送器件写地址 i2c_write_byte(0x1C); // 连续转换、15SPS、PGA=1 i2c_stop(); } unsigned int ads_read(void) { unsigned char i, high, low; unsigned int value; i2c_start(); i2c_write_byte(ADS_RD_ADDR); // 发送器件读地址 high = i2c_read_byte(0); // 读高字节,主机回 ACK low = i2c_read_byte(1); // 读低字节,主机回 NACK i2c_stop(); value = ((unsigned int)high << 8) | low; return value; }

ads_init() 里写入的 0x1C 是和 2.2 节配置字对应的:bit7 为 0 表示连续转换模式,bit6-5 为 11 表示 15SPS,bit4-3 为 00 表示 PGA=1。bit2-0 保留位写 0。ads_read() 中读地址是 0x91,注意不是 0x90,0x90 是写配置时用的地址。如果地址搞反,I2C 总线会一直收不到 ACK,ads_read 返回 0xFFFF,LCD 上显示的就是满量程。

读回 16 位原始值后,主函数里换算成毫伏:

unsigned int adc_value_mv; adc_value_mv = (unsigned int)(((unsigned long)ads_read() * 2048) / 32768);

这一步用 32 位中间变量做了乘除法,避免 16 位乘法溢出。原始值 32768 对应 2048mV,换算结果就是当前水位的电压值,单位毫伏。水位呈现非线性时,可以在这一行之后加查表或分段线性修正,不过对课程设计场景,线性映射已经足够。

4.2 带滞回的双阈值判断逻辑

这是整个工程最值得展开的部分。如果不做滞回,只写“电压大于上限就报警”,那么当水位恰好在上限附近波动时,蜂鸣器会反复响-停-响-停,严重影响使用体验。滞回判断的要点是:进入报警和退出报警使用不同的阈值点。

当前状态进入报警条件退出报警条件
正常运行adc_mv ≥ 上限阈值-
高水位报警-adc_mv ≤ 上限阈值 - 滞回值
低水位报警adc_mv ≤ 下限阈值adc_mv ≥ 下限阈值 + 滞回值

我在 main.c 中的实现是这样的:

#define HYSTERESIS 50 // 滞回窗口,单位 mV unsigned char flag_high = 0; unsigned char flag_low = 0; void alarm_check(unsigned int adc_mv) { // 高水位判断:进入用高阈值,退出用高阈值减滞回 if (!flag_high) { if (adc_mv >= high_limit) { flag_high = 1; } } else { if (adc_mv <= (high_limit - HYSTERESIS)) { flag_high = 0; } } // 低水位判断:进入用低阈值,退出用低阈值加滞回 if (!flag_low) { if (adc_mv <= low_limit) { flag_low = 1; } } else { if (adc_mv >= (low_limit + HYSTERESIS)) { flag_low = 0; } } if (flag_high || flag_low) { BUZZER = 0; // 蜂鸣器低电平导通 LED_RED = 0; // LED 点亮 } else { BUZZER = 1; LED_RED = 1; } }

用 flag_high 和 flag_low 两个标志位记录当前是否处于报警状态,而不是直接用电压比较结果去驱动蜂鸣器,这是整个逻辑的关键。HYSTERESIS 取 50mV:当水位超过上限时报警,必须回落到上限以下 50mV 才解除;低水位报警同理,必须回升到下限以上 50mV 才解除。50mV 这个值来自水面波动幅度和 ADC 噪声的折中,波动大时可以调到 100,纯实验室环境也可以减到 20。

定时器中断每隔 200ms 调用一次 alarm_check(),这个周期对水位变化足够快,又不会让蜂鸣器因为高频刷新而出现杂音。蜂鸣器驱动在实物里接 PNP 三极管,I/O 口输出低电平导通,输出高电平关闭;仿真里 Proteus 的 BUZZER 模型直接接 P1.0 也能工作,但实物必须加三极管,51 单片机 I/O 口直接驱动有源蜂鸣器电流不够。

4.3 LCD1602 的显示刷新策略

lcd1602.c 里的底层函数只做四件事:写命令、写数据、清屏、光标定位。显示内容控制在两行以内,第一行显示实时水位电压,第二行显示当前上下限值。

void disp_refresh(unsigned int adc_mv) { unsigned char buf[17]; lcd_write_cmd(0x80); // 第一行起始地址 sprintf(buf, "Level:%4d.%d V ", adc_mv / 1000, (adc_mv % 1000) / 100); lcd_write_str(buf); lcd_write_cmd(0xC0); // 第二行起始地址 sprintf(buf, "H:%d.%02d L:%d.%02d", high_limit / 1000, (high_limit % 1000) / 10, low_limit / 1000, (low_limit % 1000) / 10); lcd_write_str(buf); }

disp_refresh() 每 500ms 调用一次。刷新频率太快,LCD 会看到明显的闪烁;太慢,水位变化看起来卡顿。500ms 对 1602 这种静态显示是合适的。代码里 sprintf 把数值拆成整数位和小数位显示,LCD1602 是字符型屏,显示“Level:2.4 V”和“H:2.40 L:0.80”这种格式比显示纯数字直观得多。注意 lcd_write_cmd(0x80) 是准备第一行缓冲区,0xC0 是第二行缓冲区,这两条指令是 1602 显示控制的惯例写法。

显示出现乱码,先看是否每次刷新前都 lcd_write_cmd(0x01) 清屏了——清屏指令会全屏刷新,调用太频繁就是闪烁根源。我的做法是只覆盖当前行,不清屏,用 0x80/0xC0 直接定位光标然后整行覆盖写入,末尾的 0x20 空格负责擦除上次残留的字符。

5. Proteus 8.7 仿真搭建与实物联调验证

5.1 从 pdsprj 打开工程到跑通仿真

工程压缩包里的 New Project.pdsprj 是 Proteus 8.7 工程文件,直接用 Proteus 8 Professional 打开即可。双击原理图中的 AT89C52 芯片,把 Program File 指向主目录下编译生成的 main.hex,再把 Crystal Frequency 改成 11.0592MHz,点运行就能看到 LCD1602 亮起来显示当前模拟水位电压。

仿真中 ADS1110 的接线需要注意三个点:AIN+ 接电位器抽头,AIN- 直接接地,SCL/SDA 分别接 P3.5/P3.4,且两个引脚各接一个 4.7kΩ 上拉电阻到 VCC。在 Proteus 里不加上拉电阻,I2C 总线也能跑通,因为 ADS1110 仿真模型内部做了弱上拉处理;但实物板子上 ADS1110 属于开漏输出,不加上拉电阻,SCL/SDA 信号根本抬不到高电平,通信必然失败。仿真通过不代表实物没问题,这一点要在接线时区分清楚。

5.2 仿真和实物的三个典型差异

第一个差异是电源噪声。Proteus 里的电位器输出是理想的直流电压,ADC 读到的数值纹丝不动;实物中 4-20mA 变送器是电流环供电,电源纹波和变送器本身噪声都会叠加到采样电阻上。我给实物的 ADS1110 电源引脚并联了 100nF 陶瓷电容和 10uF 电解电容,AIN+ 输入端再串一个 1kΩ 电阻并联 100nF 电容做低通滤波,截止频率大约在 1.6kHz,对缓慢变化的水位信号毫无影响,但对 50Hz 以上噪声有明显的衰减作用。

第二个差异是报警器件驱动。Proteus 里 BUZZER 模型两端加 5V 就能响,但真实的有源蜂鸣器工作电流通常在 20-30mA,51 单片机 I/O 口最大灌电流只有 20mA 左右,推不动。用 9012 三极管放大,基极串 1kΩ 电阻接 P1.0,发射极接 VCC,集电极接蜂鸣器负极,蜂鸣器正极接 VCC。这样 P1.0 输出低电平时三极管导通,蜂鸣器得电发声。仿真里没有三极管照样响,容易让人忽略这一步,实物焊接时漏掉三极管,报警功能大概率起不来。

第三个差异是阈值电压的标定。仿真里可以随便把上限设成 1.5V 来演示;实物里液位变送器在 0% 水位输出 4mA(对应 0.2V),100% 水位输出 20mA(对应 1V)。所以实物调试时,应该在水位最低点时读一下 ADC 毫伏值记作 zero_scale,在水位最高点读最大值记作 full_scale,再把 high_limit 和 low_limit 设为两个刻度之间的百分比位置,而不是直接沿用仿真的电压值。

5.3 功能验证清单

跑完仿真和实物后,按这张表逐项确认,能覆盖这个工程的绝大部分功能点:

检查项操作预期结果
默认阈值显示上电后观察 LCD第一行显示当前水位电压,第二行显示默认上下限
上限报警电位器电压调到上限之上蜂鸣器响、LED 亮、LCD 数值随电压变化
滞回恢复电压缓慢降到上限-50mV 以下报警解除
下限报警电压调到下限之下报警器触发
阈值设置按 SET 进入设置,UP/DOWN 修改修改后回到运行态,新阈值立即生效
阈值边界尝试把下限设到上限之上数值被钳位或恢复默认,系统不死机

阈值边界这一项在仿真里容易被忽略,但答辩时评委最喜欢问:“如果我把下限设得比上限还高,会怎样?”因为初始化做了 clamp 校验,加上 alarm_check 里的滞回判断,即使出现这种误操作,也只是同时触发高水位和低水位标志,系统不会死循环。这个细节值得在 turn-in 报告里单独写一段,比贴整页代码更能体现工程意识。仿真通过后,把 ADS1110 换成实物,把电位器换成液位变送器和采样电阻,原理图和源代码不需要任何改动,这就是这套工程在硬件上留出的余量——ADS1110 的 I2C 地址固定为 0x48,不占用额外 I/O,51 单片机的 P1/P2 口还能继续挂矩阵键盘和继电器模块。

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

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

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

立即咨询