1. 项目缘起与整体设计思路
室内空气质量这件事,很多人是搬进新装修的房子、或者家里添了新车之后才开始真正在意的。甲醛超标、二氧化碳浓度过高、温湿度失衡,这三个问题凑在一起,轻则让人头晕犯困、皮肤过敏,重则影响长期健康。市面上的成品检测仪动辄几百上千,功能还未必齐全,于是拿一块单片机自己搭一套检测系统,就成了很多电子爱好者和嵌入式学习者的首选练手项目。
我这次做的这套系统,核心目标很明确:用一块STM32F103C8T6最小系统板做主控,挂上DHT11温湿度传感器和MQ-138甲醛传感器,再配一块OLED12864显示屏做本地显示,同时预留CO2检测通道,最后加上继电器控制换气扇和蜂鸣器报警。整套东西做下来成本不到一百块,但功能覆盖了“检测—显示—判断—执行”这条完整链路,拿来当课程设计、毕业设计或者个人练手项目都非常合适。
为什么选STM32F103C8T6而不是51单片机?这个问题我在动手之前也纠结过。51单片机便宜、资料多、上手快,但它的ADC精度和运算能力有限,跑多传感器融合和阈值判断的时候会比较吃力。STM32F103C8T6是ARM Cortex-M3内核,72MHz主频,自带12位ADC,多个USART和I2C接口,处理DHT11的时序、MQ-138的模拟量采集、OLED的I2C通信这几件事同时进行毫无压力。而且这块芯片的国产替代方案已经很成熟,价格压得很低,最小系统板十几块钱就能买到,性价比极高。
整个系统的设计思路可以概括为“采集层—处理层—执行层”三层结构。采集层负责把甲醛浓度、CO2浓度、温度和湿度这四个物理量转换成电信号;处理层由STM32完成数据解析、阈值比较和状态判断;执行层则根据判断结果驱动OLED显示、继电器换气和蜂鸣器报警。这个分层思路的好处是每一层都可以独立调试,传感器坏了换传感器,执行器不动作查驱动电路,不会牵一发而动全身。
提示:如果你之前只玩过51单片机,转到STM32时最大的门槛不是编程本身,而是开发环境的搭建和库函数的使用。建议先用STM32CubeMX生成初始化代码,再配合HAL库逐步添加功能,不要一上来就啃寄存器手册。
2. 核心器件选型与关键参数解析
2.1 主控芯片STM32F103C8T6的引脚分配与资源规划
STM32F103C8T6是LQFP48封装,48个引脚里有37个可用GPIO,对于这个项目来说绰绰有余。但引脚分配不能随便来,得根据外设功能来规划。我把DHT11的数据线接在PA0,因为PA0支持外部中断,DHT11的单总线协议需要精确的微秒级延时,用中断方式处理会更稳。MQ-138的模拟输出接PA1,对应ADC1的通道1。OLED的I2C接口用PB6和PB7,这是STM32F103的I2C1默认引脚,硬件I2C比软件模拟更省CPU资源。
继电器的控制引脚我放在PB0,蜂鸣器放在PB1,这两个都是普通推挽输出就行。CO2传感器如果用的是模拟输出的型号,可以接PA2,对应ADC1的通道2;如果用的是UART输出的型号,就接PA9和PA10,用USART1接收数据。这里要特别注意,STM32F103C8T6的PA9和PA10同时也是下载程序的串口引脚,如果你用串口下载,调试的时候可能会冲突,建议CO2传感器优先选模拟输出型号。
供电方面,最小系统板一般有5V和3.3V输出。DHT11和OLED用3.3V,MQ-138和继电器用5V,CO2传感器看具体型号。这里有个坑:MQ-138的加热丝电流比较大,大概150mA左右,如果和继电器同时工作,5V电源的电流要留够余量,建议用2A以上的电源适配器,否则会出现传感器读数漂移或者继电器吸合不稳定的情况。
2.2 DHT11温湿度传感器的时序要点与读取策略
DHT11是一款性价比很高的数字温湿度传感器,但它的单总线协议对时序要求非常严格。一次完整的读取过程包括:主机拉低数据线至少18ms作为起始信号,然后释放总线,等待DHT11响应。DHT11会先拉低80微秒,再拉高80微秒,之后开始传输40位数据。每一位数据都以50微秒的低电平开始,然后高电平的持续时间决定数据是0还是1——26到28微秒表示0,70微秒表示1。
这个时序用STM32的HAL库实现时,不能用HAL_Delay,因为它的最小分辨率是1毫秒,而我们需要的是微秒级延时。我的做法是用SysTick定时器做一个微秒级延时函数,或者直接用__NOP()空指令做粗略延时。实测下来,用SysTick的计数方式最稳,误差能控制在1微秒以内。
DHT11的采样周期不能太快,官方建议两次读取之间至少间隔1秒,实际上我建议间隔2秒以上。因为DHT11内部有个电阻式湿度敏感元件,每次测量后需要一定时间恢复。如果你连续快速读取,会发现数据基本不变或者出现校验错误。我在代码里设了一个2秒的定时器,每2秒读一次DHT11,读完之后做校验和判断,校验失败就丢弃本次数据,保留上一次的有效值。
注意:DHT11的湿度精度是±5%RH,温度精度是±2℃,这个精度对于室内空气质量监测来说够用,但如果你要做精密控制,建议换DHT22或者SHT30。另外DHT11的响应时间比较慢,从上电到第一次有效读取大概需要1到2秒,程序初始化后不要立刻读,先延时2秒再开始。
2.3 MQ-138甲醛传感器的预热与标定方法
MQ-138是MQ系列气体传感器中专门针对甲醛检测的型号,它的敏感材料是二氧化锡,在加热状态下接触甲醛气体会导致电阻变化,从而输出电压信号。这个传感器有两个关键特性:一是需要预热,二是输出是非线性的。
预热时间官方给的是24小时老化,但实际使用中,每次上电后至少预热5到10分钟,读数才会稳定。我实测下来,刚上电的时候输出电压会从高往低缓慢下降,大概8分钟左右趋于平稳。所以程序里我加了一个预热标志位,上电后前10分钟OLED显示“预热中”,不参与报警判断,避免误报。
标定是MQ-138最麻烦的地方。它输出的是模拟电压,对应的是甲醛浓度的相对值,不是绝对浓度。如果你要得到ppm级别的读数,需要用标准气体或者专业的甲醛检测仪做对比标定。我的做法比较简单:在已知清洁空气中读取一个基准电压V0,然后在密闭空间里用甲醛溶液挥发制造一个已知浓度的环境,读取电压V1,用两点法拟合一条曲线。虽然精度有限,但对于判断“是否超标”这个需求来说足够了。
实际电路中,MQ-138的输出需要接一个负载电阻RL,典型值是10kΩ。输出电压Vout = Vc × RL / (Rs + RL),其中Rs是传感器的敏感电阻。通过测量Vout反推Rs,再根据数据手册的曲线查浓度值。这个过程在STM32里用查表加线性插值实现,比直接算公式更准。
2.4 OLED12864显示屏的I2C驱动与界面布局
OLED12864指的是0.96寸、128×64分辨率的OLED屏,有I2C和SPI两种接口。I2C版本接线简单,只需要SCL和SDA两根线,但刷新率比SPI低。对于这个项目来说,我们只需要显示四行数据加几个状态图标,I2C完全够用。
驱动芯片一般是SSD1306,STM32的HAL库有现成的I2C接口,但SSD1306的初始化命令序列比较长,建议直接用现成的驱动库,比如网上流传很广的oled.c和oled.h文件。移植的时候注意两点:一是I2C地址,一般是0x78或0x7A,取决于SA0引脚的电平;二是显示缓冲区的大小,128×64分辨率需要1024字节的显存,STM32F103C8T6有20KB RAM,放一个显存缓冲区绰绰有余。
界面布局我设计成四行:第一行显示温度,第二行显示湿度,第三行显示甲醛浓度,第四行显示CO2浓度和系统状态。状态用图标表示,正常是绿色笑脸,超标是红色感叹号。字体用6×8和8×16两种,标题用大字体,数据用小字体。刷新策略上,不要每次循环都全屏刷新,那样会有闪烁感。我的做法是只刷新变化的数据区域,用OLED_Set_Pos定位到具体坐标,再写数据。
3. 硬件搭建与软件实现全流程
3.1 电路原理图设计与嘉立创EDA实操
画原理图我用的嘉立创EDA,免费而且元件库比较全。整个电路分成四个模块:主控最小系统、传感器接口、显示模块、执行器驱动。
主控部分直接把STM32F103C8T6最小系统板当成一个元件放上去,引出需要的引脚。传感器接口部分,DHT11接3.3V和GND,数据线接PA0并加一个4.7kΩ上拉电阻。MQ-138接5V和GND,模拟输出接PA1,负载电阻RL用10kΩ。OLED的VCC接3.3V,GND接地,SCL接PB6,SDA接PB7,I2C总线上各加一个4.7kΩ上拉电阻。
执行器驱动部分,继电器模块我用的是现成的5V继电器模块,输入控制端接PB0,模块内部已经有光耦隔离和续流二极管,直接驱动就行。蜂鸣器用有源蜂鸣器,接PB1,串联一个1kΩ限流电阻。这里要注意,有源蜂鸣器通电就响,无源蜂鸣器需要PWM驱动,买的时候看清楚。
PCB布局的时候,MQ-138的加热丝电流较大,走线要加粗,至少20mil。模拟信号线(PA1)尽量远离数字信号线,避免干扰。OLED的I2C走线不要太长,超过10厘米建议加屏蔽或者降低速率。
3.2 STM32CubeMX初始化配置与HAL库代码框架
用STM32CubeMX配置外设可以省去大量底层代码。具体配置如下:系统时钟选HSE外部晶振,主频拉到72MHz。ADC1配置为独立模式,扫描模式关闭,单次转换,通道1和通道2轮流采集。I2C1配置为标准模式,100kHz速率。USART1配置为115200波特率,用于调试输出。GPIO方面,PA0配置为输入模式,带上拉;PB0和PB1配置为推挽输出,初始电平低。
定时器我用TIM2做2秒定时中断,在中断里置一个标志位,主循环检测到标志位就执行一次传感器读取。这样比在主循环里用HAL_Delay更高效,不会阻塞其他任务。
代码框架上,我建了这几个文件:main.c负责主循环和任务调度,dht11.c和dht11.c负责温湿度读取,mq138.c负责甲醛采集和浓度换算,oled.c负责显示驱动,relay.c和buzzer.c负责执行器控制。每个模块都有独立的初始化和功能函数,main函数里按顺序调用就行。
// main.c 主循环片段 while (1) { if (timer2_flag) { timer2_flag = 0; DHT11_Read(&temp, &humi); MQ138_Read(&formaldehyde); CO2_Read(&co2); OLED_Refresh(temp, humi, formaldehyde, co2); Check_Threshold(temp, humi, formaldehyde, co2); } }3.3 多传感器数据融合与阈值判断逻辑
四个传感器读回来的数据格式不一样,DHT11是整数加小数,MQ-138是0到4095的ADC值,CO2也是ADC值。我先把它们统一转换成工程量:温度单位摄氏度,湿度单位百分比,甲醛单位mg/m³,CO2单位ppm。
阈值判断我设了三档:正常、警告、危险。温度正常范围18到28℃,警告范围10到18℃或28到35℃,超出就是危险。湿度正常40%到70%,甲醛正常低于0.08mg/m³,CO2正常低于1000ppm。判断逻辑用if-else嵌套实现,每个参数独立判断,最后综合出一个系统状态。
这里有个细节:甲醛和CO2的浓度是渐变的,如果阈值设得太死,会在临界点附近频繁触发报警。我加了一个迟滞区间,比如甲醛超过0.08报警,但要降到0.06以下才解除报警,这样避免继电器反复吸合。
3.4 自动换气与声光报警的执行器驱动
继电器控制换气扇的逻辑很简单:系统状态为警告或危险时,PB0输出高电平,继电器吸合,换气扇启动;状态恢复正常后延时30秒再关闭,确保空气充分交换。这个延时很有必要,我试过立即关闭,结果甲醛浓度很快又升上来,反复启动对继电器寿命也不好。
蜂鸣器报警分两种模式:警告状态用间歇鸣叫,响1秒停2秒;危险状态用连续鸣叫。用TIM3做PWM输出控制蜂鸣器频率,有源蜂鸣器直接给高低电平就行,无源蜂鸣器需要2kHz到4kHz的方波。
OLED显示同步更新,正常状态显示绿色“OK”,警告状态显示黄色“WARN”,危险状态显示红色“DANGER”并闪烁。闪烁用定时器控制,每500毫秒翻转一次显示状态。
4. 调试过程中的典型问题与排查实录
4.1 DHT11读取失败与校验错误的排查思路
DHT11读不出来是最常见的问题,表现是返回值为0或者校验和错误。排查步骤我总结了一个顺序:先查硬件,再查时序,最后查电源。
硬件方面,数据线的上拉电阻不能省,4.7kΩ到10kΩ都行,不加上拉的话信号上升沿太慢,STM32可能识别不到。供电电压要在3.3V到5.5V之间,低于3V DHT11不工作。接线长度不要超过20厘米,太长的话分布电容会影响时序。
时序方面,用示波器看数据线的波形最直观。起始信号的低电平要够18ms,响应信号的80微秒低电平和80微秒高电平要清晰。如果波形畸变,检查GPIO模式是不是配置成了开漏输出,应该配置成推挽输出。
电源方面,DHT11对电源纹波比较敏感,如果和继电器共用5V电源,继电器吸合的瞬间可能导致DHT11复位。我的解决办法是在DHT11的VCC和GND之间并一个100微法的电解电容,效果很明显。
4.2 MQ-138读数漂移与预热时间不足的影响
MQ-138的读数漂移问题困扰了我很久。刚上电的时候读数一路往下掉,大概10分钟后才稳定。一开始我以为传感器坏了,后来查资料才知道这是正常现象。二氧化锡敏感材料需要达到工作温度后电阻才会稳定,这个预热过程不可避免。
除了预热时间,环境温湿度也会影响MQ-138的读数。湿度越高,输出电压越低。我在代码里加了一个温湿度补偿系数,根据DHT11读到的湿度值对甲醛浓度做修正。补偿公式是:修正值 = 原始值 × (1 + 0.02 × (湿度 - 50))。这个系数是我实测拟合出来的,不一定适用于所有场景,但比不补偿强。
还有一个坑是MQ-138的负载电阻选择。RL太大,输出电压高但灵敏度低;RL太小,灵敏度高但输出范围窄。我试过4.7kΩ、10kΩ和20kΩ,最后选了10kΩ,兼顾灵敏度和量程。
4.3 OLED显示乱码与I2C通信超时的解决
OLED显示乱码一般是I2C通信出了问题。先检查地址对不对,用I2C扫描程序扫一下总线上的设备地址,确认是0x78还是0x7A。然后检查上拉电阻,I2C总线必须有上拉,STM32内部的上拉电阻比较弱,建议外接4.7kΩ。
如果I2C通信超时,可能是总线被拉死了。这种情况通常是因为某个从机在通信过程中复位,导致SDA线被持续拉低。解决办法是在初始化的时候手动发送9个时钟脉冲,让从机释放总线。代码实现就是先把SCL配置成推挽输出,翻转9次,再重新配置成I2C模式。
显示乱码还有一个可能是显存缓冲区没有正确初始化。SSD1306上电后显存内容是随机的,必须先清屏再写数据。我的做法是在OLED_Init函数最后调用一次OLED_Clear,把整个显存写0。
4.4 继电器吸合干扰与电源稳定性处理
继电器吸合的瞬间会产生反向电动势,这个电动势会通过电源线传导到单片机和传感器,导致复位或读数异常。我在继电器线圈两端并了一个续流二极管(1N4148),在继电器模块的电源输入端并了一个100微法电解电容和一个0.1微法陶瓷电容,双管齐下。
电源方面,我用的是5V 2A的适配器,单独给继电器和MQ-138供电,STM32和OLED用另一路5V转3.3V供电。两路电源共地但不共正,这样继电器的干扰不会串到单片机那边。实测下来,继电器吸合时单片机不再复位,DHT11读数也不跳变了。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| DHT11返回0 | 上拉电阻缺失 | 万用表测数据线电压 | 加4.7kΩ上拉电阻 |
| DHT11校验错误 | 时序偏差 | 示波器看波形 | 调整微秒延时函数 |
| MQ-138读数漂移 | 预热不足 | 记录上电后读数曲线 | 预热10分钟再采集 |
| OLED乱码 | I2C地址错误 | I2C扫描程序 | 确认地址0x78或0x7A |
| 继电器吸合导致复位 | 电源干扰 | 示波器看电源纹波 | 加续流二极管和滤波电容 |
| CO2读数异常 | 串口冲突 | 检查PA9/PA10占用 | 改用模拟输出传感器 |
5. 系统优化与功能扩展的实操建议
5.1 数据滤波与显示刷新率的平衡
原始ADC读数会有波动,直接显示会跳来跳去。我用了滑动平均滤波,每采集10次取平均值,这样读数平滑很多。但滑动平均会带来滞后,对于甲醛这种需要快速响应的参数,我改用一阶低通滤波,系数取0.3,兼顾平滑和响应速度。
显示刷新率方面,OLED全屏刷新一次大概需要20毫秒,如果每2秒刷新一次,CPU占用率很低。但如果你把刷新率提高到100毫秒一次,CPU就有10%的时间花在刷屏上。我的建议是数据采集2秒一次,显示刷新500毫秒一次,只刷新变化的数据区域,这样既流畅又不浪费资源。
5.2 阈值可调与参数掉电保存的实现
固定阈值不够灵活,不同场景对空气质量的要求不一样。我加了一个按键,短按切换设置项,长按进入设置模式,用另一个按键加减数值。设置好的阈值存在STM32内部的Flash里,掉电不丢失。Flash读写要注意,STM32F103的Flash擦写次数有限,不要频繁写,我是在设置完成后统一写一次。
5.3 从本地报警到远程通知的扩展思路
本地声光报警适合有人在家的场景,但如果人不在家,就需要远程通知。最简单的扩展是加一个ESP8266或ESP32模块,通过串口和STM32通信,把数据上传到云平台,手机就能随时查看。再进一步,可以加一个GSM模块,超标时直接发短信。不过这些扩展会增加成本和复杂度,建议先把基础功能调稳再考虑。
5.4 外壳设计与传感器布局的注意事项
外壳我用的3D打印,材料是PLA。设计的时候要注意,DHT11和MQ-138需要接触空气,不能密封在壳子里。我在外壳上开了通风孔,传感器用排线引出,固定在通风孔附近。OLED显示屏开一个矩形窗口,用热熔胶固定。
传感器布局上,MQ-138要远离发热元件,因为它的读数受温度影响。继电器和电源模块放在另一侧,用隔板隔开。整个外壳的尺寸大概是120×80×40毫米,放在桌面上不占地方。
提示:如果你没有3D打印机,用塑料收纳盒改造也行,关键是保证传感器周围空气流通,同时避免灰尘和油污直接落在传感器上。
6. 写在最后的一些个人体会
这套系统我从画原理图到调试完成,前后花了大概两周时间,其中大部分时间花在传感器标定和干扰排查上。硬件搭建本身不难,难的是让四个传感器同时稳定工作,互不干扰。我的经验是,电源一定要分开供电,模拟地和数字地单点接地,这两个措施能解决80%的干扰问题。
另外,不要迷信数据手册上的典型电路,实际调试中你会发现很多参数需要根据你的具体环境调整。比如MQ-138的负载电阻,数据手册推荐10kΩ,但在我这个电路里,4.7kΩ的响应更快。多试几组参数,用实测数据说话,比照搬手册靠谱得多。
最后说一个容易被忽略的点:传感器的寿命。MQ-138的加热丝长期通电会老化,DHT11的湿敏元件在潮湿环境下会漂移。建议每隔半年用标准仪器校准一次,或者直接换新传感器。这东西不贵,换一个比调半天省事。