STC89C51与DHT11多点测温:从单总线原理到Proteus仿真实现 简介基于STC89C51单片机与DHT11温湿度传感器这套多点测温方案是一套完整的课设/毕设参考资料面向单片机初学者及有开发需求的学习者。方案实现了两个测点的实时温度采集采用DHT11单总线驱动配合LCD1602液晶显示现场温度每个测点均可独立设置温度上限和下限当实测值越界时LCD对应位置显示“H”高温或“L”低温报警标志逻辑清晰、可直接复用。压缩包内共24个文件包含C语言源码.c/.h、Keil工程文件.uvproj/.uvopt、Proteus仿真设计.dsn/.dbk/.pwi、Hex固件及编译过程文件.obj/.lst等其中Proteus仿真可离线运行观察现象Hex文件可烧录验证工程源码便于二次开发。包体仅72KB轻量完整。已有199人浏览学习足见其参考价值。读者可以获得完整的双通道测温程序框架、DHT11驱动写法、LCD1602多行显示与上下限报警实现思路对理解单片机多路采集、阈值比较和人机交互设计有明确帮助适合快速移植用于课程设计、毕业设计或电子竞赛基础方案。1. STC89C51 DHT11 多点测温方案先从一根线讲起看到“多点测温”就把几个 DHT11 并联到一根 IO 线上是最常见的错误。DHT11 虽然走单总线协议但不像 DS18B20 那样带可寻址的 64 位 ROM 序列号多个器件并联等同于总线冲突读回来的数据会同时受到各路传感器的拉拽既不是 1 点的值也不是 2 点的值。真正把“STC89C51 DHT11 多点测温 Proteus 仿真”这条路径跑通要解决三件事给每路传感器分配互不干扰的 IO、把 40 位数据帧的读取写成按通道轮询的 C 代码、在 Proteus 里完成模型搭接并调好仿真参数。下面的内容按这个顺序展开驱动源码、接线表、编译配置和排错方法都会给出适合正在做课设或者想在画板前先用仿真验证方案的人。2. 多点测温选型为什么每个 DHT11 必须独立占一根 IO2.1 DHT11 单总线时序起始、应答与 40 位数据帧DHT11 的数字输出只有一个 DATA 引脚总线空闲时由上拉电阻维持在逻辑 1。主机发起一次读取时先把数据线拉低至少 18ms然后释放总线并等待 20-40us这个过程称为起始信号。DHT11 感知到起始信号后会把数据线拉低 80us 表示应答再拉高 80us之后开始按位输出 40 位数据8 位湿度整数、8 位湿度小数、8 位温度整数、8 位温度小数和 8 位校验和。每一位的传输格式都相同先由传感器拉低 50us再释放总线让上拉电阻把电平拉高。高电平持续 26-28us 表示逻辑 0持续约 70us 表示逻辑 1。区分 0 和 1 的唯一方法是测量高电平的持续时间没有第二个参考信号。整个读取过程由主机单方面发起传感器之间没有片选或地址机制这是 DHT11 不能像 DS18B20 那样在同一条总线上挂多个的关键原因。DS18B20 允许一根总线挂几十个节点靠 64 位序列号逐个寻址DHT11 数据手册里没有地址匹配流程多设备被同一根线拉住时每个传感器都会按自己的时序驱动总线数据互相叠加后主机无法识别信息来源。所以多点测温的常见做法是给每个 DHT11 分配独立的 GPIO 引脚软件上把同一套读取逻辑在多个引脚之间轮询。STC89C51 的 P2 口有 8 根准双向 IO空闲状态下内部上拉把电平拉高恰好符合 DHT11 总线空闲时的电平要求。接三个传感器只占 P2.0-P2.2剩下的引脚足够留给液晶、按键和蜂鸣器。这里要顺带提醒STC89C51 的 P0 口是开漏结构内部没有上拉如果传感器安排在 P0必须在外部各加一只 4.7k 上拉电阻否则高电平能力不足读回的数据会出现随机跳变。2.2 引脚分配与硬件连接表常见的接法是三个 DHT11 分别接到 P2.0、P2.1、P2.2传感器编号与引脚编号一一对应驱动代码里用通道号做位运算循环三次就能把三个点的温湿度全部读回。三个传感器的 VCC 和 GND 分别并联到 5V 和 GNDDATA 线各自接一只 4.7k 电阻上拉到 VCC。P2 口内部虽然有上拉外部上拉能提供更陡的上升沿减小位采样误差在 Proteus 仿真里这个上拉电阻还承担着让模型稳定输出高电平的任务缺失时会出现数据全为 0 的现象后面排错章节会专门说明。信号STC89C51 引脚外部器件参数DHT11-1 DATAP2.0R1 上拉到 VCC4.7kDHT11-2 DATAP2.1R2 上拉到 VCC4.7kDHT11-3 DATAP2.2R3 上拉到 VCC4.7kDHT11 供电VCC / GND三路并联5VLCD1602 RS / RW / ENP3.5 / P3.6 / P3.7字符液晶控制线LCD1602 D4-D7P1.0-P1.3字符液晶4 线数据模式晶振建议用 12MHz一个机器周期正好 1usDHT11 的位时序是几十微秒量级延时函数里按 1:1 换算最直观。如果换 11.0592MHz机器周期约 1.085us每次延时都要做小数折算改参数时容易出错。24MHz 也不是不能用但 Proteus 仿真时高主频会放大仿真速度不均带来的时序偏差建议先在 12MHz 下跑通再考虑提速。选 STC89C51 作为主控主要图实物阶段用 USB-TTL 就能 ISP 下载不需要额外编程器。但大部分 Proteus 元件库里搜不到 STC89C51 的原生模型常见做法是直接用 AT89C51 代换。两者指令集、引脚排列和 SFR 完全兼容仿真阶段不涉及下载协议差异只体现在 Flash 容量和下载方式上。标题写的是 STC89C51仿真工程里放的通常是 AT89C51这个对应关系在联调时不用纠结直接按同一颗芯片对待。3. 驱动代码把 40 位数据帧写成多点轮询的 C 语言3.1 延时精度与 Keil 优化等级谁在偷偷改你的时序代码的关键在于把“通道号”传进读取函数而不是给每个传感器复制一份完整函数。DHT11 的时序敏感但标准 C51 里 sbit 不能作为函数参数传递所以要先做一层端口封装。常见做法是把多个 DHT11 放在同一个 P 口的连续引脚上用P2 ~(1 ch)这类位运算选中通道另一种做法是为每路引脚单独维护一组函数用 switch 分支调度。第一种代码紧凑、便于循环第二种跳转少、时序一致性更好。推荐先按第一种搭起来如果实物读值不稳定再切到第二种对比差异通常能定位到分支跳转引入的微小时序偏移。Keil C51 在较高优化等级下会把无副作用的短循环直接折叠或调整执行顺序从而破坏延时函数的时长。这是 DHT11 驱动“时灵时不灵”的一个隐藏原因尤其是从网上直接抄来的延时函数在一台机器上编译能跑换到另一版本的 Keil 上就频繁超时。稳妥的做法是在 Options for Target - C51 - Optimization 里把 dht11.c 单独设置为 Level 0主文件则保持默认优化两者互不干扰。另外注意 Keil C51 的 int 默认是 16 位unsigned int 最大只能到 65535延时循环里的计数变量要选对类型否则一开优化参数循环次数就溢出回绕起始信号的 18ms 直接缩水成几毫秒。3.2 驱动源码请求、读字节、读数据帧三个函数下面给出的代码基于 12MHz、STC89C51Proteus 中用 AT89C51环境三个通道接 P2.0-P2.2可直接放进 Keil C51 工程编译。#include reg51.h #include intrins.h #define DHT11_PORT P2 // 三路 DHT11 的 DATA 统一挂在 P2 口 #define DHT11_MASK 0x07 // 只操作 P2.0-P2.2避免误动作其他引脚 /* 12MHz 下1 个机器周期约 1us */ void delay_us(unsigned int n) // 微秒级延时n 是循环次数 { while (n--) _nop_(); } void delay_ms(unsigned int n) // 毫秒级延时经验循环基数 114 ≈ 1ms { unsigned int i, j; for (i 0; i n; i) for (j 0; j 114; j); } /* ch 0,1,2 对应 P2.0,P2.1,P2.2 */ void DHT11_Start(unsigned char ch) // 发起起始信号 { DHT11_PORT ~(1 ch); // 主机拉低对应 DATA 线 delay_ms(20); // 持续 20ms留足余量 DHT11_PORT | (1 ch); // 释放总线靠上拉恢复高电平 delay_us(30); // 等待 30us 后进入接收状态 } unsigned char DHT11_ReadByte(unsigned char ch) { unsigned char i, dat 0; for (i 0; i 8; i) { while (((DHT11_PORT (1 ch)) 0)); // 等待 50us 低电平结束 delay_us(40); // 延时 40us 后再采样 dat 1; if ((DHT11_PORT (1 ch)) ! 0) // 高电平仍在判为逻辑 1 dat | 0x01; while (((DHT11_PORT (1 ch)) ! 0)); // 等待当前位高电平结束 } return dat; } unsigned char DHT11_ReadData(unsigned char ch, unsigned char *humi, unsigned char *temp) { unsigned char h1, h2, t1, t2, sum; DHT11_Start(ch); // 发送起始信号 if ((DHT11_PORT (1 ch)) ! 0) return 1; // 未等到应答低电平 while (((DHT11_PORT (1 ch)) 0)); // 跳过应答低电平 80us while (((DHT11_PORT (1 ch)) ! 0)); // 跳过应答高电平 80us h1 DHT11_ReadByte(ch); // 湿度整数 h2 DHT11_ReadByte(ch); // 湿度小数 t1 DHT11_ReadByte(ch); // 温度整数 t2 DHT11_ReadByte(ch); // 温度小数 sum DHT11_ReadByte(ch); // 校验和 if ((h1 h2 t1 t2) ! sum) return 2; // 校验失败 *humi h1; *temp t1; return 0; // 读取成功 }返回值的约定要说明白0 代表成功1 代表应答超时2 代表校验和错误。调用方可以根据返回值决定是否刷新显示三个通道互不影响。所有 while 等待都在读 P2 引脚值而不是读锁存器准双向口在读取引脚前必须先保证对应位输出 1DHT11_Start 里已经用|做了这一步。这里还有一个 H 代码细节DHT11 的温度整数位最高位是符号位温度低于 0 摄氏度时最高位为 1赋值给*temp的其实是一个大于 127 的无符号数做显示时需要判断这个位并取补码否则零下温度会显示成一百多度。3.3 主循环三路轮询加 1 秒刷新间隔主程序用一个 for 循环依次读三个通道数据放进两个长度为 3 的数组再交给 LCD 刷新。DHT11 对连续读取有最短间隔要求通常建议不小于 1 秒传感器需要这段时间把内部状态复位所以每次轮询完成后至少要延时 1000ms。LCD 显示用 4 线模式接线对应前面的表格初始化和写字符函数沿用常见的基础驱动即可重点是把刷新动作放在主循环里而不是嵌进读取函数。void main(void) { unsigned char humi[3] {0}, temp[3] {0}; unsigned char ch, ret; LCD_Init(); // P1.0-P1.3 数据P3.5-P3.7 控制 while (1) { for (ch 0; ch 3; ch) { ret DHT11_ReadData(ch, humi[ch], temp[ch]); if (ret 0) LCD_ShowTempHumi(ch, humi[ch], temp[ch]); else if (ret 1) LCD_ShowError(ch, 1); else LCD_ShowError(ch, 2); } delay_ms(1000); // 轮询间隔保底 1 秒 } }LCD 刷新本身耗时较长如果使用 8 线逐字符刷新三个传感器一轮主循环可能接近 500ms。读完 DHT11 再去刷新 LCD总线已被上拉维持在高电平不会丢失数据而且传感器下一次被访问前已经有足够时间恢复。真正要警惕的是读取过程中被中断打断。比如定时器中断里做了 LED 扫描或按键消抖中断服务的时间如果超过几十微秒DHT11 一个数据位的高电平时长就会被拉长原本的逻辑 0 被误判成逻辑 1表现为温度偶尔跳高几度又恢复。中断里尽量只置标志位不要在中断服务里写 LCD 或跑长延时。4. Proteus 仿真搭建与排错AT89C51 代换的完整步骤4.1 元件检索与接线清单Proteus 8 Professional 元件库里输入 DHT11 就能找到温湿度传感器模型通常在 Sensors 分类下元件名也叫 DHT11。单片机搜索 AT89C51放置后双击把 Clock Frequency 改为 12MHz。液晶选 LM016L这就是 LCD1602 的 Proteus 模型。三者加上三只 4.7k 上拉电阻和基础复位电路就可以连线不需要额外器件。元件Proteus 搜索关键字数量设置项单片机AT89C511Clock Frequency: 12MHz温湿度传感器DHT113默认参数字符液晶LM016L14-bit 模式上拉电阻RES34.7k晶振CRYSTAL112MHz复位电容CAP110uF复位电阻RES11kDHT11 在 Proteus 里的引脚通常标注为 VCC、GND 和 DATA部分版本会显示 OUT 或 SDA。DATA 和 VCC 接反后仿真不会报警但读回来的数据全是 0x00排查时优先检查这三个引脚的连接顺序。单片机 RST 引脚接一只 10uF 电容到 VCC、一只 1k 电阻到 GND这是仿真中最稳定的复位配置。EA 引脚务必接 VCC如果悬空或接地单片机可能进不了片内 Flash 的地址空间程序启动后反复复位LCD 长时间不出字。4.2 最影响结果的三个仿真参数第一个参数是上拉电阻。Proteus 的 DHT11 模型内部开漏结构不完整某些版本下不加上拉电阻数据线高电平幅度不够单片机读到的 1 会落在阈值附近抖动。具体表现是校验和总失败或者湿度一直显示 255。4.7k 是数据手册里的典型值仿真时换成 1k 能让上升沿更陡读取稳定性更好功耗在这个场景下可以忽略。第二个参数是仿真速度。Proteus 的仿真不是实时执行速度可能只有真机的数十分之一LCD 刷新和动画显示开启时会进一步拖慢。DHT11 读取函数里的 while 等待在慢速仿真下会被拉长delay_us(40) 实际占用的时间失真位采样点随之偏移。改善的办法不是反复改延时常量而是先把 LCD 的动态刷新关掉比如数值没变化时跳过重复刷新或者关闭 Proteus 的 Graphical Animation 选项。仿真时序的改善幅度比改代码明显得多。第三个参数是晶振频率与延时常数的匹配。Proteus 里双击 AT89C51 改的是芯片时钟Keil 编译 hex 时不知道这个频率两者必须手动保持一致。Proteus 设为 11.0592MHz 而代码按 12MHz 计算时起始信号的 18ms 会被拉长到 19.5ms位采样点偏离 4% 左右协议余量内勉强能跑但会让边界值判断不稳。建议统一用 12MHz实在要用 11.0592MHz把 delay_us 的循环基数按 11.0592/12 折算。4.3 Keil 工程组织与 hex 加载方式工程按功能拆分三个文件main.c 负责主循环轮询dht11.c 放驱动函数lcd1602.c 放液晶驱动。dht11.c 在 Options for Target - C51 - Optimization 里把 Level 设为 0保证时序循环不被编译器改动main.c 和 lcd1602.c 可以保持默认优化。编译后到 Output 目录拿 hex 文件在 Proteus 里双击单片机Program File 一栏指定路径即可。还有一个常被忽略的配置Target 页里的晶振频率只影响 Keil 的软件仿真对硬件烧录没有作用。Code Rom Size 建议选 Large避免代码超出默认 64KB 模型。dht11.c 里尽量不用全局变量通道号和临时数据全放在函数内部避免跨文件优化和重入问题。这个习惯对事后把代码移植到 STM32 或其他 51 增强型芯片同样有用DHT11 的时序解析逻辑可以原样保留只需替换底层的端口读写宏。5. 验证与进阶用 Proteus 虚拟示波器校准三路读取时序仿真跑通不等于读到的温度可信。Proteus 左侧工具栏找到 Virtual Instruments拖出 Digital Oscilloscope把三路 DHT11 的 DATA 引脚分别接到 A1、B1、C1 通道运行仿真并触发单次采样能清楚看到主机拉低 20ms 的起始信号、80us 的应答脉冲以及紧随其后的 40 个数据位。把 DHT11_ReadByte 里的 delay_us 从 40us 缓慢往下调观察逻辑 1 的脉宽变化就能找到这批代码在当前仿真速度下最稳的采样点。如果 40us 处读到的高电平脉宽已经不足 60us说明仿真环境比真实时序慢需要把延时基数减小反之脉宽超过 90us则要加大循环基数避免采样点逼近下一个低电平。三个通道共用一份驱动代码彼此之间没有数据交互逐通道验证是定位硬件问题最有效的手段。先把后两路的读取注释掉只留通道 0确认单路稳定且在示波器上能看到干净的 40 位波形再依次打开通道 1 和通道 2。如果某路打开后总返回 2校验失败回到 Proteus 检查对应引脚的接线和 DATA 上拉而不是怀疑驱动函数。这里有一个使用 P2 口要留意的坑DHT11_PORT ~(1 ch)这类语句是读-修改-写操作执行时会先把 P2 口 8 个引脚的当前电平都读进来再整体写回。如果没接传感器的引脚上恰好有按键输入或跳线帽接低电平写回操作会把其它引脚的状态一起改写造成不明抖动。建议把未使用的 P2 高位统一设为 1或者在硬件上避免让无关信号和 DHT11 共用同一个端口。实物移植时STC89C51 对时序的敏感性通常比 Proteus 模型更严格。DHT11 供电要在靠近传感器位置放一颗 100nF 去耦电容DATA 线尽量短远离继电器和电机驱动线。最后保留一个可复现的校准流程把 Proteus 里调好的延时参数记录到代码注释中实物上电后先跑单通道模式读回数据与温湿度计对比偏差再逐个启用其余通道。多点测温的真正难点不在传感器本身而在如何让多路轮询在共享端口、中断和慢速外设的夹击下保持稳定这个验证顺序能把这层风险拆开逐一排掉。本文还有配套的精品资源点击获取