简介:基于51单片机的滴灌控制系统是一份适合毕业设计参考的完整工程包,面向单片机初学者或需要快速搭建自动灌溉方案的开发者。系统以51系列单片机为核心,通过PT100测温、湿度传感器或电位器模拟湿度输入,支持按键设定阈值,超温或缺水时自动启动滴灌,并提供定时滴灌与手动开关功能,逻辑清晰便于移植。压缩包内共38个文件,其中包含Keil工程源码(main.c、lcd1602.c/h等)、Proteus仿真文件(DSN/PWI)、Altium Designer原理图(SchDoc)以及说明文档,整体体积约276KB,可直接打开仿真调试或参考原理图进行PCB布局。目前已有291人学习下载。整套资料兼顾源码、仿真与硬件设计,适合用于课程设计、毕业设计或项目入门,帮助用户理解传感器采集、阈值判断和定时控制等核心流程。
1. 从阈值到定时:51单片机滴灌控制系统的构成思路
做智慧农业项目时,很多团队第一反应是先上云、连 WiFi,但我见过的大棚改造项目里,断网断电反而是常态。这套基于 51 单片机的滴灌控制系统,走的是“本地闭环”的老路子:PT100 测温度、模拟量湿度传感器或仿真电位器监测湿度、按键设定阈值、继电器控制滴灌,完全不需要上位机干预。它同时支持阈值自动启动和定时滴灌两种工作模式,还预留了手动开关定时功能的入口。对硬件基础薄弱的学生,或者想快速验证水肥控制逻辑的工程师,这套方案的吸附力在于 Keil 程序、Proteus 仿真、AD 原理图三件套齐全,从逻辑验证到 PCB 画板能直接衔接,资料完整度在同类题里算拔尖的。
2. 传感链路:PT100 电桥与 TLC1543 十位 ADC 采集
这一章解决的是“现场信号怎么进单片机”的问题。湿度传感器模拟量和 PT100 电阻信号都不适合直接用单片机的比较器处理,必须经过桥式测量、放大和 ADC 量化三个环节,才能变成 51 单片机能够判断的数字量。
2.1 为什么选 PT100 加 TLC1543 这个组合
PT100 是铂电阻温度计,0℃ 时电阻 100Ω,温度每升高 1℃ 电阻大约增加 0.385Ω,线性段的重复性和抗氧性都比热敏电阻好不少。我做温室温度采集时习惯用电桥把电阻变化转成差分电压:电桥由 R1、R2 两个 10kΩ 电阻和一个 100Ω 基准电阻 R3 组成,PT100 作为电桥另一臂。当温度是 0℃ 时 PT100 等于 100Ω,电桥输出接近 0V;温度升到 100℃ 时 PT100 约 138.51Ω,电桥输出电压约 0.4V 左右。这个电压经过 LM358 运放放大 10 倍后进入 ADC 的通道 0。
TLC1543 是 TI 的 11 通道 10 位串行 ADC,相比传统 51 单片机外接的 ADC0809,它占用引脚少,P3 口四个 I/O 就能完成数据吞吐。对于滴灌控制这种秒级甚至分钟级响应的系统,10 位分辨率完全够用,不需要上 12 位 ADC。ADC 的 VCC 基准直接用 5V,输入电压范围 0 到 5V,误差控制在 0.2% 以内,对温度和湿度的判据已经足够宽裕。
在 Proteus 仿真里,PT100 通常用可变电阻模拟,湿度传感器用电位器分压模拟,这样做的目的是绕过实际传感器型号差异,先验证控制逻辑本身。信号映射关系我按下表整理:
| 信号来源 | 物理量 | 信号转换 | TLC1543 通道 | 阀值含义 |
|---|---|---|---|---|
| PT100 电桥放大 | 0℃ 到 100℃ | 约 0V 至 4V | 通道 0 | temp_set,超过启动滴灌 |
| 湿度传感器/电位器 | 0% 至 100%RH | 0V 至 5V | 通道 1 | humi_set,低于启动滴灌 |
2.2 TLC1543 的 SPI 读写时序与 C51 实现
TLC1543 的通信本质上是一个 14 位串行帧:先写 4 位通道地址,再读 10 位转换结果。片选 CS 拉低后,地址位从 ADDRESS 引脚逐位移入,每个时钟上升沿移入一位;随后数据从 DATA OUT 引脚逐位输出,下降沿取样。整个传输过程 51 单片机用普通 I/O 口模拟即可,不用占用硬件 SPI 外设。
// tlc1543.c TLC1543 驱动,使用 P3 口模拟时序 sbit tlc_cs = P3^5; // 片选,低电平有效 sbit tlc_clk = P3^6; // 串行时钟 sbit tlc_addr = P3^7; // 通道地址输入 sbit tlc_dout = P3^4; // 转换结果输出 unsigned int TLC1543_Read(unsigned char channel) { unsigned char i; unsigned int value = 0; tlc_cs = 1; tlc_clk = 0; tlc_cs = 0; // 拉低片选,启动一次转换周期 // 送入 4 位通道地址,MSB 在前 for (i = 0; i < 4; i++) { if (channel & 0x08) tlc_addr = 1; else tlc_addr = 0; tlc_clk = 1; // 地址位在时钟上升沿锁存 tlc_clk = 0; channel <<= 1; } // 读取 10 位转换结果,MSB 在前 for (i = 0; i < 10; i++) { tlc_clk = 1; // 数据在时钟上升沿移出 value <<= 1; if (tlc_dout) value |= 0x01; tlc_clk = 0; } tlc_cs = 1; // 片选拉高,结束本次转换 return (value & 0x03FF); // 只保留低 10 位有效数据 }这段代码中,channel & 0x08的作用是取出通道号的最高位,因为通道值从 0 到 10,换算成二进制最多占用低 4 位。写入时每处理完一位就把 channel 左移,保证下一位进入最高位位置。读取数据时value <<= 1先移位再拼接tlc_dout的当前值,这样 10 个时钟后 value 高位在前,最终与0x03FF做与运算截断成 10 位结果。
提示:写入地址的 4 个时钟必须是“先置数据、再拉时钟”,读数据的 10 个时钟恰好相反,“先拉时钟、再判电平”。顺序颠倒会导致通道错位或数据错位。
2.3 湿度通道与仿真电位器的接法
湿度传感器如果选用模拟量输出的土壤湿度探针,输出一般是 0V 到 3.3V 或 0V 到 5V 的直流信号,直接接到 TLC1543 的通道 1 即可。Proteus 仿真中我喜欢用 RV1 电位器模拟湿度变化,比如将电位器两端分别接 5V 和 GND,中间抽头接 TLC1543 通道 1,旋转电位器就能改变湿度读数。这样做的好处是排查问题时可以精确复现某个湿度值,而不用反复往仿真土壤里“浇水”。
湿度读数与实际物理量之间的换算公式放在主程序里处理,ADC 原始值乘以基准电压再除以 1023,就得到电压。实际项目中湿度传感器的输出电压与相对湿度往往不是严格线性,但大部分国产探针在 10% 到 90% 区间内线性度可以接受,直接用线性换算不会有明显问题。若需要提高精度,可以按传感器数据手册做两点标定,这部分在最后一章展开。
2.4 继电器与滴灌执行端的驱动
51 单片机的 P2 口灌电流能力不足以直接驱动继电器线圈,常见做法是用一个 8050 三极管或者 ULN2003 达林顿管作为开关,继电器线圈并联 1N4148 续流二极管,防止断电瞬间反电动势打坏管脚。我一般把 P2.5 作为滴灌输出,置 1 时三极管导通、继电器吸合,电磁阀水路的两个触点闭合开始滴灌;置 0 时停止滴灌。执行端带水泵也是同理,只是水泵电流更大,继电器触点容量要至少留出 2 倍余量。
3. 调度逻辑:定时器节拍、阈值比较与滴灌状态机
程序设计是整个项目的灵魂,也是评审老师最关注的模块。这一章从定时器节拍说起,把显示、按键、阈值比较和定时滴灌串成一棵主循环逻辑树。
3.1 用定时器 0 构建 1ms 系统节拍
51 单片机实现多任务调度不靠实时操作系统,而是靠定时器中断产生时间基准。晶振 12MHz 时机器周期为 1μs,定时器 0 工作在方式 1 的 16 位计数模式,每计数 1000 次就是 1ms。初始值计算为 65536 减 1000,得到 0xFC18,写入 TH0 和 TL0 后每次溢出重新装载。
// main.c 定时器0初始化,12MHz 晶振,产生 1ms 节拍 void Timer0_Init(void) { TMOD &= 0xF0; // 低四位清零,保留高四位原有配置 TMOD |= 0x01; // T0 方式1:16位定时 TH0 = 0xFC; // 1ms 重装值高字节 TL0 = 0x18; // 1ms 重装值低字节 ET0 = 1; // 允许 T0 中断 EA = 1; // 开放总中断 TR0 = 1; // 启动定时器 } unsigned int tick_1ms = 0; // 1ms 累加计数 unsigned int sec_tick = 0; // 秒计数 void Timer0_ISR(void) interrupt 1 { TH0 = 0xFC; // 重新装载初值 TL0 = 0x18; tick_1ms++; if (tick_1ms >= 1000) { tick_1ms = 0; sec_tick++; } }TMOD &= 0xF0与TMOD |= 0x01是工程上惯用的“读改写”方式,确保不破坏定时器 1 的配置。引入sec_tick后,后续的定时滴灌只需要比较秒级变量,不需要做复杂的除法运算。系统里最长的定时间隔如果达到几十分钟,sec_tick仍用 16 位 unsigned int 就会溢出,必须把计时变量按分钟累加或者换成 32 位。
3.2 ADC 值换算与 LCD1602 显示刷新
ADC 转换得到的是 0 到 1023 的原始整数,主程序里先把它换算成温度和湿度。换算方式很简单,通道 0 的 0V 至 4V 映射到 0℃ 至 100℃,通道 1 的 0V 至 5V 映射到 0% 到 100%RH。LCD1602 显示采用典型四线或八线接法,初始化时先设置显示模式,再写要显示的行首地址,最后逐字符写入待显示内容。为了避免显示刷新占用太多 CPU,我通常把屏幕更新放到主循环的 200ms 分支里,不影响中断节拍。
显示第一行放实时数据,第二行放工作模式与输出状态,例如T:25.5C H:60%和AUTO TMR:ON PUMP。这类信息对调试最有价值,用户不需要关心 ADC 原始值。lcd1602.c 中我会把写命令、写数据、写字符串三个函数隔离开,方便上层主程序直接调用,而不是在 lcd1602.c 内部塞业务逻辑。
3.3 自动阈值滴灌判定逻辑
自动滴灌的规则来自用户设定的两个阈值,温度上限 temp_set 和湿度下限 humi_set。主循环每次采样后执行一次比较,温度超过上限或者湿度低于下限,滴灌输出开启;两个条件都不满足时关闭。基础版本直接改输出引脚电平即可:
// main.c 主循环中自动阈值判定 temp_value = TLC1543_Read(0); // 读取温度通道 humi_value = TLC1543_Read(1); // 读取湿度通道 // ADC值换算成物理量,单位为0.1℃与1%RH temp_v = temp_value * 100 / 256; // 0~4V 映射到 0~100℃ humi_v = humi_value * 100 / 1023; // 0~5V 映射到 0~100% if ((temp_v > temp_set) || (humi_v < humi_set)) Pump = 1; // 启动滴灌 else Pump = 0; // 停止滴灌这段代码里的temp_v是实际计算后的温度值,temp_set是按键设定的阈值,两者都用 0.1℃ 做单位可以避免浮点运算。直接用浮点数在 51 单片机上会拖慢中断响应,采集模块对实时性要求不高,但整数运算能让主循环执行时间更稳定。
3.4 定时滴灌实现与手动开关
定时滴灌与自动阈值是两套互不干扰的触发源,按键提供一个软开关 timer_enable 来控制是否启用定时。用户设定 interval_set 为两次滴灌的间隔秒数,duration_set 为每次滴灌持续秒数。主循环用 sec_tick 做减法计时,到点后打开继电器并进入持续时间倒计时:
// main.c 定时滴灌时间片控制 if (timer_enable) // 手动打开定时功能 { if (sec_tick >= interval_cnt) // 到达间隔时间 { interval_cnt = sec_tick + interval_set; pump_remain = duration_set; // 设定本次滴灌时长 } } if (pump_remain > 0) // 还在滴灌持续期内 { Pump = 1; pump_remain--; } else { Pump = 0; }interval_cnt每次用“当前秒数加上间隔”而不是简单自加,好处是即使主循环偶尔被按键或者显示刷新延迟,也不容易累积误差。若同时开启自动阈值模式,两套逻辑可以并行,取 OR 关系,即任何一路来源触发都让水泵启动。手动关闭定时功能时,只要把 timer_enable 置 0,并清空 pump_remain,系统立刻回到离开自动判定的工作方式。
4. Keil 构建到 Proteus 仿真:把 HEX 跑起来
拿到压缩包之后,最快验证方式是先编译生成 main.hex,再把它烧进 Proteus 的 51 单片机模型里看波形和继电器动作。这一章把从 Keil 工程到 Proteus 仿真的操作路径串起来。
4.1 Keil 工程文件组织与常用构建命令
工程中主要源文件可以按下表归类,我在接手别人的类似代码时会先确认每个文件的职责:
| 文件 | 作用 | 关键依赖 |
|---|---|---|
| main.c | 主循环、状态调度、阈值判定、按键处理 | 调用 LCD 与 ADC 驱动 |
| lcd1602.c / lcd1602.h | LCD1602 初始化与字符显示 | P0 数据口、P2.0 到 P2.2 控制线 |
| tlc1543.c / tlc1543.h | TLC1543 读写驱动 | P3.4 到 P3.7 |
| start.a51 | Keil 自动生成的启动文件 | 不需要改动 |
| main.hex | 编译产物,加载到 Proteus | 由 uvproj 生成 |
| main.uvproj | Keil 工程文件,双击直接打开 | uVision4 以上 |
Kile 工程在编译前需要确认三件事:Target 选项卡里晶振频率设为 12MHz、Output 选项卡勾选 Create HEX File、C51 编译器 Memory Model 选 Small。这些配置直接影响 HEX 文件能否被 Proteus 正确识别。若不想频繁点击 Keil 界面,也可以用命令行方式批量构建:
"C:\Keil_v5\UV4\UV4.exe" -b .\main.uvproj -j0 -o .\build_log.txt参数-b表示执行构建,-j0让编译过程不弹出 GUI 窗口,-o将编译日志输出到 build_log.txt。编译成功后检查最后一行是否有0 Error(s) 0 Warning(s),如果报错,先看 TLC1543.c 里的 sbit 声明是否与 main.c 中 P3 引脚的赋值冲突。这个项目里 main.hex 是最终结果,Proteus 里加载的一定是这个文件,而不是 OBJ 或者 build_log。
4.2 Proteus 仿真电路的搭建要点
Proteus 8 打开仿真.DSN 后,首先确认单片机晶振电路和复位电路是否完整。如果只是单独放置一个芯片而不加 33pF 电容和晶振,程序经常出现不运行或者 LCD1602 黑屏。AT89C51 的 EA 引脚必须接高电平,否则单片机会从外部程序存储器取指,仿真直接跑飞。
传感器仿真部分,PT100 用电位器 RV2 模拟,湿度用电位器 RV1 模拟。继电器模型选择 Proteus 的 RELAY 类器件,线圈两端并联虚拟电压表,方便观察吸合状态。加载 HEX 文件的方法是双击单片机模型,在 Program File 一栏选择 main.hex,Clock Frequency 设为 12MHz,然后点击左下角运行按钮。
4.3 仿真验证与输出判定表
运行仿真后,旋转温度电位器和湿度电位器就能观察系统响应。为了快速验证阈值逻辑,我习惯先通过按键把温度上限设为 30℃,湿度下限设为 40%,然后手动改变两个电位器的分压。系统应有的输出行为见下表:
| 温度读数 | 湿度读数 | 自动阈值判断 | 继电器/水泵 |
|---|---|---|---|
| 28℃ | 45% | 温度未超、湿度未低 | 关闭 |
| 32℃ | 45% | 温度超限 | 吸合开启 |
| 28℃ | 32% | 湿度低于下限 | 吸合开启 |
| 32℃ | 32% | 双重超限 | 吸合开启 |
仿真中还要观察 LCD 刷新是否正常,特别容易出现的问题是 LCD1602 字符乱码。多数情况是初始化时序里的 15ms、4.1ms、100μs 延时没有给足,解决方法是把初始化函数中每个延时都开到 15ms 以上,宁可慢一些也不能让 LCD 控制器没准备好就写数据。仿真通过后,再打开 AD 原理图画 PCB 时,信号链路的互联关系就有了明确落点。
5. 边界条件调试:继电器抖动、ADC 噪声与按键冲突
代码在 Proteus 里跑通只是开始,真机或者更复杂的仿真环境会把一堆边界问题暴露出来:传感器输出在阈值附近上下浮动、按键按下时显示卡顿、继电器频繁通断。这一章的内容就是把这类耐久性问题一次讲透。
5.1 用迟滞比较消除继电器抖动
直接套用 3.3 节的“温度大于阈值就开、小于阈值就关”,在现场极其容易被传感器噪声折磨——温度在 30℃ 上下波动 0.2℃ 时,继电器会以秒为单位反复吸合断开,电磁阀线圈发热严重。工程上通用解法是引入迟滞,开启条件不变,关闭条件改成“低于阈值减回差”。温度和湿度各留一个死区窗口:
// main.c 迟滞比较:温度与湿度各带滞回区 #define HYST_TEMP 5 // 0.5℃,即 5 个 0.1℃ 单位 #define HYST_HUMI 3 // 3%RH bit drip_flag = 0; if (!drip_flag) { if ((temp_v > temp_set) || (humi_v < humi_set)) drip_flag = 1; // 满足启动条件,进入滴灌 } else { if ((temp_v < temp_set - HYST_TEMP) && (humi_v > humi_set + HYST_HUMI)) drip_flag = 0; // 必须明显恢复才关闭 } Pump = drip_flag;这里的技巧是“开要果断,关要犹豫”。只要任一条件越界,系统马上开启滴灌;关闭时必须温度和湿度都回到安全区间并留出余量,才允许停止。HYST 值也不是越大越好,0.5℃ 和 3%RH 是我在温室环境里试过的经验值,如果再大,土壤会存在明显的干湿死区,滴灌会滞后。
注意:迟滞逻辑不要写到定时器中断里,中断函数应尽量短,判断留给主循环。否则中断里做比较运算会拉长中断响应时间,影响 1ms 节拍的准确性。
5.2 对 ADC 采样值做五点中值滤波
TLC1543 在 Proteus 里几乎没有噪声,但接到真实湿度传感器后,传感器供电纹波和电磁阀动作产生的干扰会叠加进模拟信号,导致显示的湿度值跳变。常用方案是取连续 5 次采样值排序,用中间值作为最终结果,兼顾响应速度和去脉冲能力。
// tlc1543.c 五点中值滤波,返回稳定后的采样值 unsigned int ADCRead_Filter(unsigned char channel) { unsigned char i, j; unsigned int buf[5]; unsigned int temp; for (i = 0; i < 5; i++) buf[i] = TLC1543_Read(channel); // 连续采样 5 次 for (i = 0; i < 4; i++) // 简单排序,取中值 { for (j = i + 1; j < 5; j++) { if (buf[j] < buf[i]) { temp = buf[i]; buf[i] = buf[j]; buf[j] = temp; } } } return buf[2]; // 第三个数即中值 }中值滤波对 50Hz 工频干扰和空间脉冲干扰有很强的抑制能力,但代价是采样周期拉长。每次 TLC1543 读通道大约需要 14 个时钟周期,5 次采集就是 70 个时钟周期,对 12MHz 晶振约 1ms 以内,完全不影响滴灌控制。如果觉得显示数据仍然抖动,不要盲目增加采样次数,先把滤出后的信号用电压表量一次,确认是模拟输入噪声还是运放震荡。
5.3 按键边沿检测与显示刷新拆分
按键扫描最容易犯的错误是用 while 等待按键释放,导致按下按键时主循环阻塞,温度采样和继电器判断全部暂停。我维护项目时会把按键扫描改成边沿触发,只在按下瞬间执行一次功能,响应后立即退出扫描函数。
// main.c 按键边沿检测,避免按下时阻塞主循环 sbit KEY_SET = P1^0; // 设置键,低电平有效 void Key_Scan(void) { static bit key_down = 0; // 记录前一次状态 if (KEY_SET == 0) { if (!key_down) // 只有从未按下变为按下时触发 { key_down = 1; menu_step++; // 切换菜单或确认当前参数 } } else { key_down = 0; } }关键点是把按键电平状态暂存在key_down静态变量中,而不是靠循环去等。每次主循环调用一次 Key_Scan,按键按住多久都只会执行一次加法,主循环其他任务不卡顿。LCD 刷新同理,用一个refresh_timer累计 200ms 再更新界面,把显示操作从高频采样路径里拆出来,这样继电器动作时屏幕亮度就不会闪。
6. 进阶:PT100 分度表校准与阈值参数掉电保存
基础逻辑调通后,这套系统离落地还差两个实用性提升:一是把 PT100 的非线性修正掉,二是让用户设置的阈值在断电后不丢失。
PT100 的电阻与温度在 0℃ 到 100℃ 范围内近似线性,但严格来说仍有约 0.4℃ 的拟合偏差。要修正这 0.4℃,最稳的办法不是套数学公式,而是用分度表加线性插值。先把几个整十度点的标准电阻值存进一个查表数组:
| 温度/℃ | PT100 标称电阻/Ω |
|---|---|
| 0 | 100.00 |
| 25 | 109.73 |
| 50 | 119.40 |
| 75 | 129.02 |
| 100 | 138.51 |
程序里把 ADC 逆推出来的电阻值 R 与表中相邻两点比较,落在哪一段就按比例插值出温度。例如测得电阻 124.21Ω,落在 119.40 与 129.02 之间,温度就是 50 加上(124.21-119.40)/(129.02-119.40)*25,约为 67.1℃。这样处理以后,整个量程内误差可以压到 0.1℃ 以内,而且完全使用整数查表,不引入浮点库。Proteus 里用固定电阻组合验证时,这个差值并不明显,真机用高精度万用表标定时会有感知。
阈值掉电保存推荐用 51 单片机片内 IAP/ISP 模拟 EEPROM 的方式,STC 系列可以在程序运行时把参数写入 Flash 的指定地址段。保存时不要只写一个字节,而是写一个带校验的帧,格式建议为:
// 参数帧:帧头 + 温度阈值 + 湿度阈值 + 间隔秒数 + 校验和 #define FRAME_HEADER 0xA5 #define PARAM_ADDR 0x2000 unsigned char param_buf[5] = { FRAME_HEADER, temp_set, humi_set, interval_set & 0xFF, 0x00 // 校验和位置 }; param_buf[4] = param_buf[0] + param_buf[1] + param_buf[2] + param_buf[3];上电读参数时先检查首字节是 0xA5,再重新求和比对校验字节,两者任一不满足就直接使用默认阈值。这个技巧能避免新芯片初次上电读出 0xFF 时的乱值,也能在 Flash 损坏时自动回到安全配置,是一个在民用产品里都能直接用上的朴素的保护措施。
本文还有配套的精品资源,点击获取