news 2026/10/14 18:10:34

STC89C51双通道DHT11温湿度采集与LCD1602显示实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STC89C51双通道DHT11温湿度采集与LCD1602显示实战

简介:这份资源面向电子工程、嵌入式入门学习者与单片机课程设计人群,提供一套基于STC89C51的双通道DHT11实时温湿度显示系统完整工程。系统以STC89C51为控制核心,通过单总线读取两路DHT11温湿度数据,经时序解析后由LCD1602液晶实时呈现,并配套按键模块,覆盖传感器采集、字符显示与人机交互等典型环节。压缩包共22个文件,约68KB,包含C语言源码与头文件、Keil工程配置、编译生成的hex与obj等目标文件,以及硬件模块相关文件,目录按LCD、KEY、USER等模块划分,结构清晰便于对照学习。目前已有1372人学习下载。读者可据此掌握单总线通信时序、LCD1602驱动、多传感器轮询与工程编译调试流程,是学习嵌入式系统开发与传感器应用的实用案例。

1. 双通道 DHT11 温湿度系统:从一颗 STC89C51 说起

如果你手头正好有一片 STC89C51 最小系统板、两个 DHT11 模块和一块 LCD1602,却卡在“怎么让两路温湿度同时显示、不打架”这一步,那这份资源就是冲着你来的。它是一套完整的单片机温湿度采集与显示方案,核心是 STC89C51 做主控,两路 DHT11 分别挂在两个 IO 口上,LCD1602 负责把两路温度和湿度实时刷出来。适合电子类专业做课程设计的学生、刚接触 51 单片机的嵌入式新手,以及需要快速搭一个双点温湿度监测原型的工程师。单路 DHT11 的例程网上一抓一大把,但双通道同时跑、时序不互相干扰、LCD 刷新不闪烁,这几个问题凑在一起,翻车的人不在少数。这份资源把硬件连接、时序驱动、显示刷新三块都串起来了,拿到手能直接复现,也能顺着改通道数和采样周期。

2. 双通道 DHT11 的硬件连接与 IO 分配

2.1 为什么选 STC89C51 而不是 STM32

STC89C51 是 8051 内核,主频通常跑 11.0592MHz 或 12MHz,资源少、RAM 只有 128 字节(部分型号 256 字节),但它有一个被低估的优势:IO 口操作足够直接,没有 HAL 库那层封装,DHT11 这种单总线时序敏感的外设,用 51 写反而更容易把微秒级延时卡准。STM32 当然也能做,但如果你只是想快速验证双通道采集逻辑,51 的代码量和调试成本更低。常见做法是:STC89C51 跑 12MHz 晶振,机器周期 1μs,延时函数用_nop_()叠加就能凑出 DHT11 需要的 40μs 起始信号和 26-28μs 的读位窗口。

选型上还有一个现实原因:这份资源的代码结构是按 51 的寄存器风格写的,直接移植到 STM32 需要重写延时和 IO 方向切换,工作量不小。所以如果你的目标平台是 51,这份资源匹配度最高;如果是 STM32,可以参考它的双通道调度思路,但驱动层要重写。

2.2 两个 DHT11 的引脚分配与上拉电阻

DHT11 是单总线器件,数据线需要接一个 4.7kΩ 到 10kΩ 的上拉电阻到 VCC。双通道时,两个 DHT11 的数据线分别接两个独立 IO,不能挂在同一根线上——DHT11 没有片选,挂同一根线会直接冲突。

我一般这样分配:

信号引脚说明
DHT11-1 数据P2.0通道 1 数据线,接 4.7k 上拉
DHT11-2 数据P2.1通道 2 数据线,接 4.7k 上拉
LCD1602 RSP1.0寄存器选择
LCD1602 RWP1.1读写选择,通常接地只写
LCD1602 ENP1.2使能信号
LCD1602 D4-D7P1.4-P1.74 位数据模式
晶振12MHz机器周期 1μs

LCD1602 用 4 位模式可以省 4 个 IO,对 51 来说很关键,因为 P0 口通常还要留出来做其他用途。上拉电阻不能省,我见过有人直接拿 IO 口内部弱上拉去驱动 DHT11,结果读回来的数据全是 0 或者校验失败,现象很玄学,其实就是总线拉不高。

2.3 电源与去耦的细节

两个 DHT11 同时工作时,瞬态电流会比单路大。常见做法是在每个 DHT11 的 VCC 和 GND 之间并一个 100nF 陶瓷电容,位置尽量靠近传感器引脚。STC89C51 的 VCC 脚也要并一个 10μF 电解加 100nF 陶瓷。别小看这一步,电源纹波大的时候,DHT11 的时序会漂,读出来的湿度值可能跳变十几个百分点,你还以为是传感器坏了。

3. DHT11 时序驱动:40μs 起始与读位窗口

3.1 单总线协议的起始信号与响应

DHT11 的通信流程是:主机拉低总线至少 18ms,然后拉高 20-40μs,接着释放总线,等待 DHT11 拉低 80μs 再拉高 80μs 作为响应。51 上实现这段,关键是延时不能被打断。

// 主机发送起始信号 void DHT11_Start(void) { DHT11_PIN = 0; // 拉低总线 Delay_ms(20); // 保持至少 18ms DHT11_PIN = 1; // 拉高总线 Delay_us(30); // 保持 20-40us DHT11_PIN = 1; // 释放总线,等待 DHT11 响应 }

逻辑说明:Delay_ms(20)给的是 20ms,比手册要求的 18ms 略长,留余量。Delay_us(30)落在 20-40μs 窗口中间。释放总线后,DHT11 会把总线拉低 80μs,再拉高 80μs,这两个电平用轮询方式检测,不要用中断,因为中断响应时间不确定,容易错过窗口。

参数说明:如果你的晶振不是 12MHz,Delay_us和Delay_ms要重新校准。常见做法是用示波器抓一下实际延时,或者用定时器做基准。我一般会在起始信号后加一个超时计数,如果 200μs 内没等到 DHT11 的响应,直接返回错误,避免程序卡死。

3.2 读位窗口:26-28μs 的高电平判 0 和 1

DHT11 的每一位数据都以 50μs 低电平开始,然后高电平持续 26-28μs 表示 0,持续 70μs 表示 1。51 上读位时,先等低电平结束,再延时 30μs 左右采样,如果还是高电平就是 1,否则是 0。

// 读取一个位 unsigned char DHT11_ReadBit(void) { unsigned char bit = 0; while (DHT11_PIN == 0); // 等待 50us 低电平结束 Delay_us(30); // 延时 30us,落在 26-28us 和 70us 之间 if (DHT11_PIN == 1) { // 如果还是高电平,说明是 1 bit = 1; while (DHT11_PIN == 1); // 等待高电平结束 } return bit; }

逻辑说明:while (DHT11_PIN == 0)是阻塞等待,DHT11 的低电平是 50μs,51 跑 12MHz 时这条循环能跟上。Delay_us(30)是采样点,选 30μs 是因为 0 的高电平只有 26-28μs,30μs 时已经变低;1 的高电平有 70μs,30μs 时还是高。这个采样点是整个驱动最关键的参数,偏一点就会把 0 读成 1。

参数说明:Delay_us(30)的精度取决于你的延时函数。如果用的是_nop_()叠加,12MHz 下一条_nop_()是 1μs,30 条就是 30μs。但编译器优化可能会改变指令数,建议用volatile或者关中断来保证。我一般会在读位函数里加一个超时计数,防止 DHT11 没响应时死循环。

3.3 双通道分时调度:避免两个 DHT11 互相干扰

两个 DHT11 不能同时读,因为每个传感器的完整读取周期大约 20ms 以上,如果交替读,总周期会拉长到 40ms 以上。常见做法是:先读通道 1,读完立即读通道 2,中间不加额外延时。DHT11 本身有 1 秒的采样间隔要求,所以两次读取之间至少要隔 1 秒,否则读回来的数据是上一次的缓存,湿度值不会变。

// 双通道读取调度 void Read_Both_Channels(void) { DHT11_Start_Ch1(); if (DHT11_Read_Data_Ch1(&temp1, &humi1) == 0) { // 通道 1 读取成功 } DHT11_Start_Ch2(); if (DHT11_Read_Data_Ch2(&temp2, &humi2) == 0) { // 通道 2 读取成功 } }

逻辑说明:每个通道的读取是独立的,DHT11_Start_Ch1和DHT11_Start_Ch2操作不同的 IO 口。读取失败时不要立即重试,因为 DHT11 需要时间恢复,立即重试大概率还是失败。我一般会在主循环里做 1 秒的定时,每秒读一次双通道,失败就保留上一次的值,并在 LCD 上标记。

参数说明:temp1、humi1是全局变量,类型用unsigned char就够,DHT11 的温度范围 0-50℃,湿度 20-90%RH,8 位精度。如果你需要负温度,DHT11 不支持,得换 DHT22 或者 DS18B20。

4. LCD1602 显示刷新:4 位模式与双通道布局

4.1 4 位模式初始化与忙标志判断

LCD1602 用 4 位模式时,初始化序列和 8 位不同,要发两次 0x28、0x0C、0x06、0x01。忙标志判断是可选的,但如果你在刷新时发现字符错位或者闪烁,大概率是没等 LCD 内部操作完成就发了下一条指令。

// LCD1602 4 位模式初始化 void LCD_Init(void) { Delay_ms(15); LCD_Write_Cmd(0x33); // 强制复位 Delay_ms(5); LCD_Write_Cmd(0x32); // 4 位模式 Delay_ms(5); LCD_Write_Cmd(0x28); // 4 位,2 行,5x7 点阵 LCD_Write_Cmd(0x0C); // 显示开,光标关 LCD_Write_Cmd(0x06); // 写入后地址自动加 1 LCD_Write_Cmd(0x01); // 清屏 Delay_ms(2); }

逻辑说明:0x33和0x32是 4 位模式的强制复位序列,很多例程省略这一步,结果 LCD 上电后不显示。0x28设置 4 位数据、2 行显示、5x7 点阵。0x0C开显示关光标,如果你需要光标就改成0x0E。0x06是写入后地址自动加 1,配合LCD_Write_Data就能连续写字符串。

参数说明:Delay_ms(15)是上电等待,LCD1602 内部复位需要时间。LCD_Write_Cmd里要把命令拆成高 4 位和低 4 位分两次发,每次拉低 EN 再拉高。忙标志判断可以用LCD_Read_Busy实现,但需要把 RW 接 IO 而不是接地,会多占一个引脚。我一般用固定延时替代忙判断,简单可靠。

4.2 双通道温湿度在 16x2 上的布局

LCD1602 只有 16 列 2 行,要显示两路温湿度,布局得紧凑。常见做法是第一行显示通道 1 的温度和湿度,第二行显示通道 2 的温度和湿度。

位置内容示例
第 1 行T1:25C H1:60%通道 1 温度 25℃,湿度 60%
第 2 行T2:26C H2:58%通道 2 温度 26℃,湿度 58%
// 显示双通道数据 void Display_Both(void) { LCD_Set_Cursor(0, 0); LCD_Write_String("T1:"); LCD_Write_Num(temp1); LCD_Write_String("C H1:"); LCD_Write_Num(humi1); LCD_Write_String("%"); LCD_Set_Cursor(1, 0); LCD_Write_String("T2:"); LCD_Write_Num(temp2); LCD_Write_String("C H2:"); LCD_Write_Num(humi2); LCD_Write_String("%"); }

逻辑说明:LCD_Set_Cursor(0, 0)定位到第一行第一列,LCD_Set_Cursor(1, 0)定位到第二行第一列。LCD_Write_Num把数字转成字符,注意温度湿度都是整数,不需要小数点。如果数值变化频繁,整屏刷新会闪,常见做法是只更新变化的数字位,但 51 的 RAM 有限,整屏刷新在 1 秒周期下视觉上可以接受。

参数说明:LCD_Set_Cursor的行列参数,第一行地址是 0x80,第二行是 0xC0,写入时加上列偏移。LCD_Write_Num里用取模和除法拆位,注意 0 的处理,别显示成空白。

4.3 刷新周期与 DHT11 采样间隔的配合

DHT11 要求两次读取间隔至少 1 秒,LCD 刷新可以更快,但没必要。我一般把主循环设成 1 秒一次:读双通道、更新 LCD、然后延时到下一秒。这样 LCD 上的数值每秒跳一次,不会闪,也不会因为刷新太快导致 DHT11 读到重复数据。

// 主循环 void main(void) { LCD_Init(); while (1) { Read_Both_Channels(); Display_Both(); Delay_ms(1000); // 1 秒周期 } }

逻辑说明:Delay_ms(1000)是粗延时,实际周期会略大于 1 秒,因为读取和显示也占时间。对温湿度监测来说,这个精度足够。如果你需要更准的周期,用定时器中断做基准,主循环里只置标志位。

参数说明:Delay_ms(1000)在 12MHz 下用循环实现,误差在几个毫秒级别。如果晶振是 11.0592MHz,延时函数要重新算。我一般会在延时函数里用while循环加_nop_(),而不是依赖编译器内置的delay,因为不同编译器行为不一样。

5. 避坑与排查:双通道 DHT11 最常见的五个翻车点

5.1 现象:两个 DHT11 读出来的数据一模一样

原因:两个传感器的数据线接到了同一个 IO 口,或者代码里两个通道的读取函数操作了同一个引脚。DHT11 没有地址,挂同一根线时两个传感器会同时响应,数据冲突后读回来的值可能恰好相同。

解决:检查硬件连线,确认 DHT11-1 和 DHT11-2 的数据线分别接在两个独立 IO 上。代码里DHT11_Start_Ch1和DHT11_Start_Ch2操作的引脚宏要不同。用万用表量一下两个数据线之间是否短路。

5.2 现象:LCD 上电后不显示,或者只显示一行方块

原因:4 位模式初始化序列不对,或者Delay_ms(15)不够。LCD1602 内部复位需要至少 15ms,有些批次的芯片需要更长。另外0x33和0x32强制复位序列不能省。

解决:把上电延时加到 50ms,确认LCD_Write_Cmd(0x33)和0x32都发了。如果还是不行,检查 LCD 的 V0 对比度电压,通常接一个 10k 电位器调到合适亮度。对比度不对时,字符是隐形的,不是没显示。

5.3 现象:湿度值跳变十几甚至几十个百分点

原因:电源纹波大,或者 DHT11 的 VCC 没有去耦电容。双通道同时工作时,瞬态电流让 VCC 波动,DHT11 内部 ADC 参考电压不稳,湿度读数就会跳。

解决:每个 DHT11 的 VCC 和 GND 之间并 100nF 陶瓷电容,位置靠近传感器。STC89C51 的 VCC 并 10μF 电解加 100nF。如果还跳,把 DHT11 的供电单独走线,不要和 LCD 背光共用一条电源线。

5.4 现象:读回来的温度正常,湿度一直是 0 或者 255

原因:读位窗口的采样点偏了。湿度字节在温度字节之后,如果前几个位读错,后面全错。Delay_us(30)在 12MHz 下是 30μs,但如果编译器优化把_nop_()删了,实际延时可能只有十几微秒,采样点落在 0 的高电平区间内,把 0 读成 1。

解决:用示波器抓一下Delay_us(30)的实际波形,确认是 30μs。或者在读位函数里加volatile变量,防止编译器优化。我一般会在Delay_us里用volatile unsigned char i做循环变量,确保不被优化掉。

5.5 现象:程序跑一段时间后死机,LCD 不再刷新

原因:DHT11 没响应时,while (DHT11_PIN == 0)或者while (DHT11_PIN == 1)死循环,程序卡在驱动里出不来。DHT11 偶尔会因为电源波动或者时序偏差不响应,如果没有超时机制,主循环就挂了。

解决:在每个while等待循环里加超时计数,超过一定次数就返回错误。比如unsigned int timeout = 0; while (DHT11_PIN == 0 && timeout < 1000) timeout++;。超时后返回 0,主循环继续跑,LCD 保留上一次的值。

6. 进阶:把双通道改成四通道与数据校验的取舍

双通道跑通之后,很自然会想扩到四通道。STC89C51 的 IO 口够用,P2 口有 8 个引脚,挂 4 个 DHT11 没问题。但 RAM 只有 128 字节,4 个通道的温度湿度各占 1 字节,加上 LCD 缓冲和临时变量,内存会很紧张。我一般会把不用的变量定义成idata或者pdata,或者直接复用临时变量。

四通道的调度逻辑和双通道一样,只是循环次数变了:

// 四通道读取调度 void Read_Four_Channels(void) { unsigned char i; for (i = 0; i < 4; i++) { DHT11_Start(i); if (DHT11_Read_Data(i, &temp[i], &humi[i]) != 0) { // 读取失败,保留上一次的值 } } }

逻辑说明:DHT11_Start(i)和DHT11_Read_Data(i, ...)里根据i切换 IO 口,可以用数组存引脚宏,或者用switch分支。4 个通道读完大约 80-100ms,加上 1 秒延时,总周期 1.1 秒左右,对温湿度监测来说可以接受。

参数说明:temp和humi定义成unsigned char temp[4]和unsigned char humi[4],占 8 字节。如果 RAM 不够,可以把temp和humi合并成一个结构体数组,或者用code存储常量。

数据校验方面,DHT11 的校验和是前 4 字节相加取低 8 位。我一般会在读取函数里做校验,校验失败就丢弃本次数据,保留上一次的值。但要注意,如果连续多次校验失败,说明硬件有问题,这时候应该在 LCD 上显示错误标记,而不是一直显示旧数据。

// 校验和检查 unsigned char DHT11_CheckSum(unsigned char *data) { unsigned char sum = 0; unsigned char i; for (i = 0; i < 4; i++) { sum += data[i]; } return (sum == data[4]) ? 0 : 1; // 0 表示校验通过 }

逻辑说明:data[0]是湿度整数,data[1]是湿度小数(DHT11 恒为 0),data[2]是温度整数,data[3]是温度小数,data[4]是校验和。前 4 字节相加取低 8 位,和data[4]比较。校验失败返回 1,调用方决定是重试还是丢弃。

参数说明:DHT11 的小数字节恒为 0,所以实际有效数据只有整数部分。如果你需要更高精度,得换 DHT22,它的分辨率和量程都更好,但价格也更高。

从那以后我每次做多通道 DHT11 项目,都会先单独跑通一路,确认时序和校验没问题,再复制到其他通道。双通道不是简单地把单通道代码复制两份,IO 分配、调度顺序、电源去耦,每一步都可能埋坑。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 18:10:26

享元模式实战:五子棋棋子对象共享与内存优化

1. 从棋盘上密密麻麻的棋子说起&#xff1a;为什么需要享元模式第一次接触享元模式的人&#xff0c;多半会被“享元”这个翻译搞得一头雾水。英文原词是 Flyweight&#xff0c;直译过来是“蝇量级”&#xff0c;意思是拳击比赛里最轻的那个量级。放在设计模式里&#xff0c;它想…

作者头像 李华
网站建设 2026/10/14 18:10:14

微信小程序条件渲染与picker 组件:从界面交互到数据 驱动

一、需求分析本次作业实现一个小程序案例&#xff0c;综合练习 picker 选择器组件、wx:if 条件渲染、block 块渲染以及 input 输入框的失焦事件。功能要点&#xff1a;页面上方是一个 picker 选择器&#xff0c;默认显示红色文字与红色色块&#xff1b;下方是数字输入框&#x…

作者头像 李华
网站建设 2026/10/14 18:09:37

ESP32-S3+LVGL驱动1.69寸电容触摸屏:初始化、DMA与坐标映射全解

简介&#xff1a;面向嵌入式开发者的ESP32-S3图形界面驱动资源包&#xff0c;聚焦LVGL在1.69寸电容触摸屏上的落地&#xff1a;屏显部分采用ST7789显示控制器&#xff0c;触摸部分由CST816芯片实现&#xff0c;适合希望快速搭建物联网UI、学习LVGL移植或做屏幕驱动二次开发的工…

作者头像 李华