基于STM32的室内空气质量监测系统设计与实现
1. 项目缘起与整体设计思路室内空气质量这件事很多人是搬进新装修的房子、或者家里添了新车之后才开始重视的。甲醛超标、二氧化碳浓度过高导致头晕犯困、温湿度不合适影响睡眠这些问题单独拎出来都不算大但叠加在一起就是实打实的健康隐患。市面上成品的空气质量检测仪动辄几百上千功能还未必齐全而自己动手用单片机搭一套成本能压到百元以内还能把检测、显示、报警、自动换气联动全部打通。这就是我折腾这个项目的直接动机。这套系统的核心目标很明确用一颗STM32F103C8T6做主控挂载DHT11温湿度传感器、MQ-138甲醛传感器、MQ-135空气质量传感器实时采集室内环境数据通过LCD1602或OLED屏幕本地显示同时根据阈值判断是否触发蜂鸣器报警和继电器控制换气扇。说白了就是做一个能看、能响、能动的闭环小系统。为什么选STM32F103C8T6而不是51单片机这是很多人纠结的第一个问题。51单片机便宜、资料多、上手快但它的ADC精度和运算能力有限跑多路传感器轮询加显示刷新会比较吃力尤其是MQ系列传感器输出的是模拟量需要ADC采集后再做换算51自带的ADC位数和参考电压稳定性都不太够看。STM32F103C8T6自带12位ADC、72MHz主频、丰富的外设接口价格也就十几块钱性价比在这个场景下明显更高。当然如果你只是做个课程设计演示51也能跑但要做稳定可用的成品STM32是更稳妥的选择。整个系统的设计思路可以概括为“采集—处理—显示—决策—执行”五步链路。传感器负责采集原始数据单片机做滤波和换算屏幕负责把数据可视化阈值判断逻辑决定是否报警继电器则驱动外部换气设备。这个链路里最容易出问题的环节是传感器数据的稳定性和阈值判断的合理性后面会详细展开。注意MQ系列传感器需要预热时间刚上电时读数漂移很大一般建议预热3到5分钟后再采信数据。这一点在调试阶段特别容易让人误以为传感器坏了。2. 硬件选型与核心器件解析2.1 主控芯片STM32F103C8T6最小系统板STM32F103C8T6最小系统板是这个项目的核心。它自带64KB Flash、20KB SRAM对于这个项目来说绰绰有余。最小系统板上一般已经集成了8MHz晶振、32.768kHz晶振、复位电路、BOOT跳线、USB接口和稳压芯片直接拿来用就行不需要自己画电源部分。引脚分配上需要提前规划好。我用的方案是PA0接MQ-138的模拟输出PA1接MQ-135的模拟输出PA2接DHT11的数据脚PB6和PB7做I2C接OLED或者用PB0-PB5做并口接LCD1602PB8接蜂鸣器PB9接继电器控制脚。这样分配的好处是模拟采集和数字外设分开不会互相干扰。有一个坑必须提前说STM32F103C8T6的VBAT引脚在最小系统板上通常直接接了3.3V如果你用的是带RTC功能的代码注意不要在这个引脚上做文章。另外PA0-PA7对应的ADC通道是ADC1_IN0到IN7用之前确认一下你的传感器接的是哪个通道别接错了还在那儿查代码。2.2 温湿度传感器DHT11DHT11是单总线数字传感器测温度范围0到50摄氏度精度正负2摄氏度湿度范围20%到90%RH精度正负5%RH。它的优点是便宜、接口简单、驱动代码满天飞缺点是响应慢、精度一般、采样频率不能太高两次读取之间至少要间隔1秒。DHT11的驱动时序是关键。它用一根数据线完成双向通信单片机先拉低至少18毫秒作为起始信号然后释放总线DHT11会拉低80微秒再拉高80微秒作为响应之后连续输出40位数据包括湿度整数、湿度小数、温度整数、温度小数和校验和。校验和等于前四个字节之和的低8位如果对不上就说明数据有问题得丢弃重读。很多人用HAL库驱动DHT11时会遇到时序不准的问题因为HAL库的微秒级延时不够精确。我的做法是直接用定时器做微秒延时或者用__NOP()空指令做粗略延时实测下来比HAL_Delay靠谱得多。另外DHT11的数据线需要接一个4.7k到10k的上拉电阻很多模块已经自带了如果你买的是裸传感器记得自己加。2.3 甲醛传感器MQ-138与空气质量传感器MQ-135MQ-138对甲醛、苯、甲苯等挥发性有机物敏感MQ-135对氨气、硫化物、烟雾等空气质量相关气体敏感。两者都是模拟输出输出的是电压值需要经过ADC采集后换算成浓度。这里要说明一点MQ系列传感器的输出和浓度之间不是线性关系而是近似对数关系。厂家给的 datasheet 里通常有一个负载电阻RL和灵敏度曲线实际使用中如果要做精确浓度标定需要先用标准气体校准。但对于室内空气质量检测这种场景我们更多是看趋势和相对值用简单的线性映射或者分段映射就够用了。MQ-138和MQ-135都需要加热丝供电加热电压一般是5V加热电流在150mA左右。这意味着你不能直接从STM32的3.3V引脚取电必须用5V电源单独供电。另外传感器的模拟输出最大可能到5V而STM32的ADC参考电压是3.3V所以需要在输出端加一个分压电路把5V映射到3.3V以内否则会烧ADC引脚。提示MQ-138和MQ-135的加热丝引脚和信号引脚是分开的接线时看清楚模块上的标注接反了传感器不工作还可能发热异常。2.4 显示与执行器件显示部分我选了0.96寸OLEDI2C接口四根线搞定比LCD1602省引脚显示效果也好得多。LCD1602虽然经典但需要6到7根数据线加3根控制线占用引脚太多而且只能显示两行字符信息密度低。如果你手头只有LCD1602也能用但建议用I2C转接板把并口转成I2C省一半引脚。执行部分用一个5V继电器模块控制换气扇。继电器模块一般自带光耦隔离和续流二极管直接接单片机的GPIO就能驱动。注意继电器是低电平触发还是高电平触发不同模块不一样代码里要对应调整。蜂鸣器用有源蜂鸣器给高电平就响比无源蜂鸣器驱动简单。3. 电路连接与实操步骤3.1 供电方案设计整个系统需要两种电压5V和3.3V。5V给MQ传感器加热丝、继电器、蜂鸣器供电3.3V给STM32、DHT11、OLED供电。我的做法是用一个5V/2A的电源适配器输入然后通过AMS1117-3.3稳压芯片得到3.3V。注意AMS1117的压差是1.1V左右5V降到3.3V没问题但电流不要超过800mA否则发热会很严重。如果你用USB供电注意USB口一般只能提供500mA电流MQ传感器加热丝就要吃掉300mA再加上继电器和蜂鸣器可能会不够。建议用独立的5V电源或者用USB供电但不要同时开继电器和蜂鸣器。3.2 传感器接线与分压电路DHT11接线最简单VCC接3.3VGND接GNDDATA接PA2DATA和VCC之间接一个4.7k上拉电阻。MQ-138和MQ-135各需要三根线VCC接5VGND接GNDAOUT接分压电路。分压电路用两个电阻比如10k和20k5V经过10k和20k分压后得到3.33V刚好在STM32的ADC量程内。分压后的信号接PA0和PA1。OLED接线VCC接3.3VGND接GNDSCL接PB6SDA接PB7。如果你的OLED模块是SPI接口的接线会多一些但I2C版本最省事。继电器接线VCC接5VGND接GNDIN接PB9。继电器的常开触点串联在换气扇的电源线上这样单片机输出高电平时继电器吸合换气扇通电工作。蜂鸣器接线正极接PB8负极接GND。如果是无源蜂鸣器需要输出PWM波才能发声有源蜂鸣器直接给高电平就行。3.3 关键参数计算与阈值设定ADC采集到的原始值是0到4095对应0到3.3V。分压电路的分压比是20/(1020)0.667所以传感器实际输出电压等于ADC电压除以0.667。比如ADC读到2048对应1.65V传感器实际输出是1.65/0.6672.47V。甲醛浓度的换算我用的是简化的线性映射传感器输出电压在0.5V到3.0V之间对应甲醛浓度0到1.0mg/m³。这个映射不是精确的但用于室内空气质量判断足够了。如果你要更精确需要查MQ-138的灵敏度曲线用对数公式计算。阈值设定方面甲醛报警阈值我设的是0.08mg/m³这是国家标准GB/T 18883-2002里规定的室内甲醛浓度限值。CO2浓度用MQ-135的读数间接反映报警阈值设的是1000ppm对应的电压值。温湿度方面温度超过30摄氏度或湿度超过70%RH时触发提醒。注意阈值不要设得太死传感器本身有漂移建议留10%到20%的余量否则会频繁误报。4. 软件架构与核心代码解析4.1 主程序流程设计主程序采用前后台架构主循环里轮询三个任务传感器采集、数据显示、阈值判断与执行。采集任务每2秒执行一次因为DHT11不能太快读取MQ传感器也需要稳定时间。显示任务每500毫秒刷新一次保证屏幕不闪烁。阈值判断任务在每次采集完成后执行根据最新数据决定是否报警和启动换气。这种架构的优点是简单可靠不需要RTOS代码量小适合初学者理解和修改。缺点是实时性一般但对于这个应用场景来说完全够用。4.2 DHT11驱动代码要点DHT11的驱动核心是时序控制。我用的是GPIO模拟单总线的方式关键代码如下void DHT11_Start(void) { GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin GPIO_PIN_2; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); delay_us(18000); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); delay_us(30); GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); }读取40位数据的逻辑是等待DHT11拉低然后等待拉高再等待拉低根据高电平持续时间判断是0还是1。高电平持续26到28微秒表示0持续70微秒表示1。这里用定时器计数或者空指令循环来测量时间。4.3 ADC采集与滤波处理MQ传感器的模拟输出需要做滤波否则读数跳动很大。我用的是滑动平均滤波取最近10次采样的平均值。代码里定义一个长度为10的数组每次采集新数据后替换最旧的数据然后求平均。#define FILTER_LEN 10 uint16_t adc_buf[FILTER_LEN]; uint8_t adc_idx 0; uint16_t ADC_GetFilteredValue(uint8_t channel) { uint32_t sum 0; adc_buf[adc_idx] ADC_GetValue(channel); adc_idx (adc_idx 1) % FILTER_LEN; for (int i 0; i FILTER_LEN; i) { sum adc_buf[i]; } return sum / FILTER_LEN; }滑动平均滤波的缺点是响应慢如果浓度突然变化需要10个采样周期才能完全反映出来。对于室内空气质量检测来说这个延迟可以接受。4.4 显示与报警逻辑实现OLED显示我用的是u8g2库支持多种字体和图形代码简洁。显示内容分四行第一行温度湿度第二行甲醛浓度第三行空气质量第四行系统状态。报警逻辑用状态机实现。系统有三个状态正常、警告、报警。正常状态下屏幕显示绿色图标警告状态下黄色图标加蜂鸣器间歇响报警状态下红色图标加蜂鸣器长响加继电器吸合启动换气扇。状态切换有迟滞比如甲醛浓度超过0.08进入警告降到0.06以下才回到正常避免在阈值附近反复切换。5. 调试过程中的常见问题与排查技巧5.1 传感器读数异常排查DHT11读出来全是0或者全是1最常见的原因是上拉电阻没接或者阻值不对。DHT11的数据线必须接上拉电阻4.7k到10k都可以不接的话总线无法拉高。另一个原因是延时不准特别是用HAL库的HAL_Delay做微秒延时实际延时可能是几百微秒导致时序完全错乱。解决办法是用定时器做微秒延时或者用__NOP()循环。MQ传感器读数一直不变或者变化很小先检查加热丝是否正常发热。用手摸一下传感器表面正常工作时应该有温热感。如果不热说明加热丝没供电或者坏了。如果加热正常但读数不变检查分压电路是否接对用万用表量一下AOUT引脚的电压是否随气体变化。5.2 屏幕显示问题排查OLED不亮先检查供电和I2C地址。不同厂家的OLED模块I2C地址可能不一样常见的是0x78和0x7A。用I2C扫描程序扫一下总线看看能不能找到设备。如果找不到检查SCL和SDA是否接反或者上拉电阻是否缺失。LCD1602显示乱码通常是初始化时序不对或者数据线接触不良。LCD1602的初始化需要严格按照时序特别是延时时间不能省。另外对比度调节电位器要调到合适位置调得太高或太低都会看不到字符。5.3 继电器和蜂鸣器异常排查继电器不吸合先量一下控制引脚有没有输出高电平。如果没有检查代码里引脚配置是否正确是不是被其他外设占用了。如果有高电平但继电器不动作可能是继电器模块的触发方式不对有些模块是低电平触发你给高电平它反而不动。蜂鸣器一直响或者不响先确认是有源还是无源。有源蜂鸣器给高电平就响无源蜂鸣器需要给PWM波。如果是有源蜂鸣器但一直响检查引脚是不是默认输出了高电平或者被其他代码意外拉高了。5.4 常见问题速查表问题现象可能原因排查方法解决方案DHT11读数全0上拉电阻缺失万用表量数据线电压加4.7k上拉电阻DHT11读数跳变延时不准示波器看时序用定时器做微秒延时MQ传感器不发热加热丝未供电摸传感器表面检查5V供电和接线ADC读数满量程分压电路未接量AOUT电压加分压电阻OLED不亮I2C地址错误I2C扫描修改地址为0x78或0x7A继电器不动作触发方式不对量控制引脚电平改代码或换模块蜂鸣器常响引脚默认高电平量引脚电压初始化时设为低电平提示调试时建议一个一个模块单独测试确认每个模块都能正常工作后再整合到一起。一次性全部接好再调试出了问题很难定位。6. 系统优化与扩展方向6.1 数据稳定性优化传感器数据跳动是这类系统最常见的问题。除了滑动平均滤波还可以用中值滤波取最近5次采样的中位数对突发干扰的抑制效果更好。另外ADC采集时可以在软件里做多次采样取平均比如连续采16次去掉最大最小值再平均能进一步降低噪声。电源质量对传感器读数影响很大。MQ传感器的加热丝电流较大如果和单片机共用一路电源加热丝通断时会造成电压波动影响ADC参考电压。我的做法是给传感器单独一路5V供电和单片机的3.3V稳压电路分开实测读数稳定性提升明显。6.2 阈值自适应调整固定阈值在不同环境下可能不合适。比如冬天室内通风差CO2本底浓度就高固定阈值会频繁报警。可以加入自适应逻辑系统上电后先采集一段时间的环境本底值然后以本底值为基准加上一个偏移量作为动态阈值。这样系统能适应不同环境减少误报。6.3 数据记录与远程查看如果想让系统更有实用性可以加一个SD卡模块记录历史数据或者加一个ESP8266/ESP32模块把数据上传到本地服务器。这样你就能在手机或电脑上查看室内空气质量的变化趋势而不是只能看当前值。不过加这些功能会增加代码复杂度和功耗建议先把基础功能调稳再考虑扩展。6.4 外壳设计与安装位置传感器安装位置对测量结果影响很大。DHT11要远离发热源否则温度读数会偏高。MQ传感器要放在空气流通的位置不能闷在密闭盒子里。我的做法是用一个带通风孔的塑料盒传感器从盒子侧面伸出单片机和其他电路放在盒子内部。换气扇的进风口和出风口要形成对流否则换气效果很差。7. 个人实操心得与避坑建议这个项目我从画原理图到打板焊接再到调通代码前后花了大概两周时间踩过的坑比预想的多。最大的教训是不要贪便宜买劣质传感器。我一开始在某平台买了几个几块钱的MQ-138模块结果加热丝阻值不一致两个模块读数差了一倍多根本没法用。后来换了正规厂家的模块一致性好了很多。传感器这种东西省下的钱最后都会变成调试时间。另一个体会是电源设计不能省。我第一版用USB直接供电结果继电器一吸合单片机就复位查了半天才发现是USB电流不够导致电压跌落。后来加了独立的5V电源和足够的滤波电容问题就消失了。如果你也要做这个项目建议在电源部分多花点心思大电容和稳压芯片不要省。代码方面建议把传感器驱动、显示驱动、逻辑控制分成独立的文件不要全部堆在main.c里。这样调试的时候可以单独测试每个模块出了问题也容易定位。我一开始图省事全写在一起后来改一个传感器驱动要翻几百行代码效率极低。最后说一个容易被忽略的细节DHT11的采样间隔。很多人为了刷新快每500毫秒读一次DHT11结果读数经常出错。DHT11的datasheet明确写了采样间隔不能小于1秒最好2秒以上。我现在的代码是2秒读一次稳定运行了几个月没出过问题。这个系统后续还可以加很多功能比如用蓝牙模块把数据传到手机、加一个CO传感器检测一氧化碳、用PWM控制换气扇转速实现无级调速。但基础版本已经能满足大部分室内空气质量监测的需求先把基础打牢再往上加东西才稳。