简介:这份资源面向电子工程、嵌入式入门与课程设计人群,提供一套基于STC89C51单片机的双通道DHT11实时温湿度显示系统完整工程。系统以STC89C51为控制核心,通过单总线读取两路DHT11温湿度数据,经时序解析后由LCD1602液晶实时显示,并配套按键模块,覆盖传感器采集、字符屏驱动与人机交互等典型环节。压缩包共22个文件,约68KB,包含C语言源码与头文件、Keil工程及编译产物、HEX烧录文件、硬件模块代码和说明文本,目录按HARDWARE、LCD、KEY、USER等模块划分,便于对照理解工程结构。目前已有1372人学习下载。读者可据此获得可直接编译烧录的参考工程、模块化代码组织方式与调试排错思路,适合作为单片机课程设计、毕业设计或嵌入式入门练手案例,帮助快速掌握传感器驱动与显示控制的完整流程。
1. 双通道 DHT11 温湿度系统:为什么老单片机反而更适合做实时显示
很多人第一次看到“基于 STC89C51 的双通道 DHT11 实时温湿度显示系统”这个标题,第一反应是:都什么年代了,怎么还在用 51 单片机?但如果你真的在工厂车间、农业大棚、仓库或者机房做过环境监测,就会发现一个反直觉的事实——越是需要长期稳定运行、不需要联网、不需要复杂算法的场景,STC89C51 这类老芯片反而越省心。它便宜、抗干扰能力强、代码量小、不需要操作系统,上电就跑,断电再来也不丢状态。
这个系统要解决的核心问题很具体:同时采集两个位置的温度和湿度,在 LCD1602 上实时刷新显示。双通道的意义在于,很多场景下你关心的不是“当前温度是多少”,而是“两个位置的温差和湿度差是多少”。比如一个密闭机柜内外、一个温室大棚的南北两端、一个仓库的入口和深处。单点测量只能告诉你一个数字,双通道才能让你看出趋势和差异。
适合读这篇的人有三类:一是正在做单片机课程设计或毕业设计的学生,需要一套能跑通、能讲清楚、能复现的方案;二是刚转行做嵌入式开发的工程师,想找一个完整的小项目练手;三是在实际工作中需要快速搭建环境监测原型的从业者。接下来的内容会从硬件连接、DHT11 时序、LCD1602 驱动、双通道调度策略到实际调试中会遇到的坑,一步步拆开讲。
2. 硬件选型与最小系统搭建:为什么是 STC89C51 加两颗 DHT11
2.1 主控芯片的选型理由与最小系统电路
STC89C51 是 8051 内核的经典增强型单片机,相比原版 8051,它内置了更大的 Flash 和 RAM,支持串口下载,不需要专用编程器。对于这个项目来说,它的资源刚好够用:两颗 DHT11 各占一个 IO 口,LCD1602 用 4 位或 8 位并行接口占用 6 到 11 个 IO,剩下的 IO 还可以接按键或者蜂鸣器做报警扩展。
最小系统需要的东西不多:一颗 STC89C51、一个 11.0592MHz 或 12MHz 的晶振、两个 30pF 左右的起振电容、一个复位电路(10uF 电容加 10K 电阻)、一个电源滤波电容。晶振选 11.0592MHz 的好处是串口波特率更准,虽然这个项目不一定用串口,但如果你后续想加串口上传数据,这个频率会省去很多麻烦。
电源部分要注意:DHT11 的工作电压是 3.3V 到 5.5V,STC89C51 是 5V 系统,所以直接共用 5V 供电没问题。但如果你用 USB 供电,要注意 USB 口的电流输出能力,两颗 DHT11 加上 LCD1602 背光,总电流大概在 30mA 到 50mA 之间,一般的 USB 口都能带动。
2.2 DHT11 的引脚定义与上拉电阻的取值
DHT11 有两种封装:三针模块和四针裸传感器。三针模块已经集成了上拉电阻,直接插线就行;四针裸传感器需要自己接。四针的引脚定义是:VCC、DATA、NC(空脚)、GND。注意不同厂家的排列顺序可能不一样,拿到手先用万用表测一下,别凭记忆接线。
DATA 引脚必须接一个上拉电阻,典型值是 4.7K 到 10K。这个电阻的作用是在总线空闲时把电平拉高,因为 DHT11 的数据线是开漏输出,没有上拉电阻的话,总线会浮空,单片机读到的全是随机值。我一般用 4.7K,通信成功率比 10K 高一些,尤其是在线缆较长的时候。
如果你要把 DHT11 放在离单片机较远的地方,比如一米以上,建议用屏蔽线,并且在 DATA 和 GND 之间加一个 100nF 的电容做滤波。线缆越长,分布电容越大,上拉电阻就要越小,但太小会增加功耗,4.7K 是一个比较平衡的值。
2.3 LCD1602 的 4 位接口接法与对比度调节
LCD1602 支持 8 位和 4 位两种并行接口模式。8 位模式占用 8 个数据口,传输速度快,但浪费 IO;4 位模式只用 4 个数据口,分两次传一个字节,速度慢一半,但对于这个项目来说完全够用。我一般用 4 位模式,省下来的 IO 可以接按键或者继电器。
4 位模式的接线是:LCD 的 D4 到 D7 接单片机的 P2.4 到 P2.7,RS 接 P2.0,RW 接 P2.1,E 接 P2.2。RW 引脚其实可以接地,因为大多数情况下我们只写不读,接地可以省一个 IO。但如果你要用忙标志判断,就需要接 IO。我一般接 IO,因为忙标志判断比延时更可靠。
对比度调节用一个 10K 的电位器,一端接 VCC,一端接 GND,中间抽头接 LCD 的 V0。调节的时候慢慢转,直到字符清晰为止。如果怎么调都不清晰,检查一下背光是否正常,背光不亮的话,字符是看不见的。
3. DHT11 单总线时序拆解:从启动信号到 40 位数据的完整读取
3.1 启动信号与响应信号的时序要求
DHT11 的通信协议是单总线,主机和从机轮流控制数据线。一次完整的通信过程是:主机拉低总线至少 18ms,然后释放,从机检测到后拉低总线 80us 作为响应,再拉高 80us,然后开始传输数据。
这里的关键参数是:主机拉低时间不能少于 18ms,否则 DHT11 可能不响应;从机响应信号的低电平时间是 80us,高电平时间也是 80us。如果你的单片机晶振是 12MHz,一个机器周期是 1us,用延时函数可以比较精确地控制这些时间。
// 主机启动信号:拉低至少 18ms void DHT11_Start(void) { DHT11_PIN = 0; // 拉低数据线 Delay_ms(20); // 延时 20ms,留余量 DHT11_PIN = 1; // 释放总线 Delay_us(30); // 等待 20-40us,让从机拉低响应 }这段代码的逻辑是:先把数据线拉低 20ms,确保 DHT11 能检测到启动信号;然后释放总线,延时 30us 后开始检测从机的响应信号。延时 30us 是因为从机检测到启动信号后,会在 20us 到 40us 内拉低总线,如果延时太短,可能还没等到响应就误判了。
3.2 40 位数据的位判定与校验和计算
DHT11 一次传输 40 位数据,分成 5 个字节:湿度整数、湿度小数、温度整数、温度小数、校验和。校验和等于前四个字节相加的低 8 位。对于 DHT11 来说,小数部分通常都是 0,因为它的精度只有 1%RH 和 1℃。
每一位数据的判定方法是:从机先拉低总线 50us 表示位开始,然后拉高总线,高电平持续 26us 到 28us 表示数据 0,持续 70us 表示数据 1。所以主机在检测到低电平结束后,延时 40us 再读总线电平,如果还是高电平,就是数据 1,否则是数据 0。
// 读取一个字节 unsigned char DHT11_ReadByte(void) { unsigned char i, byte = 0; for (i = 0; i < 8; i++) { while (!DHT11_PIN); // 等待 50us 低电平结束 Delay_us(40); // 延时 40us,跳过数据 0 的高电平 byte <<= 1; // 左移一位 if (DHT11_PIN) { // 如果还是高电平,说明是数据 1 byte |= 1; while (DHT11_PIN); // 等待数据 1 的高电平结束 } } return byte; }这段代码的核心是延时 40us 后的采样点。数据 0 的高电平只有 26us 到 28us,延时 40us 后已经变成低电平;数据 1 的高电平有 70us,延时 40us 后还是高电平。这个时间窗口是 DHT11 协议里最关键的参数,延时太长或太短都会导致误判。
3.3 双通道分时复用的调度策略
两颗 DHT11 不能同时读取,因为单总线协议要求主机独占总线。我一般用分时复用的方式:先读第一颗,间隔至少 1 秒后再读第二颗。DHT11 的采样周期是 1 秒,如果读得太快,它会返回上一次的数据或者不响应。
// 双通道读取调度 void Read_Both_Sensors(void) { if (DHT11_Read(&temp1, &humi1) == 0) { // 读第一颗 // 更新 LCD 第一行显示 } Delay_ms(1200); // 间隔 1.2 秒 if (DHT11_Read(&temp2, &humi2) == 0) { // 读第二颗 // 更新 LCD 第二行显示 } }这里的 1.2 秒间隔是留了余量的。DHT11 手册上写的是采样周期不低于 1 秒,但实际测试中,有些批次的传感器在 1 秒整的时候会返回错误数据,1.2 秒比较稳妥。如果你要同时显示两个通道的数据,LCD 的刷新也要跟着这个节奏走,不能刷太快,否则屏幕会闪。
4. LCD1602 驱动与显示刷新:怎么让四个数据稳定不跳
4.1 4 位模式下的写命令与写数据时序
LCD1602 的 4 位模式每次传一个字节要分两次:先传高 4 位,再传低 4 位。每次传输都要拉低 E 引脚,保持一段时间,再拉高,形成使能脉冲。RS 引脚决定是写命令还是写数据:RS 为 0 是命令,RS 为 1 是数据。
// 写一个字节到 LCD(4 位模式) void LCD_WriteByte(unsigned char dat, unsigned char rs) { LCD_RS = rs; // 选择命令或数据 LCD_RW = 0; // 写模式 LCD_D4 = (dat >> 4) & 0x01; // 高 4 位 LCD_D5 = (dat >> 5) & 0x01; LCD_D6 = (dat >> 6) & 0x01; LCD_D7 = (dat >> 7) & 0x01; LCD_E = 1; // 使能脉冲 Delay_us(5); LCD_E = 0; Delay_us(100); // 等待命令执行 // 低 4 位同理 }这段代码里,E 引脚的高电平时间不能太短,至少 450ns,我一般给 5us,留足余量。命令执行时间根据命令不同而不同,清屏命令需要 1.52ms,其他命令一般 37us 到 100us。我统一用 100us 延时,虽然有点浪费,但稳定。
4.2 温湿度数据的格式化与显示位置规划
LCD1602 有两行,每行 16 个字符。我一般这样规划:第一行显示通道 1 的数据,格式是“T1:25C H1:60%”;第二行显示通道 2 的数据,格式是“T2:26C H2:58%”。这样一眼就能看出两个通道的差异。
// 格式化显示 void Display_Data(unsigned char temp, unsigned char humi, unsigned char line) { unsigned char buf[16]; if (line == 0) { sprintf(buf, "T1:%dC H1:%d%%", temp, humi); LCD_SetCursor(0, 0); } else { sprintf(buf, "T2:%dC H2:%d%%", temp, humi); LCD_SetCursor(1, 0); } LCD_WriteString(buf); }用 sprintf 的好处是格式化方便,缺点是占用 Flash 空间。STC89C51 的 Flash 一般有 4K 到 8K,sprintf 大概占 1K 左右,如果空间紧张,可以自己写一个简单的整数转字符串函数。
4.3 刷新频率与闪烁抑制的实操参数
LCD 刷新频率太高会导致屏幕闪烁,太低又显得反应迟钝。我一般用 500ms 到 1s 的刷新周期。但双通道读取需要 2.4 秒左右(每颗 1.2 秒),所以实际刷新周期是 2.4 秒。这个速度对于环境监测来说完全够用,温度和湿度不会在几秒内剧烈变化。
如果你觉得 2.4 秒太慢,可以缩短间隔到 1 秒,但要注意 DHT11 的采样周期限制。另一个办法是用两个独立的定时器,分别控制两颗 DHT11 的读取,但这样代码复杂度会上升,对于 51 单片机来说不太划算。
提示:LCD 的背光电流较大,如果直接用单片机 IO 驱动,可能会拉低电压导致单片机复位。建议用三极管或者 MOS 管驱动背光,或者直接接限流电阻到 VCC。
5. 避坑与排查:双通道 DHT11 系统最常见的五个翻车现场
5.1 现象:LCD 显示乱码或只显示一行黑块
原因:对比度电位器没调好,或者 LCD 初始化时序不对。LCD1602 上电后需要等待至少 15ms 才能发送命令,如果初始化太快,内部控制器还没准备好,就会显示异常。
解决:在初始化函数开头加 50ms 延时,然后发送 0x38(8 位模式)或 0x28(4 位模式)三次,每次间隔 5ms。对比度电位器慢慢调,直到字符清晰。如果还是不行,检查 RS、RW、E 三根线有没有接反。
5.2 现象:DHT11 返回的温湿度全是 0 或者 255
原因:上拉电阻没接,或者 DATA 线接错了引脚。DHT11 的数据线在没有上拉电阻的情况下会浮空,单片机读到的全是高电平或者随机值。另外,有些三针模块的引脚顺序是 VCC、DATA、GND,有些是 DATA、VCC、GND,接反了也会读不到数据。
解决:用万用表测 DATA 线在空闲时的电压,正常应该是 VCC 电压。如果接近 0V,说明上拉电阻没接或者接错了。检查引脚顺序,确保 VCC 接 5V,GND 接 GND,DATA 接单片机 IO。
5.3 现象:两颗 DHT11 只有一颗能读到数据
原因:两颗 DHT11 共用了一个 IO 口,或者读取间隔太短。单总线协议要求主机独占总线,如果两颗传感器接在同一个 IO 上,会互相干扰。另外,如果读取间隔小于 1 秒,DHT11 可能还没完成上一次采样,返回的是旧数据或者不响应。
解决:每颗 DHT11 单独占一个 IO 口,读取间隔至少 1.2 秒。如果 IO 不够用,可以用模拟开关切换,但会增加硬件复杂度。我一般建议直接用两个 IO,STC89C51 的 IO 足够多。
5.4 现象:数据偶尔跳变,比如温度突然变成 80 度
原因:电源纹波太大,或者 DATA 线受到干扰。DHT11 对电源质量比较敏感,如果和电机、继电器共用电源,纹波会导致通信错误。另外,DATA 线太长或者没有屏蔽,也会引入干扰。
解决:在 DHT11 的 VCC 和 GND 之间加一个 100nF 的陶瓷电容,靠近传感器引脚放置。DATA 线如果超过 20cm,用屏蔽线,屏蔽层接地。电源部分加一个 100uF 的电解电容做储能。
5.5 现象:程序跑一段时间后死机,LCD 不再刷新
原因:看门狗没开,或者中断冲突。51 单片机没有内置看门狗,如果程序跑飞,就会死循环。另外,如果用了定时器中断,中断服务函数里做了太多事情,会导致主循环卡死。
解决:加一个外部看门狗芯片,比如 MAX813,或者用软件看门狗(定时器溢出后复位)。中断服务函数里只做标志位设置,具体处理放到主循环里做。DHT11 的读取函数里加了 while 等待,如果传感器不响应,会一直等,建议加超时计数。
6. 从能跑到好用:双通道温湿度系统的三个进阶技巧
6.1 用滑动平均滤波让显示值不再跳
DHT11 的精度是 1℃,但实际读数经常会在两个值之间跳,比如 25 和 26 交替出现。这不是传感器坏了,而是它的分辨率和噪声导致的。最简单的解决办法是滑动平均:连续读 5 次,去掉最大值和最小值,剩下的取平均。
// 滑动平均滤波 unsigned char Filter_Temp(unsigned char new_val) { static unsigned char buf[5] = {0}; static unsigned char idx = 0; unsigned char i, max, min, sum = 0; buf[idx++] = new_val; if (idx >= 5) idx = 0; max = buf[0]; min = buf[0]; for (i = 0; i < 5; i++) { if (buf[i] > max) max = buf[i]; if (buf[i] < min) min = buf[i]; sum += buf[i]; } return (sum - max - min) / 3; // 去掉最大最小后取平均 }这个函数的逻辑是:维护一个 5 个元素的缓冲区,每次新数据进来替换最旧的数据,然后去掉最大值和最小值,剩下的 3 个取平均。这样既能抑制突发噪声,又不会让响应变得太慢。对于温度这种变化缓慢的物理量,5 点滤波足够了。
6.2 加一个温差报警:用蜂鸣器做阈值提示
双通道系统的一个实用功能是温差报警:当两个通道的温度差超过设定阈值时,蜂鸣器响。这个功能在农业大棚里特别有用,比如南北两端温差超过 3℃ 就说明通风不均匀。
// 温差报警 void Check_Temp_Diff(unsigned char t1, unsigned char t2) { unsigned char diff; if (t1 > t2) diff = t1 - t2; else diff = t2 - t1; if (diff >= 3) { // 温差超过 3℃ BEEP = 0; // 蜂鸣器响 } else { BEEP = 1; // 蜂鸣器停 } }阈值 3℃ 是我根据大棚场景定的,你可以根据实际需求改。如果是机房,温差超过 5℃ 才需要报警;如果是冷库,温差超过 2℃ 就要注意了。蜂鸣器建议用有源蜂鸣器,直接给电平就响,不需要 PWM 驱动。
6.3 把数据存到 EEPROM:断电后还能看历史
STC89C51 内部有 EEPROM,可以用来存历史数据。比如每分钟存一次温湿度,存满 24 小时的数据大概需要 2K 字节。这样即使断电,重新上电后也能看到之前的趋势。
// 存数据到 EEPROM void Save_To_EEPROM(unsigned char temp, unsigned char humi, unsigned int addr) { ISP_CONTR = 0x81; // 使能 ISP/IAP ISP_CMD = 0x02; // 写命令 ISP_ADDRH = addr >> 8; ISP_ADDRL = addr & 0xFF; ISP_DATA = temp; ISP_TRIG = 0x46; // 触发序列 ISP_TRIG = 0xB9; // 等待写完成,然后存湿度 }这段代码是 STC 系列单片机 EEPROM 操作的标准流程。注意写 EEPROM 需要时间,大概 20ms 左右,写太频繁会影响主循环的实时性。我一般 1 分钟存一次,一天存 1440 条,每条 2 字节,总共不到 3K,STC89C51 的 EEPROM 一般有 4K 以上,够用。
注意:EEPROM 的擦写次数有限,大概 10 万次左右。如果 1 分钟写一次,一年就是 52 万次,会超过寿命。建议用循环存储的方式,或者降低存储频率到 5 分钟一次。
我自己做这个系统的时候,最开始没加上拉电阻,调了两个晚上才发现是硬件问题,血泪经验就是:DHT11 不响应,先查上拉电阻,再查时序。后来加了滑动平均滤波,显示稳定多了,不会再出现 25 和 26 来回跳的情况。如果你也在做类似的项目,建议先把单通道跑通,再加第二通道,不要一上来就搞双通道,否则出了问题很难定位是硬件还是软件。希望帮到你。
本文还有配套的精品资源,点击获取