简介:面向51单片机与Proteus仿真学习者的温湿度报警控制系统完整资料包,以DHT11为温湿度传感器,四位数码管切换显示温度与湿度,支持上下限设定、超限声光报警及按键关闭报警。压缩包共41个文件,约1.16MB,包含Proteus仿真工程、Keil C源码(main.c与DHT11.c/h)、原理图SchDoc、流程图BMP、元件清单Excel及功能介绍文档,覆盖硬件设计到软件调试的完整链路。目前已有267人浏览学习。仿真工程可直接运行,直观复现温湿度实时显示、阈值设置与报警联动;代码与原理图模块对应,便于理解DHT11时序读取、数码管动态扫描、按键消抖等51单片机常用外设编程。整体结构清晰,适合课程设计、毕业设计或电子竞赛备赛,也可作为新手从基础到综合项目的练手资源。
1. 51单片机用DHT11拼数码管,报警系统最难的不是代码
做温湿度报警控制这类课设或小项目,51单片机、DHT11、数码管这三个词组合在一起几乎是固定搭配。硬件上就是一颗STC89C52或者AT89C51,接一个DHT11传感器,再把温湿度数值扔到数码管上显示,超过阈值就驱动蜂鸣器或者继电器。听起来结构简单,但真正动手做过的人都知道,最折腾人的不是C语言逻辑,而是DHT11那根单总线数据线的时序,以及数码管动态扫描时亮度与闪烁之间的取舍。Proteus仿真在这个项目里地位很特殊——它既能帮你把数码管段码、位选逻辑提前调通,又能让你在不碰烙铁的情况下验证DHT11的时序是否合法。这篇文章就按我平时做这个项目的顺序来讲:先立住硬件选型和原理,再写代码,接着在Proteus里跑仿真,最后落到物料清单和工程文件组织这些容易被忽略的细节上。
适合看这篇文章的人有两类:一类是正在做课程设计的学生,需要快速把整个系统跑起来并讲清楚原理;另一类是工作中偶尔要搭一个低成本环境监测节点的工程师,想确认51方案在什么场景下仍然值得选。无论是哪类,读完你至少能回答三个问题:DHT11的数据手册时序怎么落到代码上、四位数码管怎么用两个锁存器或者三极管完成动态扫描、Proteus里仿真和实物之间到底差在哪。
2. 系统拆分:51单片机、DHT11和数码管的接口与选型
2.1 为什么这个系统适合51而不是直接上STM32
先回答一个很多人会问的问题:都什么年代了,为什么还用51单片机做温湿度报警?原因很简单——这个系统的计算负载和IO需求极其有限。DHT11是一根单总线,每秒最多读两次,每次40个bit;数码管是4位共阴或者共阳,动态扫描只需占用8个段选加4个位选;报警输出就是一路电平翻转。整个系统加起来不到15个GPIO,CPU主频12MHz甚至6MHz都够用。STM32在这个场景下属于杀鸡用牛刀,而且Proteus里51的仿真模型比STM32成熟得多,教学和演示场景尤其合适。
具体型号上,最常见的选择是STC89C52RC或者AT89C51。两者都是8051内核,区别在于STC支持ISP下载、内置RC时钟,Proteus里一般用AT89C51或者AT89C52替代也完全兼容。如果你手里只有STC芯片,在Proteus里选AT89C51一样能完成仿真,管脚定义完全相同。
2.2 DHT11的工作原理和通信协议要点
DHT11是盛思锐(实际上是非数字温湿度传感器的国产经典件)生产的单总线数字温湿度传感器,内部包含一个电阻式湿度元件和NTC测温元件,通过一个8位MCU把温度和湿度数据打包成40位数据帧输出。数据帧结构是固定的:8位湿度整数 + 8位湿度小数 + 8位温度整数 + 8位温度小数 + 8位校验和。
通信是主机主动发起——主机拉低总线至少18ms,然后释放并拉高,DHT11检测到这个起始信号后响应一个80us的低电平,接着拉高80us表示准备发送数据。之后就是高低电平交替的比特流——我一般直接看高电平的持续时间来判断0和1:高电平约26us到28us是逻辑0,约70us是逻辑1。这里要注意比如DHT11的数据手册上写的规范值,Proteus仿真模型会严格按这个时序工作,如果你在真实硬件上实现时忽略时序精度,就会出现读数恒定或者CRC校验失败。
测量范围和精度也要心里有数:湿度5%到95%RH,精度正负5%;温度0到50摄氏度,精度正负2摄氏度。这个精度做环境监测和报警足够了,但别指望它做精确计量。如果你想做更高精度的方案,DHT22会稍好一点,但代码时序几乎一样。
2.3 数码管的类型选择:共阴还是共阳,几位合适
数码管的选型直接影响驱动电路设计。常见的接法是4位一体数码管,共阴或共阳都有。在这个项目中我倾向于用共阴数码管,原因后面代码部分会详细说——主要是Proteus中仿真和使用74HC573锁存器驱动时,共阴的逻辑更直观。
位数的选择取决于你要显示的内容。温湿度报警系统通常要显示两组数据:温度和湿度,每组至少两位有效数字,加上小数点和特殊符号(比如H和C的标识),最少需要4位。如果你还想显示设定阈值或状态码,那5位或6位更稳妥,但代码复杂度会上升。我这里按4位一体数码管来展开,实际项目中如果做成两个2位分体式数码管也没问题,逻辑相同。
数码管的驱动电流是另一个关键点。单个LED段的正常工作电流是5到10mA,8个段全亮时单个位需要40到80mA,如果直接用P0口灌电流驱动,单片机引脚会过载。常见做法是段选端加限流电阻(220Ω或330Ω),位选端加三极管或直接用74HC573锁存器。Proteus仿真中如果忽略驱动电流,显示效果看不出问题,但做实物时必须按这个思路来。
2.4 报警执行机构:蜂鸣器、LED还是继电器
报警输出从简单到复杂有几种选择方式:
纯蜂鸣器方案。一支有源蜂鸣器接一个NPN三极管(如S8050),单片机P2.0输出高电平驱动基极,蜂鸣器导通发声。这是最简单也最常见的,适合纯提示报警。
蜂鸣器加LED方案。LED接在另一个IO口上,亮起表示报警状态,蜂鸣器响表示触发声音提醒。适合课设展示,因为报警状态可视化更好。
继电器方案。如果你的应用是控制外部设备,比如风扇或加热器,就需要用继电器隔离控制交流设备。继电器选5V线圈的,用ULN2003驱动是比较稳妥的选择,因为ULN2003内部自带续流二极管,能吸收继电器断开时的反向电动势。
我这里按最简单的蜂鸣器报警来写,同时在Proteus里加一个LED作为报警指示灯。如果你要做实物,加一个继电器也不影响代码结构。
3. 程序框架与核心代码:DHT11时序、数码管扫描和报警逻辑
3.1 程序的整体工作流程
这个系统的软件逻辑非常简单:主循环里读传感器、刷新显示、比较阈值、决定是否报警。正常状态下,温湿度不超过设定范围时,数码管持续显示当前值;超出范围后,蜂鸣器响、报警LED亮,数码管依旧显示实时值。
需求指标列表:
- 每2秒刷新一次温湿度数据(DHT11的最快采样周期是2s,实际建议3s到5s)
- 数码管0.5ms刷新一次显示,保证无闪烁
- 温度阈值和湿度阈值在代码中宏定义,方便修改
- 蜂鸣器报警时用延时产生断续音,而不是一直响
整个程序由三大部分构成:DHT11底层驱动、数码管显示驱动、主循环报警控制逻辑。下面分别展开。
3.2 DHT11底层驱动:C语言实现时序的关键写法
3.2.1 引脚定义和初始化
先定义连接引脚。DHT11的DATA引脚接P2.1,数据线是开漏输出,真实硬件上必须接4.7k到10k上拉电阻,Proteus仿真模型内部自带弱上拉,但为了和实物一致,建议在仿真图里也加上。
#include <reg51.h> sbit DHT11_DQ = P2^1; // DHT11数据线 void delay_us(unsigned int us) { while (us--) { _nop_(); // 12MHz下约1us } }逻辑说明:
- 定义sbit是为了按位寻址,方便对单个引脚操作。
_nop_()在intrins.h头文件中定义,执行时间约为一个机器周期。12MHz晶振下,一个机器周期是1us,所以delay_us函数近似可以做到微秒级延时。- 这个延时函数只是个粗略实现,精度不高,但DHT11的时序容差允许对时序要求有一定自由度,关键在于拉低起始信号18ms要足够长,这是用延时函数不是用定时器做的最重要原因。
3.2.2 起始信号和总线释放
接下来是主机发送起始信号,然后释放总线读取从机响应。
unsigned char DHT11_Start(void) { unsigned char timeout = 0; DHT11_DQ = 0; // 主机拉低总线 delay_us(20000); // 至少18ms,20ms更保险 DHT11_DQ = 1; // 释放总线 delay_us(40); // 等待从机响应 if (!DHT11_DQ) { // 从机拉低响应 timeout = 0; while (!DHT11_DQ && timeout < 100) { // 等待80us低电平结束 timeout++; delay_us(1); } if (DHT11_DQ) { while (DHT11_DQ && timeout < 100) { // 等待80us高电平结束 timeout++; delay_us(1); } return 1; // 起始成功 } } return 0; // 无响应 }逻辑说明:
- 20ms的拉低时间远大于规格书要求的18ms,多出来2ms是为了兼容STC单片机IO速度的差异,实测有效。
- 从机响应是一个80us低电平+80us高电平——响应信号和准备发送数据的间隔,两个while循环分别等待这两个阶段结束,超时保护防止死循环。
- 这里最关键的一点:起始信号之后,DHT11会拉低总线80us作为响应,然后拉高80us,之后才开始传输数据位。这个响应信号容易被忽略,导致后续读数据错位。
3.2.3 读取一个字节:电平宽度判0和1
读取数据位是DHT11驱动的核心,也是最容易踩坑的地方。按照数据手册描述,每个数据位都是由一个低电平时隙(50us)加一个高电平时隙组成——高电平时隙较短的是0,较长的是1。判断方法很简单:测高电平持续时间。
unsigned char DHT11_ReadByte(void) { unsigned char i, data_byte = 0; unsigned int timeout; for (i = 0; i < 8; i++) { timeout = 0; // 等待低电平结束 while (!DHT11_DQ && timeout < 100) { timeout++; delay_us(1); } if (timeout >= 100) return 0; // 超时,总线卡死 delay_us(40); // 关键:延时40us后再读电平 if (DHT11_DQ) { data_byte |= (0x80 >> i); // 高电平持续超40us则为1 timeout = 0; while (DHT11_DQ && timeout < 100) { // 等待高电平结束 timeout++; delay_us(1); } } } return data_byte; }逻辑说明:
- 这个函数的关键点在第10行:低电平结束后等待40us再读引脚。因为高电平持续时间小于40us的是0,大于40us的是1,所以在40us这个时间点采样,就能区分0和1。
- 位顺序是MSB先行,所以
(0x80 >> i)是从最高位开始填数据。 - 每次读完一个bit后要等待高电平结束,否则下一个bit的低电平时隙会被误判。
- 这个判别方法是Proteus仿真和实物都能通过的写法。还有另一种写法是测量高电平宽度然后和40us比较,但那样需要更精确的计时方式,代码也复杂一些。
3.2.4 完整读取40位数据并校验
有了上面的基础,完整读取函数就是把湿度整数、湿度小数、温度整数、温度小数、校验和依次读出来,再做一次校验。
unsigned char DHT11_ReadData(unsigned char *hum_int, unsigned char *hum_dec, unsigned char *temp_int, unsigned char *temp_dec) { unsigned char hum_i, hum_d, temp_i, temp_d, checksum; if (!DHT11_Start()) return 0; // 起始失败直接返回 hum_i = DHT11_ReadByte(); hum_d = DHT11_ReadByte(); temp_i = DHT11_ReadByte(); temp_d = DHT11_ReadByte(); checksum = DHT11_ReadByte(); if ((hum_i + hum_d + temp_i + temp_d) != checksum) { return 0; // 校验失败 } *hum_int = hum_i; *hum_dec = hum_d; *temp_int = temp_i; *temp_dec = temp_d; return 1; }参数说明:
hum_int和hum_dec:湿度整数部分和小数部分(DHT11小数部分通常为0,只有DHT22才有实际小数)。temp_int和temp_dec:温度整数和小数部分。- 返回值1表示成功、0表示失败。调用方(主循环)直接根据返回值决定要不要刷新显示。
- 校验和算法是四个字节相加后取低8位,如果等于校验字就通过。这是DHT11协议里唯一的错误检测机制,不能省。
3.3 数码管显示驱动:段码表、动态扫描和消隐
3.3.1 段码表和共阴共阳的选择
数码管显示的本质是把一个字节映射到8个LED段(a到dp),共阴数码管的段码是逻辑高电平点亮,共阳则是低电平点亮。下面给出共阴数码管0到9的标准段码,如果用的是共阳,取反即可。
| 字符 | 共阴段码 | 共阳段码 | 字符 | 共阴段码 | 共阳段码 |
|---|---|---|---|---|---|
| 0 | 0x3F | 0xC0 | 5 | 0x6D | 0x92 |
| 1 | 0x06 | 0xF9 | 6 | 0x7D | 0x82 |
| 2 | 0x5B | 0xA4 | 7 | 0x07 | 0xF8 |
| 3 | 0x4F | 0xB0 | 8 | 0x7F | 0x80 |
| 4 | 0x66 | 0x99 | 9 | 0x6F | 0x90 |
3.3.2 动态扫描原理
动态扫描的原理是利用人眼视觉暂留——让4位数码管分时轮流点亮,每位的点亮时间约1ms到2ms,4位一轮总共4ms到8ms,刷新频率在125Hz以上就不会有闪烁感。看似是轮流亮,但因为切换够快,人眼看就是同时亮的。
Proteus仿真中有一个仿真速度的问题需要特别提醒:Proteus的时序仿真不是实时运行的,如果你的扫描切换过快或者过慢,都会出现显示异常,比如只有一位亮、亮度不均等。我一般把每一位的显示时间设成2ms,一轮扫描8ms,这个速度在Proteus里和实物上都能稳定工作。
3.3.3 P0口加锁存器还是直接用三极管
这是一个硬件选型问题。两种常见方案:
方案A:P0口直驱段选,P2口低4位接PNP三极管做位选。数码管通常共阴,位选用P2口控制三极管的基极——这样低电平选中,进行放大,让对应位点亮。8个段选各串一个330Ω限流电阻。
方案B:P0口接74HC573锁存器,锁存器输出驱动数码管段选,位选通过另一个锁存器或三极管。两个锁存器一个控制段选一个控制位选,这样在显示刷新期间,锁存器的输出不会被新数据的输入干扰。
课设和大部分仿真用方案A足够了。74HC573方案的好处是单片机IO口得以释放,而且锁存器的输出驱动能力更强,适合驱动大尺寸数码管(1.2英寸以上)。如果你做实物而且手头有573芯片,方案B的电路也不复杂。
3.3.4 显示函数:段码查表、位选切换、延时和消隐
下面这段是核心显示代码:
unsigned char code seg_code[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F}; sbit BELL = P2^0; // 蜂鸣器控制 sbit DHT11_DQ = P2^1; // DHT11数据线 // 共阴数码管位选定义 sbit DIG1 = P2^2; sbit DIG2 = P2^3; sbit DIG3 = P2^4; sbit DIG4 = P2^5; void Display_Process(unsigned char *disp_buf) { unsigned char i; unsigned char code bit_sel[4] = {0x01, 0x02, 0x04, 0x08}; for (i = 0; i < 4; i++) { // 先切断所有位选,防止上一位残留 DIG1 = DIG2 = DIG3 = DIG4 = 0; // 输出段码 P0 = seg_code[disp_buf[i]]; // 如果该位需要点亮小数点,则 P0 |= 0x80 // 选中当前位 switch (i) { case 0: DIG1 = 1; break; case 1: DIG2 = 1; break; case 2: DIG3 = 1; break; case 3: DIG4 = 1; break; } delay_us(2000); // 每一位显示2ms } }逻辑说明:
seg_code[]是共阴数码管0到9的段码表,code关键字表示存到程序存储器而不是RAM,节约宝贵的128字节RAM空间。- 第一个关键操作是切断所有位选(消隐)。如果不做这一步,上一个数字的残影会叠加到当前位上,产生拖影。
switch用来选中当前要显示的位。这里用的是共阴+高电平选择位的接法,对应P2口直接驱动位选。- 2ms的延时决定了亮度和刷新率。想要更亮就适当加长,但超过4ms会出现闪烁,这个要把握好。
3.4 报警逻辑:阈值比较和断续蜂鸣
报警逻辑是整个系统里最简单但最容易考虑不周的部分。直接上完整主函数代码:
void main(void) { unsigned char hum_i, hum_d, temp_i, temp_d; unsigned char disp_buf[4]; unsigned char temp_threshold = 30; // 温度报警阈值:30度 unsigned char hum_threshold = 80; // 湿度报警阈值:80%RH bit alarm_state = 0; unsigned int alarm_counter = 0; while (1) { // 每2秒读取一次DHT11 if (DHT11_ReadData(&hum_i, &hum_d, &temp_i, &temp_d)) { // 换算成完整数值 if (temp_i > temp_threshold || hum_i > hum_threshold) { alarm_state = 1; } else { alarm_state = 0; } } // 更新显示缓冲区 // 显示格式:温度两位 + 湿度两位,例如“28 65” disp_buf[0] = temp_i / 10; // 温度的十位 disp_buf[1] = temp_i % 10; // 温度的个位 disp_buf[2] = hum_i / 10; // 湿度的十位 disp_buf[3] = hum_i % 10; // 湿度的个位 // 每秒扫描240次左右,共4位数码管 Display_Process(disp_buf); // 报警处理:报警时蜂鸣器鸣响0.1秒,停0.1秒 if (alarm_state) { alarm_counter++; if (alarm_counter % 10 < 5) { BELL = 1; // 蜂鸣器响 } else { BELL = 0; // 蜂鸣器停 } } else { BELL = 0; alarm_counter = 0; } } }参数说明:
temp_threshold和hum_threshold是报警阈值,直接在宏定义里改就行。如果你想在运行时调节,可以接两个按键,通过中断或者扫描修改这两个变量,后面第5章会提这个思路。- 报警判断条件是大于阈值,没有加迟滞。实际应用中,温湿度在阈值附近反复横跳会导致蜂鸣器频繁启停,改进方案是加一个迟滞区间——比如温度超过30度报警,低于28度才解除。这个在代码里就是两个不同的比较条件。
- 报警蜂鸣器采用的是时间片轮转思想:
alarm_counter每经过一个主循环加1,当值在0到4之间时蜂鸣器响,5到9之间时停,实现0.1秒响、0.1秒停的断续音效果,这样比一直响更容易引起注意。
3.5 显示缓冲区如何映射到实际温湿度
这里有一个很多人容易搞混的点:DHT11返回的数据是什么样的格式?直接看代码里的数据流处理。
处置方式如下:
- 如果DHT11返回温度整数为28,湿度的整数为65,那么
disp_buf[0]=2, disp_buf[1]=8, disp_buf[2]=6, disp_buf[3]=5,数码管就会显示“28 65”。 - 如果你想要在数码管上同时显示“C”和“H”这种单位标识,可以在温度显示完后切到对应的带小数点或者特殊字符的段码。但4位数码管通常是这样显示的:第2位显示完温度个位后,点亮该位的小数点,表示分隔。这个操作在Display_Process里可以通过给P0的段码按位或一个0x80来实现。
- 小数部分(
hum_dec和temp_dec)在DHT11中基本都是0,所以显示时直接丢弃了。
4. Proteus仿真搭建:从原理图到流程图和物料清单
4.1 Proteus里搭建最小系统的步骤
Proteus仿真在这个项目中比实物调试更有优势——DHT11模型可以直接在元件库里找到,它的时序反应和真实芯片几乎一致,而且你可以随时暂停仿真观察引脚电平,这在实物上用示波器才能做到。
新建工程后的步骤如下:
- 选择AT89C51芯片。不需要外部时钟电路,Proteus默认模拟12MHz晶振。
- 放置DHT11元件,在元件库中搜索“DHT11”或“DHT11 Humidity-Temperature Sensor”,直接拖入画布。
- 放置一个4位一体共阴数码管,元件名通常是“7SEG-MPX4-CC”或“7SEG-MPX4-CA”。注意CC是共阴,CA是共阳。
- 放置一个蜂鸣器元件和一个LED,蜂鸣器不接三极管也可以响,Proteus模型不关心驱动电流。
- 放置电阻排用于段选限流,简化画法可以放一个RP1排阻,阻值设为220Ω。
连接方式完全对应代码中的引脚定义:
- P0口 → 段选(通过220Ω排阻)
- P2.2到P2.5 → 数码管位选(直接连接)
- P2.1 → DHT11的DATA引脚
- P2.0 → 蜂鸣器正极(负极接地)
- P0口其他引脚不需要外接上拉,数码管段选本身就有电流路径
如果你之前用的是共阳数码管,需要做两个调整:一是段码表按列取反,二是位选控制逻辑要反过来——共阳数码管的公共端是接高电平的,所以位选要用低电平选中。代码中的DIGx = 1变成DIGx = 0。
4.2 仿真时常见的时序和速度问题
Proteus仿真单片机和实物有一个本质区别:Proteus的指令执行是软件模拟的,它的推进速度和实际晶振频率基本一致,但和编译器的优化、宿主机性能都有关。以下是我在仿真中用得最顺手的经验值:
| 参数 | 实物推荐值 | Proteus推荐值 |
|---|---|---|
| DHT11起始信号拉低时间 | 18ms-20ms | 18ms-20ms(保持一致) |
| 数据位判定延时 | 40us | 40us |
| 数码管每位显示时间 | 1ms-2ms | 2ms-5ms |
| 主循环周期 | 无要求 | 尽量加一点延时 |
特别要强调的是,DHT11的起始信号如果用了delay_ms(20)这种函数,要确认编译器的优化不要把这个20ms的延时代码优化掉。我见过有人把起始信号延时写成delay_us(20000),结果被Keil优化成了空操作,导致DHT11的时序完全错误——这是因为优化器认为这个循环没有副作用,直接丢弃了。解决办法是在delay函数内部加volatile修饰变量或者在循环体内加一个_nop_(),让它无法被完全优化。
4.3 流程图的作用:不仅是文档,更是debug工具
对这个项目来说,流程图至少要画到子程序级。我画流程图的习惯是这样的:主程序流程图只画到关键判断,DHT11读取子程序单独画一个详细流程,包括超时处理和校验分支,因为这是最容易出bug的地方。数码管扫描流程可以只画函数内部逻辑,不用画主循环上下文。
画流程图的方式可以是Visio、draw.io,甚至是纸上手绘再拍照贴文档。关键在于流程图的逻辑要和代码一致。很多人流程图里画的是“温度大于阈值就报警”,但代码里实际写的是“温度大于阈值且湿度不大于阈值才算报警”,这就出现了文档和代码不一致的问题。建议在流程图每个判断框旁边标注代码中对应的行号或变量名,这样评审或答辩时一目了然。
4.4 物料清单的编写思路
物料清单看起来简单,但实际写出来很多人会漏项。下面给一份这个项目的标准清单模板:
| 序号 | 物料名称 | 规格/型号 | 数量 | 备注 |
|---|---|---|---|---|
| 1 | 单片机 | STC89C52RC(DIP40) | 1 | 可用AT89C51替代 |
| 2 | 晶振 | 12MHz | 1 | |
| 3 | 电容 | 22pF | 2 | 晶振匹配电容 |
| 4 | 电容 | 10uF/16V | 1 | 复位电路 |
| 5 | 电阻 | 10kΩ | 1 | 复位电路 |
| 6 | 电阻 | 4.7kΩ | 1 | DHT11上拉 |
| 7 | 排阻 | 220Ω x 8 | 1 | 数码管段选限流 |
| 8 | 数码管 | 4位共阴(0.36英寸) | 1 | |
| 9 | 温湿度传感器 | DHT11 | 1 | |
| 10 | 三极管 | S8050 | 2 | 位选驱动和蜂鸣器驱动 |
| 11 | 蜂鸣器 | 有源5V | 1 | |
| 12 | LED | 红色 | 2 | 电源指示和报警指示 |
| 13 | 按键 | 轻触型 | 2 | 可选,用于调阈值 |
| 14 | 电源 | USB转TTL(5V)或DC插座 | 1 |
这个清单是可以直接拿去采购的。注意有源蜂鸣器和无源蜂鸣器的区别——无源蜂鸣器需要单片机输出一定频率的方波才会响,有源蜂鸣器只要通直流电就会响。如果你用的是无源蜂鸣器,代码里的BELL = 1就要改成BELL输出一个约2kHz的方波信号。
4.5 从Proteus到实物的差异点
Proteus仿真通过之后,烧录到实物板上仍然可能出问题,这几点我在实际项目中反复踩过:
第一个差异是驱动能力。Proteus的数码管模型不考虑功耗和最大电流,P0口直接驱动的效果在仿真里看起来很完美,但实物的P0口是开漏输出,你必须外接上拉电阻才能正常工作。这一点初学者特别容易漏。
第二个差异是DHT11的响应时序。Proteus里的DHT11模型是理想化的,起始信号时序要求相对宽松。实物上如果延时偏短,DHT11可能不响应。因此代码里的起始信号拉低时间在实物上我建议至少20ms。
第三个差异是蜂鸣器驱动。Proteus仿真中一个IO口直接驱动蜂鸣器就能响,实物上用IO口直接接蜂鸣器,电流可能不足以驱动或者造成单片机口损坏,必须用三极管放大电流。
5. 进阶技巧:数据稳定性、按键调阈值和Proteus联合调试
5.1 连续多次读取取平均,用"多数表决"滤掉毛刺
DHT11传感器偶尔会出现偶发的读错误或者尖峰毛刺——尤其当供电电压不稳或者数据线过长时比较常见。这个问题的根本原因是单总线协议没有重传机制,读错一个字节就导致整帧校验失败或者读到异常值。我的处理方式是连续读三次,取多数值或平均值。
如下是配合现有代码的过滤方案:
- 连续调用三次
DHT11_ReadData,记录三次的温度和湿度值。 - 如果三次结果完全一致,直接用;如果有两次一致,取多数的值;如果三次都不同,检查校验成功与否再决定,如果三帧全部校验失败就沿用上一次的值,不加新数据。
这个方案的优点是只要代码里DHT11_ReadData返回值非1,主循环就不刷新显示,保留旧值,这样数码管上就不会出现跳动或者闪烁的0或255这种错误值。
5.2 加两个按键做阈值调节:中断扫描还是轮询扫描
有些课设要求阈值可以现场调整,这时候至少要两个按键:一个切换调节对象(温度阈值/湿度阈值),一个增加数值。如果再加一个按键做减少就更完整。
按键扫描放在主循环里的做法是时间轮询,这个方案有一个隐患——主循环里数码管扫描会占用较长时间,如果按键按下时间太短(比如小于主循环周期),可能漏掉按键事件。解决方案有两个:
一是把按键扫描放到定时器中断里。10ms定时器中断,在中断里调用按键扫描函数,判断按下和释放状态,更新阈值变量。这样做键盘响应准确,不受主循环阻塞影响。
二是用外部中断,把按键接在INT0和INT1上。下降沿触发,在中断服务函数里直接增减阈值。这是资源占用最少的方式,但如果按键没接硬件去抖电路,需要在中断里加软件消抖延时。
无论用哪种方式,阈值变量都要是全局变量,并且注意在Proteus中仿真中断的时序和实物上略有差异,中断服务函数里不要放数码管扫描这类耗时过长的操作。
5.3 用Proteus的数字示波器验证DHT11时序
这个技巧对排错很有用。Proteus虚拟示波器可以挂在DHT11的数据线上,直接观察通信时序波形。
操作方式:在Proteus里放置一个Virtual Terminal或者Digital Oscilloscope工具,把DHT11的数据线连接到示波器的A通道,运行仿真后就能看到数据传输时的波形。判断标准的波形特征:起始信号是一段约20ms的低电平,然后拉高。响应信号是80us低电平+80us高电平。数据位波形是一组高低电平交替,0的高电平短(约27us),1的高电平长(约70us)。你用虚拟示波器观察这个波形,配合代码逻辑会清楚地看到问题出在主机侧还是从机侧。
即使没有示波器,也可以通过Proteus运行时的引脚电平颜色来做粗判断:引脚上有高电平会显示红色方块,低电平是蓝色方块,中间过程中的电平变化通过慢速仿真可以看到跳动。
5.4 温湿度传感器的替代方案:什么时候该换DHT22
如果这个项目要升级为一个真正长期运行的节点,DHT11的精度和稳定性会成为瓶颈。DHT22(AM2302)是直接替换方案中的首选,它的湿度精度正负2%,温度精度正负0.5摄氏度,比DHT11整整高一个档次。而且时序协议几乎完全一样,唯一区别是数据帧中的小数部分有实际意义,需要两个字节才能完整表示。
代码上要做三个调整:一是hum_dec和temp_dec不再是固定0,要参与显示;二是读取到的16位数据要先判断最高位是否为1——为1表示负温度,实际值要取补码再除以10;三是DHT22的起始信号要求至少1ms的拉低时间,而不是DHT11的18ms,但这两种方案都统一用20ms也没问题。
如果你要用I2C接口的高精度传感器,比如SHT30或SI7021,那就需要改动较大,不仅通信协议不同,代码结构也要从单总线驱动改成I2C驱动,这个改动在51上做成本偏高,更适合直接切到STM32平台。
最后提醒一个代码习惯:调试阶段可以在Keil里用软件仿真单步运行到DHT11的读取函数,跑到40us延时那里仔细观察变量变化——这个位置的时序正确性是整个系统能够正确显示和报警的前提。
本文还有配套的精品资源,点击获取