ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

低成本单片机水质监测系统:pH、浊度、TDS、温度采集与串口报警

低成本单片机水质监测系统:pH、浊度、TDS、温度采集与串口报警 简介这份毕业设计文档以AT89C51单片机为核心围绕水中自由离子浓度与浑浊度的实时监测展开面向电子信息、自动化、环境工程等专业的本科毕业生及需要完成类似课题的开发者可用于选题参考、开题报告撰写与论文框架搭建。压缩包内为1个docx文件约1.38MB即完整毕业论文正文含中英文摘要、目录与章节内容。目前已有463人学习下载参考热度较高。文档系统梳理了我国水质监测背景、国内技术现状、监测意义与指标并讲解自由离子浓度与电导率的关系、浑浊度检测原理硬件部分涵盖AT89C51选型与最小系统、复位与晶振电路、传感器接口、A/D转换、LCD显示及GSM/GPRS或蓝牙通信电路还给出实时监控、自动化、可扩展与便携式设计等优势分析便于读者理解整体方案并借鉴写作与电路设计思路。1. 凌晨三点的溶氧告警基于单片机的水质监测系统要解决什么养殖户最怕的不是白天看见水色发浑而是凌晨三点溶氧掉到 2 mg/L 以下却没人知道。市面上一套带云平台的在线水质监测设备报价动辄上万而一个基于单片机的水质监测系统用 STC89C52RC 或 STM32F103 加四路探头物料成本能压到两三百元。它做的事很聚焦按固定节拍采 pH、浊度、温度、TDS 这几路信号本地做上下限判断超限就拉继电器启增氧泵或走串口报警同时把数据落到存储介质里。适合三类人做单片机课程设计或毕业设计的学生、需要多点位低成本部署的小型养殖与水培场景、以及想把模数混合采集链路完整走一遍的嵌入式新手。往下所有内容围绕一条主线怎么让探头读出来的数字可信怎么让单片机把可信的数字按时送出去。2. 探头选型与信号链pH、浊度、TDS、温度怎么接进单片机这一步选错后面代码怎么写都救不回来。水质监测系统的精度瓶颈从来不在单片机而在探头输出形态、调理电路和地线处理这三段。很多同学拿着现成的模块插上去读数跳动第一反应是程序滤波没做好实际情况往往是 pH 探头的高阻信号直接接进了 ADC 输入端被输入阻抗拉垮了。2.1 四类参数的探头输出形态对比先把常见四类探头的电气特性摆出来选型时对着这张表看接口。参数典型探头原理输出形态接口形式单片机侧处理pH玻璃电极内阻 10^8~10^9 Ω约 -59.16 mV/pH 25℃高阻模拟必须过阻抗变换运放浊度红外散射光光电二极管接收0~4.5 V 模拟量模拟分压后进 ADC需标定TDS双电极电导率交流激励0~2.3 V 模拟量模拟分压后进 ADC需温度补偿温度DS18B20 或 NTC数字 / 电阻单总线 / 模拟直接读寄存器或查表浊度和 TDS 模块的输出都带直流偏置别指望它从 0 V 起跳。实际接线前先用万用表量一次空载输出把偏置电压记下来标定时要减掉。2.2 pH 高阻信号的调理电路pH 电极等效于一个内阻上百兆欧的电压源直接接到 STC 系列单片机 10 位 ADC 的输入脚输入阻抗量级根本不在一个水平读数会被钳到几乎不动。常见做法是加一级电压跟随器运放选偏置电流小的型号用 CA3140、LMC6001 或专用 pH 变送板。跟随器输出再分压到 ADC 量程内例如 pH 变送输出 0~3.0 V 对应 pH 0~14先过 1/2 分压降到 0~1.5 V。// STC12C5A60S2 pH 变送模块单次 ADC 采样函数 // 采用 P1.0 作为 ADC0 输入通道 #include stc12c5a60s2.h #include intrins.h unsigned int adc_read(unsigned char ch) { ADC_CONTR 0x80 | ch; // 置位 ADC_POWER选择通道 ch _nop_(); _nop_(); _nop_(); // 等待内部模拟开关切换 ADC_CONTR | 0x08; // 置位 ADC_START启动一次转换 while (!(ADC_CONTR 0x10));// 轮询 ADC_FLAG等待转换结束 ADC_CONTR ~0x10; // 清零标志位 return ADC_RES * 4 ADC_RESL; // 10 位结果高 8 位左移补低位 }这段代码的关键在ADC_CONTR的三次写先开电源选通道再置 START最后清 FLAG。ADC_RES存高 8 位、ADC_RESL存低 2 位合成时高字节乘 4 再加低位不是简单拼接。漏掉启动后的空_nop_()等待第一次读数会偏小。2.3 DS18B20 单总线的接线要点温度用 DS18B20 防水探头最省心一线总线成本几块钱。数据脚必须接 4.7 kΩ 上拉到 3.3 V 或 5 V上拉电阻不接或阻值不对读数会固定成 85.0℃这是最常见的翻车现场。多个探头挂同一条总线时每一路都要有独立的上拉或者用 2.2 kΩ 折中。读取时序对延时敏感51 上建议用_nop_()拼微秒延时而不是空循环编译器优化等级一变空循环就废了。2.4 供电与地线把噪声压到 ADC 分辨率以下模拟探头和继电器共地是大忌。继电器吸合瞬间线圈反电动势会把地电平抬起来ADC 读数跟着跳十几个 LSB。做法是模拟部分和数字部分分开走线在电源入口单点连接继电器线圈两端并续流二极管线圈电源独立一路。模拟输入脚前面串 100 Ω 电阻、并 0.1 μF 到地做一个简单 RC 低通截止频率设在几百赫兹够滤掉开关噪声又不影响水质参数这种秒级变化。提示ADC 参考电压建议单独用一片低压差稳压器供给别和继电器共用一路 5 V。3. 主控最小系统与采集程序从定时器节拍到串口上传主控定下来采集程序的结构也就定了。这一章把最小系统、采样节拍、滤波和串口协议串起来写成能直接编译进 Keil 的骨架。3.1 STC89C52RC、STC12C5A60S2 与 STM32F103 的取舍型号内核ADC串口适用判断STC89C52RC8051无需外挂 ADC08091 路只要能显示精度要求低STC12C5A60S28051 增强8 路 10 位2 路推荐Keil C51 直接开发STM32F103C8T6Cortex-M312 位多通道3 路要多点位、要 DMA 时选如果只是做毕业设计STC12C5A60S2 是性价比最高的起点寄存器手册和 51 一脉相承ADC 内置不用再焊一片 ADC0809 和一堆地址线。想练 DMA、多路同步采集再上 STM32F103。3.2 用定时器 0 搭出 1 秒采样节拍水质参数变化慢1 秒采一轮足够采样节拍由定时器中断提供别在main里用软件延时那样串口收发会被拖住。// 定时器 0 模式 112 MHz 晶振50 ms 中断一次 #include stc12c5a60s2.h unsigned char tick 0; void timer0_init(void) { TMOD 0xF0; // 保留定时器 1 配置清定时器 0 位 TMOD | 0x01; // 定时器 0 工作在模式 116 位定时 TH0 0x3C; // 50 ms 初值高字节12T 模式 TL0 0xB0; // 50 ms 初值低字节 ET0 1; // 使能定时器 0 中断 EA 1; // 开总中断 TR0 1; // 启动定时器 } void timer0_isr(void) interrupt 1 { TH0 0x3C; // 必须重装初值模式 1 不自动重载 TL0 0xB0; if (tick 20) { // 20 × 50 ms 1 s tick 0; sample_flag 1; // 只置标志主循环里再采样 } }中断里只做计数和置标志实际采样放到主循环执行这是把采集和通信解耦的基本功。sample_flag被主循环检测到后清零再依次读四路数据。若把 ADC 采样、串口发送写进中断服务函数中断执行时间不确定容易和下一个中断撞车。3.3 滑动平均滤波与异常值剔除单次 ADC 读数上下跳几个 LSB 很正常用 8 点滑动平均压一下。但滑动平均对突发的野值无能为力所以先做一次限幅把超过前一均值 ±10% 的样本直接丢掉。#define WIN 8 unsigned int buf[WIN]; unsigned char idx 0; unsigned int filter(unsigned int newval) { unsigned long sum 0; unsigned char i; unsigned int avg; // 限幅偏离均值超过 10% 视为野值用均值替代 for (i 0; i WIN; i) sum buf[i]; avg sum / WIN; if (newval avg * 11 / 10 || newval avg * 9 / 10) newval avg; buf[idx] newval; idx (idx 1) % WIN; sum 0; for (i 0; i WIN; i) sum buf[i]; return sum / WIN; }WIN取 8 是折中太大响应变慢太小滤波无效。限幅比例 10% 是经验值如果探头本身噪声就大可以放宽到 20%但别再大否则真实的水质突变会被吃掉报警就迟了。3.4 串口帧协议把四路数据打包送出去四路数据一起发必须有帧结构否则上位机收到字节流不知道怎么切分。推荐帧头 0xAA 0x55 长度 数据 校验 帧尾 0x0D 0x0A。// 8 字节定长帧帧头2 pH2 温度2 状态1 校验1 void send_frame(unsigned int ph, int temp, unsigned char status) { unsigned char i, sum 0; unsigned char frame[8]; frame[0] 0xAA; frame[1] 0x55; frame[2] ph 8; // pH 高字节 frame[3] ph 0xFF; // pH 低字节 frame[4] (unsigned char)(temp 8); // 温度高字节有符号补码 frame[5] (unsigned char)(temp 0xFF); frame[6] status; // bit0~bit3 分别对应四路越限标志 for (i 0; i 7; i) sum frame[i]; // 前 7 字节累加和 frame[7] sum; for (i 0; i 8; i) { SBUF frame[i]; while (!TI); TI 0; } }status用一个字节的位来表示四路是否越限比发四个布尔量省字节上位机解析也简单。校验用前 7 字节累加和取低 8 位够用且计算量小51 上不需要更多。波特率建议 9600晶振 11.0592 MHz 才能整除用 12 MHz 会有累积误差。4. 校准与阈值判定让读数和真实值对得上原始 ADC 值只有相对意义校准之后才是水质参数。这一章讲两点校准、温度补偿、阈值迟滞和数据落盘怎么选。4.1 pH 的两点校准与一次线性拟合准备 pH 4.00 和 6.86 两瓶标准缓冲液把变送器输出 ADC 值分别记为adc_low和adc_high斜率 k 就是两点 pH 差除以 ADC 差。// pH 两点校准缓冲液 4.00 与 6.86 float ph_slope, ph_offset; void ph_calibrate(unsigned int adc_4, unsigned int adc_686) { // 斜率单位pH / ADC 计数 ph_slope (6.86f - 4.00f) / (float)(adc_686 - adc_4); // 以 6.86 点为基准求截距 ph_offset 6.86f - ph_slope * (float)adc_686; } float ph_value(unsigned int adc) { float v ph_slope * (float)adc ph_offset; return v; // 返回真实 pH 值 }两点校准的假设是探头在量程内线性pH 电极在 4~10 范围内基本成立。超过这个范围要三点校准或改用分段拟合。校准值建议存进 EEPROMAT24C02 就够掉电不丢换电极重新标一次即可。4.2 TDS 的温度补偿电导率随温度每升高 1℃ 大约变化 2%所以 TDS 必须做温度补偿否则夏天和冬天读出来的值能差三成。// TDS 温度补偿以 25℃ 为基准 float tds_value(float raw, float temp) { float comp 1.0f 0.02f * (temp - 25.0f); // 2%/℃ 补偿系数 return raw / comp; }raw是未经补偿的 TDS 读数temp来自 DS18B20。0.02 这个系数是水质行业的常用值淡水场景够用高盐水体要按实际标定修。4.3 阈值判定加迟滞避免继电器来回抖如果用if (ph 6.0) 开泵;这种写法pH 在 6.0 上下跳动时继电器会每秒吸放几十次触点很快烧掉。加一个迟滞带低于 6.0 才动作高于 6.3 才复位。#define PH_LOW_ON 6.0f #define PH_LOW_OFF 6.3f void check_ph(float ph, unsigned char *relay) { if (ph PH_LOW_ON) *relay 1; // 越下限吸合继电器 else if (ph PH_LOW_OFF) *relay 0; // 回到迟滞带上沿释放 // 落在 6.0~6.3 之间时保持上一次状态 }0.3 的迟滞窗口对 pH 合适浊度可以设大一些比如 5 NTU因为浊度本身非线性、抖动更大。4.4 数据落盘TF 卡还是 EEPROM存储介质容量接口51 上难度适用场景AT24C02 EEPROM256 BI²C低存校准参数、阈值W25Q 系列 Flash几 MBSPI中存几天的分钟级记录TF 卡 FATFSGB 级SPI高RAM 吃紧长期记录、要拷电脑51 单片机 RAM 只有几百字节跑 FATFS 很勉强堆栈一深就溢出。想存成表格形式的 CSV更实际的做法是用 STM32 跑 FATFS 写 TF 卡或者在 51 上定时把定长记录帧通过串口发给上位机由电脑存成表格。把校准参数和阈值放 AT24C02历史数据按需往外送是资源受限时的常见组合。5. 联调排错读数漂移、串口丢包与固件版本追溯联调阶段的问题靠改代码解决不了的多半在线路靠看现象解决不了的要靠日志。这一章给三个能立刻上手的定位手法。5.1 读数漂移的定位顺序先断探头让 ADC 输入短接到地看读数是否接近 0 且不跳。如果还跳问题在参考电压或地线去查模拟地和数字地的单点连接。接上探头再跳用万用表直流档量变送器输出如果万用表稳、单片机跳就是 ADC 前端或那一小段连线的问题重点查输入脚有没有串电阻、有没有加 RC。如果万用表也跳换一瓶标准液再测排除液体本身扰动。这个顺序能覆盖九成以上的漂移抱怨。5.2 串口丢包的三个常见原因第一是波特率失配12 MHz 晶振配 9600 波特率误差接近 8%超过 UART 容忍上限换成 11.0592 MHz 立刻好。第二是发送和中断抢TI标志把发送写进定时器中断里主循环又在发标志被两边清帧就断了。第三是上位机按固定长度收但帧里没有帧头同步丢一个字节后整体错位加帧头 0xAA 0x55 加累加校验就能自恢复。5.3 用DATE和TIME把编译时间编进固件改了半天代码却没下进去、拿错固件版本是调试期最消耗时间的乌龙。C51 和 Keil 都支持__DATE__和__TIME__两个预定义宏编译时自动展开成编译时刻的字符串把它编进一帧开机握手连上串口就能确认板上跑的到底是哪一版。// 开机发送固件版本帧便于确认真实烧录版本 void send_version(void) { char *d __DATE__; // 形如 Jun 12 2024 char *t __TIME__; // 形如 14:23:05 unsigned char i; SBUF 0xAA; while (!TI); TI 0; // 帧头 SBUF 0x55; while (!TI); TI 0; SBUF 0x01; while (!TI); TI 0; // 版本帧类型标识 for (i 0; d[i] ! \0; i) { SBUF d[i]; while (!TI); TI 0; } for (i 0; t[i] ! \0; i) { SBUF t[i]; while (!TI); TI 0; } }配合上位机脚本或串口助手的时间戳功能每次上电就能看到这一版是哪天几点编的。进阶一点的做法是把固件 CRC 也算进这一帧和版本字符串一起发烧录后比对一次避免下载器缓存导致实际写入的是旧镜像。校准参数读出来后再发一帧pH 斜率和截距一并打印现场换电极后不用拆壳就能确认参数写没写进去。本文还有配套的精品资源点击获取
返回列表