基于51单片机的环境监测系统设计与Proteus仿真调试
2026/9/16 19:54:06 网站建设 项目流程

简介:面向51单片机学习者和电子设计开发者,提供一套基于51单片机的环境监测系统完整工程,涵盖温湿度、光照、硫化氢浓度与模拟量采集,适用于智能家居、实验室监测等场景。压缩包共包含56个文件,大小约1.4MB,内含Proteus仿真工程、原理图、流程图、Keil源程序、元件清单等,可对照硬件设计与软件代码进行系统学习。已有602人学习下载,适合课程设计、毕业设计及单片机项目开发参考。系统通过LCD1602屏幕实时显示各项参数,温度、湿度、光照和硫化氢超标时分别触发不同声音报警,同时点亮不同的LED指示灯;继电器联动控制中央空调、电动窗帘和空气净化器,自动完成环境调节。设计上使用DHT11采集温湿度、光敏电阻检测光照、TLC1543对外部模拟量进行转换,并支持S1键切换四个监测参数、S2至S5键修改报警阈值,方案逻辑清晰、便于二次开发与功能扩展。

1. 为什么这套环境监测系统把功夫花在“阈值联动”上

拿到这套基于 51 单片机的环境监测资源时,我第一反应是传感器型号比较常规:DHT11 测温湿度、光敏电阻测光照、硫化氢传感器输出模拟量,显示用 LCD1602,控制端是继电器。真正让这个项目区别于“传感器数据读出来然后显示”的,是它把阈值判断、声光报警、自动调节设备联动成了一个闭环。温度超限继电器开空调,光照超限电机转起来拉窗帘,硫化氢浓度高就启动净化器,而且四个报警用不同声音、不同 LED 区分。这意味着主程序里不只有采集,还有一套状态机式的比较逻辑,这恰恰是很多初学者容易写乱的地方。

这个资源适合正在做课程设计、毕业设计,或者想用 Proteus 把完整环境监测流程跑通的人。原理图、流程图、仿真图、物料清单和源码都齐全,你可以照着搭硬件,也可以先在 Proteus 里调阈值逻辑。下面我会从硬件选型说起,把 DHT11 的时序、TLC1543 的模拟量采集、继电器联动和仿真调试一个个拆开,最后落到一个我常用的仿真验证技巧上。

2. 硬件框架与关键器件选型:51 单片机如何接起一路环境信号

2.1 系统整体结构:传感器、ADC、显示和执行器

整个系统的信号流是“传感器 → MCU → 显示/执行”。温度湿度走 DHT11 单总线数据口;光照强度和硫化氢浓度都是模拟量,先经过 TLC1543 这个 10 位逐次逼近型 ADC 转成数字量,再交给单片机判读。LCD1602 负责显示当前温度、湿度、光照、硫化氢浓度以及超标的参数值,蜂鸣器和 LED 负责报警,继电器则控制空调(仿真中用小灯泡代替)、电动窗帘(用电机正反转代替)、空气净化器(仿真中风扇代替)这些执行设备。

从原理图Sheet1.SchDoc看,单片机最小系统是经典的 51 配置:晶振 12MHz、复位电路、上拉电阻。P0 口接 LCD1602 数据线,需要外接 10k 上拉电阻排;DHT11 挂在某个普通 IO 口上;TLC1543 的三个串行控制脚(CLK、CS、DATA OUT)和地址输入脚分别连接 P1 口的不同引脚。实际使用中我习惯把传感器电源和继电器电源分开,继电器线圈用三极管 8550 驱动,避免电机启停拉低单片机电压。

2.2 为什么选 TLC1543 而不是 ADC0832

ADC0832 在 Proteus 和实物中都很常见,8 位精度、两个模拟输入通道,但这里用 TLC1543 是合理的:首先 TLC1543 是 10 位精度,光照和硫化氢传感器的输出变化幅度不一定覆盖 0-5V 满量程,8 位 ADC 在电压变化 1V 时只有大约 51 个量化台阶,10 位可以到 204 个,分辨更细。其次 TLC1543 有 11 路模拟输入,将来如果想增加 PM2.5、CO2 或者更多传感器,不需要换芯片,只换通道就行。

下表是这套资源里几个关键器件的选型和作用,物料清单元件清单.xlsx里还有更细的型号和数量:

器件型号/规格作用连接要点
主控 MCUAT89C51 或 STC89C52采集、运算、控制P0 口接 LCD,注意上拉
温湿度传感器DHT11输出温度、湿度数字量单总线,接一个 IO 口,4.7k 上拉
模数转换TLC154310 位 ADC,处理光照和硫化氢模拟量串行时钟、地址、数据线接 P1
光照检测光敏电阻模块分压输出 0-5V 模拟电压同相端接 TLC1543 某通道
硫化氢检测模拟量输出传感器模块浓度越高输出电压越高输出接 TLC1543 另一通道
显示LCD1602显示实时数据及报警参数4 位或 8 位模式,本项目用 8 位
执行继电器模块 + 小灯泡/电机/风扇模拟空调、窗帘、净化器三极管驱动,注意续流二极管
报警蜂鸣器 + 四色 LED区分不同参数超限蜂鸣器用不同延时产生不同频率

2.3 电源与去耦设计容易被忽略的细节

Proteus 仿真里电源都是理想电压源,看不出问题,但实物测试时要特别注意:TLC1543 的参考电压REF+REF-直接决定 ADC 满量程,建议REF+接经过稳压的 5V,而不是直接接电池或变压器输出。光敏电阻分压电路中的分压电阻阻值要配合光敏电阻的亮阻暗阻选,常见做法是 10k 固定电阻串联光敏电阻到 5V,中间抽头接 ADC 输入端。硫化氢传感器模块如果是电化学型,一般已经有调理电路,但上电后需要预热几十秒,模拟量才稳定。

我一般在每个芯片电源脚旁边放 0.1uF 瓷片电容,继电器电源单独走线。这套资源里的原理图采用了 Altium Designer 格式,Sheet1.PDF可以直接预览,照着检查一遍电源极性比什么都重要。对于 51 单片机的 ALE 脚,如果不需要访问外部存储器,可以悬空,但不要把 P0 口直接驱动继电器,那电流不够。

3. DHT11 温湿度采集与 LCD1602 显示驱动:时序是唯一难点

3.1 DHT11 单总线协议的关键参数

DHT11 在 Proteus 里有时会因为没有加额外库而读不到数据,实物中则容易卡在时序上。DHT11 是单总线通信,主机必须先拉低总线至少 18ms,然后再拉高,等待传感器响应。之后传感器会拉低 80us、拉高 80us 作为应答,再输出 40 位数据:8 位湿度整数、8 位湿度小数、8 位温度整数、8 位温度小数、8 位校验和。

写读取函数时,微秒级延时不能用_nop_()一个个数,12MHz 晶振下_nop_()约 1us,但函数调用和赋值语句都会消耗时间。我一般用Delay_10us()这类基于循环的短延时函数,并且把 DHT11 的数据脚配置成准双向口,先输出高,再切换方向去读输入。下面这段是资源里DHT11.c中读取一次温湿度的核心逻辑,我注释了关键点:

// DHT11.c 关键读取函数 unsigned char dht11_read_byte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { while (DHT11_PIN == 0); // 等待数据线变高,表示起始位结束 Delay_10us(4); // 约 40us 后采样数据位 dat <<= 1; if (DHT11_PIN == 1) { // 如果仍为高,则此位为 1 dat |= 0x01; } while (DHT11_PIN == 1); // 等待该位结束 } return dat; }

这段代码的逻辑是:每个数据位都以 50us 低电平开始,随后是 26-28us 高电平表示 0,70us 高电平表示 1。主机在第 40us 左右采样,能稳定区分两种宽度。注意第一个while (DHT11_PIN == 0)必须在数据位开始前确保读到高,否则会在大约 80us 的起始低电平里卡住。实际调试中,如果校验和经常错误,优先检查这个Delay_10us(4)是否真的太慢。

3.2 LCD1602 读写时序与显示刷新策略

LCD1602 的驱动在这套资源里是lcd1602.c,基本操作是写命令和写数据。写数据前必须判断忙标志,否则会丢字符。我习惯用查询方式:不断读 DB7,直到它为 0 才认为 LCD 空闲。速度上,51 用 12MHz 晶振,LCD1602 的 E 引脚高电平持续时间至少要 450ns,所以写完之后延时 1ms 也没问题。

显示刷新时不要每次都清屏,因为清屏会闪。项目里温度、湿度、光照、硫化氢四个参数分布在不同行,我推荐把屏幕分成固定区域,每次只更新数值部分,用空格覆盖掉旧的超界字符。比如第一行显示 temp:25C humi:50%,第二行显示 lux:150 h2s:8ppm。报警时,LCD 要显示超标的参数数值,这意味着主循环里要有专门的显示模式状态:正常显示全部参数,报警时切换到对应参数的放大显示。资源里的main.c就是这么做的,报警标志置位后,显示函数根据当前报警类型刷新第二行内容。

// 显示数据,第二个参数为当前报警通道,0 表示正常 void lcd_display_environment(unsigned char alarm_channel) { char buf[17]; if (alarm_channel == 0) { sprintf(buf, "temp:%02d humi:%02d%%", temp_int, humi_int); lcd_write_string(0, 0, buf); sprintf(buf, "lux:%04d h2s:%03d", light_value, h2s_value); lcd_write_string(0, 1, buf); } else { // 报警时只显示超标的参数值,方便观察 lcd_write_string(0, 0, "Alarm! parameter:"); if (alarm_channel == 1) sprintf(buf, "temp: %02d C", temp_int); else if (alarm_channel == 2) sprintf(buf, "humi: %02d %%", humi_int); else if (alarm_channel == 3) sprintf(buf, "lux: %04d", light_value); else if (alarm_channel == 4) sprintf(buf, "h2s: %03d ppm", h2s_value); lcd_write_string(0, 1, buf); } }

sprintf在 Keil C51 里会引入较大代码量,如果 ROM 紧张,可以改成手动拼字符。这里用%02d保证两位数字占位,否则显示残留的旧字符会干扰阅读。LCD1602 的地址映射是每行 40 个字节,但显示区只有前 16 个,所以写第二行时地址要偏移0x40,这部分lcd_write_string函数内部已经处理。

3.3 DHT11 与 DS18B20 并存时怎么选

资源里还出现了Ds18b20.cDs18b20.h,说明作者可能在单片机温度采集上做过 DS18B20 和 DHT11 两种方案的切换。从项目描述看,最终温度来自 DHT11,因为 DHT11 同时给温度和湿度;DS18B20 只能测温度,而且精度 12 位下读取时间更长。如果你非要两个都用,建议不要共用同一个定时器中断,因为 DHT11 的时序对中断关断非常敏感。

DHT11 在 5V 供电时测量范围是 20%-90%RH,温度 0-50°C,正好覆盖项目要求的 45%-60%RH 和 15-25°C 区间。但 DHT11 的湿度精度只有 ±5%RH,如果你要更精确,就得换 SHT30。这里的阈值判断本身有回差,所以 DHT11 够用。

4. TLC1543 模拟量采集与阈值联动:10 位 ADC 的读通道姿势

4.1 TLC1543 引脚与串行读时序

TLC1543 有 11 个模拟输入通道(A0-A10),内部自带采样保持电路,控制接口是标准的 SPI 类似时序。读一个通道需要先指定通道地址,然后串行时钟 SCLK 逐位把结果移出来。TLC1543.c中的核心代码要实现一个函数:输入通道号,输出 10 位无符号整数。要注意的是 TLC1543 在第一个 SCLK 下降沿锁存地址,之后数据从 DATA OUT 移出,先出最高位。

下面是我整理后的读取函数,基于资源里的TLC1543.c

// 从 TLC1543 指定通道读取一次 ADC 值,返回 10 位结果 unsigned int read_tlc1543(unsigned char channel) { unsigned char i; unsigned int adc_value = 0; channel &= 0x0F; // 通道号只取低 4 位 CS = 0; // 先送通道地址,共 4 位,在 CLK 下降沿锁存 for (i = 0; i < 4; i++) { if (channel & 0x08) ADDR = 1; else ADDR = 0; channel <<= 1; CLK = 1; _nop_(); _nop_(); CLK = 0; _nop_(); } // 再读 10 位数据,在 CLK 上升沿后采样 DATA OUT for (i = 0; i < 10; i++) { CLK = 1; _nop_(); _nop_(); adc_value <<= 1; if (DATA_OUT) adc_value |= 0x01; CLK = 0; _nop_(); } CS = 1; return adc_value; }

参数说明:channel对应 TLC1543 的 A0-A10,本项目里光照接 A0,硫化氢接 A1。读周期内CS必须保持低电平,否则 TLC1543 会终止一次转换。代码里每次 SCLK 翻转后加了两个_nop_(),保证时钟低电平和建立时间满足 5V 供电下的芯片要求。实物中如果读到数据跳变厉害,把CLK高低电平的延时都改成延时 1us 的函数,不要只加_nop_()

4.2 光照强度与硫化氢浓度的电压标定

光照模块输出的是分压电压,光越强光敏电阻阻值越低,分压点电压越低还是越高取决于电路接法。资源里的阈值范围是大于 50lx 小于 300lx,这个区间对应电压值需要用可调电阻校准。怎么校准?在 Proteus 里直接用滑动变阻器模拟光敏电阻,调节阻值到光敏电阻 datasheet 上对应 50lx 和 300lx 典型阻值,然后记录 ADC 读数,再换算阈值。实物中更简单的方法:记录室内正常光和遮光时的 ADC 最大值和最小值,然后线性映射到 0-300lx。

硫化氢传感器模块一般输出 0-5V 对应 0-100ppm,但不同型号灵敏度不同,不能直接套。我通常用传感器数据手册里给出的灵敏度曲线,比如某电化学模块是 0.5V/ppm,那么 10ppm 对应 5V,ADC 参考电压 5V 时满量程 1023,阈值 ADC = 10 * 0.5 / 5 * 1023 = 1023,正好满量程,这也说明这个传感器输出偏高,可能需要分压电阻。记住:一定要根据实际模块改ADC_THRESHOLD宏,不要照抄资源里的默认值。

下面是阈值判断和继电器控制的核心逻辑,来自main.c中主循环:

#define TEMP_LOW 15 #define TEMP_HIGH 25 #define HUMI_LOW 45 #define HUMI_HIGH 60 #define LUX_LOW 50 #define LUX_HIGH 300 #define H2S_MAX 10 void alarm_check(void) { unsigned char alarm_flag = 0; if (temp_int < TEMP_LOW || temp_int > TEMP_HIGH) { alarm_flag = 1; // 温度报警 RELAY_AIR_COND = 1; // 开启空调 beep_different(0); // 低温用 1kHz,高温用 2kHz led_temperature_on(); } else { RELAY_AIR_COND = 0; } if (humi_int < HUMI_LOW || humi_int > HUMI_HIGH) { alarm_flag = 1; RELAY_AIR_COND = 1; // 湿度调节也用空调 // 实际可以用一个辅助继电器控制加湿/除湿 } if (light_value < LUX_LOW || light_value > LUX_HIGH) { alarm_flag = 1; RELAY_CURTAIN = 1; // 电机正转拉窗帘 } else { RELAY_CURTAIN = 0; } if (h2s_value > H2S_MAX) { alarm_flag = 1; RELAY_PURIFIER = 1; // 启动空气净化器 led_h2s_on(); } else { RELAY_PURIFIER = 0; } if (alarm_flag) { buzzer_on(); delay_ms(2000); // 报警一段时间后自动停止 buzzer_off(); } }

这段逻辑有几个值得注意的地方:温度和湿度超限会同时打开空调继电器,但空调的制冷和制热是不同的动作,仿真中用小灯泡代替时无法区分方向,所以实际做实物时,继电器后面要接两个控制回路,一个给压缩机,一个给加热丝。光照低于 50lx 和高于 300lx 都是问题,电机正反转分别代表开窗和关窗,在代码里要用两个 IO 控制电机驱动芯片方向脚,而不是简单置高一个继电器。报警中delay_ms(2000)是阻塞式,如果还要同时响应按键,最好把报警时长改成非阻塞计数,使用定时器节拍累加。

4.3 按键 S1-S5 的阈值设置实现

四个报警用不同声音,不同的参数需要 LED 亮起不同颜色,如何用 S1 切换四个参数,S2-S5 设置阀值?这里有一个容易写错的交互逻辑:S1 按下后显示当前可调参数,S2-S5 分别对应不同的调整动作,但项目描述说 S2-S5 是“设置阀值”,常见做法是 S2 选中参数后加、S3 减、S4 确定、S5 恢复默认。为了避免按键抖动,主循环里用 20ms 消抖,并且只在按键释放时执行一次。

// 按键扫描,返回键值,0 表示无按键 unsigned char key_scan(void) { if (S1 == 0) { delay_ms(20); while (S1 == 0); return 1; } if (S2 == 0) { delay_ms(20); while (S2 == 0); return 2; } if (S3 == 0) { delay_ms(20); while (S3 == 0); return 3; } if (S4 == 0) { delay_ms(20); while (S4 == 0); return 4; } if (S5 == 0) { delay_ms(20); while (S5 == 0); return 5; } return 0; }

注意while (S1 == 0);是等待按键释放,如果用户一直按着,程序会卡死在这里。对这四个参数分别设置上下限,可以单独设一个二维数组threshold[4][2],S1 切换当前要修改的参数索引,S2 把下限加 1,S3 把上限减 1,S4 保存到 EEPROM。EEPROM 操作注意每次写入间隔要大于 5ms,频繁按钮可能损耗寿命,调试阶段不建议每按一次都写 EEPROM,先写 RAM,最终按 S5 统一保存。

5. Proteus 仿真调试与实物移植:让传感器模型替你说真话

Proteus 仿真里最大的坑就是传感器模型太理想化。DHT11 模型在 Proteus 8.6 中已经内置,大多数情况下能输出正常温湿度值,但它不会像实物那样产生微秒级时序抖动。如果你在实物上跑同一份代码发现读不到 DHT11,大概率是延时函数不准,而不是代码逻辑错。我的做法是先在 Proteus 里用DEBUG菜单中的定时器测量引脚波形,把 DHT11 数据线接到虚拟示波器上,实际测量低电平触发时间是否符合 18ms 以上。

TLC1543 在 Proteus 里的模型对时钟极性非常严格,原代码中如果_nop_()太少,仿真里偶尔能出数据,但实物时序可能不够。我习惯把 TLC1543 的 SCLK 改为高低电平各延时 2us 的函数,然后在仿真里对比 ADC 输入电压和读到的数值是否成线性。输入电压可以直接用电压探针测量,在仿真中放一个VOLTMETER,把光敏电阻分压点电压和 ADC 读数换算成电压做对比,偏差在 2 个 LSB 内即可。

关于继电器仿真中的替代,资源里描述“超出或低于范围继电器驱动中央空调(仿真是用小灯泡代替)”,这在实际 Proteus 工程里就是继电器线圈接一个 ULN2003 驱动芯片,输出端串一个灯泡和限流电阻到 5V。要注意 Proteus 的继电器默认延迟时间,如果电机正反转由两个继电器切换极性,两个继电器吸合和释放之间存在一个共同导通时间,仿真中不会烧,但实物里可能短路。我通常在两个继电器控制脚之间加 100ms 死区逻辑,或者用互锁控制。

验证整个闭环逻辑时,推荐用 Proteus 的交互仿真:调节滑动变阻器模拟光照变化,或者用可调直流电源代替硫化氢传感器,观察继电器动作和 LED 报警。为了让阈值边界清楚,可以在main.c里加一个串口打印函数,把 ADC 原始值和计算后的物理量通过虚拟串口发送到DEBUG窗口。但是 51 单片机资源有限,串口打印会占用定时器,可以用 P3.0、P3.1 复用虚拟终端,调试完再注释掉。这样你就能看到光照 ADC 到 512 时继电器刚好吸合,而不是靠肉眼猜灯泡亮度。

最后一个技巧是使用流程图辅助排错。这套资源里有流程图.bmp,它是整个程序的状态转移图。你在改代码前先对照流程图检查每个状态的条件是否完备,例如温度报警解除的条件是回到 15-25°C 区间,但如果没有回差,恰好处于边界时继电器会反复通断。给阈值程序加上回差:温度上限 25°C,报警解除设在 24°C,这样可以避免继电器抖动。Proteus 里要模拟回差,得把滑动变阻器的调节步长放小,然后缓慢跨过阈值,观察报警是否只触发一次。这个细节决定了你的系统是稳定监测还是乱跳一通。

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

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

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

立即咨询