☰
STM32驱动红外PM2.5传感器GP2Y1010AU0F完整教程与代码实现
2026/9/26 13:35:23 网站建设 项目流程

红外PM2.5传感器这名字听起来不复杂,但真要把STM32和它接好、让数据不飘不跳,里面藏着不少门道。最近我把GP2Y1010AU0F这块经典的红外粉尘传感器接到了STM32F103C8T6上,解决了时序配合、电压匹配、ADC采样这些关键问题,最终能在串口和OLED上稳定看到浓度变化。这篇文章就把我验证过的硬件接法、驱动逻辑和完整代码思路全盘托出,适合刚入手STM32环境监测项目、想做空气净化器联动或者毕业设计的同学参考。红外传感器虽然测的是估算浓度,但胜在便宜、耐造、资料多,用来学习ADC模拟采样和定时器脉冲控制再合适不过。

1. 项目背景与方案选型

1.1 红外PM2.5传感器和激光传感器的差别

市面上PM2.5传感器大致分两类,一类是红外散射式,另一类是激光散射式。PMS5003这类激光传感器自带风扇和光路结构,通过检测不同角度的散射光来统计颗粒物粒径分布,输出的是PM1.0、PM2.5、PM10的数值,精度确实高,但价格基本在几十上百块,功耗也不低,风扇一旦损坏,数据就开始离谱。

红外传感器走的是另一条路,它用红外LED照射空气中的颗粒物,只要颗粒物把光散射到光敏接收管上,内部放大电路就会输出一个和粉尘浓度相关的模拟电压。这种方案测不到细颗粒物的粒径分布,只能给一个综合的“粉尘浓度”估算值,但它便宜,模块十几二十块钱就能拿到,功耗也低,寿命反而比带风扇的激光方案更耐用。对STM32入门项目来说,红外传感器能让你把ADC、定时器、串口这些基本功练扎实,性价比非常高。

我用的GP2Y1010AU0F是夏普早年推出的经典型号,到现在各种模块上还能大量见到。它输出的模拟电压和粉尘浓度近似线性,在空气净化器、新风系统、DIY环境监测站里都有广泛使用。需要注意的是,这类传感器给出的数据更适合当“相对浓度趋势”看,不要拿着它和国际标准的检测仪数值硬比,两者的采样原理和标定方式本来就不一样。

1.2 GP2Y1010AU0F的测量原理与输出特性

GP2Y1010AU0F内部集成了红外LED、光敏接收管和信号放大电路,引脚一共六个,分别是VCC、GND、LED控制输入、LED-GND、S-GND、Vo模拟输出。平时使用时的核心逻辑是:给LED控制脚一个周期性脉冲,点亮红外LED,红外光穿过检测腔,颗粒物产生的散射光被接收管捕获,经过放大后再从Vo引脚输出。

数据手册里给了一个非常重要的时序参数,LED控制脉冲的周期典型值是10ms,高电平宽度是0.32ms。也就是说LED不是常亮的,而是每10ms闪一次,一次闪320微秒。更关键的是Vo电压并不会在整个脉冲期间都保持峰值,它会在LED点亮后大约0.28ms处达到最大值,随后在LED熄灭时迅速回落。如果把ADC采样点放在脉冲上升沿瞬间或者LED熄灭之后,读到的电压会明显偏低,这也就是很多人接好线之后发现数据始终不涨的常见原因。

输出特性方面,这个传感器在无尘环境下有一个零点电压,不同批次和不同模块差异不小,有的在0.5V左右,有的到0.9V。灵敏度典型值约0.5V对应0.1mg/m³的粉尘浓度。换算下来每1V电压变化约等于0.2mg/m³浓度,也就是200μg/m³。虽然算不上精密测量,但做空气质量的趋势判断、联动控制完全够用。

1.3 元器件清单与模块版/裸传感器区分

我在这次项目里用的硬件清单如下,先列出来方便照单抓药:

器件型号/规格数量
STM32最小系统板STM32F103C8T6蓝色板1块
红外粉尘传感器夏普GP2Y1010AU0F模块1个
OLED显示屏0.96寸 SSD1306 I2C接口1个(可选)
USB转TTLCH3401个(下载/调试用)
面包板/杜邦线若干
电阻电容1kΩ、10nF、10μF若干

买传感器的时候要注意区分模块版和裸传感器。裸传感器只有六根引脚,LED驱动三极管、电阻、电容都需要自己搭,适合想深挖电路细节的人。常见的模块版把驱动电路和接口都集成好了,一般引出VCC、GND、LED、AOUT四个引脚,直接接单片机就能用,我这次就用的模块版。如果你手里是裸传感器,需要在LED引脚外接一个三极管做开关驱动,这部分后面会详细说时序和电平问题。

2. 硬件连接:看似简单,实则三个关键点

2.1 引脚定义与接线表

模块版的GP2Y1010AU0F接线表如下,我实测按照这个接法没有任何问题:

模块引脚功能说明接STM32
VCC5V供电接5V电源正极
GND电源地接GND
LEDLED控制脉冲输入PA0
AOUT模拟电压输出PA1(ADC通道1)

PA0设置为推挽输出,用来产生LED需要的10ms周期脉冲。PA1设置为模拟输入,接ADC1的通道1。这里最容易被忽略的是供电和共地问题。GP2Y1010AU0F的手册工作电压是5V,绝对不能直接接STM32板子上的3.3V,否则传感器无法正常工作。很多开发板的5V是从USB口直接来的,我建议传感器单独从5V供电引脚取电,STM32板子由USB供电,两者GND必须连在一起,否则采集到的模拟信号会在一个悬空地参考上跳来跳去。

另外AOUT的输出范围最高能到3.6V左右,而STM32F103的ADC输入引脚耐压基本是VDD+0.3V,也就是3.6V上下。如果长时间工作在满量程高浓度环境下,直接连接存在一定风险。稳妥做法是在AOUT和PA1之间串联一个1kΩ电阻,再在PA1对地并联一个10nF电容,利用ADC输入的高阻抗特性避免分压,同时给引脚加一层保护,还能滤掉一部分高频毛刺。这个电容不能太大,太大会拖慢ADC采样时的电压建立速度,后面采样周期要拉长一些。

2.2 LED驱动脉冲与ADC采样的时序配合

如果你的单片机是5V系统,直接把LED脚接MCU引脚就行。但STM32的GPIO是3.3V输出,模块上的三极管驱动电路能不能被3.3V高电平可靠打开,需要实测确认。我手上这个模块用3.3V高电平可以正常触发,但有些模块内部电路设计不同,LED驱动信号幅值不够会导致红外LED发光强度不足,最终Vo电压整体偏低。

如果发现读数明显偏小,可以在模块LED驱动脚和STM32之间加一个电平转换电路,最省事的做法是加一个NPN三极管,把3.3V的控制信号转成5V级别去驱动传感器内部LED。这个信号要求本身就是0.32ms的脉冲,三极管开关速度完全跟得上,不用担心失真。

接下来就是时序配合。数据手册建议的采样时刻是在LED脉冲开始后的0.28ms左右。我用的是GPIO直接控制方式,流程很简单:先把PA0拉高,延时280μs,接着启动ADC转换,转换完成后立刻把PA0拉低,这样一个周期结束。STM32F103的ADC转换时间不算长,采样周期设置为239.5个周期,加上12.5个固定转换周期,在12MHz的ADC时钟下大约21μs,刚好卡在320μs脉冲结束前完成。所以GPIO延时法虽然笨,但足够可靠。

2.3 电源与滤波设计

这个传感器虽然脉冲峰值电流有几十毫安,但由于占空比很低,平均功耗其实不高。不过电源纹波对ADC读数的影响非常大。我用USB供电时发现,如果传感器和单片机共用同一个细长的杜邦线供电,每次LED点亮的瞬间都会让VCC跌掉几十毫伏,ADC数值会跟着抖动。后来把5V和GND各用一根粗一点的杜邦线直接从电源端引到传感器,又在传感器的VCC和GND之间并了一个10μF电解电容和一个100nF陶瓷电容,波形干净了很多。

模拟输出端也要做好滤波。AOUT本身是从传感器内部放大器出来的一路模拟信号,直接接ADC引脚会带入射频干扰和电源开关噪声。我习惯在PA1对地放一个10nF电容,做一阶低通,截止频率大约几十千赫兹,不会影响有用的低频浓度变化,还能滤掉LED脉冲相邻边沿的耦合噪声。如果你用的是ADC的连续转换模式,最好避免在LED熄灭期间反复触发,否则每次读到的都是同一个下降沿后的残影,数据曲线非常难看。

3. 基于标准库的代码实现

3.1 工程模板与时钟配置

我这次用的开发环境是Keil MDK5,配合STM32标准外设库3.5版本。标准库和HAL库的区别经常有人问,简单说,标准库把寄存器操作封装成函数,代码直观、执行效率高,适合学习原理和做中小型项目;HAL库则加入了更多抽象层,方便在不同厂牌芯片之间移植,但代价是代码量大、调试时跳转层级深。对于这个项目,我坚持用标准库,因为你需要理解时钟配置、GPIO模式、ADC中断标志位这些底层细节,一旦出了问题也能自己定位。

新建工程时有一点要注意,STM32F103C8T6是64KB Flash的中等容量芯片,启动文件必须选startup_stm32f10x_md.s,不要选成大容量或者高密度的启动文件。时钟配置方面,标准库的SystemInit函数默认会把外部8MHz晶振通过PLL倍频到72MHz系统时钟。如果板子上没有8MHz晶振,而是用内部HSI启动的,那需要再单独配置时钟树,把PLL源切换到内部RC后再倍频。ADC挂在APB2总线上,APB2时钟是72MHz,但ADC时钟最大不能超过14MHz,所以要设置ADC预分频,把ADCCLK降到12MHz,也就是6分频。

3.2 串口初始化与延时函数

调试环境监测项目离不开串口打印。USART1的TX是PA9,RX是PA10,我配置为115200波特率,8位数据位,1位停止位,无校验。为了让printf能直接发送到串口,需要实现fputc重定向,同时Keil工程里勾选MicroLIB,否则会和标准库的底层冲突。

延时函数是这次项目的核心之一。SysTick延时在高频短延时场景下不够方便,我改用了DWT数据观察点计数器和Cortex-M3内核的周期计数器,精度很高。初始化和延时函数如下:

void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } void DWT_Delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = (uint32_t)((uint64_t)us * (SystemCoreClock / 1000000)); while ((DWT->CYCCNT - start) < ticks); }

这个延时函数用的是CPU周期计数,在72MHz主频下,1μs就是72个周期,如果你把主频改了,延时值也会跟着变,所以适应性比SysTick抢中断的方式更好,不会有Delay卡死的问题。

3.3 传感器读取:脉冲控制加ADC采样

GPIO和ADC初始化这里直接给代码。PA0配置成推挽输出,PA1配置成模拟输入。ADC选择独立模式、单通道、软件触发、右对齐,采样时间设为239.5周期,尽量把采样电容充得更充分。

void Sensor_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); } void ADC1_Init(void) { ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; 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_Cmd(ADC1, ENABLE); }

单次读取传感器的函数是整篇代码的关键。它负责在正确的时序窗口里点亮LED、启动ADC、等待转换、关断LED:

uint16_t Read_Dust_Value(void) { uint16_t adc_value; GPIO_SetBits(GPIOA, GPIO_Pin_0); DWT_Delay_us(280); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); adc_value = ADC_GetConversionValue(ADC1); GPIO_ResetBits(GPIOA, GPIO_Pin_0); return adc_value; }

280μs延时加约21μs的ADC转换时间,总时长301μs,还没有超过320μs的脉冲宽度要求。你也许想加一个40μs的延时把脉冲拉满到320μs,但那样脉冲关闭前的时间非常局促,我实测下来多留一点余量反而更稳,数据一致性没问题。

3.4 浓度换算公式与校准系数

ADC读出来的是0到4095的原始数值,第一步还原成电压。标准库下使用3.3V参考电压,所以换算公式是:

float voltage = (float)adc_value * 3.3f / 4096.0f;

电压到浓度的换算需要知道零点电压V0。我手上这块模块在室内干净空气下实测零点电压约0.62V,不同模块可能漂移到0.5V到0.9V之间,这个数值一定要在洁净环境下实测,不要照抄别人的。浓度计算公式如下:

float pm25 = (voltage - 0.62f) * 200.0f; if (pm25 < 0.0f) pm25 = 0.0f;

为什么是乘以200?因为传感器灵敏度是0.5V对应0.1mg/m³,也就是说1V电压变化等于0.2mg/m³,即200μg/m³。所以电压每升高0.1V,浓度大约增加20μg/m³。这个线性关系成立的前提是在传感器的测量范围内,红外传感器在低浓度段的输出并不是完全直线,但作为环境监测趋势已经足够。

如果想让数据更平滑,可以在主循环里连续采样20次,丢弃最大最小值后取平均。20次采样之间至少间隔10ms,否则会在同一脉冲周期内重复读到同一个峰值。最简单的做法是每次调用Read_Dust_Value之后延时10ms再调用下一次。

3.5 主循环与实测输出

主循环的逻辑可以这样写,串口打印浓度值,并控制刷新频率:

int main(void) { SystemInit(); DWT_Init(); Sensor_GPIO_Init(); ADC1_Init(); USART1_Init(115200); printf("STM32 GP2Y1010AU0F Dust Sensor\r\n"); while (1) { uint16_t adc = 0; float voltage = 0.0f; float pm25 = 0.0f; uint8_t i = 0; for (i = 0; i < 20; i++) { adc += Read_Dust_Value(); DWT_Delay_us(10000); } adc /= 20; voltage = (float)adc * 3.3f / 4096.0f; pm25 = (voltage - 0.62f) * 200.0f; if (pm25 < 0.0f) pm25 = 0.0f; printf("ADC:%4d V:%.3f PM2.5:%.1f ug/m3\r\n", adc, voltage, pm25); DWT_Delay_us(200000); } }

每隔200ms刷新一次数据,大约5Hz的刷新率,肉眼能感觉到变化,又不至于让串口刷屏。如果你想用OLED显示,也可以用同样的数据直接驱动SSD1306,只需要把printf改成LCD绘制函数即可。我实际运行时的输出大致是这样的:

ADC: 756 V:0.609 PM2.5:0.0 ug/m3 ADC: 812 V:0.654 PM2.5:6.8 ug/m3 ADC: 987 V:0.795 PM2.5:35.0 ug/m3 ADC: 1103 V:0.888 PM2.5:53.6 ug/m3

近距离靠近点燃的蚊香烟雾时,浓度数值可以很快升到几百甚至上千μg/m³,说明传感器响应速度确实可以。

4. 实测数据与稳定化处理

4.1 干净环境和烟尘环境下的对比

我在三种场景下做了简单记录。第一是室内正常环境,门窗关好,读数基本在0到40μg/m³之间波动;第二是开窗透气之后,数值会缓慢下降一些;第三是拿一只点燃的蚊香放在传感器进风口旁边约20厘米处,浓度立刻冲到300μg/m³以上,移开后又会在几秒内回落。这个反应速度和变化方向说明整个采样链路是通的。

需要特别强调的是,红外传感器测的是空气流过检测腔时颗粒物散射光的强度,它不区分颗粒物是PM2.5还是PM10,也不区分是灰尘、烟雾还是水汽。所以潮湿天气或者厨房油烟环境都会让读数偏高,这是原理特性,不是故障。理解了这一点,你就不会因为数据在雨天涨起来而怀疑传感器坏了。

4.2 数据波动大的原因分析

刚接好线的时候我发现串口打印的数据跳得厉害,相邻两次能差到几十μg/m³。排查下来主要有三个因素。第一个因素是供电,前面说过,传感器LED瞬间电流会让VCC产生跌落,而ADC的参考电压取自VDD,参考电压波动直接等于测量误差。第二个因素是采样时刻偏差,DWT延时函数没问题,但如果你改成SysTick延时,又正好被其他中断打断,280μs就可能偏到200μs甚至更早,读数直接落到脉冲的上升沿区域。第三个因素是传感器检测腔里的颗粒物流动本身就有随机性,颗粒物分布不均匀,瞬时读数跳动属于正常物理现象。

针对前两个因素,我把传感器供电和单片机模拟电源隔开,加电容滤波,并保持ADC采样点在280μs附近。针对第三个因素,采用多次采样平均,不是简单算术平均,而是先去掉一组数据里的最大值和最小值再平均,这样可以防止偶然出现的尖峰污染结果。

4.3 移动平均与滤波的取舍

滤波算法有很多,红外传感器这个场景最适合的是滑动窗口中值平均,既能平滑随机波动,又不会像卡尔曼滤波那样带来明显的滞后。我的做法是每200ms读出20个原始采样值,去掉最大最小,把剩下18个平均,作为一次输出。这个输出再进入一个长度为5的滑动窗口,取平均后作为最终浓度值。这样做之后,数据曲线已经非常顺滑,同时仍然能看出烟雾浓度上升和下降的趋势。

有些人喜欢把所有原始值直接做卡尔曼滤波,但我个人不建议在这个低精度传感器上过度优化,红外传感器本身精度有限,你用再高级的滤波算法,也没办法把它变成一个激光粒度仪。滤波的意义是让控制逻辑不至于被瞬时尖峰误触发,而不是把测量精度提高到不可能的水平。

5. 常见问题排查速查表

5.1 典型症状与排查对策

我把这次调试中遇到的和周围朋友问过的问题整理成了一个速查表,方便你接完线之后按图索骥:

症状可能原因排查与解决方法
读数一直接近4095PA1没有配成模拟输入,或者ADC通道选错检查ADC_RegularChannelConfig里的通道号为1;测量PA1对地电压,如果悬空就会读满量程
读数始终接近0传感器没供电、没共地、LED脉冲根本没拉高用万用表确认模块VCC为5V,GND与单片机连通;在Read_Dust_Value里加GPIO翻转测试PA0
读数跳动剧烈电源纹波大、采样点太早或太晚传感器单独走线,加10μF加100nF去耦;检查DWT延时是否被中断干扰
数据整体偏低LED驱动高电平幅度不够、传感器光窗脏污用示波器看LED引脚波形,必要时加三极管转5V驱动;清洁传感器进风口
数据整体偏高零点漂移、检测腔积灰、水汽干扰在洁净空气下标定新的V0;远离加湿器、油烟和直吹气流
更换传感器后数值和之前差很多不同批次零点电压不同每次换传感器都要重新校准V0,不要沿用旧系数

如果发现串口输出完全乱码,大概率是波特率没匹配或者TXD和RXD接反了。如果程序卡死,优先检查DWT初始化是否执行,运行到DWT_Delay_us时CPU是否进入异常状态,这个延时函数在SystemInit完成后立刻调用就行,不需要额外开中断。

5.2 扩展玩法:OLED显示与联动控制

数据能稳定上报之后,这个项目可以往三个方向扩展。第一个方向是加显示,0.96寸I2C OLED屏很便宜,把来自传感器的浓度数据画成柱状图或者趋势曲线,就是一个桌面空气监测站。第二个方向是加网络功能,用STM32的USART1把浓度发给ESP8266或者ESP32,再由WiFi模块上报到HomeAssistant或者物联网平台,这样就能做到远程查看室内空气状况。第三个方向是加联动控制,当浓度超过设定阈值时,STM32通过继电器控制空气净化器或者排风扇,阈值建议设置成可配置的,不要写死在代码里,比如用按键调整或者用串口命令修改。

还有一种玩法是同时接多个传感器,把几个检测点的数据做平均或者取最大值,布置在房间不同位置,能更真实地反映室内空气分布。需要注意传感器进风口不要对着空调出风口,也不要紧贴墙壁,留出5厘米以上的空间,否则气流被挡住会让数据失真。

写在最后

我在实际调试中最深的感受是,红外PM2.5传感器看起来只有一根模拟输出线,但真正决定成败的是时序和供电这两个不起眼的细节。280μs的采样窗口看起来很短,但只要你理解了脉冲周期、采样时刻和ADC转换时间这三者之间的关系,哪怕不用定时器PWM,只用GPIO延时也能做出可靠的数据。做这类传感器项目,一定要养成留一手测量的习惯,串口打印只是辅助,关键时刻用万用表和示波器去看信号,比盲目改代码高效得多。这个项目做完之后,你对STM32的GPIO、ADC、串口、时钟配置都会形成完整认知,后续再去接其他模拟量传感器,基本可以举一反三。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询