
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、DHT111个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或者SHT30DHT22精度更高但价格贵一倍多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.7kMQ-138模拟输出PA1ADC1_IN1MQ-135模拟输出PA2ADC1_IN2LCD1602 RSPB0寄存器选择LCD1602 RWPB1读写选择接地也可LCD1602 ENPB2使能LCD1602 D4-D7PB4-PB74位数据模式蜂鸣器PB8经三极管驱动红色LEDPB9限流220Ω绿色LEDPB10限流220Ω继电器控制PB11低电平触发串口TX/RXPA9/PA10调试用LCD1602我用的是4位数据模式这样只需要6个IORS、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设到72MHzADC1开启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 × RLVc是回路电压5VRL是负载电阻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以上然后发送三次0x038位模式指令再发送0x02切换到4位模式。很多人直接发0x02结果屏幕不亮或者乱码。只显示一行的问题我遇到过是因为DDRAM地址设置错了。LCD1602第一行地址是0x80第二行是0xC0。写第二行数据前要先发0xC0指令忘了发就会覆盖第一行的内容。现象可能原因解决方法屏幕全黑对比度太低调V0电阻屏幕全白对比度太高调V0电阻显示乱码初始化时序错按标准时序重写只显示一行DDRAM地址错第二行发0xC0背光不亮限流电阻过大换100Ω或直接接5V5.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分钟就测读数比稳定后高了将近一倍完全不可信。