基于51单片机与DHT11的四路继电器温湿度控制系统设计
简介一份基于51单片机与DHT11温湿度传感器的四路继电器控制设计资料适合电子、自动化相关专业学生及单片机爱好者进行课程设计或项目开发。方案集成AT24C02芯片实现上下限掉电存储四个按键完成设置、加、减、确定操作便于现场调节阈值LCD1602实时显示温湿度并同步提示冷、热、干、湿状态。当环境参数超限时蜂鸣器闪烁报警报警信号灯点亮四路继电器自动吸合可驱动通风机、抽湿机、加热器、制冷器、加湿器及报警器等外部设备报警开关支持手动关闭或打开。压缩包为rar格式大小约35.72MB内含程序源码、原理图文件及仿真文件可直接用于Keil、Proteus等常见开发环境。目前已有1706人学习下载整套资料结构清晰既包含完整代码与硬件连接设计也提供仿真验证思路能帮助学习者快速理解温湿度控制逻辑与继电器驱动电路并在此基础上进行扩展与二次开发。1. 四路继电器温湿度控制器51单片机课程设计里容易被低估的闭环细节DHT11温湿度传感器在51单片机课程设计里出现频率极高但大多数作品做到LCD1602上显示“温度湿度”就收尾了。机房机柜、烘干箱、育苗温室真正需要的不是读数而是读数越界后谁去开加热器、谁去断通风机。这套基于51单片机DHT11设计的四路继电器温湿度控制把采集、显示、按键设置、AT24C02掉电存储和继电器输出完整串在一起程序、原理图和Proteus仿真文件都齐全。四个按键负责阈值调节超限时蜂鸣器报警、报警灯点亮、对应继电器吸合直接驱动制冷器、加热器、加湿器或除湿机。适合正在做51温湿度控制相关设计的人也适合从“只显示”升级到“能闭环动作”的现场应用。2. 系统框架与关键器件DHT11时序、51单片机引脚规划和继电器驱动电路2.1 51单片机引脚规划显示、按键、继电器在P0/P1/P2/P3口的分配先看整体数据流DHT11测到的温湿度进入51单片机LCD1602显示四个按键修改阈值AT24C02保存阈值主循环比较后控制四路继电器超限时蜂鸣器和信号灯给出声光提示。这个结构把显示、输入、存储、执行四件事分开每一步出问题都能单独定位。引脚分配我按下面这张表来布局P0口完全避开避免开漏输出需要外接上拉排阻的问题功能模块引脚说明DHT11 DATAP3.7单总线数据线外接4.7kΩ上拉到VCCLCD1602 RSP2.0寄存器选择LCD1602 ENP2.1使能信号下降沿锁存LCD1602 DB4-DB7P2.2-P2.54线模式比8线省4个IOAT24C02 SCL/SDAP2.6/P2.7I2C总线各接4.7kΩ上拉继电器1-4P1.0-P1.3经ULN2003或SS9013驱动蜂鸣器P1.4低电平触发或三极管驱动报警信号灯P1.5超限时常亮按键 设置/加/减/确定P3.0-P3.3按下为低电平软件消抖报警开关P3.4拨码开关ON时低电平P1口在51里是准双向IO输出逻辑电平可靠用来作为四路继电器的控制口比较合适。P3口是双功能口按键接在P3.0-P3.3不影响串口和中断因为本设计没有用到这些复用功能。LCD1602用4线模式RS、EN加DB4-DB7一共6根线比8线模式省出4个IO对于后续想扩展外部功能的场景这4个IO可以留给红外遥控或者串口调试。P0口为什么避开它是开漏结构输出高电平时需要外部上拉电阻很多仿真是用P0口接LCD1602一接不上拉就花屏。这种接线在仿真里勉强能动实物上就很被动。用P2口接1602等于从源头避免一个常见坑。2.2 DHT11传感器单总线时序与温湿度读取代码DHT11是单总线器件数据线只有一根主机和传感器分时占用。一次完整的读取过程分四步主机拉低总线至少18ms发起起始信号释放总线等待DHT11响应传感器拉低80us再拉高80us表示握手成功然后连续输出40bit数据。这40bit的排列格式固定湿度整数、湿度小数、温度整数、温度小数、校验和各8位校验和是前四个字节之和的低8位不一致就得丢弃本轮数据。注意数据位“0”和“1”的区分不是靠电平高低而是靠高电平的持续时间。每个bit都是50us低电平开场后面跟着的高电平若持续26-28us是“0”持续70us左右是“1”。所以要读到准确的数据采样点必须放在高电平启动后35-40us的位置这时“0”已经变低而“1”还保持高电平。// 12MHz晶振下机器周期1us sbit DHT11_PIN P3^7; void delay_us(unsigned int us) { while (us--) { _nop_(); _nop_(); } } unsigned char dht11_read_byte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { while (DHT11_PIN 0); // 跳过50us低电平 delay_us(40); // 移到高电平中段采样 if (DHT11_PIN 1) dat (dat 1) | 0x01; else dat (dat 1); while (DHT11_PIN 1); // 等待剩余高电平结束 } return dat; } unsigned char dht11_read(float *temp, float *hum) { unsigned char buf[5], i; DHT11_PIN 0; delay_ms(25); // 起始信号规范要求≥18ms DHT11_PIN 1; delay_us(30); // 拉高释放总线 if (DHT11_PIN 0) { // DHT11响应80us低80us高 while (DHT11_PIN 0); while (DHT11_PIN 1); for (i 0; i 5; i) buf[i] dht11_read_byte(); if ((buf[0] buf[1] buf[2] buf[3]) buf[4]) { *hum buf[0] buf[1] * 0.1f; // 整数位小数位 *temp buf[2] buf[3] * 0.1f; return 0; // 0读取成功 } return 1; // 校验失败数据丢弃 } return 2; // 无响应传感器未接好 }起始信号写成25ms而不是18ms是为了在传感器电源波动时留余量有些DHT11模块上还并了滤波电容上电瞬间总线状态不稳定拉低时间太短会握手失败。返回值设计成0/1/2三种上层控制逻辑根据返回值决定继续用旧数据还是进入降级保护不要把读取失败当正常数据处理。delay_us里的_nop_数量依赖晶振频率12MHz下机器周期1us两个_nop_加循环跳转接近2us左右这套参数实测可用。2.3 继电器驱动电路P1口不能直接驱动线圈的接线与参数5V继电器线圈电阻大约70Ω吸合电流约70mA51单片机任何一个IO口都提供不了这么大电流。P1口输出高电平时驱动能力更弱所以必须加一级放大常见做法是三极管或达林顿管驱动方案接线方式说明SS9013三极管基极接1kΩ电阻到P1.x发射极接地集电极接继电器线圈负端线圈两端反并1N4007适合单路成本低ULN2003IN1-IN4接P1.0-P1.3COM接继电器供电正极OUT接线圈负端一路芯片带7路自带续流二极管最适合四路继电器ULN2003内部是达林顿管对输入高电平时对应输出低电平把线圈负端拉低形成电流回路。它的COM引脚接线圈供电正极内部钳位二极管直接吸收反电动势不用再额外并1N4007。如果手头是继电器模块而不是裸继电器模块上通常会自带光耦隔离和驱动三极管P1直接接模块的信号端即可但要注意模块输入是高电平触发还是低电平触发接反了表现为“上电即吸合”或“怎么都不吸合”。提示ULN2003输入端与P1口直连即可但COM端一定要接继电器线圈供电正极接错会导致输出端无法拉低继电器完全不动作。用SS9013裸管时基极电阻不能省。1kΩ电阻把基极电流限制在几毫安量级同时保证三极管饱和导通。继电器线圈两端必须反并续流二极管IN4007线圈断电瞬间会产生反向电动势去掉这个二极管三极管集电极很容易被击穿仿真里体现不出来实物上换三极管换到怀疑人生。对外接设备时继电器只相当于一个开关接线口诀是公共端COM接电源进线、常开触点NO接设备电源端。控制220V设备时触点额定电流要留出2倍以上余量。仿真里用LED代替负载即可实物测试先用小功率灯泡验证通断再上真负载。3. AT24C02掉电存储与四按键校验参数设置模块的软硬件配合3.1 为什么把阈值放在AT24C02而不是51单片机片内Flash51单片机内部虽然也有EEPROMSTC系列叫DataFlash但不同型号的IAP/ISP操作寄存器不同代码里跨型号移植时要反复改SFR。AT24C02是标准I2C接口的EEPROM256字节100万次擦写寿命存4个阈值加1个标志字节绰绰有余。另一个实际原因I2C协议在后续驱动DS1302时钟、扩展传感器时都要用把I2C底层调通等于给这个设计留了扩展口。存储布局按下面这张表规划地址内容默认值取值范围0x00温度上限28℃0-990x01温度下限16℃0-990x02湿度上限70%RH0-990x03湿度下限40%RH0-990x10初始化标志0x55非0x55则写默认值0x10地址放的标志位是关键。AT24C02出厂数据全是0xFF如果上电直接读0x00-0x03会把温度上限读成255。所以上电时先读0x10等于0x55说明已经存过合法参数直接读阈值否则写入默认值并置标志。这个“标志字节默认值”的做法在掉电存储里很通用比单纯判断阈值是否在合理范围更可靠。3.2 I2C时序与AT24C02读写代码I2C总线上SCL和SDA都需要上拉电阻51的P2口内部有上拉但外部再接4.7k电阻可以让上升沿更陡抗干扰更强。AT24C02的器件地址是7位A0/A1/A2接地时为0xA0最后一位是读写方向写为0读为1所以读地址是0xA1。#define I2C_SCL P2_6 #define I2C_SDA P2_7 void i2c_start(void) { I2C_SDA 1; I2C_SCL 1; delay_us(5); I2C_SDA 0; // SCL高电平期间SDA下降沿起始 delay_us(5); I2C_SCL 0; } void i2c_stop(void) { I2C_SDA 0; I2C_SCL 1; delay_us(5); I2C_SDA 1; // SCL高电平期间SDA上升沿停止 delay_us(5); } bit i2c_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { I2C_SDA (dat 0x80) ? 1 : 0; dat 1; I2C_SCL 1; delay_us(2); I2C_SCL 0; delay_us(2); } I2C_SDA 1; // 释放SDA接收从机ACK I2C_SCL 1; delay_us(2); if (I2C_SDA 1) return 0; // 无ACK写失败 I2C_SCL 0; return 1; } unsigned char i2c_read_byte(void) { unsigned char i, dat 0; I2C_SDA 1; // 释放SDA读取数据 for (i 0; i 8; i) { I2C_SCL 1; delay_us(2); dat (dat 1) | I2C_SDA; I2C_SCL 0; delay_us(2); } return dat; } void at24c02_write_byte(unsigned char addr, unsigned char dat) { i2c_start(); i2c_write_byte(0xA0); // 器件地址A2/A1/A0接地 i2c_write_byte(addr); // 字地址 i2c_write_byte(dat); // 写入数据 i2c_stop(); delay_ms(10); // 等待内部写周期完成最长5ms } unsigned char at24c02_read_byte(unsigned char addr) { unsigned char dat; i2c_start(); i2c_write_byte(0xA0); i2c_write_byte(addr); i2c_start(); // 重复起始条件 i2c_write_byte(0xA1); // 进入读模式 dat i2c_read_byte(); // 单字节读取主机最后不发ACK i2c_stop(); return dat; }i2c_write_byte返回值用于判断ACKAT24C02在收到每个字节后会拉低SDA表示应答如果总线异常或者地址错误SDA保持高电平这时要放弃本次操作而不是继续往下读。at24c02_write_byte末尾的delay_ms(10)是给内部写周期用的AT24C02规格书给出的最大写周期是5ms写完后立即读会拿到旧数据。3.3 四个按键的状态机与上下限一致性校验四个按键定义为设置、加、减、确定。设置键在正常模式和设置模式之间切换加/减键修改当前项确定键保存并退出。用一个枚举变量表示当前状态比在循环里用多个flag组合判断清晰得多#define MODE_NORMAL 0 #define MODE_SET_TH 1 // 设置温度上限 #define MODE_SET_TL 2 // 设置温度下限 #define MODE_SET_HH 3 // 设置湿度上限 #define MODE_SET_HL 4 // 设置湿度下限 void key_proc(unsigned char key) { if (mode MODE_NORMAL) { if (key KEY_SET) mode MODE_SET_TH; return; } if (key KEY_SET) { // 在设置项之间切换 mode; if (mode MODE_SET_HL) mode MODE_NORMAL; return; } if (key KEY_OK) { // 确定保存并退出 save_params(); mode MODE_NORMAL; return; } if (key KEY_ADD || key KEY_SUB) { change_param(key); // 内部做上下限大小校验 } }change_param里必须要做一致性校验温度下限永远不能大于等于温度上限湿度同理。否则会出现“上限16、下限28”这种反逻辑配置控制算法会直接进入错误状态。我一般是在按键处理时直接拦截比如当前在设置温度下限、按加号时如果temp_low 1 temp_high就拒绝执行同时让蜂鸣器短响一声提示操作无效。长按连加的处理也很实用定时器每10ms扫描一次按键记录同一个键连续按下的次数超过500ms后进入连发模式每50ms自动触发一次加/减操作。这样把温度从16℃调到28℃不用按120下按键也不会因为手抖多调几度。扫描函数放在定时器中断里按键检测本身不阻塞主循环。3.4 LCD1602在设置模式下的显示切换正常模式下LCD1602第一行显示当前温度和湿度格式为“T:25.3C H:60%RH”第二行显示设备状态比如“Heat:ON Cool:OFF”。进入设置模式后第一行显示四组阈值的当前值第二行用箭头指示正在修改哪一项void lcd_show_setting(void) { lcd_write_cmd(0x80); lcd_show_string(TH:28 TL:16 HH:70 HL:40); lcd_write_cmd(0xC0); if (mode MODE_SET_TH) lcd_show_string(Set Temp-High); else if (mode MODE_SET_TL) lcd_show_string(Set Temp-Low ); else if (mode MODE_SET_HH) lcd_show_string(Set Hum-High ); else if (mode MODE_SET_HL) lcd_show_string(Set Hum-Low ); lcd_write_cmd(0x0F); // 打开光标并闪烁 }0x0F是LCD1602的显示控制指令开显示、开光标、光标闪烁。退出设置模式时执行lcd_write_cmd(0x0C)关闭光标。这个细节容易被忽略如果不关光标正常显示时一直有个方块在第二行闪。LCD1602的4线驱动代码在很多51单片机开发板例程里都有现成的参照写命令和写数据两个函数抽出来复用即可使用时注意P2口的RS、EN、DB4-DB7要与驱动函数里的引脚定义一致。4. 四路继电器联动控制算法与报警逻辑滞回、互斥与降级保护4.1 温度上下限天然构成滞回区间避免继电器频繁启停温度控制如果只用单一阈值比如“超过28℃就制冷”温度会在阈值附近上下波动继电器跟着频繁吸合释放触点寿命急剧缩短。这个设计用上限和下限两个值构造滞回区间温度降到下限以下才启动加热升到上限以上才启动制冷中间区域保持原状态不动。// 温度通道滞回控制temp_low下限temp_high上限 if (temp temp_low) { heat_on 1; cool_on 0; } else if (temp temp_high) { heat_on 0; cool_on 1; } // temp_low temp temp_high 时保持原状态这是滞回的关键容易写错的地方是最后加一个else { heat_on 0; cool_on 0; }这样每次温度回到上下限之间时设备全部关闭但此时如果实际温度更靠近下限加热器关闭后温度很快再次跌破下限形成振荡。正确做法就是上面这样只在越界时更新状态区间内不做任何操作让设备的开关状态带有“惯性”。按键设置时也要保证最小间隔。我在save_params里加一个限制temp_high - temp_low 3或者hum_high - hum_low 3时拒绝保存并恢复默认值。因为滞回区间太小即使有上下限现场设备的启停频率依然会偏高。4.2 四路控制字组装与加热/制冷、加湿/除湿互斥四路继电器对应加热器、制冷器、加湿器、除湿机在程序里用四个宏定义控制位最后通过位运算组装成一个字节写入P1口通道触发条件动作加热器温度 下限继电器吸合制冷器温度 上限继电器吸合加湿器湿度 下限继电器吸合除湿机湿度 上限继电器吸合#define RELAY_HEAT 0x01 // P1.0 #define RELAY_COOL 0x02 // P1.1 #define RELAY_HUM 0x04 // P1.2 #define RELAY_DEHUM 0x08 // P1.3 unsigned char relay_ctrl 0; if (heat_on) relay_ctrl | RELAY_HEAT; if (cool_on) relay_ctrl | RELAY_COOL; if (humid_on) relay_ctrl | RELAY_HUM; if (dehum_on) relay_ctrl | RELAY_DEHUM; // 互斥保护加热/制冷、加湿/除湿不允许同时吸合 if ((relay_ctrl RELAY_HEAT) (relay_ctrl RELAY_COOL)) relay_ctrl ~RELAY_COOL; if ((relay_ctrl RELAY_HUM) (relay_ctrl RELAY_DEHUM)) relay_ctrl ~RELAY_DEHUM; P1 (P1 0xF0) | (relay_ctrl 0x0F);位运算组装的好处是四路互不干扰每路的开关状态独立新增一个通道只需要加一个宏和一条if语句。互斥保护是写控制逻辑时必须加的温度和湿度在正常工况下不会同时超上限和下限但传感器数据在异常跳变时可能瞬间满足两个条件如果没有互斥加热器和制冷器同时吸合轻则浪费电重则损坏设备。这里的优先级是保留加热、关断制冷加湿和除湿冲突时保留加湿、关断除湿具体优先级可以根据现场设备的危险性调整。P1 (P1 0xF0) | (relay_ctrl 0x0F)这一行把P1口高四位原样保留只修改低四位。因为P1.4接着蜂鸣器、P1.5接着报警灯如果直接P1 relay_ctrl会把蜂鸣器和报警灯的状态一起冲掉。4.3 蜂鸣器闪烁报警与报警开关的应用边界报警逻辑有两层判断先是“是否超限”再是“报警开关是否打开”。注意这里有个容易误解的设计报警开关只控制蜂鸣器和报警灯不控制继电器动作。也就是说用户关掉报警声音后温度超过上限时制冷器依然会吸合只是不再吵人。如果把继电器和报警绑在一起用户嫌吵把报警关了现场设备也跟着停止保护这是很危险的。if (ALARM_SW 0) { // 报警开关ON低电平有效 if (temp temp_high || temp temp_low || hum hum_high || hum hum_low) { alarm_flag 1; } else { alarm_flag 0; } } else { alarm_flag 0; } void Timer0_ISR(void) interrupt 1 { TH0 0xFC; TL0 0x18; // 12MHz下重装1ms ms_count; if (ms_count 500) { // 达到500ms ms_count 0; BEEP alarm_flag ? ~BEEP : 0; // 闪烁持续翻转 } if (alarm_flag) LED_ALARM 1; else LED_ALARM 0; }TH00xFC; TL00x18是12MHz晶振下定时器0定时1ms的初值65536-1000645360xFC18。蜂鸣器用翻转方式而不是延时阻塞方式因为延时会让主循环停住按键扫描和DHT11刷新都会被卡死。500ms翻转一次人耳听起来是“滴-滴-滴”的断续声比长鸣更容易引起注意。如果要用1Hz或者更慢的节奏改ms_count的比较值即可。4.4 DHT11连续失败时的继电器降级保护DHT11在总线接触不良、电源波动或者时序被干扰时会出现无响应或者校验失败。如果此时保持上一帧数据继续判断温度卡在超限值上继电器可能一直吸合如果直接按0℃处理又会瞬间触发加热。这两种情况都不可取。工程做法是引入连续失败计数if (dht11_read(temp, hum) ! 0) { fail_cnt; if (fail_cnt 3) { relay_ctrl 0; // 关闭所有继电器 alarm_flag 0; // 不按超限逻辑误报 BEEP 1; // 用长鸣提示传感器异常 lcd_show_error(); // LCD显示 Sensor ERR } } else { fail_cnt 0; fail_cnt 清零恢复正常控制 }连续失败3次才动作是因为DHT11单次读取最长约30ms3次失败在100ms内完成不会让系统长时间处于失控状态又避免了偶发的一次握手失败直接切断所有设备。继电器全部关闭是保守策略现场设备停在安全状态等待人工检查传感器。这个降级逻辑在课程设计答辩里很加分因为它体现了“控制系统在异常工况下如何收尾”的工程意识。5. Proteus仿真到实物移植DHT11时序、LCD1602和继电器驱动排错5.1 Proteus仿真工程里的模型设置与常见问题在Proteus里搭建这个工程时DHT11、LCD1602、AT24C02都有现成模型但有几个默认设置不对就会“点了不反应”。排查时对照下面这张表现象原因处理LCD1602一上电全黑块V0端口没接对比度电位器用10k电位器中间抽头接V0调节对比度DHT11始终返回无响应主机起始信号不足18ms拉低时间改为25ms仿真能跑但实物乱跳Keil优化等级过高延时函数所在文件优化等级改为0继电器仿真不动作驱动端COM没接电源正极ULN2003的COM接继电器供电正极Proteus里的DHT11模型对起始信号时长比较敏感25ms拉低是实测比较稳妥的区间。LCD1602的V0不接对比度电位器时仿真模型也可能显示黑块加一个10k电位器把中间抽头电压调到0.4-0.6V左右字符就能显示出来。单片机的Crystal Frequency属性要双击芯片检查确认和Keil工程里选的12MHz一致否则代码里的delay_us和定时器初值全部偏掉。5.2 Keil编译器优化等级对时序的影响Keil C51的优化等级从0到9默认工程可能开到8甚至9。对DHT11这类依赖软件延时的单总线协议优化等级过高时编译器会调整循环结构delay_us里的空循环可能被部分折叠实际延时比预期短很多导致采样点落在数据位的高电平之前读出的数据全是0。我一般把延时函数和DHT11驱动单独放在一个.c文件里在Options for File中把该文件的Optimization改为0其它文件保持默认优化。这样既保证关键时序又不牺牲整个工程的代码密度。5.3 实物接线中的继电器供电与上电顺序实物接线的第一原则是继电器线圈供电和单片机逻辑电源分开。一个5V继电器吸合瞬间约70mA四个同时吸合接近300mA再加上单片机、LCD1602背光和DHT11普通线性稳压器很容易过热。接法上继电器用独立5V电源GND与单片机系统共地220V负载走继电器触点和5V控制线分区走线。上电顺序也讲究先给单片机系统供电等程序运行起来、P1口电平稳定后再给继电器负载电路上电。如果两个电源同时开单片机上电复位那几十毫秒内P1口电平不确定继电器可能瞬间误吸合一下。最后留一个排查技巧在Proteus里打开虚拟示波器探头接DHT11的DATA引脚触发方式设为下降沿。正常运行时会看到一次25ms低电平加一串us级脉冲把这轮波形和DHT11数据手册对照数每个bit的高电平宽度27us左右的是070us左右的是1。只要这条波形对得上DHT11采集链路的问题就全部排除了。本文还有配套的精品资源点击获取