AT89C51单片机水质监测系统:浊度电导率采集与ADC0832驱动
2026/9/20 14:51:46 网站建设 项目流程

简介:这份资源是皖西学院本科毕业设计《基于单片机的水质监测系统的设计》的完整文档,面向电子信息、自动化、测控等专业的应届毕业生及从事环境监测硬件开发的入门者。内容围绕AT89C51单片机展开,涵盖绪论中的水质监测背景与国内技术现状、监测指标,水中自由离子浓度与导电率的相互关系、浑浊度监测原理,以及硬件设计中的单片机最小系统、复位与晶振电路,数据采集、A/D转换、液晶显示与GSM/GPRS无线通信等功能电路的设计思路,并给出系统优势与应用场景分析。压缩包内为1个docx文档,约1.38MB,含中英文摘要、关键词与完整目录,结构规范,可直接作为毕业设计写作与答辩的参考模板。目前已有463人学习下载,适合需要选题参考、方案框架或论文排版的读者借鉴。

1. 从一杯浑水到一串数码:AT89C51 水质监测系统测的到底是什么

实验室里测一次浊度和电导率,往往要跑分光光度计、配标准溶液、等试剂显色,一套流程下来小半天就没了。而一块 40 脚的 AT89C51,配上光电式浊度探头和一对电导电极,就能把这两项指标变成数码管上跳动的数字,元器件成本压到几十元。这套基于单片机的方案并不追求实验室级精度,它面向的是基层水样的连续初筛、河道断面巡检、鱼塘增氧控制和工业循环水这类需要"看着趋势走"的场景,也常被拿来做电子测控方向的毕业设计与课程设计载体。整条链路的骨架很朴素:传感器把水中自由离子浓度和浑浊度转成模拟电压,ADC0832 把电压量化成 8 位数字,单片机读回来做标定换算,最后送数码管显示。真正麻烦的地方从来不在原理,而在电极怎么激励、串行时序怎么对齐、读回来的数字怎么才能不跳。

2. 两路传感器前端与 AT89C51 最小系统的硬件搭建

2.1 晶振、复位与 EA 引脚:让 40 脚芯片先跑起来

最小系统的定义很明确:一个能真正执行程序的最小配置。MCS-51 片内没有晶体振荡器,也不带复位电路,这两样必须外接。晶振接在 XTAL1、XTAL2 之间,两侧各挂一只 30pF 瓷片电容到地,常见做法是 22pF 到 33pF 之间取值,容值偏大起振慢,偏小容易受分布电容影响停振。本系统选 12MHz,对应机器周期正好 1μs,定时器初值和软件延时都好算。

复位电路有上电复位和按键复位两种。上电复位靠 RC 充电产生一个足够宽的高电平脉冲,按键复位则并联一只按钮,随时手动重启。51 单片机要求 RST 引脚上高电平持续两个机器周期以上,工程上一般保证 10ms 量级,典型取值是 10μF 电解电容配 10kΩ 电阻。EA/VPP 引脚要接高电平,表示从片内 8KB Flash 取指令;如果误接地,CPU 会去片外找程序,现象就是"程序烧进去了但一点反应没有"。

还有一个容易被忽略的点:P0 口是漏极开路结构,作为普通 I/O 输出时驱动能力弱,用它驱动数码管段码必须外接 10kΩ 上拉排阻,否则亮度低、显示乱码。P1、P2、P3 口内部有上拉电阻,可以直接用。本系统的引脚分配大致如下:

单片机引脚连接对象说明
XTAL1/XTAL212MHz 晶振 + 2×30pF时钟源
RST10μF + 10kΩ + 按键上电与手动复位
EAVCC使用片内 8KB Flash
P1.0 / P1.1 / P1.2ADC0832 的 CS / CLK / DI-DO串行三线
P0.0–P0.7数码管段码 a–dp需 10kΩ 上拉排阻
P2.0–P2.3数码管位选经三极管驱动
P3.0 / P3.1串口 RXD / TXD可选上传通道

2.2 电导电极测自由离子浓度:从电阻率到电导率的换算

纯水本身会微弱电离,所以它有微弱的导电能力;水中自由离子越多,导电性越强。水质越纯、温度越低,电离度越低,电导率就越低,超纯水几乎不导电。这套系统测自由离子浓度,本质上是测水样的电阻,再换算成电导率。

这里有几个量必须理清。电阻率 ρ 的物理意义是边长 1cm 的立方体水样的电阻值,单位 Ω·cm;它的倒数就是电导率 κ,单位 S/cm,水处理行业习惯用 μS/cm。实际测量时用的是两块固定面积、固定间距的电极,几何关系被封装成一个电极常数 K(单位 cm⁻¹),于是有:

κ = K / R

R 是万用表或测量电路读到的电极间电阻。K 值一般标在电极铭牌上,常见 0.1、1.0、10 三档,低电导水样配小 K 值电极,高电导配大 K 值电极。直接拿直流去测电阻是新手最容易踩的坑:直流会在电极表面产生极化层和电解产物,读数几分钟内就往下漂,电极还会结垢。常见做法是用方波或交流激励电极,正负半周交替,把极化效应抵消掉。

温度的影响同样不能忽略。溶液电导率随温度升高而增大,工程上每升高 1℃ 大约增加 2%,所以通常统一补偿到 25℃ 再比较:

/* 将 t℃ 下测得的电导率补偿到 25℃ 标准值 */ float cond_25(float kappa_t, float t) { return kappa_t / (1.0f + 0.02f * (t - 25.0f)); /* 0.02 为温度系数 */ }

参数kappa_t是当前温度下的电导率,t是水温,0.02是通用温度系数。如果系统不测水温,这个补偿就只能按室温估算,这是低成本方案的固有误差来源。

2.3 变光型浊度传感器的结构与接入

浊度的测量原理是光学式的:液体浊度增加,对光的阻碍增强,透过率下降,光敏电阻接收到的光就弱;浊度下降则相反。变光型浊度传感器把这个思路做成了一个闭环结构,内部包含光源、两只光敏电阻器、平衡检测器、固定电源、反馈控制和可调电源。

它最核心的设计要求是两只光敏电阻器阻值相等,也就是接受的光强一致。一旦浊度变化破坏了这种平衡,平衡检测器立刻识别出偏差,反馈控制去调整可调电源,改变光源强度,把光强差重新拉回零。这样一来,光源强度这个电学量就直接对应到被测物质的浊度。这种结构量程宽、标定过程简单、功耗低、成本低、体积小,而且在高浑浊度时分辨率比低浊度时更高,适合污水这类高浊场景。

代价是它对杂散光敏感。传感器外壳必须遮光,测量腔不能有气泡,光源老化会带来缓慢漂移,所以每隔一段时间需要用标准浊度液复核一次。另外,光敏电阻本身的响应速度和线性度都一般,动态响应要求高的场合要换光电二极管方案。

2.4 ADC0832 与单片机的四线接法及显示电路

模拟信号进不了 CPU,必须经过 A/D 转换。ADC0832 是 8 位分辨率、双通道的串行 A/D 芯片,输入输出电平与 TTL/CMOS 兼容,5V 供电时输入范围 0~5V,工作频率 250kHz,转换时间 32μs,一般功耗仅 15mW,有 8P、14P 的 DIP 和贴片封装,商用级温宽 0~70℃,工业级 40~85℃。体积小、兼容性强、性价比高,是这类小系统的常见选择。

标准接法是四根线:CS、CLK、DI、DO。但 ADC0832 在通信过程中 DI 和 DO 并不同时有效,加上单片机的 I/O 口本身是双向的,所以电路上可以把 DI 和 DO 并到一根数据线上,省出一个 I/O。接法就是前面引脚表里的 CS→P1.0、CLK→P1.1、DI/DO 合并→P1.2。

显示部分用 8 段 LED 数码管,分共阳和共阴两种。共阴数码管各段阴极连在一起接地,段码给高电平点亮;共阳则相反,公共端接 VCC,段码给低电平点亮。数字 0~9 的段码如下:

字符共阴段码共阳段码
00x3F0xC0
10x060xF9
20x5B0xA4
30x4F0xB0
40x660x99
50x6D0x92
60x7D0x82
70x070xF8
80x7F0x80
90x6F0x90

注意:段码表是最容易抄错的地方。共阴共阳搞反,现象是"该亮的灭、该灭的亮",直接把表取反即可;但如果连限流电阻都省了,数码管会在几小时内明显变暗,这个损失是不可逆的。

3. ADC0832 串行时序拆解与 C51 驱动实现

3.1 三个控制位决定一次转换

ADC0832 的通信是标准的同步串行结构,一次完整转换由 CS 拉低开始、CS 拉高结束。在 CS 有效期间,前三个 CLK 上升沿依次把三位控制信息从 DI 打进去:第一位是起始位,固定为 1;第二位是模式选择位,单端输入为 1,差分输入为 0;第三位是通道选择位,0 选 CH0,1 选 CH1。

第三个时钟脉冲之后,DI 的作用结束,芯片把 DO 切换为输出,从高位开始逐位吐出 8 位转换结果。转换结果的 MSB 先出,之后还会以 LSB 在前的顺序重复输出一遍同样的 8 位,用于校验。理解这一点很关键:很多人第一次写驱动,读出来数总是对不上,原因就是把第二段冗余输出当成了新数据接着往左移。

3.2 ADC0832_Read 函数的逐位实现

下面这段代码是 51 平台上的经典写法,把 DI 和 DO 合并到一根线上,读两遍做对比校验:

#include <reg51.h> #include <intrins.h> sbit ADC_CS = P1^0; /* 片选,低电平有效 */ sbit ADC_CLK = P1^1; /* 串行时钟,由单片机产生 */ sbit ADC_DIO = P1^2; /* 数据线,DI 与 DO 并联复用 */ /* 读一个通道,ch=0 选 CH0,ch=1 选 CH1,失败返回 0xFFFF */ unsigned int ADC0832_Read(unsigned char ch) { unsigned char i; unsigned char dat_msb = 0; /* 第一次读,MSB 在前 */ unsigned char dat_lsb = 0; /* 第二次读,LSB 在前,用于校验 */ ADC_CS = 0; /* 拉低片选,开始一次转换 */ ADC_CLK = 0; ADC_DIO = 1; /* 第 1 个时钟:起始位,固定为 1 */ ADC_DIO = 1; ADC_CLK = 1; ADC_CLK = 0; /* 第 2 个时钟:单端模式位,1 表示单端输入 */ ADC_DIO = 1; ADC_CLK = 1; ADC_CLK = 0; /* 第 3 个时钟:通道选择位,0=CH0,1=CH1 */ ADC_DIO = ch ? 1 : 0; ADC_CLK = 1; ADC_CLK = 0; ADC_DIO = 1; /* 释放数据线,交给 ADC 输出 */ /* 第 4~11 个时钟:读 8 位结果,高位在前 */ for (i = 0; i < 8; i++) { ADC_CLK = 1; ADC_CLK = 0; /* 下降沿后数据稳定 */ dat_msb = (dat_msb << 1) | ADC_DIO; } /* 第 12~19 个时钟:冗余输出,低位在前 */ for (i = 0; i < 8; i++) { dat_lsb |= (unsigned char)(ADC_DIO << i); ADC_CLK = 1; ADC_CLK = 0; } ADC_CS = 1; /* 结束转换 */ ADC_CLK = 0; if (dat_msb == dat_lsb) { return dat_msb; /* 两次读一致,数据可信 */ } return 0xFFFF; /* 不一致,本次丢弃 */ }

逻辑上分四段:三段控制位、一段正向读数、一段反向读数、一段校验。控制位的三位是芯片内部状态机的输入,顺序错了通道就选错。两次读数的校验不是为了纠错,而是判断数据线是不是被干扰或者线太长导致的时序失配,返回值 0xFFFF 交给上层丢弃重采,比读到一个错误的静态值安全得多。

参数上要注意ADC_CLK的高低电平宽度。12MHz 晶振下,每条ADC_CLK = 1; ADC_CLK = 0;大约消耗几个机器周期,实际时钟频率远低于芯片 250kHz 的上限,属于"慢速安全"区间。如果换成 24MHz 或更快的晶振,需要在时钟翻转之间插入_nop_()保证高低电平保持时间。ADC_DIO在读之前必须先写 1,51 的准双向口只有先输出高电平,读引脚时才不会被内部下拉拉低。

3.3 分辨率、参考电压与模拟量换算

ADC0832 是 8 位分辨率,输入范围取决于参考电压。5V 供电时,量化台阶是 5V / 256 ≈ 19.53mV,也就是说 ADC 每变化 1 个码值,代表约 19.5mV 的电压变化。换算关系写成整数运算避免引入浮点库:

/* 码值转毫伏,Vref = 5000mV,8 位满量程 255 */ unsigned int code_to_mv(unsigned char code) { return (unsigned int)code * 5000UL / 255UL; }

用 255 还是 256 做分母,实际影响不到半个 LSB,但对标定系数有微小影响。习惯上按满量程 255 处理,标定的时候用同一套换算,误差会被拟合系数吸收掉。真正要警惕的是参考电压本身:如果直接拿 USB 供电的 5V 做参考,电压在 4.8~5.2V 之间浮动,换算出来的电压就会随之漂移。对精度有要求的场合,参考电压要么单独用 TL431 之类的基准源,要么拿一路 ADC 通道去测基准电压做比值修正。

4. 主程序框架、数码管动态扫描与标定换算

4.1 主循环数据流与模块划分

软件用 C 语言写,好处是程序结构清晰、可移植、能直接操作硬件寄存器。整个系统按模块拆成四块:ADC 采样、数据换算、显示刷新、串口上传。主循环不做阻塞等待,每一轮把两个通道都采一遍,异常值直接丢弃,正常值进入显示缓冲区。

unsigned int mv_cond, mv_turb; /* 两路电压,单位 mV */ void main(void) { unsigned int tmp; Uart_Init(); /* 9600bps,用于上传数据 */ Timer0_Init(); /* 1ms 定时中断,驱动数码管扫描 */ while (1) { tmp = ADC0832_Read(0); /* CH0 接电导电极前端 */ if (tmp != 0xFFFF) { mv_cond = code_to_mv((unsigned char)tmp); } tmp = ADC0832_Read(1); /* CH1 接浊度传感器 */ if (tmp != 0xFFFF) { mv_turb = code_to_mv((unsigned char)tmp); } Update_Buffer(mv_cond, mv_turb); /* 换算并写入显示缓冲区 */ Uart_Send_Frame(mv_cond, mv_turb); /* 打包上传 */ Delay_ms(500); /* 采样间隔,避免刷新过快看不清 */ } }

Delay_ms(500)是采样节奏控制。水质变化本身是慢过程,采太快只会让数码管末位乱跳,反而看不清趋势;但也不能太慢,否则报警响应跟不上。经验值是 300ms 到 1s 之间,具体看场景。Update_Buffer负责把电压值按标定模型换算成浊度值和电导率值,再拆成 4 位数字放进显示缓冲区,主循环只管写,扫描交给定时中断。

4.2 四位共阴数码管的动态扫描驱动

数码管用动态扫描,逐位点亮、循环刷新,利用人眼视觉暂留形成同时显示的效果。刷新率放在定时器中断里,1ms 一次,四位轮流,整体刷新率 250Hz,不会有明显闪烁。

/* 共阴段码表,下标 0~9 对应数字,10 为空白 */ code unsigned char seg_table[] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x00 }; unsigned char disp_buf[4] = {0, 0, 0, 0}; /* 待显示的 4 位数字 */ unsigned char scan_idx = 0; /* 当前扫描位 */ void Timer0_ISR(void) interrupt 1 { TH0 = (65536 - 1000) / 256; /* 1ms @12MHz */ TL0 = (65536 - 1000) % 256; P2 = 0xFF; /* 消影:先关掉所有位 */ P0 = seg_table[disp_buf[scan_idx]]; /* 送段码 */ P2 = ~(1 << scan_idx); /* 位选,共阴低电平有效 */ scan_idx = (scan_idx + 1) & 0x03; /* 0→1→2→3→0 */ }

P2 = 0xFF这一句是消影。如果不做这一步,上一位的段码会在切换瞬间带到下一位,显示屏上会出现"鬼影",表现为数字边缘发虚或者相邻位同时微亮。code关键字把段码表放进程序存储器,不占宝贵的 128 字节片内 RAM。位选端驱动电流要经过三极管或者 ULN2003,直接拿 I/O 灌电流超过 20mA 会损伤端口。

4.3 浊度与电导率的标定模型

标定是整个系统里最花时间也最值钱的一步。做法是配一组已知浓度的标准液,测出每一点的传感器电压,再做回归。原文给出的浊度拟合关系是Z = 73.805V - 105,其中 Z 是浊度(mg/L),V 是传感器电压读数,相关系数 r = 0.9983。这个系数只适用于那一台传感器和那套电路,换一支探头就必须重标。

工程上更推荐用最小二乘把一次项和二次项都算出来,让数据自己决定用几次模型。用 Python 在电脑上离线拟合,把系数写回单片机:

import numpy as np # 标定点:电压(mV) 与对应的标准浊度(mg/L) V = np.array([106, 125, 195, 253, 350, 510, 700, 890], dtype=float) Z = np.array([ 2, 5, 12, 20, 35, 65, 110, 170], dtype=float) # 二次拟合,返回 [a2, a1, a0],对应 Z = a2*V^2 + a1*V + a0 a2, a1, a0 = np.polyfit(V, Z, 2) # 计算拟合优度,判断二次项是否真的必要 pred = a2 * V**2 + a1 * V + a0 r2 = 1 - np.sum((Z - pred)**2) / np.sum((Z - Z.mean())**2) print(f"Z = {a2:.3e}*V^2 + {a1:.4f}*V + {a0:.2f}, R2 = {r2:.4f}") if abs(a2) < 1e-6: print("二次项可忽略,直接退化为线性模型 Z = a1*V + a0")

np.polyfit的第二个参数是多项式阶数,r2越接近 1 拟合越好。如果二次项系数小到可以忽略,就退化回一次模型,单片机里只做一次乘加,省掉浮点运算的开销。这些系数最终以常量形式写进 C 代码,注意用float还是定点化要看单片机的算力,51 上跑浮点乘除会明显拖慢主循环,定点化时把所有系数统一放大 1000 倍再做整数运算更稳妥。

电导率那一路的换算链条更长:ADC 电压 → 电极分压关系 → 电极间电阻 R → 电导率 κ = K / R → 温度补偿到 25℃。每一环都有误差,所以标定时最好直接建立"电压 → 电导率"的映射,把中间环节的误差一起吸收掉。

4.4 用定时器 1 把数据送出串口

单片机系统加一路串口,就能把数据挂到上位机或者无线模块上。定时器 1 工作在方式 2 自动重装,负责产生波特率:

void Uart_Init(void) { TMOD |= 0x20; /* 定时器1,方式2,8 位自动重装 */ TH1 = 0xFD; /* 12MHz 晶振下 9600bps 的初值 */ TL1 = 0xFD; SCON = 0x50; /* 串口方式1,10 位异步,允许接收 */ PCON &= 0x7F; /* SMOD = 0,波特率不加倍 */ TR1 = 1; /* 启动定时器1 */ }

0xFD这个初值来自公式:256 − 12 000 000 / (32 × 12 × 9600) ≈ 252.7,取整得 253,即 0xFD,实际波特率误差约 0.16%,在容错范围内。如果换成 11.0592MHz 晶振,初值正好是 0xFD 且误差为零,这也是串口通信场景偏爱 11.0592MHz 的原因。数据帧格式自己定义就行,常见的是帧头 0xAA、两字节电压、一字节校验和,接收端按字节解析。

5. 标定、滤波与调试现场的常见坑

5.1 中值滤波加滑动平均压住读数抖动

ADC 读出来的原始码值一定在跳,跳 2 到 3 个 LSB 很正常,电源纹波大或者数据线走线长的时候能跳到十几。最简单的压制方式是组合滤波:先连续采 5 次取中值,剔掉偶发的尖峰干扰,再对最近 8 次的中值结果做滑动平均,平滑随机噪声。

#define WIN 8 static unsigned int win[WIN] = {0}; static unsigned char idx = 0; unsigned int filter(unsigned int new_val) { unsigned int tmp[5], i, j, t; unsigned long sum = 0; /* 第一步:连续采 5 次,冒泡取中值 */ for (i = 0; i < 5; i++) { tmp[i] = ADC0832_Read(1); Delay_us(200); } for (i = 0; i < 4; i++) for (j = 0; j < 4 - i; j++) if (tmp[j] > tmp[j + 1]) { t = tmp[j]; tmp[j] = tmp[j + 1]; tmp[j + 1] = t; } /* 第二步:中值入滑动窗口,求平均 */ win[idx] = tmp[2]; idx = (idx + 1) % WIN; for (i = 0; i < WIN; i++) sum += win[i]; return (unsigned int)(sum / WIN); }

中值滤波的次数取 5 是折中,取 3 效果不够,取 7 以上主循环耗时明显增加。滑动窗口长度 8 对应 8 个采样周期,如果采样间隔 500ms,相当于 4 秒的时间常数,响应会变迟钝,报警场景要相应缩短窗口。

5.2 故障排查表

调试阶段遇到的问题高度集中,下面这张表基本覆盖了大部分现场情况:

现象常见根因处理方式
数码管全亮或全灭共阴共阳接反、位选极性错段码表取反,检查位选驱动电平
某一位偏暗该位位选驱动电流不足换三极管或减小基极电阻
数码管有鬼影扫描时未消影切换位选前先把位选口置无效
ADC 读数恒为 0 或 255CS/CLK/DIO 接错、未释放数据线检查三线连接,读前先写 1
ADC 读数随机跳变参考电压不稳、数据线过长加基准源、缩短走线、加滤波
电导率读数持续下降电极极化、结垢改交流激励,定期清洗电极
浊度读数缓慢漂移光源老化、杂散光进入遮光处理,用标准液重新标定
上电不运行、烧录无效EA 接地、复位电路参数错EA 接 VCC,检查 RC 复位时间

还有一条不写在表里但值得单独说:电极线缆和晶振走线尽量远离,12MHz 的时钟信号很容易耦合进高阻抗的电极输入端,表现为读数随着程序运行节奏有规律地跳动。把电极前端做成屏蔽线、单点接地,往往比在软件里加十层滤波更有效。

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

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

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

立即咨询