简介:一份基于STM32的空气质量监测系统完整设计文档,共1个PDF文件,大小41.36MB,目前已有84人学习。内容以项目开发全流程为主线,从需求分析与方案设计入手,完整覆盖STM32F103RCT6主控与SHT30温湿度、PM2.5粉尘、MQ135有害气体、MS1100VOC甲醛等传感器选型连接,并结合ESP8266 WiFi模块实现TCP数据传输。文档同时给出系统框架图、原理图、实物图、硬件组装过程、KEIL工程代码分析以及Qt手机APP上位机的开发流程,涵盖OLED显示、蜂鸣器报警、锂电池供电等关键设计,并讲解了从传感数据采集、无线上传到手机端展示的完整交互链路。对于嵌入式初学者、物联网项目开发者或毕业设计选题者而言,这套资料提供了可借鉴的硬件选型清单、通信协议设计、报警提醒逻辑和低功耗供电方案,适合作为从单片机基础到综合实战项目的重要参考。 空气质量监测系统这个题目,每年都有大量同学在做,但能把数据做“可信”的其实不多。常见的套路是:传感器买回来,程序烧进去,OLED 上有数了,就觉得大功告成。直到被问一句“你这个 PM2.5 数值和天气 App 差了好几倍,误差怎么解释”,前面省掉的每一步,后面都会加倍还回来。这篇就围绕 STM32 主控,从传感器选型、硬件电路、软件分层、调试排错,一直讲到数据校准和联网扩展,把我实际项目里踩过、填平的坑都摊开说。无论是做毕设、课设,还是想给家里做个实时空气质量盒子,这套思路都能直接接得住。
1. 系统方案与器件选型:把“测空气质量”拆成能落地的模块
1.1 监测参数拆解与传感器选型
很多新人拿到题目,第一反应是“我要测空气”,然后就去淘宝搜传感器。但空气好不好是个复合指标,起码拆成三类来看:
- 温湿度:基础环境参数,直接影响体感,还参与其他传感器的温漂补偿。
- 颗粒物:PM2.5、PM10,这类东西是“看得见”的污染,也是大家最关心的数值。
- 气态污染物:CO2、TVOC、甲醛。CO2反映通风状态,TVOC/甲醛更偏向装修污染。
我按这几类参数整理了常用传感器的横向对比,给你选型时做参考:
| 参数 | 常见型号 | 接口 | 实际体验 | 参考价 |
|---|---|---|---|---|
| 温湿度 | DHT11 | 单总线 | 精度差,采样周期必须1s以上,容易互相干扰 | 3元 |
| 温湿度 | SHT30 | I2C | ±0.3℃/±2%RH,很稳,读一次就几ms | 15元 |
| PM2.5 | GP2Y1010AU0F | PWM+模拟量 | 便宜但电路麻烦,采样要和LED驱动同步 | 20元 |
| PM2.5 | PMS5003 | UART | 直接输出ug/m³,数据稳定,省心 | 80元 |
| CO2/TVOC | MQ135 | 模拟量 | 预热久、交叉敏感,只能定性用 | 5元 |
| CO2/TVOC | SGP30 | I2C | 输出eCO2/TVOC,需要基线校准 | 50元 |
| CO2 | MH-Z19 | UART | 红外原理,精度高,功耗大,偏贵 | 200元 |
这里要特别说一句,MQ 系列(MQ135、MQ2 这些)在毕设里出现频率很高,但它里面是半导体气敏材料,对酒精、丙烷、CO2 都敏感,别人在旁边喝口酒,读数就能飙上去。而且基准电压会跟着温湿度一起漂,拿来演示“数值会变化”没问题,但论文里这个数据经不起质询。我一般把它定位成“定性检测/超标报警”,不参与定量计算。
1.2 主控选型与系统框架划分
主控用 STM32F103C8T6 就够了,这是目前最成熟的组合。选它不是因为性能强,而是资源刚好踩在项目的甜点上:Cortex-M3 72MHz、64KB Flash、20KB RAM,跑一套环境监测固件加 OLED 显示完全够用;外设方面有 ADC+DMA、两个 I2C、三个 USART,一个接调试串口、一个接 PMS5003、一个留给 ESP8266,刚刚好。
接口分配我会这样规划:
- ADC1:IN0 接 GP2Y1010 模拟输出,IN1 留作扩展或备用分压通道。
- I2C1:OLED + SHT30 共用,SHT30 地址 0x44,OLED 地址 0x3C。
- USART1:调试打印,接 CH340 转 USB。
- USART2:PMS5003 或 ESP8266,按方案切换。
如果是课程设计,可以加几个独立按键、一个蜂鸣器做超标报警,整体结构更完整。STM32F103ZET6 那种一百多个引脚的板子没必要,C8T6 的核心板十块钱左右,资料还多,坏了也不心疼。
2. 硬件电路:传感器与主控搭配时最容易翻车的几个细节
2.1 电源系统的纹波处理
这个系统里最容易被忽视的就是电源。USB 的 5V 纹波其实不小,如果直接用 AMS1117-3.3 转 3.3V,前端必须加电容:输入端 10uF 电解加 100nF 瓷片,输出端同样要 10uF 加 100nF。不加电容,AMS1117 可能自激振荡,表现为 3.3V 带负载后电压掉到 2.8V 以下,STM32 就开始随机复位。
还有个常见问题:GP2Y1010 驱动LED时的脉冲电流接近 300mA,如果这个电流和 STM32 的 3.3V 共用一路电源,ADC 采出来的数值一定满屏跳。我的做法是用一个 PNP 三极管单独驱动 LED,并在传感器电源输入端串个 10Ω 电阻加 100uF 电容做局部去耦,和数字电路形成水塘隔离,效果立竿见影。
2.2 模拟量前端:分压电阻与采样电容
GP2Y1010 的输出电压范围大致是 0~3.6V,而 STM32 的 ADC 量程是 0~3.3V,直接接过去会削顶。正确做法是先用电阻分压把量程压进 3.3V 以内,比如 3.3k 串 33k,分压比约 0.1,然后再进 ADC 引脚。
分压之后还要注意输出阻抗。传感器内部输出阻抗本来就不低,加上分压电阻会更高,所以引脚处要放一个 RC 滤波:1kΩ 串联电阻加 1uF 电容到地,既滤波又降低源阻抗。如果你跳过这个电容,ADC 采出来的值会偏小,而且不稳定,前后两次读数能差几十个数。
2.3 I2C 总线上拉与多设备地址规划
SHT30 和 OLED 共用 I2C1 时,总线上必须有上拉电阻,一般取 4.7kΩ。如果用的是杜邦线飞线,线长超过 20cm,上拉要降到 2.2kΩ,否则 I2C 通信会出现“有时读得到、有时读不到”的诡异问题,而且示波器肉眼不容易抓。
地址方面,SSD1306 的 OLED 常见地址是 0x3C,SHT30 是 0x44,SGP30 是 0x58,三者不冲突。如果你后面想挂更多的 I2C 设备,建议每挂一个新设备先扫描一遍总线地址,避免冲突之后白调半天。
3. 软件实现:采样、滤波、显示与上报的分层思路
3.1 ADC 多通道 DMA 采样的正确打开方式
如果用 CubeMX 配置 ADC1 的双通道 DMA,有几个必须注意的点:开启 Scan Mode、Continuous Conversion、DMA 的 Circular Mode,数据长度要设成“通道数 × 采样次数”。比方说两个通道各采 32 次,DMA 缓冲区就是 64 个 uint16_t,内存地址自增,这样每次转换完成,环形队列里就有两组通道的数据。
数据对齐要选右对齐,读出来的值就是 0~4095。很多人忘记这一点,直接把 16 位寄存器左移,造成结果整体偏大。回调里做平均的代码大概是这样:
#define PM_CH 0 #define EXT_CH 1 #define SAMPLE_N 32 uint16_t adc_buf[2 * SAMPLE_N]; volatile uint8_t adc_updated = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc != &hadc1) return; uint32_t sum0 = 0, sum1 = 0; for (int i = 0; i < SAMPLE_N; i++) { sum0 += adc_buf[i * 2 + PM_CH]; sum1 += adc_buf[i * 2 + EXT_CH]; } g_pm25.adc_raw = sum0 / SAMPLE_N; g_extra.adc_raw = sum1 / SAMPLE_N; adc_updated = 1; }采样时间也要拉到最大,HAL 库配置里把每个通道的采样周期从默认几个周期改成 55.5 周期或更多,这样对高阻抗源更友好,数值稳定性会明显提升。
3.2 轻量滤波:滑动平均与一阶低通怎么选
传感器数据是慢变量,不需要上卡尔曼,滑动平均就够了。维护一个长度 8 到 16 的窗口,每次新采样入队、最老的出队,输出窗口均值。注意累加类型要用 uint32_t,16 次 × 4096 已经超过 uint16_t 的 65535 上限了。代码示意:
#define WINDOW_SIZE 16 static uint16_t buf[WINDOW_SIZE]; static uint8_t idx = 0; static uint32_t sum = 0; uint16_t moving_average(uint16_t new_sample) { sum -= buf[idx]; sum += new_sample; buf[idx] = new_sample; idx = (idx + 1) % WINDOW_SIZE; return sum / WINDOW_SIZE; }如果系统 RAM 很紧张,可以用一阶低通替代:y = y_old * 0.85 + x * 0.15,只需要两个变量,实时性还更好,但要注意系数选择。滑动平均适合画趋势图,一阶低通适合做实时响应,两者都够用,我一般按需求二选一,不会同时上两套滤波。
3.3 用定时器做时间片,别在主循环里 sleep
空气质量系统同时要做的事不少:2秒读一次温湿度,1分钟读一次颗粒物,500ms 刷新一次 OLED,200ms 打一条串口日志。如果全用 HAL_Delay 串行处理,一个等一个,后续任务全被拖死。正确做法是维护一个毫秒级的 tick,主循环里用时间差判断任务是否到期。
static volatile uint32_t s_tick = 0; void SysTick_Handler(void) { s_tick++; } uint32_t now_ms(void) { return s_tick; } while (1) { uint32_t now = now_ms(); if (now - last_sht30 >= 2000) { sht30_read_all(&temp, &humi); last_sht30 = now; } if (now - last_display >= 500) { oled_refresh(&temp, &humi, &pm25_value); last_display = now; } if (now - last_uart >= 200) { debug_printf("temp=%.1f humi=%.1f pm25=%d\r\n", temp, humi, pm25_value); last_uart = now; } }这里用无符号减法做时间差,自然处理绕卷,不用担心 tick 溢出。核心逻辑是所有任务都是“检查是否到期,到期就执行”,其余时间 CPU 可以处理中断或进入低功耗等待。这个结构一换到别的传感器项目里也能直接用,我后面几个项目基本就是复制这套调度框架改任务周期而已。
4. 调试踩坑:从“编译通过”到“数据能看”的完整链路
4.1 下载失败:no stm32 target found 的排查顺序
热词榜单里 “error: no stm32 target found” 常年霸榜,说明这是几乎每个人都会遇到的问题。我现在的排查顺序基本固定:
- 先查供电。ST-LINK 的 3.3V 能否带动整个板子?如果带不动,改用外部 5V 供电并把 GND 和 ST-LINK 共地。
- 再查接线。SWDIO、SWCLK、GND 三条线是不是一一对应,有没有拿 JTAG 线序去接 SWD。
- 然后查复位。NRST 引脚的电容如果太大(100uF 级别),ST-LINK 连接时拉低复位信号会失败。常规 104 电容没问题,但别手滑用大电容。
- 最后考虑 BOOT0 和读保护。如果芯片之前被写入了关掉 SWD 的代码,或者读保护开启,连接就会失败。把 BOOT0 拉到 1,重新上电,用 STM32CubeProgrammer 解除读保护,再拉回 0 就能恢复正常。
新料里提到的 Debug Authentication 多出现在部分新系列芯片上,F103 本身没有这个机制,但如果你用的核心板是较新的批次,已经把 SWD 引脚默认复用掉了,思路是一样的:先从供电和接线查起,再处理 BOOT0 和读保护。
4.2 delay 卡死:SysTick 被谁抢了
HAL_Delay 卡死的案例我见过几类。最常见的是重写了 SysTick_Handler 却没调用 HAL_IncTick,结果 uwTick 永远不增长,HAL_Delay 死等。还有一种是中断里面调用了 HAL_Delay,而该中断优先级比 SysTick 高,抢占后 SysTick 无法触发,直接卡在中断里出不来。
另外,在 FreeRTOS 工程里把 HAL_Delay 和 vTaskDelay 混着用,也可能互相踩tick。
我的建议很简单:不要依赖 HAL_Delay 做业务逻辑里的延时。用上面说的时间片调度代替,所有“等一段时间”全部改成“到期检查”。这样既不会卡死,也让系统的时序一目了然。如果一定要用中断里的延时,那也是短延时,比如 GP2Y1010 在 LED 脉冲开始后要等 280us 再采集,这种用 DWT 或定时器的 us 级延时实现,避免整毫秒阻塞。
4.3 串口乱码与 ADC 数值跳变
串口打印中文乱码,先别怀疑编码问题,九成是时钟配置不对。CubeMX 默认用外部 8MHz 晶振,如果板子实际上没焊晶振,或者晶振没起振,HAL 的 HSE 初始化失败后会回退到 HSI 8MHz。这时 SystemCoreClock 显示 8000000,而代码按 72MHz 去配波特率,串口助手收到的自然全是乱码。排查时先读一下 SystemCoreClock,如果是 8000000,问题就锁定在时钟树或晶振上。
ADC 数值跳变的常见原因有三个:传感器供电纹波大、ADC 引脚没加滤波电容、采样时间太短。其中供电纹波最阴险,USB 口的 5V 噪声能直接耦合到传感器,导致读数跟着电脑负载波动。我的经验是先用充电宝供电对比一下,如果充电宝供电时数据稳定,基本就是 USB 电源的锅。
5. 数据校准与功能扩展:让系统从“能跑”变成“能用”
5.1 廉价传感器的预热、漂移与基线校准
把硬件调通只是完成了 60%,剩下 40% 是让数据可信。MQ 系列上电后必须预热,我实测 MQ135 至少要通电 12 小时以上,输出才基本稳定,48 小时后才敢用来算基线。GP2Y1010 也有离散性,每片传感器的洁净空气输出电压 V0 和灵敏度系数 K 都不同,所以校准是必须的,不是可选项。
我的校准做法是三个步骤配合:
- 先在洁净环境(比如室外通风处)采 10 分钟数据,取平均作为零点 V0。
- 然后用参考设备(家里现成的空气质量检测仪、气象站数据都行)在同一环境同步采 10 分钟,取平均做线性回归,得到斜率 a 和截距 b。
- 最后把 V0、a、b 的值存到 STM32 内部 Flash 的最后几页,代码里直接读取使用,断电不丢。
SGP30 这种芯片更讲究,首次使用要老化 12 小时,之后在 400ppm CO2 的洁净空气里校准基线。不校准就直接用,eCO2 的绝对值可能偏 30% 以上,但是趋势是准的,所以判断“开窗通风后是否下降”没问题,报精确浓度不靠谱。
5.2 从单机版到联网版的三个扩展方向
单机版调通了,下一步通常是联网。我按难度从低到高排三个方向:
- 本地串口上位机:用 CH340 接 STM32 的 USART1,QT 或 C# 写个串口小工具,把数据画成曲线。这是最稳的保底方案,调试时还能顺便看日志。
- ESP8266 上报云平台:USART2 接 ESP8266,AT 固件连 WiFi,用 HTTP POST 把 JSON 包发到巴法云、OneNET 这类平台。AT 指令流程不复杂,但要注意 ESP8266 耗电峰值接近 300mA,不能直接用 STM32 的 3.3V 引脚给它供电,必须单独用一片 3.3V LDO。
- 低功耗电池版:把主控换成 STM32L0 或 L4 系列,RTC 定时唤醒,每次上电采样一两秒后继续睡眠。GP2Y1010 的 LED 平时也要断电,只在测量期间供电。这套方案能把平均功耗压到毫安级以下,适合做长期摆放的监测节点。
如果后面想做成自动新风系统,还需要加一路继电器控制风机,并用 PID 根据 PM2.5 偏差调节风量。那是另一个故事,但底层的采样与调度框架是可以直接复用的。
5.3 我自己的落地体会
按这套思路做下来,你得到的不是一份一次性课设代码,而是一个“换传感器、换场景都能复用”的嵌入式框架。我现在手头这套系统后来加过甲醛传感器、接了一路继电器控制新风,还在试着用 PID 控风机转速。真要说做这类项目最深的体会,就是别急着堆硬件,先把“要测什么、数据给谁看、怎么证明数据准”这三件事想明白,后面所有电路和代码都不太会跑偏。数据可信度这一点,答辩时几乎是必问的,也是区分“应付”和“认真做”的分水岭。
本文还有配套的精品资源,点击获取