简介一套基于51单片机的温湿度采集监控系统工程面向嵌入式初学者和物联网环境监测开发者解决温湿度实时采集、日历时钟显示与超标报警等常见需求适用于农业大棚、工业仓储、家庭环境等场景。工程以Keil C51为开发环境包含SHT10温湿度传感器驱动、DS1302日历时钟驱动、LCD12864液晶显示与调时功能并支持温湿度越限时蜂鸣器/LED报警源码结构清晰便于学习外设编程。压缩包共28个文件主要涵盖.c源码、.h头文件、.lst列表、.obj目标文件、.hex烧录文件以及Keil工程配置等整体仅84KB轻量完整可快速编译烧录验证。目前已有353人学习浏览可用于课程设计、毕业设计或实际环境监控项目的功能参考与二次开发。借助工程中的hex文件可直接烧录到单片机查看效果而通过源文件可深入理解传感器时序、时钟芯片读写、液晶显示等关键代码。1. 温湿度采集监控这个题目51单片机为什么还在被反复做我最早接触“51单片机实现的温湿度采集监控”是帮一个朋友改库房里的温度报警器要求不高能定时测温湿度、能显示、超过阈值能响。当时我下意识想换个大牌MCU结果朋友甩过来一板子经典51最小系统说成本就十几块钱折腾坏了也不心疼。后来我把这个方案打通发现它几乎是入门到实战最顺的一条路一根单总线连着传感器一块1602液晶显示一个蜂鸣器报警逻辑清楚到新手三天能跑通老手也能拿它做产品原型。这篇文章就把这个标题拆开讲传感器怎么选、单总线时序怎么读、电路怎么搭、代码怎么写、哪些坑最常踩最后再说串口上报和掉电保存怎么加。2. 硬件选型与温湿度传感器原理DHT11/DHT22怎么选单总线时序到底在说什么2.1 传感器选型DHT11够用、DHT22什么时候才值得温湿度采集监控的传感器有很多种SHT系列、AHT系列、DHT系列都有但51项目里最常见、资料最全的还是数字单总线方案。这里不绕圈子直接说结论普通库房、机房、暖房监控DHT11完全够用如果你要测精度要求高的环境或者经常在低温低湿下工作才去考虑DHT22。对比项DHT11DHT22温度精度±2℃±0.5℃湿度精度±5%RH±2%RH温度范围0~50℃-40~80℃湿度范围20~90%RH0~100%RH采样周期≥1s≥2s价格很便宜贵约3倍接口单总线单总线从表格能看出DHT11精度虽然一般但监控场景真正关心的不是0.1℃的差别而是“温度有没有超过28℃”“湿度是不是低于40%”这类阈值判断。DHT11便宜而且数据帧格式、驱动逻辑比I2C传感器更直白非常适合51这种没有硬件外设的老内核。DHT22适合做环境记录仪或者对温湿度波动敏感的场景比如档案室、实验室这时多花几十块钱换精度是值得的。选型还有一个容易忽略的点DHT11采样周期是1秒你不可能像读按键一样连续读它主循环里每次读完后至少要等500ms再发起下一次读。这个限制不是传感器“性能差”而是单总线协议的设计后面写代码时会再提到。2.2 单总线协议一根线怎么既送命令又回数据DHT11只用一根数据线既能接收主机的启动信号又能把温湿度数据吐回来。听起来很玄其实原理就是一段严格的时间电平变化。开始通信时主机先把数据线拉低至少18ms然后释放并由主机拉高20~40us这是告诉传感器“我要读数据”。接着DHT11会主动把线拉低80us再拉高80us表示“我准备好了”。之后连续输出40位数据格式固定湿度整数、湿度小数、温度整数、温度小数、校验和。校验和的算法是前四个字节相加结果的低8位等于第五个字节。比如湿度整数50、湿度小数0、温度整数26、温度小数0加起来是76那么校验字节就是76。如果不相等这一帧数据直接丢弃这是单总线通信里最实用的自校验手段。把这40位读完每一位的读法也靠时序区别每个数据位开始都是50us低电平然后拉高。如果高电平维持约26~28us表示这一位是0如果高电平维持约70us表示这一位是1。所以读数据的关键不是数高电平持续时间而是采样点从低电平结束开始延时40us再去读数据线此时读到低就是0读到高就是1。这个40us的延时很讲究51单片机用循环延时会有误差后面避坑章节会专门说。2.3 最小系统与引脚分配P2口做什么P3口做什么硬件上除了单片机本身还需要晶振、复位电路、电源、下载串口。我用最常见的51增强型芯片晶振用11.0592MHz原因很简单这个频率给串口波特率计算比较容易9600波特率误差很小。如果你确定不上位机串口用12MHz去驱动DHT11也没问题但后续扩展串口时会麻烦所以建议一步到位。引脚分配我习惯这样定DHT11数据线接P2.0LCD1602数据线接P0.0~P0.7LCD控制线RS、RW、E分别接P2.5、P2.6、P2.7蜂鸣器接P1.0两个按键接P3.2和P3.3。P0口内部没有上拉必须外接一排10k排阻否则LCD显示会出现乱码或者完全无显示。P2.0作为单总线也最好加上4.7k上拉电阻这样传感器空闲时数据线能稳定在高电平抗干扰更强。引脚功能说明P0.0~P0.7LCD1602数据口D0~D7必须接上拉电阻P2.5 / P2.6 / P2.7LCD1602 RS / RW / E控制线P2.0DHT11 DATA加上拉4.7kP1.0蜂鸣器低电平驱动或三极管驱动P3.2 / P3.3阈值调整按键加消抖P3.0和P3.1留给串口这也解释了为什么要选11.0592MHz晶振。如果你打算做完整的上报功能P3口就不要被其他外设占用太多否则代码连软件串口都不好编。整个模块化思路是采集一块、显示一块、控制一块后面写代码会按这个结构拆文件。3. 电路连接与实物搭建面包板到洞洞板的接线顺序3.1 51最小系统的三件套晶振、复位、电源去耦51最小系统看起来简单但新手翻车最多的地方往往不是传感器而是最小系统本身。晶振电路一般用两个22pF左右的瓷片电容分别接到晶振两端再接地电容值不用太较真20pF到30pF都能起振。复位电路用10uF电解电容和一个10k电阻上电瞬间电容充电RST引脚短暂为高完成复位。有条件的话在电源VCC和GND之间并一个1040.1uF去耦电容防止电机、继电器动作时把电压拉偏。搭电路顺序我建议先不要插DHT11和LCD先把最小系统做出来下载一个点亮LED的小程序验证单片机在运行。这个验证很重要因为在面包板上插了传感器和液晶后万用表都很难量出了问题会分不清是单片机没跑还是LCD没初始化。等点亮LED成功再接LCD显示“Hello”最后再接DHT11。按这个顺序走能省掉大量排查时间。3.2 DHT11和LCD1602接线表LCD1602是16脚直插注意背光供电和对比度调节。第3脚VL要通过一个10k电位器接到GND或者分压到VCC与GND之间。很多新手接完发现“液晶亮了但屏幕上什么字都没有”十有八九是VL脚电位不对要么太亮、要么太暗调电位器就能解决。LCD1602脚位接法1 VSSGND2 VDD5V3 VL10k电位器中间脚4 RSP2.55 RWGND只写不读6 EP2.77~14 D0~D7P0.0~P0.715 A5V背光16 KGND背光RW直接接地意思是液晶永远处于写模式不需要读忙标志代码会更简单一点但因为不用读忙每次写命令后要固定给几毫秒延时不要连续写。DHT11三个引脚如果是直插式通常是VCC接5V、DATA接P2.0、GND接地DATA和VCC之间再加一个4.7k上拉到5V。有些模块板板上已经带上拉电阻如果是裸传感器就必须自己加。3.3 供电和上拉为什么DHT11数据线要加4.7k上拉DHT11的数据线是漏极开路输出本身没有主动驱动高电平的能力。空闲时它需要靠外部上拉电阻把线拉高到5V通信时传感器通过拉低电平来表示信号。如果省掉这个上拉数据线会浮空读出来的数据要么全是0要么偶尔正常、偶尔乱跳。上拉电阻阻值选择4.7k到10k都可以我用4.7k如果线长超过30cm还可以换成2.2k增强驱动能力但不要用1k以下否则传感器拉低时电流偏大容易影响电平判断。还有一个容易忽略的点DHT11模块如果用的是3.3V供电而单片机是5V系统那输出的高电平可能达不到5V但3.3V高电平对51来说基本能识别最好还是统一5V供电不要混电。面包板上用的杜邦线不要超过20cmDHT11数据线太长、线间电容增大单总线时序容易被拉变轻则校验失败重则读不到数据。3.4 下载电路与调试口下载电路常见做法是USB转TTL串口连接单片机的P3.0和P3.1芯片还需要冷启动先点击下载再给单片机上电。很多新手卡在这一步以为点下载就能烧录其实是程序在等待握手信号没有冷启动时自然一直失败。下载器输出的TXD接单片机的RXDP3.0RXD接TXDP3.1GND共地接反了也连不上。我一般还会留一个串口调试的排针座把P3.0/P3.1/GND/VCC引出来这样后期调试上位机时不用再飞线。调试口不是必须但强烈建议预留后面所有“串口上报”功能都要用到它。如果手头是ISP下载线注意下载线和串行调试口不要同时占用相同的引脚否则通信互相干扰。4. 软件实现从DHT11驱动到阈值报警的完整流程4.1 开发环境与工程结构C51开发环境基本上是一套IDE加编译器支持标准C的子集但和桌面C有一些区别变量类型要显式用unsigned char减少RAM占用函数参数尽量少中断函数有固定写法。我习惯把工程拆成dht11.c、lcd1602.c、main.c三个文件再加一个公共头文件。这样传感器驱动、显示驱动和业务逻辑分开以后想换成SHT传感器或OLED屏只动对应模块。// main.c #include reg52.h #include dht11.h #include lcd1602.h #include alarm.h void main(void) { unsigned char temp, humi; lcd_init(); lcd_write_str(0, 0, System Ready); delay_ms(500); while (1) { if (dht11_read_data()) // 读传感器并校验 { humi dht11_buf[0]; // 湿度整数 temp dht11_buf[2]; // 温度整数 lcd_show_temp_humi(temp, humi); alarm_judge(temp, humi); } else { lcd_write_str(0, 1, Sensor Error); } delay_ms(2000); // 2秒读一次满足DHT11采样周期 } }代码里最需要注意的是dht11_read_data()返回1时才算读到有效数据否则LCD显示“Sensor Error”。温度湿度都只取了整数位做监控完全够用如果你显示到小数位就把dht11_buf[1]和dht11_buf[3]也取出来拼成字符串。主循环用2000ms延时是故意不让你读太快DHT11两次采样间隔必须大于1秒读太快传感器会不响应。4.2 DHT11驱动代码起始信号、读40位、校验DHT11驱动是整个项目的核心也是最容易出问题的部分。下面是完整驱动代码包含起始信号、读字节和校验。// dht11.c #include reg52.h #include intrins.h sbit DHT11_DQ P2^0; unsigned char dht11_buf[5]; // 湿度整、湿度小、温度整、温度小、校验 void delay_us(unsigned int us) { while (us--) { _nop_(); _nop_(); } } void delay_ms(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) { for (j 0; j 110; j); } } unsigned char dht11_read_byte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { while (!DHT11_DQ); // 等待50us低电平结束 delay_us(40); // 延时到数据位中间 if (DHT11_DQ) dat (dat 1) | 0x01; // 读到高电平这一位是1 else dat (dat 1); // 读到低电平这一位是0 while (DHT11_DQ); // 等待高电平结束准备下一位 } return dat; } unsigned char dht11_read_data(void) { unsigned char i; DHT11_DQ 1; DHT11_DQ 0; delay_ms(18); // 起始信号拉低至少18ms DHT11_DQ 1; delay_us(30); // 拉高20~40us if (!DHT11_DQ) // 传感器响应信号 { while (!DHT11_DQ); // 等待80us低电平结束 delay_us(40); while (DHT11_DQ); // 等待80us高电平结束 for (i 0; i 5; i) { dht11_buf[i] dht11_read_byte(); } if ((dht11_buf[0] dht11_buf[1] dht11_buf[2] dht11_buf[3]) dht11_buf[4]) return 1; } return 0; }逻辑说明dht11_read_byte()里第一个while (!DHT11_DQ)是把数据位开头的50us低电平跳过稳定后延时40us采样。delay_us(40)不是绝对精确但必须保证在26us到70us之间采样太早会分辨不出0和1太晚可能已经跳到下一位的低电平。delay_ms(18)是用循环近似18ms晶振频率不同实际时间略有偏差只要不低于18ms就行多几百微秒不影响。参数说明DHT11只有三个引脚数据线必须接P2.0如果换了引脚只需改sbit DHT11_DQ P2^0;这一行。校验失败返回0后主程序会显示Sensor Error不要连续重试等下一次主循环的2秒间隔再读给传感器留出恢复时间。4.3 LCD1602显示与报警状态LCD1602驱动代码网上版本很多但我更推荐只写不读版本RW接地每次写命令或写数据后都延时几毫秒。下面这段初始化代码兼容市面上大多数1602液晶。// lcd1602.c #include reg52.h #include intrins.h sbit LCD_RS P2^5; sbit LCD_RW P2^6; sbit LCD_E P2^7; #define LCD_DATA P0 void delay_ms(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 110; j); } void lcd_write_cmd(unsigned char cmd) { LCD_RS 0; LCD_RW 0; LCD_DATA cmd; LCD_E 1; delay_ms(2); LCD_E 0; } void lcd_write_data(unsigned char dat) { LCD_RS 1; LCD_RW 0; LCD_DATA dat; LCD_E 1; delay_ms(2); LCD_E 0; } void lcd_init(void) { delay_ms(15); lcd_write_cmd(0x38); // 8位数据、2行显示、5x7字型 lcd_write_cmd(0x0C); // 显示开、光标关 lcd_write_cmd(0x06); // 写入后地址自动加1 lcd_write_cmd(0x01); // 清屏 delay_ms(2); } void lcd_write_str(unsigned char line, unsigned char col, unsigned char *str) { unsigned char addr 0; if (line 0) addr 0x80 col; else if (line 1) addr 0xC0 col; lcd_write_cmd(addr); while (*str) { lcd_write_data(*str); } }逻辑说明0x38是必须的初始化命令8位数据模式。0x0C关掉光标不然屏幕上会有一个闪烁的方块影响读数。写字符串函数里第一行和第二行地址不同第一行从0x80开始第二行从0xC0开始这个地址是硬件规定的写错会跑到另一行去。如果你LCD的数据线不是P0口记得把#define LCD_DATA P0改成对应端口。显示温湿度时我一般直接把字符拼接好再调用lcd_write_str避免用sprintf增加代码量。比如温度变量temp是26就拼成“T:26C H:60%”。这里有一个小坑数字要转换成ASCII字符temp / 10 0取十位temp % 10 0取个位。忘了加字符偏移‘0’会让液晶显示一堆乱码或花屏。4.4 主循环与按键阈值设置一个可上手的完整逻辑报警逻辑是“超阈值”才会响不是温湿度一变就响。我设置两个阈值温度上限默认28℃湿度下限默认40%。按键P3.2进入设置模式按键P3.3调整当前值。项目里还要加入报警标志否则蜂鸣器会一直响个不停。// alarm.c #include reg52.h sbit BEEP P1^0; sbit KEY_SET P3^2; sbit KEY_UP P3^3; unsigned char temp_max 28; // 温度上限 unsigned char humi_min 40; // 湿度下限 void delay_ms(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 110; j); } unsigned char key_scan(void) { if (!KEY_SET) { delay_ms(10); if (!KEY_SET) return 1; } if (!KEY_UP) { delay_ms(10); if (!KEY_UP) return 2; } return 0; } void alarm_judge(unsigned char temp, unsigned char humi) { if (temp temp_max || humi humi_min) { BEEP 0; // 低电平触发蜂鸣器 } else { BEEP 1; } }逻辑说明蜂鸣器接法如果是三极管驱动低电平导通所以报警时把引脚拉低。按键扫描用了两次判断加10ms延时消抖第一个if里检测到按下第二个if里确认还是按下才返回键值避免抖动导致误触发。设置模式里如果键值等于1就切换当前调节项等于2就在当前项上加1。这个实现没有处理长按连续加只是演示实际产品里建议每200ms检测一次按键状态按下超过500ms就自动连续加。报警判断很简单但很多人在这个模块翻车阈值设置一不小心和报警判断耦合在一起调完上限再去调下限当前项指针被复位出现误报警。解决办法是设置模式下不执行报警判断只有退出设置模式后才调用alarm_judge()。你可以在主循环里加一个mode标志当mode不为0时只显示和调阈值不做报警这样既能看到调整结果也不会让蜂鸣器打扰操作。5. 温湿度采集监控排查指南时序、上拉、供电与显示这些坑5.1 DHT11一直返回0或超时晶振频率与延时误差现象LCD上始终显示Sensor Error偶尔能出一次数据但很快又报错。用万用表量数据线电压发现通信时只有几个很窄的脉冲不像正常数据帧。原因DHT11对起始信号有严格要求主机拉低不能低于18ms。很多人直接套网上代码里面延时函数假设晶振是12MHz而你用的是11.0592MHz循环延时参数就不对了。更严重的是while(!DHT11_DQ)没有超时保护如果传感器没响应程序会一直死等整个采集监控系统看起来像死机了。解决先用示波器或逻辑分析仪看DHT11数据脚的波形确认起始信号低电平宽度是否够。没有示波器的话把delay_ms(18)改成delay_ms(25)多给点余量。所有while等待循环都加上一个超时计数超过一定次数就返回失败比如等待低电平时定义一个timeout100循环里每次减1减到0就return。这个习惯能救很多次不只是DHT11任何单总线设备都适用。5.2 温度正常、湿度跳变电源噪声与采样间隔现象温度读数稳定湿度显示忽大忽小比如上一秒60%下一秒变成0%而且传感器距单片机很近。原因湿度数据对时序比温度数据更敏感因为湿度字节是数据帧前两个字节如果起始信号不干净第一个字节最容易出错。另一个常见因素是电源纹波继电器、风扇启动瞬间把5V拉低DHT11的数据输出也跟着乱。如果供电用的是电脑USB口线太长也会有压降。解决给单片机电源并一个大电容比如100uF电解电容最好加在DHT11模块旁边。同时保证采样间隔至少2秒不要在主循环里频繁读取。我测试过把采样间隔从500ms改成2s湿度跳变概率明显下降。如果还跳排查4.7k上拉电阻有没有虚焊上拉电阻和DHT11模块之间不要用长跳线。5.3 液晶只亮不显示字符对比度电位器与忙检测现象LCD背光亮屏幕有黑块或者全空白无论写什么命令都不显示换一块新LCD也一样。原因背光正常只能说明电源接对了不代表LCD初始化成功。第一个常见原因是VL脚电压不对对比度太高或太低都会导致看不到字。第二个原因是数据线P0口没有上拉P0输出高电平时实际上高阻态LCD识别不了命令。第三个原因是初始化命令里连续写太快没给LCD足够的处理时间。解决先把VL脚电位器调到中间偏GND位置再观察屏幕有没有黑块出现。如果没有黑块检查P0排阻如果有黑块但不显示字符把初始化每条命令后的延时加大到5ms特别是0x38和0x01。再不行就把RW接地改成读忙标志方式或者直接购买带转接板的I2C LCD模块省去大量接线但驱动代码就要改成I2C协议。5.4 蜂鸣器乱叫按键消抖与阈值比较逻辑现象系统刚上电蜂鸣器就响或者按一下设置键报警状态反复变化甚至不按任何键也会触发。原因按键扫描没有消抖设置键按下时产生多次高低电平抖动被当成多次按键操作阈值被意外改大改小。阈值比较逻辑也有问题比如比较运算符写反或者把温度和湿度上限混在一起。另一个隐蔽原因是P3.2、P3.3引脚内部没有上拉按键悬空时引脚状态不确定读到低电平就以为被按下。解决按键引脚外部加上拉电阻软件消抖延时10ms以上如果按键接在P3口还要确认开发板上有没有默认上拉。报警判断里要先判断是否处于设置模式只有非设置模式下才更新蜂鸣器状态。我习惯再加一个“报警锁存”标志出现报警后必须按一次键才能解除避免蜂鸣器一直响到断电。5.5 烧录后没有任何反应“冷启动”与复位电路现象程序编译通过点击下载进度条一直停在“正在检测目标单片机”单片机没有任何反应LED也不闪。原因绝大多数是下载握手时序不对51下载需要先让单片机处于复位状态下载器发出连接信号再上电。很多人直接把下载器和单片机连好单片机已经上电了点击下载永远等不到冷启动。也有可能是复位电路不正常RST引脚一直为高单片机卡在复位状态。解决连接好串口后先把下载器切换到“下载模式”点击下载按钮然后立刻给目标板重新上电。如果目标板有电源开关每次下载都拨一次开关。检查复位电容和电阻是否接反复位电容正极接VCC负极接RST电解电容方向接反会导致上电复位时间极短甚至没有复位。用USB供电时还要先把USB线插好再按下载器的冷启动按钮。6. 进阶玩法串口上报、EEPROM掉电保存和低功耗改造6.1 用串口把温湿度报给上位机把温湿度监控从“本地显示”升级成“远程可看”最小改动就是加串口上报。使用11.0592MHz晶振波特率设为9600。发送数据时我加帧头和帧尾上位机按帧解析避免把连续两帧数据混在一起。void uart_init(void) { TMOD 0x20; // 定时器1工作在模式28位自动重装 TH1 0xFD; // 9600波特率对应初值 TL1 0xFD; TR1 1; SM0 0; SM1 1; // 串口工作在方式1 } void uart_send_byte(unsigned char dat) { SBUF dat; while (!TI); TI 0; } void uart_send_temp_humi(unsigned char temp, unsigned char humi) { uart_send_byte(0xAA); // 帧头 uart_send_byte(temp); uart_send_byte(humi); uart_send_byte(0x55); // 帧尾 }逻辑说明TMOD0x20把定时器1设成8位自动重装模式这是串口波特率最常用的配置。TH10xFD对应11.0592MHz下的9600如果你用12MHz晶振这个值误差偏大接收端可能会偶尔收到乱码这也是前面建议用11.0592MHz的原因。uart_send_byte里等待TI标志置1发送完成后TI要手动清零否则下一字节发不出去。6.2 阈值掉电保存与验证方法每次手工设置阈值断电后恢复默认值会很麻烦。增强型51芯片大多自带EEPROM可以拿来存阈值。写入前要先把目标地址擦除写入完成后读回验证。我只存temp_max和humi_min两个字节读出后和预期值比对不一致再写一次。注意EEPROM写次数有限制不要在循环里反复写只有按键调整且确认退出时才写。验证方法也很朴素把温度上限临时改成28℃用电烙铁靠近传感器等温度超过28℃蜂鸣器响然后把温度上限调回30℃再靠近传感器确认不响。湿度下限验证可以用保鲜袋包住传感器向袋子里哈气湿度超过下限后蜂鸣器停止或报警解除。整个验证过程不需要任何额外仪器但能同时检验传感器、阈值、报警三条链路是否都正常。6.3 低功耗改造与最终校准技巧如果这个采集监控要长期放在仓库里功耗问题就来了。最简单有效的办法是把采样周期从2秒改到30秒或60秒DHT11只在唤醒后读一次其余时间单片机进入空闲模式。继电器和蜂鸣器改成由三极管/MOS管驱动别用单片机引脚直驱既能省电也能保护引脚。用电池供电时把LCD背光调暗或定时关闭这样整机平均电流能压到几毫安。最后做一次传感器校准把DHT11和标准温湿度计放在同一个密封袋里等温度稳定后记录偏差。偏差是固定值时直接在显示代码里加上修正偏移比如显示温度为“读到的温度1”。这种校准方法没有温度箱也能做比盲目相信传感器厂商标称精度更可靠。我自己做这类项目时最大的教训不是代码难写而是太相信硬件默认值几乎每次都需要用实物验证一遍阈值。希望这篇内容能让你少走几趟弯路做完之后也能自己加上串口上报和掉电保存把一个小监控真正用在需要的地方。希望帮到你。本文还有配套的精品资源点击获取