51单片机孵化环境温湿度监控系统设计与DHT11驱动实现
2026/9/12 11:46:42 网站建设 项目流程

简介:这款基于51单片机的孵化环境温湿度监控系统设计压缩包,面向单片机初学者、电子设计课程项目开发者,提供一套从硬件原理到软件调试的完整参考方案,可服务于课程设计、毕业设计或小型环境监控项目开发。资源共17个文件,压缩包仅100KB,涵盖Keil工程文件(.uvproj/.uvopt)、C语言源码(.c)、编译生成的HEX与LIST/M51中间文件,以及原理图截图和必看说明,可满足仿真、烧录与二次修改需求,也配套了工程备份文件供对照恢复。已有436人学习/下载。内容包含传感器数据采集、阈值判断与加热器/加湿器控制逻辑,并对LCD1602显示、串行通信等要点给出可运行代码;HEX文件方便直接烧录验证,代码注释配合原理图可快速理解初始化、采集、控制、显示主流程,适合结合DHT11/DHT22实际调试。建议读者先浏览必看说明和原理图,再按照模块阅读C代码,能够较快掌握51单片机在环境监控类项目中的典型用法。

1. 孵化环境温湿度监控:51单片机课程设计的完整闭环

禽蛋孵化对温湿度极其敏感,温度长期低于37℃会导致胚胎发育迟缓,超过39℃则可能直接致死;湿度低于45%时蛋内水分蒸发过快,影响气室正常形成。这套基于51单片机的孵化环境温湿度监控系统,正是围绕这两个核心参数做的闭环控制方案:DHT11采集温度和湿度,STC89C52或AT89S52读取数据后送到LCD1602显示,并通过继电器控制加热器和加湿器。对于电子信息工程和嵌入式方向的课设来说,这个题目几乎把所有基础外设都串起来了。如果你手里正好有这个源码包,下面从DHT11时序、LCD驱动、继电器控制到Proteus仿真和烧录排错,一条线讲完。

2. DHT11时序解析与51单片机单总线读取实现

2.1 DHT11的测量原理与40位数据帧

DHT11使用单总线协议,一条数据线负责命令和数据的双向传输。传感器上电后,单片机必须先发送开始信号:将数据线拉低至少18ms,然后释放。DHT11会回复80us低电平和80us高电平,随后连续输出40位数据。这40位按顺序是湿度整数、湿度小数、温度整数、温度小数、校验和,每项8位。DHT11的小数部分恒为0,所以很多程序直接忽略小数;DHT22则能读出0.1℃分辨率的真实小数,但成本更高。

单总线每一位的编解码依赖高电平持续时间。数据线先被拉低一段固定时间,随后释放;如果高电平持续26~28us表示逻辑0,持续70us左右表示逻辑1。51单片机没有硬件单总线控制器,必须用sbit模拟这个时序,因此代码质量取决于延时函数的精度。DHT11和DHT22的差异主要在精度和成本:DHT11精度±2℃、±5%RH,采样周期1s,价格只有几块钱,课设和常规孵化监控完全够用。

DHT11硬件接线很简单,但数据脚必须接上拉电阻,否则空闲状态无法稳定保持高电平。下表是标准接法。

DHT11引脚51单片机说明
VCC+5V供电,工作范围3.3V~5.5V
DATAP2.0单总线数据,10kΩ上拉到VCC
GNDGND共地连接

我一般建议数据引脚固定接P2.0,因为后续LCD1602和继电器会占用大量P0、P1、P2口,P2.0离继电器输出远,不容易受吸合时的电磁干扰。在Proteus仿真时,上拉电阻可以直接用RESPACK-8排阻,其中一个引脚接VCC。

2.2 开始信号、读取字节与校验

下面是一份在Keil C51中可直接编译的DHT11读取代码。它只用到寄存器和sbit位定义,不依赖特殊扩展库。

#include <reg51.h> #include <intrins.h> sbit DHT11 = P2^0; void delay_us(unsigned int t) { while (t--) { _nop_(); } } void delay_ms(unsigned int t) { unsigned int i, j; for (i = 0; i < t; i++) for (j = 0; j < 123; j++); } void DHT11_Start(void) { DHT11 = 1; delay_us(2); DHT11 = 0; delay_ms(20); // 拉低20ms,满足开始信号至少18ms要求 DHT11 = 1; // 释放总线,等待DHT11响应 delay_us(30); } unsigned char DHT11_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { while (DHT11 == 0); // 等待低电平结束,即数据位起始 delay_us(40); // 延时40us后采样 if (DHT11 == 1) dat |= (0x80 >> i); // 高电平时间长,判为1 while (DHT11 == 1); // 等待高电平结束,进入下一位 } return dat; }

这段代码的核心是delay_us(40)。逻辑1的高电平宽度约70us,逻辑0约28us,所以在高电平开始后40us处采样,只有逻辑1还维持在高,这样就区分了两种电平。dat |= (0x80 >> i)把当前位放进字节的对应位置。等待DHT11 == 1结束再进入下一位,保证每位时序对齐。

完整的读取函数需要先调用DHT11_Start(),然后检测传感器的响应电平。如果传感器没有拉低响应,说明接线或开始信号有问题,需要返回错误标志。

unsigned char DHT11_ReadData(unsigned char *dat) { unsigned char i, buf[5] = {0, 0, 0, 0, 0}; DHT11_Start(); if (DHT11 == 1) return 0; // 未检测到DHT11拉低响应 while (DHT11 == 0); // 等待80us低电平结束 while (DHT11 == 1); // 等待80us高电平结束 for (i = 0; i < 5; i++) buf[i] = DHT11_ReadByte(); if ((buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) { dat[0] = buf[0]; dat[1] = buf[1]; dat[2] = buf[2]; dat[3] = buf[3]; dat[4] = buf[4]; return 1; // 校验通过 } return 0; // 校验失败 }

调用后,dat[0]是湿度整数,dat[1]湿度小数,dat[2]温度整数,dat[3]温度小数,dat[4]校验和。DHT11的小数位没有实际参考价值,所以后面显示和控制逻辑可以只取dat[0]dat[2]。校验失败时不要直接丢弃数据,常见做法是连续读取三次,取出现次数最多的那一组,能明显降低偶发误码的影响。

2.3 延时函数在不同晶振下的误差处理

Keil C51的_nop_()在12MHz晶振下约等于1us,但整个delay_us循环还有函数调用和while判断的额外开销,所以实际延时比名义值偏长。如果单片机换成了11.0592MHz晶振,用同一份代码,40us的实际等待可能会变成45us以上。这不会导致DHT11完全失效,但会增加边界位误判概率,尤其是逻辑0容易被人为拉长到40us以上。

更稳妥的做法是使用定时器延时,但课设里大多数代码都用普通循环。我判断代码是否正常的经验是:连续读取两次,温度差不超过1℃、湿度差不超过1%,就说明时序基本正确。如果数值跳变剧烈,优先把delay_us(40)改成两个_nop_()或加一个空语句,而不是直接怀疑传感器损坏。另外,DHT11两次采样间隔不能小于1s,内部传感器完成一次测量需要一定时间,主循环里连续读会读到旧值,这个放到第4章结合主循环结构再展开。

3. LCD1602显示与继电器控制:从原理图到Keil C51工程

3.1 原理图接线与P0口上拉问题

资源包里的“原理图截图.png”是标准课设接法。LCD1602的控制线RS、RW、E接在P2口高位,数据线D0-D7接P0口。继电器控制端接在P1口,DHT11数据脚接P2.0,整体布局很清晰。

有一个细节必须单独提:P0口内部没有上拉电阻,是开漏输出。直接接LCD1602数据端时,输出高电平能力很弱,显示屏容易出现“没有字符、乱码、一个亮行”等表象。原理图上通常给P0口加10kΩ排阻到VCC。如果你的板子没有排阻,至少要把P0口所有引脚在初始化时置1,再外接上拉电阻提供高电平。

下面是一份和原理图对应的引脚分配表,方便抄板和改程序时核对。

模块信号51引脚说明
DHT11DATAP2.010kΩ上拉
LCD1602RSP2.50=指令 1=数据
LCD1602RWP2.60=写 1=读
LCD1602EP2.7高电平使能,下降沿锁存
LCD1602D0-D7P0.0-P0.7并联10kΩ排阻上拉
继电器IN1P1.0控制加热器
继电器IN2P1.1控制加湿器

继电器模块如果是低电平触发,控制逻辑要反过来。实物用PNP三极管驱动时,基极低电平导通,继电器线圈通电;NPN三极管则相反。看原理图时重点看三极管型号,不要让程序和硬件电平反着来。

3.2 LCD1602驱动:8线并行读写

LCD1602大多用8线并行方式,因为51的P0口正好8位。驱动时序不复杂:E拉高期间,RS指定命令还是数据,RW固定为写0,数据线准备好后,E的下降沿把数据锁存进LCD控制器。初始化要先等上电稳定,再发功能设置、显示开关、清屏。

下面是一份精简的8线驱动代码,接口与上一节表格一致。

#include <reg51.h> #define LCD_DB P0 sbit LCD_RS = P2^5; sbit LCD_RW = P2^6; sbit LCD_E = P2^7; void LcdDelay(unsigned char t) { unsigned char i; while (t--) for (i = 0; i < 120; i++); } void LcdWriteCmd(unsigned char cmd) { LCD_E = 0; LCD_RS = 0; // 命令模式 LCD_RW = 0; // 写模式 LCD_DB = cmd; LcdDelay(2); LCD_E = 1; LcdDelay(2); LCD_E = 0; // 下降沿锁存 } void LcdWriteData(unsigned char dat) { LCD_E = 0; LCD_RS = 1; // 数据模式 LCD_RW = 0; LCD_DB = dat; LcdDelay(2); LCD_E = 1; LcdDelay(2); LCD_E = 0; } void LcdInit(void) { LcdDelay(20); // 上电等待 LcdWriteCmd(0x38); // 8位模式,双行,5x8点阵 LcdWriteCmd(0x0C); // 开显示,不显示光标 LcdWriteCmd(0x06); // 输入模式:写一个字符后地址加1 LcdWriteCmd(0x01); // 清屏 LcdDelay(5); }

这些指令里,0x38是LCD1602的核心配置,漏掉它屏幕可能只能显示一排方块。0x01清屏需要时间,所以后面加了延时。每次写之前把E拉低,是为了避免初始化过程中GPIO电平抖动导致误写入。

显示字符串时,先设置DDRAM地址,再逐字符写入数据。首行地址从0x00开始,第二行从0x40开始,因为DDRAM基地址是0x80,所以第二行首地址是0x80+0x40=0xC0。

void LcdShowString(unsigned char addr, unsigned char *str) { LcdWriteCmd(0x80 | addr); // 设置DDRAM地址,0x00为第一行 while (*str) { LcdWriteData(*str++); } }

调用时LcdShowString(0x00, "Temp:")会在左上角显示标题,LcdShowString(0x40, "Hum:")会在第二行显示。如果要显示动态温度值,可以用sprintf把数字转成字符串,但keil C51的sprintf比较占代码空间,课设里更常见的是像第5章那样用0x30加上数字位直接转ASCII字符。

3.3 继电器驱动与加热、加湿输出

继电器部分的关键不是代码,而是驱动电路。51单片机IO口不能直接驱动5V继电器线圈,常规做法是使用PNP三极管作为开关:基极接IO口,发射极接VCC,集电极接继电器线圈,线圈两端并联续流二极管。PNP三极管高电平截止、低电平导通,所以程序里控制加热器时通常置0有效。如果使用成品低电平触发继电器模块,模块内部已经完成三极管和续流二极管,照着手册接线即可。

控制代码可以封装为独立函数,传入温度和湿度,输出加热与加湿两个IO状态。

sbit HEATER = P1^0; sbit HUMIDIFIER = P1^1; void ControlOutput(unsigned char temp, unsigned char hum) { if (temp < 37) HEATER = 0; // 低电平导通PNP,加热器启动 else if (temp > 39) HEATER = 1; // 高电平截止,加热器停止 if (hum < 50) HUMIDIFIER = 0; // 加湿器启动 else if (hum > 60) HUMIDIFIER = 1; }

这种写法在中间区间不修改IO,继电器能保持上一状态,但如果是简单阈值判断,温度在37℃附近波动时仍可能频繁通断。更平滑的做法是加回差,这个放到第4章结合仿真讲。实物调试时要注意一个现象:继电器吸合瞬间,LCD数值可能跳一个字,这通常是继电器线圈干扰DHT11数据线。解决方法是继电器供电和单片机供电分开,数据线用双绞线缩短长度。

3.4 Keil工程文件识别与编译输出

源码包里看到的1.uvproj、1.uvopt、1.c、1.hex是一套完整的Keil工程。1.uvproj是工程文件,用Keil uVision5打开;1.uvopt是窗口布局配置,删掉不影响编译;1.c是主程序源文件;1.hex是编译生成的烧录文件。1.OBJ、1.LST、1.M51都是编译中间产物,不是人看的源码。

打开工程后,按F7编译,出现“0 Error(s)”就说明编译通过。如果只拿到源码没有工程文件,也可以新建一个工程,把1.c添加进去,然后设置Atmel AT89C52或STC89C52RC芯片型号。编译前务必在Options for Target的Output选项卡勾选Create HEX File,否则不会生成新的hex。

4. 阈值控制与Proteus仿真联调

4.1 孵化温湿度阈值设置与回差逻辑

孵化过程中温度和湿度不能只做单点控制。常见禽蛋孵化参考条件是:温度设定37.8℃,允许波动范围37℃~39℃;湿度设定55%,允许波动范围50%~60%。如果代码采用“低于下限开启、高于上限关闭”的双阈值,中间区间保持现状,继电器就不会在临界点疯狂翻转。这就是回差控制的思路,也叫滞回控制。

下面代码把阈值写在宏定义里,调整方便,也方便答辩时演示参数变化。

#define TEMP_LOW 37 #define TEMP_HIGH 39 #define HUM_LOW 50 #define HUM_HIGH 60 void SmartControl(unsigned char temp, unsigned char hum) { if (temp < TEMP_LOW) { HEATER = 0; // 低电平导通继电器 } else if (temp > TEMP_HIGH) { HEATER = 1; } if (hum < HUM_LOW) { HUMIDIFIER = 0; } else if (hum > HUM_HIGH) { HUMIDIFIER = 1; } }

有人会问:温度在37℃到39℃之间时,这段代码没有对HEATER赋值,继电器会怎样?答案是保持原状态,因为HEATER对应的是IO锁存器,函数返回后引脚电平不变。所以“低于下限开、高于上限关、中间保持”,已经天然形成回差。如果采用普通单阈值,比如低于37℃开、高于37℃关,温度在临界抖动时继电器就会快速通断,触点和加热器寿命都会受影响。

如果你想让控制更精细,可以用温度小数位参与判断。DHT11的小数位恒为0,实际需要DHT22或者放大10倍的虚拟温度值。课设答辩时能说清这个局限,反而比强行显示小数位更专业。

4.2 Proteus仿真:元件查找与hex加载

资源包没有附带Proteus工程文件,但按原理图可以很快搭出仿真。Proteus 8以上版本在元件库搜索AT89C51、LM016L、DHT11、RELAY即可。注意DHT11在Proteus里的模型名称可能因版本不同有差异,有些版本叫DHT11,有些版本用AM2301模型代替。

仿真元件Proteus关键词用途
单片机AT89C51与STC89C52引脚兼容
温湿度传感器DHT11读取温湿度
显示屏LM016L对应LCD1602
继电器RELAY模拟加热/加湿开关
排阻RESPACK-8P0口上拉

在Proteus中双击AT89C51,在Program File一栏选择Keil生成的1.hex,然后运行仿真。正常情况LCD会显示当前温湿度,当温度低于37℃时加热灯亮,高于39℃时加热灯灭。仿真环境里DHT11模型默认输出固定数值,所以多数人验证的是显示和控制逻辑,而不是真实读取协议。想验证DHT11读取代码本身,可以用示波器观察数据脚波形,或者用信号发生器模拟单总线数据。

我一般会先在Proteus里把阈值调低一点,用虚拟温度变量模拟,比如让温度从35℃缓慢升到41℃,观察继电器吸合和释放的边界是否在37℃和39℃附近。这样能快速确认回差逻辑是否生效,再烧到实物上。

4.3 主循环中的采样节拍与状态刷新

DHT11两次采样之间必须间隔至少1s,否则传感器内部测量未完成,读出的数据不会更新。如果主循环写成下面这样,读取频率会远高于1s,实际温度会卡在固定值,或者偶尔读到旧数据。

// 错误示例:主循环里连续读 while (1) { DHT11_ReadData(dat); LcdShowString(0x00, "T:"); // ... }

正确做法是用定时器产生时间片。定时器0中断每10ms进入一次,累积到100次就是1s,主循环只在标志位置位时做一次完整采集、显示和控制。下面是基础框架。

volatile unsigned int tick_10ms = 0; bit flag_1s = 0; void Timer0_ISR(void) interrupt 1 { TH0 = 0xDC; // 10ms重装载,12MHz晶振 TL0 = 0x00; tick_10ms++; if (tick_10ms >= 100) { tick_10ms = 0; flag_1s = 1; // 1秒定时到 } } void main(void) { unsigned char dat[5] = {0, 0, 0, 0, 0}; LcdInit(); TMOD = 0x01; TH0 = 0xDC; TL0 = 0x00; ET0 = 1; EA = 1; TR0 = 1; while (1) { if (flag_1s) { flag_1s = 0; if (DHT11_ReadData(dat)) { SmartControl(dat[2], dat[0]); // 更新LCD显示 } } } }

定时器初值0xDC00是12MHz晶振下10ms溢出的标准值。如果晶振是11.0592MHz,TH0和TL0要重算,否则1s标志会提前或延后,导致DHT11读取频率超出限制。主循环中while(1)不阻塞,1s之内还能做按键扫描、蜂鸣器报警等其他任务,这是嵌入式系统最基础的“时间片轮转”结构,也是这个课设从“能跑”到“结构合理”的分水岭。

5. 烧录hex与现场排错:解决传感器误读和控制混乱

5.1 STC单片机的串口下载流程

资源包里的1.hex就是最终烧录文件。以STC89C52RC为例,使用USB转TTL模块连接单片机P3.0和P3.1,注意交叉:USB转TTL的TXD接单片机P3.0,RXD接P3.1,GND共地。打开STC-ISP软件,选择STC89C52RC,加载1.hex,波特率可以先选2400,点击下载后再给单片机板上电。如果提示握手失败,先检查串口号是否被占用,再确认USB转TTL模块是否插紧。

5.2 三种现场故障的排查顺序

现象优先排查点常见原因
LCD有背光无字符对比度电位器、P0上拉对比度电压异常,或P0口没有上拉电阻
温湿度显示0或255DHT11供电、上拉电阻、Data线Data线接错,或延时函数与晶振不匹配
继电器不动作三极管基极电阻、模块电平触发逻辑与三极管类型不匹配

DHT11误读是最难定位的。如果温度一直显示0,先测量Data引脚电压,空闲状态应该在3.3V以上。如果只有0.5V,说明上拉电阻焊接有问题;如果始终为高,说明DHT11没有响应,可能是数据引脚定义和原理图不一致。用LED闪烁判断程序是否卡在while(DHT11==0),是在没有示波器时最直接的办法。在读取函数里加入超时计数,也能防止传感器损坏后程序死锁。

5.3 串口输出调试:把现场数据送出来

当LCD显示和控制逻辑同时出问题时,我一般会屏蔽LCD代码,用串口把DHT11原始读到的数据发到上位机,先确认传感器本身是否工作。这样能把问题范围缩小一半。

void Uart_Init(void) { SCON = 0x50; // 模式1:8位UART,允许接收 TMOD |= 0x20; // 定时器1作为波特率发生器 TH1 = 0xFD; // 9600bps @ 11.0592MHz TL1 = 0xFD; TR1 = 1; } void Uart_SendChar(unsigned char c) { SBUF = c; while (TI == 0); TI = 0; } void Uart_SendString(unsigned char *s) { while (*s) { Uart_SendChar(*s++); } }

调用时把读到的数据转换成可见ASCII字符发送,串口助手连续收到“T:37 H:54”这样的数据,说明传感器和单片机通信正常,问题在显示或控制环节;如果收不到或全是FF,则问题在传感器或单总线时序。这个串口调试技巧同样适用于其他51单片机课设,比反复看LCD定位快得多。

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

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

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

立即咨询