简介:基于STM32的环境监测系统是一份面向嵌入式开发学习者的综合实践资料,适合电子竞赛备赛、课程设计或毕业设计场景。项目实现了温湿度与有毒气体浓度检测,通过LCD1602实时显示,并能在数值越限时触发声光报警,功能链路完整,覆盖传感器驱动、阈值判断、显示与报警控制等核心模块。资源包共221个文件,包含C源码与头文件、Proteus仿真工程与原理图、Keil工程配置、Hex烧写文件、操作演示视频和文本说明,整体约19.22MB,目录结构清楚,便于按需查阅和对照复现;已有432人学习下载。配套的讲解演示视频不仅展示运行效果,还逐步拆解硬件连接、代码逻辑和仿真调试技巧,可帮助使用者快速掌握STM32外设配置与Proteus联合仿真,减少自行摸索成本;初学者可跟随视频与源码一步步搭建环境,有经验者亦可参考其模块化设计思路,是一套可直接用于项目实践或二次开发的完整方案。
1. 为什么用 STM32 而不是买现成的环境监测仪
一个小型实验室或仓库,需要同时盯住环境温湿度和有毒气体浓度,超标立刻声光报警。市面上的环境监测仪要么贵,要么不开放数据接口,想改阈值、换传感器、接自己的告警逻辑都很难。用 STM32 自己搭一套,硬件成本几十元,还能把采集、显示、报警、仿真验证整条链路完整跑通,这就是这套基于 STM32 的环境监测系统存在的意义。它基于 STM32F103 标准外设库实现,核心功能是 DHT11 温湿度采集、气体传感器模拟量采集(对应有毒气体检测)、LCD1602 实时显示和超限声光报警,资源包内包含 Proteus 仿真图、完整源代码和讲解演示视频。适合正在做课程设计、毕业设计,或想快速跑一遍“传感器采集 → 显示 → 告警”全流程的嵌入式开发者,也适合想了解如何在仿真环境里验证传感器逻辑的工程师。
2. STM32 环境监测系统的硬件选型与最小系统搭法
2.1 DHT11 的定位与温湿度采集边界
DHT11 是课程设计和快速原型开发里最常出现的温湿度传感器,原因很直接:单总线协议,只有一个数据脚,软件模拟时序即可读取,不依赖硬件 I2C 或 SPI 外设,任何一颗 STM32 都能驱动。它的量程是湿度 20%–90%RH、温度 0–50℃,精度为湿度 ±5%RH、温度 ±2℃。对“判断环境是否超限、观察变化趋势”这类应用来说这个精度够用,但如果要做恒温恒湿机房或产品级数据采集,我一般直接换 SHT30 或 AHT20,I2C 接口、精度高一档。这套系统用 DHT11 还有一个现实原因:Proteus 里有现成的 DHT11 仿真模型,时序靠近真实模块,复现成本最低。
DHT11 的数据脚必须外接上拉电阻到 VCC,常见做法是 4.7kΩ 或 10kΩ,否则总线空闲时电平不定,时序读取必挂。单总线通信时主机先把总线拉低至少 18ms 作为起始信号,从机响应后连续输出 40 位数据,依次是湿度整数、湿度小数、温度整数、温度小数、校验和,每个字节 8 位。采样周期建议不小于 1s,读得太频繁容易拿到上一次未刷新的旧数据。工程包里 stm32f10x_tim.c 和 stm32f10x_rcc.c 的存在说明延时和时钟是独立配置的,DHT11 的微秒级延时不依赖 SysTick 时尤其要注意时钟树是否先配好。
2.2 有毒气体检测的模拟量采集链路
有毒气体检测用的是模拟量输出的气体传感器,比如 MQ-7 一氧化碳传感器或 MQ-2 烟雾/可燃气体传感器。型号不同检测目标不同,但电路结构一致:传感器内部的敏感体电阻随气体浓度变化,与外部固定负载电阻分压后输出电压,这个电压直接接 STM32 的 ADC 输入引脚。MQ 系列内部有加热丝,上电瞬间电流较大,真机供电时 5V 电源要留足裕量;仿真图里没有这个问题,但软件上我仍然会加预热等待逻辑,开机延时一段时间再读取 ADC,避免传感器初值漂移导致误报警。
气体传感器输出电压和浓度不是线性关系,而且不同批次的传感器离散性很大。所以软件策略应定为“检测 ADC 值是否超过阈值”而不是“精确算出气体浓度”,这是环境监测类项目里很常见的设计——重点在超限报警,不在绝对浓度标定。某些热词里提到“基于51单片机的红外遥控解码仿真图”也是在 Proteus 里做传感器验证,思路相同:先验证逻辑链路,再谈标定精度。
2.3 STM32 最小系统与引脚分配
主控选 STM32F103,具体型号以仿真图为准。工程里带了 stm32f10x_adc.c、stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_rcc.c,明显是标准外设库工程,不是 HAL 库。最小系统只需要四样:8MHz 晶振加两个 20pF 匹配电容、复位电路(10kΩ 上拉加 0.1μF 电容到地)、BOOT0 下拉到 GND、3.3V 电源加若干 104 去耦电容。晶振匹配电容的计算不复杂,常见做法是两个 20pF 对地,再按负载电容公式 CL=2×C1×C2/(C1+C2)+寄生电容粗算,大约落在 12–16pF 就能对上常用晶振规格。仿真图里晶振的值不一定非要精确,但真机打板时这笔账要算清楚。
引脚分配按功能拆开,DHT11 挂 PB0,气体传感器模拟输出接 PA0,LCD1602 用并口模式接 PB8–PB13,蜂鸣器和 LED 分别接 PC13 和 PC14。LCD 的 RW 脚直接接地,只写不读,省一个 IO。仿真和真机代码保持一致,后面换板子只需要改宏定义那段。
| 模块 | 引脚 | 说明 |
|---|---|---|
| DHT11 DATA | PB0 | 单总线数据,外接 4.7kΩ 上拉 |
| 有毒气体传感器输出 | PA0 | 模拟输入,ADC1 通道 0 |
| LCD1602 RS | PB8 | 寄存器选择 |
| LCD1602 EN | PB9 | 使能信号 |
| LCD1602 D4–D7 | PB10–PB13 | 并口数据线 |
| 有源蜂鸣器 | PC13 | 高电平驱动 |
| 告警 LED | PC14 | 高电平点亮 |
Proteus 里的 LCD1602 一般用 LM016L 模型,接线方式与并口一致。LM016L 没有单独的 RW 脚,直接接地处理即可。注意仿真模型和实物模块的引脚顺序略有差异,接线以仿真图为准,先跑通再对照真机调整。
2.4 系统时钟的软件配置
真机上电后系统默认跑内部 HSI 8MHz,不配置外部时钟的话外设时序全乱,所以第一步是切换外部 HSE 并倍频到 72MHz。工程里 RCC 配置一般写在 SystemInit 或 main 开头,下面是一份标准外设库写法:
void BSP_Clock_Init(void) { ErrorStatus HSEStartUpStatus; RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); // 打开外部8MHz晶振 HSEStartUpStatus = RCC_WaitForHSEStartUp(); // 等待HSE就绪 if (HSEStartUpStatus == SUCCESS) { RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB = SYSCLK = 72MHz RCC_PCLK1Config(RCC_HCLK_Div2); // APB1 = 36MHz RCC_PCLK2Config(RCC_HCLK_Div1); // APB2 = 72MHz RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 8MHz*9=72MHz RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) {} RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while (RCC_GetSYSCLKSource() != 0x08) {} } RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_ADC1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); }AHB 跑 72MHz,APB1 分频到 36MHz,APB2 跑 72MHz。APB1 分频选 2 而不是 1,是因为 APB1 上挂的 TIM2 等外设最高只能到 36MHz,超出会不稳定。ADC1 时钟挂在 APB2 上,后面还需要单独预分频到 12MHz 以下。这套配置同时解决了 DHT11 软件延时的时钟基准和 ADC 采样时钟来源,顺序不能乱。
3. 软件骨架:DHT11 时序、ADC 采样与报警逻辑
3.1 Keil 工程结构与源码文件组织
工程包里看到的 BH-STM32.uvguix 是 Keil 工程文件,DHT11.hex 是编译产物,DHT11.axf 是调试用镜像,keilkill.bat 是清理编译中间文件的脚本。写 bat 清理 .o、.d、.crf 这类临时文件是团队开发里的常见做法,提交代码前跑一下,SVN/Git 提交体积小很多。stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_rcc.c、stm32f10x_adc.c 各自对应定时器、Flash、RCC、ADC 的标准外设库实现。
源码组织建议按模块拆分,不要全堆在 main.c 里:
- main.c:主循环、阈值宏定义、状态机调用
- dht11.c:单总线时序、温湿度读取、校验
- adc_gas.c:ADC1 初始化、连续采样、均值滤波
- lcd1602.c:LCD 初始化与显示函数
- bsp_alarm.c:蜂鸣器、LED 控制
阈值用宏定义而不是写死常数,后面标定传感器时只需要改一个头文件。工程代码量不大,但模块边界清楚之后,仿真和真机切换时只改对应驱动文件,不用翻遍整个 main.c。
3.2 DHT11 读取时序与常见坑点
DHT11 的单总线协议依赖微秒级延时,GPIO 需要在输出和输入之间来回切换,每一位数据只有几十微秒宽。这里必须强调:在 Proteus 仿真环境里,如果使用了步进执行模式,时序单位会被放大,DHT11 会读到错误数据,必须选择连续仿真。这一条直接影响你的仿真图能不能跑通。
核心读取位的代码如下:
uint8_t DHT11_Read_Bit(void) { uint8_t cnt = 0; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == RESET) // 等待低电平结束 { if (++cnt > 100) return 0xFF; // 超时保护 Delay_Us(1); } cnt = 0; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == SET) // 测量高电平宽度 { if (++cnt > 100) return 0xFF; Delay_Us(1); } return (cnt > 4) ? 1 : 0; // 高电平持续约40us为1,约28us为0 }判断依据是总线高电平的持续时间:数据位 0 时高电平约 26–28μs,数据位 1 时约 70μs。代码里 cnt 以 1μs 为单位,超过 4μs 判定为 1。这个阈值要结合自己芯片主频下的 Delay_Us 实际耗时微调,仿真和真机的临界值可能不同。超时返回 0xFF 后外层要把整帧丢弃,不能把 FF 当真实温湿度显示。
完整读取一次要收 40 位数据,最后做累加校验。如果校验失败,保留上一次正确数据并返回错误码,比把错误值刷上 LCD 合理得多。DHT11 的采样间隔建议 1s 以上,循环里加个状态机或者简单的计数延时,不要无脑连续调用。
3.3 气体传感器 ADC 采样与均值滤波
有毒气体传感器的模拟电压接到 PA0 对应的 ADC1 通道 0。ADC 时钟来自 APB2,先 2 分频再 6 分频得到 12MHz,低于数据手册的 14MHz 上限。采样时间取 55.5 周期,对输出阻抗偏高的气体传感器能降低采样误差。初始化代码:
void ADC_Gas_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 必须配成模拟输入 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1) == SET) {} ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1) == SET) {} ADC_SoftwareStartConvCmd(ADC1, ENABLE); }ADC_ScanConvMode 关闭是因为当前只有单通道,后续如果加第二路气体传感器需要改为 ENABLE,并用 ADC_RegularChannelConfig 配置多个通道的采样顺序。ADC_ContinuousConvMode 打开后转换结果持续刷新到数据寄存器,主循环直接读即可,不用每次软件触发。读取时连续采样 8 次,去掉最大值最小值后取平均,能滤掉大部分传感器自身的高频抖动:
uint16_t ADC_Get_Average(uint8_t times) { uint32_t sum = 0; uint16_t max = 0, min = 4095, val; for (uint8_t i = 0; i < times; i++) { val = ADC_GetConversionValue(ADC1); if (val > max) max = val; if (val < min) min = val; sum += val; } return (uint16_t)((sum - max - min) / (times - 2)); }12 位 ADC 满量程 4095,对应 3.3V 基准电压,电压值等于 adc_val × 3.3 / 4095。这个电压反映了当前气体浓度,但阈值直接定义在 ADC 值上,省一次浮点运算。仿真时用电位器改变模拟电压,ADC 值随之变化,把电位器调到阈值电压附近即可验证报警临界状态。
3.4 阈值比较、回差与声光报警
报警逻辑不是简单的“超阈值就响”,必须加滞回区间,否则传感器数据在阈值附近抖动时蜂鸣器和 LED 会反复通断。具体做法是设置两个阈值:报警上限 ALARM_HIGH 和解除报警下限 ALARM_LOW。浓度上升超过 ALARM_HIGH 才报警,回落低于 ALARM_LOW 才解除,两个值之间留 3%–5% 回差,显示状态稳定不闪跳。
#define GAS_ALARM_HIGH 1800 // 对应约1.45V #define GAS_ALARM_LOW 1600 // 回差200个ADC码 #define TEMP_ALARM_HIGH 35 // 温度上限 #define TEMP_ALARM_LOW 30 // 温度解除 #define HUMI_ALARM_LOW 30 // 低湿度告警 #define HUMI_ALARM_HIGH 25 // 低湿度解除 void Alarm_Check(void) { static uint8_t gas_alarm = 0, temp_alarm = 0, humi_alarm = 0; if (g_gas_adc > GAS_ALARM_HIGH) gas_alarm = 1; else if (g_gas_adc < GAS_ALARM_LOW) gas_alarm = 0; if (g_temp > TEMP_ALARM_HIGH) temp_alarm = 1; else if (g_temp < TEMP_ALARM_LOW) temp_alarm = 0; if (g_humi < HUMI_ALARM_LOW) humi_alarm = 1; else if (g_humi > HUMI_ALARM_HIGH) humi_alarm = 0; if (gas_alarm || temp_alarm || humi_alarm) { Alarm_On(); } else { Alarm_Off(); } }注意湿度告警的方向和温度相反:低湿度触发在 30%RH 以下,解除时回到 30%RH 以上,所以解除阈值 HUMI_ALARM_HIGH 定义在 25 是为了避免在 30 附近反复切换。实际数值按传感器响应来定,但结构和思路必须完整。真机上如果是有源蜂鸣器,IO 拉高即响;无源蜂鸣器需要定时器输出频率信号,仿真图里要确认蜂鸣器模型是哪一种,接法完全不一样。
3.5 LCD1602 显示与固定宽度格式
LCD1602 初始化时序必须严格遵守,上电后延时大于 40ms 再发指令,仿真环境下如果一上电就初始化,经常出现首屏正常但第二行不显示的问题,我一般会在 main 开头加一个 500ms 延时再做 LCD 初始化。写显示函数时所有字符串固定宽度,不足位补空格,防止上一次的残留字符干扰读数。
void LCD_Show_Env(uint8_t *temp_str, uint8_t *humi_str, uint16_t gas_adc, uint8_t alarm_flag) { LCD_WriteString(0, 0, "T:", 3); LCD_WriteString(0, 3, temp_str, 5); LCD_WriteString(0, 8, "H:", 2); LCD_WriteString(0, 11, humi_str, 5); LCD_WriteString(1, 0, "GAS:", 4); LCD_WriteString(1, 5, Uint16ToString(gas_adc), 4); if (alarm_flag) { LCD_WriteString(1, 12, "ALARM", 5); } else { LCD_WriteString(1, 12, "OK ", 5); } }温度 "25.0" 占 4 字节,函数里写 5 字节宽度,多出的 1 字节是空格,用来清除上一次可能残留的字符。LCD1602 的显示内容要和报警状态联动,用户看一眼屏幕就知道当前有没有告警,不用盯着蜂鸣器听。
4. Proteus 仿真图搭建与调试验证方法
4.1 仿真图器件清单与最小系统连接
拿到资源里的仿真图,第一件事是看器件布局而不是直接点运行。Proteus 里这套环境监测仿真图需要的器件主要有:STM32F103 具体型号以仿真图为准、DHT11 模型、LM016L 对应的 LCD1602、POT-HG 滑动变阻器模拟气体传感器、BUZZER 蜂鸣器、LED、若干电阻电容。STM32 放置后要手动确认电源和启动引脚:VDD 接 3.3V 网络、VSS 接地、BOOT0 通过 10kΩ 电阻下拉、NRST 接复位电路。晶振两个引脚接 8MHz,旁边两个 20pF 电容分别接地。
Proteus 里最容易忽略的是 VCAP 引脚。STM32F103 某些封装有 VCAP 引脚,必须接 2.2μF 电容到地,漏接会导致芯片无法启动,仿真图一片安静。对照讲解演示视频里仿真图的连线方式,重点看 VCAP、BOOT0、NRST 三个点。
4.2 用滑动变阻器模拟有毒气体传感器输出
Proteus 自带的传感器库里没有 MQ 系列的直接模型,常见做法是用滑动变阻器的分压输出模拟气体传感器的模拟电压。把电位器一端接 3.3V,另一端接地,中间滑动端接 STM32 的 PA0。调节电位器阻值,PA0 上的电压在 0–3.3V 之间变化,相当于改变气体浓度。
这个替代方案对验证软件逻辑完全够用,因为代码不关心电压来自真实传感器还是电位器,它只看到 ADC 值。调试时把电位器输出电压当作“百分比浓度”:1V 附近认为是正常,2.5V 以上认为是超标,这样验证阈值和报警链路非常高效。仿真和真机的差别在于响应速度:真实 MQ 系列上电需要预热,响应阶跃气体浓度可能需要几十秒到几分钟,电位器是即时的。验证报警逻辑在仿真里做,验证采样稳定性和长期漂移必须回到真机。
4.3 HEX 文件加载与仿真运行
Keil 工程编译通过后,在 Options for Target 的 Output 选项卡里勾选 Create HEX File,重新编译一次,在工程输出目录生成对应 hex 文件。在 Proteus 里双击 STM32 芯片,在 Program File 一栏选择生成的 hex。每次重新编译后 Proteus 能识别到文件变化,但需要先停止仿真再重新运行,否则加载的还是内存里的旧固件。
运行后如果 LCD 直接显示数值在变化,说明整个采集链路是通的。此时拖动电位器观察 GAS 数值变化,如果跟着电位器走,说明 ADC 和 LCD 正常;如果数值恒定不变,优先查 PA0 的连线是否断开,或者 GPIO 是否配成了数字输入。DHT11 模型允许在属性面板直接编辑温度和湿度值,手动改成不同数值再运行,可以单独验证阈值比较逻辑是否覆盖了温湿度超限报警。
4.4 仿真过程中最常见的三个问题
第一个是 DHT11 读取失败,LCD 温湿度显示不变或显示 0。常见原因是代码里基于时钟周期的软件延时在 Proteus 步进模式下时间基准不对,需要切换到连续仿真模式。第二个是 LCD1602 不显示或第二行乱码,先查 VO 引脚对比度电位器是否正常,再查 RW 脚是否接地,最后检查初始化延时是否大于 40ms。第三个是 ADC 读数为 0 或 4095 两个极端值,前者查 PA0 是否确实连到电位器分压点,后者查 GPIO 是否配置成 GPIO_Mode_AIN。漏配模拟输入模式是最高频的失误,因为很多人习惯把传感器引脚一律配成上拉输入,这在数字信号上没问题,但在 ADC 引脚上会被内部上下拉直接钳位。
5. 项目验收的验证点与阈值标定实用技巧
这套系统拿到手后,不看讲解演示视频也能完成验收,按下面的验证点依次过一遍,基本能把仿真图、源码和硬件的配合逻辑全部理清。
第一个验证点:温湿度刷新。运行仿真图后,LCD 第一行的 T 和 H 数值应该在 1 秒左右刷新一次。DHT11 模型属性里手动修改温度和湿度值,保存后观察 LCD 是否同步变化。如果数值不变,问题大概率在 DHT11 的时序读取或初始化延时上,对照源码里 Delay_Us 的实现逐段排查。
第二个验证点:气体报警触发与解除。把电位器电压从低往高慢慢调,当 GAS 数值超过 ALARM_HIGH 时蜂鸣器和 LED 应同时动作,LCD 出现 ALARM 标志。此时把电位器往回调,注意要低于 ALARM_LOW 报警才会解除,这就是回差逻辑在起作用。如果报警在临界点反复通断,说明回差区间设小了,把 ALARM_LOW 再往下调 100–200 个 ADC 码。
第三个验证点:LCD 显示格式。确认两行显示内容完整,没有残留字符或乱码。重点看 GAS 后面的数值是否四位对齐,ALARM 和 OK 切换时是否覆盖干净,这关系到现场人员能不能一眼读懂状态。
阈值标定是这套系统从“能跑”到“能用”的关键一步。真实气体传感器的分压输出可以用万用表测量传感器模块的 AO 引脚电压,记录“正常环境电压”和“超标环境电压”两个值,再换算成 ADC 码:ADC值 = 电压 / 3.3 × 4095,写进阈值宏里。比如标定时正常环境测得 0.8V,对应 ADC 约 993;超标环境测得 2.0V,对应 ADC 约 2482,那 GAS_ALARM_HIGH 可以设在 2000,留出余量。不同批次传感器离散性大,换传感器后要重新标定,不要在代码里直接改 bin 文件,改完重新编译生成 hex 再加载到仿真图里验证一次。
配套的录像文件是讲解演示视频,上面说的操作流程在视频里都有对应演示,跟着跑一遍能省不少排查时间。最后提一个真机调试时的实际问题:如果用 ST-Link 下载程序时遇到 error: no stm32 target found,先查驱动是否正常、ST-Link 与板子的 SWDIO/SWCLK/GND 三根线是否接对,再看 BOOT0 是否被外部电路意外拉高。这个问题和仿真图无关,但仿真跑通后几乎所有人都会栽在真机下载这一步,提前排掉能少走一大段弯路。
本文还有配套的精品资源,点击获取