基于51单片机的环境监测系统设计与Proteus仿真调试
简介面向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 而不是 ADC0832ADC0832 在 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 高电平表示 070us 高电平表示 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.c和Ds18b20.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 对应 5VADC 参考电压 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 把下限加 1S3 把上限减 1S4 保存到 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 里要模拟回差得把滑动变阻器的调节步长放小然后缓慢跨过阈值观察报警是否只触发一次。这个细节决定了你的系统是稳定监测还是乱跳一通。本文还有配套的精品资源点击获取