1. 项目概述与目标拆解
1.1 为什么选红外PM2.5传感器而非激光传感器
先把话说明白:STM32接PM2.5传感器这事,核心不在STM32,在传感器。市面上能买到的PM2.5传感器基本分两派——红外散射式和激光散射式。激光的精度高、能测到更小的颗粒物浓度,但价格动辄五六十起步,好的上百;红外的便宜,十几二十块钱能拿下,用在室内空气质量监测、智能家居、DIY环境监测站这些场景完全够用。
这个项目选红外PM2.5传感器,核心诉求就一个字:性价比。比如夏普的GP2Y1010AU0F,或者兼容的GP2Y1014AU0F,这两个型号在DIY圈子里非常常见。它们的原理是内置一个红外LED和一个光电二极管,空气通过传感器中间的孔洞时,颗粒物散射红外光,光电二极管接收到散射光后输出一个与浓度相关的电压信号。这个原理决定了它的响应速度不会太快,精度也不如激光的,但胜在结构简单、驱动电路成熟、数据读取方便,和STM32搭起来非常顺畅。
1.2 这个项目能解决什么问题
做这个项目的典型场景有这么几种:
- 室内空气质量监测:实时采集PM2.5浓度,超过阈值就联动继电器开新风、开净化器。
- 毕业设计/课程设计:STM32作为主控,搭配OLED屏显示浓度数值,外加蜂鸣器报警,这个组合在本科生课设里出现频率极高。
- 小型环境监测节点:接入ESP8266/ESP32或者LoRa模块,把数据上传到云端,组一个多点位监测网络。
我这次做的时候,顺带把数据在0.96寸OLED上显示出来,同时通过串口打印原始ADC值和换算后的浓度值。这样调试阶段不用额外买调试器,一根USB转TTL线就能把数据看明白。
1.3 整体技术路线概览
整个项目的技术链路是:红外PM2.5传感器输出PWM或模拟电压信号 → STM32通过ADC采样或定时器输入捕获读取信号 → 单片机内部做滤波和浓度换算 → 结果输出到OLED/串口/继电器。
这里有个重要的选型决策:GP2Y1010AU0F和GP2Y1014AU0F两个型号引脚兼容,但输出方式有区别。GP2Y1010AU0F输出的是模拟电压,需要STM32的ADC来采样;GP2Y1014AU0F除了模拟电压,还支持PWM输出,PWM的高电平脉宽对应浓度值。后面我会详细讲这两种读取方式各自的优劣,以及我在实际项目中为什么最终选了ADC方案。
2. 传感器原理与读取方案选型
2.1 红外散射法的物理本质
先把原理讲透,这对后面写代码和理解数据非常有帮助。
红外PM2.5传感器内部结构大致是这样的:一个红外LED作为光源,一个光电二极管作为接收端,中间是一个能让空气自由流通的通道。工作时,LED以固定频率发射红外光(GP2Y1010AU0F的驱动脉冲频率典型值是10kHz左右,脉宽约0.32ms),当空气中存在颗粒物时,颗粒物会散射红外光,其中一部分散射光被光电二极管接收。颗粒物浓度越高,散射光强度越大,光电二极管输出的光电流就越大。
这里要特别强调一点:红外散射法对粒径较小的颗粒物(比如PM1.0以下的细颗粒)响应不灵敏,因为散射强度与粒径的六次方相关(瑞利散射区域),粒径小一截,散射光强会衰减得很厉害。这就是为什么红外传感器在空气质量很好的时候(浓度低于20ug/m³)读出来的数据波动很大,甚至会出现“虚高”的情况。这不是传感器坏了,是物理原理决定的。
2.2 三种读取方式对比:ADC、PWM捕获、UART
红外PM2.5传感器和STM32的接口方式主要有三种,我分别说一下优缺点:
| 读取方式 | 传感器输出 | STM32外设 | 优点 | 缺点 |
|---|---|---|---|---|
| 模拟电压 | 0~3.6V模拟信号 | ADC | 接线简单,单引脚读取,实时性好 | 需要外部RC滤波电路,受电源纹波影响 |
| PWM脉宽 | 周期性PWM,高电平脉宽对应浓度 | 定时器输入捕获 | 抗干扰能力强,不需要ADC | 占用定时器通道,代码稍复杂 |
| UART串口 | 主动或被动上报数字浓度 | USART | 数据直接是处理好的浓度值 | 需要协议解析,部分传感器是单向输出 |
我在实际项目里最先试的是ADC方案,原因很直接:GP2Y1010AU0F的模拟输出最经典,网上现成的参考电路和代码最多,出问题了好排查。后来为了验证PWM方案的可行性,也写了一个基于定时器输入捕获的读取代码,实测下来PWM方案确实更稳,数据跳变幅度比ADC小不少,但这个结论是在加了比较好的外部滤波电路之后对比得出的。
2.3 为什么最终选了ADC + 定时器捕获双方案
其实我这个项目最后做成了“一个传感器、两种读取方式”的兼容设计。主板上有跳线帽,拨到A侧走ADC采样,拨到P侧走PWM捕获。这样设计的好处是:
- 兼容不同型号的红外PM2.5传感器,不用换板子。
- 调试时可快速切换读取方式,对比数据稳定性,找到最适合的采样策略。
- 后续如果换激光传感器(很多激光传感器直接输出UART数据),板子的硬件改动也只是加一个串口接口的事。
从“做一个能用的东西”角度来说,ADC方案就够了;但如果你想把这个项目作为学习STM32外设用法的练手项目,我强烈建议把PWM捕获也实现一遍。定时器输入捕获是STM32非常实用的功能,测频率、测脉宽、做编码器读数全靠它,这个项目里正好用上。
3. 硬件接线与电路设计
3.1 传感器引脚定义与接线对照
以GP2Y1010AU0F为例,它一共6个引脚,功能如下:
| 引脚号 | 名称 | 功能 | 接STM32 |
|---|---|---|---|
| 1 | V-LED | LED供电正极(+5V) | 5V电源 |
| 2 | LED-GND | LED地 | GND |
| 3 | LED | LED驱动脉冲输入 | STM32定时器PWM输出引脚 |
| 4 | S-GND | 传感器地 | GND |
| 5 | Vo | 模拟电压输出 | STM32 ADC引脚 |
| 6 | Vcc | 传感器供电正极(+5V) | 5V电源 |
注意这里面最容易被坑的是第3脚LED驱动脉冲。GP2Y1010AU0F的内部LED不是常亮的,而是需要外部提供一个约10kHz、脉宽0.32ms的脉冲信号来驱动。这个脉冲信号有两个来源:一是用STM32的定时器输出PWM,频率和脉宽都要配准确;二是用NE555这类硬件电路产生固定脉冲。
如果你偷懒直接把LED引脚接高电平让LED常亮,传感器会很快发热,而且输出数据完全不对。这一点我第一次做的时候踩过坑,后来老老实实配了定时器PWM输出。
3.2 供电电路:AMS1117的坑和电容选型
红外传感器的LED需要瞬间大电流,GP2Y1010AU0F的数据手册里写的是LED峰值电流典型值20mA,但脉冲驱动时瞬间电流会更大。如果你用AMS1117-3.3给传感器供电,大概率会出现电压跌落,导致ADC读数异常波动。我的做法是:
- 传感器用5V单独供电,不和STM32的3.3V共用一路LDO。
- 如果只能用3.3V供电,必须在传感器电源引脚旁边加一个100uF的电解电容和一个0.1uF的陶瓷电容并联。
- 模拟输出Vo引脚对地加一个0.1uF电容做低通滤波,滤掉高频噪声。
关于电容选型,网上有讨论过“AMS1117把钽电容换成陶瓷电容对STM32有影响吗”这类问题。这里我说一下自己的经验:AMS1117这类LDO对输出电容的ESR有要求,换陶瓷电容后稳定性通常会更好,但前提是容值不能太小。AMS1117的数据手册推荐输出电容至少22uF,我用的是22uF陶瓷电容并联0.1uF高频去耦,实测非常稳,没出现振荡问题。
3.3 STM32引脚分配参考
我用的主控是STM32F103C8T6(蓝 pill最小系统板),引脚分配如下:
| 功能 | 引脚 | 说明 |
|---|---|---|
| ADC输入 | PA1 | ADC1_IN1,采样传感器Vo输出 |
| PWM输出 | PA8 | TIM1_CH1,10kHz,脉宽0.32ms,驱动传感器LED |
| OLED-SCL | PB6 | I2C1时钟 |
| OLED-SDA | PB7 | I2C1数据 |
| 蜂鸣器 | PB0 | 浓度超标报警 |
| USART1-TX | PA9 | 串口打印数据 |
PA8做PWM输出是因为TIM1是高级定时器,挂在APB2总线上,时钟频率72MHz,配置10kHz非常方便。ADC选择PA1是因为它和PA0相邻,后续如果想扩展双通道监测(比如同时接温湿度传感器),引脚布局上也方便。
4. 核心代码实现与参数计算
4.1 环境准备:Keil5工程搭建和芯片包安装
开发环境我用的是Keil MDK 5,搭配STM32标准外设库。如果你手头只有Keil5装不上芯片包,大概率是Pack Installer没配好。这里说两个常见问题:
- Keil5兼容C51和STM32安装:MDK和C51可以共存,安装时选不同安装目录,C51版装到Keil_v5的C51子目录,MDK版装到ARM子目录,两个共存互不影响。
- 芯片包安装:在Keil官网下载STM32F1xx_DFP包,双击安装,安装路径要指向Keil_v5的ARM/PACK目录。装完后在工程配置的Device选项卡里就能看到STM32F103C8。
4.2 定时器PWM配置:10kHz脉冲驱动传感器
GP2Y1010AU0F的LED驱动脉冲要求是:频率约10kHz,脉宽0.32ms。这意味着PWM周期是100us(1/10kHz),高电平时间320ns占周期比例是0.32%。这个占空比非常小,所以在计算定时器重载值和比较值时要用64位精度去算,避免出现整数截断误差。
以STM32F103C8T6的TIM1为例,时钟源是72MHz(APB2=72MHz,TIM1倍频后也是72MHz):
- 周期100us对应的计数器值:72MHz × 100us = 7200。所以设置TIM_Prescaler = 0,TIM_Period = 7199。
- 高电平320ns对应的计数器值:72MHz × 0.32us = 23.04 ≈ 23。所以设置TIM_Pulse = 23。
这么算下来的结果是PWM频率 = 72MHz / 7200 = 10kHz,高电平时间 = 23 / 72MHz = 319.4ns,非常接近数据手册要求的320ns。
下面是定时器PWM初始化代码,用的标准外设库:
void TIM1_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 7199; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 23; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }注意最后那行TIM_CtrlPWMOutputs(TIM1, ENABLE),这是高级定时器TIM1/TIM8特有的,必须调用,否则PWM波形不会输出。低级定时器如TIM2/TIM3不需要这行。
4.3 ADC采样配置:连续采样加均值滤波
ADC读取传感器Vo输出的思路是:开启ADC1的PA1通道,使用软件触发连续转换模式,每次转换完成后触发DMA搬运结果,这样CPU不用一直等着ADC转换,省下来的时间可以处理显示和逻辑。
ADC初始化代码如下:
void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 16; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }这里我把DMA缓冲区设成了16个半字,也就是说DMA会自动连续采样16次,我只需要在main循环里对这16个值做平均,就能得到一个比较稳定的ADC结果。ADC采样时间用239.5周期,这是为了降低信号源阻抗带来的采样误差,毕竟传感器Vo输出端的等效阻抗不算低。
4.4 浓度换算公式:从电压到ug/m³
浓度换算是这个项目最容易出错的地方。GP2Y1010AU0F的数据手册给出的典型输出电压-浓度关系近似是线性的,但不同批次的传感器个体差异比较大,手册给的只是典型曲线。
我的做法分两步:
第一步是标定。在传感器上电且空气流通稳定后,记录一个“零点电压”(纯净空气下的Vo电压),这个值通常在0.5V~1.0V左右。然后在已知浓度环境下(比如雾霾天或密闭房间点一根烟制造高浓度)再记录一个高浓度点的电压。两点连线,得出这个传感器的实际电压-浓度斜率。
第二步是换算。STM32读到的ADC值是12位(0~4095),参考电压3.3V,所以:
- 电压 = ADC值 × 3.3 / 4095
- 浓度 = (电压 - 零点电压) × 斜率系数
我用的经验公式是:
float adc_to_pm25(uint16_t adc_value) { float voltage = (float)adc_value * 3.3f / 4095.0f; float concentration = (voltage - 0.6f) * 100.0f; if (concentration < 0) concentration = 0; return concentration; }这里的0.6f是零点电压(我实测这块传感器的零点在0.55~0.65V之间),100.0f是换算斜率系数。这个斜率系数不是固定的,需要根据你自己的传感器实际标定结果来调整。如果你的传感器在空气里读到5V电压,说明零点标定错了,或者数据完全没经过滤波。
4.5 数据滤波:滑动平均和中值滤波
ADC原始数据跳变非常剧烈,尤其是浓度低的时候,信号本身信噪比差,读数可能一会儿5ug/m³一会儿30ug/m³。我的处理策略是两级滤波:
- DMA采样16次取平均,消除高频噪声。
- 每200ms做一次采样,采10次,去掉最大值和最小值,剩下的8次取平均。
代码实现:
#define FILTER_N 10 float pm25_get_average(void) { float values[FILTER_N]; float sum = 0, max = 0, min = 10000; for (int i = 0; i < FILTER_N; i++) { values[i] = adc_to_pm25(get_adc_average()); HAL_Delay(20); } for (int i = 0; i < FILTER_N; i++) { if (values[i] > max) max = values[i]; if (values[i] < min) min = values[i]; sum += values[i]; } return (sum - max - min) / (FILTER_N - 2); }每次采样间隔20ms,10次下来总共200ms左右,实时性上完全够用。如果做的是需要快速响应的报警系统,可以把间隔缩短到10ms,但要注意蜂鸣器或继电器的动作需要有阈值迟滞,避免浓度在阈值附近来回跳导致继电器频繁通断。
5. 常见问题排查与避坑指南
5.1 传感器数据虚高或者一直很大
这个现象我在调试中遇到过两次,原因完全不同:
第一次是LED驱动脉冲没有配好。PWM频率不对或者高电平时间不对,LED亮度不够,传感器增益异常,导致输出信号虚高。排查方法是直接用示波器看PA8引脚的波形,确认频率是10kHz、高电平约320ns。
第二次是传感器旁边有其他红外干扰源,比如手、衣服、墙壁反射红外光。红外散射传感器的探测区域不只是传感器正面的孔,旁边如果有强反射物,会把红外光反射回光电二极管,导致读数偏高。解决方法是传感器安装时周围要留出至少1cm的空隙,不要紧贴外壳。
5.2 串口打印数据显示NaN或者负数
这是典型的浮点数运算问题。出现负数说明电压值小于零点电压,此时浓度按0处理。NaN通常是因为除零或者未初始化变量。我在代码里做了保护:
if (concentration < 0) concentration = 0; if (isnan(concentration)) concentration = 0;还有一种情况是ADC值一直为4095,说明ADC引脚悬空或者配置错误。检查GPIO是否配置为模拟输入模式,另外DMA通道是否正确对应ADC1的通道1。
5.3 STM32无法识别USB设备
这个和本项目关系不大,但凡是做STM32项目基本都会遇到。USB识别不了一般是几种情况:板子上电后没有进入ISP模式(BOOT0拉高再复位)、USB线是纯充电线没有数据线、或者驱动没装。
如果你用的是STM32F103C8T6最小系统板,把BOOT0接到3.3V,然后按一下复位键,再插USB,通常就能识别到。下载完程序后记得把BOOT0跳回GND再复位一次,否则程序不运行。
5.4 传感器上电后发热严重
这是个大坑,很多人第一次用GP2Y1010AU0F都会遇到。发热的原因是LED驱动引脚被直接接高电平,LED持续导通。GP2Y1010AU0F的LED只能在脉冲驱动下工作,占空比约0.32%,如果持续导通,电流会远超额定值,发热是必然的,时间长了LED还会烧掉。
判断方法很简单:传感器正常工作时,用万用表测LED引脚,应该看到的是PWM波形而非持续高电平。如果没有示波器,可以摸一下传感器外壳,微热是正常的,烫手就是有问题。
5.5 数据波动大怎么办
虽然加了滑动平均和中值滤波,数据还是会有一定波动。这里有两个思路:
一是提高传感器供电质量。传感器的Vo输出直接反映了内部光电流,光电流本来就很小,很容易被电源噪声干扰。用示波器看5V电源轨上的纹波,如果超过50mV,建议在电源入口加一个100uF电解电容和0.1uF陶瓷电容。
二是适当延长滤波时间窗口。把中值滤波的采样次数从10提高到25,数据平滑度会明显改善。代价是响应时间变长,浓度突变后读数要1~2秒才能跟上。对于室内空气质量监测这个场景,响应慢两三秒完全无所谓,反而视觉上数据更稳定。
6. 项目扩展思路
6.1 和空气质量监测开源项目结合
网上有不少“基于STM32的空气质量检测开源项目”,核心功能就是PM2.5浓度采集加阈值报警。我这个项目配合OLED显示、按键阈值设置、蜂鸣器报警,就是一个完整的课设/毕设方案。如果你想做得更完整,可以加一个DHT11或SHT30温湿度传感器,凑齐“温度+湿度+PM2.5”三要素,数据展示更有说服力。
6.2 数据上报平台:串口转WiFi/4G
STM32把浓度数据通过串口发给ESP8266,ESP8266用MQTT协议上报到云平台(如巴法云、点灯科技、阿里云IoT),就能实现手机远程查看空气质量。需要注意串口波特率设置一致,两端都是115200或者9600均可。
6.3 联动控制:继电器+空气净化器
这是很自然的扩展方向:PM2.5浓度超过35ug/m³(国家标准的一级限值)时,继电器闭合,打开净化器或新风系统;低于20ug/m³时,继电器断开。控制策略上要加迟滞逻辑,避免频繁开关。
我的经验是:阈值上限设为40,下限设为25。浓度超过40启动,降到25以下才停止,中间这15ug/m³的迟滞带就是防止“临界点震荡”用的。
6.4 红外传感器和激光传感器混合使用
如果你后面预算充足,想换成激光传感器(比如攀藤PMS7003,UART输出,精度高很多),代码改动也不大。PMS7003的串口协议是标准的,读帧头0x42 0x4D,校验通过后解析出PM2.5浓度。
我当时在同一块板子上加了PMS7003的接口,红外传感器作为辅助,两个数据一对比,才发现红外传感器在低浓度时的偏差确实比激光传感器大不少,但高浓度下两者趋势是接近的。所以如果只是做“超标报警”这种定性判断,红外完全够用;要拿来做科研数据或者和国标站点的数据对比,那还是得上激光的。
7. 最后的实操总结
把这次做项目的经验浓缩成几条:
- 红外PM2.5传感器便宜好用,但精度有限,适合做趋势监测和阈值报警,不适合做精密测量。
- ADC方案最简单,PWM捕获方案更稳,双方案设计可以让你在不同场景下灵活切换。
- LED驱动脉冲一定要配准,10kHz、0.32ms高电平,少一个条件数据就全是错的。
- DMA加滑动平均加中值滤波,三级滤波下来数据已经非常能看了,不需要上卡尔曼滤波这种复杂算法。
- 供电是最大的坑,传感器独立供电,电源引脚加100uF和0.1uF电容,数据稳定性提升一个档次。
我个人在实际项目迭代中的体会是:这类传感器项目,硬件电路占六成功力,固件占四成。很多人一上来就急着写代码,结果数据不好看就开始怀疑代码写错了,其实多数时候问题在供电和信号完整性上。先把硬件调理干净,再用软件滤波做平滑,基本不会翻车。这个项目做完以后,你手里相当于有了一套完整的“STM32采集模拟信号”的模板——改一改ADC配置就能接其他模拟量传感器,改一改定时器配置就能接PWM输出型传感器,复用率非常高。