☰
基于STM32F103C8T6的室内空气质量检测系统:甲醛、CO2、温湿度监测与自动换气
2026/10/6 7:07:08 网站建设 项目流程

1. 为什么我要做这个室内空气质量检测系统

前阵子搬进新装修的房子,总觉得屋里有一股说不清的味道,待久了嗓子发干、眼睛发涩。买过几百块的成品空气质量检测仪,数据飘得厉害,甲醛读数一会儿0.02一会儿0.15,根本不知道信哪个。后来干脆自己动手,用STM32F103C8T6最小系统板搭了一套检测系统,把甲醛、CO2、温湿度全测了,还加了自动换气警示功能。实测下来,这套方案的物料成本不到一百块,数据稳定性反而比某些成品好。

这套系统能干什么?简单说就是三件事:实时采集室内甲醛浓度、二氧化碳浓度、温度和湿度;把数据在LCD1602上显示出来;当甲醛或CO2超标时,自动触发声光报警并启动换气扇。适合谁参考?电子类专业做毕业设计的同学、想入门STM32的爱好者、以及跟我一样对居住环境有顾虑的普通人。整篇文章我会把选型逻辑、电路设计、代码实现、调试踩坑全部讲透,你照着做基本能复现。

核心关键词先摆出来:单片机、STM32F103C8T6、DHT11、MQ-138、MQ-135。这几个器件是整个系统的骨架,后面每个章节都会围绕它们展开。

2. 系统整体设计与器件选型思路

2.1 主控为什么选STM32F103C8T6而不是51单片机

很多人做这类项目第一反应是用51单片机,便宜、资料多、上手快。我一开始也想过用STC89C52,但算了一下资源就放弃了。这套系统要同时驱动LCD1602(占用6个IO)、DHT11(1个IO)、两路MQ传感器(2个ADC通道)、蜂鸣器、LED、继电器,还要跑定时器和串口调试。51的IO和ADC资源(大部分型号根本没有内置ADC)会很紧张,外扩ADC0832又增加复杂度。

STM32F103C8T6的优势在这里非常明显:48个引脚、内置2个12位ADC共10个通道、3个USART、多个定时器,主频72MHz。关键是这块最小系统板市面价格只要十几块,比很多51开发板还便宜。它的ADC是12位的,分辨率4096级,而51外挂ADC0832只有8位256级,测气体浓度这种需要细分的场景,12位明显更从容。

注意:STM32F103C8T6的VBAT引脚在最小系统板上通常已经通过跳线或直接连到3.3V,如果你自己画板,VBAT不接会导致RTC和备份域异常,虽然这个项目用不到RTC,但养成接上的习惯没坏处。

2.2 传感器选型:DHT11、MQ-138、MQ-135各自负责什么

DHT11负责温湿度。它是数字输出,单总线协议,精度是温度±2℃、湿度±5%RH。有人会问为什么不选DHT22或者SHT30,DHT22精度更高但价格贵一倍多,SHT30是I2C接口精度最好但单价要十几块。对于室内环境监测,DHT11的精度完全够用,而且它的单总线协议简单,一个IO就能读,不占用ADC资源。

MQ-138负责甲醛检测。这个传感器对甲醛、苯、酒精等有机挥发物敏感,输出是模拟电压,浓度越高电压越高。它的加热丝需要5V供电,预热时间比较长,这点后面会详细说。

MQ-135负责空气质量综合检测,对CO2、氨气、烟雾、苯系物都敏感。严格来说MQ-135不是专门测CO2的,它测的是"空气污染综合指数",但在室内场景下,CO2浓度升高往往伴随其他污染物上升,所以用它的输出来间接反映CO2水平是可行的。如果你要精确测CO2,得用MH-Z19B这种NDIR传感器,但那个价格要七八十块,是MQ-135的五六倍。

器件检测对象输出类型供电预热时间单价参考
DHT11温度、湿度数字单总线3.3-5V1s3-5元
MQ-138甲醛等VOC模拟电压5V24-48h8-12元
MQ-135CO2等综合气体模拟电压5V24-48h8-12元
STM32F103C8T6主控-3.3V-12-18元
LCD1602显示并行5V-8-12元

2.3 自动换气警示的执行机构设计

超标后的动作我设计了两级:一级是声光警示,蜂鸣器间歇鸣叫加红色LED闪烁;二级是启动换气。换气我用的是5V继电器模块控制一个220V的排气扇,继电器模块自带光耦隔离,STM32的IO通过三极管驱动继电器线圈,触点侧接排气扇的火线。这里必须强调安全:220V强电部分一定要做好绝缘,接线端子用热缩管包好,调试阶段先用LED代替排气扇验证逻辑,确认无误再接强电。

提示:继电器模块选低电平触发的那种,STM32复位后IO默认是高阻态,低电平触发可以避免上电瞬间误动作。如果买的是高电平触发模块,在IO和模块之间加一个下拉电阻。

3. 硬件电路设计与关键连接细节

3.1 最小系统板的引脚分配表

拿到STM32F103C8T6最小系统板后,第一件事是把引脚分配规划好,不然后面改代码改到崩溃。我的分配方案如下:

功能模块引脚说明
DHT11数据PA0单总线,需上拉4.7k
MQ-138模拟输出PA1ADC1_IN1
MQ-135模拟输出PA2ADC1_IN2
LCD1602 RSPB0寄存器选择
LCD1602 RWPB1读写选择,接地也可
LCD1602 ENPB2使能
LCD1602 D4-D7PB4-PB74位数据模式
蜂鸣器PB8经三极管驱动
红色LEDPB9限流220Ω
绿色LEDPB10限流220Ω
继电器控制PB11低电平触发
串口TX/RXPA9/PA10调试用

LCD1602我用的是4位数据模式,这样只需要6个IO(RS、RW、EN加4根数据线),比8位模式省4个引脚。RW引脚其实可以直接接地,因为整个系统只写不读,但为了兼容性我还是接到了PB1。

3.2 DHT11的接线与上拉电阻

DHT11有三个引脚(有些模块是四个,多一个空脚):VCC、DATA、GND。DATA引脚必须接一个4.7kΩ到10kΩ的上拉电阻到VCC,因为它是开漏输出。我一开始忘了接上拉,读出来的数据全是0xFF,折腾了半小时才想起来。如果你买的是三引脚裸传感器,还要在VCC和GND之间加一个100nF的去耦电容,模块版一般已经集成了。

DHT11的供电电压范围是3.3V到5.5V,接3.3V和5V都能工作。我接的是3.3V,因为STM32的IO耐压是3.3V,虽然DHT11在5V供电时数据线也是5V电平,短时间接3.3V IO可能不会立刻坏,但长期有风险。接3.3V最稳妥。

3.3 MQ系列传感器的预热与负载电阻

MQ-138和MQ-135都是6引脚器件,其中两个是加热丝(H),两个是测量电极(A、B),另外两个是辅助电极。模块版通常已经把负载电阻RL焊好了,一般是10kΩ可调电阻。这个RL的阻值直接影响输出灵敏度,调大了输出电压高但噪声也大,调小了响应快但量程窄。

这里有个关键点:MQ传感器需要预热。冷启动时加热丝要通电加热,传感器内部的二氧化锡半导体材料需要达到工作温度才能正常响应。官方数据说预热24小时以上才能稳定,实际使用中至少预热20分钟读数才有参考价值,完全稳定要几个小时。我实测下来,通电前10分钟读数从高往低掉,20分钟后基本稳定,所以代码里我加了一个"预热倒计时",前20分钟显示"Warming up"不报警。

参数MQ-138MQ-135
加热电压5V±0.1V5V±0.1V
加热电流约150mA约150mA
加热电阻约31Ω约31Ω
负载电阻RL可调10kΩ可调10kΩ
检测范围10-1000ppm10-1000ppm

两个传感器加起来加热电流约300mA,加上STM32和其他模块,整个系统5V供电至少要1A。我用的是一个5V 2A的电源适配器,通过最小系统板的5V引脚供电。注意最小系统板上的5V引脚是直接从USB或外部电源输入的,不是稳压输出,所以外部电源要自己保证稳定。

3.4 LCD1602的对比度调节

LCD1602的V0引脚接一个10kΩ可调电阻到GND,用来调对比度。这个电阻调不好会出现两种情况:调太低屏幕全黑,调太高屏幕全白什么都看不见。正确做法是上电后慢慢旋转,直到字符清晰显示。我建议用多圈精密可调电阻,单圈的那种很难调到最佳点。

背光方面,LCD1602的A(背光正极)接5V串一个100Ω限流电阻,K(背光负极)接GND。有些模块自带限流电阻,直接接5V就行,接之前用万用表量一下。

4. 软件架构与核心代码实现

4.1 工程搭建与HAL库配置

我用的是Keil MDK5加上STM32CubeMX生成初始化代码。CubeMX里需要配置的项:RCC选外部晶振(HSE),时钟树把SYSCLK设到72MHz;ADC1开启IN1和IN2两个通道,扫描模式关闭,单次转换;USART1异步模式115200波特率;GPIO按前面的分配表配置;SysTick保持默认1ms。

生成代码后,我在main.c里加了自己的业务逻辑。整个程序结构是:初始化各模块,进入主循环,每2秒采集一次数据,更新显示,判断是否超标,执行相应动作。DHT11的读取放在定时器中断里不太合适,因为单总线协议对时序要求严格,中断里做容易被打断,我放在主循环里用阻塞方式读,读一次大概4ms,不影响整体响应。

4.2 DHT11单总线驱动的时序实现

DHT11的通信协议是:主机拉低数据线至少18ms,然后拉高20-40us,释放总线;DHT11响应拉低80us,再拉高80us;然后连续输出40位数据,每位以50us低电平开始,高电平持续26-28us表示0,持续70us表示1。

用HAL库实现时,关键函数是微秒级延时。HAL库自带的HAL_Delay是毫秒级,不够用。我用SysTick做了一个微秒延时函数:

void delay_us(uint32_t us) { uint32_t start = SysTick->VAL; uint32_t ticks = us * (SystemCoreClock / 1000000); while (1) { uint32_t now = SysTick->VAL; uint32_t elapsed = (start > now) ? (start - now) : (start + SysTick->LOAD - now); if (elapsed >= ticks) break; } }

读DHT11的流程:先把PA0配置成推挽输出,拉低18ms,再拉高30us,然后切成浮空输入,等待DHT11把线拉低。这里要注意,切换输入输出模式后要加几个微秒的稳定时间。读取40位数据时,每一位先等低电平结束,再延时40us采样,如果还是高电平就是1,否则是0。

实操心得:DHT11对时序很敏感,如果你用HAL库的HAL_GPIO_ReadPin读引脚,函数调用本身有开销,可能导致采样点偏移。我建议直接操作寄存器,用GPIOA->IDR & GPIO_PIN_0来读,速度快很多。另外读之前关掉中断,读完再开,避免被SysTick打断。

4.3 ADC多通道采集与浓度换算

MQ传感器的输出是模拟电压,STM32的ADC是12位,参考电压3.3V,所以ADC值0-4095对应0-3.3V。换算公式是:电压 = ADC值 × 3.3 / 4095。

但电压值本身没有意义,要换算成浓度。MQ传感器的数据手册给了一条对数曲线,公式是:Rs/R0 = a × (浓度)^b,其中Rs是传感器电阻,R0是洁净空气中的传感器电阻,a和b是常数。Rs的计算:Rs = (Vc - Vout) / Vout × RL,Vc是回路电压5V,RL是负载电阻10kΩ。

实际标定很麻烦,需要已知浓度的环境。我的做法是:在室外洁净空气中测出R0,然后套用典型曲线。MQ-138对甲醛的曲线大概是Rs/R0 = 1.2 × (ppm)^(-0.35),MQ-135对CO2的曲线是Rs/R0 = 3.6 × (ppm)^(-0.28)。这些系数是经验值,不同批次的传感器有差异,只能作为参考。

float get_voltage(uint8_t ch) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 100); uint16_t val = HAL_ADC_GetValue(&hadc1); return val * 3.3f / 4095.0f; } float get_ppm(float vout, float rl, float a, float b) { if (vout < 0.01f) return 0; float rs = (5.0f - vout) / vout * rl; float ratio = rs / R0; return pow(10, (log10(ratio) - log10(a)) / b); }

4.4 超标判断与自动换气逻辑

阈值我设的是:甲醛0.08mg/m³(国标GB/T 18883的限值),CO2 1000ppm。这两个值在代码里定义成宏,方便调整。判断逻辑是:如果甲醛超过0.08或者CO2超过1000,进入报警状态;报警状态下蜂鸣器以2Hz频率鸣叫,红灯闪烁,继电器吸合启动排气扇;如果两个值都降到阈值的80%以下(加迟滞避免临界抖动),退出报警状态。

迟滞设计很重要。我一开始没加迟滞,结果浓度在阈值附近波动时,继电器疯狂开关,咔咔响个不停。加了20%的迟滞带之后,动作就干净了。

#define HCHO_TH 0.08f #define CO2_TH 1000.0f #define HYST 0.8f if (hcho > HCHO_TH || co2 > CO2_TH) { alarm_on(); } else if (hcho < HCHO_TH * HYST && co2 < CO2_TH * HYST) { alarm_off(); }

5. 调试过程中踩过的坑与排查实录

5.1 传感器读数一直不变化怎么办

这是最常见的问题。我遇到过一次,MQ-135的ADC值始终是4095,也就是满量程。排查步骤:先用万用表量传感器的模拟输出引脚,如果电压确实是3.3V左右,说明传感器输出正常,问题在STM32的ADC配置;如果电压是0或者5V,说明传感器模块有问题。

那次我的问题是ADC通道配置错了。CubeMX里我开了IN1和IN2,但代码里切换通道时忘了改hadc1.Init.ScanConvMode和NbrOfConversion,导致一直读的是同一个通道。正确做法是每次采集前用HAL_ADC_ConfigChannel重新配置通道,或者开启扫描模式用DMA搬运。

还有一种情况是传感器预热不够。冷启动时MQ传感器的输出会从高电压慢慢下降,如果刚上电就读数,会得到偏高的值。我的代码里加了20分钟预热倒计时,期间显示"Warming up",不参与报警判断。

5.2 LCD1602显示乱码或只显示一行

乱码通常是初始化时序不对。LCD1602的4位模式初始化有个坑:上电后要先等15ms以上,然后发送三次0x03(8位模式指令),再发送0x02切换到4位模式。很多人直接发0x02,结果屏幕不亮或者乱码。

只显示一行的问题,我遇到过是因为DDRAM地址设置错了。LCD1602第一行地址是0x80,第二行是0xC0。写第二行数据前要先发0xC0指令,忘了发就会覆盖第一行的内容。

现象可能原因解决方法
屏幕全黑对比度太低调V0电阻
屏幕全白对比度太高调V0电阻
显示乱码初始化时序错按标准时序重写
只显示一行DDRAM地址错第二行发0xC0
背光不亮限流电阻过大换100Ω或直接接5V

5.3 继电器动作导致单片机复位

这个问题折腾了我最久。现象是继电器一吸合,STM32就重启,串口打印一堆乱码。原因是继电器线圈断电时会产生反向电动势,通过电源线耦合到STM32的供电上,导致电压瞬间跌落触发复位。

解决方法有三个:一是在继电器线圈两端并联一个续流二极管(1N4148就行),吸收反向电动势;二是在STM32的电源引脚附近加一个100uF电解电容和一个100nF陶瓷电容,提供瞬态电流;三是继电器模块和STM32用不同的电源供电,共地但不共电源。我三个都做了,问题彻底解决。

提示:如果你用的是成品继电器模块,模块上通常已经有续流二极管了,但电源去耦电容还是要加。另外继电器的触点侧接220V时,一定要确保断电操作,用万用表确认无电后再接线。

5.4 串口下载失败与程序不运行

STM32F103C8T6用串口下载需要把BOOT0接1、BOOT1接0,复位后进入系统存储器启动模式。下载完要把BOOT0接回0,再复位才运行用户程序。我见过有人下载完忘了改BOOT0,结果程序不跑,以为芯片坏了。

还有一种情况是下载软件选错了。STM32CubeProgrammer和FlyMcu都支持串口下载,但FlyMcu对CH340的兼容性更好。如果你用的是CH340串口模块,建议用FlyMcu,波特率选115200,校验方式选偶校验。STM32CubeProgrammer有时候会卡在"Connecting"不动,换FlyMcu就好了。

6. 实测数据与系统优化建议

6.1 不同环境下的实测记录

系统装好后我做了几组对比测试。在关闭门窗12小时的卧室里,CO2读数从初始的450ppm上升到1200ppm左右,甲醛从0.02mg/m³升到0.06mg/m³。打开排气扇后,CO2在15分钟内降到600ppm,甲醛降到0.03mg/m³。这个趋势和成品检测仪基本一致,说明系统是可靠的。

场景温度湿度甲醛(mg/m³)CO2(ppm)系统动作
室外洁净空气22℃45%0.01420正常
关窗12h卧室24℃55%0.061200报警+换气
开窗通风后23℃50%0.02500正常
新家具旁25℃58%0.10800报警+换气

6.2 提高测量精度的几个实用技巧

第一,MQ传感器的负载电阻要仔细调。我的做法是:在洁净空气中调RL,使输出电压在1.5V左右,这样既有足够的动态范围,又不会在污染时饱和。第二,给传感器加一个金属网罩,减少气流波动的影响,但不要完全密封,否则响应变慢。第三,DHT11不要和MQ传感器靠太近,因为MQ的加热丝会发热,影响温度读数,我隔了5厘米以上。

软件层面,我加了滑动平均滤波,每10次采样取平均,去掉最大最小值。这样读数跳动从±0.02mg/m³降到±0.005mg/m³,显示稳定多了。但滤波会带来响应延迟,如果你需要快速响应,可以把窗口缩小到5次。

6.3 后续可以扩展的方向

这套系统目前是本地显示和报警,如果想远程查看,可以加一个ESP8266模块,通过串口把数据传到手机。ESP8266用AT指令就能驱动,STM32的USART2接上,每30秒发一次数据。另外可以加一个SD卡模块,用SPI接口记录历史数据,方便分析污染趋势。如果要做产品化,把STM32换成低功耗型号,加锂电池供电,做成便携式检测仪也是可行的。

我个人在实际操作中的体会是,这类环境监测项目最花时间的不是写代码,而是传感器的标定和调试。MQ传感器的个体差异很大,同一批买的两只,R0可能差20%。所以如果你要做定量测量,一定要留出标定环节,别指望套用别人的系数就能准。另外,预热时间真的不能省,我试过只预热5分钟就测,读数比稳定后高了将近一倍,完全不可信。

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

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

立即咨询