STM32室内空气质量监测系统设计:DHT11与甲醛/CO2传感器仿真与实现
2026/9/14 16:47:01 网站建设 项目流程

简介:基于STM32的室内空气质量监测系统设计资料,面向嵌入式初学者、电子竞赛备赛者以及物联网项目开发者,提供了一套完整的温湿度、甲醛与二氧化碳浓度检测方案。系统以DHT11采集温湿度,配合独立甲醛和二氧化碳检测模块,数据实时显示于LCD,并支持阈值设定与超标报警,可直接借鉴其传感器驱动和报警逻辑。资源包共172个文件,约27.79MB,主要包含C/H源码、Keil工程文件(uvprojx/uvoptx)、Proteus仿真工程(pdsprj/pdsbak)、编译生成的HEX固件及汇编启动文件,另有MP4演示视频和批量清理脚本,便于对照仿真与实际运行效果。当前已有230人学习下载,适合作为课程设计或竞赛项目的参考模板,尤其有助于快速理解STM32标准外设库配置、多传感器协同采集和LCD显示流程,节省从零搭建环境的时间。

1. 室内空气质量监测:真正难的不是加传感器,而是把数据变成可动作的信号

装修完的房子、密闭的会议室、长时间开空调的卧室,大家在意的都是甲醛和二氧化碳,但多数方案只给一个数字没有用。用户要的是“超标了怎么办、阈值设多少、传感器漂移了怎么校准”。我最近拆了一个基于STM32的温湿度与空气质量监测实例,DHT11加独立甲醛和二氧化碳模块,带LCD显示、阈值报警,附带完整的Proteus仿真工程和Keil工程源码。下面按我从看资料到跑通仿真的顺序,把代码和仿真里值得注意的边界梳理一遍,适合正在做环境监测毕业设计、或者想快速把多传感器接入STM32的开发者。

2. 系统方案与选型:从DHT11到甲醛/CO2传感器,把“采样点”变成“系统”

先看清楚骨架。工程里能看到stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_adc.c,用的是标准外设库,主控是STM32F103系列。接入三类信号:DHT11温湿度、甲醛检测模块、二氧化碳检测模块,再加上LCD和蜂鸣器。数据流非常直白:传感器 → MCU判断 → 显示与报警,没上RTOS,靠主循环轮询加定时器中断就能满足实时性。

2.1 整体架构与数据流

传感器输出分两类:DHT11是单总线数字协议,只需一个GPIO引脚;甲醛和二氧化碳模块大多提供模拟电压输出,走ADC采样。推荐引脚划分如下表,具体以你的板子为准。

外设接口方式STM32引脚
DHT11单总线 GPIOPA0
LCD1602并行 GPIOPB0~PB2, PB10~PB15
蜂鸣器GPIO 输出PB3
甲醛模块ADC 模拟输入PA1
CO2模块ADC 模拟输入PA2
调试串口USART1PA9/PA10

这种分配的好处是ADC通道集中在PA1/PA2,可配置为规则组的两个通道,用DMA或定时触发采样,避免主循环里逐个等转换结果。系统采用“定时器调度+主循环业务”的结构。定时器产生100ms时间片,主循环里用一个状态机决定当前时间片做哪件任务:读DHT11、读ADC、刷新LCD、检查报警。这个设计比单轮大循环更可靠,因为它把慢设备(DHT11的40bit时序)和快设备(ADC)隔离开来,不会让ADC采样被温湿度读取卡成锯齿。

2.2 传感器选型:精度、响应时间与成本

DHT11精度是±2℃、±5%RH,响应较慢,大约1秒刷新一次。对室内环境监测勉强够用,但如果做研发测试,建议直接换SHT30,I2C接口,精度高一档,代码复杂度和DHT11差不多。甲醛模块常见两种:电化学型(如ZE08-CH2O)输出化学气体浓度对应电压,响应时间约30~50秒,适合长期慢速监测;半导体型(如WSP2110)成本低但受温度和湿度影响大,必须做温度补偿。二氧化碳模块优先选红外非色散型的MH-Z14,量程400~5000ppm,能自校准,劣势是体积和功耗,如果电池供电,要给它单独做电源控制。

关于阈值:甲醛室内标准通常取0.08mg/m³,二氧化碳健康阈值取1000ppm。不要一开始就把报警阈值定得太低,因为传感器在开机前十几分钟会有预热漂移,尤其电化学传感器冷启动输出偏高,加一个“上电15分钟不报警”的软启动逻辑能省掉很多误报。

2.3 开发环境搭建:芯片包、Keil5和Proteus怎么配合

代码工程是Keil5的UV5项目,第一件事装STM32F10x芯片包。装过C51开发板的机器上容易遇到Keil5只认C51不认STM32的情况。常见做法是到Keil官网下载Keil.STM32F1xx_DFP.x.x.x.pack,双击后Pack Installer会自动关联,然后在Project → Manage → Device里能看到STM32F103C8即可。

仿真用Proteus 8,不需要额外装MCU模型,但传感器模型要注意版本。DHT11、LCD1602和虚拟示波器在Proteus元件库里的名字分别是DHT11、LM016L,烧录文件则需要在MCU属性里指定工程生成的.hex。如果不指定外部hex,Proteus会把看不见的内部固件当空ROM,一运行什么都没反应,这个坑后面第4章再细说。

这套系统不依赖外部Flash,工程里出现stm32f10x_flash.c是因为标准外设库默认把Flash驱动编译进来,实际应用中没有写入除STM32固件外的数据。stm32f10x_tim.c则用于生成系统节拍,我给定时器配的是1ms中断累加计数,ADC的采样触发也用同一个时基,这样报警比较器的扫描频率就是确定的。

3. 温湿度与气体采集代码实现:GPIO、ADC和中断的协作

从代码层面看,整个项目最核心的是三块:DHT11的时序读取、多通道ADC的连续采样、阈值触发。这里不是简单地调用HAL库函数,而是要处理好时序约束和传感器噪声。

3.1 DHT11时序读取与错误处理

DHT11的单总线协议要求MCU先拉低至少18ms发出启动信号,再释放总线,传感器应答后连续输出40bit数据。常见实现是直接操作GPIO翻转和延时等待。我一般会在每次读取前关闭中断,避免定时器中断打扰微秒级的电平采样。以下代码是去掉注释后可以直接嵌入main.c的版本:

uint8_t dht11_read(uint8_t *temp, uint8_t *humi) { uint8_t buf[5] = {0}; uint8_t i, j; GPIOA->CRL &= 0xFFFFFFF0; GPIOA->CRL |= 0x00000003; // PA0 推挽输出 GPIOA->BRR = (uint16_t)1; delay_us(20000); // 至少 18ms,取 20ms 余量 GPIOA->BSRR = (uint16_t)1; delay_us(30); GPIOA->CRL &= 0xFFFFFFF0; GPIOA->CRL |= 0x00000004; // 切回浮空输入 while ((GPIOA->IDR & 1) == 1) { if (--i == 0) return 1; // 等应答低电平 } while ((GPIOA->IDR & 1) == 0) { /* 等拉高 */ } while ((GPIOA->IDR & 1) == 1) { /* 等第一个数据位开始 */ } for (i = 0; i < 5; i++) { for (j = 0; j < 8; j++) { while ((GPIOA->IDR & 1) == 0) { /* 跳过 50us 低电平 */ } delay_us(40); buf[i] = (buf[i] << 1); if (GPIOA->IDR & 1) buf[i] |= 1; // 高电平超过 40us 视为 1 while ((GPIOA->IDR & 1) == 1) { /* 等位结束 */ } } } if ((buf[0] + buf[1] + buf[2] + buf[3]) != buf[4]) return 2; *humi = buf[0]; *temp = buf[2]; return 0; }

这段代码用寄存器操作而不是HAL函数,原因是DHT11的位周期是微秒级,函数调用开销会导致采样错位。关键参数是延时40us的判断窗口:DHT11数据位“0”的高电平持续约26~28us,“1”约70us,所以在高电平开始后等40us再读引脚,能稳定区分二者。很多移植失败的案例是直接抄别人延时函数,不同主频下delay_us不准确。验证方法是在PA0接逻辑分析仪,看波形高位宽度是不是26us和70us两档。

3.2 多通道ADC与传感器标定换算

甲醛和二氧化碳模块输出0~3.3V(或0~5V)模拟信号。STM32的ADC是12位,通过配置规则组顺序采样PA1和PA2。我通常开启扫描模式和连续转换,用DMA把结果搬运到内存,避免在中断里频繁读DR寄存器。配置代码:

void adc_init(void) { RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; GPIOA->CRL &= ~(0xFF << 4); // PA1/PA2 模拟输入 RCC->CFGR &= ~(7 << 14); RCC->CFGR |= RCC_CFGR_ADCPRE_2; // PCLK2 6分频,ADC时钟约 12MHz ADC1->CR2 &= ~ADC_CR2_CONT; ADC1->CR2 |= ADC_CR2_ADON; ADC1->SMPR2 |= (7 << (3 * 1)) | (7 << (3 * 2)); // PA1/PA2 采样周期最大 ADC1->SQR1 = (1 << 20); // 规则组 2 个转换 ADC1->SQR3 = (1 << 0) | (2 << 5); // 第1个通道 PA1,第2个 PA2 ADC1->CR2 |= ADC_CR2_CAL; while (ADC1->CR2 & ADC_CR2_CAL) {} }

注意ADC采样时间和输入阻抗的匹配。甲醛模块内部等效输出阻抗通常在1kΩ到10kΩ,STM32数据手册推荐采样周期12MHz下不低于1.5us,但如果模块输出带RC滤波,可以把采样周期设到55.5us以上。实际操作中,我给PA1/PA2各对地加一个100nF电容,采样值抖动可以从±30mV降到±5mV。这比在软件里做十次平均更直接。

3.3 LCD显示与阈值报警联动

LCD1602在这里只显示两行:第一行温度与湿度,第二行甲醛和CO2读数。刷新频率不必太高,每秒刷一次即可,因为DHT11本身每秒才能响应一次。阈值判断放在主循环的1s时间片里:

void check_alarm(void) { if (temp_value > temp_limit || humi_value > humi_limit || hcho_mg_100ml > hcho_limit || co2_ppm > co2_limit) { BEEP_GPIO->BSRR = BEEP_PIN; // 开启蜂鸣器 alarm_report(ALARM_SOURCE); // 通过串口输出当前超标项 } else { BEEP_GPIO->BRR = BEEP_PIN; } }

很多初版设计只给了温度阈值,忽略湿度阈值。但霉菌和闷热感主要和湿度耦合,所以我习惯在液晶屏上用闪烁的字符标出“ALARM:TEMP”而不是只响蜂鸣器。只有蜂鸣器无法说明是哪个指标超标,现场调试时你必须逐个去看变量,效率很低。加上报警源字符串,维护成本会小很多。

4. 用Proteus做仿真验证:把hex跑起来,再谈“仿真与实物的差异”

仿真不是摆设,它解决了“没有实物板也能调逻辑”的问题。但这套工程里有些坑,Proteus模型和真实芯片不完全一样,比如DHT11模型不会提供精确到微秒的波形,蜂鸣器模型也只是高低电平控制一个发声符号。

4.1 用Proteus搭出可运行的仿真图

我没有重建原理图,直接打开项目附带的.pdsprj。如果你要自己搭,步骤固定:从元件库拖入与hex匹配的STM32F103系列型号(例如F103C8)、DHT11、LM016L、两个电压源和一个可调电阻,用可调电阻模拟甲醛和CO2模块的模拟电压。然后双击MCU,在Program File里选择Keil生成的Template.hex,设置晶振为8MHz,勾选外部时钟。加载后点击运行,LCD应该立刻出现DHT11的温湿度数据;转动可调电阻,第二行浓度变化,超过阈值后蜂鸣器开始响。

Proteus里没有直接叫“甲醛传感器”的元件,常见的做法是电压源配滑动变阻器。把电压范围设置为0~3.3V,对应模块的0~满量程。比如你的CO2模块输出0~2.5V对应400~5000ppm,就在Proteus里设可变电阻0~2.5V,然后将ADC值代入第3章的换算公式即可。

4.2 仿真中容易踩的坑:ADC输入范围与采样时序

第一个坑是不检查代码里的ADC参考电压。Proteus默认STM32的VREF+接VDD 3.3V,如果你的传感器输出超过3.3V,电压会被钳位到3.3V,看不出真实超标。同样是5V供电的电化学模块,需要先用分压电阻把输出缩放到0~3.3V。第二个坑是DHT11的应答时序,Proteus模型对启动信号要求较严格,延时20ms不够就拉不起应答,把初始延时改到30ms,仿真实测通过。

如果遇到LCD显示乱码,先量一下LCD两条对比度电压引脚。在Proteus里LM016L的3脚直接接地即可,不用像真实芯片那样接10k电位器。另一个坑是蜂鸣器仿真模型驱动能力很弱,PB3引脚直接驱动可能不响,加一个NPN三极管就能解决。这个现象和实物一致,MCU引脚的驱动电流有限,所以代码没变,硬件电路变了。

4.3 从仿真到实物:保留调试串口和日志点

仿真通过后,真正移植到实物板时,最好把USART1调试串口留着。我在代码里加了一个“上报模式”,每5秒输出一行:T=26.3 H=58.0 HCHO=0.04 CO2=1250 ALARM=GAS。因为Proteus的虚拟串口可以和真实串口工具对接,你可以在仿真阶段就检查这行日志的格式,在实物阶段接USB转TTL看数据。这个习惯能让你快速发现传感器接线松动和电压漂移,省掉拿着万用表逐个点测的时间。

5. 进阶技巧:以基线校准和自动校准替代固定阈值

项目到第四步已经能跑仿真,但真实部署时一个容易忽略的问题是传感器漂移。DHT11在90%RH高湿环境中长时间工作,湿度读数会慢慢偏高几个百分点;电化学甲醛传感器使用6个月后零点可能从0.00变成0.05mg/m³。如果一直用固定阈值,会产生持续误报。

5.1 开机基线校准和实时零点跟踪

我一般会在代码里预留“上电基线校准”:系统启动后前60秒内不计算报警,只采样传感器输出,取平均值作为零点偏移量。因为洁净空气中甲醛模块的理论输出应接近0mg/m³,二氧化碳接近400ppm。将实测值减去这个基线,再进入显示和报警判断。代码如下:

typedef struct { uint16_t raw_hcho; float offset_hcho; uint16_t raw_co2; float offset_co2; } sensor_cal_t; sensor_cal_t cal; void baseline_cal(void) { uint8_t i; uint32_t sum_hcho = 0, sum_co2 = 0; for (i = 0; i < 60; i++) { sum_hcho += read_adc(1); sum_co2 += read_adc(2); delay_ms(1000); } cal.offset_hcho = (float)sum_hcho / 60.0f; cal.offset_co2 = (float)sum_co2 / 60.0f; }

注意,这个校准不是每个上电周期都无条件执行。如果是持续运行的系统,在开机后用户正在室内,此时空气中的甲醛浓度不一定是零,用开机均值当零点会把正在产生的污染“校掉”。正确做法是只在电池首次上电、或人工按校准键时执行,平时依靠慢漂移跟踪,每小时取最低值的90%作为参考点。

5.2 用两点校准修正传感器的“斜率”

传感器老化不仅让零点偏移,也会让输出斜率产生变化。更完善的方案是两点校准:用一瓶标准气(比如1000ppm CO2)和一瓶高浓度气(5000ppm)测出实际ADC输出,再计算线性系数。这个值可以存储在STM32内部Flash的某个扇区,利用工程里已经编译的stm32f10x_flash.c,避免每次校准后重新编译固件。校准参数用结构体组织,写入前先擦除整个扇区,再按半字写入。

#define CAL_SECTOR_ADDR 0x0803F000 void save_cal_param(sensor_cal_t *params) { FLASH_Unlock(); FLASH_ErasePage(CAL_SECTOR_ADDR); FLASH_ProgramWord(CAL_SECTOR_ADDR, *(uint32_t *)&params->offset_hcho); FLASH_ProgramWord(CAL_SECTOR_ADDR + 4, *(uint32_t *)&params->offset_co2); FLASH_Lock(); }

注意Flash写入前必须确保该页没有正在执行的代码读取数据,否则总线卡死。把校准函数放在RAM里执行,或者在擦写期间禁止中断,可以将风险降到最低。这里使用最后一页作为参数区,避开主程序空间;如果使用更高容量的STM32,地址要按实际Flash大小修改。

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

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

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

立即咨询