☰
51单片机双通道DHT11温湿度系统:硬件连接、时序与避坑指南
2026/10/14 18:10:30 网站建设 项目流程

简介:这份资源面向单片机初学者与嵌入式课程设计者,提供一套基于STC89C51的双通道DHT11温湿度实时显示方案,解决环境数据采集与液晶显示联调的实际问题。压缩包共22个文件,约68KB,包含C语言源码与头文件、Keil工程配置、编译生成的hex与obj目标文件、汇编列表及构建日志等,覆盖从代码编写到烧录验证的完整链路。硬件部分涉及DHT11单总线通信、LCD1602字符显示、按键模块以及电源、时钟、复位等基础电路,软件则需处理时序控制与数据解析,是理解传感器接口与人机交互设计的典型实例。目前已有1372人学习下载,读者可据此搭建实物、对照源码梳理程序流程,并借助工程文件快速复现调试过程,适合作为嵌入式入门与综合实训的参考案例。

1. 双通道 DHT11 温湿度系统:为什么一块 51 单片机要挂两颗传感器

很多做嵌入式课设或小型环境监测的开发者,第一次看到“双通道 DHT11 实时温湿度显示系统”这个标题时,第一反应是:一颗 DHT11 不就够了,为什么要挂两颗?这正是这个方案最值得聊的地方。单点测温只能反映传感器附近那一小片空气的状态,放在机箱里测的是板子温度,放在窗边测的是窗边温度,一旦你想做“室内外对比”“进出风口温差”“上下层温度梯度”,单通道立刻不够用。双通道的价值不在于传感器多,而在于它能让你在同一块 LCD1602 上同时看到两个位置的温湿度,直接做差值判断。

这个系统适合谁?适合正在做单片机课程设计、想从“点亮一个传感器”进阶到“多传感器轮询 + 显示刷新 + 时序管理”的开发者;也适合做小型仓储、温室、机房辅助监测的从业者,用最低成本验证双点采集是否值得投入。STC89C51 这类经典 51 内核单片机资源有限,RAM 只有 128 字节、Flash 也就几 KB,正好逼着你去理解 DHT11 的单总线时序、LCD1602 的 4 位/8 位驱动方式,以及“两个传感器怎么分时复用一根数据线或各占一根 IO”的工程取舍。把这块啃下来,后面换 STM32、换 RS485 组网,思路是通的。

2. 双通道 DHT11 的硬件连接与单总线时序拆解

2.1 为什么 DHT11 不能简单并联在同一根 IO 上

DHT11 是单总线器件,通信靠一根数据线完成“主机拉低启动 → 传感器响应 → 40 位数据逐位输出”。它的数据线是开漏结构,同一时刻只能有一个器件在驱动总线。如果你把两颗 DHT11 的数据脚直接拧在一起接到同一个 IO,两颗传感器会同时响应主机的启动信号,然后同时往总线上吐数据,结果就是电平互相打架,读回来的 40 位数据全是乱的。这不是玄学,是电气层面的冲突。

常见做法有两种。第一种是“各占一根 IO”,DHT11-1 接 P2.0,DHT11-2 接 P2.1,程序里分时分别读取,互不干扰,代码最简单,代价是多占一个引脚。第二种是“单 IO 分时供电”,两颗传感器数据脚并联,但电源脚分别由两个 IO 控制,读哪颗就给哪颗上电,另一颗断电处于高阻态。这种方式省引脚,但对供电 IO 的驱动能力和上电稳定时间有要求,新手容易翻车。我一般推荐第一种,51 的 IO 够用,没必要给自己找麻烦。

连接方式数据脚电源控制优点风险
双 IO 独立P2.0 / P2.1常供电时序简单、稳定多占一个 IO
单 IO 分时供电并联到 P2.0P2.1 / P2.2 控制省数据 IO上电稳定时间难调
单 IO 分时供电(MOS 管)并联到 P2.0MOS 管切电源驱动能力强电路复杂

2.2 DHT11 一次完整读取的时序参数

DHT11 的时序对延时非常敏感,51 单片机不加晶振校准很容易读失败。一次完整通信分四段:主机启动、传感器响应、40 位数据、总线释放。主机先把数据线拉低至少 18ms,然后拉高 20~40us,接着释放总线等待传感器响应。传感器会先拉低 80us 表示收到,再拉高 80us 表示准备发数据。之后每一位数据都以“50us 低电平 + 高电平”表示,高电平持续 26~28us 代表 0,持续 70us 代表 1。

这里的关键是:51 单片机在 12MHz 晶振下,一个机器周期是 1us,普通延时函数能凑合;但如果用 11.0592MHz 晶振,机器周期约 1.085us,延时会有累积误差。很多新手读出来湿度一直是 0 或者 255,八成是延时没对准。我的习惯是先用示波器或者逻辑分析仪抓一次波形,确认启动信号和响应信号的时间,再回头调延时函数,比盲改代码快得多。

// 主机启动信号:拉低至少 18ms,再拉高 20~40us void DHT11_Start(void) { DHT11_DATA = 0; // 拉低数据线 Delay_ms(20); // 保持 20ms,满足 >18ms 要求 DHT11_DATA = 1; // 拉高 Delay_us(30); // 保持 30us,落在 20~40us 区间 DHT11_DATA = 1; // 释放总线,等待传感器响应 } // 读取一个字节:8 位,高位先出 unsigned char DHT11_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { while (!DHT11_DATA); // 等待 50us 低电平结束 Delay_us(40); // 延时 40us 后采样 dat <<= 1; if (DHT11_DATA) // 仍为高,说明是高电平 70us,即数据 1 dat |= 1; while (DHT11_DATA); // 等待该位高电平结束 } return dat; }

上面这段代码里,Delay_ms(20)和Delay_us(30)是启动时序,Delay_us(40)是采样点。采样点选在 40us 是有讲究的:数据 0 的高电平只有 26~28us,40us 时已经变低;数据 1 的高电平有 70us,40us 时还是高。这样一次采样就能区分 0 和 1,不用去精确测量高电平宽度。参数上,Delay_us的精度直接决定成败,建议用定时器或者示波器校准过的循环延时,不要随手写for(i=0;i<10;i++)就完事。

2.3 LCD1602 的 4 位驱动与引脚分配

LCD1602 支持 8 位和 4 位两种数据模式。8 位模式一次传一个字节,占用 8 根数据线;4 位模式分两次传,高 4 位先传、低 4 位后传,只占 4 根数据线。51 的 IO 本来就紧张,双 DHT11 已经占了两个,再加上 LCD 的 RS、RW、E 三根控制线,8 位模式要占 11 个 IO,4 位模式只占 7 个。所以这个方案里我一般用 4 位模式。

引脚分配可以这样:LCD 的 D4~D7 接 P0.4~P0.7,RS 接 P2.5,RW 接 P2.6,E 接 P2.7。DHT11-1 数据脚接 P2.0,DHT11-2 数据脚接 P2.1。注意 P0 口是开漏输出,做 LCD 数据线时要外接 10K 上拉排阻,否则电平拉不高,LCD 会显示乱码或者干脆不亮。这个坑我见过太多次,血泪经验就是:P0 口驱动任何东西之前,先问自己有没有上拉。

// LCD1602 4 位模式写命令 void LCD_WriteCmd(unsigned char cmd) { LCD_RS = 0; // 命令模式 LCD_RW = 0; // 写模式 LCD_DATA = (LCD_DATA & 0x0F) | (cmd & 0xF0); // 先发高 4 位 LCD_E = 1; Delay_us(2); LCD_E = 0; // 产生使能脉冲 Delay_us(100); LCD_DATA = (LCD_DATA & 0x0F) | ((cmd << 4) & 0xF0); // 再发低 4 位 LCD_E = 1; Delay_us(2); LCD_E = 0; Delay_us(100); }

这段代码里,LCD_DATA & 0x0F是保留低 4 位不变,把高 4 位替换成要发的数据。先发cmd & 0xF0取高 4 位,再发(cmd << 4) & 0xF0把低 4 位移到高 4 位发出。每次使能脉冲后延时 100us,是给 LCD 控制器处理时间,太快会丢命令。参数上,Delay_us(2)是使能脉冲宽度,一般要求大于 450ns,2us 足够;Delay_us(100)是命令执行时间,清屏命令需要更久,要单独延时 2ms 左右。

3. 双通道轮询采集与 LCD1602 刷新逻辑的实现

3.1 两个 DHT11 的分时读取流程

双通道的核心是“分时”:同一时刻只读一颗传感器,读完再读另一颗。DHT11 两次采集之间建议间隔 1 秒以上,因为它内部需要时间完成一次测量。如果你连续读,第二颗可能还没准备好,返回的数据就是上一次的旧值或者全 0。我的做法是主循环里放一个 1 秒的软定时,时间到了先读 DHT11-1,延时 100ms 再读 DHT11-2,然后统一刷新 LCD。

读取流程要加超时判断。DHT11 如果没接好或者坏了,while(!DHT11_DATA)会死循环,整个系统卡死。所以每个等待环节都要加计数器,超过一定次数就跳出并返回错误码。这个错误码可以显示在 LCD 上,比如“S1 ERR”,方便排查是哪个通道出了问题。

// 带超时的等待高电平/低电平 #define TIMEOUT 200 unsigned char DHT11_ReadData(unsigned char *temp, unsigned char *humi) { unsigned char buf[5], i, j; DHT11_Start(); if (DHT11_WaitLevel(0, TIMEOUT)) return 1; // 等待响应低电平超时 if (DHT11_WaitLevel(1, TIMEOUT)) return 2; // 等待响应高电平超时 for (i = 0; i < 5; i++) { buf[i] = DHT11_ReadByte(); } if (buf[4] != (buf[0] + buf[1] + buf[2] + buf[3])) return 3; // 校验和错误 *humi = buf[0]; *temp = buf[2]; return 0; // 成功 }

这段代码里,DHT11_WaitLevel是带超时的电平等待函数,返回 0 表示等到,返回 1 表示超时。buf[0]是湿度整数,buf[2]是温度整数,buf[4]是校验和。校验和错误返回 3,说明数据在传输过程中被干扰了,常见原因是数据线太长或者没有上拉电阻。参数上,TIMEOUT设 200 次循环,在 12MHz 下大约几百微秒,足够覆盖正常响应时间,又不会让系统卡太久。

3.2 LCD1602 双通道显示布局与刷新节流

LCD1602 只有两行,每行 16 个字符。双通道显示要在一屏里塞下两组温湿度,布局必须紧凑。我一般这样排:第一行显示“T1:25C H1:60%”,第二行显示“T2:26C H2:58%”。温度用整数,湿度用整数,省掉小数点,字符数刚好够。如果要做温差显示,可以把第二行改成“dT:+1C dH:-2%”,但这样就看不了第二通道的绝对值,取舍看需求。

刷新不能太频繁。LCD1602 的写命令需要时间,如果你每次循环都刷,屏幕会闪,而且占用大量 CPU 时间。我的习惯是 1 秒刷一次,和 DHT11 采集节奏对齐。刷新时不要整屏清空再写,那样会闪;直接定位到对应位置覆盖写,视觉上更稳。LCD 的LCD_SetCursor函数负责定位,第一行地址是 0x80,第二行地址是 0xC0,加上列偏移即可。

// 双通道显示刷新,每秒调用一次 void Display_Update(unsigned char t1, unsigned char h1, unsigned char t2, unsigned char h2) { LCD_SetCursor(0, 0); LCD_WriteString("T1:"); LCD_WriteNum(t1); LCD_WriteString("C H1:"); LCD_WriteNum(h1); LCD_WriteString("% "); // 末尾空格覆盖旧字符 LCD_SetCursor(1, 0); LCD_WriteString("T2:"); LCD_WriteNum(t2); LCD_WriteString("C H2:"); LCD_WriteNum(h2); LCD_WriteString("% "); }

这段代码里,LCD_SetCursor(row, col)把光标定位到指定行列,LCD_WriteNum把整数转成字符写入。末尾的"% "两个空格是为了覆盖上一次显示留下的多余字符,比如上次湿度是 100,这次是 60,不覆盖就会显示“60%0”。参数上,t1、h1、t2、h2都是 0~99 的整数,DHT11 的湿度范围是 20%~90%,温度是 0~50℃,整数显示足够。

3.3 主循环的时间片调度

51 单片机没有操作系统,主循环就是你的调度器。我一般把主循环分成三个时间片:10ms 处理按键(如果有)、100ms 处理 LCD 局部刷新、1000ms 处理 DHT11 采集和整屏刷新。用一个定时器产生 10ms 中断,在中断里累加计数,主循环根据计数值判断该执行哪个任务。这样采集和显示互不阻塞,系统响应更顺。

注意 DHT11 读取期间不能被打断太久,否则时序会乱。所以读 DHT11 的时候我会临时关掉总中断,读完再开。关中断的时间大概几毫秒,对按键扫描影响不大,但能保证时序稳定。这个细节很多教程不讲,但实际调试时,如果你发现数据偶尔跳变,八成就是中断打断了 DHT11 的时序。

// 主循环时间片调度 void main(void) { unsigned char t1, h1, t2, h2; unsigned int tick = 0; Timer0_Init(); // 10ms 定时中断 LCD_Init(); while (1) { if (tick % 100 == 0) { // 每 1 秒采集一次 EA = 0; // 关中断,保护 DHT11 时序 if (DHT11_ReadData(&t1, &h1) == 0) { /* 通道 1 成功 */ } if (DHT11_ReadData(&t2, &h2) == 0) { /* 通道 2 成功 */ } EA = 1; // 开中断 Display_Update(t1, h1, t2, h2); } if (tick % 10 == 0) { Key_Scan(); // 每 100ms 扫描按键 } } }

这段代码里,tick由定时器中断每 10ms 加一,tick % 100 == 0表示每 1 秒执行一次采集,tick % 10 == 0表示每 100ms 扫描一次按键。EA = 0和EA = 1是关总中断和开总中断,保护 DHT11 的时序不被定时器中断打断。参数上,定时器初值根据晶振频率计算,12MHz 下 10ms 定时器初值是 0xDC00 左右,具体要看定时器模式。

4. 双通道 DHT11 系统避坑与常见问题排查

4.1 湿度读数一直是 0 或 255

现象:LCD 上湿度显示 0% 或者 255%,温度正常或者也不正常。原因通常是 DHT11 启动信号不对,或者采样点偏移。DHT11 对启动低电平时间要求大于 18ms,如果你只延时了 10ms,传感器根本不响应。另一个原因是Delay_us不准,40us 采样点实际只有 20us,把数据 1 误判成 0,或者把 0 误判成 1,读出来的字节全错。解决方法是先用示波器抓数据线波形,确认启动低电平宽度和响应高电平宽度,再回头校准延时函数。没有示波器就用逻辑分析仪,几十块的那种够用。

4.2 两个传感器数据互相串扰

现象:通道 1 和通道 2 的读数经常一样,或者一个正常一个乱跳。原因如果是单 IO 分时供电方案,可能是电源切换不够快,上一颗传感器还没完全断电,下一颗就开始通信,总线电平被残留电荷拉低。解决方法是增加电源切换后的稳定延时,或者干脆改成双 IO 独立方案。如果是双 IO 方案还串扰,检查两颗传感器的数据线是不是走线太近,产生了耦合,分开走线或者加地线隔离。

4.3 LCD1602 显示乱码或只亮不显

现象:LCD 背光亮,但屏幕没有字符,或者显示一排黑块。原因通常是 P0 口没加上拉电阻,4 位模式下数据线电平拉不高,LCD 控制器收不到正确数据。另一个原因是初始化序列不对,4 位模式需要先发三次 0x33 唤醒,再发 0x32 切换到 4 位,很多人直接发 0x38 就完事,结果 LCD 还在 8 位模式等着。解决方法是补上拉排阻,并严格按照 4 位初始化时序写代码。对比度电位器也要调,调得太偏也会看不到字符。

4.4 系统运行一段时间后死机

现象:开机正常,跑几分钟后 LCD 不再刷新,按键也没反应。原因多半是 DHT11 读取时死循环了,while(!DHT11_DATA)没有超时退出,传感器一旦掉线,程序就卡在那里。解决方法是所有等待环节都加超时计数器,超时后返回错误码,主循环继续跑。另外,51 的看门狗如果开了,要记得喂狗,否则也会复位。我一般会在主循环里加一个 LED 闪烁指示,闪就说明程序在跑,不闪就是卡了,比猜快。

4.5 温度读数偏差大

现象:DHT11 读出来的温度比实际高 3~5℃。原因是传感器自发热,或者安装在发热元件旁边。DHT11 的精度本身是 ±2℃,如果你把它贴在稳压芯片或者单片机旁边,读的就是板子温度。解决方法是把传感器用导线引出来,远离热源,并且两次采集之间保持 1 秒以上间隔,减少自发热。如果还嫌精度不够,那就不是 DHT11 的问题了,该换 SHT30 或者 DS18B20,但那是另一个方案的事。

5. 从双通道到多点的扩展技巧与验证方法

双通道跑通之后,最自然的想法是扩展到三通道、四通道。但 51 的 IO 有限,每增加一颗 DHT11 就多占一个 IO,很快就不够用。这时候可以考虑用 74HC4051 模拟开关做通道扩展,用 3 个 IO 选择 8 个通道,数据线共用一根。代价是每次切换通道后要等模拟开关稳定,再启动 DHT11,时序上多一层延时。这个方案我试过,8 通道轮询一圈大概 8 秒,做环境监测够用,做实时控制就偏慢。

验证系统稳不稳定,我一般做三个测试。第一是“长时间跑”,连续运行 24 小时,看 LCD 有没有花屏、数据有没有跳变。第二是“拔插测试”,在运行中拔掉一颗 DHT11,看系统能不能检测到错误并继续显示另一颗的数据,而不是死机。第三是“对比测试”,用一颗标准温湿度计放在旁边,看两个通道的读数和标准值的偏差是否在 ±2℃ / ±5% 以内。这三个测试做完,基本能判断方案能不能落地。

// 错误状态显示:通道故障时在 LCD 上提示 void Display_Error(unsigned char ch, unsigned char err) { LCD_SetCursor(ch, 0); // ch=0 第一行,ch=1 第二行 LCD_WriteString("CH"); LCD_WriteNum(ch + 1); LCD_WriteString(" ERR"); LCD_WriteNum(err); // 错误码:1 响应低超时,2 响应高超时,3 校验错 LCD_WriteString(" "); }

这段代码里,ch是通道号,err是错误码。把错误码显示出来,排查时一眼就能看出是超时还是校验错。超时通常是接线问题,校验错通常是干扰问题。参数上,错误码 1 和 2 对应DHT11_WaitLevel的返回值,3 对应校验和失败。显示时末尾加空格覆盖旧内容,避免残留字符。

我自己的习惯是,每次调 DHT11 之前先把延时函数用示波器校一遍,确认 1us、10us、1ms 三个档位准了再往下写。这个步骤花十分钟,能省掉后面两小时的瞎猜。双通道系统看起来简单,但两个传感器轮询、LCD 刷新、中断保护这几件事叠在一起,任何一个环节没对齐都会翻车。把时序和超时这两件事做扎实,后面换传感器、换屏幕、换单片机,都是同一套思路。希望帮到你。

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

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

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

立即咨询