STM32接入红外PM2.5传感器GP2Y1014AU0F的完整教程
2026/9/23 20:37:27 网站建设 项目流程

最近做环境监测的项目,在STM32上又接了一次红外PM2.5传感器,型号是用得最多的GP2Y1014AU0F。这类传感器比激光PM2.5模块便宜一大截,十几元一片,对精度要求不高的场景完全够用。这篇文章把整个环节照着可复现的标准写一遍:为什么选红外、怎么接线、STM32CubeMX怎么配置、ADC怎么读、电压怎么换算成浓度、最后怎么标定和排坑。做课程设计、智能家居或者空气净化器改造的,可以直接抄作业。

1. 红外PM2.5传感器选型与基本原理

1.1 红外式和激光式,为什么我先选了红外

红外PM2.5传感器的原理说起来很直白:传感器内部有一个红外LED,LED持续或脉冲点亮后,光线会穿过一个采样腔,空气里的颗粒物把光散射到旁边的光电二极管上,光电二极管把散射光转成电流,再经过传感器内部的运算放大器输出一个模拟电压。颗粒物越多,散射光越强,输出电压就越高。

但这里有个很多人容易误解的点:红外方案并不是真正只测2.5微米以下的颗粒。它是把所有能引起散射的颗粒物混在一起总量估算,再用经验曲线去近似PM2.5浓度。激光方案因为波长单一、散射角可控,可以更准确地推算粒径分布,所以精度更高、价格也更贵。做项目的时候,如果只是判断“屋里空气质量变好了还是变差了”或者做净化器联动,红外足够;如果要做计量级监测,那还是老老实实上激光传感器,比如PMS5003系列。

1.2 GP2Y1014AU0F 的输出特性

GP2Y1014AU0F是夏普很经典的红外粉尘传感器,GP2Y1010AU0F是它的老款,两者接法和用法基本一样。传感器一共6个引脚:V-LED、LED-GND、LED、S-GND、Vo、Vcc。V-LED和Vcc都要接电源,LED是控制红外LED点亮的引脚,Vo是模拟电压输出,S-GND是信号地。

裸传感器的输出电压特性和温度、湿度、光源、老化程度都有关,数据手册上给的大致参考是:无尘环境下输出电压约0.9V,灵敏度约0.5V每0.1mg/m3,换算下来就是5V每mg/m3。换句话说,浓度每增加100ug/m3,输出电压大约上升500mV。空气质量到500ug/m3已经是重度污染,所以红外传感器在0到500ug/m3这个区间内还算能打,再高就会进入饱和区,线性度变差。这个特性决定了后面换算浓度时不能只看一个固定公式,至少要修一次零漂。

2. STM32与传感器的硬件连接

2.1 引脚接线表,新手照着插

我用的主控是STM32F103C8T6最小系统板,ADC参考电压就是3.3V。传感器供电我用5V,因为数据手册的特性曲线基本都按5V供电给出来的,3.3V供电也能工作,但输出范围会变,曲线也不是同一组。接线方案放在下面。

传感器引脚接到哪里说明
V-LED5V红外LED电源,走线要尽量粗短
LED-GNDGNDLED地
LEDSTM32 PA1 或直接接GND我这边模块是低电平点亮
S-GNDGND信号地,和LED-GND共地
Vo分压后到STM32 PA0模拟电压输出
Vcc5V内部放大电路电源

这里最容易踩的坑是Vo直接进STM32的ADC口。传感器5V供电时,Vo最大能到3.6V以上,而STM32的ADC输入范围是0到3.3V,直接接进去轻则读数一直满量程,重则长期使用有损伤风险。所以我在Vo和PA0之间加了一组分压电阻:传感器Vo先串一个4.7k电阻,再到PA0节点,PA0节点再接一个22k电阻到GND。分压比大约是22/(4.7+22)=0.82,最大电压3.6V进来后PA0上大约是2.96V,安全又能测满量程。

2.2 那两个关键电容和电阻分压不能省

如果你看过这类传感器的数据手册,会发现V-LED和LED-GND之间要接一个220uF电容。很多人觉得电容只是滤波,省略掉也能出数,实测就会遇到数据乱跳的问题。红外LED点亮瞬间电流很大,可能到几百毫安,如果电源线细、稳压芯片反应慢,Vo会被电源波动拉得抖来抖去。220uF电容相当于给LED脉冲电流一个就近的储能池,能大幅减小供电跌落。另外,Vcc和GND之间加一个0.1uF陶瓷电容,用来滤高频噪声。

分压电阻也不能随便选。电阻分压电路的等效内阻大概是两个电阻并联,4.7k和22k并联后约3.9k。这个阻抗对ADC来说不低,如果采样时间很短,ADC内部的采样电容来不及充满到稳定值,读数会偏低。解决办法后面会讲,就是把ADC采样时间调到最长。实际项目里如果不想用分压,可以改用轨到轨运放做电平搬移,或者用外部ADC芯片,但那样成本上去了,没必要。

3. 用STM32CubeMX配置ADC和串口

3.1 ADC采样时间为什么拉满

STM32CubeMX里操作很简单,把PA0的模式设为ADC1_IN0,也就是模拟输入。时钟照常用72MHz或者64MHz都行,这个传感器本来就不需要高采样率。关键是ADC参数里的Sampling Time,我直接选最长那一档,F103上通常是239.5 Cycles,部分芯片有640.5 Cycles,能选更长就选更长。

原因就是上面说的分压电阻输出阻抗。ADC采样过程可以理解成一个电容充电过程,输入阻抗越高,需要的充电时间越长。如果把采样时间设成1.5 Cycles这种极短档,采样电容还没充满就结束了,读出来的电压会明显偏低,而且不同批次芯片之间偏差还不一样。索性把采样时间拉满,用一点点时间换稳定性和一致性。一个PM2.5传感器本来就不需要每秒采几千次,几十毫秒采一次都嫌多。

3.2 串口输出和调试信息怎么加

串口是这个项目里最实用的调试输出通道。CubeMX里打开USART1,模式选Asynchronous,波特率设115200,8位数据位,1位停止位。生成工程后,可以用重定向fputc的方式让printf走串口,也可以用HAL_UART_Transmit自己封装一个输出函数。

我个人的习惯是优先用HAL_UART_Transmit,把要发的数据拼成字符串再发,这样不会依赖编译器对printf浮点支持的那套东西。Keil里默认的MicroLIB对printf支持还行,但GCC环境下偶尔会踩坑。输出内容最好把ADC原始值、分压后的真实电压、最终浓度一起打出来,接线和换算哪个环节有问题,一眼就能看出来。

4. ADC读取、滤波和浓度换算代码

4.1 轮询读取ADC的最小代码

整个读取逻辑最简单的状态就是轮询。我写了一个读取ADC原始值的函数,HAL库模式下十六进制数可以直接拿。

uint16_t read_adc_raw(ADC_HandleTypeDef *hadc) { HAL_ADC_Start(hadc); if (HAL_ADC_PollForConversion(hadc, 100) != HAL_OK) { return 0; } return (uint16_t)HAL_ADC_GetValue(hadc); }

HAL_ADC_PollForConversion的第二个参数是超时时间,单位毫秒。单次ADC转换实际耗时只有几十微秒,100ms的超时已经非常宽松了。如果程序一直卡在这里不返回,优先检查CubeMX里有没有把ADC1初始化、PA0是不是模拟输入模式,以及有没有把ADC时钟使能。

4.2 数据滤波:平均值、中位值、滑动滤波

红外传感器的信号本身就带抖动,单次采样直接拿来用,显示出来的数字会有很大的视觉噪音。我的做法是先采5次,每次间隔20ms,然后取中位值。中位值的好处是能干掉偶尔出现的脉冲式尖峰,比如某个瞬间电源波动或颗粒物正好飘过产生的异常值。

#define SAMPLE_NUM 5 uint16_t samples[SAMPLE_NUM]; uint16_t median5(void) { uint16_t tmp[SAMPLE_NUM]; uint16_t key; int i, j; for (i = 0; i < SAMPLE_NUM; i++) { tmp[i] = samples[i]; } for (i = 1; i < SAMPLE_NUM; i++) { key = tmp[i]; j = i - 1; while (j >= 0 && tmp[j] > key) { tmp[j + 1] = tmp[j]; j--; } tmp[j + 1] = key; } return tmp[SAMPLE_NUM / 2]; }

对5个元素做插入排序完全够用,不需要什么高级排序算法。拿中位值之后,再做一个滑动滤波,让数据更平滑。

float smooth_value = 0.0f; uint16_t med_value; float filtered_value; med_value = median5(); filtered_value = (float)med_value * 0.3f + smooth_value * 0.7f; smooth_value = filtered_value;

这个式子的含义是让新数据占30%,历史数据占70%,变化快慢可以自己调。红外传感器的响应速度本来就是秒级的,滤波激进一点不影响实际使用。

4.3 从电压到PM2.5浓度的换算

先把ADC值换算成PA0上的电压,再乘回分压比例,得到传感器Vo的真实电压。

#define ADC_REF_MV 3300.0f #define ADC_FULL_SCALE 4095.0f #define DIVIDER_RECOVER 1.2136f #define NO_DUST_MV 900.0f #define SENS_MV_PER_UG 5.0f float get_pm25_ugm3(void) { float adc_mv = filtered_value * ADC_REF_MV / ADC_FULL_SCALE; float real_vout_mv = adc_mv * DIVIDER_RECOVER; float density_ugm3 = (real_vout_mv - NO_DUST_MV) / SENS_MV_PER_UG; if (density_ugm3 < 0.0f) { density_ugm3 = 0.0f; } return density_ugm3; }

几个常量的来路说一下。ADC_REF_MV是开发板3.3V参考电压,如果你的板子实际供电不是3.3V,要用万用表实测后改掉。DIVIDER_RECOVER是22k和4.7k分压的还原系数,也就是(4.7+22)/22。NO_DUST_MV是零飘电压,传感器标称典型0.9V,实际每一片都不一样,后面标定环节会讲。SENS_MV_PER_UG的意思是从0.5V每0.1mg/m3换算过来的结果:0.1mg/m3就是100ug/m3,对应500mV,所以每1ug/m3对应5mV。

5. 标定与准确性验证

5.1 零点偏移怎么标定

红外传感器最需要标定的就是零漂。你手里那片传感器,无尘环境下的输出电压很可能不是0.9V,而是0.85V或者1.1V,不同批次差异不小。如果不标定,直接用900mV当零点,算出来的浓度整体偏高或偏低,空气净化器联动可能还算正常,做数据展示就非常难看。

标零点的操作很简单:找干净的HEPA滤网,或者几层一次性口罩,把传感器进风口包起来,让它抽过滤后的空气。上电先跑10分钟,让红外LED充分预热,再连续采一两百个数据取平均值,把这个平均值替换程序里的NO_DUST_MV。这里特别提醒,开机就把数据标定零点是不行的,传感器内部发热后电压会漂移,必须等热平衡了再标。

5.2 红外传感器算出来的数能不能当真

不少人是拿红外传感器的数值和几百块钱的激光PM2.5检测仪比,一比就觉得“我是不是做错了”。红外方案本身就不是计量级方案,它的输出受颗粒物粒径、颜色、水汽、传感器老化程度影响很大。打个比方,它更像是电子烟检漏那类“半定量”工具,适合判断趋势和相对变化,不适合做官方机构那种空气监测数据。

所以做毕业设计或者技术博客时,表述上也要严谨一点。别写“精确测量PM2.5浓度”,建议写“基于红外光散射法的颗粒物浓度估算装置”。你把它当成一个环境变化趋势传感器去用,很多数据波动根本不用纠结。如果有条件,可以拿一个激光传感器并排放十几分钟,根据两者数据做个线性修正,把斜率和零漂都改成自己实测的,这样可靠性会高一些。

6. 常见问题排查与避坑

6.1 数值满量程或一直为零时先查什么

调试过程中最容易遇到的就是ADC读出来一直是4095或者一直是0。这类问题大多数不是代码问题,而是硬件接线和CubeMX配置问题,我整理了个排查表。

现象大概率原因对策
一直读到4095PA0接到超过3.3V的电压,共地没做好用万用表量PA0电压,确认分压后不超过3.3V
一直读到0PA0没配置成ADC输入,接线断开CubeMX里确认PA0模式是ADC1_IN0,检查传感器供电
读数有但很固定传感器Vo线虚接,信号被分压拉到GND重新插线,焊点上锡
上电一瞬间有值,后面稳定但偏低ADC采样时间太短,采样电容没充满把Sampling Time调到最长档

排查时不要只盯着代码。先量传感器各引脚电压,Vcc和V-LED是不是5V,Vo是不是在0.9V到3.6V之间变化。如果Vo没变化,问题在传感器侧;如果Vo正常但PA0不对,问题在分压电路和ADC配置上。

6.2 数据跳动大的真凶是供电和采样时序

数据乱跳最根本的原因就是供电。红外LED点亮瞬间电流很大,如果传感器和STM32共用一根细长的面包板电源线,LED一开,整条线上的电压都会往下掉,Vo自然跟着抖。解决办法是让传感器从5V入口直接取电,地线用尽量粗短的线接到STM32系统地的同一个点,中间不要串联其他外设。

第二个容易忽略的是采样时刻。如果传感器要由LED控制脚触发点亮,那GPIO电平切换后不能马上读ADC,要延时等到散射光稳定。很多裸传感器数据手册都给出LED点亮后280us左右开始采样、持续40us左右的时序。顺序反了或者间隔不对,读到的就是LED点亮前的漏电流,数据自然乱七八糟。LED控制脚也不能悬空,悬空等于电平不定,传感器内部电路可能处于半开半关状态,输出毫无意义。

6.3 HAL库还是标准库,怎么选

做这类项目的过程中,很快会碰到HAL库和标准库的选择问题。ST官方现在主推STM32CubeMX加HAL库,工程由工具生成,外设初始化基本不用手写,换芯片型号时迁移也方便。我的建议是如果你是新手或者做毕业设计,直接学HAL,别再去折腾标准库手工建工程了。

标准库的优势是代码结构相对直观,网上老教程多,很多寄存器范例也是标准库风格。但标准库面临前面的问题:新出的STM32G0系列等型号,标准库支持并不好,遇上问题还得自己翻芯片寄存器。你如果只是想尽快把传感器数据跑起来,用CubeMX生成HAL工程,按这篇的代码去改,上手会快很多。

7. 进阶玩法:定时器控制LED脉冲采样

7.1 用DWT实现微秒延时的轮询采样

前面说LED控制脚低电平点亮,但如果你只把LED控制脚一直拉低,LED就是常亮状态。常亮会让红外LED持续发热,带来漂移和寿命损耗。规范的做法是按数据手册的时序,用脉冲点亮:LED点亮后延时280us左右读ADC,再延时40us左右熄灭,等将近10ms进入下一周期。

HAL库自带的HAL_Delay最小精度是1ms,根本没法满足280us这种微秒级延时。可以用DWT内核计数器做一个简单的微秒延时函数,在F103这些Cortex-M3芯片上很可靠。

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

采样循环大致长这样。

LED_PIN_ON(); delay_us_dwt(280); med_value = read_adc_raw(&hadc1); delay_us_dwt(40); LED_PIN_OFF(); HAL_Delay(9);

注意LED控制引脚的低电平逻辑,要对照你手上的模块说明书。如果模块是高电平点亮,就把ON和OFF反过来。

7.2 接OLED、串口屏或LoRa做环境节点

数据跑通之后,输出端可以玩出很多花样。我在本地接了一块0.96寸的OLED屏显示浓度和对应的空气状态,同时把串口数据发给LoRa模块,做成多节点环境监测。每一路节点就是一块STM32加红外传感器,成本能被压得非常低。

最后还有一个小细节:传感器放进外壳时,进风口和出风口一定要开在风道两端,让空气能持续流过采样腔。很多人调试时裸板放在桌面上数据挺好,装进盒子后数据就一直偏高,就是因为风道被堵住了。红外散射需要空气流动,不是放在一个密封壳子里就能测准的。这个坑我踩过两次,写在这个位置你可以直接避开。

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

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

立即咨询